diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml index 0c60bafac635..26d2ff64a591 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 cf44a6771871..e44df3807f15 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 9a07f6e1d92a..0604f4f38202 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 b1b73dac3bed..85ba13b70d8e 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 000000000000..c84aeed9cd24 --- /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 000000000000..67de54be5b71 --- /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 4c5af018ee44..83103fab9432 100644 --- a/src/test/recovery/t/013_crash_restart.pl +++ b/src/test/recovery/t/013_crash_restart.pl @@ -251,4 +251,115 @@ $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();