Commit 3406b26d authored by Kirill Smelkov's avatar Kirill Smelkov

.

parent eb53b7aa
#!/usr/bin/env python #!/usr/bin/env python
"""Program zopenrace.py demonstrates race-condition bug in ZODB """Program zopenrace.py demonstrates concurrency bug in ZODB Connection.open()
Connection.open() that leads to stale live cache and wrong data provided by that leads to stale live cache and wrong data provided by database to users.
database to users.
The bug is that when a connection is opened, it syncs to storage and processes
invalidations received from the storage in two _separate_ steps, potentially
newTransaction leading to situation where invalidations for transactions _past_ opened
._storage.sync() connection's view of the database are included into opened connection's cache
invalidated = ._storage.poll_invalidations(): invalidation. This leads to stale connection cache and old data provided by
ZODB.Connection when it is reopened next time.
._storage._start = ._storage._storage.lastTrasaction() + 1:
s = ._storage._storage That in turn can lead to loose of Consistency of the database if mix of current
s._lock.acquire() and old data is used to process a transaction. A classic example would be bank
head = s._ltid accounts A, B and C with A<-B and A<-C transfer transactions. If transaction
s._lock.release() that handles A<-C sees stale data for A when starting its processing, it
return head results in A loosing what it should have received from B.
XXX T2 commits here: objX
Below is timing diagram on how the bug happens on ZODB5:
._storage._lock.acquire()
r = _storage._invalidations ; T1 receives invalidations for some transactions after head Client1 or Thread1 Client2 or Thread2
._storage._lock.release()
return r # T1 begins transaction and opens zodb connection
newTransaction():
# T1 processes invalidates for [... head] _and_ some next transactions. # implementation in Connection.py[1]
# T1 thus will _not_ process invalidations for those next transactions when ._storage.sync()
# opening zconn _next_ time. The next opened zconn will thus see _stale_ data. invalidated = ._storage.poll_invalidations():
._cache.invalidate(invalidated) # implementation in MVCCAdapterInstance [2]
# T1 settles on as of which particular database state it will be
# viewing the database.
._storage._start = ._storage._storage.lastTrasaction() + 1:
s = ._storage._storage
s._lock.acquire()
head = s._ltid
s._lock.release()
return head
# T2 commits here.
# Time goes by and storage server sends
# corresponding invalidation message to T1,
# which T1 queues in its _storage._invalidations
# T1 retrieves queued invalidations which _includes_
# invalidation for transaction that T2 just has committed,
# that goes past @head.
._storage._lock.acquire()
r = _storage._invalidations
._storage._lock.release()
return r
# T1 processes invalidations for [... head] _and_ invalidations for past-@head transaction.
# T1 thus will _not_ process invalidations for that next transaction when
# opening zconn _next_ time. The next opened zconn will thus see _stale_ data.
._cache.invalidate(invalidated)
[1] https://github.com/zopefoundation/ZODB/blob/5.5.1-29-g0b3db5aee/src/ZODB/Connection.py#L734-L742
[2] https://github.com/zopefoundation/ZODB/blob/5.5.1-29-g0b3db5aee/src/ZODB/mvccadapter.py#L130-L139
""" """
from ZODB import DB from ZODB import DB
......
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