public inbox for [email protected]
help / color / mirror / Atom feed[PATCH v4] pg_rewind: options to use restore_command from command line or cluster config
19+ messages / 4 participants
[nested] [flat]
* [PATCH v4] pg_rewind: options to use restore_command from command line or cluster config
@ 2019-02-19 16:14 Alexey Kondratov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Alexey Kondratov @ 2019-02-19 16:14 UTC (permalink / raw)
---
doc/src/sgml/ref/pg_rewind.sgml | 30 ++++-
src/bin/pg_rewind/parsexlog.c | 161 +++++++++++++++++++++++++-
src/bin/pg_rewind/pg_rewind.c | 98 +++++++++++++++-
src/bin/pg_rewind/pg_rewind.h | 7 +-
src/bin/pg_rewind/t/001_basic.pl | 4 +-
src/bin/pg_rewind/t/002_databases.pl | 4 +-
src/bin/pg_rewind/t/003_extrafiles.pl | 4 +-
src/bin/pg_rewind/t/RewindTest.pm | 84 +++++++++++++-
8 files changed, 372 insertions(+), 20 deletions(-)
diff --git a/doc/src/sgml/ref/pg_rewind.sgml b/doc/src/sgml/ref/pg_rewind.sgml
index 53a64ee29e..0c2441afa7 100644
--- a/doc/src/sgml/ref/pg_rewind.sgml
+++ b/doc/src/sgml/ref/pg_rewind.sgml
@@ -67,8 +67,10 @@ PostgreSQL documentation
ancestor. In the typical failover scenario where the target cluster was
shut down soon after the divergence, this is not a problem, but if the
target cluster ran for a long time after the divergence, the old WAL
- files might no longer be present. In that case, they can be manually
- copied from the WAL archive to the <filename>pg_wal</filename> directory, or
+ files might no longer be present. In that case, they can be automatically
+ copied by <application>pg_rewind</application> from the WAL archive to the
+ <filename>pg_wal</filename> directory if either <literal>-r</literal> or
+ <literal>-R</literal> option is specified, or
fetched on startup by configuring <xref linkend="guc-primary-conninfo"/> or
<xref linkend="guc-restore-command"/>. The use of
<application>pg_rewind</application> is not limited to failover, e.g. a standby
@@ -200,6 +202,30 @@ PostgreSQL documentation
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>-r</option></term>
+ <term><option>--use-postgresql-conf</option></term>
+ <listitem>
+ <para>
+ Use restore_command in the <filename>postgresql.conf</filename> to
+ retreive missing in the target <filename>pg_wal</filename> directory
+ WAL files from the WAL archive.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>-R <replaceable class="parameter">restore_command</replaceable></option></term>
+ <term><option>--restore-command=<replaceable class="parameter">restore_command</replaceable></option></term>
+ <listitem>
+ <para>
+ Specifies the restore_command to use for retrieval of the missing
+ in the target <filename>pg_wal</filename> directory WAL files from
+ the WAL archive.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>--debug</option></term>
<listitem>
diff --git a/src/bin/pg_rewind/parsexlog.c b/src/bin/pg_rewind/parsexlog.c
index e19c265cbb..5978ec9b99 100644
--- a/src/bin/pg_rewind/parsexlog.c
+++ b/src/bin/pg_rewind/parsexlog.c
@@ -12,6 +12,7 @@
#include "postgres_fe.h"
#include <unistd.h>
+#include <sys/stat.h>
#include "pg_rewind.h"
#include "filemap.h"
@@ -45,6 +46,7 @@ static char xlogfpath[MAXPGPATH];
typedef struct XLogPageReadPrivate
{
const char *datadir;
+ const char *restoreCommand;
int tliIndex;
} XLogPageReadPrivate;
@@ -53,6 +55,9 @@ static int SimpleXLogPageRead(XLogReaderState *xlogreader,
int reqLen, XLogRecPtr targetRecPtr, char *readBuf,
TimeLineID *pageTLI);
+static int RestoreArchivedWAL(const char *path, const char *xlogfname,
+ off_t expectedSize, const char *restoreCommand);
+
/*
* Read WAL from the datadir/pg_wal, starting from 'startpoint' on timeline
* index 'tliIndex' in target timeline history, until 'endpoint'. Make note of
@@ -60,7 +65,7 @@ static int SimpleXLogPageRead(XLogReaderState *xlogreader,
*/
void
extractPageMap(const char *datadir, XLogRecPtr startpoint, int tliIndex,
- XLogRecPtr endpoint)
+ XLogRecPtr endpoint, const char *restore_command)
{
XLogRecord *record;
XLogReaderState *xlogreader;
@@ -69,6 +74,7 @@ extractPageMap(const char *datadir, XLogRecPtr startpoint, int tliIndex,
private.datadir = datadir;
private.tliIndex = tliIndex;
+ private.restoreCommand = restore_command;
xlogreader = XLogReaderAllocate(WalSegSz, &SimpleXLogPageRead,
&private);
if (xlogreader == NULL)
@@ -156,7 +162,7 @@ readOneRecord(const char *datadir, XLogRecPtr ptr, int tliIndex)
void
findLastCheckpoint(const char *datadir, XLogRecPtr forkptr, int tliIndex,
XLogRecPtr *lastchkptrec, TimeLineID *lastchkpttli,
- XLogRecPtr *lastchkptredo)
+ XLogRecPtr *lastchkptredo, const char *restoreCommand)
{
/* Walk backwards, starting from the given record */
XLogRecord *record;
@@ -181,6 +187,7 @@ findLastCheckpoint(const char *datadir, XLogRecPtr forkptr, int tliIndex,
private.datadir = datadir;
private.tliIndex = tliIndex;
+ private.restoreCommand = restoreCommand;
xlogreader = XLogReaderAllocate(WalSegSz, &SimpleXLogPageRead,
&private);
if (xlogreader == NULL)
@@ -291,9 +298,30 @@ SimpleXLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr,
if (xlogreadfd < 0)
{
- printf(_("could not open file \"%s\": %s\n"), xlogfpath,
- strerror(errno));
- return -1;
+ /*
+ * If we have no restore_command to execute, then exit.
+ */
+ if (private->restoreCommand == NULL)
+ {
+ printf(_("could not open file \"%s\": %s\n"), xlogfpath,
+ strerror(errno));
+ return -1;
+ }
+
+ /*
+ * Since we have restore_command to execute, then try to retreive
+ * missing WAL file from the archive.
+ */
+ xlogreadfd = RestoreArchivedWAL(private->datadir,
+ xlogfname,
+ WalSegSz,
+ private->restoreCommand);
+
+ if (xlogreadfd < 0)
+ return -1;
+ else
+ pg_log(PG_DEBUG, "using restored from archive version of file \"%s\"\n",
+ xlogfpath);
}
}
@@ -409,3 +437,126 @@ extractPageInfo(XLogReaderState *record)
process_block_change(forknum, rnode, blkno);
}
}
+
+/*
+ * Attempt to retrieve the specified file from off-line archival storage.
+ * If successful return a file descriptor of restored WAL file, else
+ * return -1.
+ *
+ * For fixed-size files, the caller may pass the expected size as an
+ * additional crosscheck on successful recovery. If the file size is not
+ * known, set expectedSize = 0.
+ *
+ * This is a simplified and adapted to frontend version of
+ * RestoreArchivedFile function from transam/xlogarchive.c
+ */
+static int
+RestoreArchivedWAL(const char *path, const char *xlogfname,
+ off_t expectedSize, const char *restoreCommand)
+{
+ char xlogpath[MAXPGPATH],
+ xlogRestoreCmd[MAXPGPATH],
+ *dp,
+ *endp;
+ const char *sp;
+ int rc,
+ xlogfd;
+ struct stat stat_buf;
+
+ snprintf(xlogpath, MAXPGPATH, "%s/" XLOGDIR "/%s", path, xlogfname);
+
+ /*
+ * Construct the command to be executed.
+ */
+ dp = xlogRestoreCmd;
+ endp = xlogRestoreCmd + MAXPGPATH - 1;
+ *endp = '\0';
+
+ for (sp = restoreCommand; *sp; sp++)
+ {
+ if (*sp == '%')
+ {
+ switch (sp[1])
+ {
+ case 'p':
+ /* %p: relative path of target file */
+ sp++;
+ StrNCpy(dp, xlogpath, endp - dp);
+ make_native_path(dp);
+ dp += strlen(dp);
+ break;
+ case 'f':
+ /* %f: filename of desired file */
+ sp++;
+ StrNCpy(dp, xlogfname, endp - dp);
+ dp += strlen(dp);
+ break;
+ case 'r':
+ /* %r: filename of last restartpoint */
+ pg_fatal("restore_command with %%r cannot be used during rewind process.\n");
+ break;
+ case '%':
+ /* convert %% to a single % */
+ sp++;
+ if (dp < endp)
+ *dp++ = *sp;
+ break;
+ default:
+ /* otherwise treat the % as not special */
+ if (dp < endp)
+ *dp++ = *sp;
+ break;
+ }
+ }
+ else
+ {
+ if (dp < endp)
+ *dp++ = *sp;
+ }
+ }
+ *dp = '\0';
+
+ /*
+ * Execute restore_command, which should copy
+ * the missing WAL file from archival storage.
+ */
+ rc = system(xlogRestoreCmd);
+
+ if (rc == 0)
+ {
+ /*
+ * Command apparently succeeded, but let's make sure the file is
+ * really there now and has the correct size.
+ */
+ if (stat(xlogpath, &stat_buf) == 0)
+ {
+ if (expectedSize > 0 && stat_buf.st_size != expectedSize)
+ {
+ printf(_("archive file \"%s\" has wrong size: %lu instead of %lu, %s"),
+ xlogfname, (unsigned long) stat_buf.st_size,
+ (unsigned long) expectedSize, strerror(errno));
+ }
+ else
+ {
+ xlogfd = open(xlogpath, O_RDONLY | PG_BINARY, 0);
+
+ if (xlogfd < 0)
+ printf(_("could not open restored from archive file \"%s\": %s\n"),
+ xlogpath, strerror(errno));
+ else
+ return xlogfd;
+ }
+ }
+ else
+ {
+ /* Stat failed */
+ printf(_("could not stat file \"%s\": %s"),
+ xlogpath, strerror(errno));
+ }
+ }
+
+ printf(_("could not restore file \"%s\" from archive: %s\n"),
+ xlogfname, strerror(errno));
+
+ return -1;
+}
diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c
index 7ccde5c87f..9da578ec42 100644
--- a/src/bin/pg_rewind/pg_rewind.c
+++ b/src/bin/pg_rewind/pg_rewind.c
@@ -30,6 +30,8 @@
#include "getopt_long.h"
#include "storage/bufpage.h"
+#define MAX_RESTORE_COMMAND 1024
+
static void usage(const char *progname);
static void createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli,
@@ -52,11 +54,13 @@ int WalSegSz;
char *datadir_target = NULL;
char *datadir_source = NULL;
char *connstr_source = NULL;
+char *restore_command = NULL;
bool debug = false;
bool showprogress = false;
bool dry_run = false;
bool do_sync = true;
+bool restore_wals = false;
/* Target history */
TimeLineHistoryEntry *targetHistory;
@@ -75,6 +79,9 @@ usage(const char *progname)
printf(_(" -N, --no-sync do not wait for changes to be written\n"));
printf(_(" safely to disk\n"));
printf(_(" -P, --progress write progress messages\n"));
+ printf(_(" -r, --use-postgresql-conf use restore_command in the postgresql.conf to\n"));
+ printf(_(" retreive WALs from archive\n"));
+ printf(_(" -R, --restore-command=COMMAND restore_command\n"));
printf(_(" --debug write a lot of debug messages\n"));
printf(_(" -V, --version output version information, then exit\n"));
printf(_(" -?, --help show this help, then exit\n"));
@@ -94,6 +101,8 @@ main(int argc, char **argv)
{"dry-run", no_argument, NULL, 'n'},
{"no-sync", no_argument, NULL, 'N'},
{"progress", no_argument, NULL, 'P'},
+ {"use-postgresql-conf", no_argument, NULL, 'r'},
+ {"restore-command", required_argument, NULL, 'R'},
{"debug", no_argument, NULL, 3},
{NULL, 0, NULL, 0}
};
@@ -129,7 +138,7 @@ main(int argc, char **argv)
}
}
- while ((c = getopt_long(argc, argv, "D:nNP", long_options, &option_index)) != -1)
+ while ((c = getopt_long(argc, argv, "D:nNPR:r", long_options, &option_index)) != -1)
{
switch (c)
{
@@ -141,6 +150,10 @@ main(int argc, char **argv)
showprogress = true;
break;
+ case 'r':
+ restore_wals = true;
+ break;
+
case 'n':
dry_run = true;
break;
@@ -157,6 +170,10 @@ main(int argc, char **argv)
datadir_target = pg_strdup(optarg);
break;
+ case 'R':
+ restore_command = pg_strdup(optarg);
+ break;
+
case 1: /* --source-pgdata */
datadir_source = pg_strdup(optarg);
break;
@@ -223,6 +240,78 @@ main(int argc, char **argv)
umask(pg_mode_mask);
+ if (restore_command != NULL)
+ {
+ if (restore_wals)
+ {
+ fprintf(stderr, _("%s: conflicting options: both -r and -R are specified\n"),
+ progname);
+ fprintf(stderr, _("You must run %s with either -r/--use-postgresql-conf "
+ "or -R/--restore-command.\n"), progname);
+ exit(1);
+ }
+
+ pg_log(PG_DEBUG, "using command line restore_command=\'%s\'.\n", restore_command);
+ }
+ else if (restore_wals)
+ {
+ int rc;
+ char postgres_exec_path[MAXPGPATH],
+ postgres_cmd[MAXPGPATH],
+ cmd_output[MAX_RESTORE_COMMAND];
+ FILE *output_fp;
+
+ /* Find postgres executable. */
+ rc = find_other_exec(argv[0], "postgres",
+ PG_BACKEND_VERSIONSTR,
+ postgres_exec_path);
+
+ if (rc < 0)
+ {
+ char full_path[MAXPGPATH];
+
+ if (find_my_exec(argv[0], full_path) < 0)
+ strlcpy(full_path, progname, sizeof(full_path));
+
+ if (rc == -1)
+ fprintf(stderr,
+ _("the program \"postgres\" is needed by %s "
+ "but was not found in the\n"
+ "same directory as \"%s\".\n"
+ "Check your installation.\n"),
+ progname, full_path);
+ else
+ fprintf(stderr,
+ _("the program \"postgres\" was found by \"%s\"\n"
+ "but was not the same version as %s.\n"
+ "Check your installation.\n"),
+ full_path, progname);
+ exit(1);
+ }
+
+ /* Build a command to execute for restore_command GUC retrieval if set. */
+ snprintf(postgres_cmd, sizeof(postgres_cmd), "%s -D %s -C restore_command",
+ postgres_exec_path, datadir_target);
+
+ if ((output_fp = popen(postgres_cmd, "r")) == NULL ||
+ fgets(cmd_output, sizeof(cmd_output), output_fp) == NULL)
+ pg_fatal("could not get restore_command using %s: %s\n",
+ postgres_cmd, strerror(errno));
+
+ pclose(output_fp);
+
+ /* Remove trailing newline */
+ if (strchr(cmd_output, '\n') != NULL)
+ *strchr(cmd_output, '\n') = '\0';
+
+ if (!strcmp(cmd_output, ""))
+ pg_fatal("restore_command is not set on the target cluster\n");
+
+ restore_command = pg_strdup(cmd_output);
+
+ pg_log(PG_DEBUG, "using config variable restore_command=\'%s\'.\n", restore_command);
+ }
+
/* Connect to remote server */
if (connstr_source)
libpqConnect(connstr_source);
@@ -294,9 +383,8 @@ main(int argc, char **argv)
exit(0);
}
- findLastCheckpoint(datadir_target, divergerec,
- lastcommontliIndex,
- &chkptrec, &chkpttli, &chkptredo);
+ findLastCheckpoint(datadir_target, divergerec, lastcommontliIndex,
+ &chkptrec, &chkpttli, &chkptredo, restore_command);
printf(_("rewinding from last common checkpoint at %X/%X on timeline %u\n"),
(uint32) (chkptrec >> 32), (uint32) chkptrec,
chkpttli);
@@ -319,7 +407,7 @@ main(int argc, char **argv)
*/
pg_log(PG_PROGRESS, "reading WAL in target\n");
extractPageMap(datadir_target, chkptrec, lastcommontliIndex,
- ControlFile_target.checkPoint);
+ ControlFile_target.checkPoint, restore_command);
filemap_finalize();
if (showprogress)
diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h
index 83b2898b8b..08a753475c 100644
--- a/src/bin/pg_rewind/pg_rewind.h
+++ b/src/bin/pg_rewind/pg_rewind.h
@@ -32,11 +32,10 @@ extern int targetNentries;
/* in parsexlog.c */
extern void extractPageMap(const char *datadir, XLogRecPtr startpoint,
- int tliIndex, XLogRecPtr endpoint);
+ int tliIndex, XLogRecPtr endpoint, const char *restoreCommand);
extern void findLastCheckpoint(const char *datadir, XLogRecPtr searchptr,
- int tliIndex,
- XLogRecPtr *lastchkptrec, TimeLineID *lastchkpttli,
- XLogRecPtr *lastchkptredo);
+ int tliIndex, XLogRecPtr *lastchkptrec, TimeLineID *lastchkpttli,
+ XLogRecPtr *lastchkptredo, const char *restoreCommand);
extern XLogRecPtr readOneRecord(const char *datadir, XLogRecPtr ptr,
int tliIndex);
diff --git a/src/bin/pg_rewind/t/001_basic.pl b/src/bin/pg_rewind/t/001_basic.pl
index 115192170e..8a6fa33016 100644
--- a/src/bin/pg_rewind/t/001_basic.pl
+++ b/src/bin/pg_rewind/t/001_basic.pl
@@ -1,7 +1,7 @@
use strict;
use warnings;
use TestLib;
-use Test::More tests => 10;
+use Test::More tests => 20;
use FindBin;
use lib $FindBin::RealBin;
@@ -106,5 +106,7 @@ in master, before promotion
# Run the test in both modes
run_test('local');
run_test('remote');
+run_test('archive');
+run_test('archive_conf');
exit(0);
diff --git a/src/bin/pg_rewind/t/002_databases.pl b/src/bin/pg_rewind/t/002_databases.pl
index 6dc05720a1..99ea821cfe 100644
--- a/src/bin/pg_rewind/t/002_databases.pl
+++ b/src/bin/pg_rewind/t/002_databases.pl
@@ -1,7 +1,7 @@
use strict;
use warnings;
use TestLib;
-use Test::More tests => 6;
+use Test::More tests => 12;
use FindBin;
use lib $FindBin::RealBin;
@@ -62,5 +62,7 @@ template1
# Run the test in both modes.
run_test('local');
run_test('remote');
+run_test('archive');
+run_test('archive_conf');
exit(0);
diff --git a/src/bin/pg_rewind/t/003_extrafiles.pl b/src/bin/pg_rewind/t/003_extrafiles.pl
index c4040bd562..24cec256de 100644
--- a/src/bin/pg_rewind/t/003_extrafiles.pl
+++ b/src/bin/pg_rewind/t/003_extrafiles.pl
@@ -3,7 +3,7 @@
use strict;
use warnings;
use TestLib;
-use Test::More tests => 4;
+use Test::More tests => 8;
use File::Find;
@@ -90,5 +90,7 @@ sub run_test
# Run the test in both modes.
run_test('local');
run_test('remote');
+run_test('archive');
+run_test('archive_conf');
exit(0);
diff --git a/src/bin/pg_rewind/t/RewindTest.pm b/src/bin/pg_rewind/t/RewindTest.pm
index 85cae7e47b..4560fe4430 100644
--- a/src/bin/pg_rewind/t/RewindTest.pm
+++ b/src/bin/pg_rewind/t/RewindTest.pm
@@ -39,7 +39,9 @@ use Carp;
use Config;
use Exporter 'import';
use File::Copy;
-use File::Path qw(rmtree);
+use File::Glob ':bsd_glob';
+use File::Path qw(remove_tree make_path);
+use File::Spec::Functions qw(catdir catfile);
use IPC::Run qw(run);
use PostgresNode;
use TestLib;
@@ -197,6 +199,38 @@ sub promote_standby
return;
}
+# Moves WAL files to the temporary location and returns restore_command
+# to get them back.
+sub move_wals
+{
+ my $tmp_dir = shift;
+ my $master_pgdata = shift;
+ my $wals_archive_dir = catdir($tmp_dir, "master_wals_archive");
+ my @wal_files = bsd_glob catfile($master_pgdata, "pg_wal", "0000000*");
+ my $restore_command;
+
+ remove_tree($wals_archive_dir);
+ make_path($wals_archive_dir) or die;
+
+ # Move all old master WAL files to the archive.
+ # Old master should be stopped at this point.
+ foreach my $wal_file (@wal_files)
+ {
+ move($wal_file, "$wals_archive_dir") or die;
+ }
+
+ if ($windows_os)
+ {
+ $restore_command = "copy $wals_archive_dir\\\%f \%p";
+ }
+ else
+ {
+ $restore_command = "cp $wals_archive_dir/\%f \%p";
+ }
+
+ return $restore_command;
+}
+
sub run_pg_rewind
{
my $test_mode = shift;
@@ -249,6 +283,54 @@ sub run_pg_rewind
],
'pg_rewind remote');
}
+ elsif ($test_mode eq "archive")
+ {
+
+ # Do rewind using a local pgdata as source and
+ # specified directory with target WALs archive.
+ my $restore_command = move_wals($tmp_folder, $master_pgdata);
+
+ # Stop the new master and be ready to perform the rewind.
+ $node_standby->stop;
+
+ command_ok(
+ [
+ 'pg_rewind',
+ "--debug",
+ "--source-pgdata=$standby_pgdata",
+ "--target-pgdata=$master_pgdata",
+ "--no-sync",
+ "--restore-command=$restore_command"
+ ],
+ 'pg_rewind archive');
+ }
+ elsif ($test_mode eq "archive_conf")
+ {
+
+ # Do rewind using a local pgdata as source and
+ # specified directory with target WALs archive.
+ my $master_conf_path = catfile($master_pgdata, 'postgresql.conf');
+ my $restore_command = move_wals($tmp_folder, $master_pgdata);
+
+ # Stop the new master and be ready to perform the rewind.
+ $node_standby->stop;
+
+ # Add restore_command to postgresql.conf of target cluster.
+ open(my $conf_fd, ">>", $master_conf_path) or die;
+ print $conf_fd "\nrestore_command='$restore_command'";
+ close $conf_fd;
+
+ command_ok(
+ [
+ 'pg_rewind',
+ "--debug",
+ "--source-pgdata=$standby_pgdata",
+ "--target-pgdata=$master_pgdata",
+ "--no-sync",
+ "-r"
+ ],
+ 'pg_rewind archive_conf');
+ }
else
{
base-commit: 41531e42d34f4aca117d343b5e40f3f757dec5fe
--
2.17.1
--------------F3245A8D605DB552C5CDF408--
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Reset the output buffer after sending from WalSndWriteData
@ 2025-02-21 06:17 Masahiko Sawada <[email protected]>
0 siblings, 1 reply; 19+ messages in thread
From: Masahiko Sawada @ 2025-02-21 06:17 UTC (permalink / raw)
To: Markus Wanner <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Thu, Feb 20, 2025 at 12:50 PM Markus Wanner
<[email protected]> wrote:
>
> Hi,
>
> I recently stumbled over an issue with an unintentional re-transmission.
> While this clearly was our fault in the output plugin code, I think the
> walsender's exposed API could easily be hardened to prevent the bad
> consequence from this mistake.
>
> Does anything speak against the attached one line patch?
According to the documentation[1], OutputPluginPrepareWrite() has to
be called before OutputPluginWrite(). When it comes to walsender
codes, we reset the ctx->out buffer in WalSndPrepareWrite() called via
OutputPluginPrepareWrite(). Could you share the case where you faced
the unintentional re-transmission error?
Regards,
[1] https://www.postgresql.org/docs/devel/logicaldecoding-output-plugin.html#LOGICALDECODING-OUTPUT-PLUG...
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Reset the output buffer after sending from WalSndWriteData
@ 2025-02-21 08:06 Markus Wanner <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Markus Wanner @ 2025-02-21 08:06 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; Markus Wanner <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On 2/21/25 07:17, Masahiko Sawada wrote:
> On Thu, Feb 20, 2025 at 12:50 PM Markus Wanner
> <[email protected]> wrote:
>>
>> Hi,
>>
>> I recently stumbled over an issue with an unintentional re-transmission.
>> While this clearly was our fault in the output plugin code, I think the
>> walsender's exposed API could easily be hardened to prevent the bad
>> consequence from this mistake.
>>
>> Does anything speak against the attached one line patch?
>
> According to the documentation[1], OutputPluginPrepareWrite() has to
> be called before OutputPluginWrite().
Sure, that's exactly what we forgot. My complaint is that it's a mistake
that's unnecessarily hard to spot and the API could be improved. There
is no good reason to keep the sent data in the ctx->out buffer. But
clearly, there's some danger to it.
(Note that it wasn't quite as simple as you may think, though. With an
error involved in the send/recv loop of the walsender invoked from
OutputPluginWrite. The error handling code in PG_CATCH then attempting
to flush the queue with OutputPluginWrite.)
Arguably, one could even say that replacing the call to resetStringInfo
in WalSndPrepareWrite with an Assert(ctx->out->len == 0) would catch a
variant of improper use. And another Assert in WalSndWriteData to check
OutputPluginPrepareWrite had properly been invoked before can't hurt,
either.
Best Regards
Markus
Attachments:
[text/x-patch] 0002-Add-asserts-to-guide-to-proper-API-usage.patch (1.1K, ../../[email protected]/2-0002-Add-asserts-to-guide-to-proper-API-usage.patch)
download | inline diff:
From 5b9d6285252204d7109001fc42323add00ac773f Mon Sep 17 00:00:00 2001
From: Markus Wanner <[email protected]>
Date: Fri, 21 Feb 2025 09:00:28 +0100
Subject: [PATCH] Add asserts to guide to proper API usage
---
src/backend/replication/walsender.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 96884ce152f..b5d7baf83ad 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1523,7 +1523,7 @@ WalSndPrepareWrite(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId xi
if (!last_write)
lsn = InvalidXLogRecPtr;
- resetStringInfo(ctx->out);
+ Assert(ctx->out->len == 0);
pq_sendbyte(ctx->out, 'w');
pq_sendint64(ctx->out, lsn); /* dataStart */
@@ -1549,6 +1549,8 @@ WalSndWriteData(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId xid,
{
TimestampTz now;
+ Assert(ctx->out->len >= 1 + 3 * sizeof(int64);
+
/*
* Fill the send timestamp last, so that it is taken as late as possible.
* This is somewhat ugly, but the protocol is set as it's already used for
--
2.39.5
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v4] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 35b0b72f0a7..25ebcd3cc47 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 0e624fe36b9..677137207e7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index d88a7a70308..153fbb477bb 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..d4c2f80eb46 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} PGLockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1;
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ PGLockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(PGLockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..ad34142e7d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1956,6 +1956,7 @@ PGIOAlignedBlock
PGLZ_HistEntry
PGLZ_Strategy
PGLoadBalanceType
+PGLockFile
PGMessageField
PGModuleMagicFunction
PGNoticeHooks
base-commit: 73dfe79fd6034b1e7e41e83d9c82c166dba8eb67
--
2.52.0
--tks2nr37zts5e6h7--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v3] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 5f77f3cac29..15bda6c6413 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 61cee42daa7..e24cb7a6f01 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index 568e0e150bf..3d641dc0403 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..26c3324542c 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} LockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ LockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(LockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..185d69b5520 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1653,6 +1653,7 @@ LocationLen
LockAcquireResult
LockClauseStrength
LockData
+LockFile
LockInfoData
LockInstanceData
LockMethod
base-commit: 031904048aa22e7c70dc8e9c170e2743f9b0f090
--
2.52.0
--zdel3ow7bygx53fm--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v4] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 35b0b72f0a7..25ebcd3cc47 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 0e624fe36b9..677137207e7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index d88a7a70308..153fbb477bb 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..d4c2f80eb46 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} PGLockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1;
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ PGLockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(PGLockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..ad34142e7d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1956,6 +1956,7 @@ PGIOAlignedBlock
PGLZ_HistEntry
PGLZ_Strategy
PGLoadBalanceType
+PGLockFile
PGMessageField
PGModuleMagicFunction
PGNoticeHooks
base-commit: 73dfe79fd6034b1e7e41e83d9c82c166dba8eb67
--
2.52.0
--tks2nr37zts5e6h7--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v3] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 5f77f3cac29..15bda6c6413 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 61cee42daa7..e24cb7a6f01 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index 568e0e150bf..3d641dc0403 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..26c3324542c 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} LockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ LockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(LockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..185d69b5520 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1653,6 +1653,7 @@ LocationLen
LockAcquireResult
LockClauseStrength
LockData
+LockFile
LockInfoData
LockInstanceData
LockMethod
base-commit: 031904048aa22e7c70dc8e9c170e2743f9b0f090
--
2.52.0
--zdel3ow7bygx53fm--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v4] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 35b0b72f0a7..25ebcd3cc47 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 0e624fe36b9..677137207e7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index d88a7a70308..153fbb477bb 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..d4c2f80eb46 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} PGLockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1;
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ PGLockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(PGLockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..ad34142e7d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1956,6 +1956,7 @@ PGIOAlignedBlock
PGLZ_HistEntry
PGLZ_Strategy
PGLoadBalanceType
+PGLockFile
PGMessageField
PGModuleMagicFunction
PGNoticeHooks
base-commit: 73dfe79fd6034b1e7e41e83d9c82c166dba8eb67
--
2.52.0
--tks2nr37zts5e6h7--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v3] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 5f77f3cac29..15bda6c6413 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 61cee42daa7..e24cb7a6f01 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index 568e0e150bf..3d641dc0403 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..26c3324542c 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} LockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ LockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(LockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..185d69b5520 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1653,6 +1653,7 @@ LocationLen
LockAcquireResult
LockClauseStrength
LockData
+LockFile
LockInfoData
LockInstanceData
LockMethod
base-commit: 031904048aa22e7c70dc8e9c170e2743f9b0f090
--
2.52.0
--zdel3ow7bygx53fm--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v4] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 35b0b72f0a7..25ebcd3cc47 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 0e624fe36b9..677137207e7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index d88a7a70308..153fbb477bb 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..d4c2f80eb46 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} PGLockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1;
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ PGLockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(PGLockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..ad34142e7d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1956,6 +1956,7 @@ PGIOAlignedBlock
PGLZ_HistEntry
PGLZ_Strategy
PGLoadBalanceType
+PGLockFile
PGMessageField
PGModuleMagicFunction
PGNoticeHooks
base-commit: 73dfe79fd6034b1e7e41e83d9c82c166dba8eb67
--
2.52.0
--tks2nr37zts5e6h7--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v3] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 5f77f3cac29..15bda6c6413 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 61cee42daa7..e24cb7a6f01 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index 568e0e150bf..3d641dc0403 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..26c3324542c 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} LockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ LockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(LockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..185d69b5520 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1653,6 +1653,7 @@ LocationLen
LockAcquireResult
LockClauseStrength
LockData
+LockFile
LockInfoData
LockInstanceData
LockMethod
base-commit: 031904048aa22e7c70dc8e9c170e2743f9b0f090
--
2.52.0
--zdel3ow7bygx53fm--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v4] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 35b0b72f0a7..25ebcd3cc47 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 0e624fe36b9..677137207e7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index d88a7a70308..153fbb477bb 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..d4c2f80eb46 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} PGLockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1;
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ PGLockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(PGLockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..ad34142e7d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1956,6 +1956,7 @@ PGIOAlignedBlock
PGLZ_HistEntry
PGLZ_Strategy
PGLoadBalanceType
+PGLockFile
PGMessageField
PGModuleMagicFunction
PGNoticeHooks
base-commit: 73dfe79fd6034b1e7e41e83d9c82c166dba8eb67
--
2.52.0
--tks2nr37zts5e6h7--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v3] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 5f77f3cac29..15bda6c6413 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 61cee42daa7..e24cb7a6f01 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index 568e0e150bf..3d641dc0403 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..26c3324542c 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} LockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ LockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(LockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..185d69b5520 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1653,6 +1653,7 @@ LocationLen
LockAcquireResult
LockClauseStrength
LockData
+LockFile
LockInfoData
LockInstanceData
LockMethod
base-commit: 031904048aa22e7c70dc8e9c170e2743f9b0f090
--
2.52.0
--zdel3ow7bygx53fm--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v4] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 35b0b72f0a7..25ebcd3cc47 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 0e624fe36b9..677137207e7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index d88a7a70308..153fbb477bb 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..d4c2f80eb46 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} PGLockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1;
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ PGLockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(PGLockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..ad34142e7d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1956,6 +1956,7 @@ PGIOAlignedBlock
PGLZ_HistEntry
PGLZ_Strategy
PGLoadBalanceType
+PGLockFile
PGMessageField
PGModuleMagicFunction
PGNoticeHooks
base-commit: 73dfe79fd6034b1e7e41e83d9c82c166dba8eb67
--
2.52.0
--tks2nr37zts5e6h7--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v3] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 5f77f3cac29..15bda6c6413 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 61cee42daa7..e24cb7a6f01 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index 568e0e150bf..3d641dc0403 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..26c3324542c 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} LockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ LockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(LockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..185d69b5520 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1653,6 +1653,7 @@ LocationLen
LockAcquireResult
LockClauseStrength
LockData
+LockFile
LockInfoData
LockInstanceData
LockMethod
base-commit: 031904048aa22e7c70dc8e9c170e2743f9b0f090
--
2.52.0
--zdel3ow7bygx53fm--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v4] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 35b0b72f0a7..25ebcd3cc47 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 0e624fe36b9..677137207e7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index d88a7a70308..153fbb477bb 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..d4c2f80eb46 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} PGLockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1;
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ PGLockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(PGLockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..ad34142e7d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1956,6 +1956,7 @@ PGIOAlignedBlock
PGLZ_HistEntry
PGLZ_Strategy
PGLoadBalanceType
+PGLockFile
PGMessageField
PGModuleMagicFunction
PGNoticeHooks
base-commit: 73dfe79fd6034b1e7e41e83d9c82c166dba8eb67
--
2.52.0
--tks2nr37zts5e6h7--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v3] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 5f77f3cac29..15bda6c6413 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 61cee42daa7..e24cb7a6f01 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index 568e0e150bf..3d641dc0403 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..26c3324542c 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} LockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ LockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(LockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..185d69b5520 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1653,6 +1653,7 @@ LocationLen
LockAcquireResult
LockClauseStrength
LockData
+LockFile
LockInfoData
LockInstanceData
LockMethod
base-commit: 031904048aa22e7c70dc8e9c170e2743f9b0f090
--
2.52.0
--zdel3ow7bygx53fm--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v4] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 35b0b72f0a7..25ebcd3cc47 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 0e624fe36b9..677137207e7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index d88a7a70308..153fbb477bb 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..d4c2f80eb46 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} PGLockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1;
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ PGLockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(PGLockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ PGLockFile *lock_file = (PGLockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..ad34142e7d6 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1956,6 +1956,7 @@ PGIOAlignedBlock
PGLZ_HistEntry
PGLZ_Strategy
PGLoadBalanceType
+PGLockFile
PGMessageField
PGModuleMagicFunction
PGNoticeHooks
base-commit: 73dfe79fd6034b1e7e41e83d9c82c166dba8eb67
--
2.52.0
--tks2nr37zts5e6h7--
^ permalink raw reply [nested|flat] 19+ messages in thread
* [PATCH v3] Use open file description locks for lockfiles
@ 2025-12-18 17:21 Dmitrii Dolgov <[email protected]>
0 siblings, 0 replies; 19+ messages in thread
From: Dmitrii Dolgov @ 2025-12-18 17:21 UTC (permalink / raw)
When starting up, postmaster checks for an existing data directory lockfile. If
this file contains current process PID, it's assumed to be stale. Turns out
there is another possibility: we might be running in a PID namespace, and there
is another postgres running inside another PID namespace using the same data
directory. The result is that we don't see another process due to namespace
isolation and start concurrently with the other.
To prevent such situations, at startup use fcntl to get an exclusive open file
description lock for data directory lockfile. Since such locks are associated
with open file descriptors, meaning they're not affected by PID namespace
isolation. It's a "best effort" locking, intended to work with already existing
mechanism, not replace it.
This approach was discussed multiple times in the past, and usually was
rejected as the main work horse for the data directory lockfile due to:
* Portability issues. Open file description lock was a non-POSIX extension in
Linux and similar flock is from BSD standard. But looks like everybody agrees
that such locks make more sense than a typical advisory locks, and
F_OFD_SETLK made its way into POSIX.1 2024 [1].
* Issues with NFS. The current state of things here looks like this:
- NFSv3 doesn't implement open file description locks, they're converted to
advisory locks instead. Advisory locks are subject to namespace isolation,
meaning that processes in different PID namespaces will not see each other
advisory lock, and it's still possible to run multiple postgres
instances on the same data directory.
- NFSv4 uses a lease system for locking, I haven't found any mention of
conversion to advisory locks neither in the man page nor in RFC [2].
To summarize, the approach is now considered POSIX and should fix the described
problem everywhere, except NFSv3.
Use open file description lock for both data directory and socker
lockfiles, since both are affected in the same way.
[1]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fcntl.html
[2]: https://www.rfc-editor.org/rfc/rfc7530
Reviewed-by: Ilmar Yunusov <[email protected]>
---
configure | 14 +++
configure.ac | 3 +
meson.build | 1 +
src/backend/utils/init/miscinit.c | 136 ++++++++++++++++++++++++------
src/include/pg_config.h.in | 4 +
src/tools/pgindent/typedefs.list | 1 +
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/configure b/configure
index 5f77f3cac29..15bda6c6413 100755
--- a/configure
+++ b/configure
@@ -16444,6 +16444,20 @@ cat >>confdefs.h <<_ACEOF
_ACEOF
+# Linux open file descriptor locks
+ac_fn_c_check_decl "$LINENO" "F_OFD_SETLK" "ac_cv_have_decl_F_OFD_SETLK" "#include <fcntl.h>
+"
+if test "x$ac_cv_have_decl_F_OFD_SETLK" = xyes; then :
+ ac_have_decl=1
+else
+ ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_F_OFD_SETLK $ac_have_decl
+_ACEOF
+
+
ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero"
if test "x$ac_cv_func_explicit_bzero" = xyes; then :
$as_echo "#define HAVE_EXPLICIT_BZERO 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 61cee42daa7..e24cb7a6f01 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1913,6 +1913,9 @@ AC_CHECK_DECLS([memset_s], [], [], [#define __STDC_WANT_LIB_EXT1__ 1
# This is probably only present on macOS, but may as well check always
AC_CHECK_DECLS(F_FULLFSYNC, [], [], [#include <fcntl.h>])
+# Linux open file descriptor locks
+AC_CHECK_DECLS([F_OFD_SETLK], [], [], [#include <fcntl.h>])
+
AC_REPLACE_FUNCS(m4_normalize([
explicit_bzero
getopt
diff --git a/meson.build b/meson.build
index 568e0e150bf..3d641dc0403 100644
--- a/meson.build
+++ b/meson.build
@@ -2901,6 +2901,7 @@ decl_checks = [
['strlcpy', 'string.h'],
['strsep', 'string.h'],
['timingsafe_bcmp', 'string.h'],
+ ['F_OFD_SETLK', 'fcntl.h'],
]
# Need to check for function declarations for these functions, because
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 7ffc808073a..26c3324542c 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -69,6 +69,15 @@ static List *lock_files = NIL;
static Latch LocalLatchData;
+typedef struct
+{
+ /* LockFile name. */
+ const char *filename;
+
+ /* File descriptor for open file description lock. */
+ int fd;
+} LockFile;
+
/* ----------------------------------------------------------------
* ignoring system indexes support stuff
*
@@ -1119,6 +1128,48 @@ RestoreClientConnectionInfo(char *conninfo)
*-------------------------------------------------------------------------
*/
+/*
+ * OFD lock the specified lockfile.
+ *
+ * Lock the lockfile with an open file description lock. If the lock is already
+ * taken, it's a hard stop. It's only a best effort test, and any other errors
+ * are ignored. On succes the file descriptor is duplicated, to make sure there
+ * will be at least one open copy of it to keep the lock.
+ *
+ * filename argument is used only for reporting purposes.
+ */
+static int
+OFDLockFile(int fd, const char *filename)
+{
+#if HAVE_DECL_F_OFD_SETLK
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 0;
+ lock.l_pid = 0;
+
+ if (fcntl(fd, F_OFD_SETLK, &lock) == -1)
+ {
+ if (errno == EAGAIN)
+ ereport(FATAL,
+ (errcode(ERRCODE_LOCK_FILE_EXISTS),
+ errmsg("cannot lock the lock file \"%s\"", filename),
+ errhint("Another server is starting.")));
+ else
+ {
+ elog(WARNING, "Failed locking file \"%s\", %m", filename);
+ return -1;
+ }
+ }
+ else
+ return dup(fd);
+#else
+ return -1
+#endif
+}
+
/*
* proc_exit callback to remove lockfiles.
*/
@@ -1129,9 +1180,16 @@ UnlinkLockFiles(int status, Datum arg)
foreach(l, lock_files)
{
- char *curfile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
- unlink(curfile);
+ /*
+ * Close the file descriptor, which keeps the open file description
+ * lock.
+ */
+ if (lock_file->fd > 0)
+ close(lock_file->fd);
+
+ unlink(lock_file->filename);
/* Should we complain if the unlink fails? */
}
/* Since we're about to exit, no need to reclaim storage */
@@ -1161,7 +1219,9 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *socketDir,
bool isDDLock, const char *refName)
{
- int fd;
+ int fd,
+ flock_fd = -1;
+ LockFile *lock_file;
char buffer[MAXPGPATH * 2 + 256];
int ntries;
int len;
@@ -1173,22 +1233,32 @@ CreateLockFile(const char *filename, bool amPostmaster,
const char *envvar;
/*
- * If the PID in the lockfile is our own PID or our parent's or
- * grandparent's PID, then the file must be stale (probably left over from
- * a previous system boot cycle). We need to check this because of the
- * likelihood that a reboot will assign exactly the same PID as we had in
- * the previous reboot, or one that's only one or two counts larger and
- * hence the lockfile's PID now refers to an ancestor shell process. We
- * allow pg_ctl to pass down its parent shell PID (our grandparent PID)
- * via the environment variable PG_GRANDPARENT_PID; this is so that
- * launching the postmaster via pg_ctl can be just as reliable as
- * launching it directly. There is no provision for detecting
- * further-removed ancestor processes, but if the init script is written
- * carefully then all but the immediate parent shell will be root-owned
- * processes and so the kill test will fail with EPERM. Note that we
- * cannot get a false negative this way, because an existing postmaster
- * would surely never launch a competing postmaster or pg_ctl process
- * directly.
+ * If we find an already existing lockfile containing our own PID, there
+ * are few options:
+ *
+ * - There is another process, that we don't see due to PID namespace
+ * isolation, which is already running in this data directory.
+ *
+ * To prevent two concurrent processes working with the same data
+ * directory, we first try to lock the lockfile exclusively.
+ *
+ * - The file must be stale, probably left over from a previous system
+ * boot cycle. The same if the lockfile contains our parent's or
+ * grandparent's PID.
+ *
+ * We need to check this because of the likelihood that a reboot will
+ * assign exactly the same PID as we had in the previous reboot, or one
+ * that's only one or two counts larger and hence the lockfile's PID now
+ * refers to an ancestor shell process. We allow pg_ctl to pass down its
+ * parent shell PID (our grandparent PID) via the environment variable
+ * PG_GRANDPARENT_PID; this is so that launching the postmaster via pg_ctl
+ * can be just as reliable as launching it directly. There is no
+ * provision for detecting further-removed ancestor processes, but if the
+ * init script is written carefully then all but the immediate parent
+ * shell will be root-owned processes and so the kill test will fail with
+ * EPERM. Note that we cannot get a false negative this way, because an
+ * existing postmaster would surely never launch a competing postmaster or
+ * pg_ctl process directly.
*/
my_pid = getpid();
@@ -1224,7 +1294,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, pg_file_create_mode);
if (fd >= 0)
- break; /* Success; exit the retry loop */
+ {
+ /* Success; lock and exit the retry loop */
+ flock_fd = OFDLockFile(fd, filename);
+ break;
+ }
/*
* Couldn't create the pid file. Probably it already exists.
@@ -1238,8 +1312,12 @@ CreateLockFile(const char *filename, bool amPostmaster,
/*
* Read the file to get the old owner's PID. Note race condition
* here: file might have been deleted since we tried to create it.
+ *
+ * We're going to use the same fd for flock, and want to create a
+ * write lock for the latter one. Since both fd and the lock have to
+ * be of the same type, open the file for read and write.
*/
- fd = open(filename, O_RDONLY, pg_file_create_mode);
+ fd = open(filename, O_RDWR, pg_file_create_mode);
if (fd < 0)
{
if (errno == ENOENT)
@@ -1249,6 +1327,10 @@ CreateLockFile(const char *filename, bool amPostmaster,
errmsg("could not open lock file \"%s\": %m",
filename)));
}
+
+ /* Try to lock the file. We stop here, if it's already locked. */
+ flock_fd = OFDLockFile(fd, filename);
+
pgstat_report_wait_start(WAIT_EVENT_LOCK_FILE_CREATE_READ);
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
ereport(FATAL,
@@ -1448,7 +1530,11 @@ CreateLockFile(const char *filename, bool amPostmaster,
* Use lcons so that the lock files are unlinked in reverse order of
* creation; this is critical!
*/
- lock_files = lcons(pstrdup(filename), lock_files);
+ lock_file = palloc0_object(LockFile);
+ lock_file->filename = pstrdup(filename);
+ lock_file->fd = flock_fd;
+
+ lock_files = lcons(lock_file, lock_files);
}
/*
@@ -1495,14 +1581,14 @@ TouchSocketLockFiles(void)
foreach(l, lock_files)
{
- char *socketLockFile = (char *) lfirst(l);
+ LockFile *lock_file = (LockFile *) lfirst(l);
/* No need to touch the data directory lock file, we trust */
- if (strcmp(socketLockFile, DIRECTORY_LOCK_FILE) == 0)
+ if (strcmp(lock_file->filename, DIRECTORY_LOCK_FILE) == 0)
continue;
/* we just ignore any error here */
- (void) utime(socketLockFile, NULL);
+ (void) utime(lock_file->filename, NULL);
}
}
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..cc38c06dc13 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -85,6 +85,10 @@
don't. */
#undef HAVE_DECL_F_FULLFSYNC
+/* Define to 1 if you have the declaration of `F_OFD_SETLK', and to 0 if you
+ don't. */
+#undef HAVE_DECL_F_OFD_SETLK
+
/* Define to 1 if you have the declaration of `memset_s', and to 0 if you
don't. */
#undef HAVE_DECL_MEMSET_S
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..185d69b5520 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1653,6 +1653,7 @@ LocationLen
LockAcquireResult
LockClauseStrength
LockData
+LockFile
LockInfoData
LockInstanceData
LockMethod
base-commit: 031904048aa22e7c70dc8e9c170e2743f9b0f090
--
2.52.0
--zdel3ow7bygx53fm--
^ permalink raw reply [nested|flat] 19+ messages in thread
end of thread, other threads:[~2025-12-18 17:21 UTC | newest]
Thread overview: 19+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-02-19 16:14 [PATCH v4] pg_rewind: options to use restore_command from command line or cluster config Alexey Kondratov <[email protected]>
2025-02-21 06:17 Re: Reset the output buffer after sending from WalSndWriteData Masahiko Sawada <[email protected]>
2025-02-21 08:06 ` Re: Reset the output buffer after sending from WalSndWriteData Markus Wanner <[email protected]>
2025-12-18 17:21 [PATCH v3] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v3] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v3] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v4] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v3] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v4] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v4] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v4] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v4] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v3] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v3] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v4] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v3] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v4] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v4] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
2025-12-18 17:21 [PATCH v3] Use open file description locks for lockfiles Dmitrii Dolgov <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox