Commit b0f992fd authored by Jim Fulton's avatar Jim Fulton

Removed the mvcc option. Everybody wants mvcc and removing us lets us

simplify the code a little. (We'll be able to simplify more when we
stop supporting versions.)

Suppress version-deprecation warnings for tests.
parent 05634cca
...@@ -89,7 +89,6 @@ class Connection(ExportImport, object): ...@@ -89,7 +89,6 @@ class Connection(ExportImport, object):
self.connections = {self._db.database_name: self} self.connections = {self._db.database_name: self}
self._synch = None self._synch = None
self._mvcc = None
self._version = version self._version = version
self._normal_storage = self._storage = db._storage self._normal_storage = self._storage = db._storage
...@@ -348,7 +347,7 @@ class Connection(ExportImport, object): ...@@ -348,7 +347,7 @@ class Connection(ExportImport, object):
if connection is None: if connection is None:
new_con = self._db.databases[database_name].open( new_con = self._db.databases[database_name].open(
transaction_manager=self.transaction_manager, transaction_manager=self.transaction_manager,
mvcc=self._mvcc, version=self._version, synch=self._synch, version=self._version, synch=self._synch,
) )
self.connections.update(new_con.connections) self.connections.update(new_con.connections)
new_con.connections = self.connections new_con.connections = self.connections
...@@ -871,7 +870,7 @@ class Connection(ExportImport, object): ...@@ -871,7 +870,7 @@ class Connection(ExportImport, object):
def _load_before_or_conflict(self, obj): def _load_before_or_conflict(self, obj):
"""Load non-current state for obj or raise ReadConflictError.""" """Load non-current state for obj or raise ReadConflictError."""
if not (self._mvcc and self._setstate_noncurrent(obj)): if not ((not self._version) and self._setstate_noncurrent(obj)):
self._register(obj) self._register(obj)
self._conflicts[obj._p_oid] = True self._conflicts[obj._p_oid] = True
raise ReadConflictError(object=obj) raise ReadConflictError(object=obj)
...@@ -971,8 +970,7 @@ class Connection(ExportImport, object): ...@@ -971,8 +970,7 @@ class Connection(ExportImport, object):
# return a list of [ghosts....not recently used.....recently used] # return a list of [ghosts....not recently used.....recently used]
return everything.items() + items return everything.items() + items
def open(self, transaction_manager=None, mvcc=True, synch=True, def open(self, transaction_manager=None, synch=True, delegate=True):
delegate=True):
"""Register odb, the DB that this Connection uses. """Register odb, the DB that this Connection uses.
This method is called by the DB every time a Connection This method is called by the DB every time a Connection
...@@ -984,13 +982,11 @@ class Connection(ExportImport, object): ...@@ -984,13 +982,11 @@ class Connection(ExportImport, object):
Parameters: Parameters:
odb: database that owns the Connection odb: database that owns the Connection
mvcc: boolean indicating whether MVCC is enabled
transaction_manager: transaction manager to use. None means transaction_manager: transaction manager to use. None means
use the default transaction manager. use the default transaction manager.
synch: boolean indicating whether Connection should synch: boolean indicating whether Connection should
register for afterCompletion() calls. register for afterCompletion() calls.
""" """
# TODO: Why do we go to all the trouble of setting _db and # TODO: Why do we go to all the trouble of setting _db and
# other attributes on open and clearing them on close? # other attributes on open and clearing them on close?
# A Connection is only ever associated with a single DB # A Connection is only ever associated with a single DB
...@@ -998,7 +994,6 @@ class Connection(ExportImport, object): ...@@ -998,7 +994,6 @@ class Connection(ExportImport, object):
self._opened = time() self._opened = time()
self._synch = synch self._synch = synch
self._mvcc = mvcc and not self._version
if transaction_manager is None: if transaction_manager is None:
transaction_manager = transaction.manager transaction_manager = transaction.manager
...@@ -1021,7 +1016,7 @@ class Connection(ExportImport, object): ...@@ -1021,7 +1016,7 @@ class Connection(ExportImport, object):
# delegate open to secondary connections # delegate open to secondary connections
for connection in self.connections.values(): for connection in self.connections.values():
if connection is not self: if connection is not self:
connection.open(transaction_manager, mvcc, synch, False) connection.open(transaction_manager, synch, False)
def _resetCache(self): def _resetCache(self):
"""Creates a new cache, discarding the old one. """Creates a new cache, discarding the old one.
......
...@@ -554,8 +554,7 @@ class DB(object): ...@@ -554,8 +554,7 @@ class DB(object):
def objectCount(self): def objectCount(self):
return len(self._storage) return len(self._storage)
def open(self, version='', mvcc=True, def open(self, version='', transaction_manager=None, synch=True):
transaction_manager=None, synch=True):
"""Return a database Connection for use by application code. """Return a database Connection for use by application code.
The optional `version` argument can be used to specify that a The optional `version` argument can be used to specify that a
...@@ -568,7 +567,6 @@ class DB(object): ...@@ -568,7 +567,6 @@ class DB(object):
:Parameters: :Parameters:
- `version`: the "version" that all changes will be made - `version`: the "version" that all changes will be made
in, defaults to no version. in, defaults to no version.
- `mvcc`: boolean indicating whether MVCC is enabled
- `transaction_manager`: transaction manager to use. None means - `transaction_manager`: transaction manager to use. None means
use the default transaction manager. use the default transaction manager.
- `synch`: boolean indicating whether Connection should - `synch`: boolean indicating whether Connection should
...@@ -609,7 +607,7 @@ class DB(object): ...@@ -609,7 +607,7 @@ class DB(object):
assert result is not None assert result is not None
# Tell the connection it belongs to self. # Tell the connection it belongs to self.
result.open(transaction_manager, mvcc, synch) result.open(transaction_manager, synch)
# A good time to do some cache cleanup. # A good time to do some cache cleanup.
self._connectionMap(lambda c: c.cacheGC()) self._connectionMap(lambda c: c.cacheGC())
......
...@@ -16,6 +16,7 @@ import warnings ...@@ -16,6 +16,7 @@ import warnings
import ZODB import ZODB
import ZODB.FileStorage import ZODB.FileStorage
import ZODB.MappingStorage
from ZODB.POSException import ReadConflictError, ConflictError from ZODB.POSException import ReadConflictError, ConflictError
from ZODB.POSException import TransactionFailedError from ZODB.POSException import TransactionFailedError
from ZODB.tests.warnhook import WarningsHook from ZODB.tests.warnhook import WarningsHook
...@@ -29,6 +30,9 @@ import transaction ...@@ -29,6 +30,9 @@ import transaction
warnings.filterwarnings("ignore", warnings.filterwarnings("ignore",
".*\nsubtransactions are deprecated", ".*\nsubtransactions are deprecated",
DeprecationWarning, __name__) DeprecationWarning, __name__)
warnings.filterwarnings("ignore",
"Versions are deprecated",
DeprecationWarning, __name__)
class P(Persistent): class P(Persistent):
pass pass
...@@ -213,114 +217,6 @@ class ZODBTests(unittest.TestCase): ...@@ -213,114 +217,6 @@ class ZODBTests(unittest.TestCase):
conn1.close() conn1.close()
conn2.close() conn2.close()
def checkReadConflict(self):
self.obj = P()
self.readConflict()
def readConflict(self, shouldFail=True):
# Two transactions run concurrently. Each reads some object,
# then one commits and the other tries to read an object
# modified by the first. This read should fail with a conflict
# error because the object state read is not necessarily
# consistent with the objects read earlier in the transaction.
tm1 = transaction.TransactionManager()
conn = self._db.open(mvcc=False, transaction_manager=tm1)
r1 = conn.root()
r1["p"] = self.obj
self.obj.child1 = P()
tm1.get().commit()
# start a new transaction with a new connection
tm2 = transaction.TransactionManager()
cn2 = self._db.open(mvcc=False, transaction_manager=tm2)
# start a new transaction with the other connection
r2 = cn2.root()
self.assertEqual(r1._p_serial, r2._p_serial)
self.obj.child2 = P()
tm1.get().commit()
# resume the transaction using cn2
obj = r2["p"]
# An attempt to access obj should fail, because r2 was read
# earlier in the transaction and obj was modified by the othe
# transaction.
if shouldFail:
self.assertRaises(ReadConflictError, lambda: obj.child1)
# And since ReadConflictError was raised, attempting to commit
# the transaction should re-raise it. checkNotIndependent()
# failed this part of the test for a long time.
self.assertRaises(ReadConflictError, tm2.get().commit)
# And since that commit failed, trying to commit again should
# fail again.
self.assertRaises(TransactionFailedError, tm2.get().commit)
# And again.
self.assertRaises(TransactionFailedError, tm2.get().commit)
# Etc.
self.assertRaises(TransactionFailedError, tm2.get().commit)
else:
# make sure that accessing the object succeeds
obj.child1
tm2.get().abort()
def checkReadConflictIgnored(self):
# Test that an application that catches a read conflict and
# continues can not commit the transaction later.
root = self._db.open(mvcc=False).root()
root["real_data"] = real_data = PersistentMapping()
root["index"] = index = PersistentMapping()
real_data["a"] = PersistentMapping({"indexed_value": 0})
real_data["b"] = PersistentMapping({"indexed_value": 1})
index[1] = PersistentMapping({"b": 1})
index[0] = PersistentMapping({"a": 1})
transaction.commit()
# load some objects from one connection
tm = transaction.TransactionManager()
cn2 = self._db.open(mvcc=False, transaction_manager=tm)
r2 = cn2.root()
real_data2 = r2["real_data"]
index2 = r2["index"]
real_data["b"]["indexed_value"] = 0
del index[1]["b"]
index[0]["b"] = 1
transaction.commit()
del real_data2["a"]
try:
del index2[0]["a"]
except ReadConflictError:
# This is the crux of the text. Ignore the error.
pass
else:
self.fail("No conflict occurred")
# real_data2 still ready to commit
self.assert_(real_data2._p_changed)
# index2 values not ready to commit
self.assert_(not index2._p_changed)
self.assert_(not index2[0]._p_changed)
self.assert_(not index2[1]._p_changed)
self.assertRaises(ReadConflictError, tm.get().commit)
self.assertRaises(TransactionFailedError, tm.get().commit)
tm.get().abort()
def checkIndependent(self):
self.obj = Independent()
self.readConflict(shouldFail=False)
def checkNotIndependent(self):
self.obj = DecoyIndependent()
self.readConflict()
def checkSubtxnCommitDoesntGetInvalidations(self): def checkSubtxnCommitDoesntGetInvalidations(self):
# Prior to ZODB 3.2.9 and 3.4, Connection.tpc_finish() processed # Prior to ZODB 3.2.9 and 3.4, Connection.tpc_finish() processed
# invalidations even for a subtxn commit. This could make # invalidations even for a subtxn commit. This could make
...@@ -451,49 +347,6 @@ class ZODBTests(unittest.TestCase): ...@@ -451,49 +347,6 @@ class ZODBTests(unittest.TestCase):
finally: finally:
tm1.abort() tm1.abort()
def checkReadConflictErrorClearedDuringAbort(self):
# When a transaction is aborted, the "memory" of which
# objects were the cause of a ReadConflictError during
# that transaction should be cleared.
root = self._db.open(mvcc=False).root()
data = PersistentMapping({'d': 1})
root["data"] = data
transaction.commit()
# Provoke a ReadConflictError.
tm2 = transaction.TransactionManager()
cn2 = self._db.open(mvcc=False, transaction_manager=tm2)
r2 = cn2.root()
data2 = r2["data"]
data['d'] = 2
transaction.commit()
try:
data2['d'] = 3
except ReadConflictError:
pass
else:
self.fail("No conflict occurred")
# Explicitly abort cn2's transaction.
tm2.get().abort()
# cn2 should retain no memory of the read conflict after an abort(),
# but 3.2.3 had a bug wherein it did.
data_conflicts = data._p_jar._conflicts
data2_conflicts = data2._p_jar._conflicts
self.failIf(data_conflicts)
self.failIf(data2_conflicts) # this used to fail
# And because of that, we still couldn't commit a change to data2['d']
# in the new transaction.
cn2.sync() # process the invalidation for data2['d']
data2['d'] = 3
tm2.get().commit() # 3.2.3 used to raise ReadConflictError
cn2.close()
def checkTxnBeginImpliesAbort(self): def checkTxnBeginImpliesAbort(self):
# begin() should do an abort() first, if needed. # begin() should do an abort() first, if needed.
cn = self._db.open() cn = self._db.open()
...@@ -762,7 +615,162 @@ class ZODBTests(unittest.TestCase): ...@@ -762,7 +615,162 @@ class ZODBTests(unittest.TestCase):
transaction.abort() transaction.abort()
conn.close() conn.close()
class ReadConflictTests(unittest.TestCase):
def setUp(self):
self._storage = ZODB.MappingStorage.MappingStorage()
def readConflict(self, shouldFail=True):
# Two transactions run concurrently. Each reads some object,
# then one commits and the other tries to read an object
# modified by the first. This read should fail with a conflict
# error because the object state read is not necessarily
# consistent with the objects read earlier in the transaction.
tm1 = transaction.TransactionManager()
conn = self._db.open(transaction_manager=tm1)
r1 = conn.root()
r1["p"] = self.obj
self.obj.child1 = P()
tm1.get().commit()
# start a new transaction with a new connection
tm2 = transaction.TransactionManager()
cn2 = self._db.open(transaction_manager=tm2)
# start a new transaction with the other connection
r2 = cn2.root()
self.assertEqual(r1._p_serial, r2._p_serial)
self.obj.child2 = P()
tm1.get().commit()
# resume the transaction using cn2
obj = r2["p"]
# An attempt to access obj should fail, because r2 was read
# earlier in the transaction and obj was modified by the othe
# transaction.
if shouldFail:
self.assertRaises(ReadConflictError, lambda: obj.child1)
# And since ReadConflictError was raised, attempting to commit
# the transaction should re-raise it. checkNotIndependent()
# failed this part of the test for a long time.
self.assertRaises(ReadConflictError, tm2.get().commit)
# And since that commit failed, trying to commit again should
# fail again.
self.assertRaises(TransactionFailedError, tm2.get().commit)
# And again.
self.assertRaises(TransactionFailedError, tm2.get().commit)
# Etc.
self.assertRaises(TransactionFailedError, tm2.get().commit)
else:
# make sure that accessing the object succeeds
obj.child1
tm2.get().abort()
def checkReadConflict(self):
self.obj = P()
self.readConflict()
def checkIndependent(self):
self.obj = Independent()
self.readConflict(shouldFail=False)
def checkNotIndependent(self):
self.obj = DecoyIndependent()
self.readConflict()
def checkReadConflictIgnored(self):
# Test that an application that catches a read conflict and
# continues can not commit the transaction later.
root = self._db.open().root()
root["real_data"] = real_data = PersistentMapping()
root["index"] = index = PersistentMapping()
real_data["a"] = PersistentMapping({"indexed_value": 0})
real_data["b"] = PersistentMapping({"indexed_value": 1})
index[1] = PersistentMapping({"b": 1})
index[0] = PersistentMapping({"a": 1})
transaction.commit()
# load some objects from one connection
tm = transaction.TransactionManager()
cn2 = self._db.open(transaction_manager=tm)
r2 = cn2.root()
real_data2 = r2["real_data"]
index2 = r2["index"]
real_data["b"]["indexed_value"] = 0
del index[1]["b"]
index[0]["b"] = 1
transaction.commit()
del real_data2["a"]
try:
del index2[0]["a"]
except ReadConflictError:
# This is the crux of the text. Ignore the error.
pass
else:
self.fail("No conflict occurred")
# real_data2 still ready to commit
self.assert_(real_data2._p_changed)
# index2 values not ready to commit
self.assert_(not index2._p_changed)
self.assert_(not index2[0]._p_changed)
self.assert_(not index2[1]._p_changed)
self.assertRaises(ReadConflictError, tm.get().commit)
self.assertRaises(TransactionFailedError, tm.get().commit)
tm.get().abort()
def checkReadConflictErrorClearedDuringAbort(self):
# When a transaction is aborted, the "memory" of which
# objects were the cause of a ReadConflictError during
# that transaction should be cleared.
root = self._db.open().root()
data = PersistentMapping({'d': 1})
root["data"] = data
transaction.commit()
# Provoke a ReadConflictError.
tm2 = transaction.TransactionManager()
cn2 = self._db.open(transaction_manager=tm2)
r2 = cn2.root()
data2 = r2["data"]
data['d'] = 2
transaction.commit()
try:
data2['d'] = 3
except ReadConflictError:
pass
else:
self.fail("No conflict occurred")
# Explicitly abort cn2's transaction.
tm2.get().abort()
# cn2 should retain no memory of the read conflict after an abort(),
# but 3.2.3 had a bug wherein it did.
data_conflicts = data._p_jar._conflicts
data2_conflicts = data2._p_jar._conflicts
self.failIf(data_conflicts)
self.failIf(data2_conflicts) # this used to fail
# And because of that, we still couldn't commit a change to data2['d']
# in the new transaction.
cn2.sync() # process the invalidation for data2['d']
data2['d'] = 3
tm2.get().commit() # 3.2.3 used to raise ReadConflictError
cn2.close()
class PoisonedError(Exception): class PoisonedError(Exception):
pass pass
......
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