mirror of
https://github.com/psycopg/psycopg2.git
synced 2024-11-26 10:53:44 +03:00
* tests/test_transaction.py (DeadlockSerializationTestCase): port
over some tests for serialisation and deadlock errors, demonstrating that TransactionRollbackError is generated. (QueryCancelationTests): add a test to show that QueryCanceledError is raised on statement timeouts. * psycopg2da/adapter.py (_handle_psycopg_exception): rather than checking exception messages, check for TransactionRollbackError. * psycopg/pqpath.c (exception_from_sqlstate): return TransactionRollbackError for 40xxx errors, and QueryCanceledError for 57014 errors. (pq_raise): If we are using an old server, use TransactionRollbackError if the error message contains "could not serialize" or "deadlock detected". * psycopg/psycopgmodule.c (_psyco_connect_fill_exc): remove function, since we no longer need to store pointers to the exceptions in the connection. This also fixes a reference leak. (psyco_connect): remove _psyco_connect_fill_exc() function call. * psycopg/connection.h (connectionObject): remove exception members from struct. * psycopg/connection_type.c (connectionObject_getsets): modify the exception attributes on the connection object from members to getsets. This reduces the size of the struct. * lib/extensions.py: import the two new extensions. * psycopg/psycopgmodule.c (exctable): add new QueryCanceledError and TransactionRollbackError exceptions.
This commit is contained in:
parent
46bf23caf4
commit
f64cbeda46
35
ChangeLog
35
ChangeLog
|
@ -1,3 +1,38 @@
|
|||
2008-01-16 James Henstridge <james@jamesh.id.au>
|
||||
|
||||
* tests/test_transaction.py (DeadlockSerializationTestCase): port
|
||||
over some tests for serialisation and deadlock errors,
|
||||
demonstrating that TransactionRollbackError is generated.
|
||||
(QueryCancelationTests): add a test to show that
|
||||
QueryCanceledError is raised on statement timeouts.
|
||||
|
||||
* psycopg2da/adapter.py (_handle_psycopg_exception): rather than
|
||||
checking exception messages, check for TransactionRollbackError.
|
||||
|
||||
* psycopg/pqpath.c (exception_from_sqlstate): return
|
||||
TransactionRollbackError for 40xxx errors, and QueryCanceledError
|
||||
for 57014 errors.
|
||||
(pq_raise): If we are using an old server, use
|
||||
TransactionRollbackError if the error message contains "could not
|
||||
serialize" or "deadlock detected".
|
||||
|
||||
* psycopg/psycopgmodule.c (_psyco_connect_fill_exc): remove
|
||||
function, since we no longer need to store pointers to the
|
||||
exceptions in the connection. This also fixes a reference leak.
|
||||
(psyco_connect): remove _psyco_connect_fill_exc() function call.
|
||||
|
||||
* psycopg/connection.h (connectionObject): remove exception
|
||||
members from struct.
|
||||
|
||||
* psycopg/connection_type.c (connectionObject_getsets): modify the
|
||||
exception attributes on the connection object from members to
|
||||
getsets. This reduces the size of the struct.
|
||||
|
||||
* lib/extensions.py: import the two new extensions.
|
||||
|
||||
* psycopg/psycopgmodule.c (exctable): add new QueryCanceledError
|
||||
and TransactionRollbackError exceptions.
|
||||
|
||||
2008-01-16 James Henstridge <james@jamesh.id.au>
|
||||
|
||||
* tests/__init__.py (test_suite): add date tests to test suite.
|
||||
|
|
|
@ -42,6 +42,8 @@ from _psycopg import adapt, adapters, encodings, connection, cursor
|
|||
from _psycopg import string_types, binary_types, new_type, register_type
|
||||
from _psycopg import ISQLQuote
|
||||
|
||||
from _psycopg import QueryCanceledError, TransactionRollbackError
|
||||
|
||||
"""Isolation level values."""
|
||||
ISOLATION_LEVEL_AUTOCOMMIT = 0
|
||||
ISOLATION_LEVEL_READ_COMMITTED = 1
|
||||
|
|
|
@ -67,18 +67,6 @@ typedef struct {
|
|||
/* notifies */
|
||||
PyObject *notifies;
|
||||
|
||||
/* errors (DBAPI-2.0 extension) */
|
||||
PyObject *exc_Error;
|
||||
PyObject *exc_Warning;
|
||||
PyObject *exc_InterfaceError;
|
||||
PyObject *exc_DatabaseError;
|
||||
PyObject *exc_InternalError;
|
||||
PyObject *exc_OperationalError;
|
||||
PyObject *exc_ProgrammingError;
|
||||
PyObject *exc_IntegrityError;
|
||||
PyObject *exc_DataError;
|
||||
PyObject *exc_NotSupportedError;
|
||||
|
||||
/* per-connection typecasters */
|
||||
PyObject *string_types; /* a set of typecasters for string types */
|
||||
PyObject *binary_types; /* a set of typecasters for binary types */
|
||||
|
|
|
@ -231,6 +231,14 @@ psyco_conn_get_transaction_status(connectionObject *self, PyObject *args)
|
|||
|
||||
#endif
|
||||
|
||||
static PyObject *
|
||||
psyco_conn_get_exception(PyObject *self, void *closure)
|
||||
{
|
||||
PyObject *exception = *(PyObject **)closure;
|
||||
|
||||
Py_INCREF(exception);
|
||||
return exception;
|
||||
}
|
||||
|
||||
/** the connection object **/
|
||||
|
||||
|
@ -260,32 +268,6 @@ static struct PyMethodDef connectionObject_methods[] = {
|
|||
/* object member list */
|
||||
|
||||
static struct PyMemberDef connectionObject_members[] = {
|
||||
/* DBAPI-2.0 extensions (exception objects) */
|
||||
{"Error", T_OBJECT,
|
||||
offsetof(connectionObject, exc_Error), RO, Error_doc},
|
||||
{"Warning",
|
||||
T_OBJECT, offsetof(connectionObject, exc_Warning), RO, Warning_doc},
|
||||
{"InterfaceError", T_OBJECT,
|
||||
offsetof(connectionObject, exc_InterfaceError), RO,
|
||||
InterfaceError_doc},
|
||||
{"DatabaseError", T_OBJECT,
|
||||
offsetof(connectionObject, exc_DatabaseError), RO, DatabaseError_doc},
|
||||
{"InternalError", T_OBJECT,
|
||||
offsetof(connectionObject, exc_InternalError), RO, InternalError_doc},
|
||||
{"OperationalError", T_OBJECT,
|
||||
offsetof(connectionObject, exc_OperationalError), RO,
|
||||
OperationalError_doc},
|
||||
{"ProgrammingError", T_OBJECT,
|
||||
offsetof(connectionObject, exc_ProgrammingError), RO,
|
||||
ProgrammingError_doc},
|
||||
{"IntegrityError", T_OBJECT,
|
||||
offsetof(connectionObject, exc_IntegrityError), RO,
|
||||
IntegrityError_doc},
|
||||
{"DataError", T_OBJECT,
|
||||
offsetof(connectionObject, exc_DataError), RO, DataError_doc},
|
||||
{"NotSupportedError", T_OBJECT,
|
||||
offsetof(connectionObject, exc_NotSupportedError), RO,
|
||||
NotSupportedError_doc},
|
||||
#ifdef PSYCOPG_EXTENSIONS
|
||||
{"closed", T_LONG, offsetof(connectionObject, closed), RO,
|
||||
"True if the connection is closed."},
|
||||
|
@ -309,6 +291,25 @@ static struct PyMemberDef connectionObject_members[] = {
|
|||
{NULL}
|
||||
};
|
||||
|
||||
#define EXCEPTION_GETTER(exc) \
|
||||
{ #exc, psyco_conn_get_exception, NULL, exc ## _doc, &exc }
|
||||
|
||||
static struct PyGetSetDef connectionObject_getsets[] = {
|
||||
/* DBAPI-2.0 extensions (exception objects) */
|
||||
EXCEPTION_GETTER(Error),
|
||||
EXCEPTION_GETTER(Warning),
|
||||
EXCEPTION_GETTER(InterfaceError),
|
||||
EXCEPTION_GETTER(DatabaseError),
|
||||
EXCEPTION_GETTER(InternalError),
|
||||
EXCEPTION_GETTER(OperationalError),
|
||||
EXCEPTION_GETTER(ProgrammingError),
|
||||
EXCEPTION_GETTER(IntegrityError),
|
||||
EXCEPTION_GETTER(DataError),
|
||||
EXCEPTION_GETTER(NotSupportedError),
|
||||
{NULL}
|
||||
};
|
||||
#undef EXCEPTION_GETTER
|
||||
|
||||
/* initialization and finalization methods */
|
||||
|
||||
static int
|
||||
|
@ -465,7 +466,7 @@ PyTypeObject connectionType = {
|
|||
|
||||
connectionObject_methods, /*tp_methods*/
|
||||
connectionObject_members, /*tp_members*/
|
||||
0, /*tp_getset*/
|
||||
connectionObject_getsets, /*tp_getset*/
|
||||
0, /*tp_base*/
|
||||
0, /*tp_dict*/
|
||||
|
||||
|
|
|
@ -107,9 +107,13 @@ exception_from_sqlstate(const char *sqlstate)
|
|||
case '4':
|
||||
switch (sqlstate[1]) {
|
||||
case '0': /* Class 40 - Transaction Rollback */
|
||||
#ifdef PSYCOPG_EXTENSIONS
|
||||
return TransactionRollbackError;
|
||||
#else
|
||||
return OperationalError;
|
||||
#endif
|
||||
case '2': /* Class 42 - Syntax Error or Access Rule Violation */
|
||||
case '4': /* Class 44 — WITH CHECK OPTION Violation */
|
||||
case '4': /* Class 44 - WITH CHECK OPTION Violation */
|
||||
return ProgrammingError;
|
||||
}
|
||||
break;
|
||||
|
@ -119,6 +123,11 @@ exception_from_sqlstate(const char *sqlstate)
|
|||
Class 55 - Object Not In Prerequisite State
|
||||
Class 57 - Operator Intervention
|
||||
Class 58 - System Error (errors external to PostgreSQL itself) */
|
||||
#ifdef PSYCOPG_EXTENSIONS
|
||||
if (!strcmp(sqlstate, "57014"))
|
||||
return QueryCanceledError;
|
||||
else
|
||||
#endif
|
||||
return OperationalError;
|
||||
case 'F': /* Class F0 - Configuration File Error */
|
||||
return InternalError;
|
||||
|
@ -188,6 +197,9 @@ pq_raise(connectionObject *conn, cursorObject *curs, PGresult *pgres,
|
|||
|| !strncmp(err, "ERROR: ExecAppend: Fail to add null", 36)
|
||||
|| strstr(err, "referential integrity violation"))
|
||||
exc = IntegrityError;
|
||||
else if (strstr(err, "could not serialize") ||
|
||||
strstr(err, "deadlock detected"))
|
||||
exc = TransactionRollbackError;
|
||||
else
|
||||
exc = ProgrammingError;
|
||||
}
|
||||
|
|
|
@ -85,6 +85,9 @@ extern psyco_errors_set_RETURN psyco_errors_set psyco_errors_set_PROTO;
|
|||
extern PyObject *Error, *Warning, *InterfaceError, *DatabaseError,
|
||||
*InternalError, *OperationalError, *ProgrammingError,
|
||||
*IntegrityError, *DataError, *NotSupportedError;
|
||||
#ifdef PSYCOPG_EXTENSIONS
|
||||
extern PyObject *QueryCanceledError, *TransactionRollbackError;
|
||||
#endif
|
||||
|
||||
/* python versions and compatibility stuff */
|
||||
#ifndef PyMODINIT_FUNC
|
||||
|
@ -167,6 +170,14 @@ extern void psyco_set_error(PyObject *exc, PyObject *curs, const char *msg,
|
|||
#define NotSupportedError_doc \
|
||||
"A not supported datbase API was called."
|
||||
|
||||
#ifdef PSYCOPG_EXTENSIONS
|
||||
#define QueryCanceledError_doc \
|
||||
"Error related to SQL query cancelation."
|
||||
|
||||
#define TransactionRollbackError_doc \
|
||||
"Error causing transaction rollback (deadlocks, serialisation failures, etc)."
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -100,32 +100,6 @@ _psyco_connect_fill_dsn(char *dsn, char *kw, char *v, size_t i)
|
|||
return i;
|
||||
}
|
||||
|
||||
static void
|
||||
_psyco_connect_fill_exc(connectionObject *conn)
|
||||
{
|
||||
/* fill the connection object with the exceptions */
|
||||
conn->exc_Error = Error;
|
||||
Py_INCREF(Error);
|
||||
conn->exc_Warning = Warning;
|
||||
Py_INCREF(Warning);
|
||||
conn->exc_InterfaceError = InterfaceError;
|
||||
Py_INCREF(InterfaceError);
|
||||
conn->exc_DatabaseError = DatabaseError;
|
||||
Py_INCREF(DatabaseError);
|
||||
conn->exc_InternalError = InternalError;
|
||||
Py_INCREF(InternalError);
|
||||
conn->exc_ProgrammingError = ProgrammingError;
|
||||
Py_INCREF(ProgrammingError);
|
||||
conn->exc_IntegrityError = IntegrityError;
|
||||
Py_INCREF(IntegrityError);
|
||||
conn->exc_DataError = DataError;
|
||||
Py_INCREF(DataError);
|
||||
conn->exc_NotSupportedError = NotSupportedError;
|
||||
Py_INCREF(NotSupportedError);
|
||||
conn->exc_OperationalError = OperationalError;
|
||||
Py_INCREF(OperationalError);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
psyco_connect(PyObject *self, PyObject *args, PyObject *keywds)
|
||||
{
|
||||
|
@ -215,7 +189,6 @@ psyco_connect(PyObject *self, PyObject *args, PyObject *keywds)
|
|||
/* allocate connection, fill with errors and return it */
|
||||
if (factory == NULL) factory = (PyObject *)&connectionType;
|
||||
conn = PyObject_CallFunction(factory, "s", dsn);
|
||||
if (conn) _psyco_connect_fill_exc((connectionObject*)conn);
|
||||
}
|
||||
|
||||
goto cleanup;
|
||||
|
@ -433,6 +406,9 @@ static void psyco_encodings_fill(PyObject *dict)
|
|||
PyObject *Error, *Warning, *InterfaceError, *DatabaseError,
|
||||
*InternalError, *OperationalError, *ProgrammingError,
|
||||
*IntegrityError, *DataError, *NotSupportedError;
|
||||
#ifdef PSYCOPG_EXTENSIONS
|
||||
PyObject *QueryCanceledError, *TransactionRollbackError;
|
||||
#endif
|
||||
|
||||
/* mapping between exception names and their PyObject */
|
||||
static struct {
|
||||
|
@ -455,6 +431,13 @@ static struct {
|
|||
{ "psycopg2.DataError", &DataError, &DatabaseError, DataError_doc },
|
||||
{ "psycopg2.NotSupportedError", &NotSupportedError, &DatabaseError,
|
||||
NotSupportedError_doc },
|
||||
#ifdef PSYCOPG_EXTENSIONS
|
||||
{ "psycopg2.extensions.QueryCanceledError", &QueryCanceledError,
|
||||
&OperationalError, OperationalError_doc },
|
||||
{ "psycopg2.extensions.TransactionRollbackError",
|
||||
&TransactionRollbackError, &OperationalError,
|
||||
TransactionRollbackError_doc },
|
||||
#endif
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
@ -507,6 +490,11 @@ psyco_errors_fill(PyObject *dict)
|
|||
PyDict_SetItemString(dict, "IntegrityError", IntegrityError);
|
||||
PyDict_SetItemString(dict, "DataError", DataError);
|
||||
PyDict_SetItemString(dict, "NotSupportedError", NotSupportedError);
|
||||
#ifdef PSYCOPG_EXTENSIONS
|
||||
PyDict_SetItemString(dict, "QueryCanceledError", QueryCanceledError);
|
||||
PyDict_SetItemString(dict, "TransactionRollbackError",
|
||||
TransactionRollbackError);
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -522,6 +510,11 @@ psyco_errors_set(PyObject *type)
|
|||
PyObject_SetAttrString(type, "IntegrityError", IntegrityError);
|
||||
PyObject_SetAttrString(type, "DataError", DataError);
|
||||
PyObject_SetAttrString(type, "NotSupportedError", NotSupportedError);
|
||||
#ifdef PSYCOPG_EXTENSIONS
|
||||
PyObject_SetAttrString(type, "QueryCanceledError", QueryCanceledError);
|
||||
PyObject_SetAttrString(type, "TransactionRollbackError",
|
||||
TransactionRollbackError);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* psyco_error_new
|
||||
|
|
|
@ -372,14 +372,7 @@ def _handle_psycopg_exception(error):
|
|||
If we have a serialization exception or a deadlock, we should retry the
|
||||
transaction by raising a Retry exception. Otherwise, we reraise.
|
||||
"""
|
||||
if not error.args:
|
||||
raise
|
||||
msg = error.args[0]
|
||||
# These messages are from PostgreSQL 8.0. They may change between
|
||||
# PostgreSQL releases - if so, the different messages should be added
|
||||
# rather than the existing ones changed so this logic works with
|
||||
# different versions.
|
||||
if 'could not serialize' in msg or 'deadlock detected' in msg:
|
||||
if isinstance(error, psycopg2.extensions.TransactionRollbackError):
|
||||
raise Retry(sys.exc_info())
|
||||
raise
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
import psycopg2
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
import psycopg2
|
||||
import psycopg2
|
||||
import tests
|
||||
|
||||
from psycopg2.extensions import (
|
||||
|
@ -69,6 +72,154 @@ class TransactionTestCase(unittest.TestCase):
|
|||
self.assertEqual(curs.fetchone()[0], 1)
|
||||
|
||||
|
||||
class DeadlockSerializationTestCase(unittest.TestCase):
|
||||
"""Test deadlock and serialization failure errors."""
|
||||
|
||||
def connect(self):
|
||||
conn = psycopg2.connect("dbname=%s" % tests.dbname)
|
||||
conn.set_isolation_level(ISOLATION_LEVEL_SERIALIZABLE)
|
||||
return conn
|
||||
|
||||
def setUp(self):
|
||||
self.conn = self.connect()
|
||||
curs = self.conn.cursor()
|
||||
# Drop table if it already exists
|
||||
try:
|
||||
curs.execute("DROP TABLE table1")
|
||||
self.conn.commit()
|
||||
except psycopg2.DatabaseError:
|
||||
self.conn.rollback()
|
||||
try:
|
||||
curs.execute("DROP TABLE table2")
|
||||
self.conn.commit()
|
||||
except psycopg2.DatabaseError:
|
||||
self.conn.rollback()
|
||||
# Create sample data
|
||||
curs.execute("""
|
||||
CREATE TABLE table1 (
|
||||
id int PRIMARY KEY,
|
||||
name text)
|
||||
""")
|
||||
curs.execute("INSERT INTO table1 VALUES (1, 'hello')")
|
||||
curs.execute("CREATE TABLE table2 (id int PRIMARY KEY)")
|
||||
self.conn.commit()
|
||||
|
||||
def tearDown(self):
|
||||
curs = self.conn.cursor()
|
||||
curs.execute("DROP TABLE table1")
|
||||
curs.execute("DROP TABLE table2")
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
||||
|
||||
def test_deadlock(self):
|
||||
self.thread1_error = self.thread2_error = None
|
||||
step1 = threading.Event()
|
||||
step2 = threading.Event()
|
||||
|
||||
def task1():
|
||||
try:
|
||||
conn = self.connect()
|
||||
curs = conn.cursor()
|
||||
curs.execute("LOCK table1 IN ACCESS EXCLUSIVE MODE")
|
||||
step1.set()
|
||||
step2.wait()
|
||||
curs.execute("LOCK table2 IN ACCESS EXCLUSIVE MODE")
|
||||
except psycopg2.DatabaseError, exc:
|
||||
self.thread1_error = exc
|
||||
step1.set()
|
||||
conn.close()
|
||||
def task2():
|
||||
try:
|
||||
conn = self.connect()
|
||||
curs = conn.cursor()
|
||||
step1.wait()
|
||||
curs.execute("LOCK table2 IN ACCESS EXCLUSIVE MODE")
|
||||
step2.set()
|
||||
curs.execute("LOCK table1 IN ACCESS EXCLUSIVE MODE")
|
||||
except psycopg2.DatabaseError, exc:
|
||||
self.thread2_error = exc
|
||||
step2.set()
|
||||
conn.close()
|
||||
|
||||
# Run the threads in parallel. The "step1" and "step2" events
|
||||
# ensure that the two transactions overlap.
|
||||
thread1 = threading.Thread(target=task1)
|
||||
thread2 = threading.Thread(target=task2)
|
||||
thread1.start()
|
||||
thread2.start()
|
||||
thread1.join()
|
||||
thread2.join()
|
||||
|
||||
# Exactly one of the threads should have failed with
|
||||
# TransactionRollbackError:
|
||||
self.assertFalse(self.thread1_error and self.thread2_error)
|
||||
error = self.thread1_error or self.thread2_error
|
||||
self.assertTrue(isinstance(
|
||||
error, psycopg2.extensions.TransactionRollbackError))
|
||||
|
||||
def test_serialisation_failure(self):
|
||||
self.thread1_error = self.thread2_error = None
|
||||
step1 = threading.Event()
|
||||
step2 = threading.Event()
|
||||
|
||||
def task1():
|
||||
try:
|
||||
conn = self.connect()
|
||||
curs = conn.cursor()
|
||||
curs.execute("SELECT name FROM table1 WHERE id = 1")
|
||||
curs.fetchall()
|
||||
step1.set()
|
||||
step2.wait()
|
||||
curs.execute("UPDATE table1 SET name='task1' WHERE id = 1")
|
||||
conn.commit()
|
||||
except psycopg2.DatabaseError, exc:
|
||||
self.thread1_error = exc
|
||||
step1.set()
|
||||
conn.close()
|
||||
def task2():
|
||||
try:
|
||||
conn = self.connect()
|
||||
curs = conn.cursor()
|
||||
step1.wait()
|
||||
curs.execute("UPDATE table1 SET name='task2' WHERE id = 1")
|
||||
conn.commit()
|
||||
except psycopg2.DatabaseError, exc:
|
||||
self.thread2_error = exc
|
||||
step2.set()
|
||||
conn.close()
|
||||
|
||||
# Run the threads in parallel. The "step1" and "step2" events
|
||||
# ensure that the two transactions overlap.
|
||||
thread1 = threading.Thread(target=task1)
|
||||
thread2 = threading.Thread(target=task2)
|
||||
thread1.start()
|
||||
thread2.start()
|
||||
thread1.join()
|
||||
thread2.join()
|
||||
|
||||
# Exactly one of the threads should have failed with
|
||||
# TransactionRollbackError:
|
||||
self.assertFalse(self.thread1_error and self.thread2_error)
|
||||
error = self.thread1_error or self.thread2_error
|
||||
self.assertTrue(isinstance(
|
||||
error, psycopg2.extensions.TransactionRollbackError))
|
||||
|
||||
|
||||
class QueryCancelationTests(unittest.TestCase):
|
||||
"""Tests for query cancelation."""
|
||||
|
||||
def setUp(self):
|
||||
self.conn = psycopg2.connect("dbname=%s" % tests.dbname)
|
||||
self.conn.set_isolation_level(ISOLATION_LEVEL_SERIALIZABLE)
|
||||
|
||||
def test_statement_timeout(self):
|
||||
curs = self.conn.cursor()
|
||||
# Set a low statement timeout, then sleep for a longer period.
|
||||
curs.execute('SET statement_timeout TO 10')
|
||||
self.assertRaises(psycopg2.extensions.QueryCanceledError,
|
||||
curs.execute, 'SELECT pg_sleep(50)')
|
||||
|
||||
|
||||
def test_suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user