PATCH: two phase commit

"James Henstridge" <[email protected]>
Newsgroups gmane.comp.python.db.psycopg.devel
Message-ID <[email protected]>
Attached is a patch adding a basic two phase commit API to psycopg.

It adds the following methods to the connection object:

    prepare_transaction(xid)
    commit_prepared(xid)
    rollback_prepared(xid)

These are the basic operations needed to plug a psycopg2 connection
into a transaction manager so it would be possible to properly
participate in Zope two phase commit, as an example.

I've included basic tests of the new methods.

James.

_______________________________________________
Psycopg mailing list
Psycopg-IAPFreCvJWPBWskQ1e/[email protected]
http://lists.initd.org/mailman/listinfo/psycopg
two-phase-commit.patch (text/x-patch, 13.4 KB)
Index: psycopg/connection_type.c
===================================================================
--- psycopg/connection_type.c	(revision 926)
+++ psycopg/connection_type.c	(working copy)
@@ -33,6 +33,7 @@
 #include "psycopg/psycopg.h"
 #include "psycopg/connection.h"
 #include "psycopg/cursor.h"
+#include "psycopg/pqpath.h"
 
 /** DBAPI methods **/
 
@@ -147,6 +148,67 @@
 
 #ifdef PSYCOPG_EXTENSIONS
 
+#define psyco_conn_prepare_transaction_doc \
+"prepare_transaction(xid) -- Prepare a transaction for 2-phase commit."
+
+static PyObject *
+psyco_conn_prepare_transaction(connectionObject *self, PyObject *args)
+{
+    const char *xid;
+
+    EXC_IF_CONN_CLOSED(self);
+
+    if (!PyArg_ParseTuple(args, "s", &xid))
+        return NULL;
+
+    self->mark++;
+    if (pq_prepare_transaction(self, xid) < 0)
+        return NULL;
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
+#define psyco_conn_commit_prepared_doc \
+"commit_prepared(xid) -- Commit a previously prepared transaction."
+
+static PyObject *
+psyco_conn_commit_prepared(connectionObject *self, PyObject *args)
+{
+    const char *xid;
+
+    EXC_IF_CONN_CLOSED(self);
+
+    if (!PyArg_ParseTuple(args, "s", &xid))
+        return NULL;
+
+    if (pq_commit_prepared(self, xid) < 0)
+        return NULL;
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
+#define psyco_conn_rollback_prepared_doc \
+"rollback_prepared(xid) -- Roll back a previously prepared transaction."
+
+static PyObject *
+psyco_conn_rollback_prepared(connectionObject *self, PyObject *args)
+{
+    const char *xid;
+
+    EXC_IF_CONN_CLOSED(self);
+
+    if (!PyArg_ParseTuple(args, "s", &xid))
+        return NULL;
+
+    if (pq_rollback_prepared(self, xid) < 0)
+        return NULL;
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
 /* set_isolation_level method - switch connection isolation level */
 
 #define psyco_conn_set_isolation_level_doc \
@@ -168,8 +230,6 @@
     }
 
     if (conn_switch_isolation_level(self, level) < 0) {
-        PyErr_SetString(OperationalError,
-                        PQerrorMessage(self->pgconn));
         return NULL;
     }
 
@@ -255,6 +315,12 @@
     {"rollback", (PyCFunction)psyco_conn_rollback,
      METH_VARARGS, psyco_conn_rollback_doc},
 #ifdef PSYCOPG_EXTENSIONS
+    {"prepare_transaction", (PyCFunction)psyco_conn_prepare_transaction,
+     METH_VARARGS, psyco_conn_prepare_transaction_doc},
+    {"commit_prepared", (PyCFunction)psyco_conn_commit_prepared,
+     METH_VARARGS, psyco_conn_commit_prepared_doc},
+    {"rollback_prepared", (PyCFunction)psyco_conn_rollback_prepared,
+     METH_VARARGS, psyco_conn_rollback_prepared_doc},
     {"set_isolation_level", (PyCFunction)psyco_conn_set_isolation_level,
      METH_VARARGS, psyco_conn_set_isolation_level_doc},
     {"set_client_encoding", (PyCFunction)psyco_conn_set_client_encoding,
Index: psycopg/pqpath.c
===================================================================
--- psycopg/pqpath.c	(revision 926)
+++ psycopg/pqpath.c	(working copy)
@@ -506,6 +506,132 @@
     return retvalue;
 }
 
+/* run_prepared_txn_command - a prepared transaction helper. */
+static int
+run_prepared_txn_command(connectionObject *conn, const char *command,
+                         const char *xid)
+{
+    int command_len, xid_len, buffer_len, err = 0, retvalue = 0;
+    PGresult *pgres = NULL;
+    char *error = NULL, *buffer = NULL, *tmp;
+
+    Dprintf("run_prepared_txn_command: pgconn = %p, command = %s, xid = %s",
+            conn->pgconn, command, xid);
+    command_len = strlen(command);
+    xid_len = strlen(xid);
+
+    /* PQescapeStringConn() requires that we allocate at least twice
+       the length of the escaped string plus one. */
+    buffer_len = command_len + xid_len * 2 + 5;
+    buffer = PyMem_Malloc(buffer_len);
+    if (!buffer) {
+        PyErr_NoMemory();
+        retvalue = -1;
+        goto end;
+    }
+
+    tmp = buffer;
+    strcpy(tmp, command);
+    tmp += command_len;
+    strcpy(tmp, " '");
+    tmp += 2;
+
+#if PG_MAJOR_VERSION > 8 || \
+ (PG_MAJOR_VERSION == 8 && PG_MINOR_VERSION > 1) || \
+ (PG_MAJOR_VERSION == 8 && PG_MINOR_VERSION == 1 && PG_PATCH_VERSION >= 4)
+    tmp += PQescapeStringConn(conn->pgconn, tmp, xid, xid_len, &err);
+#else
+    tmp += PQescapeString(tmp, xid, xid_len);
+#endif
+
+    if (err != 0) {
+        pq_raise(conn, NULL, NULL, NULL, NULL);
+        retvalue = -1;
+        goto end;
+    }
+    strcpy(tmp, "'");
+
+    Dprintf("run_prepared_txn_command: query is \"%s\"", buffer);
+
+    Py_BEGIN_ALLOW_THREADS;
+    pthread_mutex_lock(&conn->lock);
+
+    pq_clear_async(conn);
+    retvalue = pq_execute_command_locked(conn, buffer, &pgres, &error);
+    /* PREPARE TRANSACTION closes off the current transaction whether
+     * it passes or fails.  COMMIT PREPARED and ROLLBACK PREPARED are
+     * both run outside of a transaction.
+     */
+    conn->status = CONN_STATUS_READY;
+
+    pthread_mutex_unlock(&conn->lock);
+    Py_END_ALLOW_THREADS;
+
+    if (retvalue < 0)
+        pq_complete_error(conn, &pgres, &error);
+
+ end:
+    if (buffer)
+        PyMem_Free(buffer);
+    return retvalue;
+}
+
+/* pq_prepare_transaction - prepare a two-phase transaction */
+int
+pq_prepare_transaction(connectionObject *conn, const char *xid)
+{
+    Dprintf("pq_prepare_transaction: pgconn = %p, isolevel = %ld, "
+            "status = %d, xid = %s",
+            conn->pgconn, conn->isolation_level, conn->status, xid);
+
+    if (conn->isolation_level == 0 || conn->status != CONN_STATUS_BEGIN) {
+        Dprintf("pq_prepare_transaction: no transaction to prepare");
+        psyco_set_error(ProgrammingError, NULL, "No transaction to prepare",
+                        NULL, NULL);
+        return -1;
+    }
+
+    return run_prepared_txn_command(conn, "PREPARE TRANSACTION", xid);
+}
+
+/* pq_commit_prepared - commit a two-phase transaction */
+int
+pq_commit_prepared(connectionObject *conn, const char *xid)
+{
+    Dprintf("pq_commit_prepared: pgconn = %p, isolevel = %ld, "
+            "status = %d, xid = %s",
+            conn->pgconn, conn->isolation_level, conn->status, xid);
+
+    if (conn->isolation_level != 0 && conn->status != CONN_STATUS_READY) {
+        Dprintf("pq_commit_prepared: inside a transaction");
+        psyco_set_error(ProgrammingError, NULL,
+                        "commit_prepared cannot run inside a transaction",
+                        NULL, NULL);
+        return -1;
+    }
+
+    return run_prepared_txn_command(conn, "COMMIT PREPARED", xid);
+}
+
+/* pq_rollback_prepared - roll back a two-phase transaction */
+int
+pq_rollback_prepared(connectionObject *conn, const char *xid)
+{
+    Dprintf("pq_rollback_prepared: pgconn = %p, isolevel = %ld, "
+            "status = %d, xid = %s",
+            conn->pgconn, conn->isolation_level, conn->status, xid);
+
+    if (conn->isolation_level != 0 && conn->status != CONN_STATUS_READY) {
+        Dprintf("pq_rollback_prepared: inside a transaction");
+        psyco_set_error(ProgrammingError, NULL,
+                        "rollback_prepared cannot run inside a transaction",
+                        NULL, NULL);
+        return -1;
+    }
+
+    return run_prepared_txn_command(conn, "ROLLBACK PREPARED", xid);
+}
+
 /* pq_is_busy - consume input and return connection status
 
    a status of 1 means that a call to pq_fetch will block, while a status of 0
Index: psycopg/pqpath.h
===================================================================
--- psycopg/pqpath.h	(revision 926)
+++ psycopg/pqpath.h	(working copy)
@@ -38,6 +38,10 @@
 extern int pq_abort(connectionObject *conn);
 extern int pq_is_busy(connectionObject *conn);
 
+extern int pq_prepare_transaction(connectionObject *conn, const char *xid);
+extern int pq_commit_prepared(connectionObject *conn, const char *xid);
+extern int pq_rollback_prepared(connectionObject *conn, const char *xid);
+
 extern void pq_set_critical(connectionObject *conn, const char *msg);
 extern PyObject *pq_resolve_critical(connectionObject *conn, int close);
 
Index: ChangeLog
===================================================================
--- ChangeLog	(revision 926)
+++ ChangeLog	(working copy)
@@ -1,5 +1,20 @@
 2008-01-17  James Henstridge  <[email protected]>
 
+	* tests/test_transaction.py (TwoPhaseCommitTests): add some tests
+	for two phase commit.
+
+	* psycopg/connection_type.c: add prepare_transaction(),
+	commit_prepared() and rollback_prepared() methods.
+
+	* psycopg/pqpath.c (run_prepared_txn_command): a helper function
+	for prepared transaction commands.
+	(pq_prepare_transaction): new function for preparing a
+	transaction.
+	(pq_commit_prepared): new function for committing a prepared
+	transaction.
+	(pq_rollback_prepared): new function for rolling back a prepared
+	transaction.
+
 	* ZPsycopgDA/DA.py (Connection.__init__): Default the encoding to
 	UTF-8, fixing bug #190.
 	(App.ImageFile): simplify ImageFile import using patch from
Index: tests/test_transaction.py
===================================================================
--- tests/test_transaction.py	(revision 926)
+++ tests/test_transaction.py	(working copy)
@@ -72,6 +72,119 @@
         self.assertEqual(curs.fetchone()[0], 1)
 
 
+class TwoPhaseCommitTests(unittest.TestCase):
+    """Tests of two phase commit functionality."""
+
+    def setUp(self):
+        self.conn = psycopg2.connect("dbname=%s" % tests.dbname)
+        self.conn.set_isolation_level(ISOLATION_LEVEL_SERIALIZABLE)
+
+        # Create table for test:
+        curs = self.conn.cursor()
+        try:
+            curs.execute("DROP TABLE table1")
+            self.conn.commit()
+        except psycopg2.DatabaseError:
+            self.conn.rollback()
+        curs.execute('''
+            CREATE TABLE table1 (
+              id int PRIMARY KEY
+            )''')
+
+        # pick two unused transaction IDs for the tests:
+        existing_xids = self.get_xids()
+        self.test_xid = None
+        i = 0
+        while self.test_xid is None:
+            xid = 'test_xid_%d' % i
+            if xid not in existing_xids:
+                self.test_xid = xid
+            i += 1
+        self.conn.commit()
+
+    def tearDown(self):
+        self.conn.rollback()
+        try:
+            self.conn.rollback_prepared(self.test_xid)
+        except psycopg2.DatabaseError:
+            pass
+        curs = self.conn.cursor()
+        curs.execute('DROP TABLE table1')
+        self.conn.commit()
+        self.conn.close()
+
+    def get_xids(self):
+        curs = self.conn.cursor()
+        curs.execute('SELECT gid FROM pg_prepared_xacts')
+        return [row[0] for row in curs.fetchall()]
+
+    def test_commit_prepared(self):
+        # Prepare a transaction:
+        curs = self.conn.cursor()
+        curs.execute('INSERT INTO table1 VALUES (42)')
+        self.assertEqual(self.conn.status, STATUS_BEGIN)
+        self.conn.prepare_transaction(self.test_xid)
+        self.assertEqual(self.conn.status, STATUS_READY)
+
+        # The changes from the transaction are not visible:
+        curs.execute('SELECT COUNT(*) FROM table1 WHERE id = 42')
+        self.assertEqual(curs.fetchone()[0], 0)
+        self.conn.rollback()
+
+        # After committing the prepared transaction, the row is visible:
+        self.conn.commit_prepared(self.test_xid)
+        self.assertEqual(self.conn.status, STATUS_READY)
+        curs.execute('SELECT COUNT(*) FROM table1 WHERE id = 42')
+        self.assertEqual(curs.fetchone()[0], 1)
+
+    def test_rollback_prepared(self):
+        # Prepare a transaction:
+        curs = self.conn.cursor()
+        curs.execute('INSERT INTO table1 VALUES (42)')
+        self.assertEqual(self.conn.status, STATUS_BEGIN)
+        self.conn.prepare_transaction(self.test_xid)
+        self.assertEqual(self.conn.status, STATUS_READY)
+
+        # The changes from the transaction are not visible:
+        curs.execute('SELECT COUNT(*) FROM table1 WHERE id = 42')
+        self.assertEqual(curs.fetchone()[0], 0)
+        self.conn.rollback()
+
+        # Now roll back the transaction.  The changes are lost.
+        self.conn.rollback_prepared(self.test_xid)
+        self.assertEqual(self.conn.status, STATUS_READY)
+        curs.execute('SELECT COUNT(*) FROM table1 WHERE id = 42')
+        self.assertEqual(curs.fetchone()[0], 0)
+
+    def test_commit_unknown(self):
+        self.assertRaises(psycopg2.ProgrammingError,
+                          self.conn.commit_prepared, self.test_xid)
+
+    def test_rollback_unknown(self):
+        self.assertRaises(psycopg2.ProgrammingError,
+                          self.conn.rollback_prepared, self.test_xid)
+
+    def test_duplicate_transaction_id(self):
+        curs = self.conn.cursor()
+        curs.execute('INSERT INTO table1 VALUES (42)')
+        self.assertEqual(self.conn.status, STATUS_BEGIN)
+        self.conn.prepare_transaction(self.test_xid)
+        self.assertEqual(self.conn.status, STATUS_READY)
+
+        # Now try to reuse the transaction ID, which results in an
+        # error:
+        curs.execute('INSERT INTO table1 VALUES (43)')
+        self.assertEqual(self.conn.status, STATUS_BEGIN)
+        self.assertRaises(psycopg2.ProgrammingError,
+                          self.conn.prepare_transaction, self.test_xid)
+
+        # The changes from the failed prepared transaction have been
+        # discarded:
+        self.assertEqual(self.conn.status, STATUS_READY)
+        curs.execute('SELECT COUNT(*) FROM table1 WHERE id = 43')
+        self.assertEqual(curs.fetchone()[0], 0)
+
+
 class DeadlockSerializationTestCase(unittest.TestCase):
     """Test deadlock and serialization failure errors."""
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.