Skip to content

Commit

Permalink
Merge 4.x branch
Browse files Browse the repository at this point in the history
  • Loading branch information
jmuchemb committed Jun 22, 2016
2 parents 52ce05a + 568aa53 commit f9f3dda
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 74 deletions.
2 changes: 1 addition & 1 deletion src/ZODB/ConflictResolution.py
Expand Up @@ -28,7 +28,7 @@

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

ResolvedSerial = b'rs'
ResolvedSerial = b'rs' # deprecated: store/tpc_finish should just use True

class BadClassName(Exception):
pass
Expand Down
40 changes: 35 additions & 5 deletions src/ZODB/Connection.py
Expand Up @@ -589,22 +589,25 @@ def _store_objects(self, writer, transaction):

self._handle_serial(oid, s)

def _handle_serial(self, oid, serial, change=True):
def _handle_serial(self, oid, serial=True, change=True):

# if we write an object, we don't want to check if it was read
# while current. This is a convenient choke point to do this.
self._readCurrent.pop(oid, None)

if not serial:
return
if not isinstance(serial, bytes):
if serial is True:
serial = ResolvedSerial
elif not isinstance(serial, bytes):
raise serial
obj = self._cache.get(oid, None)
if obj is None:
return
if serial == ResolvedSerial:
del obj._p_changed # transition from changed to ghost
else:
self._warn_about_returned_serial()
if change:
obj._p_changed = 0 # transition from changed to up-to-date
obj._p_serial = serial
Expand Down Expand Up @@ -674,17 +677,44 @@ def tpc_vote(self, transaction):
raise

if s:
if type(s[0]) is bytes:
for oid in s:
self._handle_serial(oid)
return
self._warn_about_returned_serial()
for oid, serial in s:
self._handle_serial(oid, serial)

def tpc_finish(self, transaction):
"""Indicate confirmation that the transaction is done.
"""
# XXX someday, we'll care about the *real* tid we get back via
# the callback (that we're no longer calling).
self._storage.tpc_finish(transaction)
serial = self._storage.tpc_finish(transaction)
if serial is not None:
assert type(serial) is bytes, repr(serial)
for oid_iterator in self._modified, self._creating:
for oid in oid_iterator:
obj = self._cache.get(oid)
# Ignore missing objects and don't update ghosts.
if obj is not None and obj._p_changed is not None:
obj._p_changed = 0
obj._p_serial = serial
else:
self._warn_about_returned_serial()
self._tpc_cleanup()

def _warn_about_returned_serial(self):
# Do not warn about own implementations of ZODB.
# We're aware and the user can't do anything about it.
if self._normal_storage.__module__.startswith("_ZODB."):
self._warn_about_returned_serial = lambda: None
else:
warnings.warn(
"In ZODB 5+, the new API for the returned value of"
" store/tpc_vote/tpc_finish will be mandatory."
" See IStorage for more information.",
DeprecationWarning, 2)
Connection._warn_about_returned_serial = lambda self: None

def sortKey(self):
"""Return a consistent sort key for this connection."""
return "%s:%s" % (self._storage.sortKey(), id(self))
Expand Down
23 changes: 15 additions & 8 deletions src/ZODB/interfaces.py
Expand Up @@ -722,6 +722,12 @@ def tpc_finish(transaction, func = lambda tid: None):
called while the storage transaction lock is held. It takes
the new transaction id generated by the transaction.
The return value must be the committed tid. It is used to set the
serial for objects whose ids were passed to previous store calls
in the same transaction.
For compatibility, the return value can also be None, in which case
store/tpc_vote must return the serial of stored objects.
"""

def tpc_vote(transaction):
Expand All @@ -737,17 +743,18 @@ def tpc_vote(transaction):
without an error, then there must not be an error if
tpc_finish or tpc_abort is called subsequently.
The return value can be either None or a sequence of object-id
and serial pairs giving new serials for objects who's ids were
passed to previous store calls in the same transaction.
After the tpc_vote call, new serials must have been returned,
either from tpc_vote or store for objects passed to store.
The return value can be either None or a sequence of oids for which
a conflict was resolved.
A serial returned in a sequence of oid/serial pairs, may be
the special value ZODB.ConflictResolution.ResolvedSerial to
indicate that a conflict occured and that the object should be
For compatibility, the return value can also be a sequence of object-id
and serial pairs giving new serials for objects whose ids were
passed to previous store calls in the same transaction. The serial
can be the special value ZODB.ConflictResolution.ResolvedSerial to
indicate that a conflict occurred and that the object should be
invalidated.
After the tpc_vote call, all solved conflicts must have been notified,
either from tpc_vote or store for objects passed to store.
"""

class IStorageRestoreable(IStorage):
Expand Down
2 changes: 1 addition & 1 deletion src/ZODB/mvccadapter.py
Expand Up @@ -172,7 +172,7 @@ def invalidate_finish(tid):
self._base._invalidate_finish(modified, self)
func(tid)

self._storage.tpc_finish(transaction, invalidate_finish)
return self._storage.tpc_finish(transaction, invalidate_finish)

def read_only_writer(self, *a, **kw):
raise POSException.ReadOnlyError
Expand Down
4 changes: 3 additions & 1 deletion src/ZODB/tests/BasicStorage.py
Expand Up @@ -71,8 +71,10 @@ def checkSerialIsNoneForInitialRevision(self):
r1 = self._storage.store(oid, None, zodb_pickle(MinPO(11)),
'', txn)
r2 = self._storage.tpc_vote(txn)
self._storage.tpc_finish(txn)
serial = self._storage.tpc_finish(txn)
newrevid = handle_serials(oid, r1, r2)
if newrevid is None and serial is not None:
newrevid = serial
data, revid = utils.load_current(self._storage, oid)
value = zodb_unpickle(data)
eq(value, MinPO(11))
Expand Down
69 changes: 30 additions & 39 deletions src/ZODB/tests/ConflictResolution.py
Expand Up @@ -13,11 +13,10 @@
##############################################################################
"""Tests for application-level conflict resolution."""

from ZODB import DB
from ZODB.POSException import ConflictError, UndoError
from persistent import Persistent
from transaction import Transaction

from ZODB.utils import load_current
from transaction import Transaction, TransactionManager

from ZODB.tests.StorageTestBase import zodb_unpickle, zodb_pickle

Expand All @@ -28,8 +27,8 @@ class PCounter(Persistent):
def __repr__(self):
return "<PCounter %d>" % self._value

def inc(self):
self._value = self._value + 1
def inc(self, n=1):
self._value = self._value + n

def _p_resolveConflict(self, oldState, savedState, newState):
savedDiff = savedState['_value'] - oldState['_value']
Expand Down Expand Up @@ -57,46 +56,38 @@ def _p_resolveConflict(self, oldState, savedState):

class ConflictResolvingStorage:

def checkResolve(self):
obj = PCounter()
obj.inc()

oid = self._storage.new_oid()

revid1 = self._dostoreNP(oid, data=zodb_pickle(obj))
def checkResolve(self, resolvable=True):
db = DB(self._storage)

obj.inc()
obj.inc()
# The effect of committing two transactions with the same
# pickle is to commit two different transactions relative to
# revid1 that add two to _value.
revid2 = self._dostoreNP(oid, revid=revid1, data=zodb_pickle(obj))
revid3 = self._dostoreNP(oid, revid=revid1, data=zodb_pickle(obj))
t1 = TransactionManager()
c1 = db.open(t1)
o1 = c1.root()['p'] = (PCounter if resolvable else PCounter2)()
o1.inc()
t1.commit()

data, serialno = load_current(self._storage, oid)
inst = zodb_unpickle(data)
self.assertEqual(inst._value, 5)
t2 = TransactionManager()
c2 = db.open(t2)
o2 = c2.root()['p']
o2.inc(2)
t2.commit()

def checkUnresolvable(self):
obj = PCounter2()
obj.inc()

oid = self._storage.new_oid()

revid1 = self._dostoreNP(oid, data=zodb_pickle(obj))

obj.inc()
obj.inc()
# The effect of committing two transactions with the same
# pickle is to commit two different transactions relative to
# revid1 that add two to _value.
revid2 = self._dostoreNP(oid, revid=revid1, data=zodb_pickle(obj))
o1.inc(3)
try:
self._dostoreNP(oid, revid=revid1, data=zodb_pickle(obj))
t1.commit()
except ConflictError as err:
self.assertTrue("PCounter2" in str(err))
self.assertIn(".PCounter2,", str(err))
self.assertEqual(o1._value, 3)
else:
self.fail("Expected ConflictError")
self.assertTrue(resolvable, "Expected ConflictError")
self.assertEqual(o1._value, 6)

t2.begin()
self.assertEqual(o2._value, o1._value)

db.close()

def checkUnresolvable(self):
self.checkResolve(False)

def checkZClassesArentResolved(self):
from ZODB.ConflictResolution import find_global, BadClassName
Expand Down
6 changes: 5 additions & 1 deletion src/ZODB/tests/HistoryStorage.py
Expand Up @@ -34,7 +34,7 @@ def _checkHistory(self, data):
for data in data:
if sys.platform == 'win32':
# time.time() has a precision of 1ms on Windows.
sleep(0.002)
sleep(0.001)
revids.append(self._dostore(oid, revids[-1], MinPO(data)))
revids.reverse()
del revids[-1]
Expand All @@ -43,6 +43,10 @@ def _checkHistory(self, data):
h = self._storage.history(oid, size=i)
self.assertEqual([d['tid'] for d in h], revids[:i])
# Check results are sorted by timestamp, in descending order.
if sys.platform == 'win32':
# Same as above. This is also required in case this method is
# called several times for the same storage.
sleep(0.001)
a = time()
for d in h:
b = a
Expand Down
4 changes: 3 additions & 1 deletion src/ZODB/tests/MTStorage.py
Expand Up @@ -155,10 +155,12 @@ def dostore(self, i):
r2 = self.storage.tpc_vote(t)
self.pause()

self.storage.tpc_finish(t)
serial = self.storage.tpc_finish(t)
self.pause()

revid = handle_serials(oid, r1, r2)
if serial is not None and revid is None:
revid = serial
self.oids[oid] = revid

class ExtStorageClientThread(StorageClientThread):
Expand Down
4 changes: 3 additions & 1 deletion src/ZODB/tests/RevisionStorage.py
Expand Up @@ -150,10 +150,12 @@ def helper(tid, revid, x):
# Finish the transaction
r2 = self._storage.tpc_vote(t)
newrevid = handle_serials(oid, r1, r2)
self._storage.tpc_finish(t)
serial = self._storage.tpc_finish(t)
except:
self._storage.tpc_abort(t)
raise
if serial is not None and newrevid is None:
newrevid = serial
return newrevid
revid1 = helper(1, None, 1)
revid2 = helper(2, revid1, 2)
Expand Down
10 changes: 6 additions & 4 deletions src/ZODB/tests/StorageTestBase.py
Expand Up @@ -132,7 +132,7 @@ def handle_serials(oid, *args):
A helper for function _handle_all_serials().
"""
return handle_all_serials(oid, *args)[oid]
return handle_all_serials(oid, *args).get(oid)

def import_helper(name):
__import__(name)
Expand Down Expand Up @@ -189,7 +189,9 @@ def _dostore(self, oid=None, revid=None, data=None,
# Finish the transaction
r2 = self._storage.tpc_vote(t)
revid = handle_serials(oid, r1, r2)
self._storage.tpc_finish(t)
serial = self._storage.tpc_finish(t)
if serial is not None and revid is None:
revid = serial
except:
self._storage.tpc_abort(t)
raise
Expand All @@ -209,8 +211,8 @@ def _undo(self, tid, expected_oids=None, note=None):
self._storage.tpc_begin(t)
undo_result = self._storage.undo(tid, t)
vote_result = self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
if expected_oids is not None:
serial = self._storage.tpc_finish(t)
if expected_oids is not None and serial is None:
oids = list(undo_result[1]) if undo_result else []
oids.extend(oid for (oid, _) in vote_result or ())
self.assertEqual(len(oids), len(expected_oids), repr(oids))
Expand Down

0 comments on commit f9f3dda

Please sign in to comment.