diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 99e68f3b942..1fb3db98f74 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -2977,7 +2977,18 @@ processCancelRequest(Port *port, void *pkt, MsgType code) ereport(DEBUG2, (errmsg_internal("processing cancel request: sending SIGINT to process %d", backendPID))); - signal_child(bp->pid, SIGINT); +#ifdef HAVE_SETSID + /* + * The backend has called setsid() before sending its + * cancel key to the client. Signal the group once to + * avoid duplicate KeyboardInterrupts in PL/Python. + */ + if (kill(-bp->pid, SIGINT) == 0) + return; +#endif + if (kill(bp->pid, SIGINT) < 0) + elog(DEBUG3, "kill(%ld,%d) failed: %m", + (long) bp->pid, SIGINT); } } else diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index d02f33158ef..a9a96846c54 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -1858,8 +1858,9 @@ StatementTimeoutHandler(void) sig = SIGTERM; #ifdef HAVE_SETSID - /* try to signal whole process group */ - kill(-MyProcPid, sig); + /* Avoid delivering the same timeout twice to procedural languages. */ + if (kill(-MyProcPid, sig) == 0) + return; #endif kill(MyProcPid, sig); } @@ -1871,8 +1872,8 @@ static void LockTimeoutHandler(void) { #ifdef HAVE_SETSID - /* try to signal whole process group */ - kill(-MyProcPid, SIGINT); + if (kill(-MyProcPid, SIGINT) == 0) + return; #endif kill(MyProcPid, SIGINT); } diff --git a/src/pl/plpython/plpy_main.c b/src/pl/plpython/plpy_main.c index 64cb9fc5117..07c2b5b3bbd 100644 --- a/src/pl/plpython/plpy_main.c +++ b/src/pl/plpython/plpy_main.c @@ -147,6 +147,10 @@ PLy_initialize(void) int PLy_python_cancel_handler(void *arg) { + /* A queued callback can outlive the cancellation that scheduled it. */ + if (!QueryCancelPending) + return 0; + PyErr_SetNone(PyExc_KeyboardInterrupt); /* return -1 to indicate that we set an exception. */ diff --git a/src/test/isolation2/expected/cancel_plpython.out b/src/test/isolation2/expected/cancel_plpython.out index 37964e3b04c..f1bebf4e105 100644 --- a/src/test/isolation2/expected/cancel_plpython.out +++ b/src/test/isolation2/expected/cancel_plpython.out @@ -100,3 +100,39 @@ PL/Python function "pynestsleep2" 7q: ... -- end_ignore +-- A timeout must deliver only one KeyboardInterrupt, let its handler finish, +-- and leave the next PL/Python call free of pending callbacks. +CREATE OR REPLACE FUNCTION pysleephandler() RETURNS text AS $$ import time try: while True: time.sleep(0.1) except KeyboardInterrupt: GD['stage'] = 'handled' raise $$ LANGUAGE plpython3u; +CREATE + +CREATE OR REPLACE FUNCTION pystage() RETURNS text AS $$ return GD.get('stage') $$ LANGUAGE plpython3u; +CREATE + +SET statement_timeout = '1s'; +SET +SELECT pysleephandler(); +ERROR: canceling statement due to statement timeout +CONTEXT: PL/Python function "pysleephandler" +SELECT pybusyloop(); +ERROR: canceling statement due to statement timeout +CONTEXT: PL/Python function "pybusyloop" +RESET statement_timeout; +RESET +SELECT pystage(); + pystage +--------- + handled +(1 row) + +-- Catching one cancellation must not suppress a later, independent request. +CREATE OR REPLACE FUNCTION pycancelhandler() RETURNS text AS $$ import os import signal GD['stage'] = 'started' try: os.kill(os.getpid(), signal.SIGINT) while True: pass except KeyboardInterrupt: GD['stage'] = 'handled' try: os.kill(os.getpid(), signal.SIGINT) except KeyboardInterrupt: GD['stage'] = 'cancelled twice' raise GD['stage'] = 'second cancel ignored' $$ LANGUAGE plpython3u; +CREATE + +SELECT pycancelhandler(); +ERROR: canceling statement due to user request +CONTEXT: PL/Python function "pycancelhandler" +SELECT pystage(); + pystage +----------------- + cancelled twice +(1 row) diff --git a/src/test/isolation2/sql/cancel_plpython.sql b/src/test/isolation2/sql/cancel_plpython.sql index 0c78b5a7115..0c9a9989e0a 100644 --- a/src/test/isolation2/sql/cancel_plpython.sql +++ b/src/test/isolation2/sql/cancel_plpython.sql @@ -75,3 +75,46 @@ FROM pg_stat_activity WHERE query LIKE 'select pynestsleep2()%' ORDER BY pid LIM 7q: -- end_ignore +-- A timeout must deliver only one KeyboardInterrupt, let its handler finish, +-- and leave the next PL/Python call free of pending callbacks. +CREATE OR REPLACE FUNCTION pysleephandler() RETURNS text AS $$ +import time +try: + while True: + time.sleep(0.1) +except KeyboardInterrupt: + GD['stage'] = 'handled' + raise +$$ LANGUAGE plpython3u; + +CREATE OR REPLACE FUNCTION pystage() RETURNS text AS $$ +return GD.get('stage') +$$ LANGUAGE plpython3u; + +SET statement_timeout = '1s'; +SELECT pysleephandler(); +SELECT pybusyloop(); +RESET statement_timeout; +SELECT pystage(); + +-- Catching one cancellation must not suppress a later, independent request. +CREATE OR REPLACE FUNCTION pycancelhandler() RETURNS text AS $$ +import os +import signal +GD['stage'] = 'started' +try: + os.kill(os.getpid(), signal.SIGINT) + while True: + pass +except KeyboardInterrupt: + GD['stage'] = 'handled' +try: + os.kill(os.getpid(), signal.SIGINT) +except KeyboardInterrupt: + GD['stage'] = 'cancelled twice' + raise +GD['stage'] = 'second cancel ignored' +$$ LANGUAGE plpython3u; + +SELECT pycancelhandler(); +SELECT pystage();