From 22a922ecad46f67e61f1c402cd63e8acb1a1a4d8 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Sun, 14 Oct 2018 17:48:24 +0000 Subject: [PATCH] Changed the implementation of ThreadTransactionManager Changed the implementation of `ThreadTransactionManager` to be a `thread.local` that wraps a `TransactionManager` rather than a `thread.local` that inherits from `TransactionManager`. It now exposes a `manager` attribute that allows access to the wrapped transaction manager to allow cross thread calls. ZODB will use this to call `unregisterSynch` when a connection is closed from a main thread. --- docs/index.rst | 14 +++++- transaction/_manager.py | 78 ++++++++++++++++++++++++++++-- transaction/tests/test__manager.py | 30 ++++++++++++ 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index ce85b7e..6063840 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,8 +69,9 @@ that it creates the transactions and keeps track of the current one. Whenever an application wants to use the transaction machinery, it gets the current transaction from the transaction manager before starting any operations -The default transaction manager for the transaction package is thread aware. -Each thread is associated with a unique transaction. +The default transaction manager, `transaction.manager`, is thread +local. You use it as a global variable, but every thread has it's own +copy. [#wrapped]_ Application developers will most likely never need to create their own transaction managers. @@ -141,3 +142,12 @@ Additional Documentation integrations api developer + + +.. [#wrapped] The thread-local transaction manager, + `transaction.manager` wraps a regular transaction manager. You can + get the wrapped transaction manager using the `manager` attribute. + Implementers of data managers can use this **advanced** feature to + allow graceful shutdown from a central/main thread, by having their + `close` methods call `unregisterSynch` on the wrapped transaction + manager they obtained when created or opened. diff --git a/transaction/_manager.py b/transaction/_manager.py index c4b484f..80d3bca 100644 --- a/transaction/_manager.py +++ b/transaction/_manager.py @@ -215,12 +215,84 @@ def run(self, func=None, tries=3): return result -class ThreadTransactionManager(TransactionManager, threading.local): - """Thread-aware transaction manager. +_local = threading.local() +_lock = threading.Lock() +def _thread_local_manager(): + with _lock: + try: + return _local.manager + except AttributeError: + _local.manager = TransactionManager() + return _local.manager - Each thread is associated with a unique transaction. + +@implementer(ITransactionManager) +class ThreadTransactionManager(threading.local): + """ + Thread-local transaction manager. + + A thread-local transaction manager can be used as a global + variable, but has a separate copy for each thread. + + Advanced applications can use the `manager` attribute to get a + wrapped TransactionManager to allow cross-thread calls for + graceful shutdown of data managers. """ + def __init__(self): + self.manager = TransactionManager() + + @property + def explicit(self): + return self.manager.explicit + + @explicit.setter + def explicit(self, v): + self.manager.explicit = v + + def begin(self): + return self.manager.begin() + + def get(self): + return self.manager.get() + + __enter__ = get + + def commit(self): + return self.manager.commit() + + def abort(self): + return self.manager.abort() + + def __exit__(self, t, v, tb): + return self.manager.__exit__(t, v, tb) + + def doom(self): + return self.manager.doom() + + def isDoomed(self): + return self.manager.isDoomed() + + def savepoint(self, optimistic=False): + return self.manager.savepoint(optimistic) + + def registerSynch(self, synch): + return self.manager.registerSynch(synch) + + def unregisterSynch(self, synch): + return self.manager.unregisterSynch(synch) + + def clearSynchs(self): + return self.manager.clearSynchs() + + def registeredSynchs(self): + return self.manager.registeredSynchs() + + def attempts(self, number=3): + return self.manager.attempts(number) + + def run(self, func=None, tries=3): + return self.manager.run(func, tries) class Attempt(object): diff --git a/transaction/tests/test__manager.py b/transaction/tests/test__manager.py index beaa23b..058242c 100644 --- a/transaction/tests/test__manager.py +++ b/transaction/tests/test__manager.py @@ -664,6 +664,36 @@ def test_notify_transaction_late_comers(self): s.beforeCompletion.assert_called_with(t) s.afterCompletion.assert_called_with(t) + def test_unregisterSynch_on_transaction_manager_from_serparate_thread(self): + # We should be able to get the underlying manager of the thread manager + # cand call methods from other threads. + + import threading, transaction + + started = threading.Event() + stopped = threading.Event() + + synchronizer = self + + class Runner(threading.Thread): + + def __init__(self): + threading.Thread.__init__(self) + self.manager = transaction.manager.manager + self.setDaemon(True) + self.start() + + def run(self): + self.manager.registerSynch(synchronizer) + started.set() + stopped.wait() + + runner = Runner() + started.wait() + runner.manager.unregisterSynch(synchronizer) + stopped.set() + runner.join(1) + class AttemptTests(unittest.TestCase): def _makeOne(self, manager):