Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransactionalUndo closes storages it opens. #269

Merged
merged 1 commit into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@

- Drop support for Python 3.4.

- Fix ``DB.undo()`` and ``DB.undoMultiple()`` to close the storage
they open behind the scenes when the transaction is committed or
rolled back. See `issue 268
<https://github.com/zopefoundation/ZODB/issues/268>`_.

5.5.1 (2018-10-25)
==================

- Fix KeyError on releasing resources of a Connection when closing the DB.
This requires at least version 2.4 of the `transaction` package.
See `issue 208 <https://github.com/zopefoundation/ZODB/issues/208>`.
This requires at least version 2.4 of the ``transaction`` package.
See `issue 208 <https://github.com/zopefoundation/ZODB/issues/208>`_.

5.5.0 (2018-10-13)
==================
Expand Down
75 changes: 54 additions & 21 deletions src/ZODB/DB.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,29 @@
"""Database objects
"""
from __future__ import print_function

import sys
import logging
import datetime
import time
import warnings

from . import utils

from ZODB.broken import find_global
from ZODB.utils import z64
from ZODB.Connection import Connection, TransactionMetaData, noop
from ZODB._compat import Pickler, _protocol, BytesIO
import ZODB.serialize
from persistent.TimeStamp import TimeStamp
import six

import transaction
import transaction.weakset

from zope.interface import implementer

from ZODB import utils
from ZODB.interfaces import IDatabase
from ZODB.interfaces import IMVCCStorage

import transaction

from persistent.TimeStamp import TimeStamp
import six

from . import POSException, valuedoc
from ZODB.broken import find_global
from ZODB.utils import z64
from ZODB.Connection import Connection, TransactionMetaData, noop
import ZODB.serialize
from ZODB import valuedoc

logger = logging.getLogger('ZODB.DB')

Expand Down Expand Up @@ -1056,19 +1053,43 @@ class TransactionalUndo(object):

def __init__(self, db, tids):
self._db = db
self._storage = getattr(
db._mvcc_storage, 'undo_instance', db._mvcc_storage.new_instance)()
self._tids = tids
self._storage = None

def abort(self, transaction):
pass

def close(self):
if self._storage is not None:
# We actually want to release the storage we've created,
# not close it. releasing it frees external resources
# dedicated to this instance, closing might make permanent
# changes that affect other instances.
self._storage.release()
self._storage = None

def tpc_begin(self, transaction):
assert self._storage is None, "Already in an active transaction"
jamadden marked this conversation as resolved.
Show resolved Hide resolved

tdata = TransactionMetaData(
transaction.user,
transaction.description,
transaction.extension)
transaction.set_data(self, tdata)
# `undo_instance` is not part of any IStorage interface;
# it is defined in our MVCCAdapter. Regardless, we're opening
# a new storage instance, and so we must close it to be sure
# to reclaim resources in a timely manner.
#
# Once the tpc_begin method has been called, the transaction manager will
# guarantee to call either `tpc_finish` or `tpc_abort`, so those are the only
# methods we need to be concerned about calling close() from.
db_mvcc_storage = self._db._mvcc_storage
self._storage = getattr(
db_mvcc_storage,
'undo_instance',
db_mvcc_storage.new_instance)()

self._storage.tpc_begin(tdata)

def commit(self, transaction):
Expand All @@ -1081,15 +1102,27 @@ def tpc_vote(self, transaction):
self._storage.tpc_vote(transaction)

def tpc_finish(self, transaction):
transaction = transaction.data(self)
self._storage.tpc_finish(transaction)
try:
transaction = transaction.data(self)
self._storage.tpc_finish(transaction)
finally:
self.close()

def tpc_abort(self, transaction):
transaction = transaction.data(self)
self._storage.tpc_abort(transaction)
try:
transaction = transaction.data(self)
self._storage.tpc_abort(transaction)
finally:
self.close()

def sortKey(self):
return "%s:%s" % (self._storage.sortKey(), id(self))
# The transaction sorts data managers first before it calls
# `tpc_begin`, so we can't use our own storage because it's
# not open yet. Fortunately new_instances of a storage are
# supposed to return the same sort key as the original storage
# did.
return "%s:%s" % (self._db._mvcc_storage.sortKey(), id(self))


def connection(*args, **kw):
"""Create a database :class:`connection <ZODB.Connection.Connection>`.
Expand Down
113 changes: 112 additions & 1 deletion src/ZODB/tests/testDB.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,117 @@ def check(info, text):
check(db.undoLog(0, 3) , True)
check(db.undoInfo(0, 3) , True)


class TransactionalUndoTests(unittest.TestCase):

def _makeOne(self):
from ZODB.DB import TransactionalUndo

class MockStorage(object):
instance_count = 0
close_count = 0
release_count = 0
begin_count = 0
abort_count = 0

def new_instance(self):
self.instance_count += 1
return self

def tpc_begin(self, tx):
self.begin_count += 1

def close(self):
self.close_count += 1

def release(self):
self.release_count += 1

def sortKey(self):
return 'MockStorage'

class MockDB(object):

def __init__(self):
self.storage = self._mvcc_storage = MockStorage()

return TransactionalUndo(MockDB(), [1])

def test_only_new_instance_on_begin(self):
undo = self._makeOne()
self.assertIsNone(undo._storage)
self.assertEqual(0, undo._db.storage.instance_count)

undo.tpc_begin(transaction.get())
self.assertIsNotNone(undo._storage)
self.assertEqual(1, undo._db.storage.instance_count)
self.assertEqual(1, undo._db.storage.begin_count)
self.assertIsNotNone(undo._storage)

# And we can't begin again
with self.assertRaises(AssertionError):
undo.tpc_begin(transaction.get())

def test_close_many(self):
undo = self._makeOne()
self.assertIsNone(undo._storage)
self.assertEqual(0, undo._db.storage.instance_count)

undo.close()
# Not open, didn't go through
self.assertEqual(0, undo._db.storage.close_count)
self.assertEqual(0, undo._db.storage.release_count)

undo.tpc_begin(transaction.get())
undo.close()
undo.close()
self.assertEqual(0, undo._db.storage.close_count)
self.assertEqual(1, undo._db.storage.release_count)
self.assertIsNone(undo._storage)

def test_sortKey(self):
# We get the same key whether or not we're open
undo = self._makeOne()
key = undo.sortKey()
self.assertIn('MockStorage', key)

undo.tpc_begin(transaction.get())
key2 = undo.sortKey()
self.assertEqual(key, key2)

def test_tpc_abort_closes(self):
undo = self._makeOne()
undo.tpc_begin(transaction.get())
undo._db.storage.tpc_abort = lambda tx: None
undo.tpc_abort(transaction.get())
self.assertEqual(0, undo._db.storage.close_count)
self.assertEqual(1, undo._db.storage.release_count)

def test_tpc_abort_closes_on_exception(self):
undo = self._makeOne()
undo.tpc_begin(transaction.get())
with self.assertRaises(AttributeError):
undo.tpc_abort(transaction.get())
self.assertEqual(0, undo._db.storage.close_count)
self.assertEqual(1, undo._db.storage.release_count)

def test_tpc_finish_closes(self):
undo = self._makeOne()
undo.tpc_begin(transaction.get())
undo._db.storage.tpc_finish = lambda tx: None
undo.tpc_finish(transaction.get())
self.assertEqual(0, undo._db.storage.close_count)
self.assertEqual(1, undo._db.storage.release_count)

def test_tpc_finish_closes_on_exception(self):
undo = self._makeOne()
undo.tpc_begin(transaction.get())
with self.assertRaises(AttributeError):
undo.tpc_finish(transaction.get())
self.assertEqual(0, undo._db.storage.close_count)
self.assertEqual(1, undo._db.storage.release_count)


def test_invalidateCache():
"""The invalidateCache method invalidates a connection caches for all of
the connections attached to a database::
Expand Down Expand Up @@ -423,7 +534,7 @@ def cleanup_on_close():
"""

def test_suite():
s = unittest.makeSuite(DBTests)
s = unittest.defaultTestLoader.loadTestsFromName(__name__)
s.addTest(doctest.DocTestSuite(
setUp=ZODB.tests.util.setUp, tearDown=ZODB.tests.util.tearDown,
checker=checker
Expand Down