From: Andres Freund <andres@anarazel.de>
To: Antonin Houska <ah@cybertec.at>
Cc: Noah Misch <noah@leadboat.com>
Cc: pgsql-hackers@postgresql.org, Thomas Munro <thomas.munro@gmail.com>
Cc: Heikki Linnakangas <hlinnaka@iki.fi>
Cc: Robert Haas <robertmhaas@gmail.com>
Cc: Jakub Wartak <jakub.wartak@enterprisedb.com>
Cc: Jelte Fennema-Nio <postgres@jeltef.nl>
Subject: Re: AIO v2.5
Date: Thu, 20 Mar 2025 21:58:37 -0400
Message-ID: <6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22> (raw)
In-Reply-To: <zp7obmaqdd6b5u2vhpw27hovpjirtqvjakkbkaz4vju7tohyul@4kfobe7ipgxm>
References: <ge6nsuddurhpmll3xj22vucvqwp4agqz6ndtcf2mhyeydzarst@l75dman5x53p>
<20250311194108.c5.nmisch@google.com>
<5dzyoduxlvfg55oqtjyjehez5uoq6hnwgzor4kkybkfdgkj7ag@rbi4gsmzaczk>
<20250312035743.f5.nmisch@google.com>
<w6uiicyou7hzq47mbyejubtcyb2rngkkf45fk4q7inue5kfbeo@bbfad3qyubvs>
<4b3f32ug3cayekysqlgspz2qjmeb7lca3gvazayglxr2m3d4dv@il33accgsji7>
<17906.1741863183@localhost>
<3yxd5r23zly5bytvgyktbxtxq2r3gbpi7xd4dugevh3h4w4q6c@lu6oatjjpltz>
<oav52ytakcvfnk52w5o5wvxdjavxth2tgptxt25gnfmh2pdcpk@omp3xukdu5if>
<zp7obmaqdd6b5u2vhpw27hovpjirtqvjakkbkaz4vju7tohyul@4kfobe7ipgxm>
Hi,
Attached v2.11, with the following changes:
- Pushed the smgr interrupt change, as discussed on the dedicated thread
- Pushed "bufmgr: Improve stats when a buffer is read in concurrently"
It was reviewed by Melanie and there didn't seem to be any reason to wait
further.
- Addressed feedback from Melanie
- Addressed feedback from Noah
- Added a new commit: aio: Change prefix of PgAioResultStatus values to PGAIO_RS_
As suggested/requested by Melanie. I think she's unfortunately right.
- Added a patch for some comment fixups for code that's either older or
already pushed
- Added an error check for FileStartReadV() failing
FileStartReadV() actually can fail, if the file can't be re-opened. I
thought it'd be important for the error message to differ from the one
that's issued for read actually failing, so I went with:
"could not start reading blocks %u..%u in file \"%s\": %m"
but I'm not sure how good that is.
- Added a new commit to redefine set_max_safe_fds() to not subtract
already_open fds from max_files_per_process
This prevents io_method=io_uring from failing when RLIMIT_NOFILE is high
enough, but more than max_files_per_process io_uring instances need to be
created.
- Improved error message if io_uring_queue_init() fails
Added errhint()s for likely cases of failure.
Added errcode(). I was tempted to use errcode_for_file_access(), but that
doesn't support ENOSYS - perhaps I should add that instead?
- Disable io_uring method when using EXEC_BACKEND, they're not compatible
I chose to do this with a define aio.h, but I guess we could also do it at
configure time? That seems more complicated though - how would we even know
that EXEC_BACKEND is used on non-windows?
Not sure yet how to best disable testing io_uring in this case. We can't
just query EXEC_BACKEND from pg_config.h unfortunately. I guess making the
initdb not fail and checking the error log would work, but that doesn't work
nicely with Cluster.pm.
- Changed test_aio injection short-read injection point to zero out the rest
of the IO, otherwise some tests fail to fail even if a bug in retries of
partial reads is introduced
- Improved method_io_uring.c includes a bit (no pgstat.h)
Questions:
- We only "look" at BM_IO_ERROR for writes, isn't that somewhat weird?
See AbortBufferIO(Buffer buffer)
It doesn't really matter for the patchset, but it just strikes me as an oddity.
Greetings,
Andres Freund
Attachments:
[text/x-diff] v2.11-0001-aio-bufmgr-Comment-fixes.patch (2.8K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/2-v2.11-0001-aio-bufmgr-Comment-fixes.patch)
download | inline diff:
From 3432ae5d2421ab49bce015c6a64bb256ae36625d Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Thu, 20 Mar 2025 11:32:10 -0400
Subject: [PATCH v2.11 01/27] aio, bufmgr: Comment fixes
Some of these comments have been wrong for a while (12f3867f5534), some I
recently introduced (da7226993fd, 55b454d0e14). This also updates a comment in
FlushBuffer(), which will be copied in a future commit.
These changes seem big enough to be worth doing in separate commits.
Suggested-by: Noah Misch <noah@leadboat.com>
Discussion: https://postgr.es/m/20250319212530.80.nmisch@google.com
---
src/include/storage/aio.h | 2 +-
src/backend/postmaster/postmaster.c | 2 +-
src/backend/storage/buffer/bufmgr.c | 10 ++++------
3 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.hindex 7b6b7d20a85..a221c6c3d9a 100644--- a/src/include/storage/aio.h+++ b/src/include/storage/aio.h@@ -72,7 +72,7 @@ typedef enum PgAioHandleFlags
/*
* The IO operations supported by the AIO subsystem.
*
- * This could be in aio_internal.h, as it is not pubicly referenced, but+ * This could be in aio_internal.h, as it is not publicly referenced, but
* PgAioOpData currently *does* need to be public, therefore keeping this
* public seems to make sense.
*/
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.cindex a0c37532d2f..c966c2e83af 100644--- a/src/backend/postmaster/postmaster.c+++ b/src/backend/postmaster/postmaster.c@@ -4401,7 +4401,7 @@ maybe_adjust_io_workers(void)
++io_worker_count;
}
else
- break; /* XXX try again soon? */+ break; /* try again next time */
}
/* Too many running? */
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.cindex 14fc1bd1248..37fb09d8b92 100644--- a/src/backend/storage/buffer/bufmgr.c+++ b/src/backend/storage/buffer/bufmgr.c@@ -3864,9 +3864,10 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
XLogFlush(recptr);
/*
- * Now it's safe to write buffer to disk. Note that no one else should- * have been able to write it while we were busy with log flushing because- * only one process at a time can set the BM_IO_IN_PROGRESS bit.+ * Now it's safe to write the buffer to disk. Note that no one else should+ * have been able to write it, while we were busy with log flushing,+ * because we got the exclusive right to perform I/O by setting the+ * BM_IO_IN_PROGRESS bit.
*/
bufBlock = BufHdrGetBlock(buf);
@@ -5434,9 +5435,6 @@ IsBufferCleanupOK(Buffer buffer)
/*
* Functions for buffer I/O handling
*
- * Note: We assume that nested buffer I/O never occurs.- * i.e at most one BM_IO_IN_PROGRESS bit is set per proc.- *
* Also note that these are used only for shared buffers, not local ones.
*/
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0002-aio-Change-prefix-of-PgAioResultStatus-values-.patch (4.0K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/3-v2.11-0002-aio-Change-prefix-of-PgAioResultStatus-values-.patch)
download | inline diff:
From 3a7fd5b1c1e21955f1bd696873f2836e429f6801 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Wed, 19 Mar 2025 11:30:26 -0400
Subject: [PATCH v2.11 02/27] aio: Change prefix of PgAioResultStatus values to
PGAIO_RS_
The previous prefix wasn't consistent with the naming of other AIO related
enum values.
Reported-by: Melanie Plageman <melanieplageman@gmail.com>
Discussion: https://postgr.es/m/CAAKRu_Yb+JzQpNsgUxCB0gBi+sE-mi_HmcJF6ALnmO4W+UgwpA@mail.gmail.com
---
src/include/storage/aio_types.h | 8 ++++----
src/backend/storage/aio/aio.c | 12 ++++++------
src/backend/storage/aio/aio_callback.c | 6 +++---
src/backend/storage/aio/aio_init.c | 2 +-
4 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/src/include/storage/aio_types.h b/src/include/storage/aio_types.hindex a5cc658efbd..dddda3a3e2f 100644--- a/src/include/storage/aio_types.h+++ b/src/include/storage/aio_types.h@@ -73,10 +73,10 @@ typedef union PgAioTargetData
*/
typedef enum PgAioResultStatus
{
- ARS_UNKNOWN, /* not yet completed / uninitialized */- ARS_OK,- ARS_PARTIAL, /* did not fully succeed, but no error */- ARS_ERROR,+ PGAIO_RS_UNKNOWN, /* not yet completed / uninitialized */+ PGAIO_RS_OK,+ PGAIO_RS_PARTIAL, /* did not fully succeed, but no error */+ PGAIO_RS_ERROR,
} PgAioResultStatus;
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.cindex 3ed4b1dfdac..29f57f9cd1c 100644--- a/src/backend/storage/aio/aio.c+++ b/src/backend/storage/aio/aio.c@@ -216,7 +216,7 @@ pgaio_io_acquire_nb(struct ResourceOwnerData *resowner, PgAioReturn *ret)
if (ret)
{
ioh->report_return = ret;
- ret->result.status = ARS_UNKNOWN;+ ret->result.status = PGAIO_RS_UNKNOWN;
}
return ioh;
@@ -669,7 +669,7 @@ pgaio_io_reclaim(PgAioHandle *ioh)
ioh->handle_data_len = 0;
ioh->report_return = NULL;
ioh->result = 0;
- ioh->distilled_result.status = ARS_UNKNOWN;+ ioh->distilled_result.status = PGAIO_RS_UNKNOWN;
/* XXX: the barrier is probably superfluous */
pg_write_barrier();
@@ -829,13 +829,13 @@ pgaio_result_status_string(PgAioResultStatus rs)
{
switch (rs)
{
- case ARS_UNKNOWN:+ case PGAIO_RS_UNKNOWN:
return "UNKNOWN";
- case ARS_OK:+ case PGAIO_RS_OK:
return "OK";
- case ARS_PARTIAL:+ case PGAIO_RS_PARTIAL:
return "PARTIAL";
- case ARS_ERROR:+ case PGAIO_RS_ERROR:
return "ERROR";
}
diff --git a/src/backend/storage/aio/aio_callback.c b/src/backend/storage/aio/aio_callback.cindex d5a2cca28f1..09f03f296f5 100644--- a/src/backend/storage/aio/aio_callback.c+++ b/src/backend/storage/aio/aio_callback.c@@ -164,8 +164,8 @@ pgaio_result_report(PgAioResult result, const PgAioTargetData *target_data, int
PgAioHandleCallbackID cb_id = result.id;
const PgAioHandleCallbacksEntry *ce = &aio_handle_cbs[cb_id];
- Assert(result.status != ARS_UNKNOWN);- Assert(result.status != ARS_OK);+ Assert(result.status != PGAIO_RS_UNKNOWN);+ Assert(result.status != PGAIO_RS_OK);
if (ce->cb->report == NULL)
elog(ERROR, "callback %d/%s does not have report callback",
@@ -220,7 +220,7 @@ pgaio_io_call_complete_shared(PgAioHandle *ioh)
Assert(ioh->target > PGAIO_TID_INVALID && ioh->target < PGAIO_TID_COUNT);
Assert(ioh->op > PGAIO_OP_INVALID && ioh->op < PGAIO_OP_COUNT);
- result.status = ARS_OK; /* low level IO is always considered OK */+ result.status = PGAIO_RS_OK; /* low level IO is always considered OK */
result.result = ioh->result;
result.id = PGAIO_HCB_INVALID;
result.error_data = 0;
diff --git a/src/backend/storage/aio/aio_init.c b/src/backend/storage/aio/aio_init.cindex 2ede7e80b65..885c3940c66 100644--- a/src/backend/storage/aio/aio_init.c+++ b/src/backend/storage/aio/aio_init.c@@ -202,7 +202,7 @@ AioShmemInit(void)
ioh->report_return = NULL;
ioh->resowner = NULL;
ioh->num_callbacks = 0;
- ioh->distilled_result.status = ARS_UNKNOWN;+ ioh->distilled_result.status = PGAIO_RS_UNKNOWN;
ioh->flags = 0;
ConditionVariableInit(&ioh->cv);
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0003-Redefine-max_files_per_process-to-control-addi.patch (4.4K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/4-v2.11-0003-Redefine-max_files_per_process-to-control-addi.patch)
download | inline diff:
From 5f29c2c22ef09854687ca3dda6c7648090684648 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Wed, 19 Mar 2025 13:17:41 -0400
Subject: [PATCH v2.11 03/27] Redefine max_files_per_process to control
additionally opened files
Until now max_files_per_process=N limited each backend to open N files in
total (minus a safety factor), even if there were already more files opened in
postmaster and inherited by backends. Change max_files_per_process to control
how many additional files each process is allowed to open.
The main motivation for this is the patch to add io_method=io_uring, which
needs to open one file for each backend. Without this patch, even if
RLIMIT_NOFILE is high enough, postmaster will fail in set_max_safe_fds() if
started with a high max_connections. The cause of the failure is that, until
now, set_max_safe_fds() subtracted the already open files from
max_files_per_process.
Discussion: https://postgr.es/m/w6uiicyou7hzq47mbyejubtcyb2rngkkf45fk4q7inue5kfbeo@bbfad3qyubvs
Discussion: https://postgr.es/m/CAGECzQQh6VSy3KG4pN1d=h9J=D1rStFCMR+t7yh_Kwj-g87aLQ@mail.gmail.com
---
src/backend/storage/file/fd.c | 14 ++++++++------
src/backend/utils/misc/guc_tables.c | 2 +-
doc/src/sgml/config.sgml | 8 ++++++--
3 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.cindex 62f1185859f..0c3a2a756e7 100644--- a/src/backend/storage/file/fd.c+++ b/src/backend/storage/file/fd.c@@ -1047,16 +1047,17 @@ set_max_safe_fds(void)
/*----------
* We want to set max_safe_fds to
- * MIN(usable_fds, max_files_per_process - already_open)+ * MIN(usable_fds, max_files_per_process)
* less the slop factor for files that are opened without consulting
- * fd.c. This ensures that we won't exceed either max_files_per_process- * or the experimentally-determined EMFILE limit.+ * fd.c. This ensures that we won't allow to open more than+ * max_files_per_process, or the experimentally-determined EMFILE limit,+ * additional files.
*----------
*/
count_usable_fds(max_files_per_process,
&usable_fds, &already_open);
- max_safe_fds = Min(usable_fds, max_files_per_process - already_open);+ max_safe_fds = Min(usable_fds, max_files_per_process);
/*
* Take off the FDs reserved for system() etc.
@@ -1070,9 +1071,10 @@ set_max_safe_fds(void)
ereport(FATAL,
(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
errmsg("insufficient file descriptors available to start server process"),
- errdetail("System allows %d, server needs at least %d.",+ errdetail("System allows %d, server needs at least %d, %d files are already open.",
max_safe_fds + NUM_RESERVED_FDS,
- FD_MINFREE + NUM_RESERVED_FDS)));+ FD_MINFREE + NUM_RESERVED_FDS,+ already_open)));
elog(DEBUG2, "max_safe_fds = %d, usable_fds = %d, already_open = %d",
max_safe_fds, usable_fds, already_open);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.cindex 97cfd6e5a82..75bb0acdf0f 100644--- a/src/backend/utils/misc/guc_tables.c+++ b/src/backend/utils/misc/guc_tables.c@@ -2680,7 +2680,7 @@ struct config_int ConfigureNamesInt[] =
{
{"max_files_per_process", PGC_POSTMASTER, RESOURCES_KERNEL,
- gettext_noop("Sets the maximum number of simultaneously open files for each server process."),+ gettext_noop("Sets the maximum number of files each server process is allowed to open simultaneously."),
NULL
},
&max_files_per_process,
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgmlindex bdcefa8140b..c704f3f98f1 100644--- a/doc/src/sgml/config.sgml+++ b/doc/src/sgml/config.sgml@@ -2378,8 +2378,12 @@ include_dir 'conf.d'
</term>
<listitem>
<para>
- Sets the maximum number of simultaneously open files allowed to each- server subprocess. The default is one thousand files. If the kernel is enforcing+ Sets the maximum number of open files each server subprocess is+ allowed to open simultaneously, in addition to the files already open+ in postmaster. The default is one thousand files.+ </para>+ <para>+ If the kernel is enforcing
a safe per-process limit, you don't need to worry about this setting.
But on some platforms (notably, most BSD systems), the kernel will
allow individual processes to open many more files than the system
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0004-aio-Add-liburing-dependency.patch (13.4K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/5-v2.11-0004-aio-Add-liburing-dependency.patch)
download | inline diff:
From e5e829d3476ac26b41cb2ee0b056150d28c8aa54 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:05 -0400
Subject: [PATCH v2.11 04/27] aio: Add liburing dependency
Will be used in a subsequent commit, to implement io_method=io_uring. Kept
separate for easier review.
Reviewed-by: Noah Misch <noah@leadboat.com>
Discussion: https://postgr.es/m/uvrtrknj4kdytuboidbhwclo4gxhswwcpgadptsjvjqcluzmah%40brqs62irg4dt
---
meson.build | 14 ++++
meson_options.txt | 3 +
configure.ac | 11 +++
src/makefiles/meson.build | 3 +
src/include/pg_config.h.in | 3 +
src/backend/Makefile | 7 +-
doc/src/sgml/installation.sgml | 34 ++++++++
configure | 138 +++++++++++++++++++++++++++++++++
.cirrus.tasks.yml | 1 +
src/Makefile.global.in | 4 +
10 files changed, 215 insertions(+), 3 deletions(-)
diff --git a/meson.build b/meson.buildindex 7cf518a2765..108e3678071 100644--- a/meson.build+++ b/meson.build@@ -944,6 +944,18 @@ endif+###############################################################+# Library: liburing+###############################################################++liburingopt = get_option('liburing')+liburing = dependency('liburing', required: liburingopt)+if liburing.found()+ cdata.set('USE_LIBURING', 1)+endif+++
###############################################################
# Library: libxml
###############################################################
@@ -3164,6 +3176,7 @@ backend_both_deps += [
icu_i18n,
ldap,
libintl,
+ liburing,
libxml,
lz4,
pam,
@@ -3819,6 +3832,7 @@ if meson.version().version_compare('>=0.57')
'icu': icu,
'ldap': ldap,
'libcurl': libcurl,
+ 'liburing': liburing,
'libxml': libxml,
'libxslt': libxslt,
'llvm': llvm,
diff --git a/meson_options.txt b/meson_options.txtindex 702c4517145..dd7126da3a7 100644--- a/meson_options.txt+++ b/meson_options.txt@@ -106,6 +106,9 @@ option('libcurl', type : 'feature', value: 'auto',
option('libedit_preferred', type: 'boolean', value: false,
description: 'Prefer BSD Libedit over GNU Readline')
+option('liburing', type : 'feature', value: 'auto',+ description: 'io_uring support, for asynchronous I/O')+
option('libxml', type: 'feature', value: 'auto',
description: 'XML support')
diff --git a/configure.ac b/configure.acindex b6d02f5ecc7..3e2d0dc59c0 100644--- a/configure.ac+++ b/configure.ac@@ -975,6 +975,14 @@ AC_SUBST(with_readline)
PGAC_ARG_BOOL(with, libedit-preferred, no,
[prefer BSD Libedit over GNU Readline])
+#+# liburing+#+AC_MSG_CHECKING([whether to build with liburing support])+PGAC_ARG_BOOL(with, liburing, no, [io_uring support, for asynchronous I/O],+ [AC_DEFINE([USE_LIBURING], 1, [Define to build with io_uring support. (--with-liburing)])])+AC_MSG_RESULT([$with_liburing])+AC_SUBST(with_liburing)
#
# UUID library
@@ -1463,6 +1471,9 @@ elif test "$with_uuid" = ossp ; then
fi
AC_SUBST(UUID_LIBS)
+if test "$with_liburing" = yes; then+ PKG_CHECK_MODULES(LIBURING, liburing)+fi
##
## Header files
diff --git a/src/makefiles/meson.build b/src/makefiles/meson.buildindex 60e13d50235..46d8da070e8 100644--- a/src/makefiles/meson.build+++ b/src/makefiles/meson.build@@ -199,6 +199,8 @@ pgxs_empty = [
'PTHREAD_CFLAGS', 'PTHREAD_LIBS',
'ICU_LIBS',
++ 'LIBURING_CFLAGS', 'LIBURING_LIBS',
]
if host_system == 'windows' and cc.get_argument_syntax() != 'msvc'
@@ -230,6 +232,7 @@ pgxs_deps = {
'icu': icu,
'ldap': ldap,
'libcurl': libcurl,
+ 'liburing': liburing,
'libxml': libxml,
'libxslt': libxslt,
'llvm': llvm,
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.inindex db6454090d2..f2422241133 100644--- a/src/include/pg_config.h.in+++ b/src/include/pg_config.h.in@@ -672,6 +672,9 @@
/* Define to 1 to build with libcurl support. (--with-libcurl) */
#undef USE_LIBCURL
+/* Define to build with io_uring support. (--with-liburing) */+#undef USE_LIBURING+
/* Define to 1 to build with XML support. (--with-libxml) */
#undef USE_LIBXML
diff --git a/src/backend/Makefile b/src/backend/Makefileindex 42d4a28e5aa..7344c8c7f5c 100644--- a/src/backend/Makefile+++ b/src/backend/Makefile@@ -43,9 +43,10 @@ OBJS = \
$(top_builddir)/src/common/libpgcommon_srv.a \
$(top_builddir)/src/port/libpgport_srv.a
-# We put libpgport and libpgcommon into OBJS, so remove it from LIBS; also add-# libldap and ICU-LIBS := $(filter-out -lpgport -lpgcommon, $(LIBS)) $(LDAP_LIBS_BE) $(ICU_LIBS)+# We put libpgport and libpgcommon into OBJS, so remove it from LIBS.+LIBS := $(filter-out -lpgport -lpgcommon, $(LIBS))+# The backend conditionally needs libraries that most executables don't need.+LIBS += $(LDAP_LIBS_BE) $(ICU_LIBS) $(LIBURING_LIBS)
# The backend doesn't need everything that's in LIBS, however
LIBS := $(filter-out -lreadline -ledit -ltermcap -lncurses -lcurses, $(LIBS))
diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgmlindex e076cefa3b9..cc28f041330 100644--- a/doc/src/sgml/installation.sgml+++ b/doc/src/sgml/installation.sgml@@ -1156,6 +1156,24 @@ build-postgresql:
</listitem>
</varlistentry>
+ <varlistentry id="configure-option-with-liburing">+ <term><option>--with-liburing</option></term>+ <listitem>+ <para>+ Build with liburing, enabling io_uring support for asynchronous I/O.+ </para>+ <para>+ To detect the required compiler and linker options, PostgreSQL will+ query <command>pkg-config</command>.+ </para>+ <para>+ To use a liburing installation that is in an unusual location, you+ can set <command>pkg-config</command>-related environment+ variables (see its documentation).+ </para>+ </listitem>+ </varlistentry>+
<varlistentry id="configure-option-with-libxml">
<term><option>--with-libxml</option></term>
<listitem>
@@ -2611,6 +2629,22 @@ ninja install
</listitem>
</varlistentry>
+ <varlistentry id="configure-with-liburing-meson">+ <term><option>-Dliburing={ auto | enabled | disabled }</option></term>+ <listitem>+ <para>+ Build with liburing, enabling io_uring support for asynchronous I/O.+ Defaults to auto.+ </para>++ <para>+ To use a liburing installation that is in an unusual location, you+ can set <command>pkg-config</command>-related environment+ variables (see its documentation).+ </para>+ </listitem>+ </varlistentry>+
<varlistentry id="configure-with-libxml-meson">
<term><option>-Dlibxml={ auto | enabled | disabled }</option></term>
<listitem>
diff --git a/configure b/configureindex fac1e9a4e39..e98ed535185 100755--- a/configure+++ b/configure@@ -651,6 +651,8 @@ LIBOBJS
OPENSSL
ZSTD
LZ4
+LIBURING_LIBS+LIBURING_CFLAGS
UUID_LIBS
LDAP_LIBS_BE
LDAP_LIBS_FE
@@ -712,6 +714,7 @@ LIBCURL_LIBS
LIBCURL_CFLAGS
with_libcurl
with_uuid
+with_liburing
with_readline
with_systemd
with_selinux
@@ -865,6 +868,7 @@ with_selinux
with_systemd
with_readline
with_libedit_preferred
+with_liburing
with_uuid
with_ossp_uuid
with_libcurl
@@ -911,6 +915,8 @@ LDFLAGS_EX
LDFLAGS_SL
PERL
PYTHON
+LIBURING_CFLAGS+LIBURING_LIBS
MSGFMT
TCLSH'
@@ -1578,6 +1584,7 @@ Optional Packages:
--without-readline do not use GNU Readline nor BSD Libedit for editing
--with-libedit-preferred
prefer BSD Libedit over GNU Readline
+ --with-liburing io_uring support, for asynchronous I/O
--with-uuid=LIB build contrib/uuid-ossp using LIB (bsd,e2fs,ossp)
--with-ossp-uuid obsolete spelling of --with-uuid=ossp
--with-libcurl build with libcurl support
@@ -1629,6 +1636,10 @@ Some influential environment variables:
LDFLAGS_SL extra linker flags for linking shared libraries only
PERL Perl program
PYTHON Python program
+ LIBURING_CFLAGS+ C compiler flags for LIBURING, overriding pkg-config+ LIBURING_LIBS+ linker flags for LIBURING, overriding pkg-config
MSGFMT msgfmt program for NLS
TCLSH Tcl interpreter program (tclsh)
@@ -8692,6 +8703,40 @@ fi+#+# liburing+#+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with liburing support" >&5+$as_echo_n "checking whether to build with liburing support... " >&6; }++++# Check whether --with-liburing was given.+if test "${with_liburing+set}" = set; then :+ withval=$with_liburing;+ case $withval in+ yes)++$as_echo "#define USE_LIBURING 1" >>confdefs.h++ ;;+ no)+ :+ ;;+ *)+ as_fn_error $? "no argument expected for --with-liburing option" "$LINENO" 5+ ;;+ esac++else+ with_liburing=no++fi+++{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_liburing" >&5+$as_echo "$with_liburing" >&6; }+
#
# UUID library
@@ -13440,6 +13485,99 @@ fi
fi
+if test "$with_liburing" = yes; then++pkg_failed=no+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for liburing" >&5+$as_echo_n "checking for liburing... " >&6; }++if test -n "$LIBURING_CFLAGS"; then+ pkg_cv_LIBURING_CFLAGS="$LIBURING_CFLAGS"+ elif test -n "$PKG_CONFIG"; then+ if test -n "$PKG_CONFIG" && \+ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"liburing\""; } >&5+ ($PKG_CONFIG --exists --print-errors "liburing") 2>&5+ ac_status=$?+ $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5+ test $ac_status = 0; }; then+ pkg_cv_LIBURING_CFLAGS=`$PKG_CONFIG --cflags "liburing" 2>/dev/null`+ test "x$?" != "x0" && pkg_failed=yes+else+ pkg_failed=yes+fi+ else+ pkg_failed=untried+fi+if test -n "$LIBURING_LIBS"; then+ pkg_cv_LIBURING_LIBS="$LIBURING_LIBS"+ elif test -n "$PKG_CONFIG"; then+ if test -n "$PKG_CONFIG" && \+ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"liburing\""; } >&5+ ($PKG_CONFIG --exists --print-errors "liburing") 2>&5+ ac_status=$?+ $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5+ test $ac_status = 0; }; then+ pkg_cv_LIBURING_LIBS=`$PKG_CONFIG --libs "liburing" 2>/dev/null`+ test "x$?" != "x0" && pkg_failed=yes+else+ pkg_failed=yes+fi+ else+ pkg_failed=untried+fi++++if test $pkg_failed = yes; then+ { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5+$as_echo "no" >&6; }++if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then+ _pkg_short_errors_supported=yes+else+ _pkg_short_errors_supported=no+fi+ if test $_pkg_short_errors_supported = yes; then+ LIBURING_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "liburing" 2>&1`+ else+ LIBURING_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "liburing" 2>&1`+ fi+ # Put the nasty error message in config.log where it belongs+ echo "$LIBURING_PKG_ERRORS" >&5++ as_fn_error $? "Package requirements (liburing) were not met:++$LIBURING_PKG_ERRORS++Consider adjusting the PKG_CONFIG_PATH environment variable if you+installed software in a non-standard prefix.++Alternatively, you may set the environment variables LIBURING_CFLAGS+and LIBURING_LIBS to avoid the need to call pkg-config.+See the pkg-config man page for more details." "$LINENO" 5+elif test $pkg_failed = untried; then+ { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5+$as_echo "no" >&6; }+ { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5+$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}+as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it+is in your PATH or set the PKG_CONFIG environment variable to the full+path to pkg-config.++Alternatively, you may set the environment variables LIBURING_CFLAGS+and LIBURING_LIBS to avoid the need to call pkg-config.+See the pkg-config man page for more details.++To get pkg-config, see <http://pkg-config.freedesktop.org/>.+See \`config.log' for more details" "$LINENO" 5; }+else+ LIBURING_CFLAGS=$pkg_cv_LIBURING_CFLAGS+ LIBURING_LIBS=$pkg_cv_LIBURING_LIBS+ { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5+$as_echo "yes" >&6; }++fi+fi
##
## Header files
diff --git a/.cirrus.tasks.yml b/.cirrus.tasks.ymlindex 5849cbb839a..10d277f5659 100644--- a/.cirrus.tasks.yml+++ b/.cirrus.tasks.yml@@ -471,6 +471,7 @@ task:
--enable-cassert --enable-injection-points --enable-debug \
--enable-tap-tests --enable-nls \
--with-segsize-blocks=6 \
+ --with-liburing \
\
${LINUX_CONFIGURE_FEATURES} \
\
diff --git a/src/Makefile.global.in b/src/Makefile.global.inindex 8fe9d61e82a..fba607cb5af 100644--- a/src/Makefile.global.in+++ b/src/Makefile.global.in@@ -196,6 +196,7 @@ with_gssapi = @with_gssapi@
with_krb_srvnam = @with_krb_srvnam@
with_ldap = @with_ldap@
with_libcurl = @with_libcurl@
+with_liburing = @with_liburing@
with_libxml = @with_libxml@
with_libxslt = @with_libxslt@
with_llvm = @with_llvm@
@@ -222,6 +223,9 @@ krb_srvtab = @krb_srvtab@
ICU_CFLAGS = @ICU_CFLAGS@
ICU_LIBS = @ICU_LIBS@
+LIBURING_CFLAGS = @LIBURING_CFLAGS@+LIBURING_LIBS = @LIBURING_LIBS@+
TCLSH = @TCLSH@
TCL_LIBS = @TCL_LIBS@
TCL_LIB_SPEC = @TCL_LIB_SPEC@
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0005-aio-Add-io_method-io_uring.patch (21.4K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/6-v2.11-0005-aio-Add-io_method-io_uring.patch)
download | inline diff:
From 3642a03831aab68a70af15478eebf3b620f802eb Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:05 -0400
Subject: [PATCH v2.11 05/27] aio: Add io_method=io_uring
io_uring, can be considerably faster than io_method=worker. It is, however,
linux specific and requires an additional compile-time dependency (liburing).
Reviewed-by: Jakub Wartak <jakub.wartak@enterprisedb.com>
Discussion: https://postgr.es/m/uvrtrknj4kdytuboidbhwclo4gxhswwcpgadptsjvjqcluzmah%40brqs62irg4dt
Discussion: https://postgr.es/m/20210223100344.llw5an2aklengrmn@alap3.anarazel.de
Discussion: https://postgr.es/m/stj36ea6yyhoxtqkhpieia2z4krnam7qyetc57rfezgk4zgapf@gcnactj4z56m
---
src/include/storage/aio.h | 8 +
src/include/storage/aio_internal.h | 3 +
src/include/storage/lwlock.h | 1 +
src/backend/storage/aio/Makefile | 1 +
src/backend/storage/aio/aio.c | 6 +
src/backend/storage/aio/meson.build | 1 +
src/backend/storage/aio/method_io_uring.c | 474 ++++++++++++++++++
src/backend/storage/lmgr/lwlock.c | 1 +
.../utils/activity/wait_event_names.txt | 2 +
src/backend/utils/misc/postgresql.conf.sample | 3 +-
doc/src/sgml/config.sgml | 6 +
.cirrus.tasks.yml | 3 +
src/tools/pgindent/typedefs.list | 1 +
13 files changed, 509 insertions(+), 1 deletion(-)
create mode 100644 src/backend/storage/aio/method_io_uring.c
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.hindex a221c6c3d9a..57aa1c04b8b 100644--- a/src/include/storage/aio.h+++ b/src/include/storage/aio.h@@ -22,12 +22,20 @@
#include "storage/procnumber.h"
+/* io_uring is incompatible with EXEC_BACKEND */+#if defined(USE_LIBURING) && !defined(EXEC_BACKEND)+#define IOMETHOD_IO_URING_ENABLED+#endif+
/* Enum for io_method GUC. */
typedef enum IoMethod
{
IOMETHOD_SYNC = 0,
IOMETHOD_WORKER,
+#ifdef IOMETHOD_IO_URING_ENABLED+ IOMETHOD_IO_URING,+#endif
} IoMethod;
/* We'll default to worker based execution. */
diff --git a/src/include/storage/aio_internal.h b/src/include/storage/aio_internal.hindex 108fe61c7b4..6e6033519b2 100644--- a/src/include/storage/aio_internal.h+++ b/src/include/storage/aio_internal.h@@ -386,6 +386,9 @@ extern PgAioHandle *pgaio_inj_io_get(void);
/* Declarations for the tables of function pointers exposed by each IO method. */
extern PGDLLIMPORT const IoMethodOps pgaio_sync_ops;
extern PGDLLIMPORT const IoMethodOps pgaio_worker_ops;
+#ifdef IOMETHOD_IO_URING_ENABLED+extern PGDLLIMPORT const IoMethodOps pgaio_uring_ops;+#endif
extern PGDLLIMPORT const IoMethodOps *pgaio_method_ops;
extern PGDLLIMPORT PgAioCtl *pgaio_ctl;
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.hindex ffa03189e2d..4df1d25c045 100644--- a/src/include/storage/lwlock.h+++ b/src/include/storage/lwlock.h@@ -218,6 +218,7 @@ typedef enum BuiltinTrancheIds
LWTRANCHE_SUBTRANS_SLRU,
LWTRANCHE_XACT_SLRU,
LWTRANCHE_PARALLEL_VACUUM_DSA,
+ LWTRANCHE_AIO_URING_COMPLETION,
LWTRANCHE_FIRST_USER_DEFINED,
} BuiltinTrancheIds;
diff --git a/src/backend/storage/aio/Makefile b/src/backend/storage/aio/Makefileindex f51c34a37f8..c06c50771e0 100644--- a/src/backend/storage/aio/Makefile+++ b/src/backend/storage/aio/Makefile@@ -14,6 +14,7 @@ OBJS = \
aio_init.o \
aio_io.o \
aio_target.o \
+ method_io_uring.o \
method_sync.o \
method_worker.o \
read_stream.o
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.cindex 29f57f9cd1c..a30c163c3de 100644--- a/src/backend/storage/aio/aio.c+++ b/src/backend/storage/aio/aio.c@@ -65,6 +65,9 @@ static void pgaio_io_wait(PgAioHandle *ioh, uint64 ref_generation);
const struct config_enum_entry io_method_options[] = {
{"sync", IOMETHOD_SYNC, false},
{"worker", IOMETHOD_WORKER, false},
+#ifdef IOMETHOD_IO_URING_ENABLED+ {"io_uring", IOMETHOD_IO_URING, false},+#endif
{NULL, 0, false}
};
@@ -82,6 +85,9 @@ PgAioBackend *pgaio_my_backend;
static const IoMethodOps *const pgaio_method_ops_table[] = {
[IOMETHOD_SYNC] = &pgaio_sync_ops,
[IOMETHOD_WORKER] = &pgaio_worker_ops,
+#ifdef IOMETHOD_IO_URING_ENABLED+ [IOMETHOD_IO_URING] = &pgaio_uring_ops,+#endif
};
/* callbacks for the configured io_method, set by assign_io_method */
diff --git a/src/backend/storage/aio/meson.build b/src/backend/storage/aio/meson.buildindex 74f94c6e40b..2f0f03d8071 100644--- a/src/backend/storage/aio/meson.build+++ b/src/backend/storage/aio/meson.build@@ -6,6 +6,7 @@ backend_sources += files(
'aio_init.c',
'aio_io.c',
'aio_target.c',
+ 'method_io_uring.c',
'method_sync.c',
'method_worker.c',
'read_stream.c',
diff --git a/src/backend/storage/aio/method_io_uring.c b/src/backend/storage/aio/method_io_uring.cnew file mode 100644index 00000000000..abf5273376a--- /dev/null+++ b/src/backend/storage/aio/method_io_uring.c@@ -0,0 +1,474 @@+/*-------------------------------------------------------------------------+ *+ * method_io_uring.c+ * AIO - perform AIO using Linux' io_uring+ *+ * For now we create one io_uring instance for each backend. These io_uring+ * instances have to be created in postmaster, during startup, to allow other+ * backends to process IO completions, if the issuing backend is currently+ * busy doing other things. Other backends may not use another backend's+ * io_uring instance to submit IO, that'd require additional locking that+ * would likely be harmful for performance.+ *+ * We likely will want to introduce a backend-local io_uring instance in the+ * future, e.g. for FE/BE network IO.+ *+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group+ * Portions Copyright (c) 1994, Regents of the University of California+ *+ * IDENTIFICATION+ * src/backend/storage/aio/method_io_uring.c+ *+ *-------------------------------------------------------------------------+ */++#include "postgres.h"++/* included early, for IOMETHOD_IO_URING_ENABLED */+#include "storage/aio.h"++#ifdef IOMETHOD_IO_URING_ENABLED++#include <liburing.h>++#include "miscadmin.h"+#include "port/pg_iovec.h"+#include "storage/aio_internal.h"+#include "storage/fd.h"+#include "storage/proc.h"+#include "storage/shmem.h"+#include "utils/wait_event.h"+++/* number of completions processed at once */+#define PGAIO_MAX_LOCAL_COMPLETED_IO 32+++/* Entry points for IoMethodOps. */+static size_t pgaio_uring_shmem_size(void);+static void pgaio_uring_shmem_init(bool first_time);+static void pgaio_uring_init_backend(void);+static int pgaio_uring_submit(uint16 num_staged_ios, PgAioHandle **staged_ios);+static void pgaio_uring_wait_one(PgAioHandle *ioh, uint64 ref_generation);++/* helper functions */+static void pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe);+++const IoMethodOps pgaio_uring_ops = {+ .shmem_size = pgaio_uring_shmem_size,+ .shmem_init = pgaio_uring_shmem_init,+ .init_backend = pgaio_uring_init_backend,++ .submit = pgaio_uring_submit,+ .wait_one = pgaio_uring_wait_one,+};++/*+ * Per-backend state when using io_method=io_uring+ *+ * Align the whole struct to a cacheline boundary, to prevent false sharing+ * between completion_lock and prior backend's io_uring_ring.+ */+typedef struct pg_attribute_aligned (PG_CACHE_LINE_SIZE)+PgAioUringContext+{+ /*+ * Multiple backends can process completions for this backend's io_uring+ * instance (e.g. when the backend issuing IO is busy doing something+ * else). To make that safe we have to ensure that only a single backend+ * gets io completions from the io_uring instance at a time.+ */+ LWLock completion_lock;++ struct io_uring io_uring_ring;+} PgAioUringContext;++/* PgAioUringContexts for all backends */+static PgAioUringContext *pgaio_uring_contexts;++/* the current backend's context */+static PgAioUringContext *pgaio_my_uring_context;+++static uint32+pgaio_uring_procs(void)+{+ /*+ * We can subtract MAX_IO_WORKERS here as io workers are never used at the+ * same time as io_method=io_uring.+ */+ return MaxBackends + NUM_AUXILIARY_PROCS - MAX_IO_WORKERS;+}++static Size+pgaio_uring_context_shmem_size(void)+{+ return mul_size(pgaio_uring_procs(), sizeof(PgAioUringContext));+}++static size_t+pgaio_uring_shmem_size(void)+{+ return pgaio_uring_context_shmem_size();+}++static void+pgaio_uring_shmem_init(bool first_time)+{+ int TotalProcs = MaxBackends + NUM_AUXILIARY_PROCS - MAX_IO_WORKERS;+ bool found;++ pgaio_uring_contexts = (PgAioUringContext *)+ ShmemInitStruct("AioUring", pgaio_uring_shmem_size(), &found);++ if (found)+ return;++ for (int contextno = 0; contextno < TotalProcs; contextno++)+ {+ PgAioUringContext *context = &pgaio_uring_contexts[contextno];+ int ret;++ /*+ * Right now a high TotalProcs will cause problems in two ways:+ *+ * - RLIMIT_NOFILE needs to be big enough to allow all+ * io_uring_queue_init() calls to succeed.+ *+ * - RLIMIT_NOFILE needs to be big enough to still have enough file+ * descriptors to satisfy set_max_safe_fds() left over. Or, even+ * better, have max_files_per_process left over FDs.+ *+ * We probably should adjust the soft RLIMIT_NOFILE to ensure that.+ *+ *+ * XXX: Newer versions of io_uring support sharing the workers that+ * execute some asynchronous IOs between io_uring instances. It might+ * be worth using that - also need to evaluate if that causes+ * noticeable additional contention?+ */+ ret = io_uring_queue_init(io_max_concurrency, &context->io_uring_ring, 0);+ if (ret < 0)+ {+ char *hint = NULL;+ int err = ERRCODE_INTERNAL_ERROR;++ /* add hints for some failures that errno explains sufficiently */+ if (-ret == EPERM)+ {+ err = ERRCODE_INSUFFICIENT_PRIVILEGE;+ hint = _("Check if io_uring is disabled via /proc/sys/kernel/io_uring_disabled.");+ }+ else if (-ret == EMFILE)+ {+ err = ERRCODE_INSUFFICIENT_RESOURCES;+ hint = psprintf(_("Consider increasing \"ulimit -n\" to at least %d."),+ TotalProcs + max_files_per_process);+ }+ else if (-ret == ENOSYS)+ {+ err = ERRCODE_FEATURE_NOT_SUPPORTED;+ hint = _("Kernel does not support io_uring.");+ }++ /* update errno to allow %m to work */+ errno = -ret;++ ereport(ERROR,+ errcode(err),+ errmsg("io_uring_queue_init failed: %m"),+ hint != NULL ? errhint("%s", hint) : 0);+ }++ LWLockInitialize(&context->completion_lock, LWTRANCHE_AIO_URING_COMPLETION);+ }+}++static void+pgaio_uring_init_backend(void)+{+ Assert(MyProcNumber < pgaio_uring_procs());++ pgaio_my_uring_context = &pgaio_uring_contexts[MyProcNumber];+}++static int+pgaio_uring_submit(uint16 num_staged_ios, PgAioHandle **staged_ios)+{+ struct io_uring *uring_instance = &pgaio_my_uring_context->io_uring_ring;+ int in_flight_before = dclist_count(&pgaio_my_backend->in_flight_ios);++ Assert(num_staged_ios <= PGAIO_SUBMIT_BATCH_SIZE);++ for (int i = 0; i < num_staged_ios; i++)+ {+ PgAioHandle *ioh = staged_ios[i];+ struct io_uring_sqe *sqe;++ sqe = io_uring_get_sqe(uring_instance);++ if (!sqe)+ elog(ERROR, "io_uring submission queue is unexpectedly full");++ pgaio_io_prepare_submit(ioh);+ pgaio_uring_sq_from_io(ioh, sqe);++ /*+ * io_uring executes IO in process context if possible. That's+ * generally good, as it reduces context switching. When performing a+ * lot of buffered IO that means that copying between page cache and+ * userspace memory happens in the foreground, as it can't be+ * offloaded to DMA hardware as is possible when using direct IO. When+ * executing a lot of buffered IO this causes io_uring to be slower+ * than worker mode, as worker mode parallelizes the copying. io_uring+ * can be told to offload work to worker threads instead.+ *+ * If an IO is buffered IO and we already have IOs in flight or+ * multiple IOs are being submitted, we thus tell io_uring to execute+ * the IO in the background. We don't do so for the first few IOs+ * being submitted as executing in this process' context has lower+ * latency.+ */+ if (in_flight_before > 4 && (ioh->flags & PGAIO_HF_BUFFERED))+ io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);++ in_flight_before++;+ }++ while (true)+ {+ int ret;++ pgstat_report_wait_start(WAIT_EVENT_AIO_IO_URING_SUBMIT);+ ret = io_uring_submit(uring_instance);+ pgstat_report_wait_end();++ if (ret == -EINTR)+ {+ pgaio_debug(DEBUG3,+ "aio method uring: submit EINTR, nios: %d",+ num_staged_ios);+ }+ else if (ret < 0)+ {+ /*+ * The io_uring_enter() manpage suggests that the appropriate+ * reaction to EAGAIN is:+ *+ * "The application should wait for some completions and try+ * again"+ *+ * However, it seems unlikely that that would help in our case, as+ * we apply a low limit to the number of outstanding IOs and thus+ * also outstanding completions, making it unlikely that we'd get+ * EAGAIN while the OS is in good working order.+ *+ * Additionally, it would be problematic to just wait here, our+ * caller might hold critical locks. It'd possibly lead to+ * delaying the crash-restart that seems likely to occur when the+ * kernel is under such heavy memory pressure.+ *+ * Update errno to allow %m to work.+ */+ errno = -ret;+ elog(PANIC, "io_uring submit failed: %m");+ }+ else if (ret != num_staged_ios)+ {+ /* likely unreachable, but if it is, we would need to re-submit */+ elog(PANIC, "io_uring submit submitted only %d of %d",+ ret, num_staged_ios);+ }+ else+ {+ pgaio_debug(DEBUG4,+ "aio method uring: submitted %d IOs",+ num_staged_ios);+ break;+ }+ }++ return num_staged_ios;+}++static void+pgaio_uring_drain_locked(PgAioUringContext *context)+{+ int ready;+ int orig_ready;++ Assert(LWLockHeldByMeInMode(&context->completion_lock, LW_EXCLUSIVE));++ /*+ * Don't drain more events than available right now. Otherwise it's+ * plausible that one backend could get stuck, for a while, receiving CQEs+ * without actually processing them.+ */+ orig_ready = ready = io_uring_cq_ready(&context->io_uring_ring);++ while (ready > 0)+ {+ struct io_uring_cqe *cqes[PGAIO_MAX_LOCAL_COMPLETED_IO];+ uint32 ncqes;++ START_CRIT_SECTION();+ ncqes =+ io_uring_peek_batch_cqe(&context->io_uring_ring,+ cqes,+ Min(PGAIO_MAX_LOCAL_COMPLETED_IO, ready));+ Assert(ncqes <= ready);++ ready -= ncqes;++ for (int i = 0; i < ncqes; i++)+ {+ struct io_uring_cqe *cqe = cqes[i];+ PgAioHandle *ioh;++ ioh = io_uring_cqe_get_data(cqe);+ io_uring_cqe_seen(&context->io_uring_ring, cqe);++ pgaio_io_process_completion(ioh, cqe->res);+ }++ END_CRIT_SECTION();++ pgaio_debug(DEBUG3,+ "drained %d/%d, now expecting %d",+ ncqes, orig_ready, io_uring_cq_ready(&context->io_uring_ring));+ }+}++static void+pgaio_uring_wait_one(PgAioHandle *ioh, uint64 ref_generation)+{+ PgAioHandleState state;+ ProcNumber owner_procno = ioh->owner_procno;+ PgAioUringContext *owner_context = &pgaio_uring_contexts[owner_procno];+ bool expect_cqe;+ int waited = 0;++ /*+ * XXX: It would be nice to have a smarter locking scheme, nearly all the+ * time the backend owning the ring will consume the completions, making+ * the locking unnecessarily expensive.+ */+ LWLockAcquire(&owner_context->completion_lock, LW_EXCLUSIVE);++ while (true)+ {+ pgaio_debug_io(DEBUG3, ioh,+ "wait_one io_gen: %llu, ref_gen: %llu, cycle %d",+ (long long unsigned) ref_generation,+ (long long unsigned) ioh->generation,+ waited);++ if (pgaio_io_was_recycled(ioh, ref_generation, &state) ||+ state != PGAIO_HS_SUBMITTED)+ {+ /* the IO was completed by another backend */+ break;+ }+ else if (io_uring_cq_ready(&owner_context->io_uring_ring))+ {+ /* no need to wait in the kernel, io_uring has a completion */+ expect_cqe = true;+ }+ else+ {+ int ret;+ struct io_uring_cqe *cqes;++ /* need to wait in the kernel */+ pgstat_report_wait_start(WAIT_EVENT_AIO_IO_URING_COMPLETION);+ ret = io_uring_wait_cqes(&owner_context->io_uring_ring, &cqes, 1, NULL, NULL);+ pgstat_report_wait_end();++ if (ret == -EINTR)+ {+ continue;+ }+ else if (ret != 0)+ {+ /* see comment after io_uring_submit() */+ errno = -ret;+ elog(PANIC, "io_uring wait failed: %m");+ }+ else+ {+ Assert(cqes != NULL);+ expect_cqe = true;+ waited++;+ }+ }++ if (expect_cqe)+ {+ pgaio_uring_drain_locked(owner_context);+ }+ }++ LWLockRelease(&owner_context->completion_lock);++ pgaio_debug(DEBUG3,+ "wait_one with %d sleeps",+ waited);+}++static void+pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe)+{+ struct iovec *iov;++ switch (ioh->op)+ {+ case PGAIO_OP_READV:+ iov = &pgaio_ctl->iovecs[ioh->iovec_off];+ if (ioh->op_data.read.iov_length == 1)+ {+ io_uring_prep_read(sqe,+ ioh->op_data.read.fd,+ iov->iov_base,+ iov->iov_len,+ ioh->op_data.read.offset);+ }+ else+ {+ io_uring_prep_readv(sqe,+ ioh->op_data.read.fd,+ iov,+ ioh->op_data.read.iov_length,+ ioh->op_data.read.offset);++ }+ break;++ case PGAIO_OP_WRITEV:+ iov = &pgaio_ctl->iovecs[ioh->iovec_off];+ if (ioh->op_data.write.iov_length == 1)+ {+ io_uring_prep_write(sqe,+ ioh->op_data.write.fd,+ iov->iov_base,+ iov->iov_len,+ ioh->op_data.write.offset);+ }+ else+ {+ io_uring_prep_writev(sqe,+ ioh->op_data.write.fd,+ iov,+ ioh->op_data.write.iov_length,+ ioh->op_data.write.offset);+ }+ break;++ case PGAIO_OP_INVALID:+ elog(ERROR, "trying to prepare invalid IO operation for execution");+ }++ io_uring_sqe_set_data(sqe, ioh);+}++#endif /* IOMETHOD_IO_URING_ENABLED */diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.cindex 5702c35bb91..3df29658f18 100644--- a/src/backend/storage/lmgr/lwlock.c+++ b/src/backend/storage/lmgr/lwlock.c@@ -177,6 +177,7 @@ static const char *const BuiltinTrancheNames[] = {
[LWTRANCHE_SUBTRANS_SLRU] = "SubtransSLRU",
[LWTRANCHE_XACT_SLRU] = "XactSLRU",
[LWTRANCHE_PARALLEL_VACUUM_DSA] = "ParallelVacuumDSA",
+ [LWTRANCHE_AIO_URING_COMPLETION] = "AioUringCompletion",
};
StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txtindex 9fa12a555e8..44eef41f841 100644--- a/src/backend/utils/activity/wait_event_names.txt+++ b/src/backend/utils/activity/wait_event_names.txt@@ -192,6 +192,8 @@ ABI_compatibility:
Section: ClassName - WaitEventIO
+AIO_IO_URING_SUBMIT "Waiting for IO submission via io_uring."+AIO_IO_URING_COMPLETION "Waiting for IO completion via io_uring."
AIO_IO_COMPLETION "Waiting for IO completion."
BASEBACKUP_READ "Waiting for base backup to read from a file."
BASEBACKUP_SYNC "Waiting for data written by a base backup to reach durable storage."
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sampleindex 9f31e4071c7..69dc727174b 100644--- a/src/backend/utils/misc/postgresql.conf.sample+++ b/src/backend/utils/misc/postgresql.conf.sample@@ -204,7 +204,8 @@
# (change requires restart)
#io_combine_limit = 128kB # usually 1-128 blocks (depends on OS)
-#io_method = worker # worker, sync (change requires restart)+#io_method = worker # worker, io_uring, sync+ # (change requires restart)
#io_max_concurrency = -1 # Max number of IOs that one process
# can execute simultaneously
# -1 sets based on shared_buffers
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgmlindex c704f3f98f1..4f71725036b 100644--- a/doc/src/sgml/config.sgml+++ b/doc/src/sgml/config.sgml@@ -2710,6 +2710,12 @@ include_dir 'conf.d'
<literal>worker</literal> (execute asynchronous I/O using worker processes)
</para>
</listitem>
+ <listitem>+ <para>+ <literal>io_uring</literal> (execute asynchronous I/O using+ io_uring, if available)+ </para>+ </listitem>
<listitem>
<para>
<literal>sync</literal> (execute asynchronous-eligible I/O synchronously)
diff --git a/.cirrus.tasks.yml b/.cirrus.tasks.ymlindex 10d277f5659..86a1fa9bbdb 100644--- a/.cirrus.tasks.yml+++ b/.cirrus.tasks.yml@@ -493,11 +493,14 @@ task:
# - Uses undefined behaviour and alignment sanitizers, sanitizer failures
# are typically printed in the server log
# - Test both 64bit and 32 bit builds
+ # - uses io_method=io_uring
- name: Linux - Debian Bookworm - Meson
env:
CCACHE_MAXSIZE: "400M" # tests two different builds
SANITIZER_FLAGS: -fsanitize=alignment,undefined
+ PG_TEST_INITDB_EXTRA_OPTS: >-+ -c io_method=io_uring
configure_script: |
su postgres <<-EOF
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex bfa276d2d35..53abd54edd8 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -2150,6 +2150,7 @@ PgAioReturn
PgAioTargetData
PgAioTargetID
PgAioTargetInfo
+PgAioUringContext
PgAioWaitRef
PgArchData
PgBackendGSSStatus
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0006-aio-Implement-support-for-reads-in-smgr-md-fd.patch (22.8K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/7-v2.11-0006-aio-Implement-support-for-reads-in-smgr-md-fd.patch)
download | inline diff:
From 5d65d6a0c7588f392305c40d16ed23dfa49927aa Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:05 -0400
Subject: [PATCH v2.11 06/27] aio: Implement support for reads in smgr/md/fd
This implements the following:
1) An smgr AIO target, for AIO on smgr files. This should be usable not just
for md.c but also other SMGR implementation if we ever get them.
2) readv support in fd.c, which requires a small bit of infrastructure work in
fd.c
3) smgr.c and md.c support for readv
There still is nothing performing AIO, but as of this commit it would be
possible.
As part of this change FileGetRawDesc() actually ensures that the file is
opened - previously it was basically not usable. It's used to reopen a file in
IO workers.
Reviewed-by: Noah Misch <noah@leadboat.com>
Discussion: https://postgr.es/m/uvrtrknj4kdytuboidbhwclo4gxhswwcpgadptsjvjqcluzmah%40brqs62irg4dt
Discussion: https://postgr.es/m/20210223100344.llw5an2aklengrmn@alap3.anarazel.de
Discussion: https://postgr.es/m/stj36ea6yyhoxtqkhpieia2z4krnam7qyetc57rfezgk4zgapf@gcnactj4z56m
---
src/include/storage/aio.h | 6 +-
src/include/storage/aio_types.h | 10 +-
src/include/storage/fd.h | 3 +
src/include/storage/md.h | 7 +
src/include/storage/smgr.h | 14 ++
src/backend/storage/aio/aio_callback.c | 3 +
src/backend/storage/aio/aio_target.c | 2 +
src/backend/storage/file/fd.c | 40 ++++++
src/backend/storage/smgr/md.c | 186 +++++++++++++++++++++++++
src/backend/storage/smgr/smgr.c | 150 ++++++++++++++++++++
10 files changed, 417 insertions(+), 4 deletions(-)
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.hindex 57aa1c04b8b..1591a6c2fca 100644--- a/src/include/storage/aio.h+++ b/src/include/storage/aio.h@@ -117,9 +117,10 @@ typedef enum PgAioTargetID
{
/* intentionally the zero value, to help catch zeroed memory etc */
PGAIO_TID_INVALID = 0,
+ PGAIO_TID_SMGR,
} PgAioTargetID;
-#define PGAIO_TID_COUNT (PGAIO_TID_INVALID + 1)+#define PGAIO_TID_COUNT (PGAIO_TID_SMGR + 1)
/*
@@ -191,6 +192,9 @@ struct PgAioTargetInfo
typedef enum PgAioHandleCallbackID
{
PGAIO_HCB_INVALID,
++ PGAIO_HCB_MD_READV,+ PGAIO_HCB_MD_WRITEV,
} PgAioHandleCallbackID;
diff --git a/src/include/storage/aio_types.h b/src/include/storage/aio_types.hindex dddda3a3e2f..debe8163d4e 100644--- a/src/include/storage/aio_types.h+++ b/src/include/storage/aio_types.h@@ -60,11 +60,15 @@ typedef struct PgAioWaitRef
*/
typedef union PgAioTargetData
{
- /* just as an example placeholder for later */
struct
{
- uint32 queue_id;- } wal;+ RelFileLocator rlocator; /* physical relation identifier */+ BlockNumber blockNum; /* blknum relative to begin of reln */+ BlockNumber nblocks;+ ForkNumber forkNum:8; /* don't waste 4 byte for four values */+ bool is_temp:1; /* proc can be inferred by owning AIO */+ bool skip_fsync:1;+ } smgr;
} PgAioTargetData;
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.hindex e3067ab6597..b77d8e5e30e 100644--- a/src/include/storage/fd.h+++ b/src/include/storage/fd.h@@ -101,6 +101,8 @@ extern PGDLLIMPORT int max_safe_fds;
* prototypes for functions in fd.c
*/
+struct PgAioHandle;+
/* Operations on virtual Files --- equivalent to Unix kernel file ops */
extern File PathNameOpenFile(const char *fileName, int fileFlags);
extern File PathNameOpenFilePerm(const char *fileName, int fileFlags, mode_t fileMode);
@@ -109,6 +111,7 @@ extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, off_t amount, uint32 wait_event_info);
extern ssize_t FileReadV(File file, const struct iovec *iov, int iovcnt, off_t offset, uint32 wait_event_info);
extern ssize_t FileWriteV(File file, const struct iovec *iov, int iovcnt, off_t offset, uint32 wait_event_info);
+extern int FileStartReadV(struct PgAioHandle *ioh, File file, int iovcnt, off_t offset, uint32 wait_event_info);
extern int FileSync(File file, uint32 wait_event_info);
extern int FileZero(File file, off_t offset, off_t amount, uint32 wait_event_info);
extern int FileFallocate(File file, off_t offset, off_t amount, uint32 wait_event_info);
diff --git a/src/include/storage/md.h b/src/include/storage/md.hindex 05bf537066e..9d7131eff43 100644--- a/src/include/storage/md.h+++ b/src/include/storage/md.h@@ -14,11 +14,14 @@
#ifndef MD_H
#define MD_H
+#include "storage/aio_types.h"
#include "storage/block.h"
#include "storage/relfilelocator.h"
#include "storage/smgr.h"
#include "storage/sync.h"
+extern const PgAioHandleCallbacks aio_md_readv_cb;+
/* md storage manager functionality */
extern void mdinit(void);
extern void mdopen(SMgrRelation reln);
@@ -36,6 +39,9 @@ extern uint32 mdmaxcombine(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdreadv(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
void **buffers, BlockNumber nblocks);
+extern void mdstartreadv(PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,+ void **buffers, BlockNumber nblocks);
extern void mdwritev(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum,
const void **buffers, BlockNumber nblocks, bool skipFsync);
@@ -46,6 +52,7 @@ extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber old_blocks, BlockNumber nblocks);
extern void mdimmedsync(SMgrRelation reln, ForkNumber forknum);
extern void mdregistersync(SMgrRelation reln, ForkNumber forknum);
+extern int mdfd(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, uint32 *off);
extern void ForgetDatabaseSyncRequests(Oid dbid);
extern void DropRelationFiles(RelFileLocator *delrels, int ndelrels, bool isRedo);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.hindex 4016b206ad6..856ebcda350 100644--- a/src/include/storage/smgr.h+++ b/src/include/storage/smgr.h@@ -15,6 +15,7 @@
#define SMGR_H
#include "lib/ilist.h"
+#include "storage/aio_types.h"
#include "storage/block.h"
#include "storage/relfilelocator.h"
@@ -73,6 +74,8 @@ typedef SMgrRelationData *SMgrRelation;
#define SmgrIsTemp(smgr) \
RelFileLocatorBackendIsTemp((smgr)->smgr_rlocator)
+extern const PgAioTargetInfo aio_smgr_target_info;+
extern void smgrinit(void);
extern SMgrRelation smgropen(RelFileLocator rlocator, ProcNumber backend);
extern bool smgrexists(SMgrRelation reln, ForkNumber forknum);
@@ -97,6 +100,10 @@ extern uint32 smgrmaxcombine(SMgrRelation reln, ForkNumber forknum,
extern void smgrreadv(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum,
void **buffers, BlockNumber nblocks);
+extern void smgrstartreadv(PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum,+ BlockNumber blocknum,+ void **buffers, BlockNumber nblocks);
extern void smgrwritev(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum,
const void **buffers, BlockNumber nblocks,
@@ -127,4 +134,11 @@ smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
smgrwritev(reln, forknum, blocknum, &buffer, 1, skipFsync);
}
+extern void pgaio_io_set_target_smgr(PgAioHandle *ioh,+ SMgrRelationData *smgr,+ ForkNumber forknum,+ BlockNumber blocknum,+ int nblocks,+ bool skip_fsync);+
#endif /* SMGR_H */
diff --git a/src/backend/storage/aio/aio_callback.c b/src/backend/storage/aio/aio_callback.cindex 09f03f296f5..a92fa8f678e 100644--- a/src/backend/storage/aio/aio_callback.c+++ b/src/backend/storage/aio/aio_callback.c@@ -18,6 +18,7 @@
#include "miscadmin.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
+#include "storage/md.h"
/* just to have something to put into aio_handle_cbs */
@@ -37,6 +38,8 @@ typedef struct PgAioHandleCallbacksEntry
static const PgAioHandleCallbacksEntry aio_handle_cbs[] = {
#define CALLBACK_ENTRY(id, callback) [id] = {.cb = &callback, .name = #callback}
CALLBACK_ENTRY(PGAIO_HCB_INVALID, aio_invalid_cb),
++ CALLBACK_ENTRY(PGAIO_HCB_MD_READV, aio_md_readv_cb),
#undef CALLBACK_ENTRY
};
diff --git a/src/backend/storage/aio/aio_target.c b/src/backend/storage/aio/aio_target.cindex b01406a6a52..6133b04f350 100644--- a/src/backend/storage/aio/aio_target.c+++ b/src/backend/storage/aio/aio_target.c@@ -16,6 +16,7 @@
#include "storage/aio.h"
#include "storage/aio_internal.h"
+#include "storage/smgr.h"
/*
@@ -25,6 +26,7 @@ static const PgAioTargetInfo *pgaio_target_info[] = {
[PGAIO_TID_INVALID] = &(PgAioTargetInfo) {
.name = "invalid",
},
+ [PGAIO_TID_SMGR] = &aio_smgr_target_info,
};
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.cindex 0c3a2a756e7..f573aa8e7ca 100644--- a/src/backend/storage/file/fd.c+++ b/src/backend/storage/file/fd.c@@ -94,6 +94,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/startup.h"
+#include "storage/aio.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "utils/guc.h"
@@ -1296,6 +1297,8 @@ LruDelete(File file)
vfdP = &VfdCache[file];
+ pgaio_closing_fd(vfdP->fd);+
/*
* Close the file. We aren't expecting this to fail; if it does, better
* to leak the FD than to mess up our internal state.
@@ -1989,6 +1992,8 @@ FileClose(File file)
if (!FileIsNotOpen(file))
{
+ pgaio_closing_fd(vfdP->fd);+
/* close the file */
if (close(vfdP->fd) != 0)
{
@@ -2212,6 +2217,32 @@ retry:
return returnCode;
}
+int+FileStartReadV(PgAioHandle *ioh, File file,+ int iovcnt, off_t offset,+ uint32 wait_event_info)+{+ int returnCode;+ Vfd *vfdP;++ Assert(FileIsValid(file));++ DO_DB(elog(LOG, "FileStartReadV: %d (%s) " INT64_FORMAT " %d",+ file, VfdCache[file].fileName,+ (int64) offset,+ iovcnt));++ returnCode = FileAccess(file);+ if (returnCode < 0)+ return returnCode;++ vfdP = &VfdCache[file];++ pgaio_io_prep_readv(ioh, vfdP->fd, iovcnt, offset);++ return 0;+}+
ssize_t
FileWriteV(File file, const struct iovec *iov, int iovcnt, off_t offset,
uint32 wait_event_info)
@@ -2500,6 +2531,12 @@ FilePathName(File file)
int
FileGetRawDesc(File file)
{
+ int returnCode;++ returnCode = FileAccess(file);+ if (returnCode < 0)+ return returnCode;+
Assert(FileIsValid(file));
return VfdCache[file].fd;
}
@@ -2780,6 +2817,7 @@ FreeDesc(AllocateDesc *desc)
result = closedir(desc->desc.dir);
break;
case AllocateDescRawFD:
+ pgaio_closing_fd(desc->desc.fd);
result = close(desc->desc.fd);
break;
default:
@@ -2848,6 +2886,8 @@ CloseTransientFile(int fd)
/* Only get here if someone passes us a file not in allocatedDescs */
elog(WARNING, "fd passed to CloseTransientFile was not obtained from OpenTransientFile");
+ pgaio_closing_fd(fd);+
return close(fd);
}
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.cindex f3220f98dc4..2218f17e7a0 100644--- a/src/backend/storage/smgr/md.c+++ b/src/backend/storage/smgr/md.c@@ -31,6 +31,7 @@
#include "miscadmin.h"
#include "pg_trace.h"
#include "pgstat.h"
+#include "storage/aio.h"
#include "storage/bufmgr.h"
#include "storage/fd.h"
#include "storage/md.h"
@@ -152,6 +153,15 @@ static MdfdVec *_mdfd_getseg(SMgrRelation reln, ForkNumber forknum,
static BlockNumber _mdnblocks(SMgrRelation reln, ForkNumber forknum,
MdfdVec *seg);
+static PgAioResult md_readv_complete(PgAioHandle *ioh, PgAioResult prior_result, uint8 cb_data);+static void md_readv_report(PgAioResult result, const PgAioTargetData *target_data, int elevel);++const PgAioHandleCallbacks aio_md_readv_cb = {+ .complete_shared = md_readv_complete,+ .report = md_readv_report,+};++
static inline int
_mdfd_open_flags(void)
{
@@ -937,6 +947,69 @@ mdreadv(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
}
}
+/*+ * mdstartreadv() -- Asynchronous version of mdreadv().+ */+void+mdstartreadv(PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,+ void **buffers, BlockNumber nblocks)+{+ off_t seekpos;+ MdfdVec *v;+ BlockNumber nblocks_this_segment;+ struct iovec *iov;+ int iovcnt;+ int ret;++ v = _mdfd_getseg(reln, forknum, blocknum, false,+ EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY);++ seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE));++ Assert(seekpos < (off_t) BLCKSZ * RELSEG_SIZE);++ nblocks_this_segment =+ Min(nblocks,+ RELSEG_SIZE - (blocknum % ((BlockNumber) RELSEG_SIZE)));++ if (nblocks_this_segment != nblocks)+ elog(ERROR, "read crossing segment boundary");++ iovcnt = pgaio_io_get_iovec(ioh, &iov);++ Assert(nblocks <= iovcnt);++ iovcnt = buffers_to_iovec(iov, buffers, nblocks_this_segment);++ Assert(iovcnt <= nblocks_this_segment);++ if (!(io_direct_flags & IO_DIRECT_DATA))+ pgaio_io_set_flag(ioh, PGAIO_HF_BUFFERED);++ pgaio_io_set_target_smgr(ioh,+ reln,+ forknum,+ blocknum,+ nblocks,+ false);+ pgaio_io_register_callbacks(ioh, PGAIO_HCB_MD_READV, 0);++ ret = FileStartReadV(ioh, v->mdfd_vfd, iovcnt, seekpos, WAIT_EVENT_DATA_FILE_READ);+ if (ret != 0)+ ereport(ERROR,+ (errcode_for_file_access(),+ errmsg("could not start reading blocks %u..%u in file \"%s\": %m",+ blocknum,+ blocknum + nblocks_this_segment - 1,+ FilePathName(v->mdfd_vfd))));++ /*+ * The error checks corresponding to the post-read checks in mdread() are+ * in md_readv_complete().+ */+}+
/*
* mdwritev() -- Write the supplied blocks at the appropriate location.
*
@@ -1365,6 +1438,21 @@ mdimmedsync(SMgrRelation reln, ForkNumber forknum)
}
}
+int+mdfd(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, uint32 *off)+{+ MdfdVec *v = mdopenfork(reln, forknum, EXTENSION_FAIL);++ v = _mdfd_getseg(reln, forknum, blocknum, false,+ EXTENSION_FAIL);++ *off = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE));++ Assert(*off < (off_t) BLCKSZ * RELSEG_SIZE);++ return FileGetRawDesc(v->mdfd_vfd);+}+
/*
* register_dirty_segment() -- Mark a relation segment as needing fsync
*
@@ -1841,3 +1929,101 @@ mdfiletagmatches(const FileTag *ftag, const FileTag *candidate)
*/
return ftag->rlocator.dbOid == candidate->rlocator.dbOid;
}
++/*+ * AIO completion callback for mdstartreadv().+ */+static PgAioResult+md_readv_complete(PgAioHandle *ioh, PgAioResult prior_result, uint8 cb_data)+{+ PgAioTargetData *td = pgaio_io_get_target_data(ioh);+ PgAioResult result = prior_result;++ if (prior_result.result < 0)+ {+ result.status = PGAIO_RS_ERROR;+ result.id = PGAIO_HCB_MD_READV;+ /* For "hard" errors, track the error number in error_data */+ result.error_data = -prior_result.result;+ result.result = 0;++ pgaio_result_report(result, td, LOG);++ return result;+ }++ /*+ * As explained above smgrstartreadv(), the smgr API operates on the level+ * of blocks, rather than bytes. Convert.+ */+ result.result /= BLCKSZ;++ Assert(result.result <= td->smgr.nblocks);++ if (result.result == 0)+ {+ /* consider 0 blocks read a failure */+ result.status = PGAIO_RS_ERROR;+ result.id = PGAIO_HCB_MD_READV;+ result.error_data = 0;++ pgaio_result_report(result, td, LOG);++ return result;+ }++ if (result.status != PGAIO_RS_ERROR &&+ result.result < td->smgr.nblocks)+ {+ /* partial reads should be retried at upper level */+ result.status = PGAIO_RS_PARTIAL;+ result.id = PGAIO_HCB_MD_READV;+ }++ return result;+}++/*+ * AIO error reporting callback for mdstartreadv().+ *+ * Errors are encoded as follows:+ * - PgAioResult.error_data != 0 encodes IO that failed with errno != 0+ * - PgAioResult.error_data == 0 encodes IO that didn't read all data+ */+static void+md_readv_report(PgAioResult result, const PgAioTargetData *td, int elevel)+{+ RelPathStr path;++ path = relpathbackend(td->smgr.rlocator,+ td->smgr.is_temp ? MyProcNumber : INVALID_PROC_NUMBER,+ td->smgr.forkNum);++ if (result.error_data != 0)+ {+ /* for errcode_for_file_access() and %m */+ errno = result.error_data;++ ereport(elevel,+ errcode_for_file_access(),+ errmsg("could not read blocks %u..%u in file \"%s\": %m",+ td->smgr.blockNum,+ td->smgr.blockNum + td->smgr.nblocks - 1,+ path.str));+ }+ else+ {+ /*+ * NB: This will typically only be output in debug messages, while+ * retrying a partial IO.+ */+ ereport(elevel,+ errcode(ERRCODE_DATA_CORRUPTED),+ errmsg("could not read blocks %u..%u in file \"%s\": read only %zu of %zu bytes",+ td->smgr.blockNum,+ td->smgr.blockNum + td->smgr.nblocks - 1,+ path.str,+ result.result * (size_t) BLCKSZ,+ td->smgr.nblocks * (size_t) BLCKSZ));+ }+}diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.cindex af74f54b43b..545888dcdfc 100644--- a/src/backend/storage/smgr/smgr.c+++ b/src/backend/storage/smgr/smgr.c@@ -66,6 +66,7 @@
#include "access/xlogutils.h"
#include "lib/ilist.h"
#include "miscadmin.h"
+#include "storage/aio.h"
#include "storage/bufmgr.h"
#include "storage/ipc.h"
#include "storage/md.h"
@@ -106,6 +107,10 @@ typedef struct f_smgr
void (*smgr_readv) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum,
void **buffers, BlockNumber nblocks);
+ void (*smgr_startreadv) (PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum,+ BlockNumber blocknum,+ void **buffers, BlockNumber nblocks);
void (*smgr_writev) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum,
const void **buffers, BlockNumber nblocks,
@@ -117,6 +122,7 @@ typedef struct f_smgr
BlockNumber old_blocks, BlockNumber nblocks);
void (*smgr_immedsync) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_registersync) (SMgrRelation reln, ForkNumber forknum);
+ int (*smgr_fd) (SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, uint32 *off);
} f_smgr;
static const f_smgr smgrsw[] = {
@@ -134,12 +140,14 @@ static const f_smgr smgrsw[] = {
.smgr_prefetch = mdprefetch,
.smgr_maxcombine = mdmaxcombine,
.smgr_readv = mdreadv,
+ .smgr_startreadv = mdstartreadv,
.smgr_writev = mdwritev,
.smgr_writeback = mdwriteback,
.smgr_nblocks = mdnblocks,
.smgr_truncate = mdtruncate,
.smgr_immedsync = mdimmedsync,
.smgr_registersync = mdregistersync,
+ .smgr_fd = mdfd,
}
};
@@ -157,6 +165,16 @@ static dlist_head unpinned_relns;
static void smgrshutdown(int code, Datum arg);
static void smgrdestroy(SMgrRelation reln);
+static void smgr_aio_reopen(PgAioHandle *ioh);+static char *smgr_aio_describe_identity(const PgAioTargetData *sd);+++const PgAioTargetInfo aio_smgr_target_info = {+ .name = "smgr",+ .reopen = smgr_aio_reopen,+ .describe_identity = smgr_aio_describe_identity,+};+
/*
* smgrinit(), smgrshutdown() -- Initialize or shut down storage
@@ -709,6 +727,30 @@ smgrreadv(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
RESUME_INTERRUPTS();
}
+/*+ * smgrstartreadv() -- asynchronous version of smgrreadv()+ *+ * This starts an asynchronous readv IO using the IO handle `ioh`. Other than+ * `ioh` all parameters are the same as smgrreadv().+ *+ * Completion callbacks above smgr will be passed the result as the number of+ * successfully read blocks if the read [partially] succeeds (Buffers for+ * blocks not successfully read might bear unspecified modifications, up to+ * the full nblocks). This maintains the abstraction that smgr operates on the+ * level of blocks, rather than bytes.+ */+void+smgrstartreadv(PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,+ void **buffers, BlockNumber nblocks)+{+ HOLD_INTERRUPTS();+ smgrsw[reln->smgr_which].smgr_startreadv(ioh,+ reln, forknum, blocknum, buffers,+ nblocks);+ RESUME_INTERRUPTS();+}+
/*
* smgrwritev() -- Write the supplied buffers out.
*
@@ -917,6 +959,25 @@ smgrimmedsync(SMgrRelation reln, ForkNumber forknum)
RESUME_INTERRUPTS();
}
+/*+ * Return fd for the specified block number and update *off to the appropriate+ * position.+ *+ * This is only to be used for when AIO needs to perform the IO in a different+ * process than where it was issued (e.g. in an IO worker).+ */+static int+smgrfd(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, uint32 *off)+{+ int fd;++ HOLD_INTERRUPTS();+ fd = smgrsw[reln->smgr_which].smgr_fd(reln, forknum, blocknum, off);+ RESUME_INTERRUPTS();++ return fd;+}+
/*
* AtEOXact_SMgr
*
@@ -945,3 +1006,92 @@ ProcessBarrierSmgrRelease(void)
smgrreleaseall();
return true;
}
++/*+ * Set target of the IO handle to be smgr and initialize all the relevant+ * pieces of data.+ */+void+pgaio_io_set_target_smgr(PgAioHandle *ioh,+ SMgrRelationData *smgr,+ ForkNumber forknum,+ BlockNumber blocknum,+ int nblocks,+ bool skip_fsync)+{+ PgAioTargetData *sd = pgaio_io_get_target_data(ioh);++ pgaio_io_set_target(ioh, PGAIO_TID_SMGR);++ /* backend is implied via IO owner */+ sd->smgr.rlocator = smgr->smgr_rlocator.locator;+ sd->smgr.forkNum = forknum;+ sd->smgr.blockNum = blocknum;+ sd->smgr.nblocks = nblocks;+ sd->smgr.is_temp = SmgrIsTemp(smgr);+ /* Temp relations should never be fsync'd */+ sd->smgr.skip_fsync = skip_fsync && !SmgrIsTemp(smgr);+}++/*+ * Callback for smgr AIO targets, to reopen the file (e.g. because the IO is+ * executed in a worker).+ */+static void+smgr_aio_reopen(PgAioHandle *ioh)+{+ PgAioTargetData *sd = pgaio_io_get_target_data(ioh);+ PgAioOpData *od = pgaio_io_get_op_data(ioh);+ SMgrRelation reln;+ ProcNumber procno;+ uint32 off;++ if (sd->smgr.is_temp)+ procno = pgaio_io_get_owner(ioh);+ else+ procno = INVALID_PROC_NUMBER;++ reln = smgropen(sd->smgr.rlocator, procno);+ switch (pgaio_io_get_op(ioh))+ {+ case PGAIO_OP_INVALID:+ pg_unreachable();+ break;+ case PGAIO_OP_READV:+ od->read.fd = smgrfd(reln, sd->smgr.forkNum, sd->smgr.blockNum, &off);+ Assert(off == od->read.offset);+ break;+ case PGAIO_OP_WRITEV:+ od->write.fd = smgrfd(reln, sd->smgr.forkNum, sd->smgr.blockNum, &off);+ Assert(off == od->write.offset);+ break;+ }+}++/*+ * Callback for smgr AIO targets, to describe the target of the IO.+ */+static char *+smgr_aio_describe_identity(const PgAioTargetData *sd)+{+ RelPathStr path;+ char *desc;++ path = relpathbackend(sd->smgr.rlocator,+ sd->smgr.is_temp ? MyProcNumber : INVALID_PROC_NUMBER,+ sd->smgr.forkNum);++ if (sd->smgr.nblocks == 0)+ desc = psprintf(_("file \"%s\""), path.str);+ else if (sd->smgr.nblocks == 1)+ desc = psprintf(_("block %u in file \"%s\""),+ sd->smgr.blockNum,+ path.str);+ else+ desc = psprintf(_("blocks %u..%u in file \"%s\""),+ sd->smgr.blockNum,+ sd->smgr.blockNum + sd->smgr.nblocks - 1,+ path.str);++ return desc;+}--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0007-aio-Add-README.md-explaining-higher-level-desi.patch (19.0K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/8-v2.11-0007-aio-Add-README.md-explaining-higher-level-desi.patch)
download | inline diff:
From e614522f8aedaf46a27c4b08fa78105cc92459d7 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 07/27] aio: Add README.md explaining higher level design
Reviewed-by: Noah Misch <noah@leadboat.com>
Reviewed-by: Heikki Linnakangas <hlinnaka@iki.fi>
Discussion: https://postgr.es/m/uvrtrknj4kdytuboidbhwclo4gxhswwcpgadptsjvjqcluzmah%40brqs62irg4dt
Discussion: https://postgr.es/m/20210223100344.llw5an2aklengrmn@alap3.anarazel.de
Discussion: https://postgr.es/m/stj36ea6yyhoxtqkhpieia2z4krnam7qyetc57rfezgk4zgapf@gcnactj4z56m
---
src/backend/storage/aio/README.md | 423 ++++++++++++++++++++++++++++++
src/backend/storage/aio/aio.c | 2 +
2 files changed, 425 insertions(+)
create mode 100644 src/backend/storage/aio/README.md
diff --git a/src/backend/storage/aio/README.md b/src/backend/storage/aio/README.mdnew file mode 100644index 00000000000..012cf3f305a--- /dev/null+++ b/src/backend/storage/aio/README.md@@ -0,0 +1,423 @@+# Asynchronous & Direct IO++## Motivation++### Why Asynchronous IO++Until the introduction of asynchronous IO Postgres relied on the operating+system to hide the cost of synchronous IO from Postgres. While this worked+surprisingly well in a lot of workloads, it does not do as good a job on+prefetching and controlled writeback as we would like.++There are important expensive operations like `fdatasync()` where the operating+system cannot hide the storage latency. This is particularly important for WAL+writes, where the ability to asynchronously issue `fdatasync()` or O_DSYNC+writes can yield significantly higher throughput.+++### Why Direct / unbuffered IO++The main reasons to want to use Direct IO are:++- Lower CPU usage / higher throughput. Particularly on modern storage buffered+ writes are bottlenecked by the operating system having to copy data from the+ kernel's page cache to postgres buffer pool using the CPU. Whereas direct IO+ can often move the data directly between the storage devices and postgres'+ buffer cache, using DMA. While that transfer is ongoing, the CPU is free to+ perform other work.+- Reduced latency - Direct IO can have substantially lower latency than+ buffered IO, which can be impactful for OLTP workloads bottlenecked by WAL+ write latency.+- Avoiding double buffering between operating system cache and postgres'+ shared_buffers.+- Better control over the timing and pace of dirty data writeback.+++The main reasons *not* to use Direct IO are:++- Without AIO, Direct IO is unusably slow for most purposes.+- Even with AIO, many parts of postgres need to be modified to perform+ explicit prefetching.+- In situations where shared_buffers cannot be set appropriately large,+ e.g. because there are many different postgres instances hosted on shared+ hardware, performance will often be worse than when using buffered IO.+++## AIO Usage Example++In many cases code that can benefit from AIO does not directly have to+interact with the AIO interface, but can use AIO via higher-level+abstractions. See [Helpers](#helpers).++In this example, a buffer will be read into shared buffers.++```C+/*+ * Result of the operation, only to be accessed in this backend.+ */+PgAioReturn ioret;++/*+ * Acquire an AIO Handle, ioret will get result upon completion.+ *+ * Note that ioret needs to stay alive until the IO completes or+ * CurrentResourceOwner is released (i.e. an error is thrown).+ */+PgAioHandle *ioh = pgaio_io_acquire(CurrentResourceOwner, &ioret);++/*+ * Reference that can be used to wait for the IO we initiate below. This+ * reference can reside in local or shared memory and waited upon by any+ * process. An arbitrary number of references can be made for each IO.+ */+PgAioWaitRef iow;++pgaio_io_get_wref(ioh, &iow);++/*+ * Arrange for shared buffer completion callbacks to be called upon completion+ * of the IO. This callback will update the buffer descriptors associated with+ * the AioHandle, which e.g. allows other backends to access the buffer.+ *+ * A callback can be passed a small bit of data, e.g. to indicate whether to+ * zero a buffer if it is invalid.+ *+ * Multiple completion callbacks can be registered for each handle.+ */+pgaio_io_register_callbacks(ioh, PGAIO_HCB_SHARED_BUFFER_READV, 0);++/*+ * The completion callback needs to know which buffers to update when the IO+ * completes. As the AIO subsystem does not know about buffers, we have to+ * associate this information with the AioHandle, for use by the completion+ * callback registered above.+ *+ * In this example we're reading only a single buffer, hence the 1.+ */+pgaio_io_set_handle_data_32(ioh, (uint32 *) buffer, 1);++/*+ * Pass the AIO handle to lower-level function. When operating on the level of+ * buffers, we don't know how exactly the IO is performed, that is the+ * responsibility of the storage manager implementation.+ *+ * E.g. md.c needs to translate block numbers into offsets in segments.+ *+ * Once the IO handle has been handed off to smgstartreadv(), it may not+ * further be used, as the IO may immediately get executed below+ * smgrstartreadv() and the handle reused for another IO.+ *+ * To issue multiple IOs in an efficient way, a caller can call+ * pgaio_enter_batchmode() before starting multiple IOs, and end that batch+ * with pgaio_exit_batchmode(). Note that one needs to be careful while there+ * may be unsubmitted IOs, as another backend may need to wait for one of the+ * unsubmitted IOs. If this backend then had to wait for the other backend,+ * it'd end in an undetected deadlock. See pgaio_enter_batchmode() for more+ * details.+ *+ * Note that even while in batchmode an IO might get submitted immediately,+ * e.g. due to reaching a limit on the number of unsubmitted IOs, and even+ * complete before smgrstartreadv() returns.+ */+smgrstartreadv(ioh, operation->smgr, forknum, blkno,+ BufferGetBlock(buffer), 1);++/*+ * To benefit from AIO, it is beneficial to perform other work, including+ * submitting other IOs, before waiting for the IO to complete. Otherwise+ * we could just have used synchronous, blocking IO.+ */+perform_other_work();++/*+ * We did some other work and now need the IO operation to have completed to+ * continue.+ */+pgaio_wref_wait(&iow);++/*+ * At this point the IO has completed. We do not yet know whether it succeeded+ * or failed, however. The buffer's state has been updated, which allows other+ * backends to use the buffer (if the IO succeeded), or retry the IO (if it+ * failed).+ *+ * Note that in case the IO has failed, a LOG message may have been emitted,+ * but no ERROR has been raised. This is crucial, as another backend waiting+ * for this IO should not see an ERROR.+ *+ * To check whether the operation succeeded, and to raise an ERROR, or if more+ * appropriate LOG, the PgAioReturn we passed to pgaio_io_acquire() is used.+ */+if (ioret.result.status == PGAIO_RS_ERROR)+ pgaio_result_report(aio_ret.result, &aio_ret.target_data, ERROR);++/*+ * Besides having succeeded completely, the IO could also have partially+ * completed. If we e.g. tried to read many blocks at once, the read might have+ * only succeeded for the first few blocks.+ *+ * If the IO partially succeeded and this backend needs all blocks to have+ * completed, this backend needs to reissue the IO for the remaining buffers.+ * The AIO subsystem cannot handle this retry transparently.+ *+ * As this example is already long, and we only read a single block, we'll just+ * error out if there's a partial read.+ */+if (ioret.result.status == PGAIO_RS_PARTIAL)+ pgaio_result_report(aio_ret.result, &aio_ret.target_data, ERROR);++/*+ * The IO succeeded, so we can use the buffer now.+ */+```+++## Design Criteria & Motivation++### Deadlock and Starvation Dangers due to AIO++Using AIO in a naive way can easily lead to deadlocks in an environment where+the source/target of AIO are shared resources, like pages in postgres'+shared_buffers.++Consider one backend performing readahead on a table, initiating IO for a+number of buffers ahead of the current "scan position". If that backend then+performs some operation that blocks, or even just is slow, the IO completion+for the asynchronously initiated read may not be processed.++This AIO implementation solves this problem by requiring that AIO methods+either allow AIO completions to be processed by any backend in the system+(e.g. io_uring), or to guarantee that AIO processing will happen even when the+issuing backend is blocked (e.g. worker mode, which offloads completion+processing to the AIO workers).+++### IO can be started in critical sections++Using AIO for WAL writes can reduce the overhead of WAL logging substantially:++- AIO allows to start WAL writes eagerly, so they complete before needing to+ wait+- AIO allows to have multiple WAL flushes in progress at the same time+- AIO makes it more realistic to use O\_DIRECT + O\_DSYNC, which can reduce+ the number of roundtrips to storage on some OSs and storage HW (buffered IO+ and direct IO without O_DSYNC needs to issue a write and after the write's+ completion a cache flush, whereas O\_DIRECT + O\_DSYNC can use a single+ Force Unit Access (FUA) write).++The need to be able to execute IO in critical sections has substantial design+implication on the AIO subsystem. Mainly because completing IOs (see prior+section) needs to be possible within a critical section, even if the+to-be-completed IO itself was not issued in a critical section. Consider+e.g. the case of a backend first starting a number of writes from shared+buffers and then starting to flush the WAL. Because only a limited amount of+IO can be in-progress at the same time, initiating IO for flushing the WAL may+require to first complete IO that was started earlier.+++### State for AIO needs to live in shared memory++Because postgres uses a process model and because AIOs need to be+complete-able by any backend much of the state of the AIO subsystem needs to+live in shared memory.++In an `EXEC_BACKEND` build, a backend's executable code and other process+local state is not necessarily mapped to the same addresses in each process+due to ASLR. This means that the shared memory cannot contain pointers to+callbacks.+++## Design of the AIO Subsystem+++### AIO Methods++To achieve portability and performance, multiple methods of performing AIO are+implemented and others are likely worth adding in the future.+++#### Synchronous Mode++`io_method=sync` does not actually perform AIO but allows to use the AIO API+while performing synchronous IO. This can be useful for debugging. The code+for the synchronous mode is also used as a fallback by e.g. the [worker+mode](#worker) uses it to execute IO that cannot be executed by workers.+++#### Worker++`io_method=worker` is available on every platform postgres runs on, and+implements asynchronous IO - from the view of the issuing process - by+dispatching the IO to one of several worker processes performing the IO in a+synchronous manner.+++#### io_uring++`io_method=io_uring` is available on Linux 5.1+. In contrast to worker mode it+dispatches all IO from within the process, lowering context switch rate /+latency.+++### AIO Handles++The central API piece for postgres' AIO abstraction are AIO handles. To+execute an IO one first has to acquire an IO handle (`pgaio_io_acquire()`) and+then "defined", i.e. associate an IO operation with the handle.++Often AIO handles are acquired on a higher level and then passed to a lower+level to be fully defined. E.g., for IO to/from shared buffers, bufmgr.c+routines acquire the handle, which is then passed through smgr.c, md.c to be+finally fully defined in fd.c.++The functions used at the lowest level to define the operation are+`pgaio_io_prep_*()`.++Because acquisition of an IO handle+[must always succeed](#io-can-be-started-in-critical-sections)+and the number of AIO Handles+[has to be limited](#state-for-aio-needs-to-live-in-shared-memory)+AIO handles can be reused as soon as they have completed. Obviously code needs+to be able to react to IO completion. State can be updated using+[AIO Completion callbacks](#aio-callbacks)+and the issuing backend can provide a backend local variable to receive the+result of the IO, as described in+[AIO Result](#aio-results).+An IO can be waited for, by both the issuing and any other backend, using+[AIO References](#aio-wait-references).+++Because an AIO Handle is not executable just after calling+`pgaio_io_acquire()` and because `pgaio_io_acquire()` needs to always succeed+(absent a PANIC), only a single AIO Handle may be acquired (i.e. returned by+`pgaio_io_acquire()`) without causing the IO to have been defined (by,+potentially indirectly, causing `pgaio_io_prep_*()` to have been+called). Otherwise a backend could trivially self-deadlock by using up all AIO+Handles without the ability to wait for some of the IOs to complete.++If it turns out that an AIO Handle is not needed, e.g., because the handle was+acquired before holding a contended lock, it can be released without being+defined using `pgaio_io_release()`.+++### AIO Callbacks++Commonly several layers need to react to completion of an IO. E.g. for a read+md.c needs to check if the IO outright failed or was shorter than needed,+bufmgr.c needs to verify the page looks valid and bufmgr.c needs to update the+BufferDesc to update the buffer's state.++The fact that several layers / subsystems need to react to IO completion poses+a few challenges:++- Upper layers should not need to know details of lower layers. E.g. bufmgr.c+ should not assume the IO will pass through md.c. Therefore upper levels+ cannot know what lower layers would consider an error.++- Lower layers should not need to know about upper layers. E.g. smgr APIs are+ used going through shared buffers but are also used bypassing shared+ buffers. This means that e.g. md.c is not in a position to validate+ checksums.++- Having code in the AIO subsystem for every possible combination of layers+ would lead to a lot of duplication.++The "solution" to this the ability to associate multiple completion callbacks+with a handle. E.g. bufmgr.c can have a callback to update the BufferDesc+state and to verify the page and md.c can have another callback to check if+the IO operation was successful.++As [mentioned](#state-for-aio-needs-to-live-in-shared-memory), shared memory+currently cannot contain function pointers. Because of that completion+callbacks are not directly identified by function pointers but by IDs+(`PgAioHandleCallbackID`). A substantial added benefit is that that+allows callbacks to be identified by much smaller amount of memory (a single+byte currently).++In addition to completion, AIO callbacks also are called to "stage" an+IO. This is, e.g., used to increase buffer reference counts to account for the+AIO subsystem referencing the buffer, which is required to handle the case+where the issuing backend errors out and releases its own pins while the IO is+still ongoing.++As [explained earlier](#io-can-be-started-in-critical-sections) IO completions+need to be safe to execute in critical sections. To allow the backend that+issued the IO to error out in case of failure [AIO Result](#aio-results) can+be used.+++### AIO Targets++In addition to the completion callbacks describe above, each AIO Handle has+exactly one "target". Each target has some space inside an AIO Handle with+information specific to the target and can provide callbacks to allow to+reopen the underlying file (required for worker mode) and to describe the IO+operation (used for debug logging and error messages).++I.e., if two different uses of AIO can describe the identity of the file being+operated on the same way, it likely makes sense to use the same+target. E.g. different smgr implementations can describe IO with+RelFileLocator, ForkNumber and BlockNumber and can thus share a target. In+contrast, IO for a WAL file would be described with TimeLineID and XLogRecPtr+and it would not make sense to use the same target for smgr and WAL.+++### AIO Wait References++As [described above](#aio-handles), AIO Handles can be reused immediately+after completion and therefore cannot be used to wait for completion of the+IO. Waiting is enabled using AIO wait references, which do not just identify+an AIO Handle but also include the handles "generation".++A reference to an AIO Handle can be acquired using `pgaio_io_get_wref()` and+then waited upon using `pgaio_wref_wait()`.+++### AIO Results++As AIO completion callbacks+[are executed in critical sections](#io-can-be-started-in-critical-sections)+and [may be executed by any backend](#deadlock-and-starvation-dangers-due-to-aio)+completion callbacks cannot be used to, e.g., make the query that triggered an+IO ERROR out.++To allow to react to failing IOs the issuing backend can pass a pointer to a+`PgAioReturn` in backend local memory. Before an AIO Handle is reused the+`PgAioReturn` is filled with information about the IO. This includes+information about whether the IO was successful (as a value of+`PgAioResultStatus`) and enough information to raise an error in case of a+failure (via `pgaio_result_report()`, with the error details encoded in+`PgAioResult`).+++### AIO Errors++It would be very convenient to have shared completion callbacks encode the+details of errors as an `ErrorData` that could be raised at a later+time. Unfortunately doing so would require allocating memory. While elog.c can+guarantee (well, kinda) that logging a message will not run out of memory,+that only works because a very limited number of messages are in the process+of being logged. With AIO a large number of concurrently issued AIOs might+fail.++To avoid the need for preallocating a potentially large amount of memory (in+shared memory no less!), completion callbacks instead have to encode errors in+a more compact format that can be converted into an error message.+++## Helpers++Using the low-level AIO API introduces too much complexity to do so all over+the tree. Most uses of AIO should be done via reusable, higher-level,+helpers.+++### Read Stream++A common and very beneficial use of AIO are reads where a substantial number+of to-be-read locations are known ahead of time. E.g., for a sequential scan+the set of blocks that need to be read can be determined solely by knowing the+current position and checking the buffer mapping table.++The [Read Stream](../../../include/storage/read_stream.h) interface makes it+comparatively easy to use AIO for such use cases.diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.cindex a30c163c3de..3c3383b1318 100644--- a/src/backend/storage/aio/aio.c+++ b/src/backend/storage/aio/aio.c@@ -24,6 +24,8 @@
*
* - read_stream.c - helper for reading buffered relation data
*
+ * - README.md - higher-level overview over AIO+ *
*
* Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0008-localbuf-Track-pincount-in-BufferDesc-as-well.patch (4.5K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/9-v2.11-0008-localbuf-Track-pincount-in-BufferDesc-as-well.patch)
download | inline diff:
From 2822b36f45394855d60b10e4985ad08acb52e973 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 08/27] localbuf: Track pincount in BufferDesc as well
For AIO on temporary table buffers the AIO subsystem needs to be able to
ensure a pin on a buffer while AIO is going on, even if the IO issuing query
errors out. Tracking the buffer in LocalRefCount does not work, as it would
cause CheckForLocalBufferLeaks() to assert out.
Instead, also track the refcount in BufferDesc.state, not just
LocalRefCount. This also makes local buffers behave a bit more akin to shared
buffers.
Note that we still don't need locking, AIO completion callbacks for local
buffers are executed in the issuing session (i.e. nobody else has access to
the BufferDesc).
Discussion: https://postgr.es/m/uvrtrknj4kdytuboidbhwclo4gxhswwcpgadptsjvjqcluzmah%40brqs62irg4dt
---
src/backend/storage/buffer/bufmgr.c | 22 +++++++++++++++++++++
src/backend/storage/buffer/localbuf.c | 28 +++++++++++++++++++++++++--
2 files changed, 48 insertions(+), 2 deletions(-)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.cindex 37fb09d8b92..aea08b242ac 100644--- a/src/backend/storage/buffer/bufmgr.c+++ b/src/backend/storage/buffer/bufmgr.c@@ -5350,6 +5350,18 @@ ConditionalLockBufferForCleanup(Buffer buffer)
Assert(refcount > 0);
if (refcount != 1)
return false;
++ /*+ * Check that the AIO subsystem doesn't have a pin. Likely not+ * possible today, but better safe than sorry.+ */+ bufHdr = GetLocalBufferDescriptor(-buffer - 1);+ buf_state = pg_atomic_read_u32(&bufHdr->state);+ refcount = BUF_STATE_GET_REFCOUNT(buf_state);+ Assert(refcount > 0);+ if (refcount != 1)+ return false;+
/* Nobody else to wait for */
return true;
}
@@ -5403,6 +5415,16 @@ IsBufferCleanupOK(Buffer buffer)
/* There should be exactly one pin */
if (LocalRefCount[-buffer - 1] != 1)
return false;
++ /*+ * Check that the AIO subsystem doesn't have a pin. Likely not+ * possible today, but better safe than sorry.+ */+ bufHdr = GetLocalBufferDescriptor(-buffer - 1);+ buf_state = pg_atomic_read_u32(&bufHdr->state);+ if (BUF_STATE_GET_REFCOUNT(buf_state) != 1)+ return false;+
/* Nobody else to wait for */
return true;
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.cindex f172a5c7820..3a722321533 100644--- a/src/backend/storage/buffer/localbuf.c+++ b/src/backend/storage/buffer/localbuf.c@@ -249,6 +249,13 @@ GetLocalVictimBuffer(void)
pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
trycounter = NLocBuffer;
}
+ else if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)+ {+ /*+ * This can be reached if the backend initiated AIO for this+ * buffer and then errored out.+ */+ }
else
{
/* Found a usable buffer */
@@ -570,7 +577,13 @@ InvalidateLocalBuffer(BufferDesc *bufHdr, bool check_unreferenced)
buf_state = pg_atomic_read_u32(&bufHdr->state);
- if (check_unreferenced && LocalRefCount[bufid] != 0)+ /*+ * We need to test not just LocalRefCount[bufid] but also the BufferDesc+ * itself, as the latter is used to represent a pin by the AIO subsystem.+ * This can happen if AIO is initiated and then the query errors out.+ */+ if (check_unreferenced &&+ (LocalRefCount[bufid] != 0 || BUF_STATE_GET_REFCOUNT(buf_state) != 0))
elog(ERROR, "block %u of %s is still referenced (local %u)",
bufHdr->tag.blockNum,
relpathbackend(BufTagGetRelFileLocator(&bufHdr->tag),
@@ -744,12 +757,13 @@ PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount)
if (LocalRefCount[bufid] == 0)
{
NLocalPinnedBuffers++;
+ buf_state += BUF_REFCOUNT_ONE;
if (adjust_usagecount &&
BUF_STATE_GET_USAGECOUNT(buf_state) < BM_MAX_USAGE_COUNT)
{
buf_state += BUF_USAGECOUNT_ONE;
- pg_atomic_unlocked_write_u32(&buf_hdr->state, buf_state);
}
+ pg_atomic_unlocked_write_u32(&buf_hdr->state, buf_state);
}
LocalRefCount[bufid]++;
ResourceOwnerRememberBuffer(CurrentResourceOwner,
@@ -775,7 +789,17 @@ UnpinLocalBufferNoOwner(Buffer buffer)
Assert(NLocalPinnedBuffers > 0);
if (--LocalRefCount[buffid] == 0)
+ {+ BufferDesc *buf_hdr = GetLocalBufferDescriptor(buffid);+ uint32 buf_state;+
NLocalPinnedBuffers--;
++ buf_state = pg_atomic_read_u32(&buf_hdr->state);+ Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0);+ buf_state -= BUF_REFCOUNT_ONE;+ pg_atomic_unlocked_write_u32(&buf_hdr->state, buf_state);+ }
}
/*
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0009-bufmgr-Implement-AIO-read-support.patch (30.1K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/10-v2.11-0009-bufmgr-Implement-AIO-read-support.patch)
download | inline diff:
From ed356428e1058b156a158c2e319668611f3539cc Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 09/27] bufmgr: Implement AIO read support
This commit implements the infrastructure to perform asynchronous reads into
the buffer pool.
To do so, it:
- Adds readv AIO callbacks for shared and local buffers
It may be worth calling out that shared buffer completions may be run in a
different backend than where the IO started.
- Adds an AIO wait reference to BufferDesc, to allow backends to wait for
in-progress asynchronous IOs
- Adapts StartBufferIO(), WaitIO(), TerminateBufferIO(), and their localbuf.c
equivalents, to be able to deal with AIO
- Moves the code to handle BM_PIN_COUNT_WAITER into a helper function, as it
now also needs to be called on IO completion
As of this commit, nothing issues AIO on shared buffers. A future commit will
update StartReadBuffers() to do so.
Reviewed-by: Noah Misch <noah@leadboat.com>
Reviewed-by: Melanie Plageman <melanieplageman@gmail.com>
Reviewed-by: Nazir Bilal Yavuz <byavuz81@gmail.com>
Discussion: https://postgr.es/m/uvrtrknj4kdytuboidbhwclo4gxhswwcpgadptsjvjqcluzmah%40brqs62irg4dt
Discussion: https://postgr.es/m/20210223100344.llw5an2aklengrmn@alap3.anarazel.de
Discussion: https://postgr.es/m/stj36ea6yyhoxtqkhpieia2z4krnam7qyetc57rfezgk4zgapf@gcnactj4z56m
---
src/include/storage/aio.h | 4 +
src/include/storage/buf_internals.h | 7 +-
src/include/storage/bufmgr.h | 4 +
src/backend/storage/aio/aio_callback.c | 5 +
src/backend/storage/buffer/README | 9 +-
src/backend/storage/buffer/buf_init.c | 3 +
src/backend/storage/buffer/bufmgr.c | 496 +++++++++++++++++++++++--
src/backend/storage/buffer/localbuf.c | 47 ++-
8 files changed, 523 insertions(+), 52 deletions(-)
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.hindex 1591a6c2fca..9a0868a270c 100644--- a/src/include/storage/aio.h+++ b/src/include/storage/aio.h@@ -195,6 +195,10 @@ typedef enum PgAioHandleCallbackID
PGAIO_HCB_MD_READV,
PGAIO_HCB_MD_WRITEV,
++ PGAIO_HCB_SHARED_BUFFER_READV,++ PGAIO_HCB_LOCAL_BUFFER_READV,
} PgAioHandleCallbackID;
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.hindex 9327f60c44c..8a52ace37cd 100644--- a/src/include/storage/buf_internals.h+++ b/src/include/storage/buf_internals.h@@ -17,6 +17,7 @@
#include "pgstat.h"
#include "port/atomics.h"
+#include "storage/aio_types.h"
#include "storage/buf.h"
#include "storage/bufmgr.h"
#include "storage/condition_variable.h"
@@ -264,6 +265,8 @@ typedef struct BufferDesc
int wait_backend_pgprocno; /* backend of pin-count waiter */
int freeNext; /* link in freelist chain */
++ PgAioWaitRef io_wref; /* set iff AIO is in progress */
LWLock content_lock; /* to lock access to buffer contents */
} BufferDesc;
@@ -472,8 +475,8 @@ extern BlockNumber ExtendBufferedRelLocal(BufferManagerRelation bmr,
uint32 *extended_by);
extern void MarkLocalBufferDirty(Buffer buffer);
extern void TerminateLocalBufferIO(BufferDesc *bufHdr, bool clear_dirty,
- uint32 set_flag_bits);-extern bool StartLocalBufferIO(BufferDesc *bufHdr, bool forInput);+ uint32 set_flag_bits, bool syncio);+extern bool StartLocalBufferIO(BufferDesc *bufHdr, bool forInput, bool nowait);
extern void FlushLocalBuffer(BufferDesc *bufHdr, SMgrRelation reln);
extern void DropRelationLocalBuffers(RelFileLocator rlocator,
ForkNumber forkNum,
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.hindex ecc1c3a909b..b8c28fe79f4 100644--- a/src/include/storage/bufmgr.h+++ b/src/include/storage/bufmgr.h@@ -15,6 +15,7 @@
#define BUFMGR_H
#include "port/pg_iovec.h"
+#include "storage/aio_types.h"
#include "storage/block.h"
#include "storage/buf.h"
#include "storage/bufpage.h"
@@ -171,6 +172,9 @@ extern PGDLLIMPORT int checkpoint_flush_after;
extern PGDLLIMPORT int backend_flush_after;
extern PGDLLIMPORT int bgwriter_flush_after;
+extern const PgAioHandleCallbacks aio_shared_buffer_readv_cb;+extern const PgAioHandleCallbacks aio_local_buffer_readv_cb;+
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
diff --git a/src/backend/storage/aio/aio_callback.c b/src/backend/storage/aio/aio_callback.cindex a92fa8f678e..3cdd2a6b195 100644--- a/src/backend/storage/aio/aio_callback.c+++ b/src/backend/storage/aio/aio_callback.c@@ -18,6 +18,7 @@
#include "miscadmin.h"
#include "storage/aio.h"
#include "storage/aio_internal.h"
+#include "storage/bufmgr.h"
#include "storage/md.h"
@@ -40,6 +41,10 @@ static const PgAioHandleCallbacksEntry aio_handle_cbs[] = {
CALLBACK_ENTRY(PGAIO_HCB_INVALID, aio_invalid_cb),
CALLBACK_ENTRY(PGAIO_HCB_MD_READV, aio_md_readv_cb),
++ CALLBACK_ENTRY(PGAIO_HCB_SHARED_BUFFER_READV, aio_shared_buffer_readv_cb),++ CALLBACK_ENTRY(PGAIO_HCB_LOCAL_BUFFER_READV, aio_local_buffer_readv_cb),
#undef CALLBACK_ENTRY
};
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/READMEindex 011af7aff3e..a182fcd660c 100644--- a/src/backend/storage/buffer/README+++ b/src/backend/storage/buffer/README@@ -147,9 +147,12 @@ in the buffer. It is used per the rules above.
* The BM_IO_IN_PROGRESS flag acts as a kind of lock, used to wait for I/O on a
buffer to complete (and in releases before 14, it was accompanied by a
-per-buffer LWLock). The process doing a read or write sets the flag for the-duration, and processes that need to wait for it to be cleared sleep on a-condition variable.+per-buffer LWLock). The process starting a read or write sets the flag. When+the I/O is completed, be it by the process that initiated the I/O or by+another process, the flag is removed and the Buffer's condition variable is+signalled. Processes that need to wait for the I/O to complete can wait for+asynchronous I/O by using BufferDesc->io_wref and for BM_IO_IN_PROGRESS to be+unset by sleeping on the buffer's condition variable.
Normal Buffer Replacement Strategy
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.cindex ed1f8e03190..ed1dc488a42 100644--- a/src/backend/storage/buffer/buf_init.c+++ b/src/backend/storage/buffer/buf_init.c@@ -14,6 +14,7 @@
*/
#include "postgres.h"
+#include "storage/aio.h"
#include "storage/buf_internals.h"
#include "storage/bufmgr.h"
@@ -125,6 +126,8 @@ BufferManagerShmemInit(void)
buf->buf_id = i;
+ pgaio_wref_clear(&buf->io_wref);+
/*
* Initially link all the buffers together as unused. Subsequent
* management of this list is done by freelist.c.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.cindex aea08b242ac..ea5f504e38a 100644--- a/src/backend/storage/buffer/bufmgr.c+++ b/src/backend/storage/buffer/bufmgr.c@@ -48,6 +48,7 @@
#include "pg_trace.h"
#include "pgstat.h"
#include "postmaster/bgwriter.h"
+#include "storage/aio.h"
#include "storage/buf_internals.h"
#include "storage/bufmgr.h"
#include "storage/fd.h"
@@ -519,7 +520,8 @@ static int SyncOneBuffer(int buf_id, bool skip_recently_used,
static void WaitIO(BufferDesc *buf);
static bool StartBufferIO(BufferDesc *buf, bool forInput, bool nowait);
static void TerminateBufferIO(BufferDesc *buf, bool clear_dirty,
- uint32 set_flag_bits, bool forget_owner);+ uint32 set_flag_bits, bool forget_owner,+ bool syncio);
static void AbortBufferIO(Buffer buffer);
static void shared_buffer_write_error_callback(void *arg);
static void local_buffer_write_error_callback(void *arg);
@@ -1041,7 +1043,7 @@ ZeroAndLockBuffer(Buffer buffer, ReadBufferMode mode, bool already_valid)
{
/* Simple case for non-shared buffers. */
bufHdr = GetLocalBufferDescriptor(-buffer - 1);
- need_to_zero = StartLocalBufferIO(bufHdr, true);+ need_to_zero = StartLocalBufferIO(bufHdr, true, false);
}
else
{
@@ -1077,9 +1079,9 @@ ZeroAndLockBuffer(Buffer buffer, ReadBufferMode mode, bool already_valid)
/* Set BM_VALID, terminate IO, and wake up any waiters */
if (isLocalBuf)
- TerminateLocalBufferIO(bufHdr, false, BM_VALID);+ TerminateLocalBufferIO(bufHdr, false, BM_VALID, true);
else
- TerminateBufferIO(bufHdr, false, BM_VALID, true);+ TerminateBufferIO(bufHdr, false, BM_VALID, true, true);
}
else if (!isLocalBuf)
{
@@ -1391,7 +1393,8 @@ static inline bool
WaitReadBuffersCanStartIO(Buffer buffer, bool nowait)
{
if (BufferIsLocal(buffer))
- return StartLocalBufferIO(GetLocalBufferDescriptor(-buffer - 1), true);+ return StartLocalBufferIO(GetLocalBufferDescriptor(-buffer - 1),+ true, nowait);
else
return StartBufferIO(GetBufferDescriptor(buffer - 1), true, nowait);
}
@@ -1549,9 +1552,9 @@ WaitReadBuffers(ReadBuffersOperation *operation)
/* Set BM_VALID, terminate IO, and wake up any waiters */
if (persistence == RELPERSISTENCE_TEMP)
- TerminateLocalBufferIO(bufHdr, false, BM_VALID);+ TerminateLocalBufferIO(bufHdr, false, BM_VALID, true);
else
- TerminateBufferIO(bufHdr, false, BM_VALID, true);+ TerminateBufferIO(bufHdr, false, BM_VALID, true, true);
/* Report I/Os as completing individually. */
TRACE_POSTGRESQL_BUFFER_READ_DONE(forknum, io_first_block + j,
@@ -2459,7 +2462,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr,
if (lock)
LWLockAcquire(BufferDescriptorGetContentLock(buf_hdr), LW_EXCLUSIVE);
- TerminateBufferIO(buf_hdr, false, BM_VALID, true);+ TerminateBufferIO(buf_hdr, false, BM_VALID, true, true);
}
pgBufferUsage.shared_blks_written += extend_by;
@@ -2805,6 +2808,44 @@ PinBuffer_Locked(BufferDesc *buf)
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
}
+/*+ * Support for waking up another backend that is waiting for the cleanup lock+ * to be released using BM_PIN_COUNT_WAITER.+ *+ * See LockBufferForCleanup().+ *+ * Expected to be called just after releasing a buffer pin (in a BufferDesc,+ * not just reducing the backend-local pincount for the buffer).+ */+static void+WakePinCountWaiter(BufferDesc *buf)+{+ /*+ * Acquire the buffer header lock, re-check that there's a waiter. Another+ * backend could have unpinned this buffer, and already woken up the+ * waiter.+ *+ * There's no danger of the buffer being replaced after we unpinned it+ * above, as it's pinned by the waiter. The waiter removes+ * BM_PIN_COUNT_WAITER if it stops waiting for a reason other than this+ * backend waking it up.+ */+ uint32 buf_state = LockBufHdr(buf);++ if ((buf_state & BM_PIN_COUNT_WAITER) &&+ BUF_STATE_GET_REFCOUNT(buf_state) == 1)+ {+ /* we just released the last pin other than the waiter's */+ int wait_backend_pgprocno = buf->wait_backend_pgprocno;++ buf_state &= ~BM_PIN_COUNT_WAITER;+ UnlockBufHdr(buf, buf_state);+ ProcSendSignal(wait_backend_pgprocno);+ }+ else+ UnlockBufHdr(buf, buf_state);+}+
/*
* UnpinBuffer -- make buffer available for replacement.
*
@@ -2873,29 +2914,8 @@ UnpinBufferNoOwner(BufferDesc *buf)
/* Support LockBufferForCleanup() */
if (buf_state & BM_PIN_COUNT_WAITER)
- {- /*- * Acquire the buffer header lock, re-check that there's a waiter.- * Another backend could have unpinned this buffer, and already- * woken up the waiter. There's no danger of the buffer being- * replaced after we unpinned it above, as it's pinned by the- * waiter.- */- buf_state = LockBufHdr(buf);+ WakePinCountWaiter(buf);- if ((buf_state & BM_PIN_COUNT_WAITER) &&- BUF_STATE_GET_REFCOUNT(buf_state) == 1)- {- /* we just released the last pin other than the waiter's */- int wait_backend_pgprocno = buf->wait_backend_pgprocno;-- buf_state &= ~BM_PIN_COUNT_WAITER;- UnlockBufHdr(buf, buf_state);- ProcSendSignal(wait_backend_pgprocno);- }- else- UnlockBufHdr(buf, buf_state);- }
ForgetPrivateRefCountEntry(ref);
}
}
@@ -3916,7 +3936,7 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
* Mark the buffer as clean (unless BM_JUST_DIRTIED has become set) and
* end the BM_IO_IN_PROGRESS state.
*/
- TerminateBufferIO(buf, true, 0, true);+ TerminateBufferIO(buf, true, 0, true, true);
TRACE_POSTGRESQL_BUFFER_FLUSH_DONE(BufTagGetForkNum(&buf->tag),
buf->tag.blockNum,
@@ -5472,6 +5492,7 @@ WaitIO(BufferDesc *buf)
for (;;)
{
uint32 buf_state;
+ PgAioWaitRef iow;
/*
* It may not be necessary to acquire the spinlock to check the flag
@@ -5479,10 +5500,40 @@ WaitIO(BufferDesc *buf)
* play it safe.
*/
buf_state = LockBufHdr(buf);
++ /*+ * Copy the wait reference while holding the spinlock. This protects+ * against a concurrent TerminateBufferIO() in another backend from+ * clearing the wref while it's being read.+ */+ iow = buf->io_wref;
UnlockBufHdr(buf, buf_state);
+ /* no IO in progress, we don't need to wait */
if (!(buf_state & BM_IO_IN_PROGRESS))
break;
++ /*+ * The buffer has asynchronous IO in progress, wait for it to+ * complete.+ */+ if (pgaio_wref_valid(&iow))+ {+ pgaio_wref_wait(&iow);++ /*+ * The AIO subsystem internally uses condition variables and thus+ * might remove this backend from the BufferDesc's CV. While that+ * wouldn't cause a correctness issue (the first CV sleep just+ * immediately returns if not already registered), it seems worth+ * avoiding unnecessary loop iterations, given that we take care+ * to do so at the start of the function.+ */+ ConditionVariablePrepareToSleep(cv);+ continue;+ }++ /* wait on BufferDesc->cv, e.g. for concurrent synchronous IO */
ConditionVariableSleep(cv, WAIT_EVENT_BUFFER_IO);
}
ConditionVariableCancelSleep();
@@ -5491,13 +5542,12 @@ WaitIO(BufferDesc *buf)
/*
* StartBufferIO: begin I/O on this buffer
* (Assumptions)
- * My process is executing no IO+ * My process is executing no IO on this buffer
* The buffer is Pinned
*
- * In some scenarios there are race conditions in which multiple backends- * could attempt the same I/O operation concurrently. If someone else- * has already started I/O on this buffer then we will block on the- * I/O condition variable until he's done.+ * In some scenarios multiple backends could attempt the same I/O operation+ * concurrently. If someone else has already started I/O on this buffer then+ * we will wait for completion of the IO using WaitIO().
*
* Input operations are only attempted on buffers that are not BM_VALID,
* and output operations only on buffers that are BM_VALID and BM_DIRTY,
@@ -5533,9 +5583,9 @@ StartBufferIO(BufferDesc *buf, bool forInput, bool nowait)
/* Once we get here, there is definitely no I/O active on this buffer */
+ /* Check if someone else already did the I/O */
if (forInput ? (buf_state & BM_VALID) : !(buf_state & BM_DIRTY))
{
- /* someone else already did the I/O */
UnlockBufHdr(buf, buf_state);
return false;
}
@@ -5571,7 +5621,7 @@ StartBufferIO(BufferDesc *buf, bool forInput, bool nowait)
*/
static void
TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint32 set_flag_bits,
- bool forget_owner)+ bool forget_owner, bool syncio)
{
uint32 buf_state;
@@ -5586,6 +5636,14 @@ TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint32 set_flag_bits,
if (clear_dirty && !(buf_state & BM_JUST_DIRTIED))
buf_state &= ~(BM_DIRTY | BM_CHECKPOINT_NEEDED);
+ if (!syncio)+ {+ /* release ownership by the AIO subsystem */+ Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0);+ buf_state -= BUF_REFCOUNT_ONE;+ pgaio_wref_clear(&buf->io_wref);+ }+
buf_state |= set_flag_bits;
UnlockBufHdr(buf, buf_state);
@@ -5594,6 +5652,17 @@ TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint32 set_flag_bits,
BufferDescriptorGetBuffer(buf));
ConditionVariableBroadcast(BufferDescriptorGetIOCV(buf));
++ /*+ * Support LockBufferForCleanup()+ *+ * We may have just released the last pin other than the waiter's. In most+ * cases, this backend holds another pin on the buffer. But, if, for+ * example, this backend is completing an IO issued by another backend, it+ * may be time to wake the waiter.+ */+ if (buf_state & BM_PIN_COUNT_WAITER)+ WakePinCountWaiter(buf);
}
/*
@@ -5642,7 +5711,7 @@ AbortBufferIO(Buffer buffer)
}
}
- TerminateBufferIO(buf_hdr, false, BM_IO_ERROR, false);+ TerminateBufferIO(buf_hdr, false, BM_IO_ERROR, false, true);
}
/*
@@ -6093,3 +6162,352 @@ EvictUnpinnedBuffer(Buffer buf)
return result;
}
++/*+ * Generic implementation of the AIO handle staging callback for readv/writev+ * on local/shared buffers.+ *+ * Each readv/writev can target multiple buffers. The buffers have already+ * been registered with the IO handle.+ *+ * To make the IO ready for execution ("staging"), we need to ensure that the+ * targeted buffers are in an appropriate state while the IO is ongoing. For+ * that the AIO subsystem needs to have its own buffer pin, otherwise an error+ * in this backend could lead to this backend's buffer pin being released as+ * part of error handling, which in turn could lead to the buffer being+ * replaced while IO is ongoing.+ */+static pg_attribute_always_inline void+buffer_stage_common(PgAioHandle *ioh, bool is_write, bool is_temp)+{+ uint64 *io_data;+ uint8 handle_data_len;+ PgAioWaitRef io_ref;+ BufferTag first PG_USED_FOR_ASSERTS_ONLY = {0};++ io_data = pgaio_io_get_handle_data(ioh, &handle_data_len);++ pgaio_io_get_wref(ioh, &io_ref);++ /* iterate over all buffers affected by the vectored readv/writev */+ for (int i = 0; i < handle_data_len; i++)+ {+ Buffer buffer = (Buffer) io_data[i];+ BufferDesc *buf_hdr = is_temp ?+ GetLocalBufferDescriptor(-buffer - 1)+ : GetBufferDescriptor(buffer - 1);+ uint32 buf_state;++ /*+ * Check that all the buffers are actually ones that could conceivably+ * be done in one IO, i.e. are sequential. This is the last+ * buffer-aware code before IO is actually executed and confusion+ * about which buffers are target by IO can be hard to debug, making+ * it worth doing extra-paranoid checks.+ */+ if (i == 0)+ first = buf_hdr->tag;+ else+ {+ Assert(buf_hdr->tag.relNumber == first.relNumber);+ Assert(buf_hdr->tag.blockNum == first.blockNum + i);+ }++ if (is_temp)+ buf_state = pg_atomic_read_u32(&buf_hdr->state);+ else+ buf_state = LockBufHdr(buf_hdr);++ /* verify the buffer is in the expected state */+ Assert(buf_state & BM_TAG_VALID);+ if (is_write)+ {+ Assert(buf_state & BM_VALID);+ Assert(buf_state & BM_DIRTY);+ }+ else+ {+ Assert(!(buf_state & BM_VALID));+ Assert(!(buf_state & BM_DIRTY));+ }++ /* temp buffers don't use BM_IO_IN_PROGRESS */+ if (!is_temp)+ Assert(buf_state & BM_IO_IN_PROGRESS);++ Assert(BUF_STATE_GET_REFCOUNT(buf_state) >= 1);++ /*+ * Reflect that the buffer is now owned by the AIO subsystem.+ *+ * For local buffers: This can't be done just via LocalRefCount, as+ * one might initially think, as this backend could error out while+ * AIO is still in progress, releasing all the pins by the backend+ * itself.+ *+ * This pin is released again in TerminateBufferIO().+ */+ buf_state += BUF_REFCOUNT_ONE;+ buf_hdr->io_wref = io_ref;++ if (is_temp)+ pg_atomic_unlocked_write_u32(&buf_hdr->state, buf_state);+ else+ UnlockBufHdr(buf_hdr, buf_state);++ /*+ * Ensure the content lock that prevents buffer modifications while+ * the buffer is being written out is not released early due to an+ * error.+ */+ if (is_write && !is_temp)+ {+ LWLock *content_lock;++ content_lock = BufferDescriptorGetContentLock(buf_hdr);++ Assert(LWLockHeldByMe(content_lock));++ /*+ * Lock is now owned by AIO subsystem.+ */+ LWLockDisown(content_lock);+ }++ /*+ * Stop tracking this buffer via the resowner - the AIO system now+ * keeps track.+ */+ if (!is_temp)+ ResourceOwnerForgetBufferIO(CurrentResourceOwner, buffer);+ }+}++/*+ * Helper for AIO readv completion callbacks, supporting both shared and temp+ * buffers. Gets called once for each buffer in a multi-page read.+ */+static pg_attribute_always_inline PgAioResult+buffer_readv_complete_one(uint8 buf_off, Buffer buffer, uint8 flags,+ bool failed, bool is_temp)+{+ BufferDesc *buf_hdr = is_temp ?+ GetLocalBufferDescriptor(-buffer - 1)+ : GetBufferDescriptor(buffer - 1);+ BufferTag tag = buf_hdr->tag;+ char *bufdata = BufferGetBlock(buffer);+ PgAioResult result;+ uint32 set_flag_bits;++ /* check that the buffer is in the expected state for a read */+#ifdef USE_ASSERT_CHECKING+ {+ uint32 buf_state = pg_atomic_read_u32(&buf_hdr->state);++ Assert(buf_state & BM_TAG_VALID);+ Assert(!(buf_state & BM_VALID));+ /* temp buffers don't use BM_IO_IN_PROGRESS */+ if (!is_temp)+ Assert(buf_state & BM_IO_IN_PROGRESS);+ Assert(!(buf_state & BM_DIRTY));+ }+#endif++ result.status = PGAIO_RS_OK;++ /* check for garbage data */+ if (!failed &&+ !PageIsVerifiedExtended((Page) bufdata, tag.blockNum,+ PIV_LOG_WARNING | PIV_REPORT_STAT))+ {+ RelFileLocator rlocator = BufTagGetRelFileLocator(&tag);++ if ((flags & READ_BUFFERS_ZERO_ON_ERROR) || zero_damaged_pages)+ {+ ereport(WARNING,+ (errcode(ERRCODE_DATA_CORRUPTED),+ errmsg("invalid page in block %u of relation %s; zeroing out page",+ tag.blockNum,+ relpathbackend(rlocator,+ is_temp ? MyProcNumber : INVALID_PROC_NUMBER,+ tag.forkNum).str)));+ memset(bufdata, 0, BLCKSZ);+ }+ else+ {+ /* mark buffer as having failed */+ failed = true;++ /* encode error for buffer_readv_report */+ result.status = PGAIO_RS_ERROR;+ if (is_temp)+ result.id = PGAIO_HCB_LOCAL_BUFFER_READV;+ else+ result.id = PGAIO_HCB_SHARED_BUFFER_READV;+ result.error_data = buf_off;+ }+ }++ /* Terminate I/O and set BM_VALID. */+ set_flag_bits = failed ? BM_IO_ERROR : BM_VALID;+ if (is_temp)+ TerminateLocalBufferIO(buf_hdr, false, set_flag_bits, false);+ else+ TerminateBufferIO(buf_hdr, false, set_flag_bits, false, false);++ /*+ * Call the BUFFER_READ_DONE tracepoint in the callback, even though the+ * callback may not be executed in the same backend that called+ * BUFFER_READ_START. The alternative would be to defer calling the+ * tracepoint to a later point (e.g. the local completion callback for+ * shared buffer reads), which seems even less helpful and would also+ * imply some overhead.+ */+ TRACE_POSTGRESQL_BUFFER_READ_DONE(tag.forkNum,+ tag.blockNum,+ tag.spcOid,+ tag.dbOid,+ tag.relNumber,+ is_temp ? MyProcNumber : INVALID_PROC_NUMBER,+ false);++ return result;+}++/*+ * Perform completion handling of a single AIO read. This read may cover+ * multiple blocks / buffers.+ *+ * Shared between shared and local buffers, to reduce code duplication.+ */+static pg_attribute_always_inline PgAioResult+buffer_readv_complete(PgAioHandle *ioh, PgAioResult prior_result,+ uint8 cb_data, bool is_temp)+{+ PgAioResult result = prior_result;+ PgAioTargetData *td = pgaio_io_get_target_data(ioh);+ uint64 *io_data;+ uint8 handle_data_len;++ if (is_temp)+ {+ Assert(td->smgr.is_temp);+ Assert(pgaio_io_get_owner(ioh) == MyProcNumber);+ }+ else+ Assert(!td->smgr.is_temp);++ /*+ * Iterate over all the buffers affected by this IO and call the+ * per-buffer completion function for each buffer.+ */+ io_data = pgaio_io_get_handle_data(ioh, &handle_data_len);+ for (uint8 buf_off = 0; buf_off < handle_data_len; buf_off++)+ {+ Buffer buf = io_data[buf_off];+ PgAioResult buf_result;+ bool failed;++ Assert(BufferIsValid(buf));++ /*+ * If the entire I/O failed on a lower-level, each buffer needs to be+ * marked as failed. In case of a partial read, some buffers may be+ * ok.+ */+ failed =+ prior_result.status == PGAIO_RS_ERROR+ || prior_result.result <= buf_off;++ buf_result = buffer_readv_complete_one(buf_off, buf, cb_data, failed,+ is_temp);++ /*+ * If there wasn't any prior error and page verification failed in+ * some form, set the whole IO's result to the page's result.+ */+ if (result.status != PGAIO_RS_ERROR+ && buf_result.status != PGAIO_RS_OK)+ {+ result = buf_result;+ pgaio_result_report(result, td, LOG);+ }+ }++ return result;+}++/*+ * AIO error reporting callback for aio_shared_buffer_readv_cb and+ * aio_local_buffer_readv_cb.+ *+ * Errors are encoded as follows:+ * - the only error is page verification failing+ * - result.error_data is the offset of the first page that failed+ * verification in a larger IO+ */+static void+buffer_readv_report(PgAioResult result, const PgAioTargetData *target_data,+ int elevel)+{+ ProcNumber errProc;++ if (target_data->smgr.is_temp)+ errProc = MyProcNumber;+ else+ errProc = INVALID_PROC_NUMBER;++ ereport(elevel,+ errcode(ERRCODE_DATA_CORRUPTED),+ errmsg("invalid page in block %u of relation %s",+ target_data->smgr.blockNum + result.error_data,+ relpathbackend(target_data->smgr.rlocator, errProc,+ target_data->smgr.forkNum).str));+}++static void+shared_buffer_readv_stage(PgAioHandle *ioh, uint8 cb_data)+{+ buffer_stage_common(ioh, false, false);+}++static PgAioResult+shared_buffer_readv_complete(PgAioHandle *ioh, PgAioResult prior_result,+ uint8 cb_data)+{+ return buffer_readv_complete(ioh, prior_result, cb_data, false);+}++static void+local_buffer_readv_stage(PgAioHandle *ioh, uint8 cb_data)+{+ buffer_stage_common(ioh, false, true);+}++static PgAioResult+local_buffer_readv_complete(PgAioHandle *ioh, PgAioResult prior_result,+ uint8 cb_data)+{+ return buffer_readv_complete(ioh, prior_result, cb_data, true);+}++/* readv callback is passed READ_BUFFERS_* flags as callback data */+const PgAioHandleCallbacks aio_shared_buffer_readv_cb = {+ .stage = shared_buffer_readv_stage,+ .complete_shared = shared_buffer_readv_complete,+ .report = buffer_readv_report,+};++/* readv callback is passed READ_BUFFERS_* flags as callback data */+const PgAioHandleCallbacks aio_local_buffer_readv_cb = {+ .stage = local_buffer_readv_stage,++ /*+ * Note that this, in contrast to the shared_buffers case, uses+ * complete_local, as only the issuing backend has access to the required+ * datastructures. This is important in case the IO completion may be+ * consumed incidentally by another backend.+ */+ .complete_local = local_buffer_readv_complete,+ .report = buffer_readv_report,+};diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.cindex 3a722321533..8b6cd54d09e 100644--- a/src/backend/storage/buffer/localbuf.c+++ b/src/backend/storage/buffer/localbuf.c@@ -17,7 +17,9 @@
#include "access/parallel.h"
#include "executor/instrument.h"
+#include "pg_trace.h"
#include "pgstat.h"
+#include "storage/aio.h"
#include "storage/buf_internals.h"
#include "storage/bufmgr.h"
#include "storage/fd.h"
@@ -187,7 +189,7 @@ FlushLocalBuffer(BufferDesc *bufHdr, SMgrRelation reln)
* Try to start an I/O operation. There currently are no reasons for
* StartLocalBufferIO to return false, so we raise an error in that case.
*/
- if (!StartLocalBufferIO(bufHdr, false))+ if (!StartLocalBufferIO(bufHdr, false, false))
elog(ERROR, "failed to start write IO on local buffer");
/* Find smgr relation for buffer */
@@ -211,7 +213,7 @@ FlushLocalBuffer(BufferDesc *bufHdr, SMgrRelation reln)
IOOP_WRITE, io_start, 1, BLCKSZ);
/* Mark not-dirty */
- TerminateLocalBufferIO(bufHdr, true, 0);+ TerminateLocalBufferIO(bufHdr, true, 0, true);
pgBufferUsage.local_blks_written++;
}
@@ -430,7 +432,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr,
pg_atomic_unlocked_write_u32(&existing_hdr->state, buf_state);
/* no need to loop for local buffers */
- StartLocalBufferIO(existing_hdr, true);+ StartLocalBufferIO(existing_hdr, true, false);
}
else
{
@@ -446,7 +448,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr,
hresult->id = victim_buf_id;
- StartLocalBufferIO(victim_buf_hdr, true);+ StartLocalBufferIO(victim_buf_hdr, true, false);
}
}
@@ -515,13 +517,31 @@ MarkLocalBufferDirty(Buffer buffer)
* Like StartBufferIO, but for local buffers
*/
bool
-StartLocalBufferIO(BufferDesc *bufHdr, bool forInput)+StartLocalBufferIO(BufferDesc *bufHdr, bool forInput, bool nowait)
{
- uint32 buf_state = pg_atomic_read_u32(&bufHdr->state);+ uint32 buf_state;+ /*+ * With AIO the buffer could have IO in progress, e.g. when there are two+ * scans of the same relation. Either wait for the other IO or return+ * false.+ */+ if (pgaio_wref_valid(&bufHdr->io_wref))+ {+ PgAioWaitRef iow = bufHdr->io_wref;++ if (nowait)+ return false;++ pgaio_wref_wait(&iow);+ }++ /* Once we get here, there is definitely no I/O active on this buffer */++ /* Check if someone else already did the I/O */+ buf_state = pg_atomic_read_u32(&bufHdr->state);
if (forInput ? (buf_state & BM_VALID) : !(buf_state & BM_DIRTY))
{
- /* someone else already did the I/O */
return false;
}
@@ -536,7 +556,8 @@ StartLocalBufferIO(BufferDesc *bufHdr, bool forInput)
* Like TerminateBufferIO, but for local buffers
*/
void
-TerminateLocalBufferIO(BufferDesc *bufHdr, bool clear_dirty, uint32 set_flag_bits)+TerminateLocalBufferIO(BufferDesc *bufHdr, bool clear_dirty, uint32 set_flag_bits,+ bool syncio)
{
/* Only need to adjust flags */
uint32 buf_state = pg_atomic_read_u32(&bufHdr->state);
@@ -549,6 +570,14 @@ TerminateLocalBufferIO(BufferDesc *bufHdr, bool clear_dirty, uint32 set_flag_bit
if (clear_dirty)
buf_state &= ~BM_DIRTY;
+ if (!syncio)+ {+ /* release pin held by IO subsystem, see also buffer_stage_common() */+ Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0);+ buf_state -= BUF_REFCOUNT_ONE;+ pgaio_wref_clear(&bufHdr->io_wref);+ }+
buf_state |= set_flag_bits;
pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
@@ -714,6 +743,8 @@ InitLocalBuffers(void)
*/
buf->buf_id = -i - 2;
+ pgaio_wref_clear(&buf->io_wref);+
/*
* Intentionally do not initialize the buffer's atomic variable
* (besides zeroing the underlying memory above). That way we get
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0010-Support-buffer-forwarding-in-read_stream.c.patch (8.7K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/11-v2.11-0010-Support-buffer-forwarding-in-read_stream.c.patch)
download | inline diff:
From 39ac7acb4944758ae9c1c243e9da09f99dafade0 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 10/27] Support buffer forwarding in read_stream.c.
In preparation for a following change to the buffer manager, teach
streams to keep track of buffers that were "forwarded" from one call to
StartReadBuffers() to the next.
Since StartReadBuffers() buffers argument will become an in/out
argument, we need to initialize the buffer queue entries with
InvalidBuffer. We don't want to do that up front, because we try to
keep stream initialization cheap for code that uses the fast path and
stays in one single buffer queue element. Satisfy both goals by
initializing the queue incrementally on the first cycle.
Discussion: https://postgr.es/m/CA%2BhUKGK_%3D4CVmMHvsHjOVrK6t4F%3DLBpFzsrr3R%2BaJYN8kcTfWg%40mail.gmail.com
---
src/backend/storage/aio/read_stream.c | 103 +++++++++++++++++++++++---
1 file changed, 93 insertions(+), 10 deletions(-)
diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.cindex 45bdf819d57..18ecf4affc7 100644--- a/src/backend/storage/aio/read_stream.c+++ b/src/backend/storage/aio/read_stream.c@@ -95,8 +95,10 @@ struct ReadStream
int16 ios_in_progress;
int16 queue_size;
int16 max_pinned_buffers;
+ int16 forwarded_buffers;
int16 pinned_buffers;
int16 distance;
+ int16 initialized_buffers;
bool advice_enabled;
bool temporary;
@@ -224,8 +226,10 @@ static bool
read_stream_start_pending_read(ReadStream *stream)
{
bool need_wait;
+ int requested_nblocks;
int nblocks;
int flags;
+ int forwarded;
int16 io_index;
int16 overflow;
int16 buffer_index;
@@ -272,11 +276,20 @@ read_stream_start_pending_read(ReadStream *stream)
}
}
- /* How many more buffers is this backend allowed? */+ /*+ * How many more buffers is this backend allowed?+ *+ * If we already have some forwarded buffers, we can certainly use those.+ * They are already pinned, and are mapped to the starting blocks of the+ * pending read, they just don't have any I/O started yet and are not+ * counted in stream->pinned_buffers.+ */
if (stream->temporary)
buffer_limit = Min(GetAdditionalLocalPinLimit(), PG_INT16_MAX);
else
buffer_limit = Min(GetAdditionalPinLimit(), PG_INT16_MAX);
+ Assert(stream->forwarded_buffers <= stream->pending_read_nblocks);+ buffer_limit += stream->forwarded_buffers;
if (buffer_limit == 0 && stream->pinned_buffers == 0)
buffer_limit = 1; /* guarantee progress */
@@ -303,8 +316,31 @@ read_stream_start_pending_read(ReadStream *stream)
* We say how many blocks we want to read, but it may be smaller on return
* if the buffer manager decides to shorten the read.
*/
+ requested_nblocks = Min(buffer_limit, stream->pending_read_nblocks);+ nblocks = requested_nblocks;
buffer_index = stream->next_buffer_index;
io_index = stream->next_io_index;
++ /*+ * The first time around the queue we initialize it as we go, including+ * the overflow zone, because otherwise the entries would appear as+ * forwarded buffers. This avoids initializing the whole queue up front+ * in cases where it is large but we don't ever use it due to the+ * all-cached fast path or small scans.+ */+ while (stream->initialized_buffers < buffer_index + nblocks)+ stream->buffers[stream->initialized_buffers++] = InvalidBuffer;++ /*+ * Start the I/O. Any buffers that are not InvalidBuffer will be+ * interpreted as already pinned, forwarded by an earlier call to+ * StartReadBuffers(), and must map to the expected blocks. The nblocks+ * value may be smaller on return indicating the size of the I/O that+ * could be started. Buffers beyond the output nblocks number may also+ * have been pinned without starting I/O due to various edge cases. In+ * that case we'll just leave them in the queue ahead of us, "forwarded"+ * to the next call, avoiding the need to unpin/repin.+ */
need_wait = StartReadBuffers(&stream->ios[io_index].op,
&stream->buffers[buffer_index],
stream->pending_read_blocknum,
@@ -333,16 +369,35 @@ read_stream_start_pending_read(ReadStream *stream)
stream->seq_blocknum = stream->pending_read_blocknum + nblocks;
}
+ /*+ * How many pins were acquired but forwarded to the next call? These need+ * to be passed to the next StartReadBuffers() call, or released if the+ * stream ends early. We need the number for accounting purposes, since+ * they are not counted in stream->pinned_buffers but we already hold+ * them.+ */+ forwarded = 0;+ while (nblocks + forwarded < requested_nblocks &&+ stream->buffers[buffer_index + nblocks + forwarded] != InvalidBuffer)+ forwarded++;+ stream->forwarded_buffers = forwarded;+
/*
* We gave a contiguous range of buffer space to StartReadBuffers(), but
- * we want it to wrap around at queue_size. Slide overflowing buffers to- * the front of the array.+ * we want it to wrap around at queue_size. Copy overflowing buffers to+ * the front of the array where they'll be consumed, but also leave a copy+ * in the overflow zone which the I/O operation has a pointer to (it needs+ * a contiguous array). Both copies will be cleared when the buffers are+ * handed to the consumer.
*/
- overflow = (buffer_index + nblocks) - stream->queue_size;+ overflow = (buffer_index + nblocks + forwarded) - stream->queue_size;
if (overflow > 0)
- memmove(&stream->buffers[0],- &stream->buffers[stream->queue_size],- sizeof(stream->buffers[0]) * overflow);+ {+ Assert(overflow < stream->queue_size); /* can't overlap */+ memcpy(&stream->buffers[0],+ &stream->buffers[stream->queue_size],+ sizeof(stream->buffers[0]) * overflow);+ }
/* Compute location of start of next read, without using % operator. */
buffer_index += nblocks;
@@ -719,10 +774,12 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data)
/* Fast path assumptions. */
Assert(stream->ios_in_progress == 0);
+ Assert(stream->forwarded_buffers == 0);
Assert(stream->pinned_buffers == 1);
Assert(stream->distance == 1);
Assert(stream->pending_read_nblocks == 0);
Assert(stream->per_buffer_data_size == 0);
+ Assert(stream->initialized_buffers > stream->oldest_buffer_index);
/* We're going to return the buffer we pinned last time. */
oldest_buffer_index = stream->oldest_buffer_index;
@@ -771,6 +828,7 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data)
stream->distance = 0;
stream->oldest_buffer_index = stream->next_buffer_index;
stream->pinned_buffers = 0;
+ stream->buffers[oldest_buffer_index] = InvalidBuffer;
}
stream->fast_path = false;
@@ -846,10 +904,15 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data)
stream->seq_until_processed = InvalidBlockNumber;
}
-#ifdef CLOBBER_FREED_MEMORY- /* Clobber old buffer for debugging purposes. */+ /*+ * We must zap this queue entry, or else it would appear as a forwarded+ * buffer. If it's potentially in the overflow zone (ie it wrapped around+ * the queue), also zap that copy.+ */
stream->buffers[oldest_buffer_index] = InvalidBuffer;
-#endif+ if (oldest_buffer_index < io_combine_limit - 1)+ stream->buffers[stream->queue_size + oldest_buffer_index] =+ InvalidBuffer;
#if defined(CLOBBER_FREED_MEMORY) || defined(USE_VALGRIND)
@@ -894,6 +957,7 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data)
#ifndef READ_STREAM_DISABLE_FAST_PATH
/* See if we can take the fast path for all-cached scans next time. */
if (stream->ios_in_progress == 0 &&
+ stream->forwarded_buffers == 0 &&
stream->pinned_buffers == 1 &&
stream->distance == 1 &&
stream->pending_read_nblocks == 0 &&
@@ -929,6 +993,7 @@ read_stream_next_block(ReadStream *stream, BufferAccessStrategy *strategy)
void
read_stream_reset(ReadStream *stream)
{
+ int16 index;
Buffer buffer;
/* Stop looking ahead. */
@@ -942,6 +1007,24 @@ read_stream_reset(ReadStream *stream)
while ((buffer = read_stream_next_buffer(stream, NULL)) != InvalidBuffer)
ReleaseBuffer(buffer);
+ /* Unpin any unused forwarded buffers. */+ index = stream->next_buffer_index;+ while (index < stream->initialized_buffers &&+ (buffer = stream->buffers[index]) != InvalidBuffer)+ {+ Assert(stream->forwarded_buffers > 0);+ stream->forwarded_buffers--;+ ReleaseBuffer(buffer);++ stream->buffers[index] = InvalidBuffer;+ if (index < io_combine_limit - 1)+ stream->buffers[stream->queue_size + index] = InvalidBuffer;++ if (++index == stream->queue_size)+ index = 0;+ }++ Assert(stream->forwarded_buffers == 0);
Assert(stream->pinned_buffers == 0);
Assert(stream->ios_in_progress == 0);
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0011-Support-buffer-forwarding-in-StartReadBuffers.patch (10.5K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/12-v2.11-0011-Support-buffer-forwarding-in-StartReadBuffers.patch)
download | inline diff:
From eef02887f094b1fc3fdfe2ec4343f889ec592776 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 11/27] Support buffer forwarding in StartReadBuffers().
Sometimes we have to perform a short read because we hit a cached block
that ends a contiguous run of blocks requiring I/O. We don't want
StartReadBuffers() to have to start more than one I/O, so we stop there.
We also don't want to have to unpin the cached block (and repin it
later), so previously we'd silently pretend the hit was part of the I/O,
and just leave it out of the read from disk. Now, we'll "forward" it to
the next call. We still write it to the buffers[] array for the caller
to pass back to us later, but it's not included in *nblocks.
This policy means that we no longer mix hits and misses in a single
operation's results, so we avoid the requirement to call
WaitReadBuffers(), which might stall, before the caller can make use of
the hits. The caller will get the hit in the next call instead, and
know that it doesn't have to wait. That's important for later work on
out-of-order read streams that minimize I/O stalls.
This also makes life easier for proposed work on true AIO, which
occasionally needs to split a large I/O after pinning all the buffers,
while the current coding only ever forwards a single bookending hit.
This API is natural for read_stream.c: it just leaves forwarded buffers
where they are in its circular queue, where the next call will pick them
up and continue, minimizing pin churn.
If we ever think of a good reason to disable this feature, i.e. for
other users of StartReadBuffers() that don't want to deal with forwarded
buffers, then we could add a flag for that. For now read_steam.c is the
only user.
Discussion: https://postgr.es/m/CA%2BhUKGK_%3D4CVmMHvsHjOVrK6t4F%3DLBpFzsrr3R%2BaJYN8kcTfWg%40mail.gmail.com
---
src/include/storage/bufmgr.h | 1 -
src/backend/storage/buffer/bufmgr.c | 128 ++++++++++++++++++++--------
2 files changed, 91 insertions(+), 38 deletions(-)
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.hindex b8c28fe79f4..5137288c96e 100644--- a/src/include/storage/bufmgr.h+++ b/src/include/storage/bufmgr.h@@ -131,7 +131,6 @@ struct ReadBuffersOperation
BlockNumber blocknum;
int flags;
int16 nblocks;
- int16 io_buffers_len;
};
typedef struct ReadBuffersOperation ReadBuffersOperation;
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.cindex ea5f504e38a..1a5037dd913 100644--- a/src/backend/storage/buffer/bufmgr.c+++ b/src/backend/storage/buffer/bufmgr.c@@ -1254,10 +1254,10 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
Buffer *buffers,
BlockNumber blockNum,
int *nblocks,
- int flags)+ int flags,+ bool allow_forwarding)
{
int actual_nblocks = *nblocks;
- int io_buffers_len = 0;
int maxcombine = 0;
Assert(*nblocks > 0);
@@ -1267,30 +1267,80 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
{
bool found;
- buffers[i] = PinBufferForBlock(operation->rel,- operation->smgr,- operation->persistence,- operation->forknum,- blockNum + i,- operation->strategy,- &found);+ if (allow_forwarding && buffers[i] != InvalidBuffer)+ {+ BufferDesc *bufHdr;++ /*+ * This is a buffer that was pinned by an earlier call to+ * StartReadBuffers(), but couldn't be handled in one operation at+ * that time. The operation was split, and the caller has passed+ * an already pinned buffer back to us to handle the rest of the+ * operation. It must continue at the expected block number.+ */+ Assert(BufferGetBlockNumber(buffers[i]) == blockNum + i);++ /*+ * It might be an already valid buffer (a hit) that followed the+ * final contiguous block of an earlier I/O (a miss) marking the+ * end of it, or a buffer that some other backend has since made+ * valid by performing the I/O for us, in which case we can handle+ * it as a hit now. It is safe to check for a BM_VALID flag with+ * a relaxed load, because we got a fresh view of it while pinning+ * it in the previous call.+ *+ * On the other hand if we don't see BM_VALID yet, it must be an+ * I/O that was split by the previous call and we need to try to+ * start a new I/O from this block. We're also racing against any+ * other backend that might start the I/O or even manage to mark+ * it BM_VALID after this check, but StartBufferIO() will handle+ * those cases.+ */+ if (BufferIsLocal(buffers[i]))+ bufHdr = GetLocalBufferDescriptor(-buffers[i] - 1);+ else+ bufHdr = GetBufferDescriptor(buffers[i] - 1);+ found = pg_atomic_read_u32(&bufHdr->state) & BM_VALID;+ }+ else+ {+ buffers[i] = PinBufferForBlock(operation->rel,+ operation->smgr,+ operation->persistence,+ operation->forknum,+ blockNum + i,+ operation->strategy,+ &found);+ }
if (found)
{
/*
- * Terminate the read as soon as we get a hit. It could be a- * single buffer hit, or it could be a hit that follows a readable- * range. We don't want to create more than one readable range,- * so we stop here.+ * We have a hit. If it's the first block in the requested range,+ * we can return it immediately and report that WaitReadBuffers()+ * does not need to be called. If the initial value of *nblocks+ * was larger, the caller will have to call again for the rest.
*/
- actual_nblocks = i + 1;+ if (i == 0)+ {+ *nblocks = 1;+ return false;+ }++ /*+ * Otherwise we already have an I/O to perform, but this block+ * can't be included as it is already valid. Split the I/O here.+ * There may or may not be more blocks requiring I/O after this+ * one, we haven't checked, but it can't be contiguous with this+ * hit in the way. We'll leave this buffer pinned, forwarding it+ * to the next call, avoiding the need to unpin it here and re-pin+ * it in the next call.+ */+ actual_nblocks = i;
break;
}
else
{
- /* Extend the readable range to cover this block. */- io_buffers_len++;-
/*
* Check how many blocks we can cover with the same IO. The smgr
* implementation might e.g. be limited due to a segment boundary.
@@ -1311,15 +1361,11 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
}
*nblocks = actual_nblocks;
- if (likely(io_buffers_len == 0))- return false;-
/* Populate information needed for I/O. */
operation->buffers = buffers;
operation->blocknum = blockNum;
operation->flags = flags;
operation->nblocks = actual_nblocks;
- operation->io_buffers_len = io_buffers_len;
if (flags & READ_BUFFERS_ISSUE_ADVICE)
{
@@ -1334,7 +1380,7 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
smgrprefetch(operation->smgr,
operation->forknum,
blockNum,
- operation->io_buffers_len);+ actual_nblocks);
}
/* Indicate that WaitReadBuffers() should be called. */
@@ -1348,11 +1394,21 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
* actual number, which may be fewer than requested. Caller sets some of the
* members of operation; see struct definition.
*
+ * The initial contents of the elements of buffers up to *nblocks should+ * either be InvalidBuffer or an already-pinned buffer that was left by an+ * preceding call to StartReadBuffers() that had to be split. On return, some+ * elements of buffers may hold pinned buffers beyond the number indicated by+ * the updated value of *nblocks. Operations are split on boundaries known to+ * smgr (eg md.c segment boundaries that require crossing into a different+ * underlying file), or when already cached blocks are found in the buffer+ * that prevent the formation of a contiguous read.+ *
* If false is returned, no I/O is necessary. If true is returned, one I/O
* has been started, and WaitReadBuffers() must be called with the same
* operation object before the buffers are accessed. Along with the operation
* object, the caller-supplied array of buffers must remain valid until
- * WaitReadBuffers() is called.+ * WaitReadBuffers() is called, and any forwarded buffers must also be+ * preserved for a future call unless explicitly released.
*
* Currently the I/O is only started with optional operating system advice if
* requested by the caller with READ_BUFFERS_ISSUE_ADVICE, and the real I/O
@@ -1366,13 +1422,18 @@ StartReadBuffers(ReadBuffersOperation *operation,
int *nblocks,
int flags)
{
- return StartReadBuffersImpl(operation, buffers, blockNum, nblocks, flags);+ return StartReadBuffersImpl(operation, buffers, blockNum, nblocks, flags,+ true /* expect forwarded buffers */ );
}
/*
* Single block version of the StartReadBuffers(). This might save a few
* instructions when called from another translation unit, because it is
* specialized for nblocks == 1.
+ *+ * This version does not support "forwarded" buffers: they cannot be created+ * by reading only one block, and the current contents of *buffer is ignored+ * on entry.
*/
bool
StartReadBuffer(ReadBuffersOperation *operation,
@@ -1383,7 +1444,8 @@ StartReadBuffer(ReadBuffersOperation *operation,
int nblocks = 1;
bool result;
- result = StartReadBuffersImpl(operation, buffer, blocknum, &nblocks, flags);+ result = StartReadBuffersImpl(operation, buffer, blocknum, &nblocks, flags,+ false /* single block, no forwarding */ );
Assert(nblocks == 1); /* single block can't be short */
return result;
@@ -1410,24 +1472,16 @@ WaitReadBuffers(ReadBuffersOperation *operation)
IOObject io_object;
char persistence;
- /*- * Currently operations are only allowed to include a read of some range,- * with an optional extra buffer that is already pinned at the end. So- * nblocks can be at most one more than io_buffers_len.- */- Assert((operation->nblocks == operation->io_buffers_len) ||- (operation->nblocks == operation->io_buffers_len + 1));-
/* Find the range of the physical read we need to perform. */
- nblocks = operation->io_buffers_len;- if (nblocks == 0)- return; /* nothing to do */-+ nblocks = operation->nblocks;
buffers = &operation->buffers[0];
blocknum = operation->blocknum;
forknum = operation->forknum;
persistence = operation->persistence;
+ Assert(nblocks > 0);+ Assert(nblocks <= MAX_IO_COMBINE_LIMIT);+
if (persistence == RELPERSISTENCE_TEMP)
{
io_context = IOCONTEXT_NORMAL;
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0012-bufmgr-Use-AIO-in-StartReadBuffers.patch (26.8K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/13-v2.11-0012-bufmgr-Use-AIO-in-StartReadBuffers.patch)
download | inline diff:
From de33e6dd831449c50e571a61be39ce0fa9a72f45 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 12/27] bufmgr: Use AIO in StartReadBuffers()
This finally introduces the first actual use of AIO. StartReadBuffers() now
uses the AIO routines to issue IO. This converts a lot of callers to use the
AIO infrastructure.
As the implementation of StartReadBuffers() is also used by the functions for
reading individual blocks (StartReadBuffer() and through that
ReadBufferExtended()) this means all buffered read IO passes through the AIO
paths. However, as those are synchronous reads, actually performing the IO
asynchronously would be rarely beneficial. Instead such IOs are flagged to
always be executed synchronously. This way we don't have to duplicate a fair
bit of code.
When io_method=sync is used, the IO patterns generated after this change are
the same as before, i.e. actual reads are only issued in WaitReadBuffers() and
StartReadBuffers() may issue prefetch requests. This allows to bypass most of
the actual asynchronicity, which is important to make a change as big as this
less risky.
One thing worth calling out is that, if IO is actually executed
asynchronously, the precise meaning of what track_io_timing is measuring has
changed. Previously it tracked the time for each IO, but that does not make
sense when multiple IOs are executed concurrently. Now it only measures the
time actually spent waiting for IO. A subsequent commit will adjust the docs
for this.
While AIO is now actually used, the logic in read_stream.c will often prevent
using sufficiently many concurrent IOs. That will be addressed in the next
commit.
Reviewed-by: Noah Misch <noah@leadboat.com>
Reviewed-by: Nazir Bilal Yavuz <byavuz81@gmail.com>
Co-authored-by: Andres Freund <andres@anarazel.de>
Co-authored-by: Thomas Munro <thomas.munro@gmail.com>
Discussion: https://postgr.es/m/uvrtrknj4kdytuboidbhwclo4gxhswwcpgadptsjvjqcluzmah%40brqs62irg4dt
Discussion: https://postgr.es/m/20210223100344.llw5an2aklengrmn@alap3.anarazel.de
Discussion: https://postgr.es/m/stj36ea6yyhoxtqkhpieia2z4krnam7qyetc57rfezgk4zgapf@gcnactj4z56m
---
src/include/storage/bufmgr.h | 6 +
src/backend/storage/buffer/bufmgr.c | 608 +++++++++++++++++++++-------
2 files changed, 474 insertions(+), 140 deletions(-)
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.hindex 5137288c96e..7abb3bdfd9d 100644--- a/src/include/storage/bufmgr.h+++ b/src/include/storage/bufmgr.h@@ -112,6 +112,9 @@ typedef struct BufferManagerRelation
#define READ_BUFFERS_ZERO_ON_ERROR (1 << 0)
/* Call smgrprefetch() if I/O necessary. */
#define READ_BUFFERS_ISSUE_ADVICE (1 << 1)
+/* IO will immediately be waited for */+#define READ_BUFFERS_SYNCHRONOUSLY (1 << 2)+
struct ReadBuffersOperation
{
@@ -131,6 +134,9 @@ struct ReadBuffersOperation
BlockNumber blocknum;
int flags;
int16 nblocks;
+ int16 nblocks_done;+ PgAioWaitRef io_wref;+ PgAioReturn io_return;
};
typedef struct ReadBuffersOperation ReadBuffersOperation;
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.cindex 1a5037dd913..08d25e70bf2 100644--- a/src/backend/storage/buffer/bufmgr.c+++ b/src/backend/storage/buffer/bufmgr.c@@ -531,6 +531,8 @@ static inline BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr, IOContext io_context);
+static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks);+static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete);
static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context);
static void FlushBuffer(BufferDesc *buf, SMgrRelation reln,
IOObject io_object, IOContext io_context);
@@ -1231,10 +1233,14 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
return buffer;
}
+ /*+ * Signal that we are going to immediately wait. If we're immediately+ * waiting, there is no benefit in actually executing the IO+ * asynchronously, it would just add dispatch overhead.+ */+ flags = READ_BUFFERS_SYNCHRONOUSLY;
if (mode == RBM_ZERO_ON_ERROR)
- flags = READ_BUFFERS_ZERO_ON_ERROR;- else- flags = 0;+ flags |= READ_BUFFERS_ZERO_ON_ERROR;
operation.smgr = smgr;
operation.rel = rel;
operation.persistence = persistence;
@@ -1259,9 +1265,11 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
{
int actual_nblocks = *nblocks;
int maxcombine = 0;
+ bool did_start_io;
Assert(*nblocks > 0);
Assert(*nblocks <= MAX_IO_COMBINE_LIMIT);
+ Assert(*nblocks == 1 || allow_forwarding);
for (int i = 0; i < actual_nblocks; ++i)
{
@@ -1300,6 +1308,7 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
bufHdr = GetLocalBufferDescriptor(-buffers[i] - 1);
else
bufHdr = GetBufferDescriptor(buffers[i] - 1);
+ Assert(pg_atomic_read_u32(&bufHdr->state) & BM_TAG_VALID);
found = pg_atomic_read_u32(&bufHdr->state) & BM_VALID;
}
else
@@ -1324,6 +1333,20 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
if (i == 0)
{
*nblocks = 1;
++#ifdef USE_ASSERT_CHECKING++ /*+ * Initialize enough of ReadBuffersOperation to make+ * CheckReadBuffersOperation() work. Outside of assertions+ * that's not necessary when no IO is issued.+ */+ operation->buffers = buffers;+ operation->blocknum = blockNum;+ operation->nblocks = 1;+ operation->nblocks_done = 1;+ CheckReadBuffersOperation(operation, true);+#endif
return false;
}
@@ -1366,25 +1389,74 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
operation->blocknum = blockNum;
operation->flags = flags;
operation->nblocks = actual_nblocks;
+ operation->nblocks_done = 0;+ pgaio_wref_clear(&operation->io_wref);- if (flags & READ_BUFFERS_ISSUE_ADVICE)+ /*+ * When using AIO, start the IO in the background. If not, issue prefetch+ * requests if desired by the caller.+ *+ * The reason we have a dedicated path for IOMETHOD_SYNC here is to+ * de-risk the introduction of AIO somewhat. It's a large architectural+ * change, with lots of chances for unanticipated performance effects.+ *+ * Use of IOMETHOD_SYNC already leads to not actually performing IO+ * asynchronously, but without the check here we'd execute IO earlier than+ * we used to. Eventually this IOMETHOD_SYNC specific path should go away.+ */+ if (io_method != IOMETHOD_SYNC)
{
/*
- * In theory we should only do this if PinBufferForBlock() had to- * allocate new buffers above. That way, if two calls to- * StartReadBuffers() were made for the same blocks before- * WaitReadBuffers(), only the first would issue the advice. That'd be- * a better simulation of true asynchronous I/O, which would only- * start the I/O once, but isn't done here for simplicity.+ * Try to start IO asynchronously. It's possible that no IO needs to+ * be started, if another backend already performed the IO.+ *+ * Note that if an IO is started, it might not cover the entire+ * requested range, e.g. because an intermediary block has been read+ * in by another backend. In that case any "trailing" buffers we+ * already pinned above will be "forwarded" by read_stream.c to the+ * next call to StartReadBuffers().+ *+ * This is signalled to the caller by decrementing *nblocks *and*+ * reducing operation->nblocks. The latter is done here, but not below+ * WaitReadBuffers(), as in WaitReadBuffers() we can't "shorten" the+ * overall read size anymore, we need to retry until done in its+ * entirety or until failed.
*/
- smgrprefetch(operation->smgr,- operation->forknum,- blockNum,- actual_nblocks);+ did_start_io = AsyncReadBuffers(operation, nblocks);++ operation->nblocks = *nblocks;+ }+ else+ {+ operation->flags |= READ_BUFFERS_SYNCHRONOUSLY;++ if (flags & READ_BUFFERS_ISSUE_ADVICE)+ {+ /*+ * In theory we should only do this if PinBufferForBlock() had to+ * allocate new buffers above. That way, if two calls to+ * StartReadBuffers() were made for the same blocks before+ * WaitReadBuffers(), only the first would issue the advice.+ * That'd be a better simulation of true asynchronous I/O, which+ * would only start the I/O once, but isn't done here for+ * simplicity.+ */+ smgrprefetch(operation->smgr,+ operation->forknum,+ blockNum,+ actual_nblocks);+ }++ /*+ * Indicate that WaitReadBuffers() should be called. WaitReadBuffers()+ * will initiate the necessary IO.+ */+ did_start_io = true;
}
- /* Indicate that WaitReadBuffers() should be called. */- return true;+ CheckReadBuffersOperation(operation, !did_start_io);++ return did_start_io;
}
/*
@@ -1451,8 +1523,35 @@ StartReadBuffer(ReadBuffersOperation *operation,
return result;
}
+/*+ * Perform sanity checks on the ReadBuffersOperation.+ */+static void+CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete)+{+#ifdef USE_ASSERT_CHECKING+ Assert(operation->nblocks_done <= operation->nblocks);+ Assert(!is_complete || operation->nblocks == operation->nblocks_done);++ for (int i = 0; i < operation->nblocks; i++)+ {+ Buffer buffer = operation->buffers[i];+ BufferDesc *buf_hdr = BufferIsLocal(buffer) ?+ GetLocalBufferDescriptor(-buffer - 1) :+ GetBufferDescriptor(buffer - 1);++ Assert(BufferGetBlockNumber(buffer) == operation->blocknum + i);+ Assert(pg_atomic_read_u32(&buf_hdr->state) & BM_TAG_VALID);++ if (i < operation->nblocks_done)+ Assert(pg_atomic_read_u32(&buf_hdr->state) & BM_VALID);+ }+#endif+}++/* helper for ReadBuffersCanStartIO(), to avoid repetition */
static inline bool
-WaitReadBuffersCanStartIO(Buffer buffer, bool nowait)+ReadBuffersCanStartIOOnce(Buffer buffer, bool nowait)
{
if (BufferIsLocal(buffer))
return StartLocalBufferIO(GetLocalBufferDescriptor(-buffer - 1),
@@ -1461,31 +1560,243 @@ WaitReadBuffersCanStartIO(Buffer buffer, bool nowait)
return StartBufferIO(GetBufferDescriptor(buffer - 1), true, nowait);
}
-void-WaitReadBuffers(ReadBuffersOperation *operation)+/*+ * Helper for AsyncReadBuffers that tries to get the buffer ready for IO.+ */+static inline bool+ReadBuffersCanStartIO(Buffer buffer, bool nowait)
{
- Buffer *buffers;- int nblocks;- BlockNumber blocknum;- ForkNumber forknum;- IOContext io_context;- IOObject io_object;- char persistence;-- /* Find the range of the physical read we need to perform. */- nblocks = operation->nblocks;- buffers = &operation->buffers[0];- blocknum = operation->blocknum;- forknum = operation->forknum;- persistence = operation->persistence;+ /*+ * If this backend currently has staged IO, we need to submit the pending+ * IO before waiting for the right to issue IO, to avoid the potential for+ * deadlocks (and, more commonly, unnecessary delays for other backends).+ */+ if (!nowait && pgaio_have_staged())+ {+ if (ReadBuffersCanStartIOOnce(buffer, true))+ return true;++ /*+ * Unfortunately StartBufferIO() returning false doesn't allow to+ * distinguish between the buffer already being valid and IO already+ * being in progress. Since IO already being in progress is quite+ * rare, this approach seems fine.+ */+ pgaio_submit_staged();+ }++ return ReadBuffersCanStartIOOnce(buffer, nowait);+}++/*+ * Helper for WaitReadBuffers() that processes the results of a readv+ * operation, raising an error if necessary.+ */+static void+ProcessReadBuffersResult(ReadBuffersOperation *operation)+{+ PgAioReturn *aio_ret = &operation->io_return;+ int nblocks = 0;++ Assert(pgaio_wref_valid(&operation->io_wref));+ Assert(aio_ret->result.status != PGAIO_RS_UNKNOWN);++ /*+ * SMGR reports the number of blocks successfully read as the result of+ * the IO operation. Thus we can simply add that to ->nblocks_done.+ */++ if (likely(aio_ret->result.status == PGAIO_RS_OK))+ nblocks = aio_ret->result.result;+ else if (aio_ret->result.status == PGAIO_RS_PARTIAL)+ {+ /*+ * We'll retry, so we just emit a debug message to the server log (or+ * not even that in prod scenarios).+ */+ pgaio_result_report(aio_ret->result, &aio_ret->target_data, DEBUG1);+ nblocks = aio_ret->result.result;++ elog(DEBUG3, "partial read, will retry");++ }+ else if (aio_ret->result.status == PGAIO_RS_ERROR)+ {+ pgaio_result_report(aio_ret->result, &aio_ret->target_data, ERROR);+ nblocks = 0; /* silence compiler */+ }
Assert(nblocks > 0);
Assert(nblocks <= MAX_IO_COMBINE_LIMIT);
+ operation->nblocks_done += nblocks;++ Assert(operation->nblocks_done <= operation->nblocks);+}++void+WaitReadBuffers(ReadBuffersOperation *operation)+{+ PgAioReturn *aio_ret = &operation->io_return;+ IOContext io_context;+ IOObject io_object;++ if (operation->persistence == RELPERSISTENCE_TEMP)+ {+ io_context = IOCONTEXT_NORMAL;+ io_object = IOOBJECT_TEMP_RELATION;+ }+ else+ {+ io_context = IOContextForStrategy(operation->strategy);+ io_object = IOOBJECT_RELATION;+ }++ /*+ * If we get here without an IO operation having been issued, the+ * io_method == IOMETHOD_SYNC path must have been used. Otherwise the+ * caller should not have called WaitReadBuffers().+ *+ * In the case of IOMETHOD_SYNC, we start - as we used to before the+ * introducing of AIO - the IO in WaitReadBuffers(). This is done as part+ * of the retry logic below, no extra code is required.+ *+ * This path is expected to eventually go away.+ */+ if (!pgaio_wref_valid(&operation->io_wref) && io_method != IOMETHOD_SYNC)+ elog(ERROR, "waiting for read operation that didn't read");++ /*+ * To handle partial reads, and IOMETHOD_SYNC, we re-issue IO until we're+ * done. We may need multiple retries, not just because we could get+ * multiple partial reads, but also because some of the remaining+ * to-be-read buffers may have been read in by other backends, limiting+ * the IO size.+ */+ while (true)+ {+ int nblocks;++ CheckReadBuffersOperation(operation, false);++ /*+ * If there is an IO associated with the operation, we may need to+ * wait for it.+ */+ if (pgaio_wref_valid(&operation->io_wref))+ {+ /*+ * Track the time spent waiting for the IO to complete. As+ * tracking a wait even if we don't actually need to wait+ *+ * a) is not cheap, due to the timestamping overhead+ *+ * b) reports some time as waiting, even if we never waited+ *+ * we first check if we already know the IO is complete.+ */+ if (aio_ret->result.status == PGAIO_RS_UNKNOWN &&+ !pgaio_wref_check_done(&operation->io_wref))+ {+ instr_time io_start = pgstat_prepare_io_time(track_io_timing);++ pgaio_wref_wait(&operation->io_wref);++ /*+ * The IO operation itself was already counted earlier, in+ * AsyncReadBuffers(), this just accounts for the wait time.+ */+ pgstat_count_io_op_time(io_object, io_context, IOOP_READ,+ io_start, 0, 0);+ }+ else+ {+ Assert(pgaio_wref_check_done(&operation->io_wref));+ }++ /*+ * We now are sure the IO completed. Check the results. This+ * includes reporting on errors if there were any.+ */+ ProcessReadBuffersResult(operation);+ }++ /*+ * Most of the time, the one IO we already started, will read in+ * everything. But we need to deal with partial reads and buffers not+ * needing IO anymore.+ */+ if (operation->nblocks_done == operation->nblocks)+ break;++ CHECK_FOR_INTERRUPTS();++ /*+ * This may only complete the IO partially, either because some+ * buffers were already valid, or because of a partial read.+ *+ * NB: In contrast to after the AsyncReadBuffers() call in+ * StartReadBuffers(), we do *not* reduce+ * ReadBuffersOperation->nblocks here, callers expect the full+ * operation to be completed at this point (as more operations may+ * have been queued).+ */+ AsyncReadBuffers(operation, &nblocks);+ }++ CheckReadBuffersOperation(operation, true);++ /* NB: READ_DONE tracepoint was already executed in completion callback */+}++/*+ * Initiate IO for the ReadBuffersOperation+ *+ * This function only starts a single IO at a time. The size of the IO may be+ * limited to below the to-be-read blocks, if one of the buffers has+ * concurrently been read in. If the first to-be-read buffer is already valid,+ * no IO will be issued.+ *+ * To support retries after partial reads, the first operation->nblocks_done+ * buffers are skipped.+ *+ * On return *nblocks_progress is updated to reflect the number of buffers+ * affected by the call. If the first buffer is valid, *nblocks_progress is+ * set to 1 and operation->nblocks_done is incremented.+ *+ * Returns true if IO was initiated, false if no IO was necessary.+ */+static bool+AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)+{+ Buffer *buffers = &operation->buffers[0];+ int flags = operation->flags;+ BlockNumber blocknum = operation->blocknum;+ ForkNumber forknum = operation->forknum;+ char persistence = operation->persistence;+ int16 nblocks_done = operation->nblocks_done;+ Buffer *io_buffers = &operation->buffers[nblocks_done];+ int io_buffers_len = 0;+ PgAioHandle *ioh;+ uint32 ioh_flags = 0;+ void *io_pages[MAX_IO_COMBINE_LIMIT];+ IOContext io_context;+ IOObject io_object;+ bool did_start_io;++ /*+ * When this IO is executed synchronously, either because the caller will+ * immediately block waiting for the IO or because IOMETHOD_SYNC is used,+ * the AIO subsystem needs to know.+ */+ if (flags & READ_BUFFERS_SYNCHRONOUSLY)+ ioh_flags |= PGAIO_HF_SYNCHRONOUS;+
if (persistence == RELPERSISTENCE_TEMP)
{
io_context = IOCONTEXT_NORMAL;
io_object = IOOBJECT_TEMP_RELATION;
+ ioh_flags |= PGAIO_HF_REFERENCES_LOCAL;
}
else
{
@@ -1493,140 +1804,157 @@ WaitReadBuffers(ReadBuffersOperation *operation)
io_object = IOOBJECT_RELATION;
}
- for (int i = 0; i < nblocks; ++i)+ /*+ * Get IO handle before ReadBuffersCanStartIO(), as pgaio_io_acquire()+ * might block, which we don't want after setting IO_IN_PROGRESS.+ *+ * If we need to wait for IO before we can get a handle, submit already+ * staged IO first, so that other backends don't need to wait. There+ * wouldn't be a deadlock risk, as pgaio_io_acquire() just needs to wait+ * for already submitted IO, which doesn't require additional locks, but+ * it could still cause undesirable waits.+ *+ * A secondary benefit is that this would allow us to measure the time in+ * pgaio_io_acquire() without causing undue timer overhead in the common,+ * non-blocking, case. However, currently the pgstats infrastructure+ * doesn't really allow that, as it a) asserts that an operation can't+ * have time without operations b) doesn't have an API to report+ * "accumulated" time.+ */+ ioh = pgaio_io_acquire_nb(CurrentResourceOwner, &operation->io_return);+ if (unlikely(!ioh))+ {+ pgaio_submit_staged();++ ioh = pgaio_io_acquire(CurrentResourceOwner, &operation->io_return);+ }++ /*+ * Check if we can start IO on the first to-be-read buffer.+ *+ * If an I/O is already in progress in another backend, we want to wait+ * for the outcome: either done, or something went wrong and we will+ * retry.+ */+ if (!ReadBuffersCanStartIO(buffers[nblocks_done], false))+ {+ /*+ * Someone else has already completed this block, we're done.+ *+ * When IO is necessary, ->nblocks_done is updated in+ * ProcessReadBuffersResult(), but that is not called if no IO is+ * necessary. Thus update here.+ */+ operation->nblocks_done += 1;+ *nblocks_progress = 1;++ pgaio_io_release(ioh);+ pgaio_wref_clear(&operation->io_wref);+ did_start_io = false;++ /*+ * Report and track this as a 'hit' for this backend, even though it+ * must have started out as a miss in PinBufferForBlock(). The other+ * backend will track this as a 'read'.+ */+ TRACE_POSTGRESQL_BUFFER_READ_DONE(forknum, blocknum + operation->nblocks_done,+ operation->smgr->smgr_rlocator.locator.spcOid,+ operation->smgr->smgr_rlocator.locator.dbOid,+ operation->smgr->smgr_rlocator.locator.relNumber,+ operation->smgr->smgr_rlocator.backend,+ true);++ if (persistence == RELPERSISTENCE_TEMP)+ pgBufferUsage.local_blks_hit += 1;+ else+ pgBufferUsage.shared_blks_hit += 1;++ if (operation->rel)+ pgstat_count_buffer_hit(operation->rel);++ pgstat_count_io_op(io_object, io_context, IOOP_HIT, 1, 0);++ if (VacuumCostActive)+ VacuumCostBalance += VacuumCostPageHit;+ }+ else
{
- int io_buffers_len;- Buffer io_buffers[MAX_IO_COMBINE_LIMIT];- void *io_pages[MAX_IO_COMBINE_LIMIT];
instr_time io_start;
- BlockNumber io_first_block;-- /*- * Skip this block if someone else has already completed it. If an- * I/O is already in progress in another backend, this will wait for- * the outcome: either done, or something went wrong and we will- * retry.- */- if (!WaitReadBuffersCanStartIO(buffers[i], false))- {- /*- * Report and track this as a 'hit' for this backend, even though- * it must have started out as a miss in PinBufferForBlock(). The- * other backend will track this as a 'read'.- */- TRACE_POSTGRESQL_BUFFER_READ_DONE(forknum, blocknum + i,- operation->smgr->smgr_rlocator.locator.spcOid,- operation->smgr->smgr_rlocator.locator.dbOid,- operation->smgr->smgr_rlocator.locator.relNumber,- operation->smgr->smgr_rlocator.backend,- true);-- if (persistence == RELPERSISTENCE_TEMP)- pgBufferUsage.local_blks_hit += 1;- else- pgBufferUsage.shared_blks_hit += 1;-- if (operation->rel)- pgstat_count_buffer_hit(operation->rel);-- pgstat_count_io_op(io_object, io_context, IOOP_HIT, 1, 0);-- if (VacuumCostActive)- VacuumCostBalance += VacuumCostPageHit;-- continue;- }
/* We found a buffer that we need to read in. */
- io_buffers[0] = buffers[i];- io_pages[0] = BufferGetBlock(buffers[i]);- io_first_block = blocknum + i;+ Assert(io_buffers[0] == buffers[nblocks_done]);+ io_pages[0] = BufferGetBlock(buffers[nblocks_done]);
io_buffers_len = 1;
/*
* How many neighboring-on-disk blocks can we scatter-read into other
* buffers at the same time? In this case we don't wait if we see an
- * I/O already in progress. We already hold BM_IO_IN_PROGRESS for the+ * I/O already in progress. We already set BM_IO_IN_PROGRESS for the
* head block, so we should get on with that I/O as soon as possible.
- * We'll come back to this block again, above.+ *+ * We'll come back to this block in the next call to+ * StartReadBuffers() -> AsyncReadBuffers().
*/
- while ((i + 1) < nblocks &&- WaitReadBuffersCanStartIO(buffers[i + 1], true))+ for (int i = nblocks_done + 1; i < operation->nblocks; i++)
{
+ if (!ReadBuffersCanStartIO(buffers[i], true))+ break;
/* Must be consecutive block numbers. */
- Assert(BufferGetBlockNumber(buffers[i + 1]) ==- BufferGetBlockNumber(buffers[i]) + 1);+ Assert(BufferGetBlockNumber(buffers[i - 1]) ==+ BufferGetBlockNumber(buffers[i]) - 1);+ Assert(io_buffers[io_buffers_len] == buffers[i]);- io_buffers[io_buffers_len] = buffers[++i];
io_pages[io_buffers_len++] = BufferGetBlock(buffers[i]);
}
+ /* get a reference to wait for in WaitReadBuffers() */+ pgaio_io_get_wref(ioh, &operation->io_wref);++ /* provide the list of buffers to the completion callbacks */+ pgaio_io_set_handle_data_32(ioh, (uint32 *) io_buffers, io_buffers_len);++ pgaio_io_register_callbacks(ioh,+ persistence == RELPERSISTENCE_TEMP ?+ PGAIO_HCB_LOCAL_BUFFER_READV :+ PGAIO_HCB_SHARED_BUFFER_READV,+ flags);++ pgaio_io_set_flag(ioh, ioh_flags);++ /* ---+ * Even though we're trying to issue IO asynchronously, track the time+ * in smgrstartreadv():+ * - if io_method == IOMETHOD_SYNC, we will always perform the IO+ * immediately+ * - the io method might not support the IO (e.g. worker IO for a temp+ * table)+ * ---+ */
io_start = pgstat_prepare_io_time(track_io_timing);
- smgrreadv(operation->smgr, forknum, io_first_block, io_pages, io_buffers_len);- pgstat_count_io_op_time(io_object, io_context, IOOP_READ, io_start,- 1, io_buffers_len * BLCKSZ);-- /* Verify each block we read, and terminate the I/O. */- for (int j = 0; j < io_buffers_len; ++j)- {- BufferDesc *bufHdr;- Block bufBlock;-- if (persistence == RELPERSISTENCE_TEMP)- {- bufHdr = GetLocalBufferDescriptor(-io_buffers[j] - 1);- bufBlock = LocalBufHdrGetBlock(bufHdr);- }- else- {- bufHdr = GetBufferDescriptor(io_buffers[j] - 1);- bufBlock = BufHdrGetBlock(bufHdr);- }-- /* check for garbage data */- if (!PageIsVerifiedExtended((Page) bufBlock, io_first_block + j,- PIV_LOG_WARNING | PIV_REPORT_STAT))- {- if ((operation->flags & READ_BUFFERS_ZERO_ON_ERROR) || zero_damaged_pages)- {- ereport(WARNING,- (errcode(ERRCODE_DATA_CORRUPTED),- errmsg("invalid page in block %u of relation %s; zeroing out page",- io_first_block + j,- relpath(operation->smgr->smgr_rlocator, forknum).str)));- memset(bufBlock, 0, BLCKSZ);- }- else- ereport(ERROR,- (errcode(ERRCODE_DATA_CORRUPTED),- errmsg("invalid page in block %u of relation %s",- io_first_block + j,- relpath(operation->smgr->smgr_rlocator, forknum).str)));- }-- /* Set BM_VALID, terminate IO, and wake up any waiters */- if (persistence == RELPERSISTENCE_TEMP)- TerminateLocalBufferIO(bufHdr, false, BM_VALID, true);- else- TerminateBufferIO(bufHdr, false, BM_VALID, true, true);-- /* Report I/Os as completing individually. */- TRACE_POSTGRESQL_BUFFER_READ_DONE(forknum, io_first_block + j,- operation->smgr->smgr_rlocator.locator.spcOid,- operation->smgr->smgr_rlocator.locator.dbOid,- operation->smgr->smgr_rlocator.locator.relNumber,- operation->smgr->smgr_rlocator.backend,- false);- }+ smgrstartreadv(ioh, operation->smgr, forknum,+ blocknum + nblocks_done,+ io_pages, io_buffers_len);+ pgstat_count_io_op_time(io_object, io_context, IOOP_READ,+ io_start, 1, *nblocks_progress * BLCKSZ);
if (persistence == RELPERSISTENCE_TEMP)
pgBufferUsage.local_blks_read += io_buffers_len;
else
pgBufferUsage.shared_blks_read += io_buffers_len;
+ /*+ * Track vacuum cost when issuing IO, not after waiting for it.+ * Otherwise we could end up issuing a lot of IO in a short timespan,+ * despite a low cost limit.+ */
if (VacuumCostActive)
VacuumCostBalance += VacuumCostPageMiss * io_buffers_len;
++ *nblocks_progress = io_buffers_len;+ did_start_io = true;
}
++ return did_start_io;
}
/*
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0013-aio-Basic-read_stream-adjustments-for-real-AIO.patch (3.8K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/14-v2.11-0013-aio-Basic-read_stream-adjustments-for-real-AIO.patch)
download | inline diff:
From b6d647cffe0ec26b4d2cc03fe324a76cd60abdaf Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 13/27] aio: Basic read_stream adjustments for real AIO
Adapt the read stream logic for real AIO:
- If AIO is enabled, we shouldn't issue advice, but if it isn't, we should
continue issuing advice
- AIO benefits from reading ahead with direct IO- While in read_stream_look_ahead(), we can use AIO batch submission mode for
increased efficiency
There is one comment talking about max_ios logic with "real asynchronous I/O"
that I am not sure about, so I left it alone for now.
There are further improvements we should consider, e.g. waiting to issue IOs
until we can issue multiple IOs at once. But that's left for a future change,
since it would involve additional heuristics.
---
src/backend/storage/aio/read_stream.c | 29 ++++++++++++++++++++++-----
1 file changed, 24 insertions(+), 5 deletions(-)
diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.cindex 18ecf4affc7..60a841816d8 100644--- a/src/backend/storage/aio/read_stream.c+++ b/src/backend/storage/aio/read_stream.c@@ -72,6 +72,7 @@
#include "postgres.h"
#include "miscadmin.h"
+#include "storage/aio.h"
#include "storage/fd.h"
#include "storage/smgr.h"
#include "storage/read_stream.h"
@@ -99,6 +100,7 @@ struct ReadStream
int16 pinned_buffers;
int16 distance;
int16 initialized_buffers;
+ bool sync_mode;
bool advice_enabled;
bool temporary;
@@ -416,6 +418,13 @@ read_stream_start_pending_read(ReadStream *stream)
static void
read_stream_look_ahead(ReadStream *stream)
{
+ /*+ * Allow amortizing the cost of submitting IO over multiple IOs. This+ * requires that we don't do any operations that could lead to a deadlock+ * with staged-but-unsubmitted IO.+ */+ pgaio_enter_batchmode();+
while (stream->ios_in_progress < stream->max_ios &&
stream->pinned_buffers + stream->pending_read_nblocks < stream->distance)
{
@@ -463,6 +472,7 @@ read_stream_look_ahead(ReadStream *stream)
{
/* We've hit the buffer or I/O limit. Rewind and stop here. */
read_stream_unget_block(stream, blocknum);
+ pgaio_exit_batchmode();
return;
}
}
@@ -497,6 +507,8 @@ read_stream_look_ahead(ReadStream *stream)
* time.
*/
Assert(stream->pinned_buffers > 0 || stream->distance == 0);
++ pgaio_exit_batchmode();
}
/*
@@ -629,15 +641,19 @@ read_stream_begin_impl(int flags,
stream->per_buffer_data = (void *)
MAXALIGN(&stream->ios[Max(1, max_ios)]);
+ stream->sync_mode = io_method == IOMETHOD_SYNC;+
#ifdef USE_PREFETCH
/*
- * This system supports prefetching advice. We can use it as long as- * direct I/O isn't enabled, the caller hasn't promised sequential access- * (overriding our detection heuristics), and max_ios hasn't been set to- * zero.+ * This system supports prefetching advice.+ *+ * Issue advice only if AIO is not used, direct I/O isn't enabled, the+ * caller hasn't promised sequential access (overriding our detection+ * heuristics), and max_ios hasn't been set to zero.
*/
- if ((io_direct_flags & IO_DIRECT_DATA) == 0 &&+ if (stream->sync_mode &&+ (io_direct_flags & IO_DIRECT_DATA) == 0 &&
(flags & READ_STREAM_SEQUENTIAL) == 0 &&
max_ios > 0)
stream->advice_enabled = true;
@@ -647,6 +663,9 @@ read_stream_begin_impl(int flags,
* For now, max_ios = 0 is interpreted as max_ios = 1 with advice disabled
* above. If we had real asynchronous I/O we might need a slightly
* different definition.
+ *+ * FIXME: Not sure what different definition we would need? I guess we+ * could add the READ_BUFFERS_SYNCHRONOUSLY flag automatically?
*/
if (max_ios == 0)
max_ios = 1;
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0014-docs-Reframe-track_io_timing-related-docs-as-w.patch (5.3K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/15-v2.11-0014-docs-Reframe-track_io_timing-related-docs-as-w.patch)
download | inline diff:
From f8344496f6de5c018850e8953c30a1a3c93037a4 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 14/27] docs: Reframe track_io_timing related docs as
wait time
With AIO it does not make sense anymore to track the time for each individual
IO, as multiple IOs can be in-flight at the same time. Instead we now track
the time spent *waiting* for IOs.
This should be reflected in the docs. While, so far, we only do a subset of
reads, and no other operations, via AIO, describing the GUC and view columns
as measuring IO waits is accurate for synchronous and asynchronous IO.
Discussion: https://postgr.es/m/5dzyoduxlvfg55oqtjyjehez5uoq6hnwgzor4kkybkfdgkj7ag@rbi4gsmzaczk
---
doc/src/sgml/config.sgml | 4 ++--
doc/src/sgml/monitoring.sgml | 12 ++++++------
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgmlindex 4f71725036b..9f63c4574f9 100644--- a/doc/src/sgml/config.sgml+++ b/doc/src/sgml/config.sgml@@ -8578,7 +8578,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</term>
<listitem>
<para>
- Enables timing of database I/O calls. This parameter is off by+ Enables timing of database I/O waits. This parameter is off by
default, as it will repeatedly query the operating system for
the current time, which may cause significant overhead on some
platforms. You can use the <xref linkend="pgtesttiming"/> tool to
@@ -8612,7 +8612,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</term>
<listitem>
<para>
- Enables timing of WAL I/O calls. This parameter is off by default,+ Enables timing of WAL I/O waits. This parameter is off by default,
as it will repeatedly query the operating system for the current time,
which may cause significant overhead on some platforms.
You can use the <application>pg_test_timing</application> tool to
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgmlindex aaa6586d3a4..7598340072f 100644--- a/doc/src/sgml/monitoring.sgml+++ b/doc/src/sgml/monitoring.sgml@@ -2733,7 +2733,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
<structfield>read_time</structfield> <type>double precision</type>
</para>
<para>
- Time spent in read operations in milliseconds (if+ Time spent waiting for read operations in milliseconds (if
<xref linkend="guc-track-io-timing"/> is enabled and
<varname>object</varname> is not <literal>wal</literal>,
or if <xref linkend="guc-track-wal-io-timing"/> is enabled
@@ -2771,7 +2771,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
<structfield>write_time</structfield> <type>double precision</type>
</para>
<para>
- Time spent in write operations in milliseconds (if+ Time spent waiting for write operations in milliseconds (if
<xref linkend="guc-track-io-timing"/> is enabled and
<varname>object</varname> is not <literal>wal</literal>,
or if <xref linkend="guc-track-wal-io-timing"/> is enabled
@@ -2799,7 +2799,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
<structfield>writeback_time</structfield> <type>double precision</type>
</para>
<para>
- Time spent in writeback operations in milliseconds (if+ Time spent waiting for writeback operations in milliseconds (if
<xref linkend="guc-track-io-timing"/> is enabled, otherwise zero). This
includes the time spent queueing write-out requests and, potentially,
the time spent to write out the dirty data.
@@ -2835,7 +2835,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
<structfield>extend_time</structfield> <type>double precision</type>
</para>
<para>
- Time spent in extend operations in milliseconds. (if+ Time spent waiting for extend operations in milliseconds. (if
<xref linkend="guc-track-io-timing"/> is enabled and
<varname>object</varname> is not <literal>wal</literal>,
or if <xref linkend="guc-track-wal-io-timing"/> is enabled
@@ -2909,7 +2909,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
<structfield>fsync_time</structfield> <type>double precision</type>
</para>
<para>
- Time spent in fsync operations in milliseconds (if+ Time spent waiting for fsync operations in milliseconds (if
<xref linkend="guc-track-io-timing"/> is enabled and
<varname>object</varname> is not <literal>wal</literal>,
or if <xref linkend="guc-track-wal-io-timing"/> is enabled
@@ -2996,7 +2996,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
<note>
<para>
- Columns tracking I/O time will only be non-zero when+ Columns tracking I/O wait time will only be non-zero when
<xref linkend="guc-track-io-timing"/> is enabled. The user should be
careful when referencing these columns in combination with their
corresponding I/O operations in case <varname>track_io_timing</varname>
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0015-Enable-IO-concurrency-on-all-systems.patch (11.2K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/16-v2.11-0015-Enable-IO-concurrency-on-all-systems.patch)
download | inline diff:
From 59a9323ac26e99b81d1a68762ab3953c960040bf Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Wed, 19 Mar 2025 10:15:20 -0400
Subject: [PATCH v2.11 15/27] Enable IO concurrency on all systems
Previously effective_io_concurrency and maintenance_io_concurrency could
not be set above 0 on machines without fadvise support. AIO enables
IO concurrency without such support.
Currently only subsystems using the read stream API will take advantage
of this. Other users of maintenance_io_concurrency (like recovery
prefetching) which leverage OS advice directly will not benefit from
this change. In those cases, maintenance_io_concurrency will have no
effect on I/O behavior.
Author: Melanie Plageman <melanieplageman@gmail.com>
Discussion: https://postgr.es/m/CAAKRu_atGgZePo=_g6T3cNtfMf0QxpvoUh5OUqa_cnPdhLd=gw@mail.gmail.com
---
src/include/storage/bufmgr.h | 6 ----
src/include/utils/guc_hooks.h | 4 ---
src/backend/access/common/reloptions.c | 8 -----
src/backend/commands/variable.c | 30 -------------------
src/backend/utils/misc/guc_tables.c | 4 +--
src/backend/utils/misc/postgresql.conf.sample | 6 ++--
src/bin/initdb/initdb.c | 5 ----
doc/src/sgml/config.sgml | 15 +++++-----
doc/src/sgml/ref/alter_tablespace.sgml | 2 +-
doc/src/sgml/ref/create_tablespace.sgml | 2 +-
10 files changed, 14 insertions(+), 68 deletions(-)
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.hindex 7abb3bdfd9d..784df8b00cb 100644--- a/src/include/storage/bufmgr.h+++ b/src/include/storage/bufmgr.h@@ -156,14 +156,8 @@ extern PGDLLIMPORT int bgwriter_lru_maxpages;
extern PGDLLIMPORT double bgwriter_lru_multiplier;
extern PGDLLIMPORT bool track_io_timing;
-/* only applicable when prefetching is available */-#ifdef USE_PREFETCH
#define DEFAULT_EFFECTIVE_IO_CONCURRENCY 16
#define DEFAULT_MAINTENANCE_IO_CONCURRENCY 16
-#else-#define DEFAULT_EFFECTIVE_IO_CONCURRENCY 0-#define DEFAULT_MAINTENANCE_IO_CONCURRENCY 0-#endif
extern PGDLLIMPORT int effective_io_concurrency;
extern PGDLLIMPORT int maintenance_io_concurrency;
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.hindex 0f1e74f96c9..799fa7ace68 100644--- a/src/include/utils/guc_hooks.h+++ b/src/include/utils/guc_hooks.h@@ -61,8 +61,6 @@ extern bool check_default_text_search_config(char **newval, void **extra, GucSou
extern void assign_default_text_search_config(const char *newval, void *extra);
extern bool check_default_with_oids(bool *newval, void **extra,
GucSource source);
-extern bool check_effective_io_concurrency(int *newval, void **extra,- GucSource source);
extern bool check_huge_page_size(int *newval, void **extra, GucSource source);
extern void assign_io_method(int newval, void *extra);
extern bool check_io_max_concurrency(int *newval, void **extra, GucSource source);
@@ -83,8 +81,6 @@ extern bool check_log_stats(bool *newval, void **extra, GucSource source);
extern bool check_log_timezone(char **newval, void **extra, GucSource source);
extern void assign_log_timezone(const char *newval, void *extra);
extern const char *show_log_timezone(void);
-extern bool check_maintenance_io_concurrency(int *newval, void **extra,- GucSource source);
extern void assign_maintenance_io_concurrency(int newval, void *extra);
extern void assign_io_max_combine_limit(int newval, void *extra);
extern void assign_io_combine_limit(int newval, void *extra);
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.cindex 645b5c00467..46c1dce222d 100644--- a/src/backend/access/common/reloptions.c+++ b/src/backend/access/common/reloptions.c@@ -361,11 +361,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_TABLESPACE,
ShareUpdateExclusiveLock
},
-#ifdef USE_PREFETCH
-1, 0, MAX_IO_CONCURRENCY
-#else- 0, 0, 0-#endif
},
{
{
@@ -374,11 +370,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_TABLESPACE,
ShareUpdateExclusiveLock
},
-#ifdef USE_PREFETCH
-1, 0, MAX_IO_CONCURRENCY
-#else- 0, 0, 0-#endif
},
{
{
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.cindex f550a3c0c63..d3567aa7f27 100644--- a/src/backend/commands/variable.c+++ b/src/backend/commands/variable.c@@ -1145,7 +1145,6 @@ check_cluster_name(char **newval, void **extra, GucSource source)
void
assign_maintenance_io_concurrency(int newval, void *extra)
{
-#ifdef USE_PREFETCH
/*
* Reconfigure recovery prefetching, because a setting it depends on
* changed.
@@ -1153,7 +1152,6 @@ assign_maintenance_io_concurrency(int newval, void *extra)
maintenance_io_concurrency = newval;
if (AmStartupProcess())
XLogPrefetchReconfigure();
-#endif
}
/*
@@ -1249,34 +1247,6 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
-bool-check_effective_io_concurrency(int *newval, void **extra, GucSource source)-{-#ifndef USE_PREFETCH- if (*newval != 0)- {- GUC_check_errdetail("\"%s\" must be set to 0 on platforms that lack support for issuing read-ahead advice.",- "effective_io_concurrency");- return false;- }-#endif /* USE_PREFETCH */- return true;-}--bool-check_maintenance_io_concurrency(int *newval, void **extra, GucSource source)-{-#ifndef USE_PREFETCH- if (*newval != 0)- {- GUC_check_errdetail("\"%s\" must be set to 0 on platforms that lack support for issuing read-ahead advice.",- "maintenance_io_concurrency");- return false;- }-#endif /* USE_PREFETCH */- return true;-}-
bool
check_ssl(bool *newval, void **extra, GucSource source)
{
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.cindex 75bb0acdf0f..2e17dbc8c9e 100644--- a/src/backend/utils/misc/guc_tables.c+++ b/src/backend/utils/misc/guc_tables.c@@ -3245,7 +3245,7 @@ struct config_int ConfigureNamesInt[] =
&effective_io_concurrency,
DEFAULT_EFFECTIVE_IO_CONCURRENCY,
0, MAX_IO_CONCURRENCY,
- check_effective_io_concurrency, NULL, NULL+ NULL, NULL, NULL
},
{
@@ -3259,7 +3259,7 @@ struct config_int ConfigureNamesInt[] =
&maintenance_io_concurrency,
DEFAULT_MAINTENANCE_IO_CONCURRENCY,
0, MAX_IO_CONCURRENCY,
- check_maintenance_io_concurrency, assign_maintenance_io_concurrency,+ NULL, assign_maintenance_io_concurrency,
NULL
},
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sampleindex 69dc727174b..d5f5f3dbda4 100644--- a/src/backend/utils/misc/postgresql.conf.sample+++ b/src/backend/utils/misc/postgresql.conf.sample@@ -198,11 +198,11 @@
# - I/O -
#backend_flush_after = 0 # measured in pages, 0 disables
-#effective_io_concurrency = 16 # 1-1000; 0 disables prefetching-#maintenance_io_concurrency = 16 # 1-1000; 0 disables prefetching+#effective_io_concurrency = 16 # 1-1000; 0 disables issuing multiple simultaneous IO requests+#maintenance_io_concurrency = 16 # 1-1000; same as effective_io_concurrency
#io_max_combine_limit = 128kB # usually 1-128 blocks (depends on OS)
# (change requires restart)
-#io_combine_limit = 128kB # usually 1-128 blocks (depends on OS)+#io_combine_limit = 128kB # usually 1-32 blocks (depends on OS)
#io_method = worker # worker, io_uring, sync
# (change requires restart)
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.cindex 21a0fe3ecd9..b9d75799ee0 100644--- a/src/bin/initdb/initdb.c+++ b/src/bin/initdb/initdb.c@@ -1401,11 +1401,6 @@ setup_config(void)
repltok, true);
#endif
-#ifndef USE_PREFETCH- conflines = replace_guc_value(conflines, "effective_io_concurrency",- "0", true);-#endif-
#ifdef WIN32
conflines = replace_guc_value(conflines, "update_process_title",
"off", true);
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgmlindex 9f63c4574f9..863a5d025c4 100644--- a/doc/src/sgml/config.sgml+++ b/doc/src/sgml/config.sgml@@ -2585,8 +2585,7 @@ include_dir 'conf.d'
session attempts to initiate in parallel. The allowed range is
<literal>1</literal> to <literal>1000</literal>, or
<literal>0</literal> to disable issuance of asynchronous I/O requests.
- The default is <literal>16</literal> on supported systems, otherwise- <literal>0</literal>.+ The default is <literal>16</literal>.
</para>
<para>
@@ -2597,8 +2596,8 @@ include_dir 'conf.d'
</para>
<para>
- On systems without prefetch advice support, attempting to configure- any value other than <literal>0</literal> will error out.+ On systems with prefetch advice support,+ <varname>effective_io_concurrency</varname> also controls the prefetch distance.
</para>
<para>
@@ -2621,10 +2620,10 @@ include_dir 'conf.d'
for maintenance work that is done on behalf of many client sessions.
</para>
<para>
- The default is <literal>16</literal> on supported systems, otherwise- <literal>0</literal>. This value can be overridden for tables in a- particular tablespace by setting the tablespace parameter of the same- name (see <xref linkend="sql-altertablespace"/>).+ The default is <literal>16</literal>. This value can be overridden+ for tables in a particular tablespace by setting the tablespace+ parameter of the same name (see <xref+ linkend="sql-altertablespace"/>).
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/alter_tablespace.sgml b/doc/src/sgml/ref/alter_tablespace.sgmlindex 6ec863400d1..d0e08089ddb 100644--- a/doc/src/sgml/ref/alter_tablespace.sgml+++ b/doc/src/sgml/ref/alter_tablespace.sgml@@ -88,7 +88,7 @@ ALTER TABLESPACE <replaceable>name</replaceable> RESET ( <replaceable class="par
and <varname>maintenance_io_concurrency</varname>.
Setting these values for a particular tablespace will override the
planner's usual estimate of the cost of reading pages from tables in
- that tablespace, and the executor's prefetching behavior, as established+ that tablespace, and how many concurrent I/Os are issued, as established
by the configuration parameters of the
same name (see <xref linkend="guc-seq-page-cost"/>,
<xref linkend="guc-random-page-cost"/>,
diff --git a/doc/src/sgml/ref/create_tablespace.sgml b/doc/src/sgml/ref/create_tablespace.sgmlindex 9d5ab025261..b77e774c53f 100644--- a/doc/src/sgml/ref/create_tablespace.sgml+++ b/doc/src/sgml/ref/create_tablespace.sgml@@ -110,7 +110,7 @@ CREATE TABLESPACE <replaceable class="parameter">tablespace_name</replaceable>
and <varname>maintenance_io_concurrency</varname>.
Setting these values for a particular tablespace will override the
planner's usual estimate of the cost of reading pages from tables in
- that tablespace, and the executor's prefetching behavior, as established+ that tablespace, and how many concurrent I/Os are issued, as established
by the configuration parameters of the
same name (see <xref linkend="guc-seq-page-cost"/>,
<xref linkend="guc-random-page-cost"/>,
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0016-aio-Add-pg_aios-view.patch (9.8K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/17-v2.11-0016-aio-Add-pg_aios-view.patch)
download | inline diff:
From a12552dfdda77bf66f7f248ba3265220f8f848e3 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 16/27] aio: Add pg_aios view
TODO:
- decide on name- add docs
FIXME:
- catversion bump
Author:
Reviewed-by:
Discussion: https://postgr.es/m/
Backpatch:
---
src/include/catalog/pg_proc.dat | 10 ++
src/backend/catalog/system_views.sql | 3 +
src/backend/storage/aio/Makefile | 1 +
src/backend/storage/aio/aio_funcs.c | 219 +++++++++++++++++++++++++++
src/backend/storage/aio/meson.build | 1 +
src/test/regress/expected/rules.out | 16 ++
6 files changed, 250 insertions(+)
create mode 100644 src/backend/storage/aio/aio_funcs.c
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.datindex 890822eaf79..017971011f3 100644--- a/src/include/catalog/pg_proc.dat+++ b/src/include/catalog/pg_proc.dat@@ -12479,4 +12479,14 @@
proargtypes => 'int4',
prosrc => 'gist_stratnum_common' },
+# AIO related functions+{ oid => '9200', descr => 'information about in-progress asynchronous IOs',+ proname => 'pg_get_aios', prorows => '100', proretset => 't',+ provolatile => 'v', proparallel => 'r', prorettype => 'record', proargtypes => '',+ proallargtypes => '{int4,int4,int8,text,text,int8,int8,text,int2,int4,text,text,bool,bool,bool}',+ proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',+ proargnames => '{pid,io_id,io_generation,state,operation,offset,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',+ prosrc => 'pg_get_aios' },++
]
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sqlindex a4d2cfdcaf5..73902a763d1 100644--- a/src/backend/catalog/system_views.sql+++ b/src/backend/catalog/system_views.sql@@ -1390,3 +1390,6 @@ CREATE VIEW pg_stat_subscription_stats AS
CREATE VIEW pg_wait_events AS
SELECT * FROM pg_get_wait_events();
++CREATE VIEW pg_aios AS+ SELECT * FROM pg_get_aios();diff --git a/src/backend/storage/aio/Makefile b/src/backend/storage/aio/Makefileindex c06c50771e0..3f2469cc399 100644--- a/src/backend/storage/aio/Makefile+++ b/src/backend/storage/aio/Makefile@@ -11,6 +11,7 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
aio.o \
aio_callback.o \
+ aio_funcs.o \
aio_init.o \
aio_io.o \
aio_target.o \
diff --git a/src/backend/storage/aio/aio_funcs.c b/src/backend/storage/aio/aio_funcs.cnew file mode 100644index 00000000000..58883fd61d8--- /dev/null+++ b/src/backend/storage/aio/aio_funcs.c@@ -0,0 +1,219 @@+/*-------------------------------------------------------------------------+ *+ * aio_funcs.c+ * AIO - SQL interface for AIO+ *+ *+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group+ * Portions Copyright (c) 1994, Regents of the University of California+ *+ * IDENTIFICATION+ * src/backend/storage/aio/aio_funcs.c+ *+ *-------------------------------------------------------------------------+ */++#include "postgres.h"++#include "storage/aio.h"+#include "storage/aio_internal.h"+#include "utils/builtins.h"+#include "funcapi.h"+#include "storage/proc.h"+++/*+ * Byte length of an iovec.+ */+static size_t+iov_byte_length(const struct iovec *iov, int cnt)+{+ size_t len = 0;++ for (int i = 0; i < cnt; i++)+ {+ len += iov[i].iov_len;+ }++ return len;+}++Datum+pg_get_aios(PG_FUNCTION_ARGS)+{+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;++ InitMaterializedSRF(fcinfo, 0);++#define PG_GET_AIOS_COLS 15++ for (uint64 i = 0; i < pgaio_ctl->io_handle_count; i++)+ {+ PgAioHandle *live_ioh = &pgaio_ctl->io_handles[i];+ uint32 ioh_id = pgaio_io_get_id(live_ioh);+ Datum values[PG_GET_AIOS_COLS] = {0};+ bool nulls[PG_GET_AIOS_COLS] = {0};+ ProcNumber owner;+ PGPROC *owner_proc;+ int32 owner_pid;+ PgAioHandleState start_state;+ uint64 start_generation;+ PgAioHandle ioh_copy;+ struct iovec iov_copy[PG_IOV_MAX];++retry:++ /*+ * There is no lock that could prevent the state of the IO to advance+ * concurrently - and we don't want to introduce one, as that would+ * introduce atomics into a very common path. Instead we+ *+ * 1) Determine the state + generation of the IO.+ *+ * 2) Copy the IO to local memory.+ *+ * 3) Check if state or generation of the IO changed. If the state+ * changed, retry, if the generation changed don't display the IO.+ */++ /* 1) from above */+ start_generation = live_ioh->generation;+ pg_read_barrier();+ start_state = live_ioh->state;++ if (start_state == PGAIO_HS_IDLE)+ continue;++ /* 2) from above */+ memcpy(&ioh_copy, live_ioh, sizeof(PgAioHandle));++ /*+ * Safe to copy even if no iovec is used - we always reserve the+ * required space.+ */+ memcpy(&iov_copy, &pgaio_ctl->iovecs[ioh_copy.iovec_off],+ PG_IOV_MAX * sizeof(struct iovec));++ /*+ * Copy information about owner before 3) below, if the process exited+ * it'd have to wait for the IO to finish first, which we would detect+ * in 3).+ */+ owner = ioh_copy.owner_procno;+ owner_proc = GetPGProcByNumber(owner);+ owner_pid = owner_proc->pid;++ /* 3) from above */+ pg_read_barrier();++ /*+ * The IO completed and a new one was started with the same ID. Don't+ * display it - it really started after this function was called.+ * There be a risk of a livelock if we just retried endlessly, if IOs+ * complete very quickly.+ */+ if (live_ioh->generation != start_generation)+ continue;++ /*+ * The IOs state changed while we were "rendering" it. Just start from+ * scratch. There's no risk of a livelock here, as an IO has a limited+ * sets of states it can be in, and state changes go only in a single+ * direction.+ */+ if (live_ioh->state != start_state)+ goto retry;++ /*+ * Now that we have copied the IO into local memory and checked that+ * it's still in the same state, we are not allowed to access "live"+ * memory anymore. To make it slightly easier to catch such cases, set+ * the "live" pointers to NULL.+ */+ live_ioh = NULL;+ owner_proc = NULL;+++ /* column: owning pid */+ if (owner_pid != 0)+ values[0] = Int32GetDatum(owner_pid);+ else+ nulls[0] = false;++ /* column: IO's id */+ values[1] = ioh_id;++ /* column: IO's generation */+ values[2] = Int64GetDatum(start_generation);++ /* column: IO's state */+ values[3] = CStringGetTextDatum(pgaio_io_get_state_name(&ioh_copy));++ /*+ * If the IO is in PGAIO_HS_HANDED_OUT state, none of it's fields are+ * valid yet (or are in the process of being set). Therefore we don't+ * want to display any other columns.+ */+ if (start_state == PGAIO_HS_HANDED_OUT)+ {+ memset(nulls + 4, 1, (lengthof(nulls) - 4) * sizeof(bool));+ goto display;+ }++ /* column: IO's operation */+ values[4] = CStringGetTextDatum(pgaio_io_get_op_name(&ioh_copy));++ /* columns: details about the IO's operation */+ switch (ioh_copy.op)+ {+ case PGAIO_OP_INVALID:+ nulls[5] = true;+ nulls[6] = true;+ break;+ case PGAIO_OP_READV:+ values[5] = Int64GetDatum(ioh_copy.op_data.read.offset);+ values[6] =+ Int64GetDatum(iov_byte_length(iov_copy, ioh_copy.op_data.read.iov_length));+ break;+ case PGAIO_OP_WRITEV:+ values[5] = Int64GetDatum(ioh_copy.op_data.write.offset);+ values[6] =+ Int64GetDatum(iov_byte_length(iov_copy, ioh_copy.op_data.write.iov_length));+ break;+ }++ /* column: IO's target */+ values[7] = CStringGetTextDatum(pgaio_io_get_target_name(&ioh_copy));++ /* column: length of IO's data array */+ values[8] = Int16GetDatum(ioh_copy.handle_data_len);++ /* column: raw result (i.e. some form of syscall return value) */+ if (start_state == PGAIO_HS_COMPLETED_IO+ || start_state == PGAIO_HS_COMPLETED_SHARED)+ values[9] = Int32GetDatum(ioh_copy.result);+ else+ nulls[9] = true;++ /*+ * column: result in the higher level representation (unknown if not+ * finished)+ */+ values[10] =+ CStringGetTextDatum(pgaio_result_status_string(ioh_copy.distilled_result.status));++ /* column: target description */+ values[11] = CStringGetTextDatum(pgaio_io_get_target_description(&ioh_copy));++ /* columns: one for each flag */+ values[12] = BoolGetDatum(ioh_copy.flags & PGAIO_HF_SYNCHRONOUS);+ values[13] = BoolGetDatum(ioh_copy.flags & PGAIO_HF_REFERENCES_LOCAL);+ values[14] = BoolGetDatum(ioh_copy.flags & PGAIO_HF_BUFFERED);++display:++ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);+ }++ return (Datum) 0;+}diff --git a/src/backend/storage/aio/meson.build b/src/backend/storage/aio/meson.buildindex 2f0f03d8071..da6df2d3654 100644--- a/src/backend/storage/aio/meson.build+++ b/src/backend/storage/aio/meson.build@@ -3,6 +3,7 @@
backend_sources += files(
'aio.c',
'aio_callback.c',
+ 'aio_funcs.c',
'aio_init.c',
'aio_io.c',
'aio_target.c',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.outindex 62f69ac20b2..6570d0cddce 100644--- a/src/test/regress/expected/rules.out+++ b/src/test/regress/expected/rules.out@@ -1286,6 +1286,22 @@ drop table cchild;
SELECT viewname, definition FROM pg_views
WHERE schemaname = 'pg_catalog'
ORDER BY viewname;
+pg_aios| SELECT pid,+ io_id,+ io_generation,+ state,+ operation,+ "offset",+ length,+ target,+ handle_data_len,+ raw_result,+ result,+ target_desc,+ f_sync,+ f_localmem,+ f_buffered+ FROM pg_get_aios() pg_get_aios(pid, io_id, io_generation, state, operation, "offset", length, target, handle_data_len, raw_result, result, target_desc, f_sync, f_localmem, f_buffered);
pg_available_extension_versions| SELECT e.name,
e.version,
(x.extname IS NOT NULL) AS installed,
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0017-aio-Add-test_aio-module.patch (45.0K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/18-v2.11-0017-aio-Add-test_aio-module.patch)
download | inline diff:
From 7ec622ba4ff50a7b9c24ed0158376b920076259a Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 17/27] aio: Add test_aio module
---
src/include/storage/buf_internals.h | 6 +
src/backend/storage/buffer/bufmgr.c | 8 +-
src/test/modules/Makefile | 1 +
src/test/modules/meson.build | 1 +
src/test/modules/test_aio/.gitignore | 2 +
src/test/modules/test_aio/Makefile | 27 +
src/test/modules/test_aio/meson.build | 37 ++
src/test/modules/test_aio/t/001_aio.pl | 699 ++++++++++++++++++++
src/test/modules/test_aio/test_aio--1.0.sql | 97 +++
src/test/modules/test_aio/test_aio.c | 657 ++++++++++++++++++
src/test/modules/test_aio/test_aio.control | 3 +
11 files changed, 1532 insertions(+), 6 deletions(-)
create mode 100644 src/test/modules/test_aio/.gitignore
create mode 100644 src/test/modules/test_aio/Makefile
create mode 100644 src/test/modules/test_aio/meson.build
create mode 100644 src/test/modules/test_aio/t/001_aio.pl
create mode 100644 src/test/modules/test_aio/test_aio--1.0.sql
create mode 100644 src/test/modules/test_aio/test_aio.c
create mode 100644 src/test/modules/test_aio/test_aio.control
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.hindex 8a52ace37cd..4b1f52a9fc8 100644--- a/src/include/storage/buf_internals.h+++ b/src/include/storage/buf_internals.h@@ -434,6 +434,12 @@ extern void IssuePendingWritebacks(WritebackContext *wb_context, IOContext io_co
extern void ScheduleBufferTagForWriteback(WritebackContext *wb_context,
IOContext io_context, BufferTag *tag);
+/* solely to make it easier to write tests */+extern bool StartBufferIO(BufferDesc *buf, bool forInput, bool nowait);+extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint32 set_flag_bits,+ bool forget_owner, bool syncio);++
/* freelist.c */
extern IOContext IOContextForStrategy(BufferAccessStrategy strategy);
extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.cindex 08d25e70bf2..be306bfa4f2 100644--- a/src/backend/storage/buffer/bufmgr.c+++ b/src/backend/storage/buffer/bufmgr.c@@ -518,10 +518,6 @@ static uint32 WaitBufHdrUnlocked(BufferDesc *buf);
static int SyncOneBuffer(int buf_id, bool skip_recently_used,
WritebackContext *wb_context);
static void WaitIO(BufferDesc *buf);
-static bool StartBufferIO(BufferDesc *buf, bool forInput, bool nowait);-static void TerminateBufferIO(BufferDesc *buf, bool clear_dirty,- uint32 set_flag_bits, bool forget_owner,- bool syncio);
static void AbortBufferIO(Buffer buffer);
static void shared_buffer_write_error_callback(void *arg);
static void local_buffer_write_error_callback(void *arg);
@@ -5944,7 +5940,7 @@ WaitIO(BufferDesc *buf)
* find out if they can perform the I/O as part of a larger operation, without
* waiting for the answer or distinguishing the reasons why not.
*/
-static bool+bool
StartBufferIO(BufferDesc *buf, bool forInput, bool nowait)
{
uint32 buf_state;
@@ -6001,7 +5997,7 @@ StartBufferIO(BufferDesc *buf, bool forInput, bool nowait)
* resource owner. (forget_owner=false is used when the resource owner itself
* is being released)
*/
-static void+void
TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint32 set_flag_bits,
bool forget_owner, bool syncio)
{
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefileindex 4e4be3fa511..aa1d27bbed3 100644--- a/src/test/modules/Makefile+++ b/src/test/modules/Makefile@@ -14,6 +14,7 @@ SUBDIRS = \
oauth_validator \
plsample \
spgist_name_ops \
+ test_aio \
test_bloomfilter \
test_copy_callbacks \
test_custom_rmgrs \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.buildindex 2b057451473..4fda1acfb32 100644--- a/src/test/modules/meson.build+++ b/src/test/modules/meson.build@@ -1,5 +1,6 @@
# Copyright (c) 2022-2025, PostgreSQL Global Development Group
+subdir('test_aio')
subdir('brin')
subdir('commit_ts')
subdir('delay_execution')
diff --git a/src/test/modules/test_aio/.gitignore b/src/test/modules/test_aio/.gitignorenew file mode 100644index 00000000000..716e17f5a2a--- /dev/null+++ b/src/test/modules/test_aio/.gitignore@@ -0,0 +1,2 @@+# Generated subdirectories+/tmp_check/diff --git a/src/test/modules/test_aio/Makefile b/src/test/modules/test_aio/Makefilenew file mode 100644index 00000000000..87d5315ba00--- /dev/null+++ b/src/test/modules/test_aio/Makefile@@ -0,0 +1,27 @@+# src/test/modules/delay_execution/Makefile++PGFILEDESC = "test_aio - test code for AIO"++MODULE_big = test_aio+OBJS = \+ $(WIN32RES) \+ test_aio.o++EXTENSION = test_aio+DATA = test_aio--1.0.sql++TAP_TESTS = 1++export enable_injection_points+export with_liburing++ifdef USE_PGXS+PG_CONFIG = pg_config+PGXS := $(shell $(PG_CONFIG) --pgxs)+include $(PGXS)+else+subdir = src/test/modules/test_aio+top_builddir = ../../../..+include $(top_builddir)/src/Makefile.global+include $(top_srcdir)/contrib/contrib-global.mk+endifdiff --git a/src/test/modules/test_aio/meson.build b/src/test/modules/test_aio/meson.buildnew file mode 100644index 00000000000..ac846b2b6f3--- /dev/null+++ b/src/test/modules/test_aio/meson.build@@ -0,0 +1,37 @@+# Copyright (c) 2022-2024, PostgreSQL Global Development Group++test_aio_sources = files(+ 'test_aio.c',+)++if host_system == 'windows'+ test_aio_sources += rc_lib_gen.process(win32ver_rc, extra_args: [+ '--NAME', 'test_aio',+ '--FILEDESC', 'test_aio - test code for AIO',])+endif++test_aio = shared_module('test_aio',+ test_aio_sources,+ kwargs: pg_test_mod_args,+)+test_install_libs += test_aio++test_install_data += files(+ 'test_aio.control',+ 'test_aio--1.0.sql',+)++tests += {+ 'name': 'test_aio',+ 'sd': meson.current_source_dir(),+ 'bd': meson.current_build_dir(),+ 'tap': {+ 'env': {+ 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',+ 'with_liburing': liburing.found() ? 'yes' : 'no',+ },+ 'tests': [+ 't/001_aio.pl',+ ],+ },+}diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.plnew file mode 100644index 00000000000..444cabf2954--- /dev/null+++ b/src/test/modules/test_aio/t/001_aio.pl@@ -0,0 +1,699 @@+# Copyright (c) 2025, PostgreSQL Global Development Group++use strict;+use warnings FATAL => 'all';++use PostgreSQL::Test::Cluster;+use PostgreSQL::Test::Utils;+use Test::More;+++###+# Test io_method=worker+###+my $node_worker = create_node('worker');+$node_worker->start();++test_generic('worker', $node_worker);+SKIP:+{+ skip 'Injection points not supported by this build', 1+ unless $ENV{enable_injection_points} eq 'yes';+ test_inject_worker('worker', $node_worker);+}++$node_worker->stop();+++###+# Test io_method=io_uring+###++if ($ENV{with_liburing} eq 'yes')+{+ my $node_uring = create_node('io_uring');+ $node_uring->start();+ test_generic('io_uring', $node_uring);+ $node_uring->stop();+}+++###+# Test io_method=sync+###++my $node_sync = create_node('sync');++# just to have one test not use the default auto-tuning++$node_sync->append_conf(+ 'postgresql.conf', qq(+io_max_concurrency=4+));++$node_sync->start();+test_generic('sync', $node_sync);+$node_sync->stop();++done_testing();+++###+# Test Helpers+###++sub create_node+{+ local $Test::Builder::Level = $Test::Builder::Level + 1;++ my $io_method = shift;++ my $node = PostgreSQL::Test::Cluster->new($io_method);++ # Want to test initdb for each IO method, otherwise we could just reuse+ # the cluster.+ #+ # Unfortunately Cluster::init() puts PG_TEST_INITDB_EXTRA_OPTS after the+ # options specified by ->extra, if somebody puts -c io_method=xyz in+ # PG_TEST_INITDB_EXTRA_OPTS it would break this test. Fix that up if we+ # detect it.+ local $ENV{PG_TEST_INITDB_EXTRA_OPTS} = $ENV{PG_TEST_INITDB_EXTRA_OPTS};+ if (defined $ENV{PG_TEST_INITDB_EXTRA_OPTS} &&+ $ENV{PG_TEST_INITDB_EXTRA_OPTS} =~ m/io_method=/)+ {+ $ENV{PG_TEST_INITDB_EXTRA_OPTS} .= " -c io_method=$io_method";+ }++ $node->init(extra => [ '-c', "io_method=$io_method" ]);++ $node->append_conf(+ 'postgresql.conf', qq(+shared_preload_libraries=test_aio+log_min_messages = 'DEBUG3'+log_statement=all+restart_after_crash=false+));++ ok(1, "$io_method: initdb");++ return $node;+}++sub psql_like+{+ my $io_method = shift;+ my $psql = shift;+ my $name = shift;+ my $sql = shift;+ my $expected_stdout = shift;+ my $expected_stderr = shift;+ my ($cmdret, $output);++ ($output, $cmdret) = $psql->query($sql);++ like($output, $expected_stdout, "$io_method: $name: expected stdout");+ like($psql->{stderr}, $expected_stderr,+ "$io_method: $name: expected stderr");+ $psql->{stderr} = '';+}++sub query_wait_block+{+ my $io_method = shift;+ my $node = shift;+ my $psql = shift;+ my $name = shift;+ my $sql = shift;+ my $waitfor = shift;++ my $pid = $psql->query_safe('SELECT pg_backend_pid()');++ $psql->{stdin} .= qq($sql;\n);+ $psql->{run}->pump_nb();+ ok(1, "$io_method: $name: issued sql");++ $node->poll_query_until('postgres',+ qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid),+ $waitfor,);+ ok(1, "$io_method: $name: observed $waitfor wait event");+}+++###+# Sub-tests+###++sub test_handle+{+ my $io_method = shift;+ my $node = shift;++ my $psql = $node->background_psql('postgres', on_error_stop => 0);++ # leak warning: implicit xact+ psql_like(+ $io_method,+ $psql,+ "handle_get() leak in implicit xact",+ qq(SELECT handle_get()),+ qr/^$/,+ qr/leaked AIO handle/,+ "$io_method: leaky handle_get() warns");++ # leak warning: explicit xact+ psql_like(+ $io_method, $psql,+ "handle_get() leak in explicit xact",+ qq(BEGIN; SELECT handle_get(); COMMIT),+ qr/^$/, qr/leaked AIO handle/);+++ # leak warning: explicit xact, rollback+ psql_like(+ $io_method,+ $psql,+ "handle_get() leak in explicit xact, rollback",+ qq(BEGIN; SELECT handle_get(); ROLLBACK;),+ qr/^$/,+ qr/leaked AIO handle/);++ # leak warning: subtrans+ psql_like(+ $io_method,+ $psql,+ "handle_get() leak in subxact",+ qq(BEGIN; SAVEPOINT foo; SELECT handle_get(); COMMIT;),+ qr/^$/,+ qr/leaked AIO handle/);++ # leak warning + error: released in different command (thus resowner)+ psql_like(+ $io_method,+ $psql,+ "handle_release() in different command",+ qq(BEGIN; SELECT handle_get(); SELECT handle_release_last(); COMMIT;),+ qr/^$/,+ qr/leaked AIO handle.*release in unexpected state/ms);++ # no leak, release in same command+ psql_like(+ $io_method,+ $psql,+ "handle_release() in same command",+ qq(BEGIN; SELECT handle_get() UNION ALL SELECT handle_release_last(); COMMIT;),+ qr/^$/,+ qr/^$/);++ # normal handle use+ psql_like($io_method, $psql, "handle_get_release()",+ qq(SELECT handle_get_release()),+ qr/^$/, qr/^$/);++ # should error out, API violation+ psql_like($io_method, $psql, "handle_get_twice()",+ qq(SELECT handle_get_release()),+ qr/^$/, qr/^$/);++ # recover after error in implicit xact+ psql_like(+ $io_method,+ $psql,+ "handle error recovery in implicit xact",+ qq(SELECT handle_get_and_error(); SELECT 'ok', handle_get_release()),+ qr/^|ok$/,+ qr/ERROR.*as you command/);++ # recover after error in implicit xact+ psql_like(+ $io_method,+ $psql,+ "handle error recovery in explicit xact",+ qq(BEGIN; SELECT handle_get_and_error(); SELECT handle_get_release(), 'ok'; COMMIT;),+ qr/^|ok$/,+ qr/ERROR.*as you command/);++ # recover after error in subtrans+ psql_like(+ $io_method,+ $psql,+ "handle error recovery in explicit subxact",+ qq(BEGIN; SAVEPOINT foo; SELECT handle_get_and_error(); ROLLBACK TO SAVEPOINT foo; SELECT handle_get_release(); ROLLBACK;),+ qr/^|ok$/,+ qr/ERROR.*as you command/);++ $psql->quit();+}+++sub test_batch+{+ my $io_method = shift;+ my $node = shift;++ my $psql = $node->background_psql('postgres', on_error_stop => 0);++ # leak warning & recovery: implicit xact+ psql_like(+ $io_method,+ $psql,+ "batch_start() leak & cleanup in implicit xact",+ qq(SELECT batch_start()),+ qr/^$/,+ qr/open AIO batch at end/,+ "$io_method: leaky batch_start() warns");++ # leak warning & recovery: explicit xact+ psql_like(+ $io_method,+ $psql,+ "batch_start() leak & cleanup in explicit xact",+ qq(BEGIN; SELECT batch_start(); COMMIT;),+ qr/^$/,+ qr/open AIO batch at end/,+ "$io_method: leaky batch_start() warns");+++ # leak warning & recovery: explicit xact, rollback+ #+ # XXX: This doesn't fail right now, due to not getting a chance to do+ # something at transaction command commit. That's not a correctness issue,+ # it just means it's a bit harder to find buggy code.+ #psql_like($io_method, $psql,+ # "batch_start() leak & cleanup after abort",+ # qq(BEGIN; SELECT batch_start(); ROLLBACK;),+ # qr/^$/,+ # qr/open AIO batch at end/, "$io_method: leaky batch_start() warns");++ # no warning, batch closed in same command+ psql_like(+ $io_method,+ $psql,+ "batch_start(), batch_end() works",+ qq(SELECT batch_start() UNION ALL SELECT batch_end()),+ qr/^$/,+ qr/^$/,+ "$io_method: batch_start(), batch_end()");++ $psql->quit();+}++sub test_io_error+{+ my $io_method = shift;+ my $node = shift;+ my ($ret, $output);++ my $psql = $node->background_psql('postgres', on_error_stop => 0);++ # verify the error is reported in custom C code+ ($output, $ret) = $psql->query(+ qq(SELECT read_rel_block_ll('tbl_corr', 1, wait_complete=>true);));+ is($ret, 1, "$io_method: read_rel_block_ll() of tbl_corr page fails");+ like(+ $psql->{stderr},+ qr/invalid page in block 1 of relation base\/.*/,+ "$io_method: read_rel_block_ll() of tbl_corr page reports error");+ $psql->{stderr} = '';++ # verify the error is reported for bufmgr reads+ ($output, $ret) =+ $psql->query(qq(SELECT count(*) FROM tbl_corr WHERE ctid = '(1, 1)'));+ is($ret, 1, "$io_method: tid scan reading tbl_corr block fails");+ like(+ $psql->{stderr},+ qr/invalid page in block 1 of relation base\/.*/,+ "$io_method: tid scan reading tbl_corr block reports error");+ $psql->{stderr} = '';++ # verify the error is reported for bufmgr reads+ ($output, $ret) =+ $psql->query(qq(SELECT count(*) FROM tbl_corr WHERE ctid = '(1, 1)'));+ is($ret, 1, "$io_method: sequential scan reading tbl_corr block fails");+ like(+ $psql->{stderr},+ qr/invalid page in block 1 of relation base\/.*/,+ "$io_method: sequential scan reading tbl_corr block reports error");+ $psql->{stderr} = '';++ $psql->quit();+}++# Test interplay between StartBufferIO and TerminateBufferIO+sub test_startwait_io+{+ my $io_method = shift;+ my $node = shift;+ my ($ret, $output);++ my $psql_a = $node->background_psql('postgres', on_error_stop => 0);+ my $psql_b = $node->background_psql('postgres', on_error_stop => 0);++ # create a buffer we can play around with+ ($output, $ret) =+ $psql_a->query(qq(SELECT buffer_create_toy('tbl_ok', 1);));+ is($ret, 0, "$io_method: create toy succeeds");+ like($output, qr/^\d+$/, "$io_method: create toy returns numeric");+ my $buf_id = $output;++ # check that one backend can perform StartBufferIO+ psql_like(+ $io_method,+ $psql_a,+ "first StartBufferIO",+ qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>false);),+ qr/^t$/,+ qr/^$/);++ # but not twice on the same buffer (non-waiting)+ psql_like(+ $io_method,+ $psql_a,+ "second StartBufferIO fails, same session",+ qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>true);),+ qr/^f$/,+ qr/^$/);+ psql_like(+ $io_method,+ $psql_b,+ "second StartBufferIO fails, other session",+ qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>true);),+ qr/^f$/,+ qr/^$/);++ # start io in a different session, will block+ query_wait_block(+ $io_method,+ $node,+ $psql_b,+ "blocking start buffer io",+ qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>false);),+ "BufferIo");++ # Terminate the IO, without marking it as success, this should trigger the+ # waiting session to be able to start the io+ ($output, $ret) = $psql_a->query(+ qq(SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>false, io_error=>false, syncio=>true);)+ );+ is($ret, 0,+ "$io_method: blocking start buffer io, terminating io, not valid");++ # Because the IO was terminated, but not marked as valid, second session should get the right to start io+ pump_until($psql_b->{run}, $psql_b->{timeout}, \$psql_b->{stdout}, qr/t/);+ ok(1, "$io_method: blocking start buffer io, can start io");++ # terminate the IO again+ $psql_b->query_safe(+ qq(SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>false, io_error=>false, syncio=>true);)+ );+++ # same as the above scenario, but mark IO as having succeeded+ ($output, $ret) =+ $psql_a->query(qq(SELECT buffer_call_start_io($buf_id, true, false);));+ is($ret, 0,+ "$io_method: blocking buffer io w/ success: first start buffer io succeeds"+ );+ is($output, "t",+ "$io_method: blocking buffer io w/ success: first start buffer io returns true"+ );+++ # start io in a different session, will block+ query_wait_block(+ $io_method,+ $node,+ $psql_b,+ "blocking start buffer io",+ qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>false);),+ "BufferIo");++ ($output, $ret) = $psql_a->query(+ qq(SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>true, io_error=>false, syncio=>true);)+ );+ is($ret, 0,+ "$io_method: blocking start buffer io, terminating IO, valid");++ # Because the IO was terminated, and marked as valid, second session should complete but not need io+ pump_until($psql_b->{run}, $psql_b->{timeout}, \$psql_b->{stdout}, qr/f/);+ ok(1, "$io_method: blocking start buffer io, no need to start io");+++ # buffer is valid now, make it invalid again+ $buf_id = $psql_a->query_safe(qq(SELECT buffer_create_toy('tbl_ok', 1);));++ $psql_a->quit();+ $psql_b->quit();+}++# Test that if the backend issuing a read doesn't wait for the IO's+# completion, another backend can complete the IO+sub test_complete_foreign+{+ my $io_method = shift;+ my $node = shift;+ my ($ret, $output);++ my $psql_a = $node->background_psql('postgres', on_error_stop => 0);+ my $psql_b = $node->background_psql('postgres', on_error_stop => 0);++ # Issue IO without waiting for completion, then sleep+ $psql_a->query_safe(+ qq(SELECT read_rel_block_ll('tbl_ok', 1, wait_complete=>false);));++ # Check that another backend can read the relevant block+ psql_like(+ $io_method,+ $psql_b,+ "completing read started by sleeping backend",+ qq(SELECT count(*) FROM tbl_ok WHERE ctid = '(1,1)' LIMIT 1),+ qr/^1$/,+ qr/^$/);++ # Issue IO without waiting for completion, then exit+ $psql_a->query_safe(+ qq(SELECT read_rel_block_ll('tbl_ok', 1, wait_complete=>false);));+ $psql_a->reconnect_and_clear();++ # Check that another backend can read the relevant block+ psql_like(+ $io_method,+ $psql_b,+ "completing read started by exited backend",+ qq(SELECT count(*) FROM tbl_ok WHERE ctid = '(1,1)' LIMIT 1),+ qr/^1$/,+ qr/^$/);++ # Read a tbl_corr block, then sleep. The other session will retry the IO+ # and also fail. The easiest thing to verify that seems to be to check+ # that both are in the log.+ my $log_location = -s $node->logfile;+ $psql_a->query_safe(+ qq(SELECT read_rel_block_ll('tbl_corr', 1, wait_complete=>false);));++ psql_like(+ $io_method,+ $psql_b,+ "completing read of tbl_corr block started by other backend",+ qq(SELECT count(*) FROM tbl_corr WHERE ctid = '(1,1)' LIMIT 1),+ qr/^$/,+ qr/invalid page in block/);++ # The log message issued for the read_rel_block_ll() should be logged as a LOG+ $node->wait_for_log(qr/LOG[^\n]+invalid page in/, $log_location);+ ok(1,+ "$io_method: completing read of tbl_corr block started by other backend: LOG message for background read"+ );++ # But for the SELECT, it should be an ERROR+ $log_location =+ $node->wait_for_log(qr/ERROR[^\n]+invalid page in/, $log_location);+ ok(1,+ "$io_method: completing read of tbl_corr block started by other backend: ERROR message for foreground read"+ );++ $psql_a->quit();+ $psql_b->quit();+}++sub test_inject+{+ my $io_method = shift;+ my $node = shift;+ my ($ret, $output);++ my $psql = $node->background_psql('postgres', on_error_stop => 0);++ # injected what we'd expect+ $psql->query_safe(qq(SELECT inj_io_short_read_attach(8192);));+ $psql->query_safe(qq(SELECT invalidate_rel_block('tbl_ok', 2);));+ ($output, $ret) =+ $psql->query(qq(SELECT count(*) FROM tbl_ok WHERE ctid = '(2, 1)';));+ is($ret, 0,+ "$io_method: injection point not triggering failure succeeds");++ # injected a read shorter than a single block, expecting error+ $psql->query_safe(qq(SELECT inj_io_short_read_attach(17);));+ $psql->query_safe(qq(SELECT invalidate_rel_block('tbl_ok', 2);));+ ($output, $ret) =+ $psql->query(qq(SELECT count(*) FROM tbl_ok WHERE ctid = '(2, 1)';));+ is($ret, 1, "$io_method: single block short read fails");+ like(+ $psql->{stderr},+ qr/ERROR:.*could not read blocks 2\.\.2 in file "base\/.*": read only 0 of 8192 bytes/,+ "$io_method: single block short read reports error");+ $psql->{stderr} = '';++ # shorten multi-block read to a single block, should retry+ my $inval_query = qq(SELECT invalidate_rel_block('tbl_ok', 0);+SELECT invalidate_rel_block('tbl_ok', 1);+SELECT invalidate_rel_block('tbl_ok', 2);+SELECT invalidate_rel_block('tbl_ok', 3);+/* gap */+SELECT invalidate_rel_block('tbl_ok', 5);+SELECT invalidate_rel_block('tbl_ok', 6);+SELECT invalidate_rel_block('tbl_ok', 7);+SELECT invalidate_rel_block('tbl_ok', 8););++ $psql->query_safe($inval_query);+ $psql->query_safe(qq(SELECT inj_io_short_read_attach(8192);));+ ($output, $ret) = $psql->query(qq(SELECT count(*) FROM tbl_ok;));+ is($ret, 0, "$io_method: multi block short read (1 block) is retried");+ is($output, 10000, "$io_method: multi block short read (1 block) has correct result");++ # shorten multi-block read to two blocks, should retry+ $psql->query_safe($inval_query);+ $psql->query_safe(qq(SELECT inj_io_short_read_attach(8192*2);));++ ($output, $ret) = $psql->query(qq(SELECT count(*) FROM tbl_ok;));+ is($ret, 0, "$io_method: multi block short read (2 blocks) is retried");+ is($output, 10000, "$io_method: multi block short read (2 blocks) has correct result");++ # verify that page verification errors are detected even as part of a+ # shortened multi-block read (tbl_corr, block 1 is tbl_corred)+ $psql->query_safe(+ qq(+SELECT invalidate_rel_block('tbl_corr', 0);+SELECT invalidate_rel_block('tbl_corr', 1);+SELECT invalidate_rel_block('tbl_corr', 2);+SELECT inj_io_short_read_attach(8192);+ ));+ ($output, $ret) =+ $psql->query(qq(SELECT count(*) FROM tbl_corr WHERE ctid < '(2, 1)'));+ is($ret, 1,+ "$io_method: shortened multi-block read detects invalid page");+ like(+ $psql->{stderr},+ qr/ERROR:.*invalid page in block 1 of relation base\/.*/,+ "$io_method: shortened multi-block reads reports invalid page");+ $psql->{stderr} = '';++ # trigger a hard error, should error out+ $psql->query_safe(+ qq(+SELECT inj_io_short_read_attach(-errno_from_string('EIO'));+SELECT invalidate_rel_block('tbl_ok', 2);+ ));+ ($output, $ret) =+ $psql->query(qq(SELECT count(*) FROM tbl_ok;));+ is($ret, 1, "$io_method: first hard IO error is detected");+ like(+ $psql->{stderr},+ qr/ERROR:.*could not read blocks 2\.\.2 in file \"base\/.*\": Input\/output error/,+ "$io_method: first hard IO error is reported");+ $psql->{stderr} = '';++ # trigger a second hard error, should error out again+ ($output, $ret) =+ $psql->query(qq(SELECT count(*) FROM tbl_ok;));+ is($ret, 1, "$io_method: second hard IO error is detected");+ like(+ $psql->{stderr},+ qr/ERROR:.*could not read blocks 2\.\.2 in file \"base\/.*\": Input\/output error/,+ "$io_method: second hard IO error is reported");+ $psql->{stderr} = '';++ $psql->query_safe(+ qq(+SELECT inj_io_short_read_detach();+ ));++ # now the IO should be ok.+ ($output, $ret) =+ $psql->query(qq(SELECT count(*) FROM tbl_ok;));+ is($ret, 0, "$io_method: recovers after hard error");+ is($output, 10000, "$io_method: recovers after hard error, query result ok");++ $psql->quit();+}++sub test_inject_worker+{+ my $io_method = shift;+ my $node = shift;+ my ($ret, $output);++ my $psql = $node->background_psql('postgres', on_error_stop => 0);++ # trigger a failure to reopen, should error out, but should recover+ $psql->query_safe(+ qq(+SELECT inj_io_reopen_attach();+SELECT invalidate_rel_block('tbl_ok', 1);+ ));+ ($output, $ret) = $psql->query(qq(SELECT count(*) FROM tbl_ok;));+ is($ret, 1, "$io_method: failure to open: detected");+ like(+ $psql->{stderr},+ qr/ERROR:.*could not read blocks 1\.\.1 in file "base\/.*": No such file or directory/,+ "$io_method: failure to open is reported");+ $psql->{stderr} = '';++ $psql->query_safe(+ qq(+SELECT inj_io_reopen_detach();+ ));++ # check that we indeed recover+ ($output, $ret) = $psql->query(qq(SELECT count(*) FROM tbl_ok;));+ is($ret, 0, "$io_method: failure to open: recovers");+ is($output, 10000, "$io_method: failure to open: next query ok");++ $psql->quit();+}++sub test_generic+{+ my $io_method = shift;+ my $node = shift;++ is($node->safe_psql('postgres', 'SHOW io_method'),+ $io_method, "$io_method: io_method set correctly");++ $node->safe_psql(+ 'postgres', qq(+CREATE EXTENSION test_aio;+CREATE TABLE tbl_corr(data int not null) WITH (AUTOVACUUM_ENABLED = false);+CREATE TABLE tbl_ok(data int not null) WITH (AUTOVACUUM_ENABLED = false);++INSERT INTO tbl_corr SELECT generate_series(1, 10000);+INSERT INTO tbl_ok SELECT generate_series(1, 10000);+SELECT grow_rel('tbl_corr', 16);+SELECT grow_rel('tbl_ok', 16);++SELECT corrupt_rel_block('tbl_corr', 1);+CHECKPOINT;+));++ test_handle($io_method, $node);+ test_io_error($io_method, $node);+ test_batch($io_method, $node);+ test_startwait_io($io_method, $node);+ test_complete_foreign($io_method, $node);++ SKIP:+ {+ skip 'Injection points not supported by this build', 1+ unless $ENV{enable_injection_points} eq 'yes';+ test_inject($io_method, $node);+ }+}diff --git a/src/test/modules/test_aio/test_aio--1.0.sql b/src/test/modules/test_aio/test_aio--1.0.sqlnew file mode 100644index 00000000000..3d4e9bb070f--- /dev/null+++ b/src/test/modules/test_aio/test_aio--1.0.sql@@ -0,0 +1,97 @@+/* src/test/modules/test_aio/test_aio--1.0.sql */++-- complain if script is sourced in psql, rather than via CREATE EXTENSION+\echo Use "CREATE EXTENSION test_aio" to load this file. \quit+++CREATE FUNCTION errno_from_string(sym text)+RETURNS pg_catalog.int4 STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;+++CREATE FUNCTION grow_rel(rel regclass, nblocks int)+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;+++CREATE FUNCTION corrupt_rel_block(rel regclass, blockno int)+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION read_rel_block_ll(rel regclass, blockno int, wait_complete bool)+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION invalidate_rel_block(rel regclass, blockno int)+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION buffer_create_toy(rel regclass, blockno int4)+RETURNS pg_catalog.int4 STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION buffer_call_start_io(buffer int, for_input bool, nowait bool)+RETURNS pg_catalog.bool STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION buffer_call_terminate_io(buffer int, for_input bool, succeed bool, io_error bool, syncio bool)+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++++/*+ * Handle related functions+ */+CREATE FUNCTION handle_get_and_error()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION handle_get_twice()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION handle_get()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION handle_get_release()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION handle_release_last()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;+++/*+ * Batchmode related functions+ */+CREATE FUNCTION batch_start()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION batch_end()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++++/*+ * Injection point related functions+ */+CREATE FUNCTION inj_io_short_read_attach(result int)+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION inj_io_short_read_detach()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION inj_io_reopen_attach()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION inj_io_reopen_detach()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.cnew file mode 100644index 00000000000..fc55b985dad--- /dev/null+++ b/src/test/modules/test_aio/test_aio.c@@ -0,0 +1,657 @@+/*-------------------------------------------------------------------------+ *+ * delay_execution.c+ * Test module to allow delay between parsing and execution of a query.+ *+ * The delay is implemented by taking and immediately releasing a specified+ * advisory lock. If another process has previously taken that lock, the+ * current process will be blocked until the lock is released; otherwise,+ * there's no effect. This allows an isolationtester script to reliably+ * test behaviors where some specified action happens in another backend+ * between parsing and execution of any desired query.+ *+ * Copyright (c) 2020-2025, PostgreSQL Global Development Group+ *+ * IDENTIFICATION+ * src/test/modules/delay_execution/delay_execution.c+ *+ *-------------------------------------------------------------------------+ */++#include "postgres.h"++#include "access/relation.h"+#include "fmgr.h"+#include "storage/aio.h"+#include "storage/aio_internal.h"+#include "storage/buf_internals.h"+#include "storage/bufmgr.h"+#include "storage/ipc.h"+#include "storage/lwlock.h"+#include "utils/builtins.h"+#include "utils/injection_point.h"+#include "utils/rel.h"+++PG_MODULE_MAGIC;+++typedef struct InjIoErrorState+{+ bool enabled_short_read;+ bool enabled_reopen;++ bool short_read_result_set;+ int short_read_result;+} InjIoErrorState;++static InjIoErrorState * inj_io_error_state;++/* Shared memory init callbacks */+static shmem_request_hook_type prev_shmem_request_hook = NULL;+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;+++static PgAioHandle *last_handle;++++static void+test_aio_shmem_request(void)+{+ if (prev_shmem_request_hook)+ prev_shmem_request_hook();++ RequestAddinShmemSpace(sizeof(InjIoErrorState));+}++static void+test_aio_shmem_startup(void)+{+ bool found;++ if (prev_shmem_startup_hook)+ prev_shmem_startup_hook();++ /* Create or attach to the shared memory state */+ LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);++ inj_io_error_state = ShmemInitStruct("injection_points",+ sizeof(InjIoErrorState),+ &found);++ if (!found)+ {+ /*+ * First time through, so initialize. This is shared with the dynamic+ * initialization using a DSM.+ */+ inj_io_error_state->enabled_short_read = false;+ inj_io_error_state->enabled_reopen = false;++#ifdef USE_INJECTION_POINTS+ InjectionPointAttach("AIO_PROCESS_COMPLETION_BEFORE_SHARED",+ "test_aio",+ "inj_io_short_read",+ NULL,+ 0);+ InjectionPointLoad("AIO_PROCESS_COMPLETION_BEFORE_SHARED");++ InjectionPointAttach("AIO_WORKER_AFTER_REOPEN",+ "test_aio",+ "inj_io_reopen",+ NULL,+ 0);+ InjectionPointLoad("AIO_WORKER_AFTER_REOPEN");++#endif+ }+ else+ {+#ifdef USE_INJECTION_POINTS+ InjectionPointLoad("AIO_PROCESS_COMPLETION_BEFORE_SHARED");+ InjectionPointLoad("AIO_WORKER_AFTER_REOPEN");+ elog(LOG, "injection point loaded");+#endif+ }++ LWLockRelease(AddinShmemInitLock);+}++void+_PG_init(void)+{+ if (!process_shared_preload_libraries_in_progress)+ return;++ /* Shared memory initialization */+ prev_shmem_request_hook = shmem_request_hook;+ shmem_request_hook = test_aio_shmem_request;+ prev_shmem_startup_hook = shmem_startup_hook;+ shmem_startup_hook = test_aio_shmem_startup;+}+++PG_FUNCTION_INFO_V1(errno_from_string);+Datum+errno_from_string(PG_FUNCTION_ARGS)+{+ const char *sym = text_to_cstring(PG_GETARG_TEXT_PP(0));++ if (strcmp(sym, "EIO") == 0)+ PG_RETURN_INT32(EIO);+ else if (strcmp(sym, "EAGAIN") == 0)+ PG_RETURN_INT32(EAGAIN);+ else if (strcmp(sym, "EINTR") == 0)+ PG_RETURN_INT32(EINTR);+ else if (strcmp(sym, "ENOSPC") == 0)+ PG_RETURN_INT32(ENOSPC);+ else if (strcmp(sym, "EROFS") == 0)+ PG_RETURN_INT32(EROFS);++ ereport(ERROR,+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),+ errmsg_internal("%s is not a supported errno value", sym));+ PG_RETURN_INT32(0);+}+++PG_FUNCTION_INFO_V1(grow_rel);+Datum+grow_rel(PG_FUNCTION_ARGS)+{+ Oid relid = PG_GETARG_OID(0);+ uint32 nblocks = PG_GETARG_UINT32(1);+ Relation rel;+#define MAX_BUFFERS_TO_EXTEND_BY 64+ Buffer victim_buffers[MAX_BUFFERS_TO_EXTEND_BY];++ rel = relation_open(relid, AccessExclusiveLock);++ while (nblocks > 0)+ {+ uint32 extend_by_pages;++ extend_by_pages = Min(nblocks, MAX_BUFFERS_TO_EXTEND_BY);++ ExtendBufferedRelBy(BMR_REL(rel),+ MAIN_FORKNUM,+ NULL,+ 0,+ extend_by_pages,+ victim_buffers,+ &extend_by_pages);++ nblocks -= extend_by_pages;++ for (uint32 i = 0; i < extend_by_pages; i++)+ {+ ReleaseBuffer(victim_buffers[i]);+ }+ }++ relation_close(rel, NoLock);++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(corrupt_rel_block);+Datum+corrupt_rel_block(PG_FUNCTION_ARGS)+{+ Oid relid = PG_GETARG_OID(0);+ BlockNumber blkno = PG_GETARG_UINT32(1);+ Relation rel;+ Buffer buf;+ Page page;+ PageHeader ph;++ rel = relation_open(relid, AccessExclusiveLock);++ buf = ReadBuffer(rel, blkno);+ page = BufferGetPage(buf);++ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);++ MarkBufferDirty(buf);++ PageInit(page, BufferGetPageSize(buf), 0);++ ph = (PageHeader) page;+ ph->pd_special = BLCKSZ + 1;++ FlushOneBuffer(buf);++ LockBuffer(buf, BUFFER_LOCK_UNLOCK);++ ReleaseBuffer(buf);++ EvictUnpinnedBuffer(buf);++ relation_close(rel, NoLock);++ PG_RETURN_VOID();+}++/*+ * Ensures a buffer for rel & blkno is in shared buffers, without actually+ * caring about the buffer contents. Used to set up test scenarios.+ */+static Buffer+create_toy_buffer(Relation rel, BlockNumber blkno)+{+ Buffer buf;+ BufferDesc *buf_hdr;+ uint32 buf_state;+ bool was_pinned = false;++ /* place buffer in shared buffers without erroring out */+ buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK, NULL);+ LockBuffer(buf, BUFFER_LOCK_UNLOCK);++ buf_hdr = GetBufferDescriptor(buf - 1);++ buf_state = LockBufHdr(buf_hdr);++ /*+ * We should be the only backend accessing this buffer. This is just a+ * small bit of belt-and-suspenders defense, none of this code should ever+ * run in a cluster with real data.+ */+ if (BUF_STATE_GET_REFCOUNT(buf_state) > 1)+ was_pinned = true;+ else+ buf_state &= ~(BM_VALID | BM_DIRTY);++ UnlockBufHdr(buf_hdr, buf_state);++ if (was_pinned)+ elog(ERROR, "toy buffer %d was already pinned",+ buf);++ return buf;+}++PG_FUNCTION_INFO_V1(read_rel_block_ll);+Datum+read_rel_block_ll(PG_FUNCTION_ARGS)+{+ Oid relid = PG_GETARG_OID(0);+ BlockNumber blkno = PG_GETARG_UINT32(1);+ bool wait_complete = PG_GETARG_BOOL(2);+ Relation rel;+ Buffer buf;+ Page pages[1];+ PgAioReturn ior;+ PgAioHandle *ioh;+ PgAioWaitRef iow;+ SMgrRelation smgr;++ rel = relation_open(relid, AccessExclusiveLock);++ buf = create_toy_buffer(rel, blkno);++ pages[0] = BufferGetBlock(buf);++ ioh = pgaio_io_acquire(CurrentResourceOwner, &ior);+ pgaio_io_get_wref(ioh, &iow);++ smgr = RelationGetSmgr(rel);++ StartBufferIO(GetBufferDescriptor(buf - 1), true, false);++ pgaio_io_set_handle_data_32(ioh, (uint32 *) &buf, 1);+ pgaio_io_register_callbacks(ioh, PGAIO_HCB_SHARED_BUFFER_READV, 0);++ elog(LOG, "about to smgrstartreadv");+ smgrstartreadv(ioh, smgr, MAIN_FORKNUM, blkno,+ (void *) pages, 1);++ ReleaseBuffer(buf);++ if (wait_complete)+ {+ pgaio_wref_wait(&iow);++ if (ior.result.status != PGAIO_RS_OK)+ pgaio_result_report(ior.result, &ior.target_data,+ ior.result.status == PGAIO_RS_PARTIAL ? WARNING : ERROR);+ }++ relation_close(rel, NoLock);++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(invalidate_rel_block);+Datum+invalidate_rel_block(PG_FUNCTION_ARGS)+{+ Oid relid = PG_GETARG_OID(0);+ BlockNumber blkno = PG_GETARG_UINT32(1);+ Relation rel;+ PrefetchBufferResult pr;+ Buffer buf;++ rel = relation_open(relid, AccessExclusiveLock);++ /*+ * This is a gross hack, but there's no other API exposed that allows to+ * get a buffer ID without actually reading the block in.+ */+ pr = PrefetchBuffer(rel, MAIN_FORKNUM, blkno);+ buf = pr.recent_buffer;++ if (BufferIsValid(buf))+ {+ /* if the buffer contents aren't valid, this'll return false */+ if (ReadRecentBuffer(rel->rd_locator, MAIN_FORKNUM, blkno, buf))+ {+ LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);+ FlushOneBuffer(buf);+ LockBuffer(buf, BUFFER_LOCK_UNLOCK);+ ReleaseBuffer(buf);++ if (!EvictUnpinnedBuffer(buf))+ elog(ERROR, "couldn't evict");+ }+ }++ relation_close(rel, AccessExclusiveLock);++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(buffer_create_toy);+Datum+buffer_create_toy(PG_FUNCTION_ARGS)+{+ Oid relid = PG_GETARG_OID(0);+ BlockNumber blkno = PG_GETARG_UINT32(1);+ Relation rel;+ Buffer buf;++ rel = relation_open(relid, AccessExclusiveLock);++ buf = create_toy_buffer(rel, blkno);+ ReleaseBuffer(buf);++ relation_close(rel, NoLock);++ PG_RETURN_INT32(buf);+}++PG_FUNCTION_INFO_V1(buffer_call_start_io);+Datum+buffer_call_start_io(PG_FUNCTION_ARGS)+{+ Buffer buf = PG_GETARG_INT32(0);+ bool for_input = PG_GETARG_BOOL(1);+ bool nowait = PG_GETARG_BOOL(2);+ bool can_start;++ can_start = StartBufferIO(GetBufferDescriptor(buf - 1), for_input, nowait);++ /*+ * Tor tests we don't want the resowner release preventing us from+ * orchestrating odd scenarios.+ */+ if (can_start)+ ResourceOwnerForgetBufferIO(CurrentResourceOwner,+ buf);++ ereport(LOG,+ errmsg("buffer %d after StartBufferIO: %s",+ buf, DebugPrintBufferRefcount(buf)),+ errhidestmt(true), errhidecontext(true));++ PG_RETURN_BOOL(can_start);+}++/*+CREATE FUNCTION buffer_call_terminate_io(buffer int, for_input bool, failed bool, syncio bool)+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;+*/+PG_FUNCTION_INFO_V1(buffer_call_terminate_io);+Datum+buffer_call_terminate_io(PG_FUNCTION_ARGS)+{+ Buffer buf = PG_GETARG_INT32(0);+ bool for_input = PG_GETARG_BOOL(1);+ bool succeed = PG_GETARG_BOOL(2);+ bool io_error = PG_GETARG_BOOL(3);+ bool syncio = PG_GETARG_BOOL(4);+ bool clear_dirty = false;+ uint32 set_flag_bits = 0;++ if (io_error)+ set_flag_bits |= BM_IO_ERROR;++ if (for_input)+ {+ clear_dirty = false;++ if (succeed)+ set_flag_bits |= BM_VALID;+ }+ else+ {+ if (succeed)+ clear_dirty = true;+ }++ ereport(LOG,+ errmsg("buffer %d before TerminateBufferIO: %s",+ buf, DebugPrintBufferRefcount(buf)),+ errhidestmt(true), errhidecontext(true));++ TerminateBufferIO(GetBufferDescriptor(buf - 1), clear_dirty, set_flag_bits,+ false, syncio);++ ereport(LOG,+ errmsg("buffer %d after TerminateBufferIO: %s",+ buf, DebugPrintBufferRefcount(buf)),+ errhidestmt(true), errhidecontext(true));++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(handle_get);+Datum+handle_get(PG_FUNCTION_ARGS)+{+ last_handle = pgaio_io_acquire(CurrentResourceOwner, NULL);++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(handle_release_last);+Datum+handle_release_last(PG_FUNCTION_ARGS)+{+ if (!last_handle)+ elog(ERROR, "no handle");++ pgaio_io_release(last_handle);++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(handle_get_and_error);+Datum+handle_get_and_error(PG_FUNCTION_ARGS)+{+ pgaio_io_acquire(CurrentResourceOwner, NULL);++ elog(ERROR, "as you command");+ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(handle_get_twice);+Datum+handle_get_twice(PG_FUNCTION_ARGS)+{+ pgaio_io_acquire(CurrentResourceOwner, NULL);+ pgaio_io_acquire(CurrentResourceOwner, NULL);++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(handle_get_release);+Datum+handle_get_release(PG_FUNCTION_ARGS)+{+ PgAioHandle *handle;++ handle = pgaio_io_acquire(CurrentResourceOwner, NULL);+ pgaio_io_release(handle);++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(batch_start);+Datum+batch_start(PG_FUNCTION_ARGS)+{+ pgaio_enter_batchmode();+ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(batch_end);+Datum+batch_end(PG_FUNCTION_ARGS)+{+ pgaio_exit_batchmode();+ PG_RETURN_VOID();+}++#ifdef USE_INJECTION_POINTS+extern PGDLLEXPORT void inj_io_short_read(const char *name, const void *private_data);+extern PGDLLEXPORT void inj_io_reopen(const char *name, const void *private_data);++void+inj_io_short_read(const char *name, const void *private_data)+{+ PgAioHandle *ioh;++ elog(LOG, "short read called: %d", inj_io_error_state->enabled_short_read);++ if (inj_io_error_state->enabled_short_read)+ {+ ioh = pgaio_inj_io_get();++ /*+ * Only shorten reads that are actually longer than the target size,+ * otherwise we can trigger over-reads.+ */+ if (inj_io_error_state->short_read_result_set+ && ioh->op == PGAIO_OP_READV+ && inj_io_error_state->short_read_result <= ioh->result)+ {+ struct iovec *iov = &pgaio_ctl->iovecs[ioh->iovec_off];+ int32 old_result = ioh->result;+ int32 new_result = inj_io_error_state->short_read_result;+ int32 processed = 0;++ ereport(LOG,+ errmsg("short read, changing result from %d to %d",+ old_result, new_result),+ errhidestmt(true), errhidecontext(true));++ /*+ * The underlying IO actually completed OK, and thus the "invalid"+ * portion of the IOV actually contains valid data. That can hide+ * a lot of problems, e.g. if we were to wrongly mark a buffer,+ * that wasn't read according to the shortened-read, IO as valid,+ * the contents would look valid and we might miss a bug.+ *+ * To avoid that, iterate through the IOV and zero out the+ * "failed" portion of the IO.+ */+ for (int i = 0; i < ioh->op_data.read.iov_length; i++)+ {+ if (processed + iov[i].iov_len <= new_result)+ processed += iov[i].iov_len;+ else if (processed <= new_result)+ {+ uint32 ok_part = new_result - processed;++ memset((char *) iov[i].iov_base + ok_part, 0, iov[i].iov_len - ok_part);+ processed += iov[i].iov_len;+ }+ else+ {+ memset((char *) iov[i].iov_base, 0, iov[i].iov_len);+ }+ }++ ioh->result = new_result;+ }+ }+}++void+inj_io_reopen(const char *name, const void *private_data)+{+ elog(LOG, "reopen called: %d", inj_io_error_state->enabled_reopen);++ if (inj_io_error_state->enabled_reopen)+ {+ elog(ERROR, "injection point triggering failure to reopen ");+ }+}+#endif++PG_FUNCTION_INFO_V1(inj_io_short_read_attach);+Datum+inj_io_short_read_attach(PG_FUNCTION_ARGS)+{+#ifdef USE_INJECTION_POINTS+ inj_io_error_state->enabled_short_read = true;+ inj_io_error_state->short_read_result_set = !PG_ARGISNULL(0);+ if (inj_io_error_state->short_read_result_set)+ inj_io_error_state->short_read_result = PG_GETARG_INT32(0);+#else+ elog(ERROR, "injection points not supported");+#endif++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(inj_io_short_read_detach);+Datum+inj_io_short_read_detach(PG_FUNCTION_ARGS)+{+#ifdef USE_INJECTION_POINTS+ inj_io_error_state->enabled_short_read = false;+#else+ elog(ERROR, "injection points not supported");+#endif+ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(inj_io_reopen_attach);+Datum+inj_io_reopen_attach(PG_FUNCTION_ARGS)+{+#ifdef USE_INJECTION_POINTS+ inj_io_error_state->enabled_reopen = true;+#else+ elog(ERROR, "injection points not supported");+#endif++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(inj_io_reopen_detach);+Datum+inj_io_reopen_detach(PG_FUNCTION_ARGS)+{+#ifdef USE_INJECTION_POINTS+ inj_io_error_state->enabled_reopen = false;+#else+ elog(ERROR, "injection points not supported");+#endif+ PG_RETURN_VOID();+}diff --git a/src/test/modules/test_aio/test_aio.control b/src/test/modules/test_aio/test_aio.controlnew file mode 100644index 00000000000..cd91c3ed16b--- /dev/null+++ b/src/test/modules/test_aio/test_aio.control@@ -0,0 +1,3 @@+comment = 'Test code for AIO'+default_version = '1.0'+module_pathname = '$libdir/test_aio'--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0018-aio-Experimental-heuristics-to-increase-batchi.patch (1.6K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/19-v2.11-0018-aio-Experimental-heuristics-to-increase-batchi.patch)
download | inline diff:
From 564255d2458963965d29894f12b6a37dc20a37e1 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 18/27] aio: Experimental heuristics to increase batching
in read_stream.c
Author:
Reviewed-by:
Discussion: https://postgr.es/m/
Backpatch:
---
src/backend/storage/aio/read_stream.c | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.cindex 60a841816d8..facf996608e 100644--- a/src/backend/storage/aio/read_stream.c+++ b/src/backend/storage/aio/read_stream.c@@ -418,6 +418,30 @@ read_stream_start_pending_read(ReadStream *stream)
static void
read_stream_look_ahead(ReadStream *stream)
{
+ /*+ * Batch-submitting multiple IOs is more efficient than doing so+ * one-by-one. If we just ramp up to the max, we'll only be allowed to+ * submit one io_combine_limit sized IO. Defer submitting IO in that case.+ *+ * FIXME: This needs better heuristics.+ */+#if 1+ if (!stream->sync_mode && stream->distance > (io_combine_limit * 8))+ {+ if (stream->pinned_buffers + stream->pending_read_nblocks > ((stream->distance * 3) / 4))+ {+#if 0+ ereport(LOG,+ errmsg("reduce reduce reduce: pinned: %d, pending: %d, distance: %d",+ stream->pinned_buffers,+ stream->pending_read_nblocks,+ stream->distance));+#endif+ return;+ }+ }+#endif+
/*
* Allow amortizing the cost of submitting IO over multiple IOs. This
* requires that we don't do any operations that could lead to a deadlock
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0019-aio-Implement-smgr-md-fd-write-support.patch (13.3K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/20-v2.11-0019-aio-Implement-smgr-md-fd-write-support.patch)
download | inline diff:
From d9dc948108a99e8ee4052873f08b2ff10d9a0477 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 19/27] aio: Implement smgr/md/fd write support
TODO:
- register_dirty_segment_aio() can error out in edge cases---
src/include/storage/fd.h | 1 +
src/include/storage/md.h | 5 +
src/include/storage/smgr.h | 5 +
src/backend/storage/aio/aio_callback.c | 1 +
src/backend/storage/file/fd.c | 28 ++++
src/backend/storage/smgr/md.c | 199 +++++++++++++++++++++++++
src/backend/storage/smgr/smgr.c | 29 ++++
7 files changed, 268 insertions(+)
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.hindex b77d8e5e30e..2cc7c5a4761 100644--- a/src/include/storage/fd.h+++ b/src/include/storage/fd.h@@ -112,6 +112,7 @@ extern int FilePrefetch(File file, off_t offset, off_t amount, uint32 wait_event
extern ssize_t FileReadV(File file, const struct iovec *iov, int iovcnt, off_t offset, uint32 wait_event_info);
extern ssize_t FileWriteV(File file, const struct iovec *iov, int iovcnt, off_t offset, uint32 wait_event_info);
extern int FileStartReadV(struct PgAioHandle *ioh, File file, int iovcnt, off_t offset, uint32 wait_event_info);
+extern int FileStartWriteV(struct PgAioHandle *ioh, File file, int iovcnt, off_t offset, uint32 wait_event_info);
extern int FileSync(File file, uint32 wait_event_info);
extern int FileZero(File file, off_t offset, off_t amount, uint32 wait_event_info);
extern int FileFallocate(File file, off_t offset, off_t amount, uint32 wait_event_info);
diff --git a/src/include/storage/md.h b/src/include/storage/md.hindex 9d7131eff43..47ae6c36c94 100644--- a/src/include/storage/md.h+++ b/src/include/storage/md.h@@ -21,6 +21,7 @@
#include "storage/sync.h"
extern const PgAioHandleCallbacks aio_md_readv_cb;
+extern const PgAioHandleCallbacks aio_md_writev_cb;
/* md storage manager functionality */
extern void mdinit(void);
@@ -45,6 +46,10 @@ extern void mdstartreadv(PgAioHandle *ioh,
extern void mdwritev(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum,
const void **buffers, BlockNumber nblocks, bool skipFsync);
+extern void mdstartwritev(PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum,+ BlockNumber blocknum,+ const void **buffers, BlockNumber nblocks, bool skipFsync);
extern void mdwriteback(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, BlockNumber nblocks);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.hindex 856ebcda350..f00b3763ac9 100644--- a/src/include/storage/smgr.h+++ b/src/include/storage/smgr.h@@ -108,6 +108,11 @@ extern void smgrwritev(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum,
const void **buffers, BlockNumber nblocks,
bool skipFsync);
+extern void smgrstartwritev(PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum,+ BlockNumber blocknum,+ const void **buffers, BlockNumber nblocks,+ bool skipFsync);
extern void smgrwriteback(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, BlockNumber nblocks);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
diff --git a/src/backend/storage/aio/aio_callback.c b/src/backend/storage/aio/aio_callback.cindex 3cdd2a6b195..03e44b76075 100644--- a/src/backend/storage/aio/aio_callback.c+++ b/src/backend/storage/aio/aio_callback.c@@ -41,6 +41,7 @@ static const PgAioHandleCallbacksEntry aio_handle_cbs[] = {
CALLBACK_ENTRY(PGAIO_HCB_INVALID, aio_invalid_cb),
CALLBACK_ENTRY(PGAIO_HCB_MD_READV, aio_md_readv_cb),
+ CALLBACK_ENTRY(PGAIO_HCB_MD_WRITEV, aio_md_writev_cb),
CALLBACK_ENTRY(PGAIO_HCB_SHARED_BUFFER_READV, aio_shared_buffer_readv_cb),
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.cindex f573aa8e7ca..8f143c10d36 100644--- a/src/backend/storage/file/fd.c+++ b/src/backend/storage/file/fd.c@@ -2348,6 +2348,34 @@ retry:
return returnCode;
}
+int+FileStartWriteV(PgAioHandle *ioh, File file,+ int iovcnt, off_t offset,+ uint32 wait_event_info)+{+ int returnCode;+ Vfd *vfdP;++ Assert(FileIsValid(file));++ DO_DB(elog(LOG, "FileStartWriteV: %d (%s) " INT64_FORMAT " %d",+ file, VfdCache[file].fileName,+ (int64) offset,+ iovcnt));++ returnCode = FileAccess(file);+ if (returnCode < 0)+ return returnCode;++ vfdP = &VfdCache[file];++ /* FIXME: think about / reimplement temp_file_limit */++ pgaio_io_prep_writev(ioh, vfdP->fd, iovcnt, offset);++ return 0;+}+
int
FileSync(File file, uint32 wait_event_info)
{
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.cindex 2218f17e7a0..9d3dec2710c 100644--- a/src/backend/storage/smgr/md.c+++ b/src/backend/storage/smgr/md.c@@ -155,12 +155,19 @@ static BlockNumber _mdnblocks(SMgrRelation reln, ForkNumber forknum,
static PgAioResult md_readv_complete(PgAioHandle *ioh, PgAioResult prior_result, uint8 cb_data);
static void md_readv_report(PgAioResult result, const PgAioTargetData *target_data, int elevel);
+static PgAioResult md_writev_complete(PgAioHandle *ioh, PgAioResult prior_result, uint8 cb_data);+static void md_writev_report(PgAioResult result, const PgAioTargetData *target_data, int elevel);
const PgAioHandleCallbacks aio_md_readv_cb = {
.complete_shared = md_readv_complete,
.report = md_readv_report,
};
+const PgAioHandleCallbacks aio_md_writev_cb = {+ .complete_shared = md_writev_complete,+ .report = md_writev_report,+};+
static inline int
_mdfd_open_flags(void)
@@ -1115,6 +1122,64 @@ mdwritev(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
}
}
+/*+ * mdstartwritev() -- Asynchronous version of mdrwritev().+ */+void+mdstartwritev(PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,+ const void **buffers, BlockNumber nblocks, bool skipFsync)+{+ off_t seekpos;+ MdfdVec *v;+ BlockNumber nblocks_this_segment;+ struct iovec *iov;+ int iovcnt;+ int ret;++ v = _mdfd_getseg(reln, forknum, blocknum, false,+ EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY);++ seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE));++ Assert(seekpos < (off_t) BLCKSZ * RELSEG_SIZE);++ nblocks_this_segment =+ Min(nblocks,+ RELSEG_SIZE - (blocknum % ((BlockNumber) RELSEG_SIZE)));++ if (nblocks_this_segment != nblocks)+ elog(ERROR, "write crossing segment boundary");++ iovcnt = pgaio_io_get_iovec(ioh, &iov);++ Assert(nblocks <= iovcnt);++ iovcnt = buffers_to_iovec(iov, unconstify(void **, buffers), nblocks_this_segment);++ Assert(iovcnt <= nblocks_this_segment);++ if (!(io_direct_flags & IO_DIRECT_DATA))+ pgaio_io_set_flag(ioh, PGAIO_HF_BUFFERED);++ pgaio_io_set_target_smgr(ioh,+ reln,+ forknum,+ blocknum,+ nblocks,+ skipFsync);+ pgaio_io_register_callbacks(ioh, PGAIO_HCB_MD_WRITEV, 0);++ ret = FileStartWriteV(ioh, v->mdfd_vfd, iovcnt, seekpos, WAIT_EVENT_DATA_FILE_WRITE);+ if (ret != 0)+ ereport(ERROR,+ (errcode_for_file_access(),+ errmsg("could not start writing blocks %u..%u in file \"%s\": %m",+ blocknum,+ blocknum + nblocks_this_segment - 1,+ FilePathName(v->mdfd_vfd))));+}+
/*
* mdwriteback() -- Tell the kernel to write pages back to storage.
@@ -1503,6 +1568,40 @@ register_dirty_segment(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg)
}
}
+/*+ * Like register_dirty_segment(), except for use by AIO. In the completion+ * callback we don't have access to the MdfdVec (the completion callback might+ * be executed in a different backend than the issuing backend), therefore we+ * have to implement this slightly differently.+ */+static void+register_dirty_segment_aio(RelFileLocator locator, ForkNumber forknum, uint64 segno)+{+ FileTag tag;++ INIT_MD_FILETAG(tag, locator, forknum, segno);++ /*+ * Can't block here waiting for checkpointer to accept our sync request,+ * as checkpointer might be waiting for this AIO to finish if offloaded to+ * a worker.+ */+ if (!RegisterSyncRequest(&tag, SYNC_REQUEST, false /* retryOnError */ ))+ {+ char path[MAXPGPATH];++ ereport(DEBUG1,+ (errmsg_internal("could not forward fsync request because request queue is full")));++ /* reuse mdsyncfiletag() to avoid duplicating code */+ if (mdsyncfiletag(&tag, path))+ ereport(data_sync_elevel(ERROR),+ (errcode_for_file_access(),+ errmsg("could not fsync file \"%s\": %m",+ path)));+ }+}+
/*
* register_unlink_segment() -- Schedule a file to be deleted after next checkpoint
*/
@@ -2027,3 +2126,103 @@ md_readv_report(PgAioResult result, const PgAioTargetData *td, int elevel)
td->smgr.nblocks * (size_t) BLCKSZ));
}
}
++/*+ * AIO completion callback for mdstartwritev().+ */+static PgAioResult+md_writev_complete(PgAioHandle *ioh, PgAioResult prior_result, uint8 cb_data)+{+ PgAioTargetData *td = pgaio_io_get_target_data(ioh);+ PgAioResult result = prior_result;++ if (prior_result.result < 0)+ {+ result.status = PGAIO_RS_ERROR;+ result.id = PGAIO_HCB_MD_WRITEV;+ /* For "hard" errors, track the error number in error_data */+ result.error_data = -prior_result.result;+ result.result = 0;++ pgaio_result_report(result, td, LOG);++ return result;+ }++ /*+ * As explained above smgrstartwritev(), the smgr API operates on the+ * level of blocks, rather than bytes. Convert.+ */+ result.result /= BLCKSZ;++ Assert(result.result <= td->smgr.nblocks);++ if (result.result == 0)+ {+ /* consider 0 blocks written a failure */+ result.status = PGAIO_RS_ERROR;+ result.id = PGAIO_HCB_MD_WRITEV;+ result.error_data = 0;++ pgaio_result_report(result, td, LOG);++ return result;+ }++ if (result.status != PGAIO_RS_ERROR &&+ result.result < td->smgr.nblocks)+ {+ /* partial writes should be retried at upper level */+ result.status = PGAIO_RS_PARTIAL;+ result.id = PGAIO_HCB_MD_WRITEV;+ }++ if (!td->smgr.skip_fsync)+ register_dirty_segment_aio(td->smgr.rlocator, td->smgr.forkNum,+ td->smgr.blockNum / ((BlockNumber) RELSEG_SIZE));++ return result;+}++/*+ * AIO error reporting callback for mdstartwritev().+ */+static void+md_writev_report(PgAioResult result, const PgAioTargetData *td, int elevel)+{+ RelPathStr path;++ path = relpathbackend(td->smgr.rlocator,+ td->smgr.is_temp ? MyProcNumber : INVALID_PROC_NUMBER,+ td->smgr.forkNum);++ if (result.error_data != 0)+ {+ errno = result.error_data; /* for errcode_for_file_access() */++ ereport(elevel,+ errcode_for_file_access(),+ errmsg("could not write blocks %u..%u in file \"%s\": %m",+ td->smgr.blockNum,+ td->smgr.blockNum + td->smgr.nblocks,+ path.str)+ );+ }+ else+ {+ /*+ * NB: This will typically only be output in debug messages, while+ * retrying a partial IO.+ */+ ereport(elevel,+ errcode(ERRCODE_DATA_CORRUPTED),+ errmsg("could not write blocks %u..%u in file \"%s\": wrote only %zu of %zu bytes",+ td->smgr.blockNum,+ td->smgr.blockNum + td->smgr.nblocks - 1,+ path.str,+ result.result * (size_t) BLCKSZ,+ td->smgr.nblocks * (size_t) BLCKSZ+ )+ );+ }+}diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.cindex 545888dcdfc..aec2c0c565a 100644--- a/src/backend/storage/smgr/smgr.c+++ b/src/backend/storage/smgr/smgr.c@@ -115,6 +115,11 @@ typedef struct f_smgr
BlockNumber blocknum,
const void **buffers, BlockNumber nblocks,
bool skipFsync);
+ void (*smgr_startwritev) (PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum,+ BlockNumber blocknum,+ const void **buffers, BlockNumber nblocks,+ bool skipFsync);
void (*smgr_writeback) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, BlockNumber nblocks);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
@@ -142,6 +147,7 @@ static const f_smgr smgrsw[] = {
.smgr_readv = mdreadv,
.smgr_startreadv = mdstartreadv,
.smgr_writev = mdwritev,
+ .smgr_startwritev = mdstartwritev,
.smgr_writeback = mdwriteback,
.smgr_nblocks = mdnblocks,
.smgr_truncate = mdtruncate,
@@ -787,6 +793,29 @@ smgrwritev(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
RESUME_INTERRUPTS();
}
+/*+ * smgrstartwritev() -- asynchronous version of smgrwritev()+ *+ * This starts an asynchronous writev IO using the IO handle `ioh`. Other than+ * `ioh` all parameters are the same as smgrwritev().+ *+ * Completion callbacks above smgr will be passed the result as the number of+ * successfully written blocks if the write [partially] succeeds. This+ * maintains the abstraction that smgr operates on the level of blocks, rather+ * than bytes.+ */+void+smgrstartwritev(PgAioHandle *ioh,+ SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,+ const void **buffers, BlockNumber nblocks, bool skipFsync)+{+ HOLD_INTERRUPTS();+ smgrsw[reln->smgr_which].smgr_startwritev(ioh,+ reln, forknum, blocknum, buffers,+ nblocks, skipFsync);+ RESUME_INTERRUPTS();+}+
/*
* smgrwriteback() -- Trigger kernel writeback for the supplied range of
* blocks.
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0020-aio-Add-bounce-buffers.patch (23.1K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/21-v2.11-0020-aio-Add-bounce-buffers.patch)
download | inline diff:
From 8be13a1426cb9d4a9f16dc0bfee0e59c9f375742 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 20/27] aio: Add bounce buffers
---
src/include/storage/aio.h | 15 ++
src/include/storage/aio_internal.h | 34 ++++
src/include/storage/aio_types.h | 2 +
src/include/utils/resowner.h | 2 +
src/backend/storage/aio/README.md | 27 +++
src/backend/storage/aio/aio.c | 178 ++++++++++++++++++
src/backend/storage/aio/aio_init.c | 123 ++++++++++++
src/backend/utils/misc/guc_tables.c | 13 ++
src/backend/utils/misc/postgresql.conf.sample | 2 +
src/backend/utils/resowner/resowner.c | 25 ++-
src/test/modules/test_aio/test_aio--1.0.sql | 21 +++
src/test/modules/test_aio/test_aio.c | 55 ++++++
src/tools/pgindent/typedefs.list | 1 +
13 files changed, 496 insertions(+), 2 deletions(-)
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.hindex 9a0868a270c..09d6d9fe1bc 100644--- a/src/include/storage/aio.h+++ b/src/include/storage/aio.h@@ -349,6 +349,20 @@ extern bool pgaio_have_staged(void);+/* --------------------------------------------------------------------------------+ * Bounce Buffers+ * --------------------------------------------------------------------------------+ */++extern PgAioBounceBuffer *pgaio_bounce_buffer_get(void);+extern void pgaio_io_assoc_bounce_buffer(PgAioHandle *ioh, PgAioBounceBuffer *bb);+extern uint32 pgaio_bounce_buffer_id(PgAioBounceBuffer *bb);+extern void pgaio_bounce_buffer_release(PgAioBounceBuffer *bb);+extern char *pgaio_bounce_buffer_buffer(PgAioBounceBuffer *bb);+extern void pgaio_bounce_buffer_release_resowner(struct dlist_node *bb_node, bool on_error);+++
/* --------------------------------------------------------------------------------
* Other
* --------------------------------------------------------------------------------
@@ -361,6 +375,7 @@ extern void pgaio_closing_fd(int fd);
/* GUCs */
extern PGDLLIMPORT int io_method;
extern PGDLLIMPORT int io_max_concurrency;
+extern PGDLLIMPORT int io_bounce_buffers;
#endif /* AIO_H */
diff --git a/src/include/storage/aio_internal.h b/src/include/storage/aio_internal.hindex 6e6033519b2..447208c8f12 100644--- a/src/include/storage/aio_internal.h+++ b/src/include/storage/aio_internal.h@@ -122,6 +122,12 @@ struct PgAioHandle
/* raw result of the IO operation */
int32 result;
+ /*+ * List of bounce_buffers owned by IO. It would suffice to use an index+ * based linked list here.+ */+ slist_head bounce_buffers;+
/**
* In which list the handle is registered, depends on the state:
* - IDLE, in per-backend list
@@ -177,11 +183,24 @@ struct PgAioHandle
};
+/* typedef is in aio_types.h */+struct PgAioBounceBuffer+{+ slist_node node;+ struct ResourceOwnerData *resowner;+ dlist_node resowner_node;+ char *buffer;+};++
typedef struct PgAioBackend
{
/* index into PgAioCtl->io_handles */
uint32 io_handle_off;
+ /* index into PgAioCtl->bounce_buffers */+ uint32 bounce_buffers_off;+
/* IO Handles that currently are not used */
dclist_head idle_ios;
@@ -212,6 +231,12 @@ typedef struct PgAioBackend
* IOs being appended at the end.
*/
dclist_head in_flight_ios;
++ /* Bounce Buffers that currently are not used */+ slist_head idle_bbs;++ /* see handed_out_io */+ PgAioBounceBuffer *handed_out_bb;
} PgAioBackend;
@@ -239,6 +264,15 @@ typedef struct PgAioCtl
uint32 io_handle_count;
PgAioHandle *io_handles;
++ /*+ * To perform AIO on buffers that are not located in shared memory (either+ * because they are not in shared memory or because we need to operate on+ * a copy, as e.g. the case for writes when checksums are in use)+ */+ uint32 bounce_buffers_count;+ PgAioBounceBuffer *bounce_buffers;+ char *bounce_buffers_data;
} PgAioCtl;
diff --git a/src/include/storage/aio_types.h b/src/include/storage/aio_types.hindex debe8163d4e..02950ee56a7 100644--- a/src/include/storage/aio_types.h+++ b/src/include/storage/aio_types.h@@ -118,4 +118,6 @@ typedef struct PgAioReturn
} PgAioReturn;
+typedef struct PgAioBounceBuffer PgAioBounceBuffer;+
#endif /* AIO_TYPES_H */
diff --git a/src/include/utils/resowner.h b/src/include/utils/resowner.hindex aede4bfc820..7e2ec224169 100644--- a/src/include/utils/resowner.h+++ b/src/include/utils/resowner.h@@ -168,5 +168,7 @@ extern void ResourceOwnerForgetLock(ResourceOwner owner, struct LOCALLOCK *local
struct dlist_node;
extern void ResourceOwnerRememberAioHandle(ResourceOwner owner, struct dlist_node *ioh_node);
extern void ResourceOwnerForgetAioHandle(ResourceOwner owner, struct dlist_node *ioh_node);
+extern void ResourceOwnerRememberAioBounceBuffer(ResourceOwner owner, struct dlist_node *bb_node);+extern void ResourceOwnerForgetAioBounceBuffer(ResourceOwner owner, struct dlist_node *bb_node);
#endif /* RESOWNER_H */
diff --git a/src/backend/storage/aio/README.md b/src/backend/storage/aio/README.mdindex 012cf3f305a..57e6f2fef07 100644--- a/src/backend/storage/aio/README.md+++ b/src/backend/storage/aio/README.md@@ -405,6 +405,33 @@ shared memory no less!), completion callbacks instead have to encode errors in
a more compact format that can be converted into an error message.
+### AIO Bounce Buffers++For some uses of AIO there is no convenient memory location as the source /+destination of an AIO. E.g. when data checksums are enabled, writes from+shared buffers currently cannot be done directly from shared buffers, as a+shared buffer lock still allows some modification, e.g., for hint bits(see+`FlushBuffer()`). If the write were done in-place, such modifications can+cause the checksum to fail.++For synchronous IO this is solved by copying the buffer to separate memory+before computing the checksum and using that copy as the source buffer for the+AIO.++However, for AIO that is not a workable solution:+- Instead of a single buffer many buffers are required, as many IOs might be+ in flight+- When using the [worker method](#worker), the source/target of IO needs to be+ in shared memory, otherwise the workers won't be able to access the memory.++The AIO subsystem addresses this by providing a limited number of bounce+buffers that can be used as the source / target for IO. A bounce buffer be+acquired with `pgaio_bounce_buffer_get()` and multiple bounce buffers can be+associated with an AIO Handle with `pgaio_io_assoc_bounce_buffer()`.++Bounce buffers are automatically released when the IO completes.++
## Helpers
Using the low-level AIO API introduces too much complexity to do so all over
diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.cindex 3c3383b1318..ef344c32c61 100644--- a/src/backend/storage/aio/aio.c+++ b/src/backend/storage/aio/aio.c@@ -62,6 +62,8 @@ static PgAioHandle *pgaio_io_from_wref(PgAioWaitRef *iow, uint64 *ref_generation
static const char *pgaio_io_state_get_name(PgAioHandleState s);
static void pgaio_io_wait(PgAioHandle *ioh, uint64 ref_generation);
+static void pgaio_bounce_buffer_wait_for_free(void);+
/* Options for io_method. */
const struct config_enum_entry io_method_options[] = {
@@ -76,6 +78,7 @@ const struct config_enum_entry io_method_options[] = {
/* GUCs */
int io_method = DEFAULT_IO_METHOD;
int io_max_concurrency = -1;
+int io_bounce_buffers = -1;
/* global control for AIO */
PgAioCtl *pgaio_ctl;
@@ -662,6 +665,21 @@ pgaio_io_reclaim(PgAioHandle *ioh)
}
}
+ /* reclaim all associated bounce buffers */+ if (!slist_is_empty(&ioh->bounce_buffers))+ {+ slist_mutable_iter it;++ slist_foreach_modify(it, &ioh->bounce_buffers)+ {+ PgAioBounceBuffer *bb = slist_container(PgAioBounceBuffer, node, it.cur);++ slist_delete_current(&it);++ slist_push_head(&pgaio_my_backend->idle_bbs, &bb->node);+ }+ }+
if (ioh->resowner)
{
ResourceOwnerForgetAioHandle(ioh->resowner, &ioh->resowner_node);
@@ -1044,6 +1062,166 @@ pgaio_submit_staged(void)+/* --------------------------------------------------------------------------------+ * Functions primarily related to PgAioBounceBuffer+ * --------------------------------------------------------------------------------+ */++PgAioBounceBuffer *+pgaio_bounce_buffer_get(void)+{+ PgAioBounceBuffer *bb = NULL;+ slist_node *node;++ if (pgaio_my_backend->handed_out_bb != NULL)+ elog(ERROR, "can only hand out one BB");++ /*+ * XXX: It probably is not a good idea to have bounce buffers be per+ * backend, that's a fair bit of memory.+ */+ if (slist_is_empty(&pgaio_my_backend->idle_bbs))+ {+ pgaio_bounce_buffer_wait_for_free();+ }++ node = slist_pop_head_node(&pgaio_my_backend->idle_bbs);+ bb = slist_container(PgAioBounceBuffer, node, node);++ pgaio_my_backend->handed_out_bb = bb;++ bb->resowner = CurrentResourceOwner;+ ResourceOwnerRememberAioBounceBuffer(bb->resowner, &bb->resowner_node);++ return bb;+}++void+pgaio_io_assoc_bounce_buffer(PgAioHandle *ioh, PgAioBounceBuffer *bb)+{+ if (pgaio_my_backend->handed_out_bb != bb)+ elog(ERROR, "can only assign handed out BB");+ pgaio_my_backend->handed_out_bb = NULL;++ /*+ * There can be many bounce buffers assigned in case of vectorized IOs.+ */+ slist_push_head(&ioh->bounce_buffers, &bb->node);++ /* once associated with an IO, the IO has ownership */+ ResourceOwnerForgetAioBounceBuffer(bb->resowner, &bb->resowner_node);+ bb->resowner = NULL;+}++uint32+pgaio_bounce_buffer_id(PgAioBounceBuffer *bb)+{+ return bb - pgaio_ctl->bounce_buffers;+}++void+pgaio_bounce_buffer_release(PgAioBounceBuffer *bb)+{+ if (pgaio_my_backend->handed_out_bb != bb)+ elog(ERROR, "can only release handed out BB");++ slist_push_head(&pgaio_my_backend->idle_bbs, &bb->node);+ pgaio_my_backend->handed_out_bb = NULL;++ ResourceOwnerForgetAioBounceBuffer(bb->resowner, &bb->resowner_node);+ bb->resowner = NULL;+}++void+pgaio_bounce_buffer_release_resowner(dlist_node *bb_node, bool on_error)+{+ PgAioBounceBuffer *bb = dlist_container(PgAioBounceBuffer, resowner_node, bb_node);++ Assert(bb->resowner);++ if (!on_error)+ elog(WARNING, "leaked AIO bounce buffer");++ pgaio_bounce_buffer_release(bb);+}++char *+pgaio_bounce_buffer_buffer(PgAioBounceBuffer *bb)+{+ return bb->buffer;+}++static void+pgaio_bounce_buffer_wait_for_free(void)+{+ static uint32 lastpos = 0;++ if (pgaio_my_backend->num_staged_ios > 0)+ {+ pgaio_debug(DEBUG2, "submitting %d, while acquiring free bb",+ pgaio_my_backend->num_staged_ios);+ pgaio_submit_staged();+ }++ for (uint32 i = lastpos; i < lastpos + io_max_concurrency; i++)+ {+ uint32 thisoff = pgaio_my_backend->io_handle_off + (i % io_max_concurrency);+ PgAioHandle *ioh = &pgaio_ctl->io_handles[thisoff];++ switch (ioh->state)+ {+ case PGAIO_HS_IDLE:+ case PGAIO_HS_HANDED_OUT:+ continue;+ case PGAIO_HS_DEFINED: /* should have been submitted above */+ case PGAIO_HS_STAGED:+ elog(ERROR, "shouldn't get here with io:%d in state %d",+ pgaio_io_get_id(ioh), ioh->state);+ break;+ case PGAIO_HS_COMPLETED_IO:+ case PGAIO_HS_SUBMITTED:+ if (!slist_is_empty(&ioh->bounce_buffers))+ {+ pgaio_debug_io(DEBUG2, ioh,+ "waiting for IO to reclaim BB with %d in flight",+ dclist_count(&pgaio_my_backend->in_flight_ios));++ /* see comment in pgaio_io_wait_for_free() about raciness */+ pgaio_io_wait(ioh, ioh->generation);++ if (slist_is_empty(&pgaio_my_backend->idle_bbs))+ elog(WARNING, "empty after wait");++ if (!slist_is_empty(&pgaio_my_backend->idle_bbs))+ {+ lastpos = i;+ return;+ }+ }+ break;+ case PGAIO_HS_COMPLETED_SHARED:+ case PGAIO_HS_COMPLETED_LOCAL:+ /* reclaim */+ pgaio_io_reclaim(ioh);++ if (!slist_is_empty(&pgaio_my_backend->idle_bbs))+ {+ lastpos = i;+ return;+ }+ break;+ }+ }++ /*+ * The submission above could have caused the IO to complete at any time.+ */+ if (slist_is_empty(&pgaio_my_backend->idle_bbs))+ elog(PANIC, "no more bbs");+}+++
/* --------------------------------------------------------------------------------
* Other
* --------------------------------------------------------------------------------
diff --git a/src/backend/storage/aio/aio_init.c b/src/backend/storage/aio/aio_init.cindex 885c3940c66..95b10933fed 100644--- a/src/backend/storage/aio/aio_init.c+++ b/src/backend/storage/aio/aio_init.c@@ -88,6 +88,32 @@ AioHandleDataShmemSize(void)
io_max_concurrency));
}
+static Size+AioBounceBufferDescShmemSize(void)+{+ Size sz;++ /* PgAioBounceBuffer itself */+ sz = mul_size(sizeof(PgAioBounceBuffer),+ mul_size(AioProcs(), io_bounce_buffers));++ return sz;+}++static Size+AioBounceBufferDataShmemSize(void)+{+ Size sz;++ /* and the associated buffer */+ sz = mul_size(BLCKSZ,+ mul_size(io_bounce_buffers, AioProcs()));+ /* memory for alignment */+ sz += BLCKSZ;++ return sz;+}+
/*
* Choose a suitable value for io_max_concurrency.
*
@@ -113,6 +139,33 @@ AioChooseMaxConcurrency(void)
return Min(max_proportional_pins, 64);
}
+/*+ * Choose a suitable value for io_bounce_buffers.+ *+ * It's very unlikely to be useful to allocate more bounce buffers for each+ * backend than the backend is allowed to pin. Additionally, bounce buffers+ * currently are used for writes, it seems very uncommon for more than 10% of+ * shared_buffers to be written out concurrently.+ *+ * XXX: This quickly can take up significant amounts of memory, the logic+ * should probably fine tuned.+ */+static int+AioChooseBounceBuffers(void)+{+ uint32 max_backends;+ int max_proportional_pins;++ /* Similar logic to LimitAdditionalPins() */+ max_backends = MaxBackends + NUM_AUXILIARY_PROCS;+ max_proportional_pins = (NBuffers / 10) / max_backends;++ max_proportional_pins = Max(max_proportional_pins, 1);++ /* apply upper limit */+ return Min(max_proportional_pins, 256);+}+
Size
AioShmemSize(void)
{
@@ -136,11 +189,31 @@ AioShmemSize(void)
PGC_S_OVERRIDE);
}
++ /*+ * If io_bounce_buffers is -1, we automatically choose a suitable value.+ *+ * See also comment above.+ */+ if (io_bounce_buffers == -1)+ {+ char buf[32];++ snprintf(buf, sizeof(buf), "%d", AioChooseBounceBuffers());+ SetConfigOption("io_bounce_buffers", buf, PGC_POSTMASTER,+ PGC_S_DYNAMIC_DEFAULT);+ if (io_bounce_buffers == -1) /* failed to apply it? */+ SetConfigOption("io_bounce_buffers", buf, PGC_POSTMASTER,+ PGC_S_OVERRIDE);+ }+
sz = add_size(sz, AioCtlShmemSize());
sz = add_size(sz, AioBackendShmemSize());
sz = add_size(sz, AioHandleShmemSize());
sz = add_size(sz, AioHandleIOVShmemSize());
sz = add_size(sz, AioHandleDataShmemSize());
+ sz = add_size(sz, AioBounceBufferDescShmemSize());+ sz = add_size(sz, AioBounceBufferDataShmemSize());
/* Reserve space for method specific resources. */
if (pgaio_method_ops->shmem_size)
@@ -156,6 +229,9 @@ AioShmemInit(void)
uint32 io_handle_off = 0;
uint32 iovec_off = 0;
uint32 per_backend_iovecs = io_max_concurrency * io_max_combine_limit;
+ uint32 bounce_buffers_off = 0;+ uint32 per_backend_bb = io_bounce_buffers;+ char *bounce_buffers_data;
pgaio_ctl = (PgAioCtl *)
ShmemInitStruct("AioCtl", AioCtlShmemSize(), &found);
@@ -167,6 +243,7 @@ AioShmemInit(void)
pgaio_ctl->io_handle_count = AioProcs() * io_max_concurrency;
pgaio_ctl->iovec_count = AioProcs() * per_backend_iovecs;
+ pgaio_ctl->bounce_buffers_count = AioProcs() * per_backend_bb;
pgaio_ctl->backend_state = (PgAioBackend *)
ShmemInitStruct("AioBackend", AioBackendShmemSize(), &found);
@@ -179,6 +256,40 @@ AioShmemInit(void)
pgaio_ctl->handle_data = (uint64 *)
ShmemInitStruct("AioHandleData", AioHandleDataShmemSize(), &found);
+ pgaio_ctl->bounce_buffers = (PgAioBounceBuffer *)+ ShmemInitStruct("AioBounceBufferDesc", AioBounceBufferDescShmemSize(),+ &found);++ bounce_buffers_data =+ ShmemInitStruct("AioBounceBufferData", AioBounceBufferDataShmemSize(),+ &found);+ bounce_buffers_data =+ (char *) TYPEALIGN(BLCKSZ, (uintptr_t) bounce_buffers_data);+ pgaio_ctl->bounce_buffers_data = bounce_buffers_data;+++ /* Initialize IO handles. */+ for (uint64 i = 0; i < pgaio_ctl->io_handle_count; i++)+ {+ PgAioHandle *ioh = &pgaio_ctl->io_handles[i];++ ioh->op = PGAIO_OP_INVALID;+ ioh->target = PGAIO_TID_INVALID;+ ioh->state = PGAIO_HS_IDLE;++ slist_init(&ioh->bounce_buffers);+ }++ /* Initialize Bounce Buffers. */+ for (uint64 i = 0; i < pgaio_ctl->bounce_buffers_count; i++)+ {+ PgAioBounceBuffer *bb = &pgaio_ctl->bounce_buffers[i];++ bb->buffer = bounce_buffers_data;+ bounce_buffers_data += BLCKSZ;+ }++
for (int procno = 0; procno < AioProcs(); procno++)
{
PgAioBackend *bs = &pgaio_ctl->backend_state[procno];
@@ -186,9 +297,13 @@ AioShmemInit(void)
bs->io_handle_off = io_handle_off;
io_handle_off += io_max_concurrency;
+ bs->bounce_buffers_off = bounce_buffers_off;+ bounce_buffers_off += per_backend_bb;+
dclist_init(&bs->idle_ios);
memset(bs->staged_ios, 0, sizeof(PgAioHandle *) * PGAIO_SUBMIT_BATCH_SIZE);
dclist_init(&bs->in_flight_ios);
+ slist_init(&bs->idle_bbs);
/* initialize per-backend IOs */
for (int i = 0; i < io_max_concurrency; i++)
@@ -210,6 +325,14 @@ AioShmemInit(void)
dclist_push_tail(&bs->idle_ios, &ioh->node);
iovec_off += io_max_combine_limit;
}
++ /* initialize per-backend bounce buffers */+ for (int i = 0; i < per_backend_bb; i++)+ {+ PgAioBounceBuffer *bb = &pgaio_ctl->bounce_buffers[bs->bounce_buffers_off + i];++ slist_push_head(&bs->idle_bbs, &bb->node);+ }
}
out:
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.cindex 2e17dbc8c9e..89f9b1fbab7 100644--- a/src/backend/utils/misc/guc_tables.c+++ b/src/backend/utils/misc/guc_tables.c@@ -3303,6 +3303,19 @@ struct config_int ConfigureNamesInt[] =
check_io_max_concurrency, NULL, NULL
},
+ {+ {"io_bounce_buffers",+ PGC_POSTMASTER,+ RESOURCES_IO,+ gettext_noop("Number of IO Bounce Buffers reserved for each backend."),+ NULL,+ GUC_UNIT_BLOCKS+ },+ &io_bounce_buffers,+ -1, -1, 4096,+ NULL, NULL, NULL+ },+
{
{"io_workers",
PGC_SIGHUP,
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sampleindex d5f5f3dbda4..762194c61ad 100644--- a/src/backend/utils/misc/postgresql.conf.sample+++ b/src/backend/utils/misc/postgresql.conf.sample@@ -211,6 +211,8 @@
# -1 sets based on shared_buffers
# (change requires restart)
#io_workers = 3 # 1-32;
+#io_bounce_buffers = -1 # -1 sets based on shared_buffers+ # (change requires restart)
# - Worker Processes -
diff --git a/src/backend/utils/resowner/resowner.c b/src/backend/utils/resowner/resowner.cindex d39f3e1b655..81e7e27965a 100644--- a/src/backend/utils/resowner/resowner.c+++ b/src/backend/utils/resowner/resowner.c@@ -159,10 +159,11 @@ struct ResourceOwnerData
LOCALLOCK *locks[MAX_RESOWNER_LOCKS]; /* list of owned locks */
/*
- * AIO handles need be registered in critical sections and therefore- * cannot use the normal ResourceElem mechanism.+ * AIO handles & bounce buffers need be registered in critical sections+ * and therefore cannot use the normal ResourceElem mechanism.
*/
dlist_head aio_handles;
+ dlist_head aio_bounce_buffers;
};
@@ -434,6 +435,7 @@ ResourceOwnerCreate(ResourceOwner parent, const char *name)
}
dlist_init(&owner->aio_handles);
+ dlist_init(&owner->aio_bounce_buffers);
return owner;
}
@@ -742,6 +744,13 @@ ResourceOwnerReleaseInternal(ResourceOwner owner,
pgaio_io_release_resowner(node, !isCommit);
}
++ while (!dlist_is_empty(&owner->aio_bounce_buffers))+ {+ dlist_node *node = dlist_head_node(&owner->aio_bounce_buffers);++ pgaio_bounce_buffer_release_resowner(node, !isCommit);+ }
}
else if (phase == RESOURCE_RELEASE_LOCKS)
{
@@ -1111,3 +1120,15 @@ ResourceOwnerForgetAioHandle(ResourceOwner owner, struct dlist_node *ioh_node)
{
dlist_delete_from(&owner->aio_handles, ioh_node);
}
++void+ResourceOwnerRememberAioBounceBuffer(ResourceOwner owner, struct dlist_node *ioh_node)+{+ dlist_push_tail(&owner->aio_bounce_buffers, ioh_node);+}++void+ResourceOwnerForgetAioBounceBuffer(ResourceOwner owner, struct dlist_node *ioh_node)+{+ dlist_delete_from(&owner->aio_bounce_buffers, ioh_node);+}diff --git a/src/test/modules/test_aio/test_aio--1.0.sql b/src/test/modules/test_aio/test_aio--1.0.sqlindex 3d4e9bb070f..16f248619fc 100644--- a/src/test/modules/test_aio/test_aio--1.0.sql+++ b/src/test/modules/test_aio/test_aio--1.0.sql@@ -76,6 +76,27 @@ RETURNS pg_catalog.void STRICT
AS 'MODULE_PATHNAME' LANGUAGE C;
+CREATE FUNCTION bb_get_and_error()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION bb_get_twice()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION bb_get()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION bb_get_release()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++CREATE FUNCTION bb_release_last()+RETURNS pg_catalog.void STRICT+AS 'MODULE_PATHNAME' LANGUAGE C;++
/*
* Injection point related functions
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.cindex fc55b985dad..1e2127fe888 100644--- a/src/test/modules/test_aio/test_aio.c+++ b/src/test/modules/test_aio/test_aio.c@@ -53,6 +53,7 @@ static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
static PgAioHandle *last_handle;
+static PgAioBounceBuffer *last_bb;@@ -526,6 +527,60 @@ batch_end(PG_FUNCTION_ARGS)
PG_RETURN_VOID();
}
+PG_FUNCTION_INFO_V1(bb_get);+Datum+bb_get(PG_FUNCTION_ARGS)+{+ last_bb = pgaio_bounce_buffer_get();++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(bb_release_last);+Datum+bb_release_last(PG_FUNCTION_ARGS)+{+ if (!last_bb)+ elog(ERROR, "no bb");++ pgaio_bounce_buffer_release(last_bb);++ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(bb_get_and_error);+Datum+bb_get_and_error(PG_FUNCTION_ARGS)+{+ pgaio_bounce_buffer_get();++ elog(ERROR, "as you command");+ PG_RETURN_VOID();+}++PG_FUNCTION_INFO_V1(bb_get_twice);+Datum+bb_get_twice(PG_FUNCTION_ARGS)+{+ pgaio_bounce_buffer_get();+ pgaio_bounce_buffer_get();++ PG_RETURN_VOID();+}+++PG_FUNCTION_INFO_V1(bb_get_release);+Datum+bb_get_release(PG_FUNCTION_ARGS)+{+ PgAioBounceBuffer *bb;++ bb = pgaio_bounce_buffer_get();+ pgaio_bounce_buffer_release(bb);++ PG_RETURN_VOID();+}+
#ifdef USE_INJECTION_POINTS
extern PGDLLEXPORT void inj_io_short_read(const char *name, const void *private_data);
extern PGDLLEXPORT void inj_io_reopen(const char *name, const void *private_data);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex 53abd54edd8..d633c732162 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -2132,6 +2132,7 @@ PermutationStep
PermutationStepBlocker
PermutationStepBlockerType
PgAioBackend
+PgAioBounceBuffer
PgAioCtl
PgAioHandle
PgAioHandleCallbackID
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0021-bufmgr-Implement-AIO-write-support.patch (9.8K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/22-v2.11-0021-bufmgr-Implement-AIO-write-support.patch)
download | inline diff:
From 9b4e2a0758fb149e3b7dd94728f948bab74ba5f0 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 21/27] bufmgr: Implement AIO write support
As of this commit there are no users of these AIO facilities, that'll come in
later commits.
Author:
Reviewed-By:
Discussion: https://postgr.es/m/
Backpatch:
---
src/include/storage/aio.h | 2 +
src/include/storage/bufmgr.h | 2 +
src/backend/storage/aio/aio_callback.c | 2 +
src/backend/storage/buffer/bufmgr.c | 180 +++++++++++++++++++++++++
4 files changed, 186 insertions(+)
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.hindex 09d6d9fe1bc..d461485e83f 100644--- a/src/include/storage/aio.h+++ b/src/include/storage/aio.h@@ -197,8 +197,10 @@ typedef enum PgAioHandleCallbackID
PGAIO_HCB_MD_WRITEV,
PGAIO_HCB_SHARED_BUFFER_READV,
+ PGAIO_HCB_SHARED_BUFFER_WRITEV,
PGAIO_HCB_LOCAL_BUFFER_READV,
+ PGAIO_HCB_LOCAL_BUFFER_WRITEV,
} PgAioHandleCallbackID;
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.hindex 784df8b00cb..ba9bf247ddb 100644--- a/src/include/storage/bufmgr.h+++ b/src/include/storage/bufmgr.h@@ -172,7 +172,9 @@ extern PGDLLIMPORT int backend_flush_after;
extern PGDLLIMPORT int bgwriter_flush_after;
extern const PgAioHandleCallbacks aio_shared_buffer_readv_cb;
+extern const PgAioHandleCallbacks aio_shared_buffer_writev_cb;
extern const PgAioHandleCallbacks aio_local_buffer_readv_cb;
+extern const PgAioHandleCallbacks aio_local_buffer_writev_cb;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
diff --git a/src/backend/storage/aio/aio_callback.c b/src/backend/storage/aio/aio_callback.cindex 03e44b76075..c49bf4a2384 100644--- a/src/backend/storage/aio/aio_callback.c+++ b/src/backend/storage/aio/aio_callback.c@@ -44,8 +44,10 @@ static const PgAioHandleCallbacksEntry aio_handle_cbs[] = {
CALLBACK_ENTRY(PGAIO_HCB_MD_WRITEV, aio_md_writev_cb),
CALLBACK_ENTRY(PGAIO_HCB_SHARED_BUFFER_READV, aio_shared_buffer_readv_cb),
+ CALLBACK_ENTRY(PGAIO_HCB_SHARED_BUFFER_WRITEV, aio_shared_buffer_writev_cb),
CALLBACK_ENTRY(PGAIO_HCB_LOCAL_BUFFER_READV, aio_local_buffer_readv_cb),
+ CALLBACK_ENTRY(PGAIO_HCB_LOCAL_BUFFER_WRITEV, aio_local_buffer_writev_cb),
#undef CALLBACK_ENTRY
};
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.cindex be306bfa4f2..94f3caf2224 100644--- a/src/backend/storage/buffer/bufmgr.c+++ b/src/backend/storage/buffer/bufmgr.c@@ -5507,7 +5507,15 @@ LockBuffer(Buffer buffer, int mode)
else if (mode == BUFFER_LOCK_SHARE)
LWLockAcquire(BufferDescriptorGetContentLock(buf), LW_SHARED);
else if (mode == BUFFER_LOCK_EXCLUSIVE)
+ {+ /*+ * FIXME: Wait for AIO writes, otherwise there would be a risk of+ * deadlock. This isn't entirely trivial to do in a race-free way, IO+ * could be started between us checking whether there is IO and+ * enqueueing ourselves for the lock.+ */
LWLockAcquire(BufferDescriptorGetContentLock(buf), LW_EXCLUSIVE);
+ }
else
elog(ERROR, "unrecognized buffer lock mode: %d", mode);
}
@@ -5522,6 +5530,19 @@ ConditionalLockBuffer(Buffer buffer)
{
BufferDesc *buf;
+ /*+ * FIXME: Wait for AIO writes. Some code does not deal well+ * ConditionalLockBuffer() continuously failing, e.g.+ * spginsert()->spgdoinsert() ends up busy-looping (easily reproducible by+ * just making this function always fail and running the regression+ * tests). While that code could be fixed, it'd be hard to find all+ * problematic places.+ *+ * It would be OK to wait for the IO as waiting for IO completion does not+ * need to wait for any locks that could lead to an undetected deadlock or+ * such.+ */+
Assert(BufferIsPinned(buffer));
if (BufferIsLocal(buffer))
return true; /* act as though we got it */
@@ -5594,6 +5615,11 @@ LockBufferForCleanup(Buffer buffer)
{
uint32 buf_state;
+ /*+ * FIXME: LockBuffer()'s handling of in-progress writes (once+ * implemented) should suffice to deal with deadlock risk.+ */+
/* Try to acquire lock */
LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
buf_state = LockBufHdr(bufHdr);
@@ -5741,6 +5767,14 @@ ConditionalLockBufferForCleanup(Buffer buffer)
Assert(BufferIsValid(buffer));
+ /*+ * FIXME: Should wait for IO for the same reason as in+ * ConditionalLockBuffer(). Needs to happen before the+ * ConditionalLockBuffer() call below, as we'd never reach the+ * ConditionalLockBuffer() call due the buffer pin held for the duration+ * of the IO.+ */+
if (BufferIsLocal(buffer))
{
refcount = LocalRefCount[-buffer - 1];
@@ -6843,12 +6877,131 @@ buffer_readv_report(PgAioResult result, const PgAioTargetData *target_data,
target_data->smgr.forkNum).str));
}
+/*+ * Helper for AIO writev completion callbacks, supporting both shared and temp+ * buffers. Gets called once for each buffer in a multi-page write.+ */+static pg_attribute_always_inline PgAioResult+buffer_writev_complete_one(uint8 buf_off, Buffer buffer, uint8 flags,+ bool failed, bool is_temp)+{+ BufferDesc *buf_hdr = is_temp ?+ GetLocalBufferDescriptor(-buffer - 1)+ : GetBufferDescriptor(buffer - 1);+ PgAioResult result;+ bool clear_dirty;+ uint32 set_flag_bits;++#ifdef USE_ASSERT_CHECKING+ {+ uint32 buf_state = pg_atomic_read_u32(&buf_hdr->state);++ Assert(buf_state & BM_VALID);+ Assert(buf_state & BM_TAG_VALID);+ /* temp buffers don't use BM_IO_IN_PROGRESS */+ if (!is_temp)+ Assert(buf_state & BM_IO_IN_PROGRESS);+ Assert(buf_state & BM_DIRTY);+ }+#endif++ result.status = PGAIO_RS_OK;++ clear_dirty = failed ? false : true;+ set_flag_bits = failed ? BM_IO_ERROR : 0;++ if (is_temp)+ TerminateLocalBufferIO(buf_hdr, clear_dirty, set_flag_bits, false);+ else+ TerminateBufferIO(buf_hdr, clear_dirty, set_flag_bits, false, false);++ /*+ * The initiator of IO is not managing the lock (i.e. we called+ * LWLockDisown()), we are.+ */+ if (!is_temp)+ LWLockReleaseDisowned(BufferDescriptorGetContentLock(buf_hdr),+ LW_SHARED);++ /* FIXME: tracepoint */++ return result;+}++/*+ * Perform completion handling of a single AIO write. This write may cover+ * multiple blocks / buffers.+ *+ * Shared between shared and local buffers, to reduce code duplication.+ */+static pg_attribute_always_inline PgAioResult+buffer_writev_complete(PgAioHandle *ioh, PgAioResult prior_result,+ uint8 cb_data, bool is_temp)+{+ PgAioResult result = prior_result;+ PgAioTargetData *td = pgaio_io_get_target_data(ioh);+ uint64 *io_data;+ uint8 handle_data_len;++ if (is_temp)+ {+ Assert(td->smgr.is_temp);+ Assert(pgaio_io_get_owner(ioh) == MyProcNumber);+ }+ else+ Assert(!td->smgr.is_temp);++ /*+ * Iterate over all the buffers affected by this IO and call appropriate+ * per-buffer completion function for each buffer.+ */+ io_data = pgaio_io_get_handle_data(ioh, &handle_data_len);+ for (uint8 buf_off = 0; buf_off < handle_data_len; buf_off++)+ {+ Buffer buf = io_data[buf_off];+ PgAioResult buf_result;+ bool failed;++ Assert(BufferIsValid(buf));++ /*+ * If the entire failed on a lower-level, each buffer needs to be+ * marked as failed. In case of a partial read, some buffers may be+ * ok.+ */+ failed =+ prior_result.status == PGAIO_RS_ERROR+ || prior_result.result <= buf_off;++ buf_result = buffer_writev_complete_one(buf_off, buf, cb_data, failed,+ is_temp);++ /*+ * If there wasn't any prior error and the IO for this page failed in+ * some form, set the whole IO's to the page's result.+ */+ if (result.status != PGAIO_RS_ERROR && buf_result.status != PGAIO_RS_OK)+ {+ result = buf_result;+ pgaio_result_report(result, td, LOG);+ }+ }++ return result;+}+
static void
shared_buffer_readv_stage(PgAioHandle *ioh, uint8 cb_data)
{
buffer_stage_common(ioh, false, false);
}
+static void+shared_buffer_writev_stage(PgAioHandle *ioh, uint8 cb_data)+{+ buffer_stage_common(ioh, true, false);+}+
static PgAioResult
shared_buffer_readv_complete(PgAioHandle *ioh, PgAioResult prior_result,
uint8 cb_data)
@@ -6856,6 +7009,13 @@ shared_buffer_readv_complete(PgAioHandle *ioh, PgAioResult prior_result,
return buffer_readv_complete(ioh, prior_result, cb_data, false);
}
+static PgAioResult+shared_buffer_writev_complete(PgAioHandle *ioh, PgAioResult prior_result,+ uint8 cb_data)+{+ return buffer_writev_complete(ioh, prior_result, cb_data, false);+}+
static void
local_buffer_readv_stage(PgAioHandle *ioh, uint8 cb_data)
{
@@ -6869,6 +7029,17 @@ local_buffer_readv_complete(PgAioHandle *ioh, PgAioResult prior_result,
return buffer_readv_complete(ioh, prior_result, cb_data, true);
}
+static void+local_buffer_writev_stage(PgAioHandle *ioh, uint8 cb_data)+{+ /*+ * Currently this is unreachable as the only write support is for+ * checkpointer / bgwriter, which don't deal with local buffers.+ */+ elog(ERROR, "should be unreachable");+}++
/* readv callback is passed READ_BUFFERS_* flags as callback data */
const PgAioHandleCallbacks aio_shared_buffer_readv_cb = {
.stage = shared_buffer_readv_stage,
@@ -6876,6 +7047,11 @@ const PgAioHandleCallbacks aio_shared_buffer_readv_cb = {
.report = buffer_readv_report,
};
+const PgAioHandleCallbacks aio_shared_buffer_writev_cb = {+ .stage = shared_buffer_writev_stage,+ .complete_shared = shared_buffer_writev_complete,+};+
/* readv callback is passed READ_BUFFERS_* flags as callback data */
const PgAioHandleCallbacks aio_local_buffer_readv_cb = {
.stage = local_buffer_readv_stage,
@@ -6889,3 +7065,7 @@ const PgAioHandleCallbacks aio_local_buffer_readv_cb = {
.complete_local = local_buffer_readv_complete,
.report = buffer_readv_report,
};
++const PgAioHandleCallbacks aio_local_buffer_writev_cb = {+ .stage = local_buffer_writev_stage,+};--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0022-aio-Add-IO-queue-helper.patch (7.4K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/23-v2.11-0022-aio-Add-IO-queue-helper.patch)
download | inline diff:
From 90d1cada1a2782a6bf06c0da69860ddf3c589185 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 22/27] aio: Add IO queue helper
This is likely never going to anywhere - Thomas Munro is working on something
more complete. But I needed a way to exercise aio for checkpointer / bgwriter.
---
src/include/storage/io_queue.h | 33 +++++
src/backend/storage/aio/Makefile | 1 +
src/backend/storage/aio/io_queue.c | 204 ++++++++++++++++++++++++++++
src/backend/storage/aio/meson.build | 1 +
src/tools/pgindent/typedefs.list | 2 +
5 files changed, 241 insertions(+)
create mode 100644 src/include/storage/io_queue.h
create mode 100644 src/backend/storage/aio/io_queue.c
diff --git a/src/include/storage/io_queue.h b/src/include/storage/io_queue.hnew file mode 100644index 00000000000..92b1e9afe6f--- /dev/null+++ b/src/include/storage/io_queue.h@@ -0,0 +1,33 @@+/*-------------------------------------------------------------------------+ *+ * io_queue.h+ * Mechanism for tracking many IOs+ *+ *+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group+ * Portions Copyright (c) 1994, Regents of the University of California+ *+ * src/include/storage/io_queue.h+ *+ *-------------------------------------------------------------------------+ */+#ifndef IO_QUEUE_H+#define IO_QUEUE_H++#include "storage/aio_types.h"++struct IOQueue;+typedef struct IOQueue IOQueue;++struct PgAioWaitRef;++extern IOQueue *io_queue_create(int depth, int flags);+extern void io_queue_track(IOQueue *ioq, const PgAioWaitRef *iow);+extern void io_queue_wait_one(IOQueue *ioq);+extern void io_queue_wait_all(IOQueue *ioq);+extern bool io_queue_is_empty(IOQueue *ioq);+extern void io_queue_reserve(IOQueue *ioq);+extern PgAioHandle *io_queue_acquire_io(IOQueue *ioq);+extern void io_queue_free(IOQueue *ioq);++#endif /* IO_QUEUE_H */diff --git a/src/backend/storage/aio/Makefile b/src/backend/storage/aio/Makefileindex 3f2469cc399..86fa4276fda 100644--- a/src/backend/storage/aio/Makefile+++ b/src/backend/storage/aio/Makefile@@ -15,6 +15,7 @@ OBJS = \
aio_init.o \
aio_io.o \
aio_target.o \
+ io_queue.o \
method_io_uring.o \
method_sync.o \
method_worker.o \
diff --git a/src/backend/storage/aio/io_queue.c b/src/backend/storage/aio/io_queue.cnew file mode 100644index 00000000000..526aa1d5e06--- /dev/null+++ b/src/backend/storage/aio/io_queue.c@@ -0,0 +1,204 @@+/*-------------------------------------------------------------------------+ *+ * io_queue.c+ * AIO - Mechanism for tracking many IOs+ *+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group+ * Portions Copyright (c) 1994, Regents of the University of California+ *+ * IDENTIFICATION+ * src/backend/storage/aio/io_queue.c+ *+ *-------------------------------------------------------------------------+ */++#include "postgres.h"++#include "lib/ilist.h"+#include "storage/aio.h"+#include "storage/io_queue.h"+#include "utils/resowner.h"++++typedef struct TrackedIO+{+ PgAioWaitRef iow;+ dlist_node node;+} TrackedIO;++struct IOQueue+{+ int depth;+ int unsubmitted;++ bool has_reserved;++ dclist_head idle;+ dclist_head in_progress;++ TrackedIO tracked_ios[FLEXIBLE_ARRAY_MEMBER];+};+++IOQueue *+io_queue_create(int depth, int flags)+{+ size_t sz;+ IOQueue *ioq;++ sz = offsetof(IOQueue, tracked_ios)+ + sizeof(TrackedIO) * depth;++ ioq = palloc0(sz);++ ioq->depth = 0;++ for (int i = 0; i < depth; i++)+ {+ TrackedIO *tio = &ioq->tracked_ios[i];++ pgaio_wref_clear(&tio->iow);+ dclist_push_tail(&ioq->idle, &tio->node);+ }++ return ioq;+}++void+io_queue_wait_one(IOQueue *ioq)+{+ /* submit all pending IO before waiting */+ pgaio_submit_staged();++ while (!dclist_is_empty(&ioq->in_progress))+ {+ /* FIXME: Should we really pop here already? */+ dlist_node *node = dclist_pop_head_node(&ioq->in_progress);+ TrackedIO *tio = dclist_container(TrackedIO, node, node);++ pgaio_wref_wait(&tio->iow);+ dclist_push_head(&ioq->idle, &tio->node);+ }+}++void+io_queue_reserve(IOQueue *ioq)+{+ if (ioq->has_reserved)+ return;++ if (dclist_is_empty(&ioq->idle))+ io_queue_wait_one(ioq);++ Assert(!dclist_is_empty(&ioq->idle));++ ioq->has_reserved = true;+}++PgAioHandle *+io_queue_acquire_io(IOQueue *ioq)+{+ PgAioHandle *ioh;++ io_queue_reserve(ioq);++ Assert(!dclist_is_empty(&ioq->idle));++ if (!io_queue_is_empty(ioq))+ {+ ioh = pgaio_io_acquire_nb(CurrentResourceOwner, NULL);+ if (ioh == NULL)+ {+ /*+ * Need to wait for all IOs, blocking might not be legal in the+ * context.+ *+ * XXX: This doesn't make a whole lot of sense, we're also+ * blocking here. What was I smoking when I wrote the above?+ */+ io_queue_wait_all(ioq);+ ioh = pgaio_io_acquire(CurrentResourceOwner, NULL);+ }+ }+ else+ {+ ioh = pgaio_io_acquire(CurrentResourceOwner, NULL);+ }++ return ioh;+}++void+io_queue_track(IOQueue *ioq, const struct PgAioWaitRef *iow)+{+ dlist_node *node;+ TrackedIO *tio;++ Assert(ioq->has_reserved);+ ioq->has_reserved = false;++ Assert(!dclist_is_empty(&ioq->idle));++ node = dclist_pop_head_node(&ioq->idle);+ tio = dclist_container(TrackedIO, node, node);++ tio->iow = *iow;++ dclist_push_tail(&ioq->in_progress, &tio->node);++ ioq->unsubmitted++;++ /*+ * XXX: Should have some smarter logic here. We don't want to wait too+ * long to submit, that'll mean we're more likely to block. But we also+ * don't want to have the overhead of submitting every IO individually.+ */+ if (ioq->unsubmitted >= 4)+ {+ pgaio_submit_staged();+ ioq->unsubmitted = 0;+ }+}++void+io_queue_wait_all(IOQueue *ioq)+{+ /* submit all pending IO before waiting */+ pgaio_submit_staged();++ while (!dclist_is_empty(&ioq->in_progress))+ {+ /* wait for the last IO to minimize unnecessary wakeups */+ dlist_node *node = dclist_tail_node(&ioq->in_progress);+ TrackedIO *tio = dclist_container(TrackedIO, node, node);++ if (!pgaio_wref_check_done(&tio->iow))+ {+ ereport(DEBUG3,+ errmsg("io_queue_wait_all for io:%d",+ pgaio_wref_get_id(&tio->iow)),+ errhidestmt(true),+ errhidecontext(true));++ pgaio_wref_wait(&tio->iow);+ }++ dclist_delete_from(&ioq->in_progress, &tio->node);+ dclist_push_head(&ioq->idle, &tio->node);+ }+}++bool+io_queue_is_empty(IOQueue *ioq)+{+ return dclist_is_empty(&ioq->in_progress);+}++void+io_queue_free(IOQueue *ioq)+{+ io_queue_wait_all(ioq);++ pfree(ioq);+}diff --git a/src/backend/storage/aio/meson.build b/src/backend/storage/aio/meson.buildindex da6df2d3654..270c4a64428 100644--- a/src/backend/storage/aio/meson.build+++ b/src/backend/storage/aio/meson.build@@ -7,6 +7,7 @@ backend_sources += files(
'aio_init.c',
'aio_io.c',
'aio_target.c',
+ 'io_queue.c',
'method_io_uring.c',
'method_sync.c',
'method_worker.c',
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex d633c732162..9c07fa04b49 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -1191,6 +1191,7 @@ IOContext
IOFuncSelector
IOObject
IOOp
+IOQueue
IO_STATUS_BLOCK
IPCompareMethod
ITEM
@@ -3016,6 +3017,7 @@ TocEntry
TokenAuxData
TokenizedAuthLine
TrackItem
+TrackedIO
TransApplyAction
TransInvalidationInfo
TransState
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0023-bufmgr-use-AIO-in-checkpointer-bgwriter.patch (31.3K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/24-v2.11-0023-bufmgr-use-AIO-in-checkpointer-bgwriter.patch)
download | inline diff:
From 920a84a3fbe31183a01a711dc4fc68ba5a57497c Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 23/27] bufmgr: use AIO in checkpointer, bgwriter
This is far from ready - just included to be able to exercise AIO writes and
get some preliminary numbers. In all likelihood this will instead be based
on-top of work by Thomas Munro instead of the preceding commit.
---
src/include/postmaster/bgwriter.h | 3 +-
src/include/storage/buf_internals.h | 2 +
src/include/storage/bufmgr.h | 3 +-
src/include/storage/bufpage.h | 1 +
src/backend/postmaster/bgwriter.c | 19 +-
src/backend/postmaster/checkpointer.c | 11 +-
src/backend/storage/buffer/bufmgr.c | 594 +++++++++++++++++++++++---
src/backend/storage/page/bufpage.c | 10 +
src/tools/pgindent/typedefs.list | 1 +
9 files changed, 586 insertions(+), 58 deletions(-)
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.hindex 800ecbfd13b..a8081d411b6 100644--- a/src/include/postmaster/bgwriter.h+++ b/src/include/postmaster/bgwriter.h@@ -31,7 +31,8 @@ pg_noreturn extern void BackgroundWriterMain(const void *startup_data, size_t st
pg_noreturn extern void CheckpointerMain(const void *startup_data, size_t startup_data_len);
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);+struct IOQueue;+extern void CheckpointWriteDelay(struct IOQueue *ioq, int flags, double progress);
extern bool ForwardSyncRequest(const FileTag *ftag, SyncRequestType type);
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.hindex 4b1f52a9fc8..a3df3192d12 100644--- a/src/include/storage/buf_internals.h+++ b/src/include/storage/buf_internals.h@@ -21,6 +21,8 @@
#include "storage/buf.h"
#include "storage/bufmgr.h"
#include "storage/condition_variable.h"
+#include "storage/io_queue.h"+#include "storage/latch.h"
#include "storage/lwlock.h"
#include "storage/procnumber.h"
#include "storage/shmem.h"
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.hindex ba9bf247ddb..af5035317b7 100644--- a/src/include/storage/bufmgr.h+++ b/src/include/storage/bufmgr.h@@ -295,7 +295,8 @@ extern bool ConditionalLockBufferForCleanup(Buffer buffer);
extern bool IsBufferCleanupOK(Buffer buffer);
extern bool HoldingBufferPinThatDelaysRecovery(void);
-extern bool BgBufferSync(struct WritebackContext *wb_context);+struct IOQueue;+extern bool BgBufferSync(struct IOQueue *ioq, struct WritebackContext *wb_context);
extern uint32 GetPinLimit(void);
extern uint32 GetLocalPinLimit(void);
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.hindex 6646b6f6371..9c045e81857 100644--- a/src/include/storage/bufpage.h+++ b/src/include/storage/bufpage.h@@ -509,5 +509,6 @@ extern bool PageIndexTupleOverwrite(Page page, OffsetNumber offnum,
Item newtup, Size newsize);
extern char *PageSetChecksumCopy(Page page, BlockNumber blkno);
extern void PageSetChecksumInplace(Page page, BlockNumber blkno);
+extern bool PageNeedsChecksumCopy(Page page);
#endif /* BUFPAGE_H */
diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.cindex 72f5acceec7..6e8801a39e3 100644--- a/src/backend/postmaster/bgwriter.c+++ b/src/backend/postmaster/bgwriter.c@@ -38,11 +38,13 @@
#include "postmaster/auxprocess.h"
#include "postmaster/bgwriter.h"
#include "postmaster/interrupt.h"
+#include "storage/aio.h"
#include "storage/aio_subsys.h"
#include "storage/buf_internals.h"
#include "storage/bufmgr.h"
#include "storage/condition_variable.h"
#include "storage/fd.h"
+#include "storage/io_queue.h"
#include "storage/lwlock.h"
#include "storage/proc.h"
#include "storage/procsignal.h"
@@ -90,6 +92,7 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len)
sigjmp_buf local_sigjmp_buf;
MemoryContext bgwriter_context;
bool prev_hibernate;
+ IOQueue *ioq;
WritebackContext wb_context;
Assert(startup_data_len == 0);
@@ -131,6 +134,7 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len)
ALLOCSET_DEFAULT_SIZES);
MemoryContextSwitchTo(bgwriter_context);
+ ioq = io_queue_create(128, 0);
WritebackContextInit(&wb_context, &bgwriter_flush_after);
/*
@@ -228,12 +232,22 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len)
/* Clear any already-pending wakeups */
ResetLatch(MyLatch);
+ /*+ * FIXME: this is theoretically racy, but I didn't want to copy+ * ProcessMainLoopInterrupts() remaining body here.+ */+ if (ShutdownRequestPending)+ {+ io_queue_wait_all(ioq);+ io_queue_free(ioq);+ }+
ProcessMainLoopInterrupts();
/*
* Do one cycle of dirty-buffer writing.
*/
- can_hibernate = BgBufferSync(&wb_context);+ can_hibernate = BgBufferSync(ioq, &wb_context);
/* Report pending statistics to the cumulative stats system */
pgstat_report_bgwriter();
@@ -250,6 +264,9 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len)
smgrdestroyall();
}
+ /* finish IO before sleeping, to avoid blocking other backends */+ io_queue_wait_all(ioq);+
/*
* Log a new xl_running_xacts every now and then so replication can
* get into a consistent state faster (think of suboverflowed
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.cindex fda91ffd1ce..904fe167eb4 100644--- a/src/backend/postmaster/checkpointer.c+++ b/src/backend/postmaster/checkpointer.c@@ -49,10 +49,12 @@
#include "postmaster/bgwriter.h"
#include "postmaster/interrupt.h"
#include "replication/syncrep.h"
+#include "storage/aio.h"
#include "storage/aio_subsys.h"
#include "storage/bufmgr.h"
#include "storage/condition_variable.h"
#include "storage/fd.h"
+#include "storage/io_queue.h"
#include "storage/ipc.h"
#include "storage/lwlock.h"
#include "storage/pmsignal.h"
@@ -766,7 +768,7 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)+CheckpointWriteDelay(IOQueue *ioq, int flags, double progress)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -800,6 +802,13 @@ CheckpointWriteDelay(int flags, double progress)
/* Report interim statistics to the cumulative stats system */
pgstat_report_checkpointer();
+ /*+ * Ensure all pending IO is submitted to avoid unnecessary delays for+ * other processes.+ */+ io_queue_wait_all(ioq);++
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.cindex 94f3caf2224..0909888c342 100644--- a/src/backend/storage/buffer/bufmgr.c+++ b/src/backend/storage/buffer/bufmgr.c@@ -52,6 +52,7 @@
#include "storage/buf_internals.h"
#include "storage/bufmgr.h"
#include "storage/fd.h"
+#include "storage/io_queue.h"
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/proc.h"
@@ -76,6 +77,7 @@
/* Bits in SyncOneBuffer's return value */
#define BUF_WRITTEN 0x01
#define BUF_REUSABLE 0x02
+#define BUF_CANT_MERGE 0x04
#define RELS_BSEARCH_THRESHOLD 20
@@ -515,8 +517,6 @@ static void UnpinBuffer(BufferDesc *buf);
static void UnpinBufferNoOwner(BufferDesc *buf);
static void BufferSync(int flags);
static uint32 WaitBufHdrUnlocked(BufferDesc *buf);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used,- WritebackContext *wb_context);
static void WaitIO(BufferDesc *buf);
static void AbortBufferIO(Buffer buffer);
static void shared_buffer_write_error_callback(void *arg);
@@ -532,6 +532,7 @@ static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_c
static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context);
static void FlushBuffer(BufferDesc *buf, SMgrRelation reln,
IOObject io_object, IOContext io_context);
+
static void FindAndDropRelationBuffers(RelFileLocator rlocator,
ForkNumber forkNum,
BlockNumber nForkBlock,
@@ -3298,6 +3299,57 @@ UnpinBufferNoOwner(BufferDesc *buf)
}
}
+typedef struct BuffersToWrite+{+ int nbuffers;+ BufferTag start_at_tag;+ uint32 max_combine;++ XLogRecPtr max_lsn;++ PgAioHandle *ioh;+ PgAioWaitRef iow;++ uint64 total_writes;++ Buffer buffers[IOV_MAX];+ PgAioBounceBuffer *bounce_buffers[IOV_MAX];+ const void *data_ptrs[IOV_MAX];+} BuffersToWrite;++static int PrepareToWriteBuffer(BuffersToWrite *to_write, Buffer buf,+ bool skip_recently_used,+ IOQueue *ioq, WritebackContext *wb_context);++static void WriteBuffers(BuffersToWrite *to_write,+ IOQueue *ioq, WritebackContext *wb_context);++static void+BuffersToWriteInit(BuffersToWrite *to_write,+ IOQueue *ioq, WritebackContext *wb_context)+{+ to_write->total_writes = 0;+ to_write->nbuffers = 0;+ to_write->ioh = NULL;+ pgaio_wref_clear(&to_write->iow);+ to_write->max_lsn = InvalidXLogRecPtr;++ pgaio_enter_batchmode();+}++static void+BuffersToWriteEnd(BuffersToWrite *to_write)+{+ if (to_write->ioh != NULL)+ {+ pgaio_io_release(to_write->ioh);+ to_write->ioh = NULL;+ }++ pgaio_exit_batchmode();+}++
#define ST_SORT sort_checkpoint_bufferids
#define ST_ELEMENT_TYPE CkptSortItem
#define ST_COMPARE(a, b) ckpt_buforder_comparator(a, b)
@@ -3329,7 +3381,10 @@ BufferSync(int flags)
binaryheap *ts_heap;
int i;
int mask = BM_DIRTY;
+ IOQueue *ioq;
WritebackContext wb_context;
+ BuffersToWrite to_write;+ int max_combine;
/*
* Unless this is a shutdown checkpoint or we have been explicitly told,
@@ -3391,7 +3446,9 @@ BufferSync(int flags)
if (num_to_scan == 0)
return; /* nothing to do */
+ ioq = io_queue_create(512, 0);
WritebackContextInit(&wb_context, &checkpoint_flush_after);
+ max_combine = Min(io_bounce_buffers, io_combine_limit);
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_scan);
@@ -3499,48 +3556,91 @@ BufferSync(int flags)
*/
num_processed = 0;
num_written = 0;
++ BuffersToWriteInit(&to_write, ioq, &wb_context);+
while (!binaryheap_empty(ts_heap))
{
BufferDesc *bufHdr = NULL;
CkptTsStatus *ts_stat = (CkptTsStatus *)
DatumGetPointer(binaryheap_first(ts_heap));
+ bool batch_continue = true;- buf_id = CkptBufferIds[ts_stat->index].buf_id;- Assert(buf_id != -1);-- bufHdr = GetBufferDescriptor(buf_id);-- num_processed++;+ Assert(ts_stat->num_scanned <= ts_stat->num_to_scan);
/*
- * We don't need to acquire the lock here, because we're only looking- * at a single bit. It's possible that someone else writes the buffer- * and clears the flag right after we check, but that doesn't matter- * since SyncOneBuffer will then do nothing. However, there is a- * further race condition: it's conceivable that between the time we- * examine the bit here and the time SyncOneBuffer acquires the lock,- * someone else not only wrote the buffer but replaced it with another- * page and dirtied it. In that improbable case, SyncOneBuffer will- * write the buffer though we didn't need to. It doesn't seem worth- * guarding against this, though.+ * Collect a batch of buffers to write out from the current+ * tablespace. That causes some imbalance between the tablespaces, but+ * that's more than outweighed by the efficiency gain due to batching.
*/
- if (pg_atomic_read_u32(&bufHdr->state) & BM_CHECKPOINT_NEEDED)+ while (batch_continue &&+ to_write.nbuffers < max_combine &&+ ts_stat->num_scanned < ts_stat->num_to_scan)
{
- if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)+ buf_id = CkptBufferIds[ts_stat->index].buf_id;+ Assert(buf_id != -1);++ bufHdr = GetBufferDescriptor(buf_id);++ num_processed++;++ /*+ * We don't need to acquire the lock here, because we're only+ * looking at a single bit. It's possible that someone else writes+ * the buffer and clears the flag right after we check, but that+ * doesn't matter since SyncOneBuffer will then do nothing.+ * However, there is a further race condition: it's conceivable+ * that between the time we examine the bit here and the time+ * SyncOneBuffer acquires the lock, someone else not only wrote+ * the buffer but replaced it with another page and dirtied it. In+ * that improbable case, SyncOneBuffer will write the buffer+ * though we didn't need to. It doesn't seem worth guarding+ * against this, though.+ */+ if (pg_atomic_read_u32(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
{
- TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);- PendingCheckpointerStats.buffers_written++;- num_written++;+ int result = PrepareToWriteBuffer(&to_write, buf_id + 1, false,+ ioq, &wb_context);++ if (result & BUF_CANT_MERGE)+ {+ Assert(to_write.nbuffers > 0);+ WriteBuffers(&to_write, ioq, &wb_context);++ result = PrepareToWriteBuffer(&to_write, buf_id + 1, false,+ ioq, &wb_context);+ Assert(result != BUF_CANT_MERGE);+ }++ if (result & BUF_WRITTEN)+ {+ TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);+ PendingCheckpointerStats.buffers_written++;+ num_written++;+ }+ else+ {+ batch_continue = false;+ }
}
+ else+ {+ if (to_write.nbuffers > 0)+ WriteBuffers(&to_write, ioq, &wb_context);+ }++ /*+ * Measure progress independent of actually having to flush the+ * buffer - otherwise writing become unbalanced.+ */+ ts_stat->progress += ts_stat->progress_slice;+ ts_stat->num_scanned++;+ ts_stat->index++;
}
- /*- * Measure progress independent of actually having to flush the buffer- * - otherwise writing become unbalanced.- */- ts_stat->progress += ts_stat->progress_slice;- ts_stat->num_scanned++;- ts_stat->index++;+ if (to_write.nbuffers > 0)+ WriteBuffers(&to_write, ioq, &wb_context);+
/* Have all the buffers from the tablespace been processed? */
if (ts_stat->num_scanned == ts_stat->num_to_scan)
@@ -3558,15 +3658,23 @@ BufferSync(int flags)
*
* (This will check for barrier events even if it doesn't sleep.)
*/
- CheckpointWriteDelay(flags, (double) num_processed / num_to_scan);+ CheckpointWriteDelay(ioq, flags, (double) num_processed / num_to_scan);
}
+ Assert(to_write.nbuffers == 0);+ io_queue_wait_all(ioq);+
/*
* Issue all pending flushes. Only checkpointer calls BufferSync(), so
* IOContext will always be IOCONTEXT_NORMAL.
*/
IssuePendingWritebacks(&wb_context, IOCONTEXT_NORMAL);
+ io_queue_wait_all(ioq); /* IssuePendingWritebacks might have added+ * more */+ io_queue_free(ioq);+ BuffersToWriteEnd(&to_write);+
pfree(per_ts_stat);
per_ts_stat = NULL;
binaryheap_free(ts_heap);
@@ -3592,7 +3700,7 @@ BufferSync(int flags)
* bgwriter_lru_maxpages to 0.)
*/
bool
-BgBufferSync(WritebackContext *wb_context)+BgBufferSync(IOQueue *ioq, WritebackContext *wb_context)
{
/* info obtained from freelist.c */
int strategy_buf_id;
@@ -3635,6 +3743,9 @@ BgBufferSync(WritebackContext *wb_context)
long new_strategy_delta;
uint32 new_recent_alloc;
+ BuffersToWrite to_write;+ int max_combine;+
/*
* Find out where the freelist clock sweep currently is, and how many
* buffer allocations have happened since our last call.
@@ -3655,6 +3766,8 @@ BgBufferSync(WritebackContext *wb_context)
return true;
}
+ max_combine = Min(io_bounce_buffers, io_combine_limit);+
/*
* Compute strategy_delta = how many buffers have been scanned by the
* clock sweep since last time. If first time through, assume none. Then
@@ -3811,11 +3924,25 @@ BgBufferSync(WritebackContext *wb_context)
num_written = 0;
reusable_buffers = reusable_buffers_est;
+ BuffersToWriteInit(&to_write, ioq, wb_context);+
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int sync_state = SyncOneBuffer(next_to_clean, true,- wb_context);+ int sync_state;++ sync_state = PrepareToWriteBuffer(&to_write, next_to_clean + 1,+ true, ioq, wb_context);+ if (sync_state & BUF_CANT_MERGE)+ {+ Assert(to_write.nbuffers > 0);++ WriteBuffers(&to_write, ioq, wb_context);++ sync_state = PrepareToWriteBuffer(&to_write, next_to_clean + 1,+ true, ioq, wb_context);+ Assert(sync_state != BUF_CANT_MERGE);+ }
if (++next_to_clean >= NBuffers)
{
@@ -3826,6 +3953,13 @@ BgBufferSync(WritebackContext *wb_context)
if (sync_state & BUF_WRITTEN)
{
+ Assert(sync_state & BUF_REUSABLE);++ if (to_write.nbuffers == max_combine)+ {+ WriteBuffers(&to_write, ioq, wb_context);+ }+
reusable_buffers++;
if (++num_written >= bgwriter_lru_maxpages)
{
@@ -3837,6 +3971,11 @@ BgBufferSync(WritebackContext *wb_context)
reusable_buffers++;
}
+ if (to_write.nbuffers > 0)+ WriteBuffers(&to_write, ioq, wb_context);++ BuffersToWriteEnd(&to_write);+
PendingBgWriterStats.buf_written_clean += num_written;
#ifdef BGW_DEBUG
@@ -3875,8 +4014,66 @@ BgBufferSync(WritebackContext *wb_context)
return (bufs_to_lap == 0 && recent_alloc == 0);
}
+static inline bool+BufferTagsSameRel(const BufferTag *tag1, const BufferTag *tag2)+{+ return (tag1->spcOid == tag2->spcOid) &&+ (tag1->dbOid == tag2->dbOid) &&+ (tag1->relNumber == tag2->relNumber) &&+ (tag1->forkNum == tag2->forkNum)+ ;+}++static bool+CanMergeWrite(BuffersToWrite *to_write, BufferDesc *cur_buf_hdr)+{+ BlockNumber cur_block = cur_buf_hdr->tag.blockNum;++ Assert(to_write->nbuffers > 0); /* can't merge with nothing */+ Assert(to_write->start_at_tag.relNumber != InvalidOid);+ Assert(to_write->start_at_tag.blockNum != InvalidBlockNumber);++ Assert(to_write->ioh != NULL);++ /*+ * First check if the blocknumber is one that we could actually merge,+ * that's cheaper than checking the tablespace/db/relnumber/fork match.+ */+ if (to_write->start_at_tag.blockNum + to_write->nbuffers != cur_block)+ return false;++ if (!BufferTagsSameRel(&to_write->start_at_tag, &cur_buf_hdr->tag))+ return false;++ /*+ * Need to check with smgr how large a write we're allowed to make. To+ * reduce the overhead of the smgr check, only inquire once, when+ * processing the first to-be-merged buffer. That avoids the overhead in+ * the common case of writing out buffers that definitely not mergeable.+ */+ if (to_write->nbuffers == 1)+ {+ SMgrRelation smgr;++ smgr = smgropen(BufTagGetRelFileLocator(&to_write->start_at_tag), INVALID_PROC_NUMBER);++ to_write->max_combine = smgrmaxcombine(smgr,+ to_write->start_at_tag.forkNum,+ to_write->start_at_tag.blockNum);+ }+ else+ {+ Assert(to_write->max_combine > 0);+ }++ if (to_write->start_at_tag.blockNum + to_write->max_combine <= cur_block)+ return false;++ return true;+}+
/*
- * SyncOneBuffer -- process a single buffer during syncing.+ * PrepareToWriteBuffer -- process a single buffer during syncing.
*
* If skip_recently_used is true, we don't write currently-pinned buffers, nor
* buffers marked recently used, as these are not replacement candidates.
@@ -3885,22 +4082,50 @@ BgBufferSync(WritebackContext *wb_context)
* BUF_WRITTEN: we wrote the buffer.
* BUF_REUSABLE: buffer is available for replacement, ie, it has
* pin count 0 and usage count 0.
+ * BUF_CANT_MERGE: can't combine this write with prior writes, caller needs+ * to issue those first
*
* (BUF_WRITTEN could be set in error if FlushBuffer finds the buffer clean
* after locking it, but we don't care all that much.)
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)+PrepareToWriteBuffer(BuffersToWrite *to_write, Buffer buf,+ bool skip_recently_used,+ IOQueue *ioq, WritebackContext *wb_context)
{
- BufferDesc *bufHdr = GetBufferDescriptor(buf_id);+ BufferDesc *cur_buf_hdr = GetBufferDescriptor(buf - 1);+ uint32 buf_state;
int result = 0;
- uint32 buf_state;- BufferTag tag;+ XLogRecPtr cur_buf_lsn;+ LWLock *content_lock;+ bool may_block;++ /*+ * Check if this buffer can be written out together with already prepared+ * writes. We check before we have pinned the buffer, so the buffer can be+ * written out and replaced between this check and us pinning the buffer -+ * we'll recheck below. The reason for the pre-check is that we don't want+ * to pin the buffer just to find out that we can't merge the IO.+ */+ if (to_write->nbuffers != 0)+ {+ if (!CanMergeWrite(to_write, cur_buf_hdr))+ {+ result |= BUF_CANT_MERGE;+ return result;+ }+ }+ else+ {+ to_write->start_at_tag = cur_buf_hdr->tag;+ }
/* Make sure we can handle the pin */
ReservePrivateRefCountEntry();
ResourceOwnerEnlarge(CurrentResourceOwner);
+ /* XXX: Should also check if we are allowed to pin one more buffer */+
/*
* Check whether buffer needs writing.
*
@@ -3910,7 +4135,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
* don't worry because our checkpoint.redo points before log record for
* upcoming changes and so we are not required to write such dirty buffer.
*/
- buf_state = LockBufHdr(bufHdr);+ buf_state = LockBufHdr(cur_buf_hdr);
if (BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
BUF_STATE_GET_USAGECOUNT(buf_state) == 0)
@@ -3919,40 +4144,300 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
}
else if (skip_recently_used)
{
+#if 0+ elog(LOG, "at block %d: skip recent with nbuffers %d",+ cur_buf_hdr->tag.blockNum, to_write->nbuffers);+#endif
/* Caller told us not to write recently-used buffers */
- UnlockBufHdr(bufHdr, buf_state);+ UnlockBufHdr(cur_buf_hdr, buf_state);
return result;
}
if (!(buf_state & BM_VALID) || !(buf_state & BM_DIRTY))
{
/* It's clean, so nothing to do */
- UnlockBufHdr(bufHdr, buf_state);+ UnlockBufHdr(cur_buf_hdr, buf_state);
return result;
}
+ /* pin the buffer, from now on its identity can't change anymore */+ PinBuffer_Locked(cur_buf_hdr);++ /*+ * Acquire IO, if needed, now that it's likely that we'll need to write.+ */+ if (to_write->ioh == NULL)+ {+ /* otherwise we should already have acquired a handle */+ Assert(to_write->nbuffers == 0);++ to_write->ioh = io_queue_acquire_io(ioq);+ pgaio_io_get_wref(to_write->ioh, &to_write->iow);+ }++ /*+ * If we are merging, check if the buffer's identity possibly changed+ * while we hadn't yet pinned it.+ *+ * XXX: It might be worth checking if we still want to write the buffer+ * out, e.g. it could have been replaced with a buffer that doesn't have+ * BM_CHECKPOINT_NEEDED set.+ */+ if (to_write->nbuffers != 0)+ {+ if (!CanMergeWrite(to_write, cur_buf_hdr))+ {+ elog(LOG, "changed identity");+ UnpinBuffer(cur_buf_hdr);++ result |= BUF_CANT_MERGE;++ return result;+ }+ }++ may_block = to_write->nbuffers == 0+ && !pgaio_have_staged()+ && io_queue_is_empty(ioq)+ ;+ content_lock = BufferDescriptorGetContentLock(cur_buf_hdr);++ if (!may_block)+ {+ if (LWLockConditionalAcquire(content_lock, LW_SHARED))+ {+ /* done */+ }+ else if (to_write->nbuffers == 0)+ {+ /*+ * Need to wait for all prior IO to finish before blocking for+ * lock acquisition, to avoid the risk a deadlock due to us+ * waiting for another backend that is waiting for our unsubmitted+ * IO to complete.+ */+ pgaio_submit_staged();+ io_queue_wait_all(ioq);++ elog(DEBUG2, "at block %u: can't block, nbuffers = 0",+ cur_buf_hdr->tag.blockNum+ );++ may_block = to_write->nbuffers == 0+ && !pgaio_have_staged()+ && io_queue_is_empty(ioq)+ ;+ Assert(may_block);++ LWLockAcquire(content_lock, LW_SHARED);+ }+ else+ {+ elog(DEBUG2, "at block %d: can't block nbuffers = %d",+ cur_buf_hdr->tag.blockNum,+ to_write->nbuffers);++ UnpinBuffer(cur_buf_hdr);+ result |= BUF_CANT_MERGE;+ Assert(to_write->nbuffers > 0);++ return result;+ }+ }+ else+ {+ LWLockAcquire(content_lock, LW_SHARED);+ }++ if (!may_block)+ {+ if (!StartBufferIO(cur_buf_hdr, false, !may_block))+ {+ pgaio_submit_staged();+ io_queue_wait_all(ioq);++ may_block = io_queue_is_empty(ioq) && to_write->nbuffers == 0 && !pgaio_have_staged();++ if (!StartBufferIO(cur_buf_hdr, false, !may_block))+ {+ elog(DEBUG2, "at block %d: non-waitable StartBufferIO returns false, %d",+ cur_buf_hdr->tag.blockNum,+ may_block);++ /*+ * FIXME: can't tell whether this is because the buffer has+ * been cleaned+ */+ if (!may_block)+ {+ result |= BUF_CANT_MERGE;+ Assert(to_write->nbuffers > 0);+ }+ LWLockRelease(content_lock);+ UnpinBuffer(cur_buf_hdr);++ return result;+ }+ }+ }+ else+ {+ if (!StartBufferIO(cur_buf_hdr, false, false))+ {+ elog(DEBUG2, "waitable StartBufferIO returns false");+ LWLockRelease(content_lock);+ UnpinBuffer(cur_buf_hdr);++ /*+ * FIXME: Historically we returned BUF_WRITTEN in this case, which+ * seems wrong+ */+ return result;+ }+ }+
/*
- * Pin it, share-lock it, write it. (FlushBuffer will do nothing if the- * buffer is clean by the time we've locked it.)+ * Run PageGetLSN while holding header lock, since we don't have the+ * buffer locked exclusively in all cases.
*/
- PinBuffer_Locked(bufHdr);- LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);+ buf_state = LockBufHdr(cur_buf_hdr);++ cur_buf_lsn = BufferGetLSN(cur_buf_hdr);- FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);+ /* To check if block content changes while flushing. - vadim 01/17/97 */+ buf_state &= ~BM_JUST_DIRTIED;- LWLockRelease(BufferDescriptorGetContentLock(bufHdr));+ UnlockBufHdr(cur_buf_hdr, buf_state);- tag = bufHdr->tag;+ to_write->buffers[to_write->nbuffers] = buf;+ to_write->nbuffers++;- UnpinBuffer(bufHdr);+ if (buf_state & BM_PERMANENT &&+ (to_write->max_lsn == InvalidXLogRecPtr || to_write->max_lsn < cur_buf_lsn))+ {+ to_write->max_lsn = cur_buf_lsn;+ }++ result |= BUF_WRITTEN;++ return result;+}++static void+WriteBuffers(BuffersToWrite *to_write,+ IOQueue *ioq, WritebackContext *wb_context)+{+ SMgrRelation smgr;+ Buffer first_buf;+ BufferDesc *first_buf_hdr;+ bool needs_checksum;++ Assert(to_write->nbuffers > 0 && to_write->nbuffers <= io_combine_limit);++ first_buf = to_write->buffers[0];+ first_buf_hdr = GetBufferDescriptor(first_buf - 1);++ smgr = smgropen(BufTagGetRelFileLocator(&first_buf_hdr->tag), INVALID_PROC_NUMBER);
/*
- * SyncOneBuffer() is only called by checkpointer and bgwriter, so- * IOContext will always be IOCONTEXT_NORMAL.+ * Force XLOG flush up to buffer's LSN. This implements the basic WAL+ * rule that log updates must hit disk before any of the data-file changes+ * they describe do.+ *+ * However, this rule does not apply to unlogged relations, which will be+ * lost after a crash anyway. Most unlogged relation pages do not bear+ * LSNs since we never emit WAL records for them, and therefore flushing+ * up through the buffer LSN would be useless, but harmless. However,+ * GiST indexes use LSNs internally to track page-splits, and therefore+ * unlogged GiST pages bear "fake" LSNs generated by+ * GetFakeLSNForUnloggedRel. It is unlikely but possible that the fake+ * LSN counter could advance past the WAL insertion point; and if it did+ * happen, attempting to flush WAL through that location would fail, with+ * disastrous system-wide consequences. To make sure that can't happen,+ * skip the flush if the buffer isn't permanent.
*/
- ScheduleBufferTagForWriteback(wb_context, IOCONTEXT_NORMAL, &tag);+ if (to_write->max_lsn != InvalidXLogRecPtr)+ XLogFlush(to_write->max_lsn);++ /*+ * Now it's safe to write the buffer to disk. Note that no one else should+ * have been able to write it, while we were busy with log flushing,+ * because we got the exclusive right to perform I/O by setting the+ * BM_IO_IN_PROGRESS bit.+ */++ for (int nbuf = 0; nbuf < to_write->nbuffers; nbuf++)+ {+ Buffer cur_buf = to_write->buffers[nbuf];+ BufferDesc *cur_buf_hdr = GetBufferDescriptor(cur_buf - 1);+ Block bufBlock;+ char *bufToWrite;++ bufBlock = BufHdrGetBlock(cur_buf_hdr);+ needs_checksum = PageNeedsChecksumCopy((Page) bufBlock);++ /*+ * Update page checksum if desired. Since we have only shared lock on+ * the buffer, other processes might be updating hint bits in it, so+ * we must copy the page to a bounce buffer if we do checksumming.+ */+ if (needs_checksum)+ {+ PgAioBounceBuffer *bb = pgaio_bounce_buffer_get();- return result | BUF_WRITTEN;+ pgaio_io_assoc_bounce_buffer(to_write->ioh, bb);++ bufToWrite = pgaio_bounce_buffer_buffer(bb);+ memcpy(bufToWrite, bufBlock, BLCKSZ);+ PageSetChecksumInplace((Page) bufToWrite, cur_buf_hdr->tag.blockNum);+ }+ else+ {+ bufToWrite = bufBlock;+ }++ to_write->data_ptrs[nbuf] = bufToWrite;+ }++ pgaio_io_set_handle_data_32(to_write->ioh,+ (uint32 *) to_write->buffers,+ to_write->nbuffers);+ pgaio_io_register_callbacks(to_write->ioh, PGAIO_HCB_SHARED_BUFFER_WRITEV, 0);++ smgrstartwritev(to_write->ioh, smgr,+ BufTagGetForkNum(&first_buf_hdr->tag),+ first_buf_hdr->tag.blockNum,+ to_write->data_ptrs,+ to_write->nbuffers,+ false);+ pgstat_count_io_op(IOOBJECT_RELATION, IOCONTEXT_NORMAL,+ IOOP_WRITE, 1, BLCKSZ * to_write->nbuffers);+++ for (int nbuf = 0; nbuf < to_write->nbuffers; nbuf++)+ {+ Buffer cur_buf = to_write->buffers[nbuf];+ BufferDesc *cur_buf_hdr = GetBufferDescriptor(cur_buf - 1);++ UnpinBuffer(cur_buf_hdr);+ }++ io_queue_track(ioq, &to_write->iow);+ to_write->total_writes++;++ /* clear state for next write */+ to_write->nbuffers = 0;+ to_write->start_at_tag.relNumber = InvalidOid;+ to_write->start_at_tag.blockNum = InvalidBlockNumber;+ to_write->max_combine = 0;+ to_write->max_lsn = InvalidXLogRecPtr;+ to_write->ioh = NULL;+ pgaio_wref_clear(&to_write->iow);++ /*+ * FIXME: Implement issuing writebacks (note wb_context isn't used here).+ * Possibly needs to be integrated with io_queue.c.+ */
}
/*
@@ -4326,6 +4811,7 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
error_context_stack = errcallback.previous;
}
+
/*
* RelationGetNumberOfBlocksInFork
* Determines the current number of pages in the specified relation fork.
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.cindex ecc81aacfc3..71dc6f559dd 100644--- a/src/backend/storage/page/bufpage.c+++ b/src/backend/storage/page/bufpage.c@@ -1480,6 +1480,16 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum,
return true;
}
+bool+PageNeedsChecksumCopy(Page page)+{+ if (PageIsNew(page))+ return false;++ /* If we don't need a checksum, just return the passed-in data */+ return DataChecksumsEnabled();+}+
/*
* Set checksum for a page in shared buffers.
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.listindex 9c07fa04b49..96144af6a4a 100644--- a/src/tools/pgindent/typedefs.list+++ b/src/tools/pgindent/typedefs.list@@ -348,6 +348,7 @@ BufferManagerRelation
BufferStrategyControl
BufferTag
BufferUsage
+BuffersToWrite
BuildAccumulator
BuiltinScript
BulkInsertState
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0024-Ensure-a-resowner-exists-for-all-paths-that-ma.patch (2.6K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/25-v2.11-0024-Ensure-a-resowner-exists-for-all-paths-that-ma.patch)
download | inline diff:
From ae835445290a70888f0969d5ce1da973ca1a728c Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 24/27] Ensure a resowner exists for all paths that may
perform AIO
Reviewed-by: Noah Misch <noah@leadboat.com>
Discussion: https://postgr.es/m/1f6b50a7-38ef-4d87-8246-786d39f46ab9@iki.fi
---
src/backend/bootstrap/bootstrap.c | 7 +++++++
src/backend/replication/logical/logical.c | 6 ++++++
src/backend/utils/init/postinit.c | 6 +++++-
3 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.cindex 6db864892d0..e554504e1f0 100644--- a/src/backend/bootstrap/bootstrap.c+++ b/src/backend/bootstrap/bootstrap.c@@ -361,8 +361,15 @@ BootstrapModeMain(int argc, char *argv[], bool check_only)
BaseInit();
bootstrap_signals();
++ /* need a resowner for IO during BootStrapXLOG() */+ CreateAuxProcessResourceOwner();+
BootStrapXLOG(bootstrap_data_checksum_version);
+ ReleaseAuxProcessResources(true);+ CurrentResourceOwner = NULL;+
/*
* To ensure that src/common/link-canary.c is linked into the backend, we
* must call it from somewhere. Here is as good as anywhere.
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.cindex 8ea846bfc3b..2b936714040 100644--- a/src/backend/replication/logical/logical.c+++ b/src/backend/replication/logical/logical.c@@ -386,6 +386,12 @@ CreateInitDecodingContext(const char *plugin,
slot->data.plugin = plugin_name;
SpinLockRelease(&slot->mutex);
+ if (CurrentResourceOwner == NULL)+ {+ Assert(am_walsender);+ CurrentResourceOwner = AuxProcessResourceOwner;+ }+
if (XLogRecPtrIsInvalid(restart_lsn))
ReplicationSlotReserveWal();
else
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.cindex 7958ea11b73..222e24bcb08 100644--- a/src/backend/utils/init/postinit.c+++ b/src/backend/utils/init/postinit.c@@ -785,8 +785,12 @@ InitPostgres(const char *in_dbname, Oid dboid,
* We don't yet have an aux-process resource owner, but StartupXLOG
* and ShutdownXLOG will need one. Hence, create said resource owner
* (and register a callback to clean it up after ShutdownXLOG runs).
+ *+ * In bootstrap mode CreateAuxProcessResourceOwner() was already+ * called in BootstrapModeMain().
*/
- CreateAuxProcessResourceOwner();+ if (!bootstrap)+ CreateAuxProcessResourceOwner();
StartupXLOG();
/* Release (and warn about) any buffer pins leaked in StartupXLOG */
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0025-Temporary-Increase-BAS_BULKREAD-size.patch (1.3K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/26-v2.11-0025-Temporary-Increase-BAS_BULKREAD-size.patch)
download | inline diff:
From e3e9545899e6b2ea34e589f88f427472a803e2ff Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 25/27] Temporary: Increase BAS_BULKREAD size
Without this we only can execute very little AIO for sequential scans, as
there's just not enough buffers in the ring. This isn't the right fix, as
just increasing the ring size can have negative performance implications in
workloads where the kernel has all the data cached.
Author:
Reviewed-By:
Discussion: https://postgr.es/m/
Backpatch:
---
src/backend/storage/buffer/freelist.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.cindex 336715b6c63..b72a5957a20 100644--- a/src/backend/storage/buffer/freelist.c+++ b/src/backend/storage/buffer/freelist.c@@ -555,7 +555,12 @@ GetAccessStrategy(BufferAccessStrategyType btype)
return NULL;
case BAS_BULKREAD:
- ring_size_kb = 256;++ /*+ * FIXME: Temporary increase to allow large enough streaming reads+ * to actually benefit from AIO. This needs a better solution.+ */+ ring_size_kb = 2 * 1024;
break;
case BAS_BULKWRITE:
ring_size_kb = 16 * 1024;
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0026-WIP-Use-MAP_POPULATE.patch (1.1K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/27-v2.11-0026-WIP-Use-MAP_POPULATE.patch)
download | inline diff:
From 9cba27b9fb50afe9b01d97bbfa967dcf12d69779 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 26/27] WIP: Use MAP_POPULATE
For benchmarking it's quite annoying that the first time a memory is touched
has completely different perf characteristics than subsequent accesses. Using
MAP_POPULATE reduces that substantially.
---
src/backend/port/sysv_shmem.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/backend/port/sysv_shmem.c b/src/backend/port/sysv_shmem.cindex 197926d44f6..a700b02d5a1 100644--- a/src/backend/port/sysv_shmem.c+++ b/src/backend/port/sysv_shmem.c@@ -620,7 +620,7 @@ CreateAnonymousSegment(Size *size)
allocsize += hugepagesize - (allocsize % hugepagesize);
ptr = mmap(NULL, allocsize, PROT_READ | PROT_WRITE,
- PG_MMAP_FLAGS | mmap_flags, -1, 0);+ PG_MMAP_FLAGS | MAP_POPULATE | mmap_flags, -1, 0);
mmap_errno = errno;
if (huge_pages == HUGE_PAGES_TRY && ptr == MAP_FAILED)
elog(DEBUG1, "mmap(%zu) with MAP_HUGETLB failed, huge pages disabled: %m",
--
2.48.1.76.g4e746b1a31.dirty
[text/x-diff] v2.11-0027-StartReadBuffers-debug-stuff.patch (1.9K, ../6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22/28-v2.11-0027-StartReadBuffers-debug-stuff.patch)
download | inline diff:
From af56fb208e9b6f45de7c2f1b2d8e25214d2370c3 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Tue, 18 Mar 2025 14:40:06 -0400
Subject: [PATCH v2.11 27/27] StartReadBuffers debug stuff
---
src/backend/storage/buffer/bufmgr.c | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.cindex 0909888c342..ba695badbeb 100644--- a/src/backend/storage/buffer/bufmgr.c+++ b/src/backend/storage/buffer/bufmgr.c@@ -1781,6 +1781,18 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
IOObject io_object;
bool did_start_io;
+#if 0+ ereport(DEBUG3,+ errmsg("%s: op->blocks: %d, op->blocks_done: %d, *nblocks_progress: %d, first buf %d",+ __func__,+ operation->nblocks,+ nblocks_done,+ *nblocks_progress,+ buffers[0]),+ errhidestmt(true),+ errhidecontext(true));+#endif+
/*
* When this IO is executed synchronously, either because the caller will
* immediately block waiting for the IO or because IOMETHOD_SYNC is used,
@@ -1845,6 +1857,13 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
operation->nblocks_done += 1;
*nblocks_progress = 1;
+ ereport(DEBUG3,+ errmsg("%s - trunc: %d",+ __func__,+ operation->nblocks_done),+ errhidestmt(true),+ errhidecontext(true));+
pgaio_io_release(ioh);
pgaio_wref_clear(&operation->io_wref);
did_start_io = false;
@@ -1894,6 +1913,12 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
*/
for (int i = nblocks_done + 1; i < operation->nblocks; i++)
{
+#if 0+ /* FIXME: Remove forced short read */+ if (i > 3)+ break;+#endif+
if (!ReadBuffersCanStartIO(buffers[i], true))
break;
/* Must be consecutive block numbers. */
--
2.48.1.76.g4e746b1a31.dirty
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: andres@anarazel.de, ah@cybertec.at, noah@leadboat.com, thomas.munro@gmail.com, hlinnaka@iki.fi, robertmhaas@gmail.com, jakub.wartak@enterprisedb.com, postgres@jeltef.nl
Subject: Re: AIO v2.5
In-Reply-To: <6ak556uyqiptdwjaci4kbi5eykwkmzqgkbtkyaosjnopjhncrc@2v4ac2jwyz22>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox