public inbox for [email protected]  
help / color / mirror / Atom feed
From: Greg Lamberson <[email protected]>
To: [email protected] <[email protected]>
Subject: Extensible sync handler registration (register_sync_handler)
Date: Fri, 10 Apr 2026 21:46:44 +0000
Message-ID: <IA1PR07MB983072521EE7FDEE98902534A9592@IA1PR07MB9830.namprd07.prod.outlook.com> (raw)

Hackers,

Motivation:

I am working on Lamstore, an extent based storage backend whose data lives
at byte offsets inside a volume file rather than as md segment files.
Lamstore uses a custom storage manager to route backend I/O, which works
today against stock PostgreSQL and Percona Server for PostgreSQL 18. The
checkpoint fsync pipeline, however, is a dead end: syncsw[] in sync.c is
a static const SyncOps[] indexed by the SyncRequestHandler enum, and there
is no way for an extension to install its own dispatch entry. An extension
cannot reuse SYNC_HANDLER_MD because mdsyncfiletag() resolves paths via
relpathperm() plus OpenTransientFile() plus fsync(), which does not reach
Lamstore storage.

The attached two patch series adds a dynamic register_sync_handler() API
to sync.c, parallel to RegisterCustomRmgr and the smgr_register shape
being developed on CF 5616. Built in handlers (MD, CLOG, commit_ts,
multixact_offset, multixact_member) keep their existing enum indices 0
through 4. Extensions get IDs starting at a new SYNC_HANDLER_FIRST_DYNAMIC.
The patch is strictly additive: zero WAL format changes, zero FileTag
layout changes, zero shared memory changes, zero catalog involvement.

Background:

Thomas Munro's 2019 refactor of the checkpointer fsync queue (commit
3eb77eba5a5, PG12) explicitly anticipated non md consumers. The commit
message reads:

  "For now only md.c uses the new interface, but other users are being
  proposed. Since there may be use cases that are not strictly SMGR
  implementations, use a new function table for sync handlers rather
  than extending the traditional SMGR one."

Seven years later the framework is in place but the registration
mechanism has never been added. This patch closes that gap.

Two reviewers have already voiced needs that this API makes trivial. In
Tristan Partin's 2024-01-12 message on the CF 5616 thread
(CAEze2WgMySu2suO_TLvFyGY3URa4mAx22WeoEicnK=PCNWEMrA@mail.gmail.com),
Heikki Linnakangas wrote:

  "[I would like] a debugging tool that checks that we're not missing
  any fsyncs. I bumped into a few missing fsync bugs with unlogged
  tables lately and a tool like that would've been very helpful."

And Andres Freund wrote:

  "I've been thinking that we need a validation layer for fsyncs, it's
  too hard to get right without testing. My thought was that we could
  keep a shared hash table of all files created / dirtied at the fd
  layer, with the filename as key and the value the current LSN. We'd
  delete files from it when they're fsynced."

Both needs become straightforward with register_sync_handler(): a
validation extension loads in shared_preload_libraries, registers its
own handler, and observes every sync request flowing through
ProcessSyncRequests() regardless of the underlying storage manager.
This is strictly stronger than the fsync_checker extension in CF 5616
v5+, which wraps md and can only observe md backed data.

What it does now:

syncsw[] is declared as:

  static const SyncOps syncsw[] = {
      [SYNC_HANDLER_MD]               = { mdsyncfiletag, ... },
      [SYNC_HANDLER_CLOG]             = { clogsyncfiletag, ... },
      [SYNC_HANDLER_COMMIT_TS]        = { committssyncfiletag, ... },
      [SYNC_HANDLER_MULTIXACT_OFFSET] = { multixactoffsetssyncfiletag, ... },
      [SYNC_HANDLER_MULTIXACT_MEMBER] = { multixactmemberssyncfiletag, ... },
  };

Adding a new handler requires patching core. No extension path exists.

What it will do:

The patch converts syncsw[] from static const SyncOps[] to a heap
allocated static SyncOps * grown via doubling repalloc, bounded by
shared_preload_libraries count. Built in handlers are pre-registered at
their existing enum indices by a new InitSyncHandlers() called from
PostmasterMain before process_shared_preload_libraries(). Extensions
call register_sync_handler() from _PG_init and receive a stable int16
handler ID, which they store in their FileTag.handler field at register
time:

  /* src/include/storage/sync.h */
  typedef struct SyncOps {
      int  (*sync_syncfiletag)   (const FileTag *ftag, char *path);
      int  (*sync_unlinkfiletag) (const FileTag *ftag, char *path);
      bool (*sync_filetagmatches)(const FileTag *ftag, const FileTag *candidate);
  } SyncOps;

  extern int16 register_sync_handler(const SyncOps *ops, const char *name);

  typedef enum SyncRequestHandler {
      SYNC_HANDLER_MD               = 0,
      SYNC_HANDLER_CLOG             = 1,
      SYNC_HANDLER_COMMIT_TS        = 2,
      SYNC_HANDLER_MULTIXACT_OFFSET = 3,
      SYNC_HANDLER_MULTIXACT_MEMBER = 4,
      SYNC_HANDLER_FIRST_DYNAMIC    = 5,
      SYNC_HANDLER_MAX              = INT16_MAX,
      SYNC_HANDLER_NONE             = -1,
  } SyncRequestHandler;

Dispatch is syncsw[idx].fn() both before and after. Each caller that
dispatches through the table (ProcessSyncRequests, SyncPostCheckpoint,
RememberSyncRequest) caches the base pointer in a local SyncOps *ops
at function entry so the compiler keeps it in a register for the
lifetime of the function, matching the register allocation the
pre-patch static const array received. Verified with objdump on GCC
14.2 at -O2 that the per-dispatch instruction sequence (movswq, mov,
mov, lea, call *(%r14,%rax,8)) is byte identical between stock and
patched builds. The only remaining delta is one additional memory
load at function entry to fetch the syncsw pointer (mov 0x0(%rip),%r14
versus stock's lea 0x0(%rip),%r14): a single L1 cache hit, paid once
per call to ProcessSyncRequests, not per dispatch.

The 0002 patch adds src/test/modules/test_sync_handler with a TAP test
verifying registration, dispatch, HASH_BLOBS coalescing, and cycle_ctr
skip semantics. Test layout mirrors fsync_checker in CF 5616 v5+.

Risk:

Strictly additive. Zero WAL format changes. Zero FileTag layout
changes. Zero shared memory changes. Zero catalog involvement.
Per-dispatch assembly is byte identical to stock (see above). Built
in enum values (SYNC_HANDLER_MD=0 et al) are preserved. ABI safe.
Recovery path unaffected: extensions cannot register during recovery
because InitSyncHandlers() runs in PostmasterMain before any child
process, and the built ins are all pre-registered at fixed IDs. No
behaviour change when no extension registers.

Pre-empting Andres Freund's 2023 objections to CF 4428 v1:

Your four review comments on the earlier smgr_register prototype
(postgrespro.com thread id 2654666, 2023-07-01) apply here by analogy.
Addressing them directly:

1. "Not a good place to initialize, we'll need it in multiple places
   that way. How about putting it into BaseInit()?"

   Our InitSyncHandlers() is called from a single site, PostmasterMain,
   immediately before process_shared_preload_libraries(). Built ins
   register via a private sync_handler_register_internal() from that
   one site. Child processes inherit the array via fork(), which is a
   full POSIX memory barrier. No per-process re-initialization. This
   is the same lifecycle pattern as RegisterCustomRmgr.

2. "This adds another level of indirection. I would rather limit the
   number of registerable smgrs than do that."

   Doubling repalloc is bounded by shared_preload_libraries count
   (small). Amortized O(1) cost at preload time. Registration happens
   exactly once per extension per postmaster startup, before any
   backend exists. Per-dispatch hot path cost is zero by construction:
   each caller hoists syncsw into a local SyncOps *ops at function
   entry, so the compiler keeps the base pointer in a register and
   the per-entry dispatch compiles to byte-identical assembly as the
   pre-patch static const array (verified with objdump, see Risk
   paragraph above). The only measurable delta is one additional
   memory load at function entry, paid once per ProcessSyncRequests
   call (not per dispatch). If you still prefer a hard cap I am happy
   to add SYNC_HANDLER_MAX_DYNAMIC as a compile-time constant.

3. "Huh, what's that about?" (on pg_compiler_barrier in registration)

   Not included. Registration is single threaded during preload. No
   barrier needed.

4. "It looked to me like you determined this globally, why do we need
   it in every entry then?" (on per entry size fields)

   Not applicable. SyncOps is pure function pointers. No per entry
   size tracking.

Relationship to CF 5616:

This patch is a narrow focused companion to CF 5616 (Extensible storage
manager API), not a dependency or replacement. CF 5616 makes smgrsw[]
dynamic; this patch does the same for syncsw[]. The two are orthogonal:
none of 5616 v6's six sub patches touch sync.c or sync.h. This patch
applies cleanly against master today without 5616, and introduces none
of 5616's unresolved design questions (hook-vs-registration, catalog
recovery, per tablespace vs per relation config, GUC chaining). If 5616
lands later the two compose naturally: extensions call smgr_register()
and register_sync_handler() back to back in their _PG_init.

Percona carryover:

The patch also applies cleanly to percona/postgres PSP_REL_18_STABLE
for our production deployment. sync.c and sync.h have no Percona
specific changes on that branch (verified via gh api), so the same
patch applies identically modulo mechanical offsets. Upstream master
is the primary submission venue. A companion PR against Percona's
branch will reference this thread.

Verified locally:

  * make check-world green on master (PG 19devel) with both patches
    applied
  * make -C src/test/modules/test_sync_handler check green on both
    upstream master and percona/postgres PSP_REL_18_STABLE

I will open a CF 59 (PG20-1) entry today and link this thread's
Message-Id.

Thanks for reading. Feedback, counterproposals, and design pushback
all welcome.

Greg Lamberson
Lamco Development
[email protected]



Attachments:

  [application/octet-stream] v1-0001-Make-sync.c-syncsw-extensible-via-register_sync_h.patch (21.1K, 3-v1-0001-Make-sync.c-syncsw-extensible-via-register_sync_h.patch)
  download | inline diff:
From 0022ac81f9eb4f15d7ac43e52767f446e58df1fc Mon Sep 17 00:00:00 2001
From: Greg Lamberson <[email protected]>
Date: Fri, 10 Apr 2026 07:27:14 -0500
Subject: [PATCH v1 1/2] Make sync.c syncsw[] extensible via
 register_sync_handler()

sync.c's syncsw[] dispatch table is currently a static const array
indexed by the SyncRequestHandler enum. Extension-provided storage
managers cannot add their own sync handlers: the only options are to
fake being SYNC_HANDLER_MD (which only works if the extension's data
lives as md segment files) or to bypass sync.c entirely (losing
checkpoint batching, cycle_ctr semantics, SYNC_FORGET_REQUEST and
SYNC_FILTER_REQUEST cancellation, and the existing ring-buffer
protocol).

This commit adds a public register_sync_handler() API parallel to
smgr_register(), converts syncsw[] to a heap-allocated dynamic array,
and pre-registers the five built-in handlers (MD, CLOG, commit_ts,
multixact_offset, multixact_member) so their canonical enum values
0..4 are preserved exactly.

Extension IDs start at SYNC_HANDLER_FIRST_DYNAMIC. Extensions call
register_sync_handler() from _PG_init() during shared_preload_libraries
load; late calls raise FATAL. Built-in registration uses a private
sync_handler_register_internal() helper that bypasses the preload-phase
guard, because InitSync() runs in auxiliary processes after preload is
done (for idempotency: the NSyncHandlers == 0 guard makes repeated
calls safe).

Built-in registration happens in the postmaster before
process_shared_preload_libraries() runs, so the five built-ins always
occupy IDs 0..4 before any extension gets a chance to claim an ID.
This is the key ordering constraint: the enum values SYNC_HANDLER_MD=0
et al. are compile-time constants that the rest of the tree still
uses, so the runtime table must be populated in that exact order.

Design notes:

- SyncOps is moved to a public typedef in sync.h so extensions can
  declare const SyncOps at file scope. It is placed after FileTag
  because its function-pointer fields take const FileTag *.
- SYNC_HANDLER_NONE becomes -1 (sentinel) instead of 5.
  SYNC_HANDLER_FIRST_DYNAMIC takes the post-MULTIXACT_MEMBER slot
  that SYNC_HANDLER_NONE used to occupy.
- Assert(NSyncHandlers == SYNC_HANDLER_FIRST_DYNAMIC) enforces the
  invariant: if a new built-in is added to the enum, the build
  fail-fasts at first boot until a matching sync_handler_register_
  internal() call is added to InitSyncHandlers().
- ProcessSyncRequests() and related dispatch sites gain defensive
  bounds-check Asserts.
- No WAL format changes. No FileTag layout changes. No shared memory
  changes.

Dispatch hot path: because syncsw is now a mutable static pointer
instead of a const array, GCC cannot hoist the base-address load out
of the dispatch loop on its own (it must conservatively assume the
pointer could change between iterations). To recover the stock register
allocation, each caller that dispatches through the table caches the
base pointer in a local at function entry:

    SyncOps *ops = syncsw;
    ...
    if (ops[entry->tag.handler].sync_syncfiletag(&entry->tag, path) == 0)

This applies to ProcessSyncRequests() and SyncPostCheckpoint(). The
SYNC_FILTER_REQUEST branch of RememberSyncRequest() caches the per-
handler filetagmatches function pointer directly since both match
loops call the same one. With this hoisting, the per-dispatch instruction
sequence compiles to byte-identical assembly as the pre-patch static
const array (verified with objdump on GCC 14.2 at -O2). The only
remaining delta is one additional memory load at function entry to
fetch the syncsw pointer: a single L1-cache hit, paid once per call
to ProcessSyncRequests, not per dispatch.

Motivating use case: lamstore, a new extent-based storage backend
whose data lives at byte offsets inside a volume rather than as md
segment files. Additional beneficiaries include the fsync validation
tooling Heikki Linnakangas and Andres Freund have asked for in
passing (quoted in Tristan Partin's 2024-01-12 message in the CF 5616
thread): with this API, a validation extension can register its own
handler and observe every sync request regardless of the underlying
storage manager.

Relationship to commitfest 5616 'Extensible storage manager API':
this is a narrow, focused companion, not a dependency or replacement.
5616 makes smgrsw[] dynamic; this patch does the same for syncsw[].
The two are orthogonal: this patch applies cleanly against master
today without 5616, and does not depend on any of 5616's unresolved
design questions (catalog recovery, GUC chaining, per-tablespace vs
per-relation configuration, performance benchmarks). If 5616 lands
later, the two patches compose naturally: extensions will call
smgr_register() and register_sync_handler() back to back in their
_PG_init.

Tested: make check-world passes. See the next commit which adds
src/test/modules/test_sync_handler, a minimal extension and TAP test
exercising registration, dispatch, HASH_BLOBS coalescing, and
cycle_ctr skip behavior.

Signed-off-by: Greg Lamberson <[email protected]>
---
 src/backend/postmaster/postmaster.c |  11 ++
 src/backend/storage/sync/sync.c     | 259 ++++++++++++++++++++++++----
 src/include/storage/sync.h          |  64 ++++++-
 3 files changed, 293 insertions(+), 41 deletions(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 6e0f41d2661..8d2ab37ce26 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -116,6 +116,7 @@
 #include "storage/pmsignal.h"
 #include "storage/proc.h"
 #include "storage/shmem_internal.h"
+#include "storage/sync.h"
 #include "tcop/backend_startup.h"
 #include "tcop/tcopprot.h"
 #include "utils/datetime.h"
@@ -929,6 +930,16 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	RegisterBuiltinShmemCallbacks();
 
+	/*
+	 * Register the built-in sync handlers (md, CLOG, commit_ts,
+	 * multixact_offset, multixact_member).  This must happen before
+	 * process_shared_preload_libraries() so that extensions which
+	 * call register_sync_handler() from their _PG_init() receive IDs
+	 * starting at SYNC_HANDLER_FIRST_DYNAMIC instead of colliding
+	 * with the built-in slots.
+	 */
+	InitSyncHandlers();
+
 	/*
 	 * process any libraries that should be preloaded at postmaster start
 	 */
diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c
index 2c964b6f3d9..222829310cf 100644
--- a/src/backend/storage/sync/sync.c
+++ b/src/backend/storage/sync/sync.c
@@ -80,50 +80,200 @@ static CycleCtr checkpoint_cycle_ctr = 0;
 #define UNLINKS_PER_ABSORB		10
 
 /*
- * Function pointers for handling sync and unlink requests.
+ * Sync handler dispatch table.
+ *
+ * Populated by register_sync_handler(), which is called from InitSync()
+ * for the built-in handlers and from extension _PG_init() functions
+ * for extension handlers.  After shared_preload_libraries finishes
+ * loading, syncsw[] is effectively immutable: every backend and the
+ * checkpointer inherit the same fully-populated array via fork() from
+ * the postmaster.
+ *
+ * SyncOps itself is defined in sync.h so that extensions can declare
+ * const SyncOps instances at file scope.
  */
-typedef struct SyncOps
-{
-	int			(*sync_syncfiletag) (const FileTag *ftag, char *path);
-	int			(*sync_unlinkfiletag) (const FileTag *ftag, char *path);
-	bool		(*sync_filetagmatches) (const FileTag *ftag,
-										const FileTag *candidate);
-} SyncOps;
+static SyncOps *syncsw = NULL;
+static const char **sync_handler_names = NULL;
+static int	NSyncHandlers = 0;
+static int	sync_handlers_capacity = 0;
 
 /*
- * These indexes must correspond to the values of the SyncRequestHandler enum.
+ * Built-in SyncOps, registered in enum order during InitSync() so that
+ * SYNC_HANDLER_MD == 0, SYNC_HANDLER_CLOG == 1, etc.
  */
-static const SyncOps syncsw[] = {
-	/* magnetic disk */
-	[SYNC_HANDLER_MD] = {
-		.sync_syncfiletag = mdsyncfiletag,
-		.sync_unlinkfiletag = mdunlinkfiletag,
-		.sync_filetagmatches = mdfiletagmatches
-	},
-	/* pg_xact */
-	[SYNC_HANDLER_CLOG] = {
-		.sync_syncfiletag = clogsyncfiletag
-	},
-	/* pg_commit_ts */
-	[SYNC_HANDLER_COMMIT_TS] = {
-		.sync_syncfiletag = committssyncfiletag
-	},
-	/* pg_multixact/offsets */
-	[SYNC_HANDLER_MULTIXACT_OFFSET] = {
-		.sync_syncfiletag = multixactoffsetssyncfiletag
-	},
-	/* pg_multixact/members */
-	[SYNC_HANDLER_MULTIXACT_MEMBER] = {
-		.sync_syncfiletag = multixactmemberssyncfiletag
-	}
+static const SyncOps builtin_md_ops = {
+	.sync_syncfiletag = mdsyncfiletag,
+	.sync_unlinkfiletag = mdunlinkfiletag,
+	.sync_filetagmatches = mdfiletagmatches,
+};
+static const SyncOps builtin_clog_ops = {
+	.sync_syncfiletag = clogsyncfiletag,
+};
+static const SyncOps builtin_committs_ops = {
+	.sync_syncfiletag = committssyncfiletag,
+};
+static const SyncOps builtin_multixact_offset_ops = {
+	.sync_syncfiletag = multixactoffsetssyncfiletag,
+};
+static const SyncOps builtin_multixact_member_ops = {
+	.sync_syncfiletag = multixactmemberssyncfiletag,
 };
 
+/*
+ * Internal helper that adds an entry to syncsw[] without performing the
+ * preload-phase check.  Used by InitSync() to install the built-in
+ * handlers, which must be present in every process that calls into
+ * sync.c (including the checkpointer, which runs after
+ * shared_preload_libraries has finished loading).
+ */
+static int16
+sync_handler_register_internal(const SyncOps *ops, const char *name)
+{
+	int16		my_id;
+	MemoryContext old;
+
+	if (ops == NULL || ops->sync_syncfiletag == NULL)
+		ereport(FATAL,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("register_sync_handler: sync_syncfiletag is required")));
+
+	if (name == NULL || *name == '\0')
+		ereport(FATAL,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("register_sync_handler: name must be non-empty")));
+
+	if (NSyncHandlers >= SYNC_HANDLER_MAX)
+		ereport(FATAL,
+				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+				 errmsg("too many sync handlers registered (limit %d)",
+						SYNC_HANDLER_MAX)));
+
+	old = MemoryContextSwitchTo(TopMemoryContext);
+
+	if (NSyncHandlers >= sync_handlers_capacity)
+	{
+		int			new_cap = (sync_handlers_capacity == 0)
+			? 8
+			: sync_handlers_capacity * 2;
+
+		if (new_cap > SYNC_HANDLER_MAX)
+			new_cap = SYNC_HANDLER_MAX;
+
+		if (syncsw == NULL)
+		{
+			syncsw = palloc(sizeof(SyncOps) * new_cap);
+			sync_handler_names = palloc(sizeof(char *) * new_cap);
+		}
+		else
+		{
+			syncsw = repalloc(syncsw, sizeof(SyncOps) * new_cap);
+			sync_handler_names = repalloc(sync_handler_names,
+										  sizeof(char *) * new_cap);
+		}
+		sync_handlers_capacity = new_cap;
+	}
+
+	my_id = (int16) NSyncHandlers++;
+	memcpy(&syncsw[my_id], ops, sizeof(SyncOps));
+	sync_handler_names[my_id] = pstrdup(name);
+
+	MemoryContextSwitchTo(old);
+
+	/*
+	 * No barrier needed: registration only happens during
+	 * shared_preload_libraries load, which is single-threaded in the
+	 * postmaster.  All backends and the checkpointer inherit the
+	 * fully-populated array via fork() after preload returns.
+	 */
+	return my_id;
+}
+
+/*
+ * Public registration entry point for extensions.  See sync.h for the
+ * contract.
+ *
+ * Extensions must call this from their _PG_init() while the postmaster
+ * is still loading shared_preload_libraries; late calls raise FATAL.
+ * Built-in handlers bypass this guard via sync_handler_register_internal()
+ * because the checkpointer auxiliary process calls InitSync() after
+ * preload has finished, and the built-in dispatch table must still be
+ * populated in that process.
+ */
+int16
+register_sync_handler(const SyncOps *ops, const char *name)
+{
+	if (process_shared_preload_libraries_done)
+		ereport(FATAL,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("sync handlers must be registered in "
+						"shared_preload_libraries phase")));
+
+	return sync_handler_register_internal(ops, name);
+}
+
+/*
+ * Register the built-in sync handlers.
+ *
+ * This MUST run before any call to register_sync_handler() from
+ * extension _PG_init() code, so that the built-in handlers occupy
+ * their canonical IDs (SYNC_HANDLER_MD = 0, SYNC_HANDLER_CLOG = 1,
+ * etc.) and extension handlers are assigned IDs >=
+ * SYNC_HANDLER_FIRST_DYNAMIC.
+ *
+ * Called from:
+ *   - PostmasterMain(), just before process_shared_preload_libraries()
+ *   - AuxiliaryProcessMain() (not currently needed because aux procs
+ *     fork from the postmaster with syncsw[] already populated, but
+ *     see the idempotent NSyncHandlers==0 guard below)
+ *   - Standalone backend init (via InitSync -> InitSyncHandlers)
+ *
+ * Idempotent: the NSyncHandlers == 0 guard ensures built-ins are
+ * registered exactly once per process. Safe to call from multiple
+ * init paths.
+ */
+void
+InitSyncHandlers(void)
+{
+	if (NSyncHandlers != 0)
+		return;
+
+	(void) sync_handler_register_internal(&builtin_md_ops, "md");
+	(void) sync_handler_register_internal(&builtin_clog_ops, "clog");
+	(void) sync_handler_register_internal(&builtin_committs_ops, "commit_ts");
+	(void) sync_handler_register_internal(&builtin_multixact_offset_ops,
+										  "multixact_offset");
+	(void) sync_handler_register_internal(&builtin_multixact_member_ops,
+										  "multixact_member");
+
+	/*
+	 * Enforce the enum-to-count invariant: if a new built-in is added
+	 * to the SyncRequestHandler enum, the build will fail-fast at
+	 * first boot until a matching sync_handler_register_internal()
+	 * call is added here.
+	 */
+	Assert(NSyncHandlers == SYNC_HANDLER_FIRST_DYNAMIC);
+}
+
 /*
  * Initialize data structures for the file sync tracking.
+ *
+ * This runs in processes that actually need the pendingOps hash table
+ * (standalone backends and the checkpointer). It also calls
+ * InitSyncHandlers() defensively in case this process reached here
+ * without the postmaster having done so, e.g., standalone mode.
  */
 void
 InitSync(void)
 {
+	/*
+	 * Make sure built-in handlers are registered. In the postmaster,
+	 * this was already called from PostmasterMain() before
+	 * process_shared_preload_libraries(); in standalone mode it is
+	 * called here for the first (and only) time. The NSyncHandlers
+	 * guard inside InitSyncHandlers() makes it idempotent.
+	 */
+	InitSyncHandlers();
+
 	/*
 	 * Create pending-operations hashtable if we need it.  Currently, we need
 	 * it if we are standalone (not under a postmaster) or if we are a
@@ -205,6 +355,19 @@ SyncPostCheckpoint(void)
 	int			absorb_counter;
 	ListCell   *lc;
 
+	/*
+	 * Cache the syncsw base pointer in a local for the duration of this
+	 * function. Without this, the compiler cannot hoist the load of the
+	 * mutable static pointer out of the dispatch loop, and each dispatch
+	 * costs an extra memory load plus an address-materialization LEA
+	 * (verified with objdump on GCC 14.2 -O2). With the local cached, the
+	 * per-entry dispatch compiles down to identical assembly as the
+	 * pre-patch static-const array. Safe because register_sync_handler()
+	 * is forbidden after process_shared_preload_libraries_done and syncsw
+	 * is never mutated outside registration.
+	 */
+	SyncOps    *ops = syncsw;
+
 	absorb_counter = UNLINKS_PER_ABSORB;
 	foreach(lc, pendingUnlinks)
 	{
@@ -227,9 +390,12 @@ SyncPostCheckpoint(void)
 		if (entry->cycle_ctr == checkpoint_cycle_ctr)
 			break;
 
+		Assert(entry->tag.handler >= 0 &&
+			   entry->tag.handler < NSyncHandlers);
+
 		/* Unlink the file */
-		if (syncsw[entry->tag.handler].sync_unlinkfiletag(&entry->tag,
-														  path) < 0)
+		if (ops[entry->tag.handler].sync_unlinkfiletag(&entry->tag,
+													   path) < 0)
 		{
 			/*
 			 * There's a race condition, when the database is dropped at the
@@ -301,6 +467,9 @@ ProcessSyncRequests(void)
 	uint64		longest = 0;
 	uint64		total_elapsed = 0;
 
+	/* See comment in SyncPostCheckpoint() above. */
+	SyncOps    *ops = syncsw;
+
 	/*
 	 * This is only called during checkpoints, and checkpoints should only
 	 * occur in processes that have created a pendingOps.
@@ -412,9 +581,12 @@ ProcessSyncRequests(void)
 			{
 				char		path[MAXPGPATH];
 
+				Assert(entry->tag.handler >= 0 &&
+					   entry->tag.handler < NSyncHandlers);
+
 				INSTR_TIME_SET_CURRENT(sync_start);
-				if (syncsw[entry->tag.handler].sync_syncfiletag(&entry->tag,
-																path) == 0)
+				if (ops[entry->tag.handler].sync_syncfiletag(&entry->tag,
+															 path) == 0)
 				{
 					/* Success; update statistics about sync timing */
 					INSTR_TIME_SET_CURRENT(sync_end);
@@ -506,13 +678,24 @@ RememberSyncRequest(const FileTag *ftag, SyncRequestType type)
 		HASH_SEQ_STATUS hstat;
 		PendingFsyncEntry *pfe;
 		ListCell   *cell;
+		bool		(*filetagmatches) (const FileTag *ftag,
+									   const FileTag *candidate);
+
+		Assert(ftag->handler >= 0 && ftag->handler < NSyncHandlers);
+
+		/*
+		 * Cache the per-handler filetagmatches function pointer once so
+		 * both match loops keep it in a register. See comment in
+		 * SyncPostCheckpoint().
+		 */
+		filetagmatches = syncsw[ftag->handler].sync_filetagmatches;
 
 		/* Cancel matching fsync requests */
 		hash_seq_init(&hstat, pendingOps);
 		while ((pfe = (PendingFsyncEntry *) hash_seq_search(&hstat)) != NULL)
 		{
 			if (pfe->tag.handler == ftag->handler &&
-				syncsw[ftag->handler].sync_filetagmatches(ftag, &pfe->tag))
+				filetagmatches(ftag, &pfe->tag))
 				pfe->canceled = true;
 		}
 
@@ -522,7 +705,7 @@ RememberSyncRequest(const FileTag *ftag, SyncRequestType type)
 			PendingUnlinkEntry *pue = (PendingUnlinkEntry *) lfirst(cell);
 
 			if (pue->tag.handler == ftag->handler &&
-				syncsw[ftag->handler].sync_filetagmatches(ftag, &pue->tag))
+				filetagmatches(ftag, &pue->tag))
 				pue->canceled = true;
 		}
 	}
diff --git a/src/include/storage/sync.h b/src/include/storage/sync.h
index 88290500bc9..5452eb714da 100644
--- a/src/include/storage/sync.h
+++ b/src/include/storage/sync.h
@@ -29,8 +29,13 @@ typedef enum SyncRequestType
 } SyncRequestType;
 
 /*
- * Which set of functions to use to handle a given request.  The values of
- * the enumerators must match the indexes of the function table in sync.c.
+ * Which set of functions to use to handle a given request.  Built-in
+ * handlers occupy the fixed enum values below; extensions register
+ * additional handlers via register_sync_handler() during
+ * shared_preload_libraries initialization and receive IDs starting
+ * at SYNC_HANDLER_FIRST_DYNAMIC. The values of the built-in
+ * enumerators must match the order in which InitSync() pre-registers
+ * the corresponding SyncOps structs in sync.c.
  */
 typedef enum SyncRequestHandler
 {
@@ -39,9 +44,19 @@ typedef enum SyncRequestHandler
 	SYNC_HANDLER_COMMIT_TS,
 	SYNC_HANDLER_MULTIXACT_OFFSET,
 	SYNC_HANDLER_MULTIXACT_MEMBER,
-	SYNC_HANDLER_NONE,
+
+	/* Extensions' dynamic handler IDs start here. */
+	SYNC_HANDLER_FIRST_DYNAMIC,
+
+	/*
+	 * Sentinel for "no handler": fits in int16, outside the valid ID
+	 * range so it cannot be confused with any registered handler.
+	 */
+	SYNC_HANDLER_NONE = -1,
 } SyncRequestHandler;
 
+#define SYNC_HANDLER_MAX	INT16_MAX
+
 /*
  * A tag identifying a file.  Currently it has the members required for md.c's
  * usage, but sync.c has no knowledge of the internal structure, and it is
@@ -55,6 +70,25 @@ typedef struct FileTag
 	uint64		segno;
 } FileTag;
 
+/*
+ * Dispatch table entry for a sync handler.  Public so extensions can
+ * define their own SyncOps and pass them to register_sync_handler().
+ *
+ * sync_syncfiletag is required.  sync_unlinkfiletag and
+ * sync_filetagmatches may be NULL if the handler does not support
+ * SYNC_UNLINK_REQUEST or SYNC_FILTER_REQUEST respectively, matching
+ * the pattern of the built-in CLOG/commit_ts/multixact handlers which
+ * only define sync_syncfiletag.
+ */
+typedef struct SyncOps
+{
+	int			(*sync_syncfiletag) (const FileTag *ftag, char *path);
+	int			(*sync_unlinkfiletag) (const FileTag *ftag, char *path);
+	bool		(*sync_filetagmatches) (const FileTag *ftag,
+										const FileTag *candidate);
+} SyncOps;
+
+extern void InitSyncHandlers(void);
 extern void InitSync(void);
 extern void SyncPreCheckpoint(void);
 extern void SyncPostCheckpoint(void);
@@ -63,4 +97,28 @@ extern void RememberSyncRequest(const FileTag *ftag, SyncRequestType type);
 extern bool RegisterSyncRequest(const FileTag *ftag, SyncRequestType type,
 								bool retryOnError);
 
+/*
+ * Register a custom sync handler.  Returns the assigned handler ID
+ * which the extension stores in FileTag.handler when queueing sync
+ * requests via RegisterSyncRequest().
+ *
+ * MUST be called during shared_preload_libraries initialization
+ * (before process_shared_preload_libraries_done is set); later calls
+ * raise FATAL.  `name` is used for error messages and is pstrdup'd
+ * into TopMemoryContext by the caller; callers do not need to keep
+ * the buffer alive.
+ *
+ * `ops->sync_syncfiletag` is required; the other two pointers may
+ * be NULL if the handler does not participate in SYNC_UNLINK_REQUEST
+ * or SYNC_FILTER_REQUEST flows.
+ *
+ * The returned ID is stable for the lifetime of the postmaster.
+ * Sync requests live only in the checkpointer's in-memory pendingOps
+ * hash table (they are not persisted across restarts), so there is
+ * no cross-restart stability requirement beyond the same
+ * shared_preload_libraries order that smgr_register() already relies
+ * on.
+ */
+extern int16 register_sync_handler(const SyncOps *ops, const char *name);
+
 #endif							/* SYNC_H */
-- 
2.47.3



  [application/octet-stream] v1-0002-Add-test-module-for-sync-handler-registration.patch (16.6K, 4-v1-0002-Add-test-module-for-sync-handler-registration.patch)
  download | inline diff:
From 47e81c9b6001f93041c52708b3c2c0a444194c41 Mon Sep 17 00:00:00 2001
From: Greg Lamberson <[email protected]>
Date: Fri, 10 Apr 2026 07:27:44 -0500
Subject: [PATCH v1 2/2] Add test module for sync handler registration

Adds src/test/modules/test_sync_handler, a minimal extension that
exercises the register_sync_handler() API introduced in the previous
commit. The module:

- Registers a trivial SyncOps via register_sync_handler() at
  _PG_init() time and stores the returned handler ID.
- Exposes SQL-callable test_sync_handler_register(seg bigint) that
  queues a FileTag via RegisterSyncRequest(SYNC_REQUEST).
- Tracks the number of sync_syncfiletag callback invocations in a
  shared-memory counter (via GetNamedDSMSegment) so that the
  checkpointer's increments are visible to the backend that calls
  test_sync_handler_count().

The TAP test in t/001_basic.pl verifies:

- The registered handler ID is >= SYNC_HANDLER_FIRST_DYNAMIC, proving
  that built-in handlers still occupy IDs 0..4.
- Queuing 5 distinct FileTags produces 5 callback invocations after
  CHECKPOINT, confirming that dispatch flows through the new dynamic
  table for extension-registered handlers.
- Queuing 10 identical FileTags produces only 1 additional callback
  invocation, confirming that HASH_BLOBS coalescing still works
  correctly with extension-assigned handler IDs.
- An idle CHECKPOINT (with no new queued entries) does not re-invoke
  the callback, confirming cycle_ctr skip semantics.

Module layout follows the pattern established by
src/test/modules/test_slru. The test sets fsync=on in its TAP cluster
config (overriding the default fsync=off that TAP clusters use for
speed) because ProcessSyncRequests skips dispatch entirely when
fsync is off.

This mirrors how fsync_checker is structured in CF 5616 v5+: a
minimal reference consumer that demonstrates the API surface, lives
in src/test/modules so installcheck users don't need to preload it,
and has a focused TAP test.

Signed-off-by: Greg Lamberson <[email protected]>
---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_sync_handler/.gitignore |   4 +
 src/test/modules/test_sync_handler/Makefile   |  27 +++
 .../modules/test_sync_handler/meson.build     |  33 ++++
 .../modules/test_sync_handler/t/001_basic.pl  |  96 +++++++++
 .../test_sync_handler--1.0.sql                |  13 ++
 .../test_sync_handler/test_sync_handler.c     | 187 ++++++++++++++++++
 .../test_sync_handler.control                 |   4 +
 9 files changed, 366 insertions(+)
 create mode 100644 src/test/modules/test_sync_handler/.gitignore
 create mode 100644 src/test/modules/test_sync_handler/Makefile
 create mode 100644 src/test/modules/test_sync_handler/meson.build
 create mode 100644 src/test/modules/test_sync_handler/t/001_basic.pl
 create mode 100644 src/test/modules/test_sync_handler/test_sync_handler--1.0.sql
 create mode 100644 src/test/modules/test_sync_handler/test_sync_handler.c
 create mode 100644 src/test/modules/test_sync_handler/test_sync_handler.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 0a74ab5c86f..2a3334d7508 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -52,6 +52,7 @@ SUBDIRS = \
 		  test_shmem \
 		  test_shm_mq \
 		  test_slru \
+		  test_sync_handler \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 4bca42bb370..00bc7454cc8 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -53,6 +53,7 @@ subdir('test_saslprep')
 subdir('test_shmem')
 subdir('test_shm_mq')
 subdir('test_slru')
+subdir('test_sync_handler')
 subdir('test_tidstore')
 subdir('typcache')
 subdir('unsafe_tests')
diff --git a/src/test/modules/test_sync_handler/.gitignore b/src/test/modules/test_sync_handler/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/src/test/modules/test_sync_handler/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_sync_handler/Makefile b/src/test/modules/test_sync_handler/Makefile
new file mode 100644
index 00000000000..22326a47e9c
--- /dev/null
+++ b/src/test/modules/test_sync_handler/Makefile
@@ -0,0 +1,27 @@
+# src/test/modules/test_sync_handler/Makefile
+
+MODULE_big = test_sync_handler
+OBJS = \
+	$(WIN32RES) \
+	test_sync_handler.o
+PGFILEDESC = "test_sync_handler - test module for sync handler registration"
+
+EXTENSION = test_sync_handler
+DATA = test_sync_handler--1.0.sql
+
+TAP_TESTS = 1
+
+# Tests require shared_preload_libraries=test_sync_handler which typical
+# installcheck users do not have. Match test_slru's convention.
+NO_INSTALLCHECK = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_sync_handler
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_sync_handler/meson.build b/src/test/modules/test_sync_handler/meson.build
new file mode 100644
index 00000000000..e7f03616ba0
--- /dev/null
+++ b/src/test/modules/test_sync_handler/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_sync_handler_sources = files(
+  'test_sync_handler.c',
+)
+
+if host_system == 'windows'
+  test_sync_handler_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_sync_handler',
+    '--FILEDESC', 'test_sync_handler - test module for sync handler registration',])
+endif
+
+test_sync_handler = shared_module('test_sync_handler',
+  test_sync_handler_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_sync_handler
+
+test_install_data += files(
+  'test_sync_handler.control',
+  'test_sync_handler--1.0.sql',
+)
+
+tests += {
+  'name': 'test_sync_handler',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_basic.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_sync_handler/t/001_basic.pl b/src/test/modules/test_sync_handler/t/001_basic.pl
new file mode 100644
index 00000000000..29c0fc3c61e
--- /dev/null
+++ b/src/test/modules/test_sync_handler/t/001_basic.pl
@@ -0,0 +1,96 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+#
+# Basic test for register_sync_handler() dispatch.
+#
+# Verifies that a custom sync handler registered via register_sync_handler()
+# in _PG_init() receives callback invocations from ProcessSyncRequests() at
+# CHECKPOINT time, that identical FileTags coalesce via HASH_BLOBS
+# deduplication, that distinct FileTags produce distinct callbacks, and
+# that an idle checkpoint does not re-dispatch entries that were already
+# processed (cycle_ctr skip).
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('sync_handler');
+$node->init;
+$node->append_conf(
+	'postgresql.conf', q{
+shared_preload_libraries = 'test_sync_handler'
+# TAP clusters set fsync = off by default for speed; re-enable here so
+# that ProcessSyncRequests actually dispatches our sync handler callback.
+fsync = on
+});
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_sync_handler');
+
+# The handler ID must be >= SYNC_HANDLER_FIRST_DYNAMIC. Built-ins
+# currently occupy IDs 0..4, so the first extension handler should be
+# at least 5.
+my $id = $node->safe_psql('postgres', 'SELECT test_sync_handler_id()');
+ok($id >= 5,
+	"handler id $id is >= SYNC_HANDLER_FIRST_DYNAMIC (built-ins = 5)")
+  or diag("got id=$id");
+
+# Baseline: no dispatches before we queue anything.
+my $baseline =
+  $node->safe_psql('postgres', 'SELECT test_sync_handler_count()');
+is($baseline, '0', 'baseline dispatch count is zero');
+
+# Queue 5 distinct FileTags (differing in segno only) and checkpoint.
+# Expect 5 callback invocations since they are all distinct hash keys.
+$node->safe_psql(
+	'postgres', q{
+SELECT test_sync_handler_register(1);
+SELECT test_sync_handler_register(2);
+SELECT test_sync_handler_register(3);
+SELECT test_sync_handler_register(4);
+SELECT test_sync_handler_register(5);
+});
+$node->safe_psql('postgres', 'CHECKPOINT');
+my $after_distinct =
+  $node->safe_psql('postgres', 'SELECT test_sync_handler_count()');
+is($after_distinct, '5',
+	'5 distinct FileTags produce 5 sync_syncfiletag callbacks')
+  or diag("got $after_distinct");
+
+# Queue 10 duplicate FileTags (same segno 42) and checkpoint.
+# Expect exactly 1 additional callback because pendingOps uses HASH_BLOBS
+# and collapses identical FileTags into a single hash entry.
+$node->safe_psql(
+	'postgres', q{
+SELECT test_sync_handler_register(42);
+SELECT test_sync_handler_register(42);
+SELECT test_sync_handler_register(42);
+SELECT test_sync_handler_register(42);
+SELECT test_sync_handler_register(42);
+SELECT test_sync_handler_register(42);
+SELECT test_sync_handler_register(42);
+SELECT test_sync_handler_register(42);
+SELECT test_sync_handler_register(42);
+SELECT test_sync_handler_register(42);
+});
+$node->safe_psql('postgres', 'CHECKPOINT');
+my $after_coalesce =
+  $node->safe_psql('postgres', 'SELECT test_sync_handler_count()');
+is($after_coalesce, '6',
+	'10 duplicate FileTags coalesce via HASH_BLOBS to 1 additional callback')
+  or diag("got $after_coalesce");
+
+# Second CHECKPOINT with no new requests. The count must stay the same:
+# every entry from the previous checkpoint was processed and removed
+# from pendingOps, and no new entries have been queued, so
+# ProcessSyncRequests has nothing to dispatch.
+$node->safe_psql('postgres', 'CHECKPOINT');
+my $after_idle =
+  $node->safe_psql('postgres', 'SELECT test_sync_handler_count()');
+is($after_idle, '6', 'idle checkpoint does not re-dispatch')
+  or diag("got $after_idle");
+
+$node->stop;
+
+done_testing();
diff --git a/src/test/modules/test_sync_handler/test_sync_handler--1.0.sql b/src/test/modules/test_sync_handler/test_sync_handler--1.0.sql
new file mode 100644
index 00000000000..07ea297f15f
--- /dev/null
+++ b/src/test/modules/test_sync_handler/test_sync_handler--1.0.sql
@@ -0,0 +1,13 @@
+/* src/test/modules/test_sync_handler/test_sync_handler--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_sync_handler" to load this file. \quit
+
+CREATE FUNCTION test_sync_handler_id() RETURNS int4
+  AS 'MODULE_PATHNAME', 'test_sync_handler_id' LANGUAGE C STRICT;
+
+CREATE FUNCTION test_sync_handler_register(seg bigint) RETURNS void
+  AS 'MODULE_PATHNAME', 'test_sync_handler_register' LANGUAGE C STRICT;
+
+CREATE FUNCTION test_sync_handler_count() RETURNS bigint
+  AS 'MODULE_PATHNAME', 'test_sync_handler_count' LANGUAGE C STRICT;
diff --git a/src/test/modules/test_sync_handler/test_sync_handler.c b/src/test/modules/test_sync_handler/test_sync_handler.c
new file mode 100644
index 00000000000..055d90b55de
--- /dev/null
+++ b/src/test/modules/test_sync_handler/test_sync_handler.c
@@ -0,0 +1,187 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_sync_handler.c
+ *		Minimal extension exercising register_sync_handler() + dispatch.
+ *
+ * This module demonstrates the sync.c extensibility API by registering a
+ * trivial SyncOps during _PG_init(), exposing SQL-callable helpers to
+ * queue FileTags for the registered handler, and tracking how many times
+ * the handler's sync_syncfiletag callback is invoked.
+ *
+ * Because sync_syncfiletag runs in the checkpointer process but
+ * test_sync_handler_count() runs in a regular backend, the call counter
+ * lives in shared memory via GetNamedDSMSegment().
+ *
+ * The TAP test in t/001_basic.pl uses this module to verify:
+ *   - register_sync_handler() returns an ID >= SYNC_HANDLER_FIRST_DYNAMIC
+ *   - Queued FileTags round-trip through the checkpointer and land in
+ *     the registered sync_syncfiletag callback at CHECKPOINT time
+ *   - Identical FileTags coalesce via HASH_BLOBS deduplication in
+ *     pendingOps (N duplicates -> 1 callback)
+ *   - Distinct FileTags produce distinct callbacks
+ *   - Idle checkpoints do not re-dispatch (cycle_ctr skip)
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_sync_handler/test_sync_handler.c
+ *
+ *--------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "pg_config.h"
+#include "port/atomics.h"
+#include "storage/dsm_registry.h"
+#include "storage/sync.h"
+#include "utils/builtins.h"
+
+PG_MODULE_MAGIC;
+
+void		_PG_init(void);
+
+typedef struct TshSharedState
+{
+	pg_atomic_uint64 call_count;
+} TshSharedState;
+
+static int16 tsh_handler_id = -1;
+static TshSharedState *tsh_shared = NULL;
+
+/*
+ * GetNamedDSMSegment's init_callback signature gained an extra `arg`
+ * parameter in PG 19devel. Provide both shapes so the test module is
+ * buildable across 18 and 19.
+ */
+#if PG_VERSION_NUM >= 190000
+static void
+tsh_init_shmem(void *ptr, void *arg)
+#else
+static void
+tsh_init_shmem(void *ptr)
+#endif
+{
+	TshSharedState *state = (TshSharedState *) ptr;
+
+	pg_atomic_init_u64(&state->call_count, 0);
+}
+
+static void
+tsh_attach_shmem(void)
+{
+	bool		found;
+
+	if (tsh_shared != NULL)
+		return;
+#if PG_VERSION_NUM >= 190000
+	tsh_shared = GetNamedDSMSegment("test_sync_handler",
+									sizeof(TshSharedState),
+									tsh_init_shmem,
+									&found,
+									NULL);
+#else
+	tsh_shared = GetNamedDSMSegment("test_sync_handler",
+									sizeof(TshSharedState),
+									tsh_init_shmem,
+									&found);
+#endif
+}
+
+static int
+test_sync_syncfiletag(const FileTag *ftag, char *path)
+{
+	/*
+	 * This runs in the checkpointer process. Attach to the shared
+	 * memory segment the first time we're called so that counter
+	 * increments are visible to the backend that queries
+	 * test_sync_handler_count().
+	 */
+	tsh_attach_shmem();
+	pg_atomic_fetch_add_u64(&tsh_shared->call_count, 1);
+
+	if (path != NULL)
+		snprintf(path, MAXPGPATH, "test_sync_handler:seg%llu",
+				 (unsigned long long) ftag->segno);
+	return 0;
+}
+
+static int
+test_sync_unlinkfiletag(const FileTag *ftag, char *path)
+{
+	if (path != NULL)
+		snprintf(path, MAXPGPATH, "test_sync_handler:unlink");
+	return 0;
+}
+
+static bool
+test_sync_filetagmatches(const FileTag *ftag, const FileTag *candidate)
+{
+	/*
+	 * Match on dbOid, mirroring md.c's DROP DATABASE semantics. The
+	 * test doesn't exercise the filter path today, but the callback
+	 * is defined so extensions can use this module as a copy-paste
+	 * starting point.
+	 */
+	return ftag->rlocator.dbOid == candidate->rlocator.dbOid;
+}
+
+static const SyncOps test_sync_ops = {
+	.sync_syncfiletag = test_sync_syncfiletag,
+	.sync_unlinkfiletag = test_sync_unlinkfiletag,
+	.sync_filetagmatches = test_sync_filetagmatches,
+};
+
+void
+_PG_init(void)
+{
+	tsh_handler_id = register_sync_handler(&test_sync_ops, "test_sync_handler");
+	elog(LOG, "test_sync_handler: registered as id %d",
+		 (int) tsh_handler_id);
+}
+
+PG_FUNCTION_INFO_V1(test_sync_handler_id);
+Datum
+test_sync_handler_id(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32((int32) tsh_handler_id);
+}
+
+PG_FUNCTION_INFO_V1(test_sync_handler_register);
+Datum
+test_sync_handler_register(PG_FUNCTION_ARGS)
+{
+	int64		seg = PG_GETARG_INT64(0);
+	FileTag		tag;
+
+	if (tsh_handler_id < 0)
+		ereport(ERROR,
+				(errmsg("test_sync_handler: registration failed at _PG_init")));
+
+	/*
+	 * Mandatory memset: pendingOps uses HASH_BLOBS which hashes every
+	 * byte of the FileTag. Uninitialized padding would break coalescing.
+	 */
+	memset(&tag, 0, sizeof(FileTag));
+	tag.handler = tsh_handler_id;
+	tag.forknum = 0;
+	tag.rlocator.spcOid = 1;
+	tag.rlocator.dbOid = MyDatabaseId;
+	tag.rlocator.relNumber = 1;
+	tag.segno = (uint64) seg;
+
+	if (!RegisterSyncRequest(&tag, SYNC_REQUEST, true /* retryOnError */))
+		ereport(ERROR,
+				(errmsg("test_sync_handler: RegisterSyncRequest failed")));
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_sync_handler_count);
+Datum
+test_sync_handler_count(PG_FUNCTION_ARGS)
+{
+	tsh_attach_shmem();
+	PG_RETURN_INT64((int64) pg_atomic_read_u64(&tsh_shared->call_count));
+}
diff --git a/src/test/modules/test_sync_handler/test_sync_handler.control b/src/test/modules/test_sync_handler/test_sync_handler.control
new file mode 100644
index 00000000000..3d528f7a866
--- /dev/null
+++ b/src/test/modules/test_sync_handler/test_sync_handler.control
@@ -0,0 +1,4 @@
+comment = 'Test module for sync handler registration'
+default_version = '1.0'
+module_pathname = '$libdir/test_sync_handler'
+relocatable = true
-- 
2.47.3



view thread (2+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected]
  Subject: Re: Extensible sync handler registration (register_sync_handler)
  In-Reply-To: <IA1PR07MB983072521EE7FDEE98902534A9592@IA1PR07MB9830.namprd07.prod.outlook.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox