From 8e43c7656980a8bcf86f9b592b21de4636502076 Mon Sep 17 00:00:00 2001 From: Bryan Green Date: Thu, 30 Oct 2025 11:14:55 -0600 Subject: [PATCH v2] Use Windows Job Objects to prevent orphaned child processes. When the postmaster exits on Windows, backends can continue running because Windows lacks Unix's getppid() orphan detection. Orphaned backends hold locks and shared memory, preventing clean restart. Create a Job Object at postmaster startup and assign the postmaster to it. Children inherit job membership automatically. Configure with KILL_ON_JOB_CLOSE so the kernel terminates all children when the job handle closes on postmaster exit. This is more reliable than existing approaches (inherited handles, shared memory flags) because it's kernel-enforced with no polling. Job creation is allowed to fail non-fatally. Some environments run PostgreSQL under an existing job, and Windows 7 disallows nested jobs. In such cases we log a message and proceed without orphan protection. Author: Bryan Green --- doc/src/sgml/runtime.sgml | 21 +++ src/backend/postmaster/postmaster.c | 12 ++ 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, 366 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 0c60bafac6..26d2ff64a5 100644 --- a/doc/src/sgml/runtime.sgml +++ b/doc/src/sgml/runtime.sgml @@ -1501,6 +1501,27 @@ $ cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages + + + Process Management on <systemitem class="osname">Windows</systemitem> + + + On Windows, + PostgreSQL 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. + + + + In some cases, Job Object creation may fail (for example, when + PostgreSQL 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. + + diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 00de559ba8..14697b95e2 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 "tcop/backend_startup.h" @@ -1005,6 +1006,17 @@ PostmasterMain(int argc, char *argv[]) */ CreateSharedMemoryAndSemaphores(); +#ifdef WIN32 + /* + * On Windows, create a job object to prevent orphaned backends. + * If postmaster crashes, Windows will automatically kill all + * child processes in the job. + * + * We do this after port binding so that if job creation fails, + * it's not fatal - we can still run (just without orphan protection). + */ + 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 9a07f6e1d9..0604f4f382 100644 --- a/src/backend/storage/ipc/Makefile +++ b/src/backend/storage/ipc/Makefile @@ -28,4 +28,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 b1b73dac3b..85ba13b70d 100644 --- a/src/backend/storage/ipc/meson.build +++ b/src/backend/storage/ipc/meson.build @@ -21,3 +21,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 0000000000..c84aeed9cd --- /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 0000000000..67de54be5b --- /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 4c5af018ee..db3af2c7a4 100644 --- a/src/test/recovery/t/013_crash_restart.pl +++ b/src/test/recovery/t/013_crash_restart.pl @@ -251,4 +251,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.46.0.windows.1