Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/backend/postmaster/postmaster.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/backend/utils/init/postinit.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions src/pl/plpython/plpy_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
36 changes: 36 additions & 0 deletions src/test/isolation2/expected/cancel_plpython.out
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,39 @@ PL/Python function "pynestsleep2"
7q: ... <quitting>
-- 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)
43 changes: 43 additions & 0 deletions src/test/isolation2/sql/cancel_plpython.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Loading