Commit d7a558d1 authored by Jeremy Hylton's avatar Jeremy Hylton

Handle empty transactions without touching the storage.

# NB: commit() is responsible for calling tpc_begin() on the storage.
# It uses self._begun to track whether it has been called.  When
# self._begun is None, it has not been called.

# This arrangement allows us to handle the special case of a
# transaction with no modified objects.  It is possible for
# registration to be occur unintentionally and for a persistent
# object to compensate by making itself as unchanged.  When this
# happens, it's possible to commit a transaction with no modified
# objects.

# Since tpc_begin() may raise a ReadOnlyError, don't call it if there
# are no objects.  This avoids spurious (?) errors when working with
# a read-only storage.

Add code to handle this in Connection's tpc_begin() and commit()
methods.

Add two tests in testZODB.
parent 38646258
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
############################################################################## ##############################################################################
"""Database connection support """Database connection support
$Id: Connection.py,v 1.74 2002/09/07 16:40:59 jeremy Exp $""" $Id: Connection.py,v 1.75 2002/09/07 17:22:09 jeremy Exp $"""
from cPickleCache import PickleCache, MUCH_RING_CHECKING from cPickleCache import PickleCache, MUCH_RING_CHECKING
from POSException import ConflictError, ReadConflictError from POSException import ConflictError, ReadConflictError
...@@ -268,13 +268,33 @@ class Connection(ExportImport.ExportImport): ...@@ -268,13 +268,33 @@ class Connection(ExportImport.ExportImport):
self.__onCommitActions.append((method_name, args, kw)) self.__onCommitActions.append((method_name, args, kw))
get_transaction().register(self) get_transaction().register(self)
# NB: commit() is responsible for calling tpc_begin() on the storage.
# It uses self._begun to track whether it has been called. When
# self._begun is None, it has not been called.
# This arrangement allows us to handle the special case of a
# transaction with no modified objects. It is possible for
# registration to be occur unintentionally and for a persistent
# object to compensate by making itself as unchanged. When this
# happens, it's possible to commit a transaction with no modified
# objects.
# Since tpc_begin() may raise a ReadOnlyError, don't call it if there
# are no objects. This avoids spurious (?) errors when working with
# a read-only storage.
def commit(self, object, transaction): def commit(self, object, transaction):
if object is self: if object is self:
if self._begun is None:
self._storage.tpc_begin(transaction)
self._begun = 1
# We registered ourself. Execute a commit action, if any. # We registered ourself. Execute a commit action, if any.
if self.__onCommitActions is not None: if self.__onCommitActions is not None:
method_name, args, kw = self.__onCommitActions.pop(0) method_name, args, kw = self.__onCommitActions.pop(0)
apply(getattr(self, method_name), (transaction,) + args, kw) apply(getattr(self, method_name), (transaction,) + args, kw)
return return
oid = object._p_oid oid = object._p_oid
invalid = self._invalid invalid = self._invalid
if oid is None or object._p_jar is not self: if oid is None or object._p_jar is not self:
...@@ -293,6 +313,10 @@ class Connection(ExportImport.ExportImport): ...@@ -293,6 +313,10 @@ class Connection(ExportImport.ExportImport):
# Nothing to do # Nothing to do
return return
if self._begun is None:
self._storage.tpc_begin(transaction)
self._begun = 1
stack = [object] stack = [object]
# Create a special persistent_id that passes T and the subobject # Create a special persistent_id that passes T and the subobject
...@@ -592,31 +616,33 @@ class Connection(ExportImport.ExportImport): ...@@ -592,31 +616,33 @@ class Connection(ExportImport.ExportImport):
if self.__onCommitActions is not None: if self.__onCommitActions is not None:
del self.__onCommitActions del self.__onCommitActions
self._storage.tpc_abort(transaction) self._storage.tpc_abort(transaction)
cache=self._cache self._cache.invalidate(self._invalidated)
cache.invalidate(self._invalidated) self._cache.invalidate(self._invalidating)
cache.invalidate(self._invalidating)
self._invalidate_creating() self._invalidate_creating()
def tpc_begin(self, transaction, sub=None): def tpc_begin(self, transaction, sub=None):
self._invalidating = [] self._invalidating = []
self._creating = [] self._creating = []
self._begun = None
if sub: if sub:
# Sub-transaction! # Sub-transaction!
_tmp=self._tmp if self._tmp is None:
if _tmp is None: _tmp = TmpStore.TmpStore(self._version)
_tmp=TmpStore.TmpStore(self._version) self._tmp = self._storage
self._tmp=self._storage self._storage = _tmp
self._storage=_tmp
_tmp.registerDB(self._db, 0) _tmp.registerDB(self._db, 0)
self._storage.tpc_begin(transaction) # It's okay to always call tpc_begin() for a sub-transaction
# because this isn't the real storage.
self._storage.tpc_begin(transaction)
self._begun = 1
def tpc_vote(self, transaction): def tpc_vote(self, transaction):
if self.__onCommitActions is not None: if self.__onCommitActions is not None:
del self.__onCommitActions del self.__onCommitActions
try: try:
vote=self._storage.tpc_vote vote = self._storage.tpc_vote
except AttributeError: except AttributeError:
return return
s = vote(transaction) s = vote(transaction)
......
...@@ -94,6 +94,38 @@ class ZODBTests(unittest.TestCase, ExportImportTests): ...@@ -94,6 +94,38 @@ class ZODBTests(unittest.TestCase, ExportImportTests):
self._storage.close() self._storage.close()
removefs("ZODBTests.fs") removefs("ZODBTests.fs")
def checkUnmodifiedObject(self):
# Test that a transaction with only unmodified objects works
# correctly. The specific sequence of events is:
# - an object is modified
# - it is registered with the transaction
# - the object is explicitly "unmodified"
# - the transaction commits, but now has no modified objects
# We'd like to avoid doing anything with the storage.
ltid = self._storage.lastTransaction()
_objects = get_transaction()._objects
self.assertEqual(len(_objects), 0)
r = self._db.open().root()
obj = r["test"][0]
obj[1] = 1
self.assertEqual(obj._p_changed, 1)
self.assertEqual(len(_objects), 1)
del obj._p_changed
self.assertEqual(obj._p_changed, None)
self.assertEqual(len(_objects), 1)
get_transaction().commit()
self.assertEqual(ltid, self._storage.lastTransaction())
def checkVersionOnly(self):
# Make sure the changes to make empty transactions a no-op
# still allow things like abortVersion(). This should work
# because abortVersion() calls tpc_begin() itself.
r = self._db.open("version").root()
r[1] = 1
get_transaction().commit()
self._db.abortVersion("version")
get_transaction().commit()
def test_suite(): def test_suite():
return unittest.makeSuite(ZODBTests, 'check') return unittest.makeSuite(ZODBTests, 'check')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment