]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Add documentation for two-phase commit support
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sun, 28 Nov 2021 18:26:24 +0000 (19:26 +0100)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sun, 28 Nov 2021 18:26:24 +0000 (19:26 +0100)
docs/api/connections.rst
docs/basic/transactions.rst
docs/news.rst
psycopg/psycopg/_tpc.py
psycopg/psycopg/connection.py

index e5dee351011600750a64652da4312a929b22803c..143fa63c23f0b76abbe7d6ea891c1d293fffa97f 100644 (file)
@@ -265,6 +265,122 @@ The `!Connection` class
     .. automethod:: fileno
 
 
+    .. _tpc-methods:
+
+    .. rubric:: Two-Phase Commit support methods
+
+    .. versionadded:: 3.1
+
+    .. seealso:: :ref:`two-phase-commit` for an introductory explanation of
+        these methods.
+
+    .. automethod:: xid
+
+    .. automethod:: tpc_begin
+
+        :param xid: The id of the transaction
+        :type xid: Xid or str
+
+        This method should be called outside of a transaction (i.e. nothing
+        may have executed since the last `commit()` or `rollback()` and
+        `~ConnectionInfo.transaction_status` is `~pq.TransactionStatus.IDLE`).
+
+        Furthermore, it is an error to call `!commit()` or `!rollback()`
+        within the TPC transaction: in this case a `ProgrammingError`
+        is raised.
+
+        The *xid* may be either an object returned by the `xid()` method or a
+        plain string: the latter allows to create a transaction using the
+        provided string as PostgreSQL transaction id. See also
+        `tpc_recover()`.
+
+
+    .. automethod:: tpc_prepare
+
+        A `ProgrammingError` is raised if this method is used outside of a TPC
+        transaction.
+
+        After calling `!tpc_prepare()`, no statements can be executed until
+        `tpc_commit()` or `tpc_rollback()` will be
+        called.
+
+        .. seealso:: The |PREPARE TRANSACTION|_ PostgreSQL command.
+
+        .. |PREPARE TRANSACTION| replace:: :sql:`PREPARE TRANSACTION`
+        .. _PREPARE TRANSACTION: https://www.postgresql.org/docs/current/static/sql-prepare-transaction.html
+
+
+    .. automethod:: tpc_commit
+
+        :param xid: The id of the transaction
+        :type xid: Xid or str
+
+        When called with no arguments, `!tpc_commit()` commits a TPC
+        transaction previously prepared with `tpc_prepare()`.
+
+        If `!tpc_commit()` is called prior to `!tpc_prepare()`, a single phase
+        commit is performed.  A transaction manager may choose to do this if
+        only a single resource is participating in the global transaction.
+
+        When called with a transaction ID *xid*, the database commits the
+        given transaction.  If an invalid transaction ID is provided, a
+        `ProgrammingError` will be raised.  This form should be called outside
+        of a transaction, and is intended for use in recovery.
+
+        On return, the TPC transaction is ended.
+
+        .. seealso:: The |COMMIT PREPARED|_ PostgreSQL command.
+
+        .. |COMMIT PREPARED| replace:: :sql:`COMMIT PREPARED`
+        .. _COMMIT PREPARED: https://www.postgresql.org/docs/current/static/sql-commit-prepared.html
+
+
+    .. automethod:: tpc_rollback
+
+        :param xid: The id of the transaction
+        :type xid: Xid or str
+
+        When called with no arguments, `!tpc_rollback()` rolls back a TPC
+        transaction.  It may be called before or after `tpc_prepare()`.
+
+        When called with a transaction ID *xid*, it rolls back the given
+        transaction.  If an invalid transaction ID is provided, a
+        `ProgrammingError` is raised.  This form should be called outside of a
+        transaction, and is intended for use in recovery.
+
+        On return, the TPC transaction is ended.
+
+        .. seealso:: The |ROLLBACK PREPARED|_ PostgreSQL command.
+
+        .. |ROLLBACK PREPARED| replace:: :sql:`ROLLBACK PREPARED`
+        .. _ROLLBACK PREPARED: https://www.postgresql.org/docs/current/static/sql-rollback-prepared.html
+
+
+    .. automethod:: tpc_recover
+
+        Returns a list of `Xid` representing pending transactions, suitable
+        for use with `tpc_commit()` or `tpc_rollback()`.
+
+        If a transaction was not initiated by Psycopg, the returned Xids will
+        have attributes `~Xid.format_id` and `~Xid.bqual` set to `!None` and
+        the `~Xid.gtrid` set to the PostgreSQL transaction ID: such Xids are
+        still usable for recovery.  Psycopg uses the same algorithm of the
+        `PostgreSQL JDBC driver`__ to encode a XA triple in a string, so
+        transactions initiated by a program using such driver should be
+        unpacked correctly.
+
+        .. __: https://jdbc.postgresql.org/
+
+        Xids returned by `!tpc_recover()` also have extra attributes
+        `~Xid.prepared`, `~Xid.owner`, `~Xid.database` populated with the
+        values read from the server.
+
+        .. seealso:: the |pg_prepared_xacts|_ system view.
+
+        .. |pg_prepared_xacts| replace:: `pg_prepared_xacts`
+        .. _pg_prepared_xacts: https://www.postgresql.org/docs/current/static/view-pg-prepared-xacts.html
+
+
 The `!AsyncConnection` class
 ----------------------------
 
@@ -329,6 +445,11 @@ The `!AsyncConnection` class
     .. automethod:: set_read_only
     .. automethod:: set_deferrable
 
+    .. automethod:: tpc_prepare
+    .. automethod:: tpc_commit
+    .. automethod:: tpc_rollback
+    .. automethod:: tpc_recover
+
 
 Connection support objects
 --------------------------
@@ -474,3 +595,42 @@ Connection support objects
       the `Transaction` *tx* (returned by a statement such as :samp:`with
       conn.transaction() as {tx}:` and all the blocks nested within. The
       program will continue after the *tx* block.
+
+
+.. autoclass:: Xid()
+
+    See :ref:`two-phase-commit` for details.
+
+    .. autoattribute:: format_id
+
+        Format Identifier of the two-phase transaction.
+
+    .. autoattribute:: gtrid
+
+        Global Transaction Identifier of the two-phase transaction.
+
+        If the Xid doesn't follow the XA standard, it will be the PostgreSQL
+        ID of the transaction (in which case `format_id` and `bqual` will be
+        `!None`).
+
+    .. autoattribute:: bqual
+
+        Branch Qualifier of the two-phase transaction.
+
+    .. autoattribute:: prepared
+
+        Timestamp at which the transaction was prepared for commit.
+
+        Only available on transactions recovered by `~Connection.tpc_recover()`.
+
+    .. autoattribute:: owner
+
+        Named of the user that executed the transaction.
+
+        Only available on recovered transactions.
+
+    .. autoattribute:: database
+
+        Named of the database in which the transaction was executed.
+
+        Only available on recovered transactions.
index 1db04d5ddc133e145949e3b674b2e847ffa20dfc..0cf8dfd54995462a9f45e345facc5c11dfc62064 100644 (file)
@@ -335,3 +335,54 @@ connection.
 
    .. __: https://www.postgresql.org/docs/current/transaction-iso.html
           #XACT-REPEATABLE-READ
+
+
+.. index::
+    pair: Two-phase commit; Transaction
+
+.. _two-phase-commit:
+
+Two-Phase Commit protocol support
+---------------------------------
+
+.. versionadded:: 3.1
+
+Psycopg exposes the two-phase commit features available in PostgreSQL
+implementing the `two-phase commit extensions`__ proposed by the DBAPI.
+
+The DBAPI model of two-phase commit is inspired by the `XA specification`__,
+according to which transaction IDs are formed from three components:
+
+- a format ID (non-negative 32 bit integer)
+- a global transaction ID (string not longer than 64 bytes)
+- a branch qualifier (string not longer than 64 bytes)
+
+For a particular global transaction, the first two components will be the same
+for all the resources. Every resource will be assigned a different branch
+qualifier.
+
+According to the DBAPI specification, a transaction ID is created using the
+`Connection.xid()` method. Once you have a transaction id, a distributed
+transaction can be started with `Connection.tpc_begin()`, prepared using
+`~Connection.tpc_prepare()` and completed using `~Connection.tpc_commit()` or
+`~Connection.tpc_rollback()`.  Transaction IDs can also be retrieved from the
+database using `~Connection.tpc_recover()` and completed using the above
+`!tpc_commit()` and `!tpc_rollback()`.
+
+PostgreSQL doesn't follow the XA standard though, and the ID for a PostgreSQL
+prepared transaction can be any string up to 200 characters long. Psycopg's
+`Xid` objects can represent both XA-style transactions IDs (such as the ones
+created by the `!xid()` method) and PostgreSQL transaction IDs identified by
+an unparsed string.
+
+The format in which the Xids are converted into strings passed to the
+database is the same employed by the `PostgreSQL JDBC driver`__: this should
+allow interoperation between tools written in Python and in Java. For example
+a recovery tool written in Python would be able to recognize the components of
+transactions produced by a Java program.
+
+For further details see the documentation for the :ref:`tpc-methods`.
+
+.. __: https://www.python.org/dev/peps/pep-0249/#optional-two-phase-commit-extensions
+.. __: https://publications.opengroup.org/c193
+.. __: https://jdbc.postgresql.org/
index cb117f0a3efa7eb44cdabd3a3c7839eed873c3d0..39afb48581965e367c3fada3b905824da67bdd9b 100644 (file)
@@ -7,6 +7,12 @@
 ``psycopg`` release notes
 =========================
 
+Psycopg 3.1 (unreleased)
+------------------------
+
+- Add :ref:`Two-Phase Commit <two-phase-commit>` support (:ticket:`#72`).
+
+
 Current release
 ---------------
 
index 1c778c1f61e5b0fb4dca4aec05668e0b88448ec2..43e3df62c710f3308062d47a355093ede9c88ae3 100644 (file)
@@ -15,7 +15,12 @@ _re_xid = re.compile(r"^(\d+)_([^_]*)_([^_]*)$")
 
 @dataclass(frozen=True)
 class Xid:
-    """A two-phase commit transaction identifier."""
+    """A two-phase commit transaction identifier.
+
+    The object can also be unpacked as a 3-item tuple (`format_id`, `gtrid`,
+    `bqual`).
+
+    """
 
     format_id: Optional[int]
     gtrid: str
index 8a7ce44503231212b8ce64430312eaef5525fe4b..39bfb17856bbe91a33f750337e1c73054a41eb38 100644 (file)
@@ -514,6 +514,15 @@ class BaseConnection(Generic[Row]):
             yield from self._exec_command(cmd)
 
     def xid(self, format_id: int, gtrid: str, bqual: str) -> Xid:
+        """
+        Returns a `Xid` to pass to the `!tpc_*()` methods of this connection.
+
+        The argument types and constraints are explained in
+        :ref:`two-phase-commit`.
+
+        The values passed to the method will be available on the returned
+        object as the members `~Xid.format_id`, `~Xid.gtrid`, `~Xid.bqual`.
+        """
         return Xid.from_parts(format_id, gtrid, bqual)
 
     def _tpc_begin_gen(self, xid: Union[Xid, str]) -> PQGen[None]:
@@ -871,10 +880,16 @@ class Connection(BaseConnection[Row]):
             super()._set_deferrable(value)
 
     def tpc_begin(self, xid: Union[Xid, str]) -> None:
+        """
+        Begin a TPC transaction with the given transaction ID *xid*.
+        """
         with self.lock:
             self.wait(self._tpc_begin_gen(xid))
 
     def tpc_prepare(self) -> None:
+        """
+        Perform the first phase of a transaction started with `tpc_begin()`.
+        """
         try:
             with self.lock:
                 self.wait(self._tpc_prepare_gen())
@@ -882,10 +897,16 @@ class Connection(BaseConnection[Row]):
             raise e.NotSupportedError(str(ex)) from None
 
     def tpc_commit(self, xid: Union[Xid, str, None] = None) -> None:
+        """
+        Commit a prepared two-phase transaction.
+        """
         with self.lock:
             self.wait(self._tpc_finish_gen("commit", xid))
 
     def tpc_rollback(self, xid: Union[Xid, str, None] = None) -> None:
+        """
+        Roll back a prepared two-phase transaction.
+        """
         with self.lock:
             self.wait(self._tpc_finish_gen("rollback", xid))