v2 attached. While running the v1 patches under CFBot (and
subsequently reproducing locally with CPPFLAGS=-DEXEC_BACKEND on
Linux), I discovered that v1 mis-handles processes that do not
inherit the postmaster's address space via fork(). v2 fixes this
and also addresses several unrelated items that I should have
caught in v1 self-review.
Changes in v2-0001:
* EXEC_BACKEND load-order fix. On platforms without fork()
(Windows, plus any build with CPPFLAGS=-DEXEC_BACKEND), each
child process re-runs process_shared_preload_libraries() in its
own fresh address space. In v1, extension _PG_init() could
reach register_sync_handler() before the child had called
InitSync(), so the first dynamic registration landed at ID 0,
colliding with SYNC_HANDLER_MD, and the idempotent guard in
InitSyncHandlers() ("if NSyncHandlers != 0 return") then
suppressed built-in registration entirely for the rest of that
process's lifetime. v2 calls InitSyncHandlers() at the top of
register_sync_handler() and replaces the counter-based guard
with an explicit builtin_sync_handlers_registered flag. The
v1 test that reported "got id=0" on Windows and FreeBSD CFBot
now reports id=5 under both fork and EXEC_BACKEND.
* Documentation. v1 shipped without SGML docs; v2 adds
doc/src/sgml/custom-sync-handler.sgml (modeled on
custom-rmgr.sgml) and registers it in filelist.sgml and
postgres.sgml. The doc build is clean.
* Error-message style. The four errmsg() strings that embedded
function names ("register_sync_handler: ...",
"test_sync_handler: ...") are reworded per the error-message
style guide. The two developer-bug paths that used
ERRCODE_NULL_VALUE_NOT_ALLOWED are changed to elog(FATAL, ...)
since they cannot be triggered from SQL.
* Stale comment. The v1 comment in
sync_handler_register_internal() claiming "fork() is full
POSIX barrier" was accurate only on fork-based platforms.
Rewritten to cover both fork and EXEC_BACKEND paths.
* SYNC_HANDLER_NONE enum value. Previously implicit 5 (after
MULTIXACT_MEMBER=4), changed in v1 to explicit -1 so that a
sentinel "no handler" value cannot be confused with a valid
index. I audited uses in master: the only references are
!= comparisons in slru.c at lines 1057, 1442, and 1558, which
are value-agnostic. Flagging explicitly here because it is
an ABI-visible enum value change.
Changes in v2-0002:
* Error-message style fixes in the test module to match the
core-side cleanups above.
* pgindent pass (required adding TshSharedState via
--list-of-typedefs since it is a test-module-local type).
Verification for v2:
* make check-world under autoconf on Linux, fork-based: all PASS
* make check-world under autoconf on Linux,
CPPFLAGS=-DEXEC_BACKEND: all PASS
* meson test under Linux with c_args='-DEXEC_BACKEND':
344 OK / 34 SKIP / 0 FAIL
* test_sync_handler/001_basic: 5/5 under all four combinations
* src/test/recovery: 597/597 under -DEXEC_BACKEND
* test_slru: 18/18 under -DEXEC_BACKEND (SLRU is the main user
of SYNC_HANDLER_NONE; the enum value change is safe)
* pgindent, pgperltidy (20230309), pgperlcritic: clean
* headerscheck on src/include/storage/sync.h in regular and
--cplusplus modes: clean
* doc/src/sgml builds cleanly; new chapter renders as expected
I apologize for the v1 verification gap. v1's "make check-world
green" claim was accurate on fork-based Linux only and did not
exercise EXEC_BACKEND; that is the single most important reason
the Windows failure slipped through. My pre-submission checklist
now includes the -DEXEC_BACKEND path.
Thanks,
Greg
From cd1cd0e668d6f7d7f315fd2e6aa41b81c9a4b724 Mon Sep 17 00:00:00 2001
From: Greg Lamberson <[email protected]>
Date: Fri, 10 Apr 2026 07:27:14 -0500
Subject: [PATCH v2 1/2] Make sync.c syncsw[] extensible via
register_sync_handler()
Introduce a public extension API, register_sync_handler(), that lets
extensions install their own entries in the sync.c dispatch table.
This enables storage-related extensions to participate in the
checkpoint fsync pipeline without faking md.c segments or bypassing
sync.c's request coalescing and cancellation machinery.
The previously static syncsw[] array becomes a heap-allocated
dispatch table populated in two phases: the five built-in handlers
(MD, CLOG, commit_ts, multixact_offset, multixact_member) are
registered via InitSyncHandlers() before process_shared_preload_-
libraries(), and extension _PG_init() calls receive sequentially
assigned IDs starting at SYNC_HANDLER_FIRST_DYNAMIC. Registration
is forbidden after process_shared_preload_libraries_done is set.
InitSyncHandlers() is called from both PostmasterMain() (for the
fork() path) and from register_sync_handler() itself (for the
EXEC_BACKEND path, where each child re-runs shared_preload_libraries
in its own address space and may reach an extension's registration
call before it has called InitSync()). An explicit
builtin_sync_handlers_registered flag guards against repeated
built-in registration.
SYNC_HANDLER_NONE is changed from its previous implicit value of 5
to an explicit -1 so that the "no handler" sentinel cannot be
confused with a valid handler index. The only consumers in
core are value-agnostic != comparisons in slru.c.
Documentation: doc/src/sgml/custom-sync-handler.sgml, modeled on
doc/src/sgml/custom-rmgr.sgml.
Discussion: https://postgr.es/m/ia1pr07mb983072521ee7fdee98902534a9...@ia1pr07mb9830.namprd07.prod.outlook.com
---
doc/src/sgml/custom-sync-handler.sgml | 118 +++++++++++
doc/src/sgml/filelist.sgml | 1 +
doc/src/sgml/postgres.sgml | 1 +
src/backend/postmaster/postmaster.c | 11 +
src/backend/storage/sync/sync.c | 278 ++++++++++++++++++++++----
src/include/storage/sync.h | 64 +++++-
6 files changed, 432 insertions(+), 41 deletions(-)
create mode 100644 doc/src/sgml/custom-sync-handler.sgml
diff --git a/doc/src/sgml/custom-sync-handler.sgml b/doc/src/sgml/custom-sync-handler.sgml
new file mode 100644
index 00000000000..6d95efe7440
--- /dev/null
+++ b/doc/src/sgml/custom-sync-handler.sgml
@@ -0,0 +1,118 @@
+<!-- doc/src/sgml/custom-sync-handler.sgml -->
+
+<chapter id="custom-sync-handler">
+ <title>Custom Sync Handlers for Extensions</title>
+
+ <para>
+ This chapter explains the interface between the core
+ <productname>PostgreSQL</productname> system and custom sync handlers,
+ which enable extensions to participate in the checkpoint
+ <function>fsync</function> pipeline implemented in
+ <filename>src/backend/storage/sync/sync.c</filename>.
+ </para>
+
+ <para>
+ Extensions that manage storage outside the standard relation-file layout,
+ such as a <link linkend="tableam">Table Access Method</link> that stores
+ its data in a non-file format, may need their data to be
+ <function>fsync</function>ed at checkpoint time in the same manner as the
+ built-in handlers do for relation segments, <acronym>CLOG</acronym>,
+ <structname>commit_ts</structname>, and multixact data. A custom sync
+ handler lets an extension register its own sync callback, participate in
+ the same request coalescing and cancellation mechanisms, and benefit from
+ the checkpointer's batching and <varname>cycle_ctr</varname> semantics
+ without reimplementing the machinery or faking its data as
+ <function>md.c</function> segments.
+ </para>
+
+ <para>
+ To create a custom sync handler, first define a
+ <structname>SyncOps</structname> structure containing the handler
+ callbacks. The structure is defined in
+ <filename>src/include/storage/sync.h</filename>:
+<programlisting>
+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;
+</programlisting>
+ Only <structfield>sync_syncfiletag</structfield> is required; the other
+ two pointers may be <literal>NULL</literal> if the handler does not
+ participate in <literal>SYNC_UNLINK_REQUEST</literal> or
+ <literal>SYNC_FILTER_REQUEST</literal> flows. This mirrors the built-in
+ handlers for <acronym>CLOG</acronym>, <structname>commit_ts</structname>,
+ and multixact data, which only define
+ <structfield>sync_syncfiletag</structfield>.
+ </para>
+
+ <para>
+ Then, register the handler and record the returned handler ID:
+<programlisting>
+extern int16 register_sync_handler(const SyncOps *ops, const char *name);
+</programlisting>
+ <function>register_sync_handler</function> must be called from the
+ extension module's <link linkend="xfunc-c-dynload">_PG_init</link>
+ function while <varname>shared_preload_libraries</varname> is still being
+ loaded; calls made after that phase has completed raise
+ <literal>FATAL</literal>. The extension must therefore be placed in
+ <xref linkend="guc-shared-preload-libraries"/>.
+ </para>
+
+ <para>
+ The returned <type>int16</type> handler ID is the value the extension
+ stores in <structfield>FileTag.handler</structfield> when queuing sync
+ requests via <function>RegisterSyncRequest</function>. Extension handler
+ IDs are assigned sequentially starting at
+ <literal>SYNC_HANDLER_FIRST_DYNAMIC</literal>, which is the first value
+ after the built-in handler IDs <literal>SYNC_HANDLER_MD</literal>,
+ <literal>SYNC_HANDLER_CLOG</literal>,
+ <literal>SYNC_HANDLER_COMMIT_TS</literal>,
+ <literal>SYNC_HANDLER_MULTIXACT_OFFSET</literal>, and
+ <literal>SYNC_HANDLER_MULTIXACT_MEMBER</literal>. The assigned ID is
+ stable for the lifetime of a given server configuration, that is, it
+ does not change between backends, the checkpointer, or auxiliary
+ processes within a single postmaster lifetime. Because sync requests
+ live only in the checkpointer's in-memory pending-operations hash table
+ and are not persisted across server restarts, the assigned ID does not
+ need to be stable across restarts or across changes to
+ <varname>shared_preload_libraries</varname>.
+ </para>
+
+ <para>
+ The <structname>FileTag</structname> structure passed to the handler
+ callbacks has a small fixed layout that all handlers share. Its
+ contents are opaque to <filename>sync.c</filename>; each handler
+ interprets the fields according to its own convention. Because
+ <filename>sync.c</filename> deduplicates pending sync requests by
+ hashing the raw bytes of the <structname>FileTag</structname>
+ (<literal>HASH_BLOBS</literal>), every field including any padding
+ must be zeroed before the structure is populated, otherwise logically
+ identical tags with different padding bytes will not coalesce into a
+ single callback invocation. A simple <function>memset</function> to
+ zero before assignment is sufficient.
+ </para>
+
+ <para>
+ The <filename>src/test/modules/test_sync_handler</filename> module
+ contains a minimal working example, which demonstrates registration
+ from <function>_PG_init</function>, the per-checkpoint callback
+ dispatch, request coalescing via <literal>HASH_BLOBS</literal>, and
+ the <varname>cycle_ctr</varname> skip behaviour on idle checkpoints.
+ The TAP test in that module also serves as a copy-paste starting point
+ for new sync-handler extensions.
+ </para>
+
+ <note>
+ <para>
+ The extension must remain in <varname>shared_preload_libraries</varname>
+ as long as any data managed by its sync handler may require
+ checkpointing. If the extension is removed while such data exists,
+ <productname>PostgreSQL</productname> will not be able to dispatch
+ pending sync requests for that data, which may lead to durability
+ issues at the next checkpoint.
+ </para>
+ </note>
+</chapter>
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 25a85082759..c6a4f1745ae 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -113,6 +113,7 @@
<!ENTITY wal-for-extensions SYSTEM "wal-for-extensions.sgml">
<!ENTITY generic-wal SYSTEM "generic-wal.sgml">
<!ENTITY custom-rmgr SYSTEM "custom-rmgr.sgml">
+<!ENTITY custom-sync-handler SYSTEM "custom-sync-handler.sgml">
<!ENTITY backup-manifest SYSTEM "backup-manifest.sgml">
<!ENTITY oauth-validators SYSTEM "oauth-validators.sgml">
diff --git a/doc/src/sgml/postgres.sgml b/doc/src/sgml/postgres.sgml
index 2101442c90f..c91877c8dd8 100644
--- a/doc/src/sgml/postgres.sgml
+++ b/doc/src/sgml/postgres.sgml
@@ -259,6 +259,7 @@ break is not needed in a wider output rendering.
&tableam;
&indexam;
&wal-for-extensions;
+ &custom-sync-handler;
&indextypes;
&storage;
&transaction;
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..ff6239680cf 100644
--- a/src/backend/storage/sync/sync.c
+++ b/src/backend/storage/sync/sync.c
@@ -80,50 +80,219 @@ 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 InitSyncHandlers() for the five built-in handlers (MD,
+ * CLOG, commit_ts, multixact_offset, multixact_member) and by
+ * register_sync_handler() for handlers installed by extensions from
+ * their _PG_init() function. After shared_preload_libraries has
+ * finished loading, syncsw[] is effectively immutable for the life
+ * of the process.
+ *
+ * Every process that can call into sync.c (postmaster, backends, and
+ * the checkpointer and other auxiliary processes) obtains its own
+ * populated syncsw[] either by inheriting it via fork() from the
+ * postmaster, or, on EXEC_BACKEND platforms where there is no fork(),
+ * by re-running InitSyncHandlers() and process_shared_preload_libraries()
+ * in its own address space during startup.
+ *
+ * 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;
+static bool builtin_sync_handlers_registered = false;
/*
- * 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)
+ elog(FATAL, "sync handler registration requires a non-NULL sync callback");
+
+ if (name == NULL || *name == '\0')
+ elog(FATAL, "sync handler name must not be empty");
+
+ if (NSyncHandlers >= SYNC_HANDLER_MAX)
+ ereport(FATAL,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("too many sync handlers registered (maximum is %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. On fork-based
+ * platforms backends and auxiliary processes inherit the fully-populated
+ * array from the postmaster via fork(). On EXEC_BACKEND platforms each
+ * child repeats the single-threaded registration sequence in its own
+ * address space during startup. In either case, the array is immutable by
+ * the time any concurrent reader can observe it.
+ */
+ return my_id;
+}
+
+/*
+ * Public registration entry point for extensions. See sync.h for the
+ * contract.
+ *
+ * Extensions must call this from their _PG_init() while
+ * shared_preload_libraries is still being processed; later calls raise
+ * FATAL. Built-in handlers bypass this guard via
+ * sync_handler_register_internal() because the checkpointer and other
+ * auxiliary processes call InitSync() after preload has finished, and
+ * the built-in dispatch table must still be populated in those
+ * processes.
+ *
+ * On EXEC_BACKEND platforms each child process repeats
+ * process_shared_preload_libraries() in its own fresh address space
+ * during startup, and an extension's _PG_init() can reach this
+ * function before the child has called InitSync(). Call
+ * InitSyncHandlers() here to ensure the five built-in handlers always
+ * occupy IDs 0..SYNC_HANDLER_FIRST_DYNAMIC-1 before any dynamic ID is
+ * assigned, which keeps handler IDs consistent across every process
+ * that dispatches sync requests. InitSyncHandlers() is idempotent.
+ */
+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 handler registration is only permitted while shared_preload_libraries is being loaded")));
+
+ InitSyncHandlers();
+
+ 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 (builtin_sync_handlers_registered)
+ 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");
+
+ builtin_sync_handlers_registered = true;
+
+ /*
+ * 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 +374,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 +409,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 +486,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 +600,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 +697,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 +724,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..959a4f72a52 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
From a9566b9110b53491f792a61c83f5d76b01fb029f Mon Sep 17 00:00:00 2001
From: Greg Lamberson <[email protected]>
Date: Fri, 10 Apr 2026 07:27:44 -0500
Subject: [PATCH v2 2/2] Add test module for sync handler registration
test_sync_handler exercises register_sync_handler() from _PG_init()
and verifies:
- The registered handler ID is at least SYNC_HANDLER_FIRST_DYNAMIC.
- Distinct FileTags produce distinct sync_syncfiletag callbacks
at CHECKPOINT time.
- Duplicate FileTags coalesce via HASH_BLOBS to a single dispatch.
- Idle checkpoints do not re-dispatch already-processed entries
(cycle_ctr skip).
Shared state between the backend and the checkpointer uses
GetNamedDSMSegment() so the dispatch counter is visible to the
backend that queries it.
Discussion: https://postgr.es/m/ia1pr07mb983072521ee7fdee98902534a9...@ia1pr07mb9830.namprd07.prod.outlook.com
---
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..b2cd25cc18d
--- /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,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("sync handler was not registered during module initialization")));
+
+ /*
+ * 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,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("could not register sync request")));
+
+ 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