Commit 1070adcb authored by Dieter Maurer's avatar Dieter Maurer Committed by GitHub

Merge pull request #174 from zopefoundation/ZODB5.6_compatible_lastTransaction#166

let `lastTransaction` change only after invalidation processing
parents 60c9d106 334eb91c
...@@ -4,6 +4,12 @@ Changelog ...@@ -4,6 +4,12 @@ Changelog
5.2.3 (unreleased) 5.2.3 (unreleased)
------------------ ------------------
- Ensure ``ZEO`` satisfies the ``ZODB >= 5.6`` requirement that
``lastTransaction()`` changes only after invalidation processing.
Violating this requirement can lead to race conditions and
associated data corruption
`#166 <https://github.com/zopefoundation/ZEO/issues/166>`_.
- Add automated tests against the ZODB ``master`` branch - Add automated tests against the ZODB ``master`` branch
see `issue 177 <https://github.com/zopefoundation/ZEO/issues/177>`_. see `issue 177 <https://github.com/zopefoundation/ZEO/issues/177>`_.
......
...@@ -649,11 +649,31 @@ class Client(object): ...@@ -649,11 +649,31 @@ class Client(object):
try: try:
tid = yield self.protocol.fut('tpc_finish', tid) tid = yield self.protocol.fut('tpc_finish', tid)
cache = self.cache cache = self.cache
# The cache invalidation here and that in
# ``invalidateTransaction`` are both performed
# in the IO thread. Thus there is no interference.
# Other threads might observe a partially invalidated
# cache. However, regular loads will access
# object state before ``tid``; therefore,
# partial invalidation for ``tid`` should not harm.
for oid, data, resolved in updates: for oid, data, resolved in updates:
cache.invalidate(oid, tid) cache.invalidate(oid, tid)
if data and not resolved: if data and not resolved:
cache.store(oid, tid, None, data) cache.store(oid, tid, None, data)
# ZODB >= 5.6 requires that ``lastTransaction`` changes
# only after invalidation processing (performed in
# the ``f`` call below) (for ``ZEO``, ``lastTransaction``
# is implemented as ``cache.getLastTid()``).
# Some tests involve ``f`` in the verification that
# ``tpc_finish`` modifies ``lastTransaction`` and require
# that ``cache.setLastTid`` is called before ``f``.
# We use locking below to ensure that the
# effect of ``setLastTid`` is observable by other
# threads only after ``f`` has been called.
with cache._lock:
cache.setLastTid(tid) cache.setLastTid(tid)
f(tid)
future.set_result(tid)
except Exception as exc: except Exception as exc:
future.set_exception(exc) future.set_exception(exc)
...@@ -662,9 +682,6 @@ class Client(object): ...@@ -662,9 +682,6 @@ class Client(object):
# recovering to a consistent state. # recovering to a consistent state.
self.protocol.close() self.protocol.close()
self.disconnected(self.protocol) self.disconnected(self.protocol)
else:
f(tid)
future.set_result(tid)
else: else:
future.set_exception(ClientDisconnected()) future.set_exception(ClientDisconnected())
...@@ -674,6 +691,8 @@ class Client(object): ...@@ -674,6 +691,8 @@ class Client(object):
def invalidateTransaction(self, tid, oids): def invalidateTransaction(self, tid, oids):
if self.ready: if self.ready:
# see the cache related comment in ``tpc_finish_threadsafe``
# why we think that locking is not necessary at this place
for oid in oids: for oid in oids:
self.cache.invalidate(oid, tid) self.cache.invalidate(oid, tid)
self.client.invalidateTransaction(tid, oids) self.client.invalidateTransaction(tid, oids)
......
...@@ -9,7 +9,7 @@ from zope.testing import setupstack ...@@ -9,7 +9,7 @@ from zope.testing import setupstack
from concurrent.futures import Future from concurrent.futures import Future
import mock import mock
from ZODB.POSException import ReadOnlyError from ZODB.POSException import ReadOnlyError
from ZODB.utils import maxtid from ZODB.utils import maxtid, RLock
import collections import collections
import logging import logging
...@@ -699,6 +699,7 @@ class MemoryCache(object): ...@@ -699,6 +699,7 @@ class MemoryCache(object):
# { oid -> [(start, end, data)] } # { oid -> [(start, end, data)] }
self.data = collections.defaultdict(list) self.data = collections.defaultdict(list)
self.last_tid = None self.last_tid = None
self._lock = RLock()
clear = __init__ clear = __init__
......
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