Skip to content
Snippets Groups Projects
Commit aa0589b57da0 authored by Nicolas Évrard's avatar Nicolas Évrard :space_invader:
Browse files

Support Two-Phase Commit protocol in Transaction

issue3553
review23741003
parent 74f22b3b9e48
No related branches found
No related tags found
No related merge requests found
* Support Two-Phase Commit in Transaction
* Allow Report to generate text plain, XML, HTML and XHTML
* Add workflow graph on ir.model
* Add context model on ir.action.act_window
......
......@@ -82,5 +82,15 @@
Create a new transaction with the same database, user and context as the
original transaction and adds it to the stack of transactions.
.. method:: Transaction.join(datamanager)
Register in the transaction a data manager conforming to the `Two-Phase
Commit protocol`_. More information on how to implement such data manager
is available at the `Zope documentation`_.
This method returns the registered datamanager. It could be a different yet
equivalent (in term of python equality) datamanager than the one passed to the
method.
.. _`context manager`: http://docs.python.org/reference/datamodel.html#context-managers
.. _`PEP-0249`: https://www.python.org/dev/peps/pep-0249/
......@@ -85,2 +95,4 @@
.. _`context manager`: http://docs.python.org/reference/datamodel.html#context-managers
.. _`PEP-0249`: https://www.python.org/dev/peps/pep-0249/
.. _`Two-Phase Commit protocol`: https://en.wikipedia.org/wiki/Two-phase_commit_protocol
.. _`Zope documentation`: http://zodb.readthedocs.org/en/latest/transactions.html#the-two-phase-commit-protocol-in-practice
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import unittest
from mock import Mock
from trytond.tests.test_tryton import DB_NAME, USER, CONTEXT, install_module
from trytond.transaction import Transaction
......@@ -75,6 +76,51 @@
self.assertIs(Transaction(), new_transaction)
self.assertIs(Transaction(), transaction)
def test_two_phase_commit(self):
# A successful transaction
dm = Mock()
with Transaction().start(DB_NAME, USER, context=CONTEXT) \
as transaction:
transaction.join(dm)
dm.tpc_begin.assert_called_once_with(transaction)
dm.commit.assert_called_once_with(transaction)
dm.tpc_vote.assert_called_once_with(transaction)
dm.tpc_abort.not_called()
dm.tpc_finish.assert_called_once_with(transaction)
# Failing in the datamanager
dm.reset_mock()
dm.tpc_vote.side_effect = ValueError('Failing the datamanager')
try:
with Transaction().start(DB_NAME, USER, context=CONTEXT) \
as transaction:
transaction.join(dm)
except ValueError:
pass
dm.tpc_begin.assert_called_once_with(transaction)
dm.commit.assert_called_once_with(transaction)
dm.tpc_vote.assert_called_once_with(transaction)
dm.tpc_abort.assert_called_once_with(transaction)
dm.tpc_finish.assert_not_called()
# Failing in tryton
dm.reset_mock()
try:
with Transaction().start(DB_NAME, USER, context=CONTEXT) \
as transaction:
transaction.join(dm)
raise ValueError('Failing in tryton')
except ValueError:
pass
dm.tpc_begin.assert_not_called()
dm.commit.assert_not_called()
dm.tpc_vote.assert_not_called()
dm.tpc_abort.assert_called_once_with(transaction)
dm.tpc_finish.assert_not_called()
def suite():
return unittest.TestLoader().loadTestsFromTestCase(TransactionTestCase)
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import logging
from threading import local
from sql import Flavor
from trytond import backend
from trytond.config import config
......@@ -3,9 +4,11 @@
from threading import local
from sql import Flavor
from trytond import backend
from trytond.config import config
logger = logging.getLogger(__name__)
class _AttributeManager(object):
'''
......@@ -95,6 +98,7 @@
self.delete = {}
self.timestamp = {}
self.counter = 0
self._datamanagers = []
return self
def __enter__(self):
......@@ -102,24 +106,31 @@
def __exit__(self, type, value, traceback):
transactions = self._local.transactions
if transactions.count(self) == 1:
if type is None and not self.readonly:
self.commit()
try:
self.rollback()
self.database.put_connection(self.connection, self.close)
finally:
self.database = None
self.readonly = False
self.connection = None
self.close = None
self.user = None
self.context = None
self.create_records = None
self.delete_records = None
self.delete = None
self.timestamp = None
current_instance = transactions.pop()
try:
if transactions.count(self) == 1:
try:
try:
if type is None and not self.readonly:
self.commit()
else:
self.rollback()
finally:
self.database.put_connection(
self.connection, self.close)
finally:
self.database = None
self.readonly = False
self.connection = None
self.close = None
self.user = None
self.context = None
self.create_records = None
self.delete_records = None
self.delete = None
self.timestamp = None
self._datamanagers = []
finally:
current_instance = transactions.pop()
assert current_instance is self, transactions
def set_context(self, context=None, **kwargs):
......@@ -162,8 +173,27 @@
autocommit=autocommit)
def commit(self):
self.connection.commit()
try:
if self._datamanagers:
for datamanager in self._datamanagers:
datamanager.tpc_begin(self)
for datamanager in self._datamanagers:
datamanager.commit(self)
for datamanager in self._datamanagers:
datamanager.tpc_vote(self)
self.connection.commit()
except:
self.rollback()
raise
else:
try:
for datamanager in self._datamanagers:
datamanager.tpc_finish(self)
except:
logger.critical('A datamanager raised an exception in'
' tpc_finish, the data might be inconsistant',
exc_info=True)
def rollback(self):
for cache in self.cache.itervalues():
cache.clear()
......@@ -166,6 +196,8 @@
def rollback(self):
for cache in self.cache.itervalues():
cache.clear()
for datamanager in self._datamanagers:
datamanager.tpc_abort(self)
self.connection.rollback()
......@@ -170,5 +202,13 @@
self.connection.rollback()
def join(self, datamanager):
try:
idx = self._datamanagers.index(datamanager)
return self._datamanagers[idx]
except ValueError:
self._datamanagers.append(datamanager)
return datamanager
@property
def language(self):
def get_language():
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment