Hi,

This patch implements async fsyncs for crash recovery and checkpoints
by using the AIO system. I got the idea from Andres' older work [1].

----------

High level design and implementation details:

- Since there is no interface like read-stream for the fsyncs, each
user has its own AIO functions like register_one(), drain_one() and
drain_all().

- Number of in-flight I/Os are determined by
GetFsyncConcurrencyLimit(). It uses both io_max_concurrency and the
file descriptor budget to determine max number of available in-flight
fsync I/Os.

- I/O workers need re-open files to do async I/O. Some targets open
files by path (for example, SyncDataDirectory()). This path needs to
be copied to shared memory so that worker processes can re-open these
files. However, adding 'char path[MAXPGPATH]' to PgAioTargetData seems
overkill because of the size; so it is not implemented for now. I am
open to suggestions.

----------

Patches:

- 0001 is just a base patch for adding fsync support to the AIO
system. There is no user of this patch yet.

- 0002 makes SyncDataDirectory() use async fsyncs. Its files are
identified only by path; I/O workers cannot re-open them. So, this
path is asynchronous with io_uring, while worker behaves like sync.
pre_sync_fname() doesn't use AIO because it is already very fast, less
than 1/1000 of of datadir_fsync_fname() time in my benchmarks.

- 0003 makes ProcessSyncRequests() use async fsyncs. Its files use the
smgr target and can be re-opened by an I/O worker, so both worker and
io_uring can execute these fsyncs asynchronously.

- 0004 adds a FileTag-based target for SLRU files. This gives I/O
workers enough information to re-open SLRU segments, so they can use
async fsyncs in the worker mode.

- benchmark-repro.sh is a benchmark script that I used.

----------

Benchmark:

I used a simple benchmark script generated by an LLM. I checked the
script, and it works correctly. The numbers below use the default
settings + the following:

```
checkpoint_timeout = '1h'
max_wal_size = '32GB'
checkpoint_flush_after = 0
bgwriter_lru_maxpages = 0
autovacuum = off
```
io_min_workers, io_max_workers and io_max_concurrency are default in
[2]. They are changed to 8, 8, and 64, respectively, for the second
benchmark [3].

You can run the attached benchmark script by './benchmark-repro.sh
{--syncdatadirectory, --checkpointer, --slru}' command.

- SyncDataDirectory() synced a data directory containing 4000 small relations:

[2]
io_method  |         startup (ms) | speedup vs sync
-----------+----------------------+----------------
sync       |               1411.8 |           1.00x
worker     |               1210.6 |           1.17x
io_uring   |                310.3 |           4.55

[3]
io_method  |         startup (ms) | speedup vs sync
-----------+----------------------+----------------
sync       |               1310.6 |           1.00x
worker     |               1210.0 |           1.08x
io_uring   |                310.6 |           4.22x

- Checkpointer benchmark dirtied 4000 relation files before checkpoint:

[2]
io_method  |      checkpoint (ms) | speedup vs sync
-----------+----------------------+----------------
sync       |               1860.5 |           1.00x
worker     |                697.1 |           2.67x
io_uring   |                478.4 |           3.89x

[3]
io_method  |      checkpoint (ms) | speedup vs sync
-----------+----------------------+----------------
sync       |               1875.5 |           1.00x
worker     |                541.1 |           3.47x
io_uring   |                479.9 |           3.91x

- SLRU benchmark created 640000 transactions, producing ~30 files to sync:

[2]
io_method  | SLRU checkpoint (ms) | speedup vs sync
-----------+----------------------+----------------
sync       |                 94.1 |           1.00x
worker     |                 67.1 |           1.40x
io_uring   |                 57.1 |           1.65x

[3]
io_method  | SLRU checkpoint (ms) | speedup vs sync
-----------+----------------------+----------------
sync       |                 97.3 |           1.00x
worker     |                 48.7 |           2.00x
io_uring   |                 37.4 |           2.60x

Any feedback would be appreciated.

[1]
https://github.com/anarazel/postgres/commit/9829a1f18176e572759ecf8b8bb99205a71a476f
https://github.com/anarazel/postgres/commit/2e180547aef6fe2a409cc144e04cbc2f0a607bed

--
Regards,
Nazir Bilal Yavuz
Microsoft
From d3197f4e6fb6a24d5ea35448b521edeca9717aeb Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <[email protected]>
Date: Tue, 11 Aug 2026 10:40:14 +0300
Subject: [PATCH v1 1/4] aio: Add fsync support

The AIO subsystem currently supports only reads and writes (writes are
not used yet). Add PGAIO_OP_FSYNC so callers can submit fsync() and
fdatasync() operations through AIO. This allows callers such as the
checkpointer to keep multiple syncs in flight instead of waiting for
each one in turn.

Let callers select the wait event because fsync targets can represent
different kinds of files. The process that performs the operation
reports that event; io_uring uses the generic AIO wait events because
the kernel performs the operation. (Also see [1])

No callers are converted in this commit; subsequent commits do that.

[1] https://postgr.es/m/can55fz0rp+94rdq-zetx0sf9h1e4uwzxh8r1cvxzx0swfqw...@mail.gmail.com
---
 src/backend/storage/aio/aio_funcs.c       |  4 ++++
 src/backend/storage/aio/aio_io.c          | 27 +++++++++++++++++++++++
 src/backend/storage/aio/method_io_uring.c |  6 +++++
 src/backend/storage/smgr/smgr.c           |  3 +++
 src/include/storage/aio.h                 | 14 ++++++++++--
 5 files changed, 52 insertions(+), 2 deletions(-)

diff --git a/src/backend/storage/aio/aio_funcs.c b/src/backend/storage/aio/aio_funcs.c
index bcdd82318f7..2719556e858 100644
--- a/src/backend/storage/aio/aio_funcs.c
+++ b/src/backend/storage/aio/aio_funcs.c
@@ -191,6 +191,10 @@ retry:
 				values[6] =
 					Int64GetDatum(iov_byte_length(iov_copy, ioh_copy.op_data.write.iov_length));
 				break;
+			case PGAIO_OP_FSYNC:
+				nulls[5] = true;
+				nulls[6] = true;
+				break;
 		}
 
 		/* column: IO's target */
diff --git a/src/backend/storage/aio/aio_io.c b/src/backend/storage/aio/aio_io.c
index 132868130e7..324fb6911e2 100644
--- a/src/backend/storage/aio/aio_io.c
+++ b/src/backend/storage/aio/aio_io.c
@@ -100,6 +100,19 @@ pgaio_io_start_writev(PgAioHandle *ioh,
 	pgaio_io_stage(ioh, PGAIO_OP_WRITEV);
 }
 
+void
+pgaio_io_start_fsync(PgAioHandle *ioh,
+					 int fd, bool datasync, uint32 wait_event_info)
+{
+	pgaio_io_before_start(ioh);
+
+	ioh->op_data.fsync.fd = fd;
+	ioh->op_data.fsync.datasync = datasync;
+	ioh->op_data.fsync.wait_event_info = wait_event_info;
+
+	pgaio_io_stage(ioh, PGAIO_OP_FSYNC);
+}
+
 
 
 /* --------------------------------------------------------------------------------
@@ -137,6 +150,14 @@ pgaio_io_perform_synchronously(PgAioHandle *ioh)
 								ioh->op_data.write.offset);
 			pgstat_report_wait_end();
 			break;
+		case PGAIO_OP_FSYNC:
+			pgstat_report_wait_start(ioh->op_data.fsync.wait_event_info);
+			if (ioh->op_data.fsync.datasync)
+				result = pg_fdatasync(ioh->op_data.fsync.fd);
+			else
+				result = pg_fsync(ioh->op_data.fsync.fd);
+			pgstat_report_wait_end();
+			break;
 		case PGAIO_OP_INVALID:
 			elog(ERROR, "trying to execute invalid IO operation");
 	}
@@ -189,6 +210,8 @@ pgaio_io_get_op_name(PgAioHandle *ioh)
 			return "readv";
 		case PGAIO_OP_WRITEV:
 			return "writev";
+		case PGAIO_OP_FSYNC:
+			return "fsync";
 	}
 
 	return NULL;				/* silence compiler */
@@ -209,6 +232,8 @@ pgaio_io_uses_fd(PgAioHandle *ioh, int fd)
 			return ioh->op_data.read.fd == fd;
 		case PGAIO_OP_WRITEV:
 			return ioh->op_data.write.fd == fd;
+		case PGAIO_OP_FSYNC:
+			return ioh->op_data.fsync.fd == fd;
 		case PGAIO_OP_INVALID:
 			return false;
 	}
@@ -233,6 +258,8 @@ pgaio_io_get_iovec_length(PgAioHandle *ioh, struct iovec **iov)
 			return ioh->op_data.read.iov_length;
 		case PGAIO_OP_WRITEV:
 			return ioh->op_data.write.iov_length;
+		case PGAIO_OP_FSYNC:
+			return 0;
 		default:
 			pg_unreachable();
 			return 0;
diff --git a/src/backend/storage/aio/method_io_uring.c b/src/backend/storage/aio/method_io_uring.c
index 3ffe5061a20..675a91fb5f7 100644
--- a/src/backend/storage/aio/method_io_uring.c
+++ b/src/backend/storage/aio/method_io_uring.c
@@ -802,6 +802,12 @@ pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe)
 
 			break;
 
+		case PGAIO_OP_FSYNC:
+			io_uring_prep_fsync(sqe,
+								ioh->op_data.fsync.fd,
+								ioh->op_data.fsync.datasync ? IORING_FSYNC_DATASYNC : 0);
+			break;
+
 		case PGAIO_OP_INVALID:
 			elog(ERROR, "trying to prepare invalid IO operation for execution");
 	}
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 5391640d861..69e61ea1661 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -1094,6 +1094,9 @@ smgr_aio_reopen(PgAioHandle *ioh)
 			od->write.fd = smgrfd(reln, sd->smgr.forkNum, sd->smgr.blockNum, &off);
 			Assert(off == od->write.offset);
 			break;
+		case PGAIO_OP_FSYNC:
+			od->fsync.fd = smgrfd(reln, sd->smgr.forkNum, sd->smgr.blockNum, &off);
+			break;
 	}
 }
 
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.h
index ec543b78409..a1d5f49e51a 100644
--- a/src/include/storage/aio.h
+++ b/src/include/storage/aio.h
@@ -91,10 +91,10 @@ typedef enum PgAioOp
 
 	PGAIO_OP_READV,
 	PGAIO_OP_WRITEV,
+	PGAIO_OP_FSYNC,
 
 	/**
 	 * In the near term we'll need at least:
-	 * - fsync / fdatasync
 	 * - flush_range
 	 *
 	 * Eventually we'll additionally want at least:
@@ -104,7 +104,7 @@ typedef enum PgAioOp
 	 **/
 } PgAioOp;
 
-#define PGAIO_OP_COUNT	(PGAIO_OP_WRITEV + 1)
+#define PGAIO_OP_COUNT	(PGAIO_OP_FSYNC + 1)
 
 
 /*
@@ -146,6 +146,13 @@ typedef union
 		uint16		iov_length;
 		uint64		offset;
 	}			write;
+
+	struct
+	{
+		int			fd;
+		bool		datasync;
+		uint32		wait_event_info;
+	}			fsync;
 } PgAioOpData;
 
 
@@ -300,6 +307,9 @@ extern void pgaio_io_start_readv(PgAioHandle *ioh,
 								 int fd, int iovcnt, uint64 offset);
 extern void pgaio_io_start_writev(PgAioHandle *ioh,
 								  int fd, int iovcnt, uint64 offset);
+extern void pgaio_io_start_fsync(PgAioHandle *ioh, int fd, bool datasync,
+								 uint32 wait_event_info);
+
 
 /* functions in aio_target.c */
 extern void pgaio_io_set_target(PgAioHandle *ioh, PgAioTargetID targetid);
-- 
2.47.3

From 4a3f5e41edbef815059ab4ed4c64abc85e5e227a Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <[email protected]>
Date: Wed, 19 Aug 2026 15:56:39 +0300
Subject: [PATCH v1 2/4] Issue SyncDataDirectory() fsyncs through AIO

After a crash, the startup process fsyncs every file in the data
directory serially. Submit the fsyncs through AIO and keep a bounded
ring of operations in flight. Each operation holds a transient file
descriptor, while the directory walk needs descriptors of its own.
Limit the ring with GetFsyncConcurrencyLimit() and wait before opening
the next file to avoid exhausting the descriptor reserve.

The files are identified by paths held only in the startup process.
PGAIO_TID_SYNC does not copy those paths into shared memory, so the
worker method cannot reopen the files and executes the fsyncs
synchronously in the startup process. The io_uring method can execute
them concurrently. Storing MAXPGPATH bytes in every AIO target does not
seem justified for this startup-only operation.

Use the DATA_DIR_SYNC wait event when an fsync is executed
synchronously.
---
 src/backend/storage/aio/aio_target.c          |  24 ++
 src/backend/storage/file/fd.c                 | 277 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   1 +
 src/include/storage/aio.h                     |   3 +-
 src/include/storage/fd.h                      |   1 +
 src/tools/pgindent/typedefs.list              |   2 +
 6 files changed, 265 insertions(+), 43 deletions(-)

diff --git a/src/backend/storage/aio/aio_target.c b/src/backend/storage/aio/aio_target.c
index fa98f010d5a..82b24b0026d 100644
--- a/src/backend/storage/aio/aio_target.c
+++ b/src/backend/storage/aio/aio_target.c
@@ -18,6 +18,17 @@
 #include "storage/aio_internal.h"
 #include "storage/smgr.h"
 
+static char *pgaio_sync_describe_identity(const PgAioTargetData *sd);
+
+/*
+ * Target info for generic file syncs (PGAIO_TID_SYNC). The file being synced
+ * is identified by a path that is not stored in shared memory, therefore no
+ * reopen callback is provided.
+ */
+static const PgAioTargetInfo aio_sync_target_info = {
+	.name = "sync",
+	.describe_identity = pgaio_sync_describe_identity,
+};
 
 /*
  * Registry for entities that can be the target of AIO.
@@ -27,9 +38,22 @@ static const PgAioTargetInfo *pgaio_target_info[] = {
 		.name = "invalid",
 	},
 	[PGAIO_TID_SMGR] = &aio_smgr_target_info,
+	[PGAIO_TID_SYNC] = &aio_sync_target_info,
 };
 
 
+/*
+ * describe_identity callback for PGAIO_TID_SYNC. As we do not store the path
+ * of the file being synced in shared memory, only a generic description can
+ * be provided.
+ */
+static char *
+pgaio_sync_describe_identity(const PgAioTargetData *sd)
+{
+	return pstrdup("generic file sync");
+}
+
+
 
 /* --------------------------------------------------------------------------------
  * Public target related functions operating on IO Handles
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 190c9974494..0fcae14541d 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -268,6 +268,31 @@ typedef struct
 	}			desc;
 } AllocateDesc;
 
+/*
+ * State for asynchronously issuing the fsync()s in SyncDataDirectory(). We
+ * keep a bounded number of fsync IOs in flight, using a simple ring of
+ * entries; when the ring is full, we wait for the oldest IO to complete
+ * before issuing another.
+ */
+typedef struct DataDirSyncEntry
+{
+	int			fd;				/* transient fd being synced */
+	bool		isdir;			/* is this a directory? */
+	char		path[MAXPGPATH];	/* path to file being synced */
+	PgAioReturn ioret;
+	PgAioWaitRef iow;
+	bool		in_use;			/* is this entry being used */
+} DataDirSyncEntry;
+
+typedef struct DataDirSyncState
+{
+	int			elevel;			/* level to log errors at */
+	int			max_inflight;	/* size of the ring */
+	int			head;			/* index of oldest in-flight entry */
+	int			count;			/* number of in-flight entries */
+	DataDirSyncEntry *entries;	/* ring of max_inflight entries */
+} DataDirSyncState;
+
 static int	numAllocatedDescs = 0;
 static int	maxAllocatedDescs = 0;
 static AllocateDesc *allocatedDescs = NULL;
@@ -346,14 +371,20 @@ static void RemovePgTempRelationFiles(const char *tsdirname);
 static void RemovePgTempRelationFilesInDbspace(const char *dbspacedirname);
 
 static void walkdir(const char *path,
-					void (*action) (const char *fname, bool isdir, int elevel),
+					void (*action) (const char *fname, bool isdir, int elevel, DataDirSyncState *state),
 					bool process_symlinks,
-					int elevel);
+					int elevel,
+					DataDirSyncState *state);
 #ifdef PG_FLUSH_DATA_WORKS
-static void pre_sync_fname(const char *fname, bool isdir, int elevel);
+static void pre_sync_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state);
 #endif
-static void datadir_fsync_fname(const char *fname, bool isdir, int elevel);
-static void unlink_if_exists_fname(const char *fname, bool isdir, int elevel);
+static void datadir_fsync_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state);
+static void unlink_if_exists_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state);
+static bool fsync_fname_open(const char *fname, bool isdir, bool ignore_perm,
+							 int elevel, int *ret);
+static bool fsync_fname_close(const char *fname, bool isdir, int elevel, int fd, int returncode);
+static void datadir_sync_wait_one(DataDirSyncState *state);
+static void datadir_sync_drain_all(DataDirSyncState *state);
 
 static int	fsync_parent_path(const char *fname, int elevel);
 
@@ -1691,7 +1722,7 @@ PathNameDeleteTemporaryDir(const char *dirname)
 	 * used in a cleanup path, we wouldn't actually behave differently: we'll
 	 * just log failures.
 	 */
-	walkdir(dirname, unlink_if_exists_fname, false, LOG);
+	walkdir(dirname, unlink_if_exists_fname, false, LOG, NULL);
 }
 
 /*
@@ -3566,6 +3597,24 @@ do_syncfs(const char *path)
 }
 #endif
 
+/*
+ * Return a safe upper bound for the number of fsyncs to keep in flight.
+ *
+ * Callers that fsync files opened with OpenTransientFile() hold one
+ * AllocateDesc for each in-flight IO.  At most max_safe_fds / 3 of those can
+ * be allocated at a time (see reserveAllocatedDesc()), and the callers of
+ * interest also traverse directories (see walkdir()), which needs
+ * AllocateDescs of its own. So, hand out at most half of the budget.
+ *
+ * XXX: This is too safe for places that don't use OpenTransientFile() and/or
+ * walkdir().
+ */
+int
+GetFsyncConcurrencyLimit(void)
+{
+	return Max(1, Min(io_max_concurrency, max_safe_fds / 6));
+}
+
 /*
  * Issue fsync recursively on PGDATA and all its contents, or issue syncfs for
  * all potential filesystem, depending on recovery_init_sync_method setting.
@@ -3592,6 +3641,7 @@ void
 SyncDataDirectory(void)
 {
 	bool		xlog_is_symlink;
+	DataDirSyncState sync_state_data = {0};
 
 	/* We can skip this whole thing if fsync is disabled. */
 	if (!enableFsync)
@@ -3663,15 +3713,22 @@ SyncDataDirectory(void)
 	 * directory and its contents.  Errors in this step are even less
 	 * interesting than normal, so log them only at DEBUG1.
 	 */
-	walkdir(".", pre_sync_fname, false, DEBUG1);
+	walkdir(".", pre_sync_fname, false, DEBUG1, NULL);
 	if (xlog_is_symlink)
-		walkdir("pg_wal", pre_sync_fname, false, DEBUG1);
-	walkdir(PG_TBLSPC_DIR, pre_sync_fname, true, DEBUG1);
+		walkdir("pg_wal", pre_sync_fname, false, DEBUG1, NULL);
+	walkdir(PG_TBLSPC_DIR, pre_sync_fname, true, DEBUG1, NULL);
 #endif
 
 	/* Prepare to report progress syncing the data directory via fsync. */
 	begin_startup_progress_phase();
 
+	sync_state_data.elevel = LOG;
+	sync_state_data.max_inflight = GetFsyncConcurrencyLimit();
+	sync_state_data.head = 0;
+	sync_state_data.count = 0;
+	sync_state_data.entries = palloc0(sizeof(DataDirSyncEntry) *
+									  sync_state_data.max_inflight);
+
 	/*
 	 * Now we do the fsync()s in the same order.
 	 *
@@ -3681,10 +3738,14 @@ SyncDataDirectory(void)
 	 * in pg_tblspc, they'll get fsync'd twice.  That's not an expected case
 	 * so we don't worry about optimizing it.
 	 */
-	walkdir(".", datadir_fsync_fname, false, LOG);
+	walkdir(".", datadir_fsync_fname, false, LOG, &sync_state_data);
 	if (xlog_is_symlink)
-		walkdir("pg_wal", datadir_fsync_fname, false, LOG);
-	walkdir(PG_TBLSPC_DIR, datadir_fsync_fname, true, LOG);
+		walkdir("pg_wal", datadir_fsync_fname, false, LOG, &sync_state_data);
+	walkdir(PG_TBLSPC_DIR, datadir_fsync_fname, true, LOG, &sync_state_data);
+
+	datadir_sync_drain_all(&sync_state_data);
+
+	pfree(sync_state_data.entries);
 }
 
 /*
@@ -3704,9 +3765,10 @@ SyncDataDirectory(void)
  */
 static void
 walkdir(const char *path,
-		void (*action) (const char *fname, bool isdir, int elevel),
+		void (*action) (const char *fname, bool isdir, int elevel, DataDirSyncState *state),
 		bool process_symlinks,
-		int elevel)
+		int elevel,
+		DataDirSyncState *state)
 {
 	DIR		   *dir;
 	struct dirent *de;
@@ -3728,10 +3790,10 @@ walkdir(const char *path,
 		switch (get_dirent_type(subpath, de, process_symlinks, elevel))
 		{
 			case PGFILETYPE_REG:
-				(*action) (subpath, false, elevel);
+				(*action) (subpath, false, elevel, state);
 				break;
 			case PGFILETYPE_DIR:
-				walkdir(subpath, action, false, elevel);
+				walkdir(subpath, action, false, elevel, state);
 				break;
 			default:
 
@@ -3753,7 +3815,7 @@ walkdir(const char *path,
 	 * might not be robust against that.
 	 */
 	if (dir)
-		(*action) (path, true, elevel);
+		(*action) (path, true, elevel, state);
 }
 
 
@@ -3766,10 +3828,12 @@ walkdir(const char *path,
 #ifdef PG_FLUSH_DATA_WORKS
 
 static void
-pre_sync_fname(const char *fname, bool isdir, int elevel)
+pre_sync_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state)
 {
 	int			fd;
 
+	Assert(!state);
+
 	/* Don't try to flush directories, it'll likely just fail */
 	if (isdir)
 		return;
@@ -3804,21 +3868,98 @@ pre_sync_fname(const char *fname, bool isdir, int elevel)
 #endif							/* PG_FLUSH_DATA_WORKS */
 
 static void
-datadir_fsync_fname(const char *fname, bool isdir, int elevel)
+datadir_sync_wait_one(DataDirSyncState *state)
+{
+	DataDirSyncEntry *entry;
+
+	Assert(state->count > 0);
+
+	entry = &state->entries[state->head];
+	Assert(entry->in_use);
+
+	pgaio_wref_wait(&entry->iow);
+
+	/*
+	 * As we didn't register a completion callback, the IO's status is always
+	 * PGAIO_RS_OK; the raw fsync() return (0 or -errno) is available in
+	 * ->result.result.  Use the same error handling as fsync_fname_ext().
+	 */
+	if (entry->ioret.result.result < 0)
+		errno = -entry->ioret.result.result;
+
+	fsync_fname_close(entry->path, entry->isdir, state->elevel, entry->fd, entry->ioret.result.result);
+
+	entry->in_use = false;
+	state->head = (state->head + 1) % state->max_inflight;
+	state->count--;
+}
+
+static void
+datadir_sync_drain_all(DataDirSyncState *state)
+{
+	while (state->count > 0)
+		datadir_sync_wait_one(state);
+
+	Assert(state->count == 0);
+}
+
+static void
+datadir_fsync_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state)
 {
+	int			fd;
+	int			slot;
+	DataDirSyncEntry *entry;
+	PgAioHandle *ioh;
+
 	ereport_startup_progress("syncing data directory (fsync), elapsed time: %ld.%02d s, current path: %s",
 							 fname);
 
 	/*
-	 * We want to silently ignoring errors about unreadable files.  Pass that
-	 * desire on to fsync_fname_ext().
+	 * If the ring is full, wait before opening the next file, so that the
+	 * number of open descriptors never temporarily exceeds the ring size.
 	 */
-	fsync_fname_ext(fname, isdir, true, elevel);
+	if (state->count == state->max_inflight)
+		datadir_sync_wait_one(state);
+
+	/*
+	 * We want to silently ignore errors about unreadable files.  Pass that
+	 * desire on to fsync_fname_open().
+	 */
+	if (!fsync_fname_open(fname, isdir, true, elevel, &fd))
+		return;
+
+	Assert(state->count < state->max_inflight);
+	slot = (state->head + state->count) % state->max_inflight;
+	entry = &state->entries[slot];
+	Assert(!entry->in_use);
+
+	entry->fd = fd;
+	entry->isdir = isdir;
+	strlcpy(entry->path, fname, MAXPGPATH);
+	entry->in_use = true;
+
+	ioh = pgaio_io_acquire(CurrentResourceOwner, &entry->ioret);
+	pgaio_io_set_target(ioh, PGAIO_TID_SYNC);
+	pgaio_io_get_wref(ioh, &entry->iow);
+
+	/*
+	 * Interrupts must be held across staging the IO, so that the file
+	 * descriptor it references cannot be closed by interrupt processing
+	 * before the IO has been submitted.
+	 */
+	HOLD_INTERRUPTS();
+	pgaio_io_start_fsync(ioh, fd, false, WAIT_EVENT_DATA_DIR_SYNC);
+	RESUME_INTERRUPTS();
+
+	state->count++;
 }
 
 static void
-unlink_if_exists_fname(const char *fname, bool isdir, int elevel)
+unlink_if_exists_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state)
 {
+
+	Assert(!state);
+
 	if (isdir)
 	{
 		if (rmdir(fname) != 0 && errno != ENOENT)
@@ -3834,19 +3975,24 @@ unlink_if_exists_fname(const char *fname, bool isdir, int elevel)
 }
 
 /*
- * fsync_fname_ext -- Try to fsync a file or directory
+ * Helper for opening a file as part of fsync_fname_ext() and
+ * datadir_fsync_fname().  Split out because the latter performs the fsync
+ * asynchronously via AIO.
  *
- * If ignore_perm is true, ignore errors upon trying to open unreadable
- * files. Logs other errors at a caller-specified level.
+ * If ignore_perm is true, ignore errors upon trying to open unreadable files.
  *
- * Returns 0 if the operation succeeded, -1 otherwise.
+ * If the file could not be opened, false is returned; *fd is set to 0 if the
+ * failure should be ignored, or -1 otherwise.  Other errors are logged at the
+ * caller-specified level.
+ *
+ * If the file was opened successfully, true is returned and *fd is set to
+ * the file descriptor.
  */
-int
-fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel)
+static bool
+fsync_fname_open(const char *fname, bool isdir, bool ignore_perm,
+				 int elevel, int *fd)
 {
-	int			fd;
 	int			flags;
-	int			returncode;
 
 	/*
 	 * Some OSs require directories to be opened read-only whereas other
@@ -3860,27 +4006,48 @@ fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel)
 	else
 		flags |= O_RDONLY;
 
-	fd = OpenTransientFile(fname, flags);
+	*fd = OpenTransientFile(fname, flags);
 
 	/*
 	 * Some OSs don't allow us to open directories at all (Windows returns
 	 * EACCES), just ignore the error in that case.  If desired also silently
 	 * ignoring errors about unreadable files. Log others.
 	 */
-	if (fd < 0 && isdir && (errno == EISDIR || errno == EACCES))
-		return 0;
-	else if (fd < 0 && ignore_perm && errno == EACCES)
-		return 0;
-	else if (fd < 0)
+	if (*fd >= 0)
+		return true;
+	if (isdir && (errno == EISDIR || errno == EACCES))
 	{
+		*fd = 0;
+		return false;
+	}
+	else if (ignore_perm && errno == EACCES)
+	{
+		*fd = 0;
+		return false;
+	}
+	else
+	{
+		*fd = -1;
 		ereport(elevel,
 				(errcode_for_file_access(),
 				 errmsg("could not open file \"%s\": %m", fname)));
-		return -1;
+		return false;
 	}
+}
 
-	returncode = pg_fsync(fd);
-
+/*
+ * Helper for closing a file, and reporting any fsync failure, as part of
+ * fsync_fname_ext() and datadir_fsync_fname().  Split out because the latter
+ * performs the fsync asynchronously via AIO.
+ *
+ * "returncode" is the result of the preceding fsync; if it is negative, errno
+ * must describe the failure.
+ *
+ * Returns true if the operation succeeded, false otherwise.
+ */
+static bool
+fsync_fname_close(const char *fname, bool isdir, int elevel, int fd, int returncode)
+{
 	/*
 	 * Some OSes don't allow us to fsync directories at all, so we can ignore
 	 * those errors. Anything else needs to be logged.
@@ -3897,7 +4064,7 @@ fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel)
 		ereport(elevel,
 				(errcode_for_file_access(),
 				 errmsg("could not fsync file \"%s\": %m", fname)));
-		return -1;
+		return false;
 	}
 
 	if (CloseTransientFile(fd) != 0)
@@ -3905,9 +4072,35 @@ fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel)
 		ereport(elevel,
 				(errcode_for_file_access(),
 				 errmsg("could not close file \"%s\": %m", fname)));
-		return -1;
+		return false;
 	}
 
+	return true;
+}
+
+/*
+ * fsync_fname_ext -- Try to fsync a file or directory
+ *
+ * If ignore_perm is true, ignore errors upon trying to open unreadable
+ * files. Logs other errors at a caller-specified level.
+ *
+ * Returns 0 if the operation succeeded, -1 otherwise.
+ */
+int
+fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel)
+{
+	int			fd;
+	int			returncode;
+
+	if (!fsync_fname_open(fname, isdir, ignore_perm, elevel, &fd))
+		return fd;
+
+	Assert(fd >= 0);
+	returncode = pg_fsync(fd);
+
+	if (!fsync_fname_close(fname, isdir, elevel, fd, returncode))
+		return -1;
+
 	return 0;
 }
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 256b3a3c02e..aede94dcfc3 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -219,6 +219,7 @@ COPY_FILE_READ	"Waiting for a read during a file copy operation."
 COPY_FILE_WRITE	"Waiting for a write during a file copy operation."
 COPY_FROM_READ	"Waiting to read data from a pipe, a file or a program during COPY FROM."
 COPY_TO_WRITE	"Waiting to write data to a pipe, a file or a program during COPY TO."
+DATA_DIR_SYNC	"Waiting for the contents of the data directory to reach durable storage after a crash."
 DATA_FILE_EXTEND	"Waiting for a relation data file to be extended."
 DATA_FILE_FLUSH	"Waiting for a relation data file to reach durable storage."
 DATA_FILE_IMMEDIATE_SYNC	"Waiting for an immediate synchronization of a relation data file to durable storage."
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.h
index a1d5f49e51a..428504c0472 100644
--- a/src/include/storage/aio.h
+++ b/src/include/storage/aio.h
@@ -118,9 +118,10 @@ typedef enum PgAioTargetID
 	/* intentionally the zero value, to help catch zeroed memory etc */
 	PGAIO_TID_INVALID = 0,
 	PGAIO_TID_SMGR,
+	PGAIO_TID_SYNC,
 } PgAioTargetID;
 
-#define PGAIO_TID_COUNT (PGAIO_TID_SMGR + 1)
+#define PGAIO_TID_COUNT (PGAIO_TID_SYNC + 1)
 
 
 /*
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 8ac466fd346..c79f3312544 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -219,6 +219,7 @@ extern int	fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int
 extern int	durable_rename(const char *oldfile, const char *newfile, int elevel);
 extern int	durable_unlink(const char *fname, int elevel);
 extern void SyncDataDirectory(void);
+extern int	GetFsyncConcurrencyLimit(void);
 extern int	data_sync_elevel(int elevel);
 
 static inline ssize_t
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6c366d3a523..a95b09859b5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -639,7 +639,9 @@ DataChecksumsStateStruct
 DataChecksumsWorkerDatabase
 DataChecksumsWorkerOperation
 DataChecksumsWorkerResult
+DataDirSyncEntry
 DataDirSyncMethod
+DataDirSyncState
 DataDumperPtr
 DataPageDeleteStack
 DataTypesUsageChecks
-- 
2.47.3

From 1845c07e610d9651759f9ce4239c042074916e7c Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <[email protected]>
Date: Tue, 25 Aug 2026 11:27:22 +0300
Subject: [PATCH v1 3/4] Issue checkpointer fsyncs asynchronously

ProcessSyncRequests() previously fsynced pending files one at a time.
For checkpoints with many files, this serialized I/O that storage could
perform concurrently.

Submit the fsyncs through AIO, keeping a bounded set in flight and
reaping completions in submission order. Each in-flight SLRU fsync
holds a transient file descriptor, so limit the depth with
GetFsyncConcurrencyLimit() rather than io_max_concurrency to avoid
exhausting the descriptor reserve.

Reshape the sync handler API so handlers open the file, assign an AIO
target, start the fsync, and record how to close it. sync.c manages the
in-flight operations, errors, retries, and pendingOps bookkeeping.

Absorbing requests while fsyncs are in flight requires some care:

- Recheck cancellation when an operation is reaped because a request
  can be canceled after its fsync starts.
- Defer completion bookkeeping until the pendingOps scan ends because
  dynahash permits removing only the entry most recently returned.
- Keep a new request for a file already in flight until the next
  checkpoint cycle because the running fsync might not cover its write.

Relation files can be reopened through the smgr target, allowing I/O
workers to perform their fsyncs. SLRU files use the generic sync target
and execute synchronously with the worker I/O method, although io_uring
can still overlap them.

Because handlers now return before an fsync completes, md.c reports
submission time to pg_stat_io, as it does for asynchronous reads.
Methods that execute the fsync immediately still report its duration,
and fsync counts are unchanged.

The per-file checkpoint timing statistics now measure submission-to-reap
time rather than fsync duration. Submission-order reaping can overstate
individual times, and overlapping operations can make the aggregate
exceed the wall-clock time of the sync phase.
---
 src/backend/access/transam/clog.c      |   6 +-
 src/backend/access/transam/commit_ts.c |   6 +-
 src/backend/access/transam/multixact.c |  12 +-
 src/backend/access/transam/slru.c      |  35 +-
 src/backend/storage/file/fd.c          |  23 ++
 src/backend/storage/smgr/md.c          |  68 +++-
 src/backend/storage/sync/sync.c        | 537 ++++++++++++++++++++-----
 src/include/access/clog.h              |   2 +-
 src/include/access/commit_ts.h         |   2 +-
 src/include/access/multixact.h         |   4 +-
 src/include/access/slru.h              |   2 +-
 src/include/storage/fd.h               |   1 +
 src/include/storage/md.h               |   2 +-
 src/include/storage/sync.h             |  56 +++
 src/test/modules/test_slru/test_slru.c |  45 ++-
 src/tools/pgindent/typedefs.list       |   3 +
 16 files changed, 645 insertions(+), 159 deletions(-)

diff --git a/src/backend/access/transam/clog.c b/src/backend/access/transam/clog.c
index 6f7f6b86eb6..89fb77ea5da 100644
--- a/src/backend/access/transam/clog.c
+++ b/src/backend/access/transam/clog.c
@@ -1117,8 +1117,8 @@ clog_redo(XLogReaderState *record)
 /*
  * Entrypoint for sync.c to sync clog files.
  */
-int
-clogsyncfiletag(const FileTag *ftag, char *path)
+void
+clogsyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry)
 {
-	return SlruSyncFileTag(XactCtl, ftag, path);
+	SlruSyncFileTag(XactCtl, ioh, entry);
 }
diff --git a/src/backend/access/transam/commit_ts.c b/src/backend/access/transam/commit_ts.c
index 9e6fd5d4657..7cbbad383b2 100644
--- a/src/backend/access/transam/commit_ts.c
+++ b/src/backend/access/transam/commit_ts.c
@@ -1028,8 +1028,8 @@ commit_ts_redo(XLogReaderState *record)
 /*
  * Entrypoint for sync.c to sync commit_ts files.
  */
-int
-committssyncfiletag(const FileTag *ftag, char *path)
+void
+committssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry)
 {
-	return SlruSyncFileTag(CommitTsCtl, ftag, path);
+	SlruSyncFileTag(CommitTsCtl, ioh, entry);
 }
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index d688815083c..deb866cd7f0 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -2998,17 +2998,17 @@ multixact_redo(XLogReaderState *record)
 /*
  * Entrypoint for sync.c to sync offsets files.
  */
-int
-multixactoffsetssyncfiletag(const FileTag *ftag, char *path)
+void
+multixactoffsetssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry)
 {
-	return SlruSyncFileTag(MultiXactOffsetCtl, ftag, path);
+	SlruSyncFileTag(MultiXactOffsetCtl, ioh, entry);
 }
 
 /*
  * Entrypoint for sync.c to sync members files.
  */
-int
-multixactmemberssyncfiletag(const FileTag *ftag, char *path)
+void
+multixactmemberssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry)
 {
-	return SlruSyncFileTag(MultiXactMemberCtl, ftag, path);
+	SlruSyncFileTag(MultiXactMemberCtl, ioh, entry);
 }
diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c
index 885fd068535..b1e513ac3b6 100644
--- a/src/backend/access/transam/slru.c
+++ b/src/backend/access/transam/slru.c
@@ -68,6 +68,7 @@
 #include "access/xlogutils.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "storage/aio.h"
 #include "storage/fd.h"
 #include "storage/shmem.h"
 #include "storage/shmem_internal.h"
@@ -1880,26 +1881,32 @@ SlruScanDirectory(SlruDesc *ctl, SlruScanCallback callback, void *data)
  * build the path), but they just forward to this common implementation that
  * performs the fsync.
  */
-int
-SlruSyncFileTag(SlruDesc *ctl, const FileTag *ftag, char *path)
+void
+SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, InflightSyncEntry *entry)
 {
 	int			fd;
-	int			save_errno;
-	int			result;
 
-	SlruFileName(ctl, path, ftag->segno);
+	SlruFileName(ctl, entry->path, entry->tag.segno);
 
-	fd = OpenTransientFile(path, O_RDWR | PG_BINARY);
+	fd = OpenTransientFile(entry->path, O_RDWR | PG_BINARY);
 	if (fd < 0)
-		return -1;
+	{
+		entry->started = false;
+		entry->open_errno = errno;
+		return;
+	}
 
-	pgstat_report_wait_start(WAIT_EVENT_SLRU_FLUSH_SYNC);
-	result = pg_fsync(fd);
-	pgstat_report_wait_end();
-	save_errno = errno;
+	/*
+	 * Use the generic sync target.  SLRU segments are not smgr relations and
+	 * cannot be reopened from a FileTag in another process, so this fsync
+	 * will run synchronously in worker mode.
+	 */
+	pgaio_io_set_target(ioh, PGAIO_TID_SYNC);
 
-	CloseTransientFile(fd);
+	/* Start the asynchronous fsync; the fd is closed once it completes. */
+	pgaio_io_start_fsync(ioh, fd, false, WAIT_EVENT_SLRU_FLUSH_SYNC);
 
-	errno = save_errno;
-	return result;
+	entry->started = true;
+	entry->close_method = SYNC_CLOSE_TRANSIENT;
+	entry->close_file = fd;
 }
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 0fcae14541d..f91ee488b89 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -2258,6 +2258,29 @@ FileStartReadV(PgAioHandle *ioh, File file,
 	return 0;
 }
 
+int
+FileStartSync(PgAioHandle *ioh, File file, bool datasync,
+			  uint32 wait_event_info)
+{
+	int			returnCode;
+	Vfd		   *vfdP;
+
+	Assert(FileIsValid(file));
+
+	DO_DB(elog(LOG, "FileStartSync: %d (%s)",
+			   file, VfdCache[file].fileName));
+
+	returnCode = FileAccess(file);
+	if (returnCode < 0)
+		return returnCode;
+
+	vfdP = &VfdCache[file];
+
+	pgaio_io_start_fsync(ioh, vfdP->fd, datasync, wait_event_info);
+
+	return 0;
+}
+
 ssize_t
 FileWriteV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset,
 		   uint32 wait_event_info)
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 780c88c0630..e1fca54d9d9 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -1896,26 +1896,27 @@ _mdnblocks(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg)
 }
 
 /*
- * Sync a file to disk, given a file tag.  Write the path into an output
- * buffer so the caller can use it in error messages.
+ * Sync a file to disk, given a file tag.
  *
- * Return 0 on success, -1 on failure, with errno set.
+ * Starts an asynchronous fsync on the given AIO handle and records in "entry"
+ * the path (for error messages), whether an IO was started, and how the file
+ * is to be closed once the fsync has completed.
  */
-int
-mdsyncfiletag(const FileTag *ftag, char *path)
+void
+mdsyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry)
 {
+	FileTag    *ftag = &entry->tag;
 	SMgrRelation reln = smgropen(ftag->rlocator, INVALID_PROC_NUMBER);
+	BlockNumber segfirstblock = ftag->segno * ((BlockNumber) RELSEG_SIZE);
 	File		file;
-	instr_time	io_start;
 	bool		need_to_close;
-	int			result,
-				save_errno;
+	instr_time	io_start;
 
 	/* See if we already have the file open, or need to open it. */
 	if (ftag->segno < reln->md_num_open_segs[ftag->forknum])
 	{
 		file = reln->md_seg_fds[ftag->forknum][ftag->segno].mdfd_vfd;
-		strlcpy(path, FilePathName(file), MAXPGPATH);
+		strlcpy(entry->path, FilePathName(file), MAXPGPATH);
 		need_to_close = false;
 	}
 	else
@@ -1923,28 +1924,53 @@ mdsyncfiletag(const FileTag *ftag, char *path)
 		MdPathStr	p;
 
 		p = _mdfd_segpath(reln, ftag->forknum, ftag->segno);
-		strlcpy(path, p.str, MD_PATH_STR_MAXLEN);
+		strlcpy(entry->path, p.str, MD_PATH_STR_MAXLEN);
 
-		file = PathNameOpenFile(path, _mdfd_open_flags());
+		file = PathNameOpenFile(entry->path, _mdfd_open_flags());
 		if (file < 0)
-			return -1;
+		{
+			entry->started = false;
+			entry->open_errno = errno;
+			return;
+		}
 		need_to_close = true;
 	}
 
+	pgaio_io_set_target_smgr(ioh, reln, ftag->forknum, segfirstblock,
+							 0, false);
+
+	/*
+	 * As with asynchronous reads, measure the time spent starting the IO.
+	 * Synchronous execution includes the fsync itself; otherwise this only
+	 * measures submission.
+	 */
 	io_start = pgstat_prepare_io_time(track_io_timing);
 
-	/* Sync the file. */
-	result = FileSync(file, WAIT_EVENT_DATA_FILE_SYNC);
-	save_errno = errno;
+	if (FileStartSync(ioh, file, false, WAIT_EVENT_DATA_FILE_SYNC) < 0)
+	{
+		entry->started = false;
+		entry->open_errno = errno;
+		if (need_to_close)
+			FileClose(file);
+		return;
+	}
 
-	if (need_to_close)
-		FileClose(file);
+	pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL, IOOP_FSYNC,
+							io_start, 1, 0);
 
-	pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL,
-							IOOP_FSYNC, io_start, 1, 0);
+	entry->started = true;
 
-	errno = save_errno;
-	return result;
+	/*
+	 * If we opened the segment ourselves it has to be closed once the fsync
+	 * has completed; segments owned by smgr are left to smgr to manage.
+	 */
+	if (need_to_close)
+	{
+		entry->close_method = SYNC_CLOSE_VFD;
+		entry->close_file = (int) file;
+	}
+	else
+		entry->close_method = SYNC_CLOSE_NONE;
 }
 
 /*
diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c
index 2c964b6f3d9..4263881ddd8 100644
--- a/src/backend/storage/sync/sync.c
+++ b/src/backend/storage/sync/sync.c
@@ -26,7 +26,9 @@
 #include "pgstat.h"
 #include "portability/instr_time.h"
 #include "postmaster/bgwriter.h"
+#include "storage/aio.h"
 #include "storage/fd.h"
+#include "storage/ipc.h"
 #include "storage/latch.h"
 #include "storage/md.h"
 #include "utils/hsearch.h"
@@ -54,11 +56,22 @@
  */
 typedef uint16 CycleCtr;		/* can be any convenient integer size */
 
-typedef struct
+typedef struct PendingFsyncEntry
 {
 	FileTag		tag;			/* identifies handler and file */
 	CycleCtr	cycle_ctr;		/* sync_cycle_ctr of oldest request */
 	bool		canceled;		/* canceled is true if we canceled "recently" */
+
+	/*
+	 * Set when a request arrives for a tag that already has an entry, and
+	 * cleared whenever an fsync for it is started.  If it is set once that
+	 * fsync completes, the request came in while the fsync was in flight, so
+	 * the fsync cannot be assumed to have covered it.
+	 */
+	bool		re_requested;
+
+	/* fsync is done, pending hash-table bookkeeping */
+	bool		sync_completed;
 } PendingFsyncEntry;
 
 typedef struct
@@ -68,10 +81,44 @@ typedef struct
 	bool		canceled;		/* true if request has been canceled */
 } PendingUnlinkEntry;
 
+/*
+ * Transient state used while processing a batch of fsync requests.  A single
+ * SyncState instance lives on the stack of ProcessSyncRequests() so that no
+ * partial state survives across calls.
+ */
+typedef struct SyncState
+{
+	dlist_head	inflight;		/* InflightSyncEntry being fsync'd right now */
+	dlist_head	retry;			/* InflightSyncEntry to be retried */
+	int			inflight_count; /* number of entries in "inflight" */
+	int			max_inflight;	/* max number of concurrent fsyncs */
+	int			absorb_counter;
+
+	/* stats */
+	int			processed;
+	instr_time	longest;
+	instr_time	total_elapsed;
+} SyncState;
+
 static HTAB *pendingOps = NULL;
 static List *pendingUnlinks = NIL;
 static MemoryContext pendingOpsCxt; /* context for the above  */
 
+/*
+ * Context for the InflightSyncEntry structs allocated while a batch of fsync
+ * requests is being processed.  It is kept separate from pendingOpsCxt (which
+ * must survive for the lifetime of the process, as it holds pendingOps
+ * itself), so that it can be reset between batches.
+ */
+static MemoryContext inflightSyncCxt;
+
+/*
+ * All InflightSyncEntry structs that have not yet been freed.  Unlike the
+ * lists in SyncState, this survives an error so that handler-owned files can
+ * be closed before their entries are discarded.
+ */
+static dlist_head activeSyncEntries = DLIST_STATIC_INIT(activeSyncEntries);
+
 static CycleCtr sync_cycle_ctr = 0;
 static CycleCtr checkpoint_cycle_ctr = 0;
 
@@ -84,7 +131,7 @@ static CycleCtr checkpoint_cycle_ctr = 0;
  */
 typedef struct SyncOps
 {
-	int			(*sync_syncfiletag) (const FileTag *ftag, char *path);
+	void		(*sync_syncfiletag) (PgAioHandle *ioh, InflightSyncEntry *entry);
 	int			(*sync_unlinkfiletag) (const FileTag *ftag, char *path);
 	bool		(*sync_filetagmatches) (const FileTag *ftag,
 										const FileTag *candidate);
@@ -155,6 +202,10 @@ InitSync(void)
 								 &hash_ctl,
 								 HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
 		pendingUnlinks = NIL;
+
+		inflightSyncCxt = AllocSetContextCreate(TopMemoryContext,
+												"Inflight sync context",
+												ALLOCSET_DEFAULT_SIZES);
 	}
 }
 
@@ -281,25 +332,295 @@ SyncPostCheckpoint(void)
 }
 
 /*
- *	ProcessSyncRequests() -- Process queued fsync requests.
+ * Close the file that a sync handler opened for an in-flight fsync.
  */
-void
-ProcessSyncRequests(void)
+static void
+sync_close_file(InflightSyncEntry *entry)
 {
-	static bool sync_in_progress = false;
+	switch (entry->close_method)
+	{
+		case SYNC_CLOSE_NONE:
+			break;
+		case SYNC_CLOSE_TRANSIENT:
+			CloseTransientFile(entry->close_file);
+			break;
+		case SYNC_CLOSE_VFD:
+			FileClose((File) entry->close_file);
+			break;
+		default:
+			pg_unreachable();
+	}
+
+	entry->close_method = SYNC_CLOSE_NONE;
+}
+
+static void
+sync_free_entry(InflightSyncEntry *entry)
+{
+	dlist_delete_from(&activeSyncEntries, &entry->cleanup_node);
+	pfree(entry);
+}
+
+/*
+ * Error cleanup callback for ProcessSyncRequests().
+ */
+static void
+sync_cleanup_inflight(int code, Datum arg)
+{
+	while (!dlist_is_empty(&activeSyncEntries))
+	{
+		dlist_node *node = dlist_pop_head_node(&activeSyncEntries);
+		InflightSyncEntry *entry;
+
+		entry = dlist_container(InflightSyncEntry, cleanup_node, node);
+
+		if (entry->started)
+			pgaio_wref_wait(&entry->iow);
+
+		sync_close_file(entry);
+		pfree(entry);
+	}
+}
+
+static void
+sync_start_one(SyncState *sync_state, InflightSyncEntry *entry)
+{
+	struct PgAioHandle *ioh;
+	instr_time	io_start;
+
+	INSTR_TIME_SET_CURRENT(io_start);
+	entry->start_time = io_start;
+
+	entry->started = false;
+	entry->open_errno = 0;
+	entry->close_method = SYNC_CLOSE_NONE;
+	pgaio_wref_clear(&entry->iow);
+
+	/*
+	 * Any request that arrives from here on may cover data that the fsync
+	 * started below does not, so start out with a clean slate.  This has to
+	 * happen before the IO is submitted; requests absorbed in between are
+	 * covered by the fsync, so treating them as newer is merely conservative.
+	 */
+	entry->hash_entry->re_requested = false;
+
+	ioh = pgaio_io_acquire(CurrentResourceOwner, &entry->ioret);
+	pgaio_io_get_wref(ioh, &entry->iow);
+
+	/*
+	 * The handler opens the file, assigns the target and stages the fsync.
+	 * Hold interrupts so that the referenced descriptor cannot be closed
+	 * during submission.
+	 */
+	HOLD_INTERRUPTS();
+	syncsw[entry->tag.handler].sync_syncfiletag(ioh, entry);
+	RESUME_INTERRUPTS();
+
+	if (!entry->started)
+		pgaio_io_release(ioh);
+
+	dlist_push_tail(&sync_state->inflight, &entry->node);
+	sync_state->inflight_count++;
+}
+
+static void
+sync_drain_one(SyncState *sync_state)
+{
+	dlist_node *node;
+	InflightSyncEntry *entry;
+	int			result;
+
+	Assert(sync_state->inflight_count > 0);
+
+	node = dlist_pop_head_node(&sync_state->inflight);
+	entry = dlist_container(InflightSyncEntry, node, node);
+	sync_state->inflight_count--;
+
+	if (entry->started)
+	{
+		pgaio_wref_wait(&entry->iow);
+
+		/*
+		 * We did not register a completion callback, so the distilled status
+		 * is always PGAIO_RS_OK and the raw fsync() return value (0 on
+		 * success, -errno on failure) is available in ->result.result.
+		 */
+		result = -entry->ioret.result.result;
+	}
+	else
+		result = entry->open_errno;
+
+	sync_close_file(entry);
+
+	if (!result)
+	{
+		instr_time	io_time;
+
+		/*
+		 * These values measure submission-to-reap time, not necessarily fsync
+		 * duration.  Submission-order reaping can overstate individual
+		 * durations, and the aggregate can exceed wall-clock time because
+		 * fsyncs overlap.
+		 */
+		INSTR_TIME_SET_CURRENT(io_time);
+		INSTR_TIME_SUBTRACT(io_time, entry->start_time);
+
+		if (INSTR_TIME_GT(io_time, sync_state->longest))
+			sync_state->longest = io_time;
+		INSTR_TIME_ADD(sync_state->total_elapsed, io_time);
+		sync_state->processed++;
+
+		if (log_checkpoints)
+			elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
+				 sync_state->processed,
+				 entry->path,
+				 INSTR_TIME_GET_MILLISEC(io_time));
+
+		entry->hash_entry->sync_completed = true;
+		sync_free_entry(entry);
+	}
+	else
+	{
+		/*
+		 * The request may have been canceled after we started the fsync, e.g.
+		 * because the relation was dropped in the meantime and an intervening
+		 * AbsorbSyncRequests() picked up the cancel message.  Since
+		 * mdunlink() queues the "cancel" before actually unlinking, a
+		 * cancellation means the failure is expected and the entry can simply
+		 * be dropped.
+		 *
+		 * The upstream, synchronous code checked this at the top of its retry
+		 * loop; because the fsync is now in flight while requests are being
+		 * absorbed, we have to re-check it here.
+		 */
+		if (entry->hash_entry->canceled)
+		{
+			entry->hash_entry->sync_completed = true;
+			sync_free_entry(entry);
+			return;
+		}
+
+		/*
+		 * It is possible that the relation has been dropped or truncated
+		 * since the fsync request was entered. Therefore, allow ENOENT, but
+		 * only if we didn't fail already on this file.
+		 */
+		errno = result;
+		if (!FILE_POSSIBLY_DELETED(errno) || entry->retry_count > 0)
+			ereport(data_sync_elevel(ERROR),
+					(errcode_for_file_access(),
+					 errmsg("could not fsync file \"%s\": %m",
+							entry->path)));
+		else
+			ereport(DEBUG1,
+					(errcode_for_file_access(),
+					 errmsg_internal("could not fsync file \"%s\" but retrying: %m",
+									 entry->path)));
+
+		entry->retry_count++;
+		dlist_push_tail(&sync_state->retry, &entry->node);
+	}
+}
 
+static void
+sync_drain_all(SyncState *sync_state)
+{
+	while (sync_state->inflight_count)
+		sync_drain_one(sync_state);
+}
+
+/*
+ * Finish requests whose fsyncs have completed.
+ *
+ * The main hash scan may only remove the entry it most recently returned, so
+ * completion processing is deferred until it ends.  This second scan can then
+ * remove each completed entry as the current entry.  Recheck the hash entry
+ * now because requests absorbed since the fsync completed may require it to
+ * remain for the next checkpoint cycle.
+ */
+static void
+sync_process_completed(void)
+{
 	HASH_SEQ_STATUS hstat;
 	PendingFsyncEntry *entry;
-	int			absorb_counter;
 
-	/* Statistics on sync times */
-	int			processed = 0;
-	instr_time	sync_start,
-				sync_end,
-				sync_diff;
-	uint64		elapsed;
-	uint64		longest = 0;
-	uint64		total_elapsed = 0;
+	hash_seq_init(&hstat, pendingOps);
+	while ((entry = (PendingFsyncEntry *) hash_seq_search(&hstat)) != NULL)
+	{
+		if (!entry->sync_completed)
+			continue;
+
+		/*
+		 * We are done with this entry, unless a request for it arrived while
+		 * the fsync was in flight.  A cancel supersedes any such request, as
+		 * RememberSyncRequest() clears "canceled" when it records a new one.
+		 */
+		if (!entry->re_requested || entry->canceled)
+		{
+			if (hash_search(pendingOps, &entry->tag, HASH_REMOVE, NULL) == NULL)
+				elog(ERROR, "pendingOps corrupted");
+		}
+		else
+			entry->sync_completed = false;
+	}
+}
+
+/*
+ * Reissue any fsync requests that previously failed with an ignorable error.
+ *
+ * The fsync table could contain requests to fsync segments that have been
+ * deleted (unlinked) by the time we get to them. Rather than just hoping an
+ * ENOENT (or EACCES on Windows) error can be ignored, what we do on error is
+ * absorb pending requests and then retry. Since mdunlink() queues a "cancel"
+ * message before actually unlinking, the fsync request is guaranteed to be
+ * marked canceled after the absorb if it really was this case.
+ */
+static void
+sync_process_retries(SyncState *sync_state)
+{
+	if (dlist_is_empty(&sync_state->retry))
+		return;
+
+	AbsorbSyncRequests();
+
+	while (!dlist_is_empty(&sync_state->retry))
+	{
+		dlist_node *node = dlist_pop_head_node(&sync_state->retry);
+		InflightSyncEntry *entry = dlist_container(InflightSyncEntry, node, node);
+
+		if (entry->hash_entry->canceled)
+		{
+			/* Safe to remove here, the scan has already finished. */
+			if (hash_search(pendingOps, &entry->hash_entry->tag,
+							HASH_REMOVE, NULL) == NULL)
+				elog(ERROR, "pendingOps corrupted");
+			sync_free_entry(entry);
+			continue;
+		}
+
+		Assert(sync_state->inflight_count <= sync_state->max_inflight);
+		if (sync_state->inflight_count == sync_state->max_inflight)
+			sync_drain_one(sync_state);
+
+		sync_start_one(sync_state, entry);
+	}
+
+	sync_drain_all(sync_state);
+	sync_process_completed();
+}
+
+/*
+ * Process queued fsync requests.  The public wrapper ensures that any error
+ * closes files owned by in-flight entries.
+ */
+static void
+ProcessSyncRequestsInternal(void)
+{
+	static bool sync_in_progress = false;
+
+	HASH_SEQ_STATUS hstat;
+	PendingFsyncEntry *entry;
+	SyncState	sync_state;
 
 	/*
 	 * This is only called during checkpoints, and checkpoints should only
@@ -350,6 +671,7 @@ ProcessSyncRequests(void)
 		while ((entry = (PendingFsyncEntry *) hash_seq_search(&hstat)) != NULL)
 		{
 			entry->cycle_ctr = sync_cycle_ctr;
+			entry->sync_completed = false;
 		}
 	}
 
@@ -359,13 +681,26 @@ ProcessSyncRequests(void)
 	/* Set flag to detect failure if we don't reach the end of the loop */
 	sync_in_progress = true;
 
+	/*
+	 * Bound concurrent fsyncs by both the AIO handle and transient descriptor
+	 * budgets.
+	 */
+	dlist_init(&sync_state.inflight);
+	dlist_init(&sync_state.retry);
+	sync_state.inflight_count = 0;
+	sync_state.max_inflight = GetFsyncConcurrencyLimit();
+	sync_state.processed = 0;
+	INSTR_TIME_SET_ZERO(sync_state.longest);
+	INSTR_TIME_SET_ZERO(sync_state.total_elapsed);
+
+	Assert(dlist_is_empty(&activeSyncEntries));
+	MemoryContextReset(inflightSyncCxt);
+
 	/* Now scan the hashtable for fsync requests to process */
-	absorb_counter = FSYNCS_PER_ABSORB;
+	sync_state.absorb_counter = FSYNCS_PER_ABSORB;
 	hash_seq_init(&hstat, pendingOps);
 	while ((entry = (PendingFsyncEntry *) hash_seq_search(&hstat)) != NULL)
 	{
-		int			failures;
-
 		/*
 		 * If the entry is new then don't process it this time; it is new.
 		 * Note "continue" bypasses the hash-remove call at the bottom of the
@@ -378,103 +713,94 @@ ProcessSyncRequests(void)
 		Assert((CycleCtr) (entry->cycle_ctr + 1) == sync_cycle_ctr);
 
 		/*
-		 * If fsync is off then we don't have to bother opening the file at
-		 * all.  (We delay checking until this point so that changing fsync on
-		 * the fly behaves sensibly.)
+		 * If in checkpointer, we want to absorb pending requests every so
+		 * often to prevent overflow of the fsync request queue.  It is
+		 * unspecified whether newly-added entries will be visited by
+		 * hash_seq_search, but we don't care since we don't need to process
+		 * them anyway.
 		 */
-		if (enableFsync)
+		if (enableFsync && --sync_state.absorb_counter <= 0)
 		{
-			/*
-			 * If in checkpointer, we want to absorb pending requests every so
-			 * often to prevent overflow of the fsync request queue.  It is
-			 * unspecified whether newly-added entries will be visited by
-			 * hash_seq_search, but we don't care since we don't need to
-			 * process them anyway.
-			 */
-			if (--absorb_counter <= 0)
-			{
-				AbsorbSyncRequests();
-				absorb_counter = FSYNCS_PER_ABSORB;
-			}
+			AbsorbSyncRequests();
+			sync_state.absorb_counter = FSYNCS_PER_ABSORB;
+		}
+
+		if (!enableFsync || entry->canceled)
+		{
+			/* We are done with this entry, remove it */
+			if (hash_search(pendingOps, &entry->tag, HASH_REMOVE, NULL) == NULL)
+				elog(ERROR, "pendingOps corrupted");
+		}
+		else
+		{
+			InflightSyncEntry *inflight_entry;
+
+			Assert(sync_state.inflight_count <= sync_state.max_inflight);
+			if (sync_state.inflight_count == sync_state.max_inflight)
+				sync_drain_one(&sync_state);
 
 			/*
-			 * The fsync table could contain requests to fsync segments that
-			 * have been deleted (unlinked) by the time we get to them. Rather
-			 * than just hoping an ENOENT (or EACCES on Windows) error can be
-			 * ignored, what we do on error is absorb pending requests and
-			 * then retry. Since mdunlink() queues a "cancel" message before
-			 * actually unlinking, the fsync request is guaranteed to be
-			 * marked canceled after the absorb if it really was this case.
-			 * DROP DATABASE likewise has to tell us to forget fsync requests
-			 * before it starts deletions.
+			 * Mark the entry as already dealt with in this cycle.  It must
+			 * remain in the hash table until its fsync completes and the scan
+			 * ends.  If a new request arrives meanwhile, this cycle counter
+			 * leaves the entry to be processed by the next checkpoint.
 			 */
-			for (failures = 0; !entry->canceled; failures++)
-			{
-				char		path[MAXPGPATH];
-
-				INSTR_TIME_SET_CURRENT(sync_start);
-				if (syncsw[entry->tag.handler].sync_syncfiletag(&entry->tag,
-																path) == 0)
-				{
-					/* Success; update statistics about sync timing */
-					INSTR_TIME_SET_CURRENT(sync_end);
-					sync_diff = sync_end;
-					INSTR_TIME_SUBTRACT(sync_diff, sync_start);
-					elapsed = INSTR_TIME_GET_MICROSEC(sync_diff);
-					if (elapsed > longest)
-						longest = elapsed;
-					total_elapsed += elapsed;
-					processed++;
-
-					if (log_checkpoints)
-						elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
-							 processed,
-							 path,
-							 (double) elapsed / 1000);
-
-					break;		/* out of retry loop */
-				}
-
-				/*
-				 * It is possible that the relation has been dropped or
-				 * truncated since the fsync request was entered. Therefore,
-				 * allow ENOENT, but only if we didn't fail already on this
-				 * file.
-				 */
-				if (!FILE_POSSIBLY_DELETED(errno) || failures > 0)
-					ereport(data_sync_elevel(ERROR),
-							(errcode_for_file_access(),
-							 errmsg("could not fsync file \"%s\": %m",
-									path)));
-				else
-					ereport(DEBUG1,
-							(errcode_for_file_access(),
-							 errmsg_internal("could not fsync file \"%s\" but retrying: %m",
-											 path)));
-
-				/*
-				 * Absorb incoming requests and check to see if a cancel
-				 * arrived for this relation fork.
-				 */
-				AbsorbSyncRequests();
-				absorb_counter = FSYNCS_PER_ABSORB; /* might as well... */
-			}					/* end retry loop */
+			entry->cycle_ctr = sync_cycle_ctr;
+
+			inflight_entry = MemoryContextAllocZero(inflightSyncCxt,
+													sizeof(InflightSyncEntry));
+			inflight_entry->tag = entry->tag;
+			inflight_entry->hash_entry = entry;
+			dlist_push_tail(&activeSyncEntries,
+							&inflight_entry->cleanup_node);
+
+			sync_start_one(&sync_state, inflight_entry);
 		}
+	}
 
-		/* We are done with this entry, remove it */
-		if (hash_search(pendingOps, &entry->tag, HASH_REMOVE, NULL) == NULL)
-			elog(ERROR, "pendingOps corrupted");
-	}							/* end loop over hashtable entries */
+	sync_drain_all(&sync_state);
+	sync_process_completed();
+
+	/*
+	 * A second failure raises an error, so normally one retry pass is enough.
+	 * Keep an explicit bound in case that changes.
+	 */
+	for (int failures = 0; failures < 5; failures++)
+	{
+		if (dlist_is_empty(&sync_state.retry))
+			break;
+
+		sync_process_retries(&sync_state);
+	}
+
+	if (!dlist_is_empty(&sync_state.inflight) ||
+		!dlist_is_empty(&sync_state.retry))
+		elog(PANIC, "in-flight sync requests remain after ProcessSyncRequests");
 
 	/* Return sync performance metrics for report at checkpoint end */
-	CheckpointStats.ckpt_sync_rels = processed;
-	CheckpointStats.ckpt_longest_sync = longest;
-	CheckpointStats.ckpt_agg_sync_time = total_elapsed;
+	CheckpointStats.ckpt_sync_rels = sync_state.processed;
+	CheckpointStats.ckpt_longest_sync = INSTR_TIME_GET_MICROSEC(sync_state.longest);
+	CheckpointStats.ckpt_agg_sync_time = INSTR_TIME_GET_MICROSEC(sync_state.total_elapsed);
 
 	/* Flag successful completion of ProcessSyncRequests */
 	sync_in_progress = false;
 }
 
+/*
+ *	ProcessSyncRequests() -- Process queued fsync requests.
+ */
+void
+ProcessSyncRequests(void)
+{
+	PG_ENSURE_ERROR_CLEANUP(sync_cleanup_inflight, (Datum) 0);
+	{
+		ProcessSyncRequestsInternal();
+	}
+	PG_END_ENSURE_ERROR_CLEANUP(sync_cleanup_inflight, (Datum) 0);
+
+	Assert(dlist_is_empty(&activeSyncEntries));
+}
+
 /*
  * RememberSyncRequest() -- callback from checkpointer side of sync request
  *
@@ -554,11 +880,20 @@ RememberSyncRequest(const FileTag *ftag, SyncRequestType type)
 												  ftag,
 												  HASH_ENTER,
 												  &found);
+
+		/*
+		 * If an entry already existed, an fsync for it may be in flight right
+		 * now, in which case it cannot be assumed to cover this request; see
+		 * sync_drain_one().
+		 */
+		entry->re_requested = found;
+
 		/* if new entry, or was previously canceled, initialize it */
 		if (!found || entry->canceled)
 		{
 			entry->cycle_ctr = sync_cycle_ctr;
 			entry->canceled = false;
+			entry->sync_completed = false;
 		}
 
 		/*
diff --git a/src/include/access/clog.h b/src/include/access/clog.h
index 7894998c763..e089106f7fe 100644
--- a/src/include/access/clog.h
+++ b/src/include/access/clog.h
@@ -47,7 +47,7 @@ extern void CheckPointCLOG(void);
 extern void ExtendCLOG(TransactionId newestXact);
 extern void TruncateCLOG(TransactionId oldestXact, Oid oldestxid_datoid);
 
-extern int	clogsyncfiletag(const FileTag *ftag, char *path);
+extern void clogsyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry);
 
 /* XLOG stuff */
 #define CLOG_ZEROPAGE		0x00
diff --git a/src/include/access/commit_ts.h b/src/include/access/commit_ts.h
index 825ccda90ed..fa4880e0d03 100644
--- a/src/include/access/commit_ts.h
+++ b/src/include/access/commit_ts.h
@@ -38,7 +38,7 @@ extern void SetCommitTsLimit(TransactionId oldestXact,
 							 TransactionId newestXact);
 extern void AdvanceOldestCommitTsXid(TransactionId oldestXact);
 
-extern int	committssyncfiletag(const FileTag *ftag, char *path);
+extern void committssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry);
 
 /* XLOG stuff */
 #define COMMIT_TS_ZEROPAGE		0x00
diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h
index 6be5299ab68..3f980b4120d 100644
--- a/src/include/access/multixact.h
+++ b/src/include/access/multixact.h
@@ -114,8 +114,8 @@ extern bool MultiXactIdPrecedes(MultiXactId multi1, MultiXactId multi2);
 extern bool MultiXactIdPrecedesOrEquals(MultiXactId multi1,
 										MultiXactId multi2);
 
-extern int	multixactoffsetssyncfiletag(const FileTag *ftag, char *path);
-extern int	multixactmemberssyncfiletag(const FileTag *ftag, char *path);
+extern void multixactoffsetssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry);
+extern void multixactmemberssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry);
 
 extern void AtEOXact_MultiXact(void);
 extern void AtPrepare_MultiXact(void);
diff --git a/src/include/access/slru.h b/src/include/access/slru.h
index b4adb1789c7..0e91df5609c 100644
--- a/src/include/access/slru.h
+++ b/src/include/access/slru.h
@@ -240,7 +240,7 @@ typedef bool (*SlruScanCallback) (SlruDesc *ctl, char *filename, int64 segpage,
 extern bool SlruScanDirectory(SlruDesc *ctl, SlruScanCallback callback, void *data);
 extern void SlruDeleteSegment(SlruDesc *ctl, int64 segno);
 
-extern int	SlruSyncFileTag(SlruDesc *ctl, const FileTag *ftag, char *path);
+extern void SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, struct InflightSyncEntry *entry);
 
 /* SlruScanDirectory public callbacks */
 extern bool SlruScanDirCbReportPresence(SlruDesc *ctl, char *filename,
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index c79f3312544..f39469058b7 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -138,6 +138,7 @@ extern int	FilePrefetch(File file, pgoff_t offset, pgoff_t amount, uint32 wait_e
 extern ssize_t FileReadV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset, uint32 wait_event_info);
 extern ssize_t FileWriteV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset, uint32 wait_event_info);
 extern int	FileStartReadV(struct PgAioHandle *ioh, File file, int iovcnt, pgoff_t offset, uint32 wait_event_info);
+extern int	FileStartSync(struct PgAioHandle *ioh, File file, bool datasync, uint32 wait_event_info);
 extern int	FileSync(File file, uint32 wait_event_info);
 extern int	FileZero(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info);
 extern int	FileFallocate(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info);
diff --git a/src/include/storage/md.h b/src/include/storage/md.h
index b8d10329eb8..53f75802ac0 100644
--- a/src/include/storage/md.h
+++ b/src/include/storage/md.h
@@ -58,7 +58,7 @@ extern void ForgetDatabaseSyncRequests(Oid dbid);
 extern void DropRelationFiles(RelFileLocator *delrels, int ndelrels, bool isRedo);
 
 /* md sync callbacks */
-extern int	mdsyncfiletag(const FileTag *ftag, char *path);
+extern void mdsyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry);
 extern int	mdunlinkfiletag(const FileTag *ftag, char *path);
 extern bool mdfiletagmatches(const FileTag *ftag, const FileTag *candidate);
 
diff --git a/src/include/storage/sync.h b/src/include/storage/sync.h
index 88290500bc9..a72ce4c3fd3 100644
--- a/src/include/storage/sync.h
+++ b/src/include/storage/sync.h
@@ -13,6 +13,9 @@
 #ifndef SYNC_H
 #define SYNC_H
 
+#include "lib/ilist.h"
+#include "portability/instr_time.h"
+#include "storage/aio_types.h"
 #include "storage/relfilelocator.h"
 
 /*
@@ -55,6 +58,59 @@ typedef struct FileTag
 	uint64		segno;
 } FileTag;
 
+struct PendingFsyncEntry;
+struct PgAioHandle;
+
+/*
+ * How the file opened by a sync handler must be closed once its asynchronous
+ * fsync has completed.
+ */
+typedef enum SyncFileCloseMethod
+{
+	SYNC_CLOSE_NONE = 0,		/* nothing to close */
+	SYNC_CLOSE_TRANSIENT,		/* CloseTransientFile(close_file) */
+	SYNC_CLOSE_VFD,				/* FileClose((File) close_file) */
+} SyncFileCloseMethod;
+
+/*
+ * State for a single in-flight asynchronous fsync request.  A sync handler
+ * opens the file to be synced, fills in the fields it is responsible for, and
+ * starts an asynchronous fsync on the AIO handle it is given.
+ */
+typedef struct InflightSyncEntry
+{
+	FileTag		tag;			/* identifies handler and file */
+
+	char		path[MAXPGPATH];
+
+	/*
+	 * Set by the handler: whether it started an asynchronous fsync on the
+	 * passed-in AIO handle.  If the file could not be opened, the handler
+	 * sets started = false and open_errno to the errno of the failed open.
+	 */
+	bool		started;
+	int			open_errno;
+
+	/* set by the handler: how to close the opened file after completion */
+	SyncFileCloseMethod close_method;
+	int			close_file;		/* fd, or File, depending on close_method */
+
+	struct PendingFsyncEntry *hash_entry;
+
+	int			retry_count;
+
+	instr_time	start_time;
+
+	PgAioReturn ioret;
+	PgAioWaitRef iow;
+
+	/* membership in the inflight / retry lists */
+	dlist_node	node;
+
+	/* membership in the error-cleanup list */
+	dlist_node	cleanup_node;
+} InflightSyncEntry;
+
 extern void InitSync(void);
 extern void SyncPreCheckpoint(void);
 extern void SyncPostCheckpoint(void);
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 40efffdbf62..ccac09a9285 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -17,10 +17,13 @@
 #include "access/slru.h"
 #include "access/transam.h"
 #include "miscadmin.h"
+#include "storage/aio.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/shmem.h"
+#include "storage/sync.h"
 #include "utils/builtins.h"
+#include "utils/resowner.h"
 
 PG_MODULE_MAGIC;
 
@@ -152,15 +155,47 @@ Datum
 test_slru_page_sync(PG_FUNCTION_ARGS)
 {
 	int64		pageno = PG_GETARG_INT64(0);
-	FileTag		ftag;
-	char		path[MAXPGPATH];
+	InflightSyncEntry entry = {0};
+	PgAioHandle *ioh;
+	int			result;
 
 	/* note that this flushes the full file a segment is located in */
-	ftag.segno = pageno / SLRU_PAGES_PER_SEGMENT;
-	SlruSyncFileTag(TestSlruCtl, &ftag, path);
+	entry.tag.segno = pageno / SLRU_PAGES_PER_SEGMENT;
+
+	/*
+	 * SlruSyncFileTag() now performs the fsync asynchronously.  Drive it the
+	 * same way sync.c does: acquire an AIO handle, let the handler start the
+	 * fsync, wait for its completion and close the file it opened.
+	 */
+	ioh = pgaio_io_acquire(CurrentResourceOwner, &entry.ioret);
+	pgaio_io_get_wref(ioh, &entry.iow);
+
+	HOLD_INTERRUPTS();
+	SlruSyncFileTag(TestSlruCtl, ioh, &entry);
+	RESUME_INTERRUPTS();
+
+	if (entry.started)
+	{
+		pgaio_wref_wait(&entry.iow);
+		result = -entry.ioret.result.result;
+		CloseTransientFile(entry.close_file);
+	}
+	else
+	{
+		pgaio_io_release(ioh);
+		result = entry.open_errno;
+	}
+
+	if (result != 0)
+	{
+		errno = result;
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not fsync file \"%s\": %m", entry.path)));
+	}
 
 	elog(NOTICE, "Called SlruSyncFileTag() for segment %" PRIu64 " on path %s",
-		 ftag.segno, path);
+		 entry.tag.segno, entry.path);
 
 	PG_RETURN_VOID();
 }
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a95b09859b5..4fd08012e1b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1361,6 +1361,7 @@ IndexVacuumInfo
 IndxInfo
 InferClause
 InferenceElem
+InflightSyncEntry
 InfoItem
 InhInfo
 InheritableSocket
@@ -3078,12 +3079,14 @@ SupportRequestSimplify
 SupportRequestSimplifyAggref
 SupportRequestWFuncMonotonic
 Syn
+SyncFileCloseMethod
 SyncOps
 SyncRepConfigData
 SyncRepStandbyData
 SyncRequestHandler
 SyncRequestType
 SyncStandbySlotsConfigData
+SyncState
 SyncingRelationsState
 SysCacheIdentifier
 SysFKRelationship
-- 
2.47.3

From 203cf306618d1440e74e7b192edc3b13b3c679a7 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <[email protected]>
Date: Tue, 25 Aug 2026 15:00:40 +0300
Subject: [PATCH v1 4/4] Allow IO workers to execute SLRU fsyncs

The SLRU fsyncs introduced by the preceding commit use the generic
PGAIO_TID_SYNC target. It does not store paths in shared memory, so I/O
workers cannot reopen the files and the checkpointer must perform the
operations itself.

Add PGAIO_TID_SYNC_FILETAG, which identifies a file by the FileTag
registered with sync.c. A FileTag contains enough information to
reopen the file in another process. Dispatch reopening through an
optional SyncOps callback, implemented for SLRUs using the new
SlruOpenFileTag(). SLRUs that are not registered with sync.c continue
to use PGAIO_TID_SYNC.

Define the shared FileTag representation in storage/aio_types.h so
PgAioTargetData can store it directly without duplicating its layout.
Describe the target and worker-side open errors using the SLRU name and
segment number.

Unlike the smgr target, the FileTag target opens a new descriptor for
each operation instead of using a cache. Add an optional
PgAioTargetInfo close callback so I/O workers release these descriptors
after executing the operation.

SLRU_FLUSH_SYNC is now reported by the I/O worker rather than the
checkpointer, as for relation fsyncs. SyncDataDirectory() continues to
use PGAIO_TID_SYNC because its files have no FileTag.
---
 src/backend/access/transam/clog.c      |   8 +-
 src/backend/access/transam/commit_ts.c |   8 +-
 src/backend/access/transam/multixact.c |  16 +++-
 src/backend/access/transam/slru.c      |  32 ++++++-
 src/backend/storage/aio/aio_io.c       |   8 ++
 src/backend/storage/aio/aio_target.c   |  36 +++++++
 src/backend/storage/sync/sync.c        | 125 ++++++++++++++++++++++++-
 src/include/access/clog.h              |   1 +
 src/include/access/commit_ts.h         |   1 +
 src/include/access/multixact.h         |   2 +
 src/include/access/slru.h              |   1 +
 src/include/storage/aio.h              |  13 ++-
 src/include/storage/aio_internal.h     |   1 +
 src/include/storage/aio_types.h        |  14 +++
 src/include/storage/sync.h             |  19 ++--
 src/tools/pgindent/typedefs.list       |   1 +
 16 files changed, 263 insertions(+), 23 deletions(-)

diff --git a/src/backend/access/transam/clog.c b/src/backend/access/transam/clog.c
index 89fb77ea5da..0fe0491d877 100644
--- a/src/backend/access/transam/clog.c
+++ b/src/backend/access/transam/clog.c
@@ -1115,10 +1115,16 @@ clog_redo(XLogReaderState *record)
 }
 
 /*
- * Entrypoint for sync.c to sync clog files.
+ * Entrypoints for sync.c to sync and reopen clog files.
  */
 void
 clogsyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry)
 {
 	SlruSyncFileTag(XactCtl, ioh, entry);
 }
+
+int
+clogopenfiletag(const FileTag *ftag)
+{
+	return SlruOpenFileTag(XactCtl, ftag);
+}
diff --git a/src/backend/access/transam/commit_ts.c b/src/backend/access/transam/commit_ts.c
index 7cbbad383b2..4bb95e415ac 100644
--- a/src/backend/access/transam/commit_ts.c
+++ b/src/backend/access/transam/commit_ts.c
@@ -1026,10 +1026,16 @@ commit_ts_redo(XLogReaderState *record)
 }
 
 /*
- * Entrypoint for sync.c to sync commit_ts files.
+ * Entrypoints for sync.c to sync and reopen commit_ts files.
  */
 void
 committssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry)
 {
 	SlruSyncFileTag(CommitTsCtl, ioh, entry);
 }
+
+int
+committsopenfiletag(const FileTag *ftag)
+{
+	return SlruOpenFileTag(CommitTsCtl, ftag);
+}
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index deb866cd7f0..11f5f266075 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -2996,7 +2996,7 @@ multixact_redo(XLogReaderState *record)
 }
 
 /*
- * Entrypoint for sync.c to sync offsets files.
+ * Entrypoints for sync.c to sync and reopen offsets files.
  */
 void
 multixactoffsetssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry)
@@ -3004,11 +3004,23 @@ multixactoffsetssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry)
 	SlruSyncFileTag(MultiXactOffsetCtl, ioh, entry);
 }
 
+int
+multixactoffsetsopenfiletag(const FileTag *ftag)
+{
+	return SlruOpenFileTag(MultiXactOffsetCtl, ftag);
+}
+
 /*
- * Entrypoint for sync.c to sync members files.
+ * Entrypoints for sync.c to sync and reopen members files.
  */
 void
 multixactmemberssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry)
 {
 	SlruSyncFileTag(MultiXactMemberCtl, ioh, entry);
 }
+
+int
+multixactmembersopenfiletag(const FileTag *ftag)
+{
+	return SlruOpenFileTag(MultiXactMemberCtl, ftag);
+}
diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c
index b1e513ac3b6..8e0d9af16df 100644
--- a/src/backend/access/transam/slru.c
+++ b/src/backend/access/transam/slru.c
@@ -1897,11 +1897,16 @@ SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, InflightSyncEntry *entry
 	}
 
 	/*
-	 * Use the generic sync target.  SLRU segments are not smgr relations and
-	 * cannot be reopened from a FileTag in another process, so this fsync
-	 * will run synchronously in worker mode.
+	 * If this SLRU is registered with sync.c, identify the file by its
+	 * FileTag, so that the fsync can be executed by an IO worker, which will
+	 * reopen the file with SlruOpenFileTag().  Otherwise there is no handler
+	 * to reopen the file through, so use the generic sync target, whose IOs
+	 * cannot be handed off to a worker.
 	 */
-	pgaio_io_set_target(ioh, PGAIO_TID_SYNC);
+	if (ctl->options.sync_handler != SYNC_HANDLER_NONE)
+		pgaio_io_set_target_sync_filetag(ioh, &entry->tag);
+	else
+		pgaio_io_set_target(ioh, PGAIO_TID_SYNC);
 
 	/* Start the asynchronous fsync; the fd is closed once it completes. */
 	pgaio_io_start_fsync(ioh, fd, false, WAIT_EVENT_SLRU_FLUSH_SYNC);
@@ -1910,3 +1915,22 @@ SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, InflightSyncEntry *entry
 	entry->close_method = SYNC_CLOSE_TRANSIENT;
 	entry->close_file = fd;
 }
+
+/*
+ * Counterpart to SlruSyncFileTag(), opening the segment identified by ftag in
+ * a process that did not stage the IO.  As with SlruSyncFileTag(), individual
+ * SLRUs have to provide the handler function, so that the correct "SlruCtl"
+ * is used.
+ *
+ * Returns a file descriptor opened with OpenTransientFile(), or -1 with errno
+ * set.
+ */
+int
+SlruOpenFileTag(SlruDesc *ctl, const FileTag *ftag)
+{
+	char		path[MAXPGPATH];
+
+	SlruFileName(ctl, path, ftag->segno);
+
+	return OpenTransientFile(path, O_RDWR | PG_BINARY);
+}
diff --git a/src/backend/storage/aio/aio_io.c b/src/backend/storage/aio/aio_io.c
index 324fb6911e2..070a5ff70b3 100644
--- a/src/backend/storage/aio/aio_io.c
+++ b/src/backend/storage/aio/aio_io.c
@@ -169,6 +169,14 @@ pgaio_io_perform_synchronously(PgAioHandle *ioh)
 	Assert(result <= INT_MAX);
 	ioh->result = result < 0 ? -errno : result;
 
+	/*
+	 * If we, rather than the process that staged the IO, opened the file,
+	 * close it again.  Has to happen after the result has been determined, as
+	 * closing may clobber errno, and before the completion is processed, as
+	 * that can recycle the handle.
+	 */
+	pgaio_io_close_reopened(ioh);
+
 	pgaio_io_process_completion(ioh, ioh->result);
 
 	END_CRIT_SECTION();
diff --git a/src/backend/storage/aio/aio_target.c b/src/backend/storage/aio/aio_target.c
index 82b24b0026d..795e8ac7046 100644
--- a/src/backend/storage/aio/aio_target.c
+++ b/src/backend/storage/aio/aio_target.c
@@ -17,6 +17,7 @@
 #include "storage/aio.h"
 #include "storage/aio_internal.h"
 #include "storage/smgr.h"
+#include "storage/sync.h"
 
 static char *pgaio_sync_describe_identity(const PgAioTargetData *sd);
 
@@ -39,8 +40,16 @@ static const PgAioTargetInfo *pgaio_target_info[] = {
 	},
 	[PGAIO_TID_SMGR] = &aio_smgr_target_info,
 	[PGAIO_TID_SYNC] = &aio_sync_target_info,
+	[PGAIO_TID_SYNC_FILETAG] = &aio_sync_filetag_target_info,
 };
 
+/*
+ * The IO whose descriptor this process reopened and whose target requires the
+ * descriptor to be released after execution.  Only set between
+ * pgaio_io_reopen() and pgaio_io_close_reopened().
+ */
+static PgAioHandle *pgaio_reopened_ioh = NULL;
+
 
 /*
  * describe_identity callback for PGAIO_TID_SYNC. As we do not store the path
@@ -141,6 +150,33 @@ pgaio_io_reopen(PgAioHandle *ioh)
 {
 	Assert(ioh->target > PGAIO_TID_INVALID && ioh->target < PGAIO_TID_COUNT);
 	Assert(ioh->op > PGAIO_OP_INVALID && ioh->op < PGAIO_OP_COUNT);
+	Assert(pgaio_reopened_ioh == NULL);
 
 	pgaio_target_info[ioh->target]->reopen(ioh);
+
+	/*
+	 * Remember that this process, rather than the one that staged the IO,
+	 * owns the file descriptor now, so that pgaio_io_close_reopened() can
+	 * release it once the IO has been executed.
+	 */
+	if (pgaio_target_info[ioh->target]->close != NULL)
+		pgaio_reopened_ioh = ioh;
+}
+
+/*
+ * Internal: Counterpart to pgaio_io_reopen(), releasing the file descriptor it
+ * acquired.  Does nothing unless this process reopened this very IO and its
+ * target needs the descriptor to be released.
+ *
+ * This has to be called before the IO's completion is processed, as that can
+ * make the handle be reused for an unrelated IO.
+ */
+void
+pgaio_io_close_reopened(PgAioHandle *ioh)
+{
+	if (pgaio_reopened_ioh != ioh)
+		return;
+
+	pgaio_reopened_ioh = NULL;
+	pgaio_target_info[ioh->target]->close(ioh);
 }
diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c
index 4263881ddd8..35f87793171 100644
--- a/src/backend/storage/sync/sync.c
+++ b/src/backend/storage/sync/sync.c
@@ -132,9 +132,19 @@ static CycleCtr checkpoint_cycle_ctr = 0;
 typedef struct SyncOps
 {
 	void		(*sync_syncfiletag) (PgAioHandle *ioh, InflightSyncEntry *entry);
+
+	/*
+	 * Optional.  Reopen the file identified by ftag, so that an fsync started
+	 * by sync_syncfiletag() can be executed in a different process, e.g. an
+	 * IO worker.  Returns a file descriptor opened with OpenTransientFile(),
+	 * or -1 with errno set.  Handlers that provide this must use the
+	 * PGAIO_TID_SYNC_FILETAG target (see pgaio_io_set_target_sync_filetag()).
+	 */
+	int			(*sync_openfiletag) (const FileTag *ftag);
 	int			(*sync_unlinkfiletag) (const FileTag *ftag, char *path);
 	bool		(*sync_filetagmatches) (const FileTag *ftag,
 										const FileTag *candidate);
+	const char *sync_target_name;
 } SyncOps;
 
 /*
@@ -149,22 +159,129 @@ static const SyncOps syncsw[] = {
 	},
 	/* pg_xact */
 	[SYNC_HANDLER_CLOG] = {
-		.sync_syncfiletag = clogsyncfiletag
+		.sync_syncfiletag = clogsyncfiletag,
+		.sync_openfiletag = clogopenfiletag,
+		.sync_target_name = "pg_xact"
 	},
 	/* pg_commit_ts */
 	[SYNC_HANDLER_COMMIT_TS] = {
-		.sync_syncfiletag = committssyncfiletag
+		.sync_syncfiletag = committssyncfiletag,
+		.sync_openfiletag = committsopenfiletag,
+		.sync_target_name = "pg_commit_ts"
 	},
 	/* pg_multixact/offsets */
 	[SYNC_HANDLER_MULTIXACT_OFFSET] = {
-		.sync_syncfiletag = multixactoffsetssyncfiletag
+		.sync_syncfiletag = multixactoffsetssyncfiletag,
+		.sync_openfiletag = multixactoffsetsopenfiletag,
+		.sync_target_name = "pg_multixact/offsets"
 	},
 	/* pg_multixact/members */
 	[SYNC_HANDLER_MULTIXACT_MEMBER] = {
-		.sync_syncfiletag = multixactmemberssyncfiletag
+		.sync_syncfiletag = multixactmemberssyncfiletag,
+		.sync_openfiletag = multixactmembersopenfiletag,
+		.sync_target_name = "pg_multixact/members"
 	}
 };
 
+static void sync_aio_reopen(PgAioHandle *ioh);
+static void sync_aio_close(PgAioHandle *ioh);
+static char *sync_aio_describe_identity(const PgAioTargetData *sd);
+
+/*
+ * Target info for files identified by a FileTag (see PGAIO_TID_SYNC_FILETAG).
+ * Unlike PGAIO_TID_SYNC, a FileTag contains everything needed to find the
+ * file again in another process, so such IOs can be executed by IO workers.
+ */
+const PgAioTargetInfo aio_sync_filetag_target_info = {
+	.name = "sync_filetag",
+	.reopen = sync_aio_reopen,
+	.close = sync_aio_close,
+	.describe_identity = sync_aio_describe_identity,
+};
+
+/*
+ * Set up ioh to operate on the file identified by ftag.
+ */
+void
+pgaio_io_set_target_sync_filetag(PgAioHandle *ioh, const FileTag *ftag)
+{
+	PgAioTargetData *sd = pgaio_io_get_target_data(ioh);
+
+	Assert(syncsw[ftag->handler].sync_openfiletag != NULL);
+	Assert(syncsw[ftag->handler].sync_target_name != NULL);
+
+	pgaio_io_set_target(ioh, PGAIO_TID_SYNC_FILETAG);
+
+	sd->sync_filetag = *ftag;
+}
+
+static FileTag
+sync_aio_filetag(const PgAioTargetData *sd)
+{
+	return sd->sync_filetag;
+}
+
+/*
+ * reopen callback for PGAIO_TID_SYNC_FILETAG, to open the file in the process
+ * executing the IO.
+ */
+static void
+sync_aio_reopen(PgAioHandle *ioh)
+{
+	PgAioTargetData *sd = pgaio_io_get_target_data(ioh);
+	PgAioOpData *od = pgaio_io_get_op_data(ioh);
+	FileTag		ftag = sync_aio_filetag(sd);
+	int			fd;
+
+	/*
+	 * The caller needs to prevent interrupts from being processed, otherwise
+	 * the FD could be closed again before we get to executing the IO.
+	 */
+	Assert(!INTERRUPTS_CAN_BE_PROCESSED());
+
+	Assert(pgaio_io_get_op(ioh) == PGAIO_OP_FSYNC);
+
+	fd = syncsw[ftag.handler].sync_openfiletag(&ftag);
+	if (fd < 0)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open segment " UINT64_FORMAT " of SLRU \"%s\": %m",
+						ftag.segno,
+						syncsw[ftag.handler].sync_target_name)));
+
+	od->fsync.fd = fd;
+}
+
+/*
+ * close callback for PGAIO_TID_SYNC_FILETAG, releasing the descriptor
+ * acquired by sync_aio_reopen().
+ *
+ * Called in a critical section, so a failure to close cannot be reported.
+ * That is not a meaningful loss: the data has already been flushed by the
+ * fsync, and the descriptor is not written to.
+ */
+static void
+sync_aio_close(PgAioHandle *ioh)
+{
+	PgAioOpData *od = pgaio_io_get_op_data(ioh);
+
+	(void) CloseTransientFile(od->fsync.fd);
+	od->fsync.fd = -1;
+}
+
+/*
+ * describe_identity callback for PGAIO_TID_SYNC_FILETAG.
+ */
+static char *
+sync_aio_describe_identity(const PgAioTargetData *sd)
+{
+	FileTag		ftag = sync_aio_filetag(sd);
+
+	return psprintf(_("segment " UINT64_FORMAT " of SLRU \"%s\""),
+					ftag.segno,
+					syncsw[ftag.handler].sync_target_name);
+}
+
 /*
  * Initialize data structures for the file sync tracking.
  */
diff --git a/src/include/access/clog.h b/src/include/access/clog.h
index e089106f7fe..fc06794d186 100644
--- a/src/include/access/clog.h
+++ b/src/include/access/clog.h
@@ -48,6 +48,7 @@ extern void ExtendCLOG(TransactionId newestXact);
 extern void TruncateCLOG(TransactionId oldestXact, Oid oldestxid_datoid);
 
 extern void clogsyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry);
+extern int	clogopenfiletag(const FileTag *ftag);
 
 /* XLOG stuff */
 #define CLOG_ZEROPAGE		0x00
diff --git a/src/include/access/commit_ts.h b/src/include/access/commit_ts.h
index fa4880e0d03..ebb87d9534e 100644
--- a/src/include/access/commit_ts.h
+++ b/src/include/access/commit_ts.h
@@ -39,6 +39,7 @@ extern void SetCommitTsLimit(TransactionId oldestXact,
 extern void AdvanceOldestCommitTsXid(TransactionId oldestXact);
 
 extern void committssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry);
+extern int	committsopenfiletag(const FileTag *ftag);
 
 /* XLOG stuff */
 #define COMMIT_TS_ZEROPAGE		0x00
diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h
index 3f980b4120d..3f5233a3bcd 100644
--- a/src/include/access/multixact.h
+++ b/src/include/access/multixact.h
@@ -115,7 +115,9 @@ extern bool MultiXactIdPrecedesOrEquals(MultiXactId multi1,
 										MultiXactId multi2);
 
 extern void multixactoffsetssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry);
+extern int	multixactoffsetsopenfiletag(const FileTag *ftag);
 extern void multixactmemberssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry);
+extern int	multixactmembersopenfiletag(const FileTag *ftag);
 
 extern void AtEOXact_MultiXact(void);
 extern void AtPrepare_MultiXact(void);
diff --git a/src/include/access/slru.h b/src/include/access/slru.h
index 0e91df5609c..43429392c7d 100644
--- a/src/include/access/slru.h
+++ b/src/include/access/slru.h
@@ -241,6 +241,7 @@ extern bool SlruScanDirectory(SlruDesc *ctl, SlruScanCallback callback, void *da
 extern void SlruDeleteSegment(SlruDesc *ctl, int64 segno);
 
 extern void SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, struct InflightSyncEntry *entry);
+extern int	SlruOpenFileTag(SlruDesc *ctl, const FileTag *ftag);
 
 /* SlruScanDirectory public callbacks */
 extern bool SlruScanDirCbReportPresence(SlruDesc *ctl, char *filename,
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.h
index 428504c0472..904ab185458 100644
--- a/src/include/storage/aio.h
+++ b/src/include/storage/aio.h
@@ -119,9 +119,10 @@ typedef enum PgAioTargetID
 	PGAIO_TID_INVALID = 0,
 	PGAIO_TID_SMGR,
 	PGAIO_TID_SYNC,
+	PGAIO_TID_SYNC_FILETAG,
 } PgAioTargetID;
 
-#define PGAIO_TID_COUNT (PGAIO_TID_SYNC + 1)
+#define PGAIO_TID_COUNT (PGAIO_TID_SYNC_FILETAG + 1)
 
 
 /*
@@ -171,6 +172,16 @@ struct PgAioTargetInfo
 	 */
 	void		(*reopen) (PgAioHandle *ioh);
 
+	/*
+	 * Optional counterpart to reopen, releasing the file descriptor it
+	 * acquired.  Called in the process that reopened the IO, after the IO has
+	 * been executed.  Targets whose reopen callback hands out a descriptor
+	 * that is cached and reused, like smgr's, do not need this.
+	 *
+	 * This is called in a critical section, so it must not raise errors.
+	 */
+	void		(*close) (PgAioHandle *ioh);
+
 	/* describe the target of the IO, used for log messages and views */
 	char	   *(*describe_identity) (const PgAioTargetData *sd);
 
diff --git a/src/include/storage/aio_internal.h b/src/include/storage/aio_internal.h
index 9ca4087aa7f..9d2e92c6538 100644
--- a/src/include/storage/aio_internal.h
+++ b/src/include/storage/aio_internal.h
@@ -360,6 +360,7 @@ extern int	pgaio_io_get_iovec_length(PgAioHandle *ioh, struct iovec **iov);
 /* aio_target.c */
 extern bool pgaio_io_can_reopen(PgAioHandle *ioh);
 extern void pgaio_io_reopen(PgAioHandle *ioh);
+extern void pgaio_io_close_reopened(PgAioHandle *ioh);
 extern const char *pgaio_io_get_target_name(PgAioHandle *ioh);
 
 
diff --git a/src/include/storage/aio_types.h b/src/include/storage/aio_types.h
index 17b59aeed7c..90bc4c73989 100644
--- a/src/include/storage/aio_types.h
+++ b/src/include/storage/aio_types.h
@@ -23,6 +23,18 @@ typedef struct PgAioHandle PgAioHandle;
 typedef struct PgAioHandleCallbacks PgAioHandleCallbacks;
 typedef struct PgAioTargetInfo PgAioTargetInfo;
 
+/*
+ * A tag identifying a file handled by sync.c.  This is defined here so that
+ * PgAioTargetData can store it without duplicating its representation.
+ */
+typedef struct PgAioSyncFileTag
+{
+	int16		handler;		/* SyncRequestHandler value */
+	int16		forknum;		/* ForkNumber */
+	RelFileLocator rlocator;	/* physical relation identifier */
+	uint64		segno;
+} PgAioSyncFileTag;
+
 /*
  * A reference to an IO that can be used to wait for the IO (using
  * pgaio_wref_wait()) to complete.
@@ -69,6 +81,8 @@ typedef union PgAioTargetData
 		bool		is_temp:1;	/* proc can be inferred by owning AIO */
 		bool		skip_fsync:1;
 	}			smgr;
+
+	PgAioSyncFileTag sync_filetag;
 } PgAioTargetData;
 
 
diff --git a/src/include/storage/sync.h b/src/include/storage/sync.h
index a72ce4c3fd3..4a2cca50703 100644
--- a/src/include/storage/sync.h
+++ b/src/include/storage/sync.h
@@ -46,17 +46,11 @@ typedef enum SyncRequestHandler
 } SyncRequestHandler;
 
 /*
- * 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
- * liable to change as required by future handlers.
+ * A tag identifying a file.  Its representation is shared with AIO target
+ * data, so changes are automatically visible to processes that reopen files
+ * on behalf of sync.c.
  */
-typedef struct FileTag
-{
-	int16		handler;		/* SyncRequestHandler value, saving space */
-	int16		forknum;		/* ForkNumber, saving space */
-	RelFileLocator rlocator;
-	uint64		segno;
-} FileTag;
+typedef PgAioSyncFileTag FileTag;
 
 struct PendingFsyncEntry;
 struct PgAioHandle;
@@ -119,4 +113,9 @@ extern void RememberSyncRequest(const FileTag *ftag, SyncRequestType type);
 extern bool RegisterSyncRequest(const FileTag *ftag, SyncRequestType type,
 								bool retryOnError);
 
+/* AIO support */
+extern PGDLLIMPORT const PgAioTargetInfo aio_sync_filetag_target_info;
+extern void pgaio_io_set_target_sync_filetag(PgAioHandle *ioh,
+											 const FileTag *ftag);
+
 #endif							/* SYNC_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 4fd08012e1b..0812aae6793 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2285,6 +2285,7 @@ PgAioOpData
 PgAioResult
 PgAioResultStatus
 PgAioReturn
+PgAioSyncFileTag
 PgAioTargetData
 PgAioTargetID
 PgAioTargetInfo
-- 
2.47.3

Attachment: benchmark-repro.sh
Description: application/shellscript

Reply via email to