mirror of
				https://github.com/psycopg/psycopg2.git
				synced 2025-10-31 07:47:30 +03:00 
			
		
		
		
	Support query cancellation.
Add a cancel() method do the connection object that will interrupt the current query using the libpq PQcancel() function.
This commit is contained in:
		
							parent
							
								
									9f78141532
								
							
						
					
					
						commit
						751bfa1ea6
					
				|  | @ -264,6 +264,26 @@ The ``connection`` class | ||||||
|         (0) or closed (1). |         (0) or closed (1). | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |     .. method:: cancel | ||||||
|  | 
 | ||||||
|  |         Cancel the current database operation. | ||||||
|  | 
 | ||||||
|  |         The method interrupts the processing of the current operation. If no | ||||||
|  |         query is being executed, it does nothing. You can call this function | ||||||
|  |         from a different thread than the one currently executing a database | ||||||
|  |         operation, for instance if you want to cancel a long running query if a | ||||||
|  |         button is pushed in the UI. Interrupting query execution will cause the | ||||||
|  |         cancelled method to raise a | ||||||
|  |         `~psycopg2.extensions.QueryCanceledError`. Note that the termination | ||||||
|  |         of the query is not guaranteed to succeed: see the documentation for | ||||||
|  |         |PQcancel|_. | ||||||
|  | 
 | ||||||
|  |         .. |PQcancel| replace:: `!PQcancel()` | ||||||
|  |         .. _PQcancel: http://www.postgresql.org/docs/8.4/static/libpq-cancel.html#AEN34765 | ||||||
|  | 
 | ||||||
|  |         .. versionadded:: 2.2.3 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     .. method:: reset |     .. method:: reset | ||||||
| 
 | 
 | ||||||
|         Reset the connection to the default. |         Reset the connection to the default. | ||||||
|  |  | ||||||
|  | @ -101,6 +101,7 @@ typedef struct { | ||||||
|     int server_version;       /* server version */ |     int server_version;       /* server version */ | ||||||
| 
 | 
 | ||||||
|     PGconn *pgconn;           /* the postgresql connection */ |     PGconn *pgconn;           /* the postgresql connection */ | ||||||
|  |     PGcancel *cancel;         /* the cancellation structure */ | ||||||
| 
 | 
 | ||||||
|     PyObject *async_cursor;   /* a cursor executing an asynchronous query */ |     PyObject *async_cursor;   /* a cursor executing an asynchronous query */ | ||||||
|     int async_status;         /* asynchronous execution status */ |     int async_status;         /* asynchronous execution status */ | ||||||
|  |  | ||||||
|  | @ -282,6 +282,12 @@ conn_get_server_version(PGconn *pgconn) | ||||||
|     return (int)PQserverVersion(pgconn); |     return (int)PQserverVersion(pgconn); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | PGcancel * | ||||||
|  | conn_get_cancel(PGconn *pgconn) | ||||||
|  | { | ||||||
|  |     return PQgetCancel(pgconn); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| /* Return 1 if the server datestyle allows us to work without problems,
 | /* Return 1 if the server datestyle allows us to work without problems,
 | ||||||
|    0 if it needs to be set to something better, e.g. ISO. */ |    0 if it needs to be set to something better, e.g. ISO. */ | ||||||
|  | @ -320,6 +326,12 @@ conn_setup(connectionObject *self, PGconn *pgconn) | ||||||
|         return -1; |         return -1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     self->cancel = conn_get_cancel(self->pgconn); | ||||||
|  |     if (self->cancel == NULL) { | ||||||
|  |         PyErr_SetString(OperationalError, "can't get cancellation key"); | ||||||
|  |         return -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     Py_BEGIN_ALLOW_THREADS; |     Py_BEGIN_ALLOW_THREADS; | ||||||
|     pthread_mutex_lock(&self->lock); |     pthread_mutex_lock(&self->lock); | ||||||
|     Py_BLOCK_THREADS; |     Py_BLOCK_THREADS; | ||||||
|  | @ -645,6 +657,11 @@ _conn_poll_setup_async(connectionObject *self) | ||||||
|         if (self->encoding == NULL) { |         if (self->encoding == NULL) { | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|  |         self->cancel = conn_get_cancel(self->pgconn); | ||||||
|  |         if (self->cancel == NULL) { | ||||||
|  |             PyErr_SetString(OperationalError, "can't get cancellation key"); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         /* asynchronous connections always use isolation level 0, the user is
 |         /* asynchronous connections always use isolation level 0, the user is
 | ||||||
|          * expected to manage the transactions himself, by sending |          * expected to manage the transactions himself, by sending | ||||||
|  | @ -782,8 +799,10 @@ conn_close(connectionObject *self) | ||||||
| 
 | 
 | ||||||
|     if (self->pgconn) { |     if (self->pgconn) { | ||||||
|         PQfinish(self->pgconn); |         PQfinish(self->pgconn); | ||||||
|  |         PQfreeCancel(self->cancel); | ||||||
|         Dprintf("conn_close: PQfinish called"); |         Dprintf("conn_close: PQfinish called"); | ||||||
|         self->pgconn = NULL; |         self->pgconn = NULL; | ||||||
|  |         self->cancel = NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pthread_mutex_unlock(&self->lock); |     pthread_mutex_unlock(&self->lock); | ||||||
|  |  | ||||||
|  | @ -697,6 +697,37 @@ psyco_conn_isexecuting(connectionObject *self) | ||||||
|     return Py_False; |     return Py_False; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | /* extension: cancel - cancel the current operation */ | ||||||
|  | 
 | ||||||
|  | #define psyco_conn_cancel_doc                           \ | ||||||
|  | "cancel() -- cancel the current operation" | ||||||
|  | 
 | ||||||
|  | static PyObject * | ||||||
|  | psyco_conn_cancel(connectionObject *self) | ||||||
|  | { | ||||||
|  |     char errbuf[256]; | ||||||
|  | 
 | ||||||
|  |     EXC_IF_CONN_CLOSED(self); | ||||||
|  | 
 | ||||||
|  |     /* do not allow cancellation while the connection is being built */ | ||||||
|  |     Dprintf("psyco_conn_cancel: cancelling with key %p", self->cancel); | ||||||
|  |     if (self->status != CONN_STATUS_READY && | ||||||
|  |         self->status != CONN_STATUS_BEGIN) { | ||||||
|  |         PyErr_SetString(OperationalError, | ||||||
|  |                         "asynchronous connection attempt underway"); | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (PQcancel(self->cancel, errbuf, sizeof(errbuf)) == 0) { | ||||||
|  |         Dprintf("psyco_conn_cancel: cancelling failed: %s", errbuf); | ||||||
|  |         PyErr_SetString(OperationalError, errbuf); | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  |     Py_INCREF(Py_None); | ||||||
|  |     return Py_None; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #endif  /* PSYCOPG_EXTENSIONS */ | #endif  /* PSYCOPG_EXTENSIONS */ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -747,6 +778,8 @@ static struct PyMethodDef connectionObject_methods[] = { | ||||||
|      METH_NOARGS, psyco_conn_fileno_doc}, |      METH_NOARGS, psyco_conn_fileno_doc}, | ||||||
|     {"isexecuting", (PyCFunction)psyco_conn_isexecuting, |     {"isexecuting", (PyCFunction)psyco_conn_isexecuting, | ||||||
|      METH_NOARGS, psyco_conn_isexecuting_doc}, |      METH_NOARGS, psyco_conn_isexecuting_doc}, | ||||||
|  |     {"cancel", (PyCFunction)psyco_conn_cancel, | ||||||
|  |      METH_NOARGS, psyco_conn_cancel_doc}, | ||||||
| #endif | #endif | ||||||
|     {NULL} |     {NULL} | ||||||
| }; | }; | ||||||
|  | @ -827,6 +860,7 @@ connection_setup(connectionObject *self, const char *dsn, long int async) | ||||||
|     self->async_cursor = NULL; |     self->async_cursor = NULL; | ||||||
|     self->async_status = ASYNC_DONE; |     self->async_status = ASYNC_DONE; | ||||||
|     self->pgconn = NULL; |     self->pgconn = NULL; | ||||||
|  |     self->cancel = NULL; | ||||||
|     self->mark = 0; |     self->mark = 0; | ||||||
|     self->string_types = PyDict_New(); |     self->string_types = PyDict_New(); | ||||||
|     self->binary_types = PyDict_New(); |     self->binary_types = PyDict_New(); | ||||||
|  |  | ||||||
|  | @ -46,6 +46,7 @@ import test_copy | ||||||
| import test_notify | import test_notify | ||||||
| import test_async | import test_async | ||||||
| import test_green | import test_green | ||||||
|  | import test_cancel | ||||||
| 
 | 
 | ||||||
| def test_suite(): | def test_suite(): | ||||||
|     suite = unittest.TestSuite() |     suite = unittest.TestSuite() | ||||||
|  | @ -64,6 +65,7 @@ def test_suite(): | ||||||
|     suite.addTest(test_notify.test_suite()) |     suite.addTest(test_notify.test_suite()) | ||||||
|     suite.addTest(test_async.test_suite()) |     suite.addTest(test_async.test_suite()) | ||||||
|     suite.addTest(test_green.test_suite()) |     suite.addTest(test_green.test_suite()) | ||||||
|  |     suite.addTest(test_cancel.test_suite()) | ||||||
|     return suite |     return suite | ||||||
| 
 | 
 | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|  |  | ||||||
							
								
								
									
										89
									
								
								tests/test_cancel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								tests/test_cancel.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,89 @@ | ||||||
|  | #!/usr/bin/env python | ||||||
|  | 
 | ||||||
|  | import time | ||||||
|  | import threading | ||||||
|  | import unittest | ||||||
|  | 
 | ||||||
|  | import tests | ||||||
|  | import psycopg2 | ||||||
|  | import psycopg2.extensions | ||||||
|  | from psycopg2 import extras | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class CancelTests(unittest.TestCase): | ||||||
|  | 
 | ||||||
|  |     def setUp(self): | ||||||
|  |         self.conn = psycopg2.connect(tests.dsn) | ||||||
|  |         cur = self.conn.cursor() | ||||||
|  |         cur.execute(''' | ||||||
|  |             CREATE TEMPORARY TABLE table1 ( | ||||||
|  |               id int PRIMARY KEY | ||||||
|  |             )''') | ||||||
|  |         self.conn.commit() | ||||||
|  | 
 | ||||||
|  |     def tearDown(self): | ||||||
|  |         self.conn.close() | ||||||
|  | 
 | ||||||
|  |     def test_empty_cancel(self): | ||||||
|  |         self.conn.cancel() | ||||||
|  | 
 | ||||||
|  |     def test_cancel(self): | ||||||
|  |         errors = [] | ||||||
|  | 
 | ||||||
|  |         def neverending(conn): | ||||||
|  |             cur = conn.cursor() | ||||||
|  |             try: | ||||||
|  |                 self.assertRaises(psycopg2.extensions.QueryCanceledError, | ||||||
|  |                                   cur.execute, "select pg_sleep(10000)") | ||||||
|  |             # make sure the connection still works | ||||||
|  |                 conn.rollback() | ||||||
|  |                 cur.execute("select 1") | ||||||
|  |                 self.assertEqual(cur.fetchall(), [(1, )]) | ||||||
|  |             except Exception, e: | ||||||
|  |                 errors.append(e) | ||||||
|  |                 raise | ||||||
|  | 
 | ||||||
|  |         def canceller(conn): | ||||||
|  |             cur = conn.cursor() | ||||||
|  |             try: | ||||||
|  |                 conn.cancel() | ||||||
|  |             except Exception, e: | ||||||
|  |                 errors.append(e) | ||||||
|  |                 raise | ||||||
|  | 
 | ||||||
|  |         thread1 = threading.Thread(target=neverending, args=(self.conn, )) | ||||||
|  |         # wait a bit to make sure that the other thread is already in | ||||||
|  |         # pg_sleep -- ugly and racy, but the chances are ridiculously low | ||||||
|  |         thread2 = threading.Timer(0.3, canceller, args=(self.conn, )) | ||||||
|  |         thread1.start() | ||||||
|  |         thread2.start() | ||||||
|  |         thread1.join() | ||||||
|  |         thread2.join() | ||||||
|  | 
 | ||||||
|  |         self.assertEqual(errors, []) | ||||||
|  | 
 | ||||||
|  |     def test_async_cancel(self): | ||||||
|  |         async_conn = psycopg2.connect(tests.dsn, async=True) | ||||||
|  |         self.assertRaises(psycopg2.OperationalError, async_conn.cancel) | ||||||
|  |         extras.wait_select(async_conn) | ||||||
|  |         cur = async_conn.cursor() | ||||||
|  |         cur.execute("select pg_sleep(10000)") | ||||||
|  |         self.assertTrue(async_conn.isexecuting()) | ||||||
|  |         async_conn.cancel() | ||||||
|  |         self.assertRaises(psycopg2.extensions.QueryCanceledError, | ||||||
|  |                           extras.wait_select, async_conn) | ||||||
|  |         cur.execute("select 1") | ||||||
|  |         extras.wait_select(async_conn) | ||||||
|  |         self.assertEqual(cur.fetchall(), [(1, )]) | ||||||
|  | 
 | ||||||
|  |     def test_async_connection_cancel(self): | ||||||
|  |         async_conn = psycopg2.connect(tests.dsn, async=True) | ||||||
|  |         async_conn.close() | ||||||
|  |         self.assertTrue(async_conn.closed) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_suite(): | ||||||
|  |     return unittest.TestLoader().loadTestsFromName(__name__) | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     unittest.main() | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user