Skip to content

Commit d4924a8

Browse files
x4mcursoragent
andcommitted
injection_points: drive wait points through the filesystem, without SQL
Add a filesystem-based way to attach a wait point and release a specific waiter, for code paths that run before the server can answer SQL (e.g. early postmaster startup). It works alongside the SQL path and does not touch the core registry. State lives under pg_injection_points/ in the data directory: pg_injection_points/<point>/ present -> <point> attached as a wait pg_injection_points/<point>/<pid> present -> that backend is parked here Each subdirectory is scanned once at startup and attached, so shared_preload_libraries is needed to arm a point before startup. A backend reaching the point publishes its <pid> file and polls with stat() until it is removed, besides watching the wakeup counter; removing the file wakes that one backend, and listing the directory shows which backends are blocked. The tree is dropped at shutdown. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 80cb6d9 commit d4924a8

4 files changed

Lines changed: 234 additions & 3 deletions

File tree

‎src/test/modules/injection_points/Makefile‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ ISOLATION = basic \
2424
# some isolation tests require wal_level=replica
2525
ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
2626

27+
TAP_TESTS = 1
28+
2729
# The injection points are cluster-wide, so disable installcheck
2830
NO_INSTALLCHECK = 1
2931

‎src/test/modules/injection_points/injection_points.c‎

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@
1717

1818
#include "postgres.h"
1919

20+
#include <fcntl.h>
21+
#include <sys/stat.h>
22+
23+
#include "common/file_utils.h"
2024
#include "fmgr.h"
2125
#include "funcapi.h"
2226
#include "injection_points.h"
2327
#include "miscadmin.h"
2428
#include "nodes/pg_list.h"
2529
#include "nodes/value.h"
2630
#include "storage/dsm_registry.h"
31+
#include "storage/fd.h"
2732
#include "storage/ipc.h"
2833
#include "storage/lwlock.h"
2934
#include "storage/shmem.h"
@@ -41,6 +46,39 @@ PG_MODULE_MAGIC;
4146
#define INJ_MAX_WAIT 8
4247
#define INJ_NAME_MAXLEN 64
4348

49+
/*
50+
* Filesystem markers used to drive wait points without any SQL connection, for
51+
* code paths that run before the server can answer SQL (e.g. early postmaster
52+
* startup). They live under a directory in the data directory, one
53+
* subdirectory per attached point and one file per parked process:
54+
*
55+
* pg_injection_points/<point>/ present = <point> attached as a wait;
56+
* pg_injection_points/<point>/<pid> present = that process is parked here.
57+
*
58+
* A test attaches a point by creating its directory (scanned at startup), sees
59+
* who is waiting by listing the directory, and wakes a specific waiter by
60+
* removing its PID file. Everything is plain filesystem state checked with
61+
* stat(), so there is no platform-specific logic and no out-of-process access
62+
* to shared memory. Paths are relative to the data directory, which is the
63+
* working directory of the postmaster and every backend.
64+
*/
65+
#define INJ_POINTS_DIR "pg_injection_points"
66+
67+
/* "pg_injection_points/<point>" */
68+
static void
69+
injection_point_dir(char *buf, size_t bufsize, const char *name)
70+
{
71+
snprintf(buf, bufsize, "%s/%s", INJ_POINTS_DIR, name);
72+
}
73+
74+
/* "pg_injection_points/<point>/<pid>" */
75+
static void
76+
injection_point_waiter_path(char *buf, size_t bufsize,
77+
const char *name, int pid)
78+
{
79+
snprintf(buf, bufsize, "%s/%s/%d", INJ_POINTS_DIR, name, pid);
80+
}
81+
4482
/*
4583
* List of injection points stored in TopMemoryContext attached
4684
* locally to this process.
@@ -113,6 +151,60 @@ injection_shmem_request(void *arg)
113151
);
114152
}
115153

154+
/*
155+
* Scan INJ_POINTS_DIR for point subdirectories and attach each as a wait point.
156+
* Run once at postmaster startup so the points are in place before any process
157+
* - the postmaster's own startup sequence or its children - reaches them, all
158+
* without an SQL connection. A point can therefore be attached, by creating
159+
* its directory, before the server is started.
160+
*/
161+
static void
162+
injection_points_preload(void)
163+
{
164+
DIR *dir;
165+
struct dirent *de;
166+
167+
dir = AllocateDir(INJ_POINTS_DIR);
168+
if (dir == NULL)
169+
return; /* no directory means nothing attached */
170+
171+
while ((de = ReadDir(dir, INJ_POINTS_DIR)) != NULL)
172+
{
173+
InjectionPointCondition condition = {.type = INJ_CONDITION_ALWAYS};
174+
char path[MAXPGPATH];
175+
struct stat st;
176+
177+
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
178+
continue;
179+
if (strlen(de->d_name) >= INJ_NAME_MAXLEN)
180+
continue;
181+
182+
/* Each subdirectory names a wait point to attach. */
183+
injection_point_dir(path, sizeof(path), de->d_name);
184+
if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode))
185+
continue;
186+
187+
InjectionPointAttach(de->d_name, "injection_points", "injection_wait",
188+
&condition, sizeof(condition));
189+
}
190+
FreeDir(dir);
191+
}
192+
193+
/*
194+
* proc_exit callback that removes INJ_POINTS_DIR so markers do not survive the
195+
* cluster. Forked children inherit this callback but must not run it, hence
196+
* the IsUnderPostmaster guard: only the lifecycle owner (postmaster, or a
197+
* standalone backend) cleans up.
198+
*/
199+
static void
200+
injection_points_dir_cleanup(int code, Datum arg)
201+
{
202+
struct stat st;
203+
204+
if (!IsUnderPostmaster && stat(INJ_POINTS_DIR, &st) == 0)
205+
(void) rmtree(INJ_POINTS_DIR, true);
206+
}
207+
116208
static void
117209
injection_shmem_init(void *arg)
118210
{
@@ -121,6 +213,13 @@ injection_shmem_init(void *arg)
121213
* initialization using a DSM.
122214
*/
123215
injection_point_init_state(inj_state, NULL);
216+
217+
/*
218+
* Attach any wait points requested out of band through marker directories,
219+
* and make sure the directory does not outlive the cluster.
220+
*/
221+
injection_points_preload();
222+
on_proc_exit(injection_points_dir_cleanup, 0);
124223
}
125224

126225
/*
@@ -219,14 +318,16 @@ injection_notice(const char *name, const void *private_data, void *arg)
219318
elog(NOTICE, "notice triggered for injection point %s", name);
220319
}
221320

222-
/* Wait until injection_points_wakeup() is called */
321+
/* Wait until released by injection_points_wakeup() or a removed marker file */
223322
void
224323
injection_wait(const char *name, const void *private_data, void *arg)
225324
{
226325
uint32 old_wait_counts = 0;
227326
int index = -1;
228327
uint32 injection_wait_event = 0;
229328
const InjectionPointCondition *condition = private_data;
329+
char waiter_path[MAXPGPATH];
330+
bool have_waiter_file = false;
230331

231332
if (inj_state == NULL)
232333
injection_init_shmem();
@@ -262,7 +363,35 @@ injection_wait(const char *name, const void *private_data, void *arg)
262363
name);
263364

264365
/*
265-
* Wait until the counter is bumped by injection_points_wakeup().
366+
* Publish our presence in the filesystem so an out-of-process test can see
367+
* that we are parked here and release us with no SQL connection: create
368+
* "pg_injection_points/<name>/<pid>" and wait until it is removed. The
369+
* directories are created on demand, so this works for points attached
370+
* through SQL too, not only those attached from a marker directory.
371+
*/
372+
{
373+
char dirpath[MAXPGPATH];
374+
int fd;
375+
376+
injection_point_dir(dirpath, sizeof(dirpath), name);
377+
(void) MakePGDirectory(INJ_POINTS_DIR);
378+
(void) MakePGDirectory(dirpath);
379+
380+
injection_point_waiter_path(waiter_path, sizeof(waiter_path), name,
381+
MyProcPid);
382+
fd = OpenTransientFile(waiter_path, O_RDWR | O_CREAT | O_TRUNC);
383+
if (fd >= 0)
384+
{
385+
CloseTransientFile(fd);
386+
have_waiter_file = true;
387+
}
388+
}
389+
390+
/*
391+
* Wait until released, either by injection_points_wakeup() bumping the
392+
* counter or by our waiter file being removed. The latter needs no SQL
393+
* connection, so it works in code paths that run before the server can
394+
* answer queries.
266395
*
267396
* This loop starts with a short delay for responsiveness, enlarged to
268397
* ease the CPU workload in slower environments.
@@ -272,8 +401,10 @@ injection_wait(const char *name, const void *private_data, void *arg)
272401
pgstat_report_wait_start(injection_wait_event);
273402
{
274403
int delay_us = INJ_WAIT_INITIAL_US;
404+
struct stat st;
275405

276-
while (pg_atomic_read_u32(&inj_state->wait_counts[index]) == old_wait_counts)
406+
while (pg_atomic_read_u32(&inj_state->wait_counts[index]) == old_wait_counts &&
407+
(!have_waiter_file || stat(waiter_path, &st) == 0))
277408
{
278409
CHECK_FOR_INTERRUPTS();
279410
pg_usleep(delay_us);
@@ -283,6 +414,10 @@ injection_wait(const char *name, const void *private_data, void *arg)
283414
}
284415
pgstat_report_wait_end();
285416

417+
/* Clean up our waiter file; it may still exist after a counter wakeup. */
418+
if (have_waiter_file)
419+
(void) unlink(waiter_path);
420+
286421
/* Remove this injection point from the waiters. */
287422
SpinLockAcquire(&inj_state->lock);
288423
inj_state->name[index][0] = '\0';

‎src/test/modules/injection_points/meson.build‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,12 @@ tests += {
6060
'--temp-config', files('extra.conf'),
6161
],
6262
},
63+
'tap': {
64+
'env': {
65+
'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
66+
},
67+
'tests': [
68+
't/001_wait_without_sql.pl',
69+
],
70+
},
6371
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright (c) 2026, PostgreSQL Global Development Group
2+
3+
# Exercise driving a wait injection point purely through the filesystem, with
4+
# no SQL used for the coordination itself. This is meant for code paths that
5+
# run before the server can answer queries (e.g. early postmaster startup).
6+
#
7+
# Layout under the data directory:
8+
# pg_injection_points/<point>/ present -> <point> attached as a wait
9+
# pg_injection_points/<point>/<pid> present -> that backend is parked here
10+
# Removing the <pid> file wakes that backend.
11+
12+
use strict;
13+
use warnings FATAL => 'all';
14+
15+
use Time::HiRes qw(usleep);
16+
17+
use PostgreSQL::Test::Cluster;
18+
use PostgreSQL::Test::Utils;
19+
use Test::More;
20+
21+
if ($ENV{enable_injection_points} ne 'yes')
22+
{
23+
plan skip_all => 'Injection points not supported by this build';
24+
}
25+
26+
my $point = 'no-sql-wait';
27+
28+
my $node = PostgreSQL::Test::Cluster->new('node');
29+
$node->init;
30+
$node->append_conf('postgresql.conf',
31+
"shared_preload_libraries = 'injection_points'");
32+
33+
# Attach the wait point before the server is even running, without any SQL:
34+
# just create its directory. The module scans these at startup.
35+
my $root = $node->data_dir . '/pg_injection_points';
36+
my $pdir = "$root/$point";
37+
mkdir $root or die "could not create $root: $!";
38+
mkdir $pdir or die "could not create $pdir: $!";
39+
40+
$node->start;
41+
42+
# The module attached the point from its directory at startup; we never called
43+
# injection_points_attach().
44+
$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
45+
46+
my $listed = $node->safe_psql('postgres',
47+
"SELECT count(*) FROM injection_points_list() WHERE point_name = '$point';"
48+
);
49+
is($listed, '1', 'wait point attached from its directory, without SQL');
50+
51+
# Fire the point in a background session; it must block in injection_wait().
52+
my $bg = $node->background_psql('postgres');
53+
$bg->query_until(
54+
qr/start/, qq[
55+
\\echo start
56+
SELECT injection_points_run('$point');
57+
]);
58+
59+
# Detect that the backend is parked, purely through the filesystem: it
60+
# publishes a file named after its PID inside the point directory.
61+
my $waiter;
62+
foreach my $i (1 .. 1800)
63+
{
64+
if (opendir(my $dh, $pdir))
65+
{
66+
($waiter) = grep { /^\d+\z/ } readdir($dh);
67+
closedir($dh);
68+
last if defined $waiter;
69+
}
70+
usleep(100_000);
71+
}
72+
ok(defined $waiter, 'backend published its waiter file (filesystem-observable)');
73+
74+
# Wake that specific backend without any SQL: remove its waiter file.
75+
unlink "$pdir/$waiter" or die "could not remove waiter file: $!";
76+
77+
# The blocked statement now finishes, proving the filesystem wakeup worked.
78+
like($bg->query_safe('SELECT 1;'),
79+
qr/^1$/m, 'backend released by removing its waiter file, without SQL');
80+
$bg->quit;
81+
82+
# The directory does not survive the cluster.
83+
$node->stop;
84+
ok(!-d $root, 'injection points directory removed at shutdown');
85+
86+
done_testing();

0 commit comments

Comments
 (0)