Commit 29091374 authored by Jim Fulton's avatar Jim Fulton

Abstract ZODB's MVCC implementation into a storage adapter.

That's applied to storages other than RelStorage.

See: https://groups.google.com/forum/#!topic/zodb/QJYmvF0eUUM
parent 9bcd944e
...@@ -20,7 +20,7 @@ to application logic. ZODB includes features such as a plugable storage ...@@ -20,7 +20,7 @@ to application logic. ZODB includes features such as a plugable storage
interface, rich transaction support, and undo. interface, rich transaction support, and undo.
""" """
version = "4.3.0.dev0" version = "5.0.dev0"
import os import os
from setuptools import setup, find_packages from setuptools import setup, find_packages
......
This diff is collapsed.
...@@ -401,12 +401,6 @@ class DB(object): ...@@ -401,12 +401,6 @@ class DB(object):
- `xrefs` - Boolian flag indicating whether implicit cross-database - `xrefs` - Boolian flag indicating whether implicit cross-database
references are allowed references are allowed
""" """
if isinstance(storage, six.string_types):
from ZODB import FileStorage
storage = ZODB.FileStorage.FileStorage(storage, **storage_args)
elif storage is None:
from ZODB import MappingStorage
storage = ZODB.MappingStorage.MappingStorage(**storage_args)
# Allocate lock. # Allocate lock.
x = utils.RLock() x = utils.RLock()
...@@ -423,12 +417,24 @@ class DB(object): ...@@ -423,12 +417,24 @@ class DB(object):
self._historical_cache_size_bytes = historical_cache_size_bytes self._historical_cache_size_bytes = historical_cache_size_bytes
# Setup storage # Setup storage
if isinstance(storage, six.string_types):
from ZODB import FileStorage
storage = ZODB.FileStorage.FileStorage(storage, **storage_args)
elif storage is None:
from ZODB import MappingStorage
storage = ZODB.MappingStorage.MappingStorage(**storage_args)
else:
assert not storage_args
self.storage = storage self.storage = storage
if IMVCCStorage.providedBy(storage):
self._mvcc_storage = storage
else:
from .mvccadapter import MVCCAdapter
self._mvcc_storage = MVCCAdapter(storage)
self.references = ZODB.serialize.referencesf self.references = ZODB.serialize.referencesf
try:
storage.registerDB(self)
except TypeError:
storage.registerDB(self, None) # Backward compat
if (not hasattr(storage, 'tpc_vote')) and not storage.isReadOnly(): if (not hasattr(storage, 'tpc_vote')) and not storage.isReadOnly():
warnings.warn( warnings.warn(
...@@ -438,12 +444,10 @@ class DB(object): ...@@ -438,12 +444,10 @@ class DB(object):
DeprecationWarning, 2) DeprecationWarning, 2)
storage.tpc_vote = lambda *args: None storage.tpc_vote = lambda *args: None
if IMVCCStorage.providedBy(storage): temp_storage = self._mvcc_storage.new_instance()
temp_storage = storage.new_instance()
else:
temp_storage = storage
try: try:
try: try:
temp_storage.poll_invalidations()
temp_storage.load(z64, '') temp_storage.load(z64, '')
except KeyError: except KeyError:
# Create the database's root in the storage if it doesn't exist # Create the database's root in the storage if it doesn't exist
...@@ -462,8 +466,7 @@ class DB(object): ...@@ -462,8 +466,7 @@ class DB(object):
temp_storage.tpc_vote(t) temp_storage.tpc_vote(t)
temp_storage.tpc_finish(t) temp_storage.tpc_finish(t)
finally: finally:
if IMVCCStorage.providedBy(temp_storage): temp_storage.release()
temp_storage.release()
# Multi-database setup. # Multi-database setup.
if databases is None: if databases is None:
...@@ -634,16 +637,13 @@ class DB(object): ...@@ -634,16 +637,13 @@ class DB(object):
@self._connectionMap @self._connectionMap
def _(c): def _(c):
if c.opened: c.transaction_manager.abort()
c.transaction_manager.abort()
# Note that this will modify our pool, but this is safe, because
# _connectionMap makes a list of the pool to iterate over
c.close()
c.afterCompletion = c.newTransaction = c.close = noop c.afterCompletion = c.newTransaction = c.close = noop
c._release_resources() c._release_resources()
self.storage.close() self._mvcc_storage.close()
del self.storage del self.storage
del self._mvcc_storage
def getCacheSize(self): def getCacheSize(self):
return self._cache_size return self._cache_size
...@@ -675,27 +675,6 @@ class DB(object): ...@@ -675,27 +675,6 @@ class DB(object):
def getHistoricalTimeout(self): def getHistoricalTimeout(self):
return self.historical_pool.timeout return self.historical_pool.timeout
def invalidate(self, tid, oids, connection=None, version=''):
"""Invalidate references to a given oid.
This is used to indicate that one of the connections has committed a
change to the object. The connection commiting the change should be
passed in to prevent useless (but harmless) messages to the
connection.
"""
# Storages, esp. ZEO tests, need the version argument still. :-/
assert version==''
# Notify connections.
def inval(c):
if c is not connection:
c.invalidate(tid, oids)
self._connectionMap(inval)
def invalidateCache(self):
"""Invalidate each of the connection caches
"""
self._connectionMap(lambda c: c.invalidateCache())
transform_record_data = untransform_record_data = lambda self, data: data transform_record_data = untransform_record_data = lambda self, data: data
def objectCount(self): def objectCount(self):
...@@ -1001,9 +980,9 @@ class TransactionalUndo(object): ...@@ -1001,9 +980,9 @@ class TransactionalUndo(object):
def __init__(self, db, tids): def __init__(self, db, tids):
self._db = db self._db = db
self._storage = db.storage self._storage = getattr(
db._mvcc_storage, 'undo_instance', db._mvcc_storage.new_instance)()
self._tids = tids self._tids = tids
self._oids = set()
def abort(self, transaction): def abort(self, transaction):
pass pass
...@@ -1013,19 +992,13 @@ class TransactionalUndo(object): ...@@ -1013,19 +992,13 @@ class TransactionalUndo(object):
def commit(self, transaction): def commit(self, transaction):
for tid in self._tids: for tid in self._tids:
result = self._storage.undo(tid, transaction) self._storage.undo(tid, transaction)
if result:
self._oids.update(result[1])
def tpc_vote(self, transaction): def tpc_vote(self, transaction):
for oid, _ in self._storage.tpc_vote(transaction) or (): self._storage.tpc_vote(transaction)
self._oids.add(oid)
def tpc_finish(self, transaction): def tpc_finish(self, transaction):
self._storage.tpc_finish( self._storage.tpc_finish(transaction)
transaction,
lambda tid: self._db.invalidate(tid, self._oids)
)
def tpc_abort(self, transaction): def tpc_abort(self, transaction):
self._storage.tpc_abort(transaction) self._storage.tpc_abort(transaction)
......
...@@ -273,8 +273,6 @@ test. ...@@ -273,8 +273,6 @@ test.
>>> conn.root()['first']['count'] += 1 >>> conn.root()['first']['count'] += 1
>>> conn.root()['third'] = persistent.mapping.PersistentMapping() >>> conn.root()['third'] = persistent.mapping.PersistentMapping()
>>> transaction.commit() >>> transaction.commit()
>>> len(historical_conn._invalidated)
0
>>> historical_conn.close() >>> historical_conn.close()
Note that if you try to open an historical connection to a time in the future, Note that if you try to open an historical connection to a time in the future,
......
This diff is collapsed.
"""Adapt IStorage objects to IMVCCStorage
This is a largely internal implementation of ZODB, especially DB and
Connection. It takes the MVCC implementation involving invalidations
and start time and moves it into a storage adapter. This allows ZODB
to treat Relstoage and other storages in pretty much the same way and
also simplifies the implementation of the DB and Connection classes.
"""
import threading
import zope.interface
from . import interfaces, serialize, POSException
from .utils import p64, u64
class Base(object):
_copy_methods = (
'getName', 'getSize', 'history', 'lastTransaction', 'sortKey',
'loadBlob', 'openCommittedBlobFile',
)
def __init__(self, storage):
self._storage = storage
for name in self._copy_methods:
if hasattr(self._storage, name):
setattr(self, name, getattr(storage, name))
if interfaces.IBlobStorage.providedBy(storage):
zope.interface.alsoProvides(self, interfaces.IBlobStorage)
def isReadOnly(self):
return self._storage.isReadOnly()
def supportsUndo(self):
try:
return self._storage.supportsUndo()
except AttributeError:
return False
def undoLog(self, first, last, filter=None):
try:
return self._storage.undoLog(first, last, filter)
except AttributeError:
return False
def undoInfo(self, first=0, last=20, specification=None):
try:
return self._storage.undoInfo(first, last, specification)
except AttributeError:
return False
def temporaryDirectory(self):
return self._storage.temporaryDirectory()
def __len__():
return len(self._storage)
class MVCCAdapter(Base):
def __init__(self, storage):
Base.__init__(self, storage)
self._instances = set()
if hasattr(storage, 'registerDB'):
storage.registerDB(self)
def new_instance(self):
instance = MVCCAdapterInstance(self)
self._instances.add(instance)
return instance
def before_instance(self, before=None):
return BeforeAdapterInstance(self, before)
def undo_instance(self):
return UndoAdapterInstance(self)
def _release(self, instance):
self._instances.remove(instance)
closed = False
def close(self):
if not self.closed:
self.closed = True
self._storage.close()
del self._instances
del self._storage
def invalidateCache(self):
for instance in self._instances:
instance._invalidateCache()
def invalidate(self, transaction_id, oids, version=''):
for instance in self._instances:
instance._invalidate(oids)
references = serialize.referencesf
transform_record_data = untransform_record_data = lambda self, data: data
def _invalidate_finish(self, oids, committing_instance):
for instance in self._instances:
if instance is not committing_instance:
instance._invalidate(oids)
def pack(self, pack_time, referencesf):
return self._storage.pack(pack_time, referencesf)
class MVCCAdapterInstance(Base):
_copy_methods = Base._copy_methods + (
'isReadOnly', 'loadSerial', 'new_oid', 'tpc_vote',
'checkCurrentSerialInTransaction', 'tpc_abort',
)
def __init__(self, base):
self._base = base
Base.__init__(self, base._storage)
self._lock = threading.Lock()
self._invalidations = set()
self._start = None # Transaction start time
self._sync = getattr(self._storage, 'sync', lambda : None)
def release(self):
self._base._release(self)
close = release
def _invalidateCache(self):
with self._lock:
self._invalidations = None
def _invalidate(self, oids):
with self._lock:
try:
self._invalidations.update(oids)
except AttributeError:
if self._invalidations is not None:
raise
def sync(self, force=True):
if force:
self._sync()
def poll_invalidations(self):
self._start = p64(u64(self._storage.lastTransaction()) + 1)
with self._lock:
if self._invalidations is None:
self._invalidations = set()
return None
else:
result = list(self._invalidations)
self._invalidations.clear()
return result
def load(self, oid, version=''):
assert self._start is not None
r = self._storage.loadBefore(oid, self._start)
if r is None:
raise POSException.ReadConflictError(repr(oid))
return r[:2]
def tpc_begin(self, transaction):
self._storage.tpc_begin(transaction)
self._modified = set()
def store(self, oid, serial, data, version, transaction):
s = self._storage.store(oid, serial, data, version, transaction)
self._modified.add(oid)
return s
def storeBlob(self, oid, serial, data, blobfilename, version, transaction):
s = self._storage.storeBlob(
oid, serial, data, blobfilename, '', transaction)
self._modified.add(oid)
return s
def tpc_finish(self, transaction, func = lambda tid: None):
def invalidate_finish(tid):
self._base._invalidate_finish(self._modified, self)
func(tid)
del self._modified
self._storage.tpc_finish(transaction, invalidate_finish)
def read_only_writer(self, *a, **kw):
raise POSException.ReadOnlyError
class BeforeAdapterInstance(Base):
_copy_methods = Base._copy_methods + (
'loadSerial', 'tpc_begin', 'tpc_finish', 'tpc_abort', 'tpc_vote',
'checkCurrentSerialInTransaction',
)
def __init__(self, base, before=None):
Base.__init__(self, base._storage)
self._before = before
def isReadOnly(self):
return True
def supportsUndo(self):
return False
def release(self):
pass
close = release
def sync(self, force=True):
pass
def poll_invalidations(self):
return []
new_oid = pack = store = read_only_writer
def load(self, oid, version=''):
r = self._storage.loadBefore(oid, self._before)
if r is None:
raise POSException.POSKeyError(oid)
return r[:2]
class UndoAdapterInstance(Base):
_copy_methods = Base._copy_methods + (
'tpc_abort',
)
def __init__(self, base):
self._base = base
Base.__init__(self, base._storage)
def release(self):
pass
close = release
def tpc_begin(self, transaction):
self._storage.tpc_begin(transaction)
self._undone = set()
def undo(self, transaction_id, transaction):
result = self._storage.undo(transaction_id, transaction)
if result:
self._undone.update(result[1])
return result
def tpc_vote(self, transaction):
result = self._storage.tpc_vote(transaction)
if result:
self._undone.update(result[1])
def tpc_finish(self, transaction, func = lambda tid: None):
def invalidate_finish(tid):
self._base._invalidate_finish(self._undone, None)
func(tid)
self._storage.tpc_finish(transaction, invalidate_finish)
...@@ -74,7 +74,7 @@ You can't put blobs into a database that has uses a Non-Blob-Storage, though: ...@@ -74,7 +74,7 @@ You can't put blobs into a database that has uses a Non-Blob-Storage, though:
>>> transaction2.commit() # doctest: +ELLIPSIS >>> transaction2.commit() # doctest: +ELLIPSIS
Traceback (most recent call last): Traceback (most recent call last):
... ...
Unsupported: Storing Blobs in <ZODB.MappingStorage.MappingStorage object at ...> is not supported. Unsupported: Storing Blobs in ...
>>> transaction2.abort() >>> transaction2.abort()
>>> connection2.close() >>> connection2.close()
......
...@@ -27,6 +27,8 @@ from persistent import Persistent ...@@ -27,6 +27,8 @@ from persistent import Persistent
from zope.interface.verify import verifyObject from zope.interface.verify import verifyObject
from zope.testing import loggingsupport, renormalizing from zope.testing import loggingsupport, renormalizing
from .. import mvccadapter
checker = renormalizing.RENormalizing([ checker = renormalizing.RENormalizing([
# Python 3 bytes add a "b". # Python 3 bytes add a "b".
(re.compile("b('.*?')"), r"\1"), (re.compile("b('.*?')"), r"\1"),
...@@ -154,7 +156,8 @@ class ConnectionDotAdd(ZODB.tests.util.TestCase): ...@@ -154,7 +156,8 @@ class ConnectionDotAdd(ZODB.tests.util.TestCase):
self.datamgr.add(obj) self.datamgr.add(obj)
self.datamgr.tpc_begin(self.transaction) self.datamgr.tpc_begin(self.transaction)
self.datamgr.tpc_finish(self.transaction) self.datamgr.tpc_finish(self.transaction)
self.assertTrue(obj._p_oid not in self.datamgr._storage._stored) self.assertTrue(obj._p_oid not in
self.datamgr._storage._storage._stored)
def test__resetCacheResetsReader(self): def test__resetCacheResetsReader(self):
# https://bugs.launchpad.net/zodb/+bug/142667 # https://bugs.launchpad.net/zodb/+bug/142667
...@@ -435,8 +438,11 @@ class UserMethodTests(unittest.TestCase): ...@@ -435,8 +438,11 @@ class UserMethodTests(unittest.TestCase):
... ...
ConnectionStateError: The database connection is closed ConnectionStateError: The database connection is closed
>>> db.close()
An expedient way to create a read-only storage: An expedient way to create a read-only storage:
>>> db = databaseFromString("<zodb>\n<mappingstorage/>\n</zodb>")
>>> db.storage.isReadOnly = lambda: True >>> db.storage.isReadOnly = lambda: True
>>> cn = db.open() >>> cn = db.open()
>>> cn.isReadOnly() >>> cn.isReadOnly()
...@@ -510,7 +516,9 @@ class InvalidationTests(unittest.TestCase): ...@@ -510,7 +516,9 @@ class InvalidationTests(unittest.TestCase):
they have the expected effect. they have the expected effect.
>>> db = databaseFromString("<zodb>\n<mappingstorage/>\n</zodb>") >>> db = databaseFromString("<zodb>\n<mappingstorage/>\n</zodb>")
>>> mvcc_storage = db._mvcc_storage
>>> cn = db.open() >>> cn = db.open()
>>> mvcc_instance = cn._storage
>>> p1 = Persistent() >>> p1 = Persistent()
>>> p2 = Persistent() >>> p2 = Persistent()
>>> p3 = Persistent() >>> p3 = Persistent()
...@@ -521,29 +529,18 @@ class InvalidationTests(unittest.TestCase): ...@@ -521,29 +529,18 @@ class InvalidationTests(unittest.TestCase):
Transaction ids are 8-byte strings, just like oids; p64() will Transaction ids are 8-byte strings, just like oids; p64() will
create one from an int. create one from an int.
>>> cn.invalidate(p64(1), {p1._p_oid: 1}) >>> mvcc_storage.invalidate(p64(1), {p1._p_oid: 1})
Transaction start times are based on storage's last Transaction start times are based on storage's last
transaction. (Previousely, they were based on the first transaction. (Previousely, they were based on the first
invalidation seen in a transaction.) invalidation seen in a transaction.)
>>> cn._txn_time == p64(u64(db.storage.lastTransaction()) + 1) >>> mvcc_instance.poll_invalidations() == [p1._p_oid]
True True
>>> mvcc_instance._start == p64(u64(db.storage.lastTransaction()) + 1)
>>> p1._p_oid in cn._invalidated
True
>>> p2._p_oid in cn._invalidated
False
>>> cn.invalidate(p64(10), {p2._p_oid: 1, p64(76): 1})
>>> cn._txn_time == p64(u64(db.storage.lastTransaction()) + 1)
True True
>>> p1._p_oid in cn._invalidated >>> mvcc_storage.invalidate(p64(10), {p2._p_oid: 1, p64(76): 1})
True
>>> p2._p_oid in cn._invalidated
True
Calling invalidate() doesn't affect the object state until Calling invalidate() doesn't affect the object state until
a transaction boundary. a transaction boundary.
...@@ -560,24 +557,24 @@ class InvalidationTests(unittest.TestCase): ...@@ -560,24 +557,24 @@ class InvalidationTests(unittest.TestCase):
>>> cn.sync() >>> cn.sync()
>>> p1._p_state >>> p1._p_state
-1 0
>>> p2._p_state >>> p2._p_state
-1 -1
>>> p3._p_state >>> p3._p_state
0 0
>>> cn._invalidated
set([])
>>> db.close() >>> db.close()
""" """
def doctest_invalidateCache(): def doctest_invalidateCache():
"""The invalidateCache method invalidates a connection's cache. It also """The invalidateCache method invalidates a connection's cache.
prevents reads until the end of a transaction::
It also prevents reads until the end of a transaction::
>>> from ZODB.tests.util import DB >>> from ZODB.tests.util import DB
>>> import transaction >>> import transaction
>>> db = DB() >>> db = DB()
>>> mvcc_storage = db._mvcc_storage
>>> tm = transaction.TransactionManager() >>> tm = transaction.TransactionManager()
>>> connection = db.open(transaction_manager=tm) >>> connection = db.open(transaction_manager=tm)
>>> connection.root()['a'] = StubObject() >>> connection.root()['a'] = StubObject()
...@@ -593,53 +590,33 @@ def doctest_invalidateCache(): ...@@ -593,53 +590,33 @@ def doctest_invalidateCache():
So we have a connection and an active transaction with some modifications. So we have a connection and an active transaction with some modifications.
Lets call invalidateCache: Lets call invalidateCache:
>>> connection.invalidateCache() >>> mvcc_storage.invalidateCache()
Now, if we try to load an object, we'll get a read conflict:
>>> connection.root()['b'].x
Traceback (most recent call last):
...
ReadConflictError: database read conflict error
If we try to commit the transaction, we'll get a conflict error:
>>> tm.commit() This won't have any effect until the next transaction:
Traceback (most recent call last):
...
ConflictError: database conflict error
and the cache will have been cleared: >>> connection.root()['a']._p_changed
0
>>> connection.root()['b']._p_changed
>>> connection.root()['c']._p_changed
1
>>> print(connection.root()['a']._p_changed) But if we sync():
None
>>> print(connection.root()['b']._p_changed)
None
>>> print(connection.root()['c']._p_changed)
None
But we'll be able to access data again: >>> connection.sync()
>>> connection.root()['b'].x All of our data was invalidated:
1
Aborting a transaction after a read conflict also lets us read data and go >>> connection.root()['a']._p_changed
on about our business: >>> connection.root()['b']._p_changed
>>> connection.root()['c']._p_changed
>>> connection.invalidateCache() But we can load data as usual:
>>> connection.root()['c'].x Now, if we try to load an object, we'll get a read conflict:
Traceback (most recent call last):
...
ReadConflictError: database read conflict error
>>> tm.abort() >>> connection.root()['b'].x
>>> connection.root()['c'].x
1 1
>>> connection.root()['c'].x = 2
>>> tm.commit()
>>> db.close() >>> db.close()
""" """
...@@ -1333,6 +1310,7 @@ class StubDatabase: ...@@ -1333,6 +1310,7 @@ class StubDatabase:
def __init__(self): def __init__(self):
self.storage = StubStorage() self.storage = StubStorage()
self._mvcc_storage = mvccadapter.MVCCAdapter(self.storage)
self.new_oid = self.storage.new_oid self.new_oid = self.storage.new_oid
classFactory = None classFactory = None
......
...@@ -83,34 +83,40 @@ def test_invalidateCache(): ...@@ -83,34 +83,40 @@ def test_invalidateCache():
>>> from ZODB.tests.util import DB >>> from ZODB.tests.util import DB
>>> import transaction >>> import transaction
>>> db = DB() >>> db = DB()
>>> mvcc_storage = db._mvcc_storage
>>> tm1 = transaction.TransactionManager() >>> tm1 = transaction.TransactionManager()
>>> c1 = db.open(transaction_manager=tm1) >>> c1 = db.open(transaction_manager=tm1)
>>> c1.root()['a'] = MinPO(1) >>> c1.root()['a'] = MinPO(1)
>>> tm1.commit() >>> tm1.commit()
>>> tm2 = transaction.TransactionManager() >>> tm2 = transaction.TransactionManager()
>>> c2 = db.open(transaction_manager=tm2) >>> c2 = db.open(transaction_manager=tm2)
>>> c1.root()['a']._p_deactivate() >>> c2.root()['a'].value
1
>>> tm3 = transaction.TransactionManager() >>> tm3 = transaction.TransactionManager()
>>> c3 = db.open(transaction_manager=tm3) >>> c3 = db.open(transaction_manager=tm3)
>>> c3.root()['a'].value >>> c3.root()['a'].value
1 1
>>> c3.close() >>> c3.close()
>>> db.invalidateCache()
>>> c1.root()['a'].value
Traceback (most recent call last):
...
ReadConflictError: database read conflict error
>>> c2.root()['a'].value
Traceback (most recent call last):
...
ReadConflictError: database read conflict error
>>> mvcc_storage.invalidateCache()
>>> c1.root.a._p_changed
0
>>> c1.sync()
>>> c1.root.a._p_changed
>>> c2.root.a._p_changed
0
>>> c2.sync()
>>> c2.root.a._p_changed
>>> c3 is db.open(transaction_manager=tm3) >>> c3 is db.open(transaction_manager=tm3)
True True
>>> print(c3.root()['a']._p_changed) >>> c3.root.a._p_changed
None
>>> c1.root()['a'].value
1
>>> c2.root()['a'].value
1
>>> c3.root()['a'].value
1
>>> db.close() >>> db.close()
""" """
......
...@@ -547,14 +547,13 @@ def loadblob_tmpstore(): ...@@ -547,14 +547,13 @@ def loadblob_tmpstore():
>>> transaction.commit() >>> transaction.commit()
>>> blob_oid = root['blob']._p_oid >>> blob_oid = root['blob']._p_oid
>>> tid = connection._storage.lastTransaction() >>> tid = connection._storage.lastTransaction()
>>> _txn_time = connection._txn_time
Now we open a database with a TmpStore in front: Now we open a database with a TmpStore in front:
>>> database.close() >>> database.close()
>>> from ZODB.Connection import TmpStore >>> from ZODB.Connection import TmpStore
>>> tmpstore = TmpStore(blob_storage, _txn_time) >>> tmpstore = TmpStore(blob_storage)
We can access the blob correctly: We can access the blob correctly:
......
...@@ -68,9 +68,9 @@ Now open a second connection. ...@@ -68,9 +68,9 @@ Now open a second connection.
>>> tm2 = transaction.TransactionManager() >>> tm2 = transaction.TransactionManager()
>>> cn2 = db.open(transaction_manager=tm2) >>> cn2 = db.open(transaction_manager=tm2)
>>> from ZODB.utils import p64, u64 >>> from ZODB.utils import p64, u64
>>> cn2._txn_time == p64(u64(st.lastTransaction()) + 1) >>> cn2._storage._start == p64(u64(st.lastTransaction()) + 1)
True True
>>> txn_time2 = cn2._txn_time >>> txn_time2 = cn2._storage._start
Connection high-water mark Connection high-water mark
-------------------------- --------------------------
...@@ -85,13 +85,13 @@ storage has seen. ...@@ -85,13 +85,13 @@ storage has seen.
>>> cn = db.open() >>> cn = db.open()
>>> cn._txn_time == p64(u64(st.lastTransaction()) + 1) >>> cn._storage._start == p64(u64(st.lastTransaction()) + 1)
True True
>>> cn.invalidate(100, dict.fromkeys([1, 2])) >>> cn.db()._mvcc_storage.invalidate(100, dict.fromkeys([1, 2]))
>>> cn._txn_time == p64(u64(st.lastTransaction()) + 1) >>> cn._storage._start == p64(u64(st.lastTransaction()) + 1)
True True
>>> cn.invalidate(200, dict.fromkeys([1, 2])) >>> cn.db()._mvcc_storage.invalidate(200, dict.fromkeys([1, 2]))
>>> cn._txn_time == p64(u64(st.lastTransaction()) + 1) >>> cn._storage._start == p64(u64(st.lastTransaction()) + 1)
True True
A connection's high-water mark is set to the transaction id taken from A connection's high-water mark is set to the transaction id taken from
...@@ -105,7 +105,7 @@ but that doesn't work unless an object is modified. sync() will abort ...@@ -105,7 +105,7 @@ but that doesn't work unless an object is modified. sync() will abort
a transaction and process invalidations. a transaction and process invalidations.
>>> cn.sync() >>> cn.sync()
>>> cn._txn_time == p64(u64(st.lastTransaction()) + 1) >>> cn._storage._start == p64(u64(st.lastTransaction()) + 1)
True True
Basic functionality Basic functionality
...@@ -121,14 +121,14 @@ will modify "a." The other transaction will then modify "b" and commit. ...@@ -121,14 +121,14 @@ will modify "a." The other transaction will then modify "b" and commit.
The second connection already has its high-water mark set. The second connection already has its high-water mark set.
>>> cn2._txn_time == txn_time2 >>> cn2._storage._start == txn_time2
True True
It is safe to read "b," because it was not modified by the concurrent It is safe to read "b," because it was not modified by the concurrent
transaction. transaction.
>>> r2 = cn2.root() >>> r2 = cn2.root()
>>> r2["b"]._p_serial < cn2._txn_time >>> r2["b"]._p_serial < cn2._storage._start
True True
>>> r2["b"].value >>> r2["b"].value
1 1
...@@ -140,7 +140,7 @@ non-current version. ...@@ -140,7 +140,7 @@ non-current version.
>>> r2["a"].value >>> r2["a"].value
1 1
>>> r2["a"]._p_serial < cn2._txn_time >>> r2["a"]._p_serial < cn2._storage._start
True True
We can confirm that we have a non-current revision by asking the We can confirm that we have a non-current revision by asking the
...@@ -153,32 +153,32 @@ It's possible to modify "a", but we get a conflict error when we ...@@ -153,32 +153,32 @@ It's possible to modify "a", but we get a conflict error when we
commit the transaction. commit the transaction.
>>> r2["a"].value = 3 >>> r2["a"].value = 3
>>> tm2.get().commit() >>> tm2.get().commit() # doctest: +ELLIPSIS
Traceback (most recent call last): Traceback (most recent call last):
... ...
ConflictError: database conflict error (oid 0x01, class ZODB.tests.MinPO.MinPO) ConflictError: database conflict error (oid 0x01, class ZODB.tests.MinPO...
>>> tm2.get().abort() >>> tm2.get().abort()
This example will demonstrate that we can commit a transaction if we only This example will demonstrate that we can commit a transaction if we only
modify current revisions. modify current revisions.
>>> cn2._txn_time == p64(u64(st.lastTransaction()) + 1) >>> cn2._storage._start == p64(u64(st.lastTransaction()) + 1)
True True
>>> txn_time2 = cn2._txn_time >>> txn_time2 = cn2._storage._start
>>> r1 = cn1.root() >>> r1 = cn1.root()
>>> r1["a"].value = 3 >>> r1["a"].value = 3
>>> tm1.get().commit() >>> tm1.get().commit()
>>> txn = db.lastTransaction() >>> txn = db.lastTransaction()
>>> cn2._txn_time == txn_time2 >>> cn2._storage._start == txn_time2
True True
>>> r2["b"].value = r2["a"].value + 1 >>> r2["b"].value = r2["a"].value + 1
>>> r2["b"].value >>> r2["b"].value
3 3
>>> tm2.get().commit() >>> tm2.get().commit()
>>> cn2._txn_time == p64(u64(st.lastTransaction()) + 1) >>> cn2._storage._start == p64(u64(st.lastTransaction()) + 1)
True True
Object cache Object cache
...@@ -362,18 +362,18 @@ This test is kinda screwy because it depends on an old approach that ...@@ -362,18 +362,18 @@ This test is kinda screwy because it depends on an old approach that
has changed. We'll hack the _txn_time to get the original expected has changed. We'll hack the _txn_time to get the original expected
result, even though what's going on now is much simpler. result, even though what's going on now is much simpler.
>>> cn1._txn_time = ts.lastTransaction() >>> cn1._storage._start = ts.lastTransaction()
Once the oid is hooked, an invalidation will be delivered the next Once the oid is hooked, an invalidation will be delivered the next
time it is activated. The code below activates the object, then time it is activated. The code below activates the object, then
confirms that the hook worked and that the old state was retrieved. confirms that the hook worked and that the old state was retrieved.
>>> oid in cn1._invalidated >>> oid in cn1._storage._invalidations
False False
>>> r1["b"]._p_state >>> r1["b"]._p_state
-1 -1
>>> r1["b"]._p_activate() >>> r1["b"]._p_activate()
>>> oid in cn1._invalidated >>> oid in cn1._storage._invalidations
True True
>>> ts.count >>> ts.count
1 1
...@@ -406,15 +406,15 @@ Again, once the oid is hooked, an invalidation will be delivered the next ...@@ -406,15 +406,15 @@ Again, once the oid is hooked, an invalidation will be delivered the next
time it is activated. The code below activates the object, but unlike the time it is activated. The code below activates the object, but unlike the
section above, this is no older state to retrieve. section above, this is no older state to retrieve.
>>> oid in cn1._invalidated >>> oid in cn1._storage._invalidations
False False
>>> r1["b"]._p_state >>> r1["b"]._p_state
-1 -1
>>> cn1._txn_time = ts.lastTransaction() >>> cn1._storage._start = ts.lastTransaction()
>>> r1["b"]._p_activate() >>> r1["b"]._p_activate() # doctest: +ELLIPSIS
Traceback (most recent call last): Traceback (most recent call last):
... ...
ReadConflictError: database read conflict error ReadConflictError: ...
>>> db.close() >>> db.close()
""" """
......
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