From d939eef1f5ddfa2a122024b6d7371b4a67a6564f Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Thu, 1 Oct 2026 06:52:59 +0800 Subject: [PATCH v2 2/2] pg_walinspect: add function to locate WAL by time Add pg_get_wal_location_at_time(), which uses timestamped WAL records to locate a useful WAL range around a requested wall-clock time. Use an approximate binary search over retained segments and allow callers to widen the requested interval when timestamp reordering makes the initial result insufficient. Move WAL record timestamp extraction from recovery into xlogreader so pg_walinspect can recognize the same timestamp-bearing record types without duplicating that logic. Author: Chao Li Reviewed-by: Surya Poondla Discussion: https://postgr.es/m/80E9F0AD-CFC5-4BE5-81DE-D8FE35E10A1C@gmail.com --- .../pg_walinspect/expected/oldextversions.out | 9 +- .../pg_walinspect/expected/pg_walinspect.out | 161 +++++++ contrib/pg_walinspect/meson.build | 1 + .../pg_walinspect/pg_walinspect--1.1--1.2.sql | 21 + contrib/pg_walinspect/pg_walinspect.c | 422 ++++++++++++++++++ contrib/pg_walinspect/sql/pg_walinspect.sql | 118 +++++ contrib/pg_walinspect/t/002_time_search.pl | 51 +++ doc/src/sgml/pgwalinspect.sgml | 107 +++++ src/backend/access/transam/xlogreader.c | 36 ++ src/backend/access/transam/xlogrecovery.c | 48 +- src/include/access/xlogreader.h | 3 + src/tools/pgindent/typedefs.list | 1 + 12 files changed, 932 insertions(+), 46 deletions(-) create mode 100644 contrib/pg_walinspect/t/002_time_search.pl diff --git a/contrib/pg_walinspect/expected/oldextversions.out b/contrib/pg_walinspect/expected/oldextversions.out index 311017d66ca..6228ad9ef39 100644 --- a/contrib/pg_walinspect/expected/oldextversions.out +++ b/contrib/pg_walinspect/expected/oldextversions.out @@ -51,15 +51,16 @@ ALTER EXTENSION pg_walinspect UPDATE TO '1.1'; ALTER EXTENSION pg_walinspect UPDATE TO '1.2'; -- List what the current version contains. \dx+ pg_walinspect - Objects in extension "pg_walinspect" - Object description -------------------------------------------------------- + Objects in extension "pg_walinspect" + Object description +---------------------------------------------------------------------------------- function pg_get_wal_block_info(pg_lsn,pg_lsn,boolean) function pg_get_wal_files(pg_lsn,pg_lsn) + function pg_get_wal_location_at_time(timestamp with time zone,interval,interval) function pg_get_wal_record_info(pg_lsn) function pg_get_wal_records_info(pg_lsn,pg_lsn) function pg_get_wal_stats(pg_lsn,pg_lsn,boolean) -(5 rows) +(6 rows) SELECT pg_drop_replication_slot('regress_pg_walinspect_slot'); pg_drop_replication_slot diff --git a/contrib/pg_walinspect/expected/pg_walinspect.out b/contrib/pg_walinspect/expected/pg_walinspect.out index 34864c65ec7..5933c80071d 100644 --- a/contrib/pg_walinspect/expected/pg_walinspect.out +++ b/contrib/pg_walinspect/expected/pg_walinspect.out @@ -139,6 +139,142 @@ FROM pg_get_wal_files(pg_current_wal_lsn(), 'FFFFFFFF/FFFFFFFF'); SELECT * FROM pg_get_wal_files('0/0', '0/1'); ERROR: could not read WAL at LSN 0/00000000 -- =================================================================== +-- Tests for locating WAL by timestamps stored in WAL records +-- =================================================================== +-- Put two WAL-logged transaction commits between the requested boundaries. +INSERT INTO sample_tbl VALUES (5, 5); +SELECT clock_timestamp() AS wal_time_lower \gset +INSERT INTO sample_tbl VALUES (6, 6); +INSERT INTO sample_tbl VALUES (10, 10); +SELECT clock_timestamp() AS wal_time_upper \gset +SELECT start_timestamp >= :'wal_time_lower'::timestamptz + AS start_ok, + end_timestamp > :'wal_time_lower'::timestamptz AND + end_timestamp <= :'wal_time_upper'::timestamptz AS end_ok, + start_lsn < end_lsn AS lsn_ok +FROM pg_get_wal_location_at_time(:'wal_time_lower', + before => interval '1 microsecond', + after => :'wal_time_upper'::timestamptz - + :'wal_time_lower'::timestamptz); + start_ok | end_ok | lsn_ok +----------+--------+-------- + t | t | t +(1 row) + +-- A single matching timestamped record is returned for both boundaries. +SELECT clock_timestamp() AS single_time_lower \gset +INSERT INTO sample_tbl VALUES (12, 12); +SELECT clock_timestamp() AS single_time_upper \gset +SELECT location.start_timestamp = location.end_timestamp AS timestamp_ok, + location.start_lsn = location.end_lsn AS lsn_ok, + info.record_type = 'COMMIT' AS record_ok +FROM pg_get_wal_location_at_time( + :'single_time_lower'::timestamptz + interval '1 microsecond', + interval '1 microsecond', + :'single_time_upper'::timestamptz - + :'single_time_lower'::timestamptz - interval '1 microsecond') AS location, + LATERAL pg_get_wal_record_info(location.start_lsn) AS info; + timestamp_ok | lsn_ok | record_ok +--------------+--------+----------- + t | t | t +(1 row) + +-- Exercise a time window containing a COMMIT PREPARED record. Concurrent +-- activity can cause another timestamped record to be selected as the anchor. +INSERT INTO sample_tbl VALUES (11, 11); +BEGIN; +INSERT INTO sample_tbl VALUES (7, 7); +PREPARE TRANSACTION 'regress_pg_walinspect_time'; +SELECT clock_timestamp() AS prepared_time_target \gset +COMMIT PREPARED 'regress_pg_walinspect_time'; +SELECT clock_timestamp() AS prepared_time_upper \gset +SELECT record_type IN ('COMMIT', 'COMMIT_PREPARED', 'ABORT', + 'ABORT_PREPARED', 'RESTORE_POINT') AS timestamped_ok +FROM pg_get_wal_location_at_time(:'prepared_time_target', + :'prepared_time_target'::timestamptz - + :'wal_time_upper'::timestamptz, + :'prepared_time_upper'::timestamptz - + :'prepared_time_target'::timestamptz) AS location, + LATERAL pg_get_wal_record_info(location.end_lsn); + timestamped_ok +---------------- + t +(1 row) + +-- Exercise a time window containing restore points, allowing for other +-- timestamped WAL records generated concurrently. +SELECT pg_create_restore_point('regress_wal_time_lower') AS restore_lsn \gset +SELECT clock_timestamp() AS restore_time_target \gset +SELECT pg_create_restore_point('regress_wal_time_upper') AS restore_lsn \gset +SELECT clock_timestamp() AS restore_time_upper \gset +INSERT INTO sample_tbl VALUES (8, 8); +SELECT start_info.record_type IN ('COMMIT', 'COMMIT_PREPARED', 'ABORT', + 'ABORT_PREPARED', 'RESTORE_POINT') AS start_ok, + end_info.record_type IN ('COMMIT', 'COMMIT_PREPARED', 'ABORT', + 'ABORT_PREPARED', 'RESTORE_POINT') AS end_ok +FROM pg_get_wal_location_at_time(:'restore_time_target', + :'restore_time_target'::timestamptz - + :'wal_time_upper'::timestamptz, + :'restore_time_upper'::timestamptz - + :'restore_time_target'::timestamptz) AS location, + LATERAL pg_get_wal_record_info(location.start_lsn) AS start_info, + LATERAL pg_get_wal_record_info(location.end_lsn) AS end_info; + start_ok | end_ok +----------+-------- + t | t +(1 row) + +-- A future upper bound uses a timestamped WAL record at or before now. +SELECT clock_timestamp() AS current_time_target \gset +INSERT INTO sample_tbl VALUES (9, 9); +SELECT location.start_timestamp >= :'restore_time_upper'::timestamptz AND + location.start_timestamp <= :'current_time_target'::timestamptz + AS start_ok, + location.end_timestamp >= :'current_time_target'::timestamptz AND + location.end_timestamp <= clock_timestamp() AS capped_end_ok, + location.start_lsn < location.end_lsn AS lsn_ok, + end_info.record_type = 'COMMIT' AS real_record_ok +FROM pg_get_wal_location_at_time(:'current_time_target', + :'current_time_target'::timestamptz - + :'restore_time_upper'::timestamptz, + interval '1 day') AS location, + LATERAL pg_get_wal_record_info(location.end_lsn) AS end_info; + start_ok | capped_end_ok | lsn_ok | real_record_ok +----------+---------------+--------+---------------- + t | t | t | t +(1 row) + +SELECT pg_get_function_arguments( + 'pg_get_wal_location_at_time(timestamptz, interval, interval)'::regprocedure) + LIKE '%before interval DEFAULT ''@ 1 min''::interval, after interval DEFAULT ''@ 1 min''::interval%' + AS defaults_ok; + defaults_ok +------------- + t +(1 row) + +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + interval '0', interval '1 second'); +ERROR: before interval must be finite and greater than zero +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + interval '1 second', interval '0'); +ERROR: after interval must be finite and greater than zero +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', interval '-1 second'); +ERROR: before interval must be finite and greater than zero +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + after => interval '-1 second'); +ERROR: after interval must be finite and greater than zero +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + before => interval '1 day 1 microsecond'); +ERROR: before interval must not exceed one day +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + after => interval '1 day 1 microsecond'); +ERROR: after interval must not exceed one day +SELECT * FROM pg_get_wal_location_at_time(clock_timestamp() - interval '100 years'); +ERROR: could not locate a WAL time boundary +SELECT * FROM pg_get_wal_location_at_time(clock_timestamp() + interval '100 years'); +ERROR: requested time follows the available WAL range +-- =================================================================== -- Test for filtering out WAL records of a particular table -- =================================================================== SELECT oid AS sample_tbl_oid FROM pg_class WHERE relname = 'sample_tbl' \gset @@ -231,6 +367,13 @@ SELECT has_function_privilege('regress_pg_walinspect', f (1 row) +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_location_at_time(timestamptz, interval, interval)', 'EXECUTE'); -- no + has_function_privilege +------------------------ + f +(1 row) + SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- no has_function_privilege @@ -268,6 +411,13 @@ SELECT has_function_privilege('regress_pg_walinspect', t (1 row) +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_location_at_time(timestamptz, interval, interval)', 'EXECUTE'); -- yes + has_function_privilege +------------------------ + t +(1 row) + SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- yes has_function_privilege @@ -285,6 +435,8 @@ GRANT EXECUTE ON FUNCTION pg_get_wal_stats(pg_lsn, pg_lsn, boolean) TO regress_pg_walinspect; GRANT EXECUTE ON FUNCTION pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) TO regress_pg_walinspect; +GRANT EXECUTE ON FUNCTION pg_get_wal_location_at_time(timestamptz, interval, interval) + TO regress_pg_walinspect; GRANT EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) TO regress_pg_walinspect; SELECT has_function_privilege('regress_pg_walinspect', @@ -315,6 +467,13 @@ SELECT has_function_privilege('regress_pg_walinspect', t (1 row) +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_location_at_time(timestamptz, interval, interval)', 'EXECUTE'); -- yes + has_function_privilege +------------------------ + t +(1 row) + SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- yes has_function_privilege @@ -330,6 +489,8 @@ REVOKE EXECUTE ON FUNCTION pg_get_wal_stats(pg_lsn, pg_lsn, boolean) FROM regress_pg_walinspect; REVOKE EXECUTE ON FUNCTION pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) FROM regress_pg_walinspect; +REVOKE EXECUTE ON FUNCTION pg_get_wal_location_at_time(timestamptz, interval, interval) + FROM regress_pg_walinspect; REVOKE EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) FROM regress_pg_walinspect; -- =================================================================== diff --git a/contrib/pg_walinspect/meson.build b/contrib/pg_walinspect/meson.build index 6f8b4a6359a..79e94e33e4d 100644 --- a/contrib/pg_walinspect/meson.build +++ b/contrib/pg_walinspect/meson.build @@ -41,6 +41,7 @@ tests += { 'tap': { 'tests': [ 't/001_timeline.pl', + 't/002_time_search.pl', ], }, } diff --git a/contrib/pg_walinspect/pg_walinspect--1.1--1.2.sql b/contrib/pg_walinspect/pg_walinspect--1.1--1.2.sql index 9cc9523b4dc..f72d99a3ceb 100644 --- a/contrib/pg_walinspect/pg_walinspect--1.1--1.2.sql +++ b/contrib/pg_walinspect/pg_walinspect--1.1--1.2.sql @@ -3,6 +3,27 @@ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION pg_walinspect UPDATE TO '1.2'" to load this file. \quit +-- +-- pg_get_wal_location_at_time() +-- +CREATE FUNCTION pg_get_wal_location_at_time( + IN target_time timestamptz, + IN before interval DEFAULT '1 minute', + IN after interval DEFAULT '1 minute', + OUT start_timestamp timestamptz, + OUT start_lsn pg_lsn, + OUT end_timestamp timestamptz, + OUT end_lsn pg_lsn +) +AS 'MODULE_PATHNAME', 'pg_get_wal_location_at_time' +LANGUAGE C STRICT PARALLEL SAFE; + +REVOKE EXECUTE ON FUNCTION + pg_get_wal_location_at_time(timestamptz, interval, interval) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_wal_location_at_time(timestamptz, interval, interval) + TO pg_read_server_files; + -- -- pg_get_wal_files() -- diff --git a/contrib/pg_walinspect/pg_walinspect.c b/contrib/pg_walinspect/pg_walinspect.c index 42f366bdb4e..0511bb591e7 100644 --- a/contrib/pg_walinspect/pg_walinspect.c +++ b/contrib/pg_walinspect/pg_walinspect.c @@ -12,6 +12,8 @@ */ #include "postgres.h" +#include + #include "access/htup_details.h" #include "access/timeline.h" #include "access/xlog.h" @@ -26,7 +28,9 @@ #include "storage/fd.h" #include "utils/array.h" #include "utils/builtins.h" +#include "utils/fmgrprotos.h" #include "utils/pg_lsn.h" +#include "utils/timestamp.h" #include "utils/tuplestore.h" /* @@ -41,6 +45,7 @@ PG_MODULE_MAGIC_EXT( PG_FUNCTION_INFO_V1(pg_get_wal_block_info); PG_FUNCTION_INFO_V1(pg_get_wal_files); +PG_FUNCTION_INFO_V1(pg_get_wal_location_at_time); PG_FUNCTION_INFO_V1(pg_get_wal_record_info); PG_FUNCTION_INFO_V1(pg_get_wal_records_info); PG_FUNCTION_INFO_V1(pg_get_wal_records_info_till_end_of_wal); @@ -71,15 +76,51 @@ static void GetWalStats(FunctionCallInfo fcinfo, static void GetWALBlockInfo(FunctionCallInfo fcinfo, XLogReaderState *record, bool show_data); +/* Selected WAL position and timestamp for one time boundary. */ +typedef struct WalTimeBoundary +{ + bool found; /* matching record was found */ + TimestampTz time; /* record's local event time */ + XLogRecPtr lsn; /* record's start LSN */ + int segment_index; /* index of the segment containing the record */ +} WalTimeBoundary; + /* Metadata for one retained segment in the current timeline's history. */ typedef struct WalTimeSegment { XLogSegNo segno; /* segment number, used for WAL ordering */ TimeLineID tli; /* timeline containing this segment */ + bool scanned; /* segment has already been decoded */ + bool has_timestamp; /* segment contains timestamped records */ + TimestampTz min_time; /* minimum record timestamp in segment */ + TimestampTz max_time; /* maximum record timestamp in segment */ + WalTimeBoundary lower; /* best lower anchor in segment */ + WalTimeBoundary upper; /* best upper anchor in segment */ } WalTimeSegment; static int wal_time_segment_cmp(const void *a, const void *b); static WalTimeSegment *GetWalTimeSegments(int *nsegments); +static int FindWalTimeCandidate(WalTimeSegment *segments, + int *search_segments, + int *nsearch_segments, + TimestampTz target_time, + bool search_left, + TimestampTz wanted_start, + TimestampTz wanted_end, + int *candidate_position, + int *nscanned); +static void ScanWalTimeSegment(WalTimeSegment *segment, int segment_index, + TimestampTz wanted_start, + TimestampTz wanted_end); +static void MergeWalTimeSegment(WalTimeSegment *segment, + WalTimeBoundary *lower, + WalTimeBoundary *upper); +static void ProbeLowerBoundary(WalTimeBoundary *boundary, + TimestampTz time, XLogRecPtr lsn, + int segment_index); +static void ProbeUpperBoundary(WalTimeBoundary *boundary, + TimestampTz time, XLogRecPtr lsn, + int segment_index); /* * Return the LSN up to which the server has WAL, and optionally its timeline. @@ -223,6 +264,44 @@ wal_time_segment_cmp(const void *a, const void *b) return 0; } +/* + * Probe a timestamped record at or after a requested time boundary. Among + * records examined so far, prefer the earliest time, breaking ties in favor + * of the earlier WAL record. + */ +static void +ProbeLowerBoundary(WalTimeBoundary *boundary, TimestampTz time, + XLogRecPtr lsn, int segment_index) +{ + if (!boundary->found || time < boundary->time || + (time == boundary->time && lsn < boundary->lsn)) + { + boundary->found = true; + boundary->time = time; + boundary->lsn = lsn; + boundary->segment_index = segment_index; + } +} + +/* + * Probe a timestamped record at or before a requested time boundary. Among + * records examined so far, prefer the latest time, breaking ties in favor of + * the later WAL record. + */ +static void +ProbeUpperBoundary(WalTimeBoundary *boundary, TimestampTz time, + XLogRecPtr lsn, int segment_index) +{ + if (!boundary->found || time > boundary->time || + (time == boundary->time && lsn > boundary->lsn)) + { + boundary->found = true; + boundary->time = time; + boundary->lsn = lsn; + boundary->segment_index = segment_index; + } +} + /* * Get the WAL segments that form the history of the server's current * timeline. Other timelines may have files with the same segment number in @@ -297,6 +376,140 @@ GetWalTimeSegments(int *nsegments) return segments; } +/* + * Approximately locate the segment containing target_time by binary search. + * Record timestamps are expected to generally increase with WAL position. + * Remove segments without timestamped records from the search candidates, + * since they provide no value with which to choose either half. + */ +static int +FindWalTimeCandidate(WalTimeSegment *segments, int *search_segments, + int *nsearch_segments, + TimestampTz target_time, bool search_left, + TimestampTz wanted_start, TimestampTz wanted_end, + int *candidate_position, int *nscanned) +{ + int left = 0; + int right = *nsearch_segments - 1; + int result; + + while (left <= right) + { + int middle = left + (right - left) / 2; + int segment_index = search_segments[middle]; + WalTimeSegment *segment = &segments[segment_index]; + + if (!segment->scanned) + { + ScanWalTimeSegment(segment, segment_index, wanted_start, wanted_end); + (*nscanned)++; + } + + if (!segment->has_timestamp) + { + memmove(&search_segments[middle], &search_segments[middle + 1], + (*nsearch_segments - middle - 1) * sizeof(int)); + (*nsearch_segments)--; + right--; + continue; + } + else if (target_time < segment->min_time) + right = middle - 1; + else if (target_time > segment->max_time) + left = middle + 1; + else + { + if (candidate_position != NULL) + *candidate_position = middle; + return search_segments[middle]; + } + } + + if (*nsearch_segments == 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not locate a WAL time boundary"), + errdetail("No retained WAL segment contains a timestamped record.")); + + if (search_left) + result = Max(right, 0); + else + result = Min(left, *nsearch_segments - 1); + + if (candidate_position != NULL) + *candidate_position = result; + return search_segments[result]; +} + +/* + * Inspect timestamped records beginning in one segment and save its time + * range and best boundary candidates. XLogReadRecord() may read the following + * segment to assemble a record crossing the boundary, but that record is + * attributed to the segment containing its starting LSN. + */ +static void +ScanWalTimeSegment(WalTimeSegment *segment, int segment_index, + TimestampTz wanted_start, TimestampTz wanted_end) +{ + XLogRecPtr seg_start = segment->segno * wal_segment_size; + XLogRecPtr seg_end = seg_start + wal_segment_size; + XLogReaderState *xlogreader; + + Assert(!segment->scanned); + segment->scanned = true; + xlogreader = InitXLogReaderState(seg_start); + + while (ReadNextXLogRecord(xlogreader)) + { + TimestampTz record_time; + + if (xlogreader->ReadRecPtr >= seg_end) + break; + + if (!GetXLogRecordTimestamp(xlogreader, &record_time)) + continue; + + if (!segment->has_timestamp) + { + segment->has_timestamp = true; + segment->min_time = segment->max_time = record_time; + } + else + { + segment->min_time = Min(segment->min_time, record_time); + segment->max_time = Max(segment->max_time, record_time); + } + + if (record_time >= wanted_start) + ProbeLowerBoundary(&segment->lower, record_time, + xlogreader->ReadRecPtr, segment_index); + + if (record_time <= wanted_end) + ProbeUpperBoundary(&segment->upper, record_time, + xlogreader->ReadRecPtr, segment_index); + + CHECK_FOR_INTERRUPTS(); + } + + pfree(xlogreader->private_data); + XLogReaderFree(xlogreader); +} + +/* Merge one segment's boundary candidates into the result. */ +static void +MergeWalTimeSegment(WalTimeSegment *segment, WalTimeBoundary *lower, + WalTimeBoundary *upper) +{ + Assert(segment->scanned); + + if (lower != NULL && segment->lower.found) + ProbeLowerBoundary(lower, segment->lower.time, segment->lower.lsn, + segment->lower.segment_index); + if (upper != NULL && segment->upper.found) + ProbeUpperBoundary(upper, segment->upper.time, segment->upper.lsn, + segment->upper.segment_index); +} + /* * Output values that make up a row describing caller's WAL record. * @@ -623,6 +836,215 @@ pg_get_wal_record_info(PG_FUNCTION_ARGS) #undef PG_GET_WAL_RECORD_INFO_COLS } +/* + * Locate a WAL region around a wall-clock time range using timestamped WAL + * records. + * + * Locate each boundary by an approximate binary search over retained WAL + * segments, assuming that record timestamps generally increase with WAL + * position. Also examine one preceding segment for the lower boundary and + * one following segment for the upper boundary to allow for limited timestamp + * reordering. The lower boundary uses the earliest timestamped record at or + * after the requested start time, while the upper boundary uses the latest + * one at or before the requested end time. A future upper bound is first + * capped at the current time. On success, the boundaries form a forward WAL + * range, but are not necessarily the globally closest timestamped records to + * the requested bounds. + */ +Datum +pg_get_wal_location_at_time(PG_FUNCTION_ARGS) +{ +#define PG_GET_WAL_LOCATION_AT_TIME_COLS 4 + TimestampTz target_time = PG_GETARG_TIMESTAMPTZ(0); + Interval *before = PG_GETARG_INTERVAL_P(1); + Interval *after = PG_GETARG_INTERVAL_P(2); + Interval zero = {0}; + Interval one_day = {.day = 1}; + TimestampTz wanted_start; + TimestampTz wanted_end; + TimestampTz current_time; + WalTimeSegment *segments; + WalTimeBoundary lower = {0}; + WalTimeBoundary upper = {0}; + Datum values[PG_GET_WAL_LOCATION_AT_TIME_COLS]; + bool nulls[PG_GET_WAL_LOCATION_AT_TIME_COLS] = {0}; + TupleDesc tupdesc; + HeapTuple tuple; + int nsegments; + int *search_segments; + int nsearch_segments; + int lower_candidate; + int upper_candidate; + int lower_candidate_position; + int nupper_search_segments; + int lower_start; + int upper_end; + int nscanned = 0; + char lower_fname[MAXFNAMELEN]; + char upper_fname[MAXFNAMELEN]; + + if (TIMESTAMP_NOT_FINITE(target_time)) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("target time must be finite")); + if (INTERVAL_NOT_FINITE(before) || + DatumGetInt32(DirectFunctionCall2(interval_cmp, + IntervalPGetDatum(before), + IntervalPGetDatum(&zero))) <= 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("before interval must be finite and greater than zero")); + if (DatumGetInt32(DirectFunctionCall2(interval_cmp, + IntervalPGetDatum(before), + IntervalPGetDatum(&one_day))) > 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("before interval must not exceed one day")); + if (INTERVAL_NOT_FINITE(after) || + DatumGetInt32(DirectFunctionCall2(interval_cmp, + IntervalPGetDatum(after), + IntervalPGetDatum(&zero))) <= 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("after interval must be finite and greater than zero")); + if (DatumGetInt32(DirectFunctionCall2(interval_cmp, + IntervalPGetDatum(after), + IntervalPGetDatum(&one_day))) > 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("after interval must not exceed one day")); + + /* The timestamp functions provide the normal overflow checks. */ + wanted_start = DatumGetTimestampTz(DirectFunctionCall2(timestamptz_mi_interval, + TimestampTzGetDatum(target_time), + IntervalPGetDatum(before))); + wanted_end = DatumGetTimestampTz(DirectFunctionCall2(timestamptz_pl_interval, + TimestampTzGetDatum(target_time), + IntervalPGetDatum(after))); + current_time = GetCurrentTimestamp(); + + /* Cap a future upper bound at the time currently available. */ + if (wanted_end > current_time) + { + if (wanted_start >= current_time) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("requested time follows the available WAL range"), + errdetail("The requested lower bound is not earlier than the current time.")); + + wanted_end = current_time; + } + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + segments = GetWalTimeSegments(&nsegments); + search_segments = palloc_array(int, nsegments); + for (int i = 0; i < nsegments; i++) + search_segments[i] = i; + nsearch_segments = nsegments; + lower_candidate = FindWalTimeCandidate(segments, search_segments, + &nsearch_segments, wanted_start, + false, wanted_start, wanted_end, + &lower_candidate_position, + &nscanned); + + /* Search for the later upper boundary from the lower candidate onward. */ + nupper_search_segments = nsearch_segments - lower_candidate_position; + upper_candidate = FindWalTimeCandidate(segments, + search_segments + lower_candidate_position, + &nupper_search_segments, wanted_end, + true, wanted_start, wanted_end, + NULL, + &nscanned); + XLogFileName(lower_fname, segments[lower_candidate].tli, + segments[lower_candidate].segno, wal_segment_size); + XLogFileName(upper_fname, segments[upper_candidate].tli, + segments[upper_candidate].segno, wal_segment_size); + ereport(DEBUG1, + errmsg_internal("WAL time search selected lower segment %s and upper segment %s from %d retained segments", + lower_fname, upper_fname, + nsegments)); + + /* + * Allow for limited timestamp reordering by examining one preceding + * segment for the lower boundary and one following segment for the upper + * boundary. Do not extend across a missing segment. + */ + lower_start = lower_candidate; + if (lower_start > 0 && + segments[lower_start - 1].segno + 1 == segments[lower_start].segno) + lower_start--; + upper_end = upper_candidate; + if (upper_end + 1 < nsegments && + segments[upper_end].segno + 1 == segments[upper_end + 1].segno) + upper_end++; + + for (int i = lower_start; i <= lower_candidate; i++) + { + if (!segments[i].scanned) + { + ScanWalTimeSegment(&segments[i], i, wanted_start, wanted_end); + nscanned++; + } + MergeWalTimeSegment(&segments[i], &lower, NULL); + } + for (int i = upper_candidate; i <= upper_end; i++) + { + if (!segments[i].scanned) + { + ScanWalTimeSegment(&segments[i], i, wanted_start, wanted_end); + nscanned++; + } + MergeWalTimeSegment(&segments[i], NULL, &upper); + } + + ereport(DEBUG1, + errmsg_internal("WAL time search decoded %d of %d retained segments", + nscanned, nsegments)); + + if (!lower.found) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not locate a WAL time boundary"), + errdetail("The WAL search did not find a timestamped record at or after the requested lower bound.")); + if (!upper.found) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not locate a WAL time boundary"), + errdetail("The WAL search did not find a timestamped record at or before the requested upper bound.")); + + if (lower.lsn > upper.lsn) + ereport(ERROR, + errcode(ERRCODE_DATA_EXCEPTION), + errmsg("could not find a valid WAL range for the requested time window"), + errdetail("The matching lower boundary follows the upper boundary in WAL.")); + + Assert(lower.segment_index >= 0 && lower.segment_index < nsegments); + Assert(upper.segment_index >= 0 && upper.segment_index < nsegments); + for (int i = lower.segment_index + 1; i <= upper.segment_index; i++) + { + if (segments[i - 1].segno + 1 != segments[i].segno) + ereport(ERROR, + errcode(ERRCODE_DATA_EXCEPTION), + errmsg("WAL segment needed for the time search is missing"), + errdetail("The start LSN is %X/%08X, the end LSN is %X/%08X, and segment " UINT64_FORMAT " is not present in pg_wal.", + LSN_FORMAT_ARGS(lower.lsn), + LSN_FORMAT_ARGS(upper.lsn), + (uint64) (segments[i - 1].segno + 1))); + } + + values[0] = TimestampTzGetDatum(lower.time); + values[1] = LSNGetDatum(lower.lsn); + values[2] = TimestampTzGetDatum(upper.time); + values[3] = LSNGetDatum(upper.lsn); + + pfree(segments); + tuple = heap_form_tuple(tupdesc, values, nulls); + PG_RETURN_DATUM(HeapTupleGetDatum(tuple)); +#undef PG_GET_WAL_LOCATION_AT_TIME_COLS +} + /* * List the retained WAL segment files intersecting [start_lsn, end_lsn), or * the segment containing start_lsn when end_lsn is omitted or equal to it. diff --git a/contrib/pg_walinspect/sql/pg_walinspect.sql b/contrib/pg_walinspect/sql/pg_walinspect.sql index 3176a17d855..dbc3240ae30 100644 --- a/contrib/pg_walinspect/sql/pg_walinspect.sql +++ b/contrib/pg_walinspect/sql/pg_walinspect.sql @@ -75,6 +75,114 @@ SELECT count(*) = 1 AS future_end_ok FROM pg_get_wal_files(pg_current_wal_lsn(), 'FFFFFFFF/FFFFFFFF'); SELECT * FROM pg_get_wal_files('0/0', '0/1'); +-- =================================================================== +-- Tests for locating WAL by timestamps stored in WAL records +-- =================================================================== + +-- Put two WAL-logged transaction commits between the requested boundaries. +INSERT INTO sample_tbl VALUES (5, 5); +SELECT clock_timestamp() AS wal_time_lower \gset +INSERT INTO sample_tbl VALUES (6, 6); +INSERT INTO sample_tbl VALUES (10, 10); +SELECT clock_timestamp() AS wal_time_upper \gset + +SELECT start_timestamp >= :'wal_time_lower'::timestamptz + AS start_ok, + end_timestamp > :'wal_time_lower'::timestamptz AND + end_timestamp <= :'wal_time_upper'::timestamptz AS end_ok, + start_lsn < end_lsn AS lsn_ok +FROM pg_get_wal_location_at_time(:'wal_time_lower', + before => interval '1 microsecond', + after => :'wal_time_upper'::timestamptz - + :'wal_time_lower'::timestamptz); + +-- A single matching timestamped record is returned for both boundaries. +SELECT clock_timestamp() AS single_time_lower \gset +INSERT INTO sample_tbl VALUES (12, 12); +SELECT clock_timestamp() AS single_time_upper \gset +SELECT location.start_timestamp = location.end_timestamp AS timestamp_ok, + location.start_lsn = location.end_lsn AS lsn_ok, + info.record_type = 'COMMIT' AS record_ok +FROM pg_get_wal_location_at_time( + :'single_time_lower'::timestamptz + interval '1 microsecond', + interval '1 microsecond', + :'single_time_upper'::timestamptz - + :'single_time_lower'::timestamptz - interval '1 microsecond') AS location, + LATERAL pg_get_wal_record_info(location.start_lsn) AS info; + +-- Exercise a time window containing a COMMIT PREPARED record. Concurrent +-- activity can cause another timestamped record to be selected as the anchor. +INSERT INTO sample_tbl VALUES (11, 11); +BEGIN; +INSERT INTO sample_tbl VALUES (7, 7); +PREPARE TRANSACTION 'regress_pg_walinspect_time'; +SELECT clock_timestamp() AS prepared_time_target \gset +COMMIT PREPARED 'regress_pg_walinspect_time'; +SELECT clock_timestamp() AS prepared_time_upper \gset + +SELECT record_type IN ('COMMIT', 'COMMIT_PREPARED', 'ABORT', + 'ABORT_PREPARED', 'RESTORE_POINT') AS timestamped_ok +FROM pg_get_wal_location_at_time(:'prepared_time_target', + :'prepared_time_target'::timestamptz - + :'wal_time_upper'::timestamptz, + :'prepared_time_upper'::timestamptz - + :'prepared_time_target'::timestamptz) AS location, + LATERAL pg_get_wal_record_info(location.end_lsn); + +-- Exercise a time window containing restore points, allowing for other +-- timestamped WAL records generated concurrently. +SELECT pg_create_restore_point('regress_wal_time_lower') AS restore_lsn \gset +SELECT clock_timestamp() AS restore_time_target \gset +SELECT pg_create_restore_point('regress_wal_time_upper') AS restore_lsn \gset +SELECT clock_timestamp() AS restore_time_upper \gset +INSERT INTO sample_tbl VALUES (8, 8); + +SELECT start_info.record_type IN ('COMMIT', 'COMMIT_PREPARED', 'ABORT', + 'ABORT_PREPARED', 'RESTORE_POINT') AS start_ok, + end_info.record_type IN ('COMMIT', 'COMMIT_PREPARED', 'ABORT', + 'ABORT_PREPARED', 'RESTORE_POINT') AS end_ok +FROM pg_get_wal_location_at_time(:'restore_time_target', + :'restore_time_target'::timestamptz - + :'wal_time_upper'::timestamptz, + :'restore_time_upper'::timestamptz - + :'restore_time_target'::timestamptz) AS location, + LATERAL pg_get_wal_record_info(location.start_lsn) AS start_info, + LATERAL pg_get_wal_record_info(location.end_lsn) AS end_info; + +-- A future upper bound uses a timestamped WAL record at or before now. +SELECT clock_timestamp() AS current_time_target \gset +INSERT INTO sample_tbl VALUES (9, 9); +SELECT location.start_timestamp >= :'restore_time_upper'::timestamptz AND + location.start_timestamp <= :'current_time_target'::timestamptz + AS start_ok, + location.end_timestamp >= :'current_time_target'::timestamptz AND + location.end_timestamp <= clock_timestamp() AS capped_end_ok, + location.start_lsn < location.end_lsn AS lsn_ok, + end_info.record_type = 'COMMIT' AS real_record_ok +FROM pg_get_wal_location_at_time(:'current_time_target', + :'current_time_target'::timestamptz - + :'restore_time_upper'::timestamptz, + interval '1 day') AS location, + LATERAL pg_get_wal_record_info(location.end_lsn) AS end_info; + +SELECT pg_get_function_arguments( + 'pg_get_wal_location_at_time(timestamptz, interval, interval)'::regprocedure) + LIKE '%before interval DEFAULT ''@ 1 min''::interval, after interval DEFAULT ''@ 1 min''::interval%' + AS defaults_ok; +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + interval '0', interval '1 second'); +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + interval '1 second', interval '0'); +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', interval '-1 second'); +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + after => interval '-1 second'); +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + before => interval '1 day 1 microsecond'); +SELECT * FROM pg_get_wal_location_at_time(:'wal_time_lower', + after => interval '1 day 1 microsecond'); +SELECT * FROM pg_get_wal_location_at_time(clock_timestamp() - interval '100 years'); +SELECT * FROM pg_get_wal_location_at_time(clock_timestamp() + interval '100 years'); + -- =================================================================== -- Test for filtering out WAL records of a particular table -- =================================================================== @@ -134,6 +242,8 @@ SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_stats(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- no SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- no +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_location_at_time(timestamptz, interval, interval)', 'EXECUTE'); -- no SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- no @@ -148,6 +258,8 @@ SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_stats(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- yes SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- yes +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_location_at_time(timestamptz, interval, interval)', 'EXECUTE'); -- yes SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- yes @@ -162,6 +274,8 @@ GRANT EXECUTE ON FUNCTION pg_get_wal_stats(pg_lsn, pg_lsn, boolean) TO regress_pg_walinspect; GRANT EXECUTE ON FUNCTION pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) TO regress_pg_walinspect; +GRANT EXECUTE ON FUNCTION pg_get_wal_location_at_time(timestamptz, interval, interval) + TO regress_pg_walinspect; GRANT EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) TO regress_pg_walinspect; @@ -173,6 +287,8 @@ SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_stats(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- yes SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- yes +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_location_at_time(timestamptz, interval, interval)', 'EXECUTE'); -- yes SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- yes @@ -184,6 +300,8 @@ REVOKE EXECUTE ON FUNCTION pg_get_wal_stats(pg_lsn, pg_lsn, boolean) FROM regress_pg_walinspect; REVOKE EXECUTE ON FUNCTION pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) FROM regress_pg_walinspect; +REVOKE EXECUTE ON FUNCTION pg_get_wal_location_at_time(timestamptz, interval, interval) + FROM regress_pg_walinspect; REVOKE EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) FROM regress_pg_walinspect; diff --git a/contrib/pg_walinspect/t/002_time_search.pl b/contrib/pg_walinspect/t/002_time_search.pl new file mode 100644 index 00000000000..85ed7fb7bf4 --- /dev/null +++ b/contrib/pg_walinspect/t/002_time_search.pl @@ -0,0 +1,51 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test the bounded binary search for WAL time boundaries across segments. +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('time_search'); +$node->init(extra => ['--wal-segsize=1']); +$node->append_conf('postgresql.conf', "autovacuum = off\n"); +$node->start; +$node->safe_psql('postgres', + 'CREATE EXTENSION pg_walinspect; CREATE TABLE test_table (a int);'); +$node->safe_psql('postgres', 'SELECT pg_switch_wal()'); + +my ($lower_time, $upper_time); +for my $i (0 .. 8) +{ + $node->safe_psql('postgres', "INSERT INTO test_table VALUES ($i)"); + $lower_time = $node->safe_psql('postgres', 'SELECT clock_timestamp()') + if $i == 3; + $upper_time = $node->safe_psql('postgres', 'SELECT clock_timestamp()') + if $i == 5; + $node->safe_psql('postgres', 'SELECT pg_switch_wal()') if $i < 8; +} + +my $log_offset = -s $node->logfile; +my $result = $node->safe_psql( + 'postgres', qq{ +SET log_min_messages = debug1; +SELECT start_timestamp >= '$lower_time'::timestamptz - interval '1 microsecond', + end_timestamp > '$lower_time'::timestamptz, + end_timestamp <= '$upper_time'::timestamptz, + start_lsn < end_lsn, + pg_walfile_name(start_lsn) <> pg_walfile_name(end_lsn) +FROM pg_get_wal_location_at_time('$lower_time', + interval '1 microsecond', + '$upper_time'::timestamptz - + '$lower_time'::timestamptz)}); +is($result, 't|t|t|t|t', 'binary search finds boundaries across WAL segments'); + +my $log = slurp_file($node->logfile, $log_offset); +$log =~ /WAL time search decoded (\d+) of (\d+) retained segments/ + or die "could not find WAL time search statistics in server log"; +cmp_ok($1, '<', $2, 'binary search does not decode all retained WAL'); + +$node->stop; +done_testing(); diff --git a/doc/src/sgml/pgwalinspect.sgml b/doc/src/sgml/pgwalinspect.sgml index b4ecae8cfb1..e00beb7d657 100644 --- a/doc/src/sgml/pgwalinspect.sgml +++ b/doc/src/sgml/pgwalinspect.sgml @@ -88,6 +88,113 @@ + + + pg_get_wal_location_at_time(target_time timestamptz, before interval DEFAULT '1 minute', after interval DEFAULT '1 minute') returns record + + + + + Locates a nearby WAL region using records containing local event + timestamps as historical time anchors. The requested time range extends + from target_time minus + before through + target_time plus + after. It returns + start_timestamp, + start_lsn, + end_timestamp, and + end_lsn. On successful return, the following + relationships hold: + +start_timestamp >= target_time - before +end_timestamp <= target_time + after +start_lsn <= end_lsn + + The start boundary is the earliest timestamped record found at or after + the requested start time, and the end boundary is the latest timestamped + record found at or before the requested end time. If + target_time plus + after is later than the current time, the + requested upper bound is first capped at the current time. The function + does not construct an end boundary from the current WAL position. Thus, + both returned boundaries come from actual timestamped WAL records. If + only one timestamped record matches the requested time range, both + boundaries identify that record and the returned timestamps and LSNs + are equal. Both interval arguments must be greater than zero and not + greater than one day. + + + Most WAL records do not contain a wall-clock timestamp. Historical + anchors can be transaction COMMIT, + COMMIT PREPARED, ABORT, + or ABORT PREPARED records, as well as restore point + records. These are the WAL record types from which PostgreSQL recovery + can extract timestamps. Their timestamps describe the corresponding + event; they do not assign timestamps to heap, index, or other records. + + + In particular, a commit timestamp describes when a transaction + committed, not when its earlier changes generated WAL. For example, a + transaction might update a row at 14:00 and commit at 15:00; the heap + WAL was generated around 14:00, while the commit record is timestamped + 15:00. The returned LSNs therefore do not identify the exact generation + time of heap, index, or other WAL records, nor do they guarantee that + every record generated during the requested wall-clock range lies + between the returned LSNs. On a cluster with little timestamped + activity, the returned timestamps can be far from the requested bounds. + + + The function assumes that WAL record timestamps generally increase with + WAL position and uses an approximate binary search to locate each time + boundary. WAL record timestamps are not guaranteed to follow WAL order, + for example because of concurrent activity, clock adjustments, or + delayed WAL insertion. Such timestamp reordering can cause relevant + records to fall outside the returned range. Therefore, the returned + records are not guaranteed to be the globally closest timestamped + records to either requested time bound. If the returned range does not + contain the records of interest, the user can widen the search range and + repeat the search. If the matching lower boundary follows the upper + boundary in WAL, the function reports an error rather than returning a + range with the LSNs reversed. + + + Only WAL that is currently retained in pg_wal can + be searched. Unlike the other pg_walinspect + functions, this function may inspect retained WAL from the current + timeline's history as well as the current timeline. The function + reports an error when either boundary cannot be found by the bounded + search or when the resulting range would cross a missing WAL segment. + + + For example, a DBA can locate WAL around activity near a given time and + then inspect the returned range: + +SELECT * +FROM pg_get_wal_location_at_time( + '2026-08-01 14:35:00+08', + before => '10 minutes', + after => '10 minutes'); + +SELECT * +FROM pg_get_wal_files('start_lsn', + 'end_lsn'); + +SELECT * +FROM pg_get_wal_records_info('start_lsn', + 'end_lsn'); + + The corresponding retained WAL files can be obtained with + pg_get_wal_files. To widen the search range, + increase before if the relevant activity + might have occurred earlier, or increase + after if it might have occurred later, and + repeat the search. Failure to find a record in the first returned range + does not necessarily mean that the record is absent from retained WAL. + + + + pg_get_wal_record_info(in_lsn pg_lsn) returns record diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c index 7db7c273b0c..02f49f64b3b 100644 --- a/src/backend/access/transam/xlogreader.c +++ b/src/backend/access/transam/xlogreader.c @@ -26,6 +26,7 @@ #endif #include "access/transam.h" +#include "access/xact.h" #include "access/xlog_internal.h" #include "access/xlogreader.h" #include "access/xlogrecord.h" @@ -65,6 +66,41 @@ static void WALOpenSegmentInit(WALOpenSegment *seg, WALSegmentContext *segcxt, */ #define DEFAULT_DECODE_BUFFER_SIZE (64 * 1024) +/* + * Extract timestamp from WAL record. + * + * If the record contains a timestamp, returns true, and saves the timestamp + * in *recordXtime. If the record type has no timestamp, returns false. + * Currently, only transaction commit/abort records and restore points contain + * timestamps. + */ +bool +GetXLogRecordTimestamp(XLogReaderState *record, TimestampTz *recordXtime) +{ + uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; + uint8 xact_info = info & XLOG_XACT_OPMASK; + uint8 rmid = XLogRecGetRmid(record); + + if (rmid == RM_XLOG_ID && info == XLOG_RESTORE_POINT) + { + *recordXtime = ((xl_restore_point *) XLogRecGetData(record))->rp_time; + return true; + } + if (rmid == RM_XACT_ID && (xact_info == XLOG_XACT_COMMIT || + xact_info == XLOG_XACT_COMMIT_PREPARED)) + { + *recordXtime = ((xl_xact_commit *) XLogRecGetData(record))->xact_time; + return true; + } + if (rmid == RM_XACT_ID && (xact_info == XLOG_XACT_ABORT || + xact_info == XLOG_XACT_ABORT_PREPARED)) + { + *recordXtime = ((xl_xact_abort *) XLogRecGetData(record))->xact_time; + return true; + } + return false; +} + /* * Construct a string in state->errormsg_buf explaining what's wrong with * the current record being read. diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index 54aaec9529f..06a5b10d092 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -356,7 +356,6 @@ static void xlog_outrec(StringInfo buf, XLogReaderState *record); static void xlog_block_info(StringInfo buf, XLogReaderState *record); static void checkTimeLineSwitch(XLogRecPtr lsn, TimeLineID newTLI, TimeLineID prevTLI, TimeLineID replayTLI); -static bool getRecordTimestamp(XLogReaderState *record, TimestampTz *recordXtime); static void verifyBackupPageConsistency(XLogReaderState *record); static bool recoveryStopsBefore(XLogReaderState *record); @@ -2405,41 +2404,6 @@ checkTimeLineSwitch(XLogRecPtr lsn, TimeLineID newTLI, TimeLineID prevTLI, } -/* - * Extract timestamp from WAL record. - * - * If the record contains a timestamp, returns true, and saves the timestamp - * in *recordXtime. If the record type has no timestamp, returns false. - * Currently, only transaction commit/abort records and restore points contain - * timestamps. - */ -static bool -getRecordTimestamp(XLogReaderState *record, TimestampTz *recordXtime) -{ - uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; - uint8 xact_info = info & XLOG_XACT_OPMASK; - uint8 rmid = XLogRecGetRmid(record); - - if (rmid == RM_XLOG_ID && info == XLOG_RESTORE_POINT) - { - *recordXtime = ((xl_restore_point *) XLogRecGetData(record))->rp_time; - return true; - } - if (rmid == RM_XACT_ID && (xact_info == XLOG_XACT_COMMIT || - xact_info == XLOG_XACT_COMMIT_PREPARED)) - { - *recordXtime = ((xl_xact_commit *) XLogRecGetData(record))->xact_time; - return true; - } - if (rmid == RM_XACT_ID && (xact_info == XLOG_XACT_ABORT || - xact_info == XLOG_XACT_ABORT_PREPARED)) - { - *recordXtime = ((xl_xact_abort *) XLogRecGetData(record))->xact_time; - return true; - } - return false; -} - /* * Checks whether the current buffer page and backup page stored in the * WAL record are consistent or not. Before comparing the two pages, a @@ -2663,10 +2627,10 @@ recoveryStopsBefore(XLogReaderState *record) /* * Note: we must fetch recordXtime regardless of recoveryTarget setting. - * We don't expect getRecordTimestamp ever to fail, since we already know - * this is a commit or abort record; but test its result anyway. + * We don't expect GetXLogRecordTimestamp ever to fail, since we already + * know this is a commit or abort record; but test its result anyway. */ - if (getRecordTimestamp(record, &recordXtime) && + if (GetXLogRecordTimestamp(record, &recordXtime) && recoveryTarget == RECOVERY_TARGET_TIME) { /* @@ -2747,7 +2711,7 @@ recoveryStopsAfter(XLogReaderState *record) recoveryStopAfter = true; recoveryStopXid = InvalidTransactionId; recoveryStopLSN = InvalidXLogRecPtr; - (void) getRecordTimestamp(record, &recoveryStopTime); + (void) GetXLogRecordTimestamp(record, &recoveryStopTime); strlcpy(recoveryStopName, recordRestorePointData->rp_name, MAXFNAMELEN); ereport(LOG, @@ -2787,7 +2751,7 @@ recoveryStopsAfter(XLogReaderState *record) TransactionId recordXid; /* Update the last applied transaction timestamp */ - if (getRecordTimestamp(record, &recordXtime)) + if (GetXLogRecordTimestamp(record, &recordXtime)) SetLatestXTime(recordXtime); /* Extract the XID of the committed/aborted transaction */ @@ -3006,7 +2970,7 @@ recoveryApplyDelay(XLogReaderState *record) xact_info != XLOG_XACT_COMMIT_PREPARED) return false; - if (!getRecordTimestamp(record, &xtime)) + if (!GetXLogRecordTimestamp(record, &xtime)) return false; delayUntil = TimestampTzPlusMilliseconds(xtime, recovery_min_apply_delay); diff --git a/src/include/access/xlogreader.h b/src/include/access/xlogreader.h index 4a9a687e879..3016ae4d5da 100644 --- a/src/include/access/xlogreader.h +++ b/src/include/access/xlogreader.h @@ -39,6 +39,7 @@ #endif #include "access/xlogrecord.h" +#include "datatype/timestamp.h" #include "storage/buf.h" /* WALOpenSegment represents a WAL segment being read. */ @@ -440,5 +441,7 @@ extern bool XLogRecGetBlockTagExtended(XLogReaderState *record, uint8 block_id, RelFileLocator *rlocator, ForkNumber *forknum, BlockNumber *blknum, Buffer *prefetch_buffer); +extern bool GetXLogRecordTimestamp(XLogReaderState *record, + TimestampTz *recordXtime); #endif /* XLOGREADER_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 1f1bcf8d527..c85279cf423 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3433,6 +3433,7 @@ WalSndState WalSummarizerData WalSummaryFile WalSummaryIO +WalTimeBoundary WalTimeSample WalTimeSegment WalUsage -- 2.50.1 (Apple Git-155)