Regarding reproduction, the two attachments contain a local rebase of your public v2, followed by my anonymous-job supplement. The prerequisite retains the Job logic and recovery test - it is not a new author revision.
The exact 158b6d5109fa source passed ten server crash/restart cycles per architecture after clean native Windows x64 and ARM64 builds. The prerequisite still attempts nested assignment while its comment says nesting is skipped. Could the comment describe that attempt, and the access-denied report preserve the actual failure code without assuming the outer supervisor handles cleanup? Anonymous naming doesn't establish that policy or prevent privileged handle duplication. Best, Harrison
From a5a5f52a5a0af2c87478e4686665460206ea29d4 Mon Sep 17 00:00:00 2001 From: Bryan Green <[email protected]> Date: Thu, 30 Oct 2025 11:14:55 -0600 Subject: [PATCH 1/2] Use Windows Job Objects to prevent orphaned child processes (local rebase) Original proposal: Bryan Green, CF6201 public v2. Discussion: https://www.postgresql.org/message-id/[email protected] Locally rebased by Harrison Booth for review against recorded 158b6d5109fad9d5f0c30693fa85041a1c621fe1. This is not a new Bryan revision or an author-endorsed change. The existing local integration shortens the postmaster call-site comment; original Job logic, diagnostics and recovery test are preserved. Known nested-job comment and supervisor-policy claims still need author review. Apply before the separate anonymous-job supplement. --- doc/src/sgml/runtime.sgml | 21 +++ src/backend/postmaster/postmaster.c | 6 + src/backend/storage/ipc/Makefile | 4 + src/backend/storage/ipc/meson.build | 7 + src/backend/storage/ipc/pg_job_object.c | 176 +++++++++++++++++++++++ src/include/storage/pg_job_object.h | 35 +++++ src/test/recovery/t/013_crash_restart.pl | 111 ++++++++++++++ 7 files changed, 360 insertions(+) create mode 100644 src/backend/storage/ipc/pg_job_object.c create mode 100644 src/include/storage/pg_job_object.h diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml index d9984910cc46c9e5b3a655bda4945580131af161..87e4f4e0a0d6bafca2ff6335caa382c4dba75ba0 100644 --- a/doc/src/sgml/runtime.sgml +++ b/doc/src/sgml/runtime.sgml @@ -1520,6 +1520,27 @@ $ <userinput>cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages</userinp </para> </sect2> + + <sect2 id="windows-process-management"> + <title>Process Management on <systemitem class="osname">Windows</systemitem></title> + + <para> + On <systemitem class="osname">Windows</systemitem>, + <productname>PostgreSQL</productname> uses Job Objects to + manage child processes. If the postmaster exits unexpectedly + (due to a crash or external termination), the operating system + automatically terminates all backend processes. This prevents + orphaned backends that could hold locks or corrupt shared memory. + </para> + + <para> + In some cases, Job Object creation may fail (for example, when + <productname>PostgreSQL</productname> is already running under a + job-aware service manager). The server will log a message and continue + to run without orphan protection. This is safe but means that backends + may need to be manually terminated if the postmaster crashes. + </para> + </sect2> </sect1> diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index f53aa9c404d5f9027e89817b3bd1b174f9222eff..f4b2bd099fc9fe3bf4abdda769409b0b834bd870 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -113,6 +113,7 @@ #include "storage/fd.h" #include "storage/io_worker.h" #include "storage/ipc.h" +#include "storage/pg_job_object.h" #include "storage/pmsignal.h" #include "storage/proc.h" #include "storage/shmem_internal.h" @@ -1016,6 +1017,11 @@ PostmasterMain(int argc, char *argv[]) */ CreateSharedMemoryAndSemaphores(); +#ifdef WIN32 + /* Configure child cleanup before launching any Windows backends. */ + pg_create_job_object(); +#endif + /* * Estimate number of openable files. This must happen after setting up * semaphores, because on some platforms semaphores count as open files. diff --git a/src/backend/storage/ipc/Makefile b/src/backend/storage/ipc/Makefile index f71653bbe482e36a57f2cfad93bd6a975c2aef66..fbb69c0d20d50b3eda9869c7879acdc2c2ef9f6c 100644 --- a/src/backend/storage/ipc/Makefile +++ b/src/backend/storage/ipc/Makefile @@ -29,4 +29,8 @@ OBJS = \ standby.o \ waiteventset.o +ifeq ($(PORTNAME), win32) +OBJS += pg_job_object.o +endif + include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/storage/ipc/meson.build b/src/backend/storage/ipc/meson.build index b8c31e29967dad06a7b991584dc53c2b8a63fd97..eb7c7047e0e4f3d43043af23ef30289fba4b3f50 100644 --- a/src/backend/storage/ipc/meson.build +++ b/src/backend/storage/ipc/meson.build @@ -22,3 +22,10 @@ backend_sources += files( 'waiteventset.c', ) + +# Windows-specific files +if host_system == 'windows' + backend_sources += files( + 'pg_job_object.c', + ) +endif diff --git a/src/backend/storage/ipc/pg_job_object.c b/src/backend/storage/ipc/pg_job_object.c new file mode 100644 index 0000000000000000000000000000000000000000..4c9bd90f3b3c1515e9e217f31f88d50a94e3e577 --- /dev/null +++ b/src/backend/storage/ipc/pg_job_object.c @@ -0,0 +1,176 @@ +/*------------------------------------------------------------------------- + * + * pg_job_object.c + * Windows Job Object support for preventing orphaned backends + * + * On Unix, backends can detect when the postmaster dies via getppid(). + * Windows has no equivalent mechanism. We solve this by using Job Objects, + * a Windows kernel feature that groups processes and can automatically + * terminate all members when the job handle closes. + * + * By configuring JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, we ensure that if + * the postmaster exits (cleanly or via crash), Windows immediately kills + * all backends. This prevents orphaned processes that hold locks and + * prevent clean restart. + * + * The job object handle is stored in a static variable and never explicitly + * closed. This is intentional - we rely on Windows closing it automatically + * when the postmaster process exits, which triggers the child termination. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/storage/ipc/pg_job_object.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#ifdef WIN32 + +#include "postmaster/postmaster.h" +#include "storage/ipc.h" +#include "storage/pg_job_object.h" + +static HANDLE pg_job_object = NULL; + + +/* + * pg_create_job_object + * + * Create job object for this PostgreSQL instance and configure it to + * kill all children when the postmaster exits. + * + * Failure is not fatal - we log a warning and continue. PostgreSQL will + * run without orphan protection, which is no worse than current behavior. + */ +void +pg_create_job_object(void) +{ + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limit_info; + char job_name[128]; + DWORD error; + + snprintf(job_name, sizeof(job_name), "PostgreSQL_Port_%d_PID_%lu", + PostPortNumber, GetCurrentProcessId()); + + pg_job_object = CreateJobObjectA(NULL, job_name); + + if (pg_job_object == NULL) + { + error = GetLastError(); + ereport(LOG, + (errmsg("could not create job object \"%s\": error code %lu", + job_name, error), + errdetail("Orphaned process cleanup will not be available."))); + return; + } + + elog(DEBUG1, "created job object \"%s\"", job_name); + + /* + * Set KILL_ON_JOB_CLOSE. When the job handle closes (either explicit + * close or process termination), all processes in the job are terminated. + * + * This is the critical flag that prevents orphaned backends. + */ + memset(&limit_info, 0, sizeof(limit_info)); + limit_info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if (!SetInformationJobObject(pg_job_object, + JobObjectExtendedLimitInformation, + &limit_info, + sizeof(limit_info))) + { + error = GetLastError(); + ereport(WARNING, + (errmsg("could not configure job object: error code %lu", error), + errdetail("Job object created but KILL_ON_JOB_CLOSE not set."), + errhint("Orphaned processes may occur if postmaster crashes."))); + CloseHandle(pg_job_object); + pg_job_object = NULL; + return; + } + + if (!AssignProcessToJobObject(pg_job_object, GetCurrentProcess())) + { + error = GetLastError(); + + /* + * ERROR_ACCESS_DENIED means we're already in a job. This can happen + * when PostgreSQL runs under a job-aware supervisor (Windows service + * on older Windows, or any process manager using nested jobs). + * + * On Windows 8+, we could use nested jobs, but for simplicity we just + * skip job creation. The parent job should handle cleanup. + */ + if (error == ERROR_ACCESS_DENIED) + { + ereport(LOG, + (errmsg("postmaster is already in a job object"), + errdetail("This can occur when PostgreSQL is run under a job-aware supervisor."), + errhint("Automatic orphan cleanup will not be available."))); + } + else + { + ereport(WARNING, + (errmsg("could not assign postmaster to job object: error code %lu", error))); + } + + CloseHandle(pg_job_object); + pg_job_object = NULL; + return; + } + + elog(LOG, "PostgreSQL job object configured successfully - orphaned process prevention enabled"); +} + + +/* + * pg_destroy_job_object + * + * Explicitly close the job object handle. This will trigger KILL_ON_JOB_CLOSE, + * terminating all backends. + * + * Note: In most cases we don't call this - we rely on Windows closing the + * handle automatically when the postmaster exits. Explicit close is only + * needed if we want to control the exact timing of backend termination. + */ +void +pg_destroy_job_object(void) +{ + if (pg_job_object != NULL) + { + elog(DEBUG1, "closing job object - all child processes will terminate"); + CloseHandle(pg_job_object); + pg_job_object = NULL; + } +} + + +/* + * pg_is_in_job_object + * + * Check if current process is in the PostgreSQL job object. + * Used primarily for testing and verification. + */ +bool +pg_is_in_job_object(void) +{ + BOOL in_job = FALSE; + + if (pg_job_object == NULL) + return false; + + if (!IsProcessInJob(GetCurrentProcess(), pg_job_object, &in_job)) + { + elog(DEBUG1, "IsProcessInJob failed: error code %lu", GetLastError()); + return false; + } + + return (bool) in_job; +} + +#endif /* WIN32 */ diff --git a/src/include/storage/pg_job_object.h b/src/include/storage/pg_job_object.h new file mode 100644 index 0000000000000000000000000000000000000000..67de54be5b71484f6977718b964fb45c2d633082 --- /dev/null +++ b/src/include/storage/pg_job_object.h @@ -0,0 +1,35 @@ +/*------------------------------------------------------------------------- + * + * pg_job_object.h + * Windows Job Object support for preventing orphaned backends + * + * When the postmaster crashes on Windows, child processes continue running + * because Windows has no equivalent to Unix's parent death detection. Job + * Objects solve this by allowing the kernel to terminate all children when + * the job handle closes (which happens automatically on process exit). + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/storage/pg_job_object.h + * + *------------------------------------------------------------------------- + */ +#ifndef PG_JOB_OBJECT_H +#define PG_JOB_OBJECT_H + +#ifdef WIN32 + +extern void pg_create_job_object(void); +extern void pg_destroy_job_object(void); +extern bool pg_is_in_job_object(void); + +#else /* !WIN32 */ + +#define pg_create_job_object() ((void) 0) +#define pg_destroy_job_object() ((void) 0) +#define pg_is_in_job_object() (false) + +#endif /* WIN32 */ + +#endif /* PG_JOB_OBJECT_H */ diff --git a/src/test/recovery/t/013_crash_restart.pl b/src/test/recovery/t/013_crash_restart.pl index 0fde920713e53c1f52e87d4edc15a370803c1309..60834d31c763587e6410d88f14351eea88279d32 100644 --- a/src/test/recovery/t/013_crash_restart.pl +++ b/src/test/recovery/t/013_crash_restart.pl @@ -278,4 +278,115 @@ is( $node->safe_psql( $node->stop(); +# Test Windows Job Objects orphan prevention +if ($windows_os) +{ + note "testing Windows Job Object orphan prevention"; + + my $jobtest = PostgreSQL::Test::Cluster->new('jobtest'); + $jobtest->init(allows_streaming => 1); + $jobtest->start(); + + my $log_contents = slurp_file($jobtest->logfile); + like($log_contents, qr/orphaned process prevention enabled/, + "Job Objects enabled on startup"); + + $jobtest->safe_psql('postgres', + q[ALTER SYSTEM SET restart_after_crash = 1; + SELECT pg_reload_conf();]); + + my ($backend_stdin, $backend_stdout, $backend_stderr) = ('', '', ''); + my $backend = IPC::Run::start( + [ + 'psql', '--no-psqlrc', '--quiet', '--no-align', '--tuples-only', + '--file', '-', '--dbname' => $jobtest->connstr('postgres') + ], + '<' => \$backend_stdin, + '>' => \$backend_stdout, + '2>' => \$backend_stderr, + $psql_timeout); + + $backend_stdin .= q[ +BEGIN; +CREATE TABLE jobtest(id int); +SELECT pg_backend_pid(); +]; + ok( pump_until( + $backend, $psql_timeout, + \$backend_stdout, qr/[[:digit:]]+[\r\n]$/m), + 'acquired backend pid for job object test'); + my $backend_pid = $backend_stdout; + chomp($backend_pid); + $backend_stdout = ''; + + note "backend PID: $backend_pid"; + + my $postmaster_pidfile = $jobtest->data_dir . '/postmaster.pid'; + open(my $pidfh, '<', $postmaster_pidfile) + or die "cannot open postmaster.pid: $!"; + my $postmaster_pid = <$pidfh>; + close($pidfh); + chomp($postmaster_pid); + + note "postmaster PID: $postmaster_pid"; + ok($postmaster_pid =~ /^\d+$/ && $postmaster_pid > 0, + "got valid postmaster PID: $postmaster_pid"); + + note "killing postmaster PID $postmaster_pid"; + my $ret = PostgreSQL::Test::Utils::system_log('pg_ctl', 'kill', 'KILL', + $postmaster_pid); + is($ret, 0, "killed postmaster with pg_ctl kill KILL"); + + eval { $backend->finish; }; + + note "waiting for postmaster process to exit"; + my $postmaster_gone = 0; + for (my $i = 0; $i < 30; $i++) + { + my $check = `tasklist /FI "PID eq $postmaster_pid" /NH 2>&1`; + if ($check !~ /postgres\.exe/) + { + $postmaster_gone = 1; + note "postmaster exited after " . (($i + 1) * 100) . "ms"; + last; + } + select(undef, undef, undef, 0.1); + } + + ok($postmaster_gone, "postmaster process exited"); + + $jobtest->{_pid} = undef; + + unlink($postmaster_pidfile); + + # THE REAL TEST: Can we restart without conflicts? + # If orphans exist, they'll block the port or shared memory + note "attempting restart (will fail if orphans present)"; + + my $restart_ok = 0; + eval { + $jobtest->start(); + $restart_ok = 1; + }; + + if (!$restart_ok) { + fail("restart failed - orphans may be blocking resources: $@"); + } else { + pass("restart succeeded - no orphans blocking port or shared memory"); + + eval { + my $result = $jobtest->safe_psql('postgres', 'SELECT 1'); + is($result, '1', 'server functional after restart'); + }; + if ($@) { + note "query after restart failed: $@"; + fail("query after restart failed"); + } + } + + note "stopping test cluster"; + eval { $jobtest->stop('immediate'); }; +} + + done_testing(); -- 2.53.0.windows.2
From bd0fc1ee65e81fc614215895448ef7840cf6d6ba Mon Sep 17 00:00:00 2001 From: Harrison Booth <[email protected]> Date: Mon, 12 Oct 2026 20:51:37 -0500 Subject: [PATCH 2/2] Use an unnamed job for PostgreSQL orphan cleanup Keep the cleanup job private to the postmaster's handle. A predictable name allows CreateJobObject to open an existing object, or another process to open the configured job. Either can keep the last handle alive after postmaster exit and delay KILL_ON_JOB_CLOSE cleanup. An unnamed, non-inherited job avoids that named-object dependency while preserving automatic child membership and compatible nested-job use. Update diagnostics that formerly displayed the unused job name. This supplements Bryan Green's CF6201 v2 proposal. Source-extracted ARM64 and emulated x64 tests verify suspended-child cleanup and show the named-collision delay disappears with the anonymous candidate. --- src/backend/storage/ipc/pg_job_object.c | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/backend/storage/ipc/pg_job_object.c b/src/backend/storage/ipc/pg_job_object.c index 4c9bd90f3b3c1515e9e217f31f88d50a94e3e577..c67edc338e6b91e372e42153d8e4b30da90b466e 100644 --- a/src/backend/storage/ipc/pg_job_object.c +++ b/src/backend/storage/ipc/pg_job_object.c @@ -50,25 +50,21 @@ void pg_create_job_object(void) { JOBOBJECT_EXTENDED_LIMIT_INFORMATION limit_info; - char job_name[128]; DWORD error; - snprintf(job_name, sizeof(job_name), "PostgreSQL_Port_%d_PID_%lu", - PostPortNumber, GetCurrentProcessId()); - - pg_job_object = CreateJobObjectA(NULL, job_name); + /* Keep cleanup independent of other processes opening a named job. */ + pg_job_object = CreateJobObjectA(NULL, NULL); if (pg_job_object == NULL) { error = GetLastError(); ereport(LOG, - (errmsg("could not create job object \"%s\": error code %lu", - job_name, error), + (errmsg("could not create job object: error code %lu", error), errdetail("Orphaned process cleanup will not be available."))); return; } - elog(DEBUG1, "created job object \"%s\"", job_name); + elog(DEBUG1, "created job object"); /* * Set KILL_ON_JOB_CLOSE. When the job handle closes (either explicit -- 2.53.0.windows.2
