Skip to content
Merged
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
22 changes: 18 additions & 4 deletions src/pl/plpython/plpy_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ void PLy_handle_cancel_interrupt(void);

bool PLy_enter_python_intepreter = false;

static volatile sig_atomic_t plpy_cancel_callback_queued = false;

void
_PG_init(void)
{
Expand Down Expand Up @@ -147,6 +149,12 @@ PLy_initialize(void)
int
PLy_python_cancel_handler(void *arg)
{
plpy_cancel_callback_queued = false;

/* 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 All @@ -166,11 +174,17 @@ PLy_handle_cancel_interrupt(void)
* We can't do much in a signal handler, so just tell the Python
* interpreter to call us back when possible.
*
* We don't bother to check the return value, as there's nothing we could
* do if it fails for some reason.
* A single timeout or cancel request can deliver SIGINT twice, to the
* process group and to the backend itself. Keep at most one callback
* queued, so that a duplicate KeyboardInterrupt cannot abort the Python
* code handling the first one.
*/
if (PLy_enter_python_intepreter)
(void) Py_AddPendingCall(PLy_python_cancel_handler, NULL);
if (PLy_enter_python_intepreter && !plpy_cancel_callback_queued)
{
plpy_cancel_callback_queued = true;
if (Py_AddPendingCall(PLy_python_cancel_handler, NULL) != 0)
plpy_cancel_callback_queued = false;
}

if (prev_cancel_pending_hook)
prev_cancel_pending_hook();
Expand Down
35 changes: 35 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,38 @@ PL/Python function "pynestsleep2"
7q: ... <quitting>
-- end_ignore

-- A timeout must deliver only one KeyboardInterrupt and let its handler
-- finish. The handler makes a call so that, on any Python version, a
-- duplicate callback would run inside it.
CREATE OR REPLACE FUNCTION pysleephandler() RETURNS text AS $$ import time try: while True: time.sleep(0.1) except KeyboardInterrupt: time.sleep(0.1) 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"
RESET statement_timeout;
RESET
SELECT pystage();
pystage
---------
handled
(1 row)

-- Once a KeyboardInterrupt has been raised, a later SIGINT must raise
-- another one.
CREATE OR REPLACE FUNCTION pycancelhandler() RETURNS text AS $$ import os import signal try: os.kill(os.getpid(), signal.SIGINT) while True: pass except KeyboardInterrupt: pass 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)
44 changes: 44 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,47 @@ FROM pg_stat_activity WHERE query LIKE 'select pynestsleep2()%' ORDER BY pid LIM
7q:
-- end_ignore

-- A timeout must deliver only one KeyboardInterrupt and let its handler
-- finish. The handler makes a call so that, on any Python version, a
-- duplicate callback would run inside it.
CREATE OR REPLACE FUNCTION pysleephandler() RETURNS text AS $$
import time
try:
while True:
time.sleep(0.1)
except KeyboardInterrupt:
time.sleep(0.1)
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();
RESET statement_timeout;
SELECT pystage();

-- Once a KeyboardInterrupt has been raised, a later SIGINT must raise
-- another one.
CREATE OR REPLACE FUNCTION pycancelhandler() RETURNS text AS $$
import os
import signal
try:
os.kill(os.getpid(), signal.SIGINT)
while True:
pass
except KeyboardInterrupt:
pass
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