Commit b053b2a8 authored by Jim Fulton's avatar Jim Fulton

New Feature:

When transactions are aborted, new object ids allocated during the
  transaction are saved and used in subsequent transactions. This can
  help in situations where object ids are used as BTree keys and the
  sequential allocation of object ids leads to conflict errors.
parent 25cad82a
...@@ -2,6 +2,17 @@ ...@@ -2,6 +2,17 @@
Change History Change History
================ ================
3.10.0a2 (2010-??-??)
=====================
New Features
------------
- When transactions are aborted, new object ids allocated during the
transaction are saved and used in subsequent transactions. This can
help in situations where object ids are used as BTree keys and the
sequential allocation of object ids leads to conflict errors.
3.10.0a1 (2010-02-08) 3.10.0a1 (2010-02-08)
===================== =====================
......
...@@ -104,7 +104,7 @@ class Connection(ExportImport, object): ...@@ -104,7 +104,7 @@ class Connection(ExportImport, object):
self._mvcc_storage = False self._mvcc_storage = False
self._normal_storage = self._storage = storage self._normal_storage = self._storage = storage
self.new_oid = storage.new_oid self.new_oid = db.new_oid
self._savepoint_storage = None self._savepoint_storage = None
# Do we need to join a txn manager? # Do we need to join a txn manager?
...@@ -214,7 +214,7 @@ class Connection(ExportImport, object): ...@@ -214,7 +214,7 @@ class Connection(ExportImport, object):
" added to a Connection.", obj) " added to a Connection.", obj)
elif obj._p_jar is None: elif obj._p_jar is None:
assert obj._p_oid is None assert obj._p_oid is None
oid = obj._p_oid = self._storage.new_oid() oid = obj._p_oid = self.new_oid()
obj._p_jar = self obj._p_jar = self
if self._added_during_commit is not None: if self._added_during_commit is not None:
self._added_during_commit.append(obj) self._added_during_commit.append(obj)
...@@ -426,6 +426,7 @@ class Connection(ExportImport, object): ...@@ -426,6 +426,7 @@ class Connection(ExportImport, object):
if self._savepoint_storage is not None: if self._savepoint_storage is not None:
self._abort_savepoint() self._abort_savepoint()
self._invalidate_creating()
self._tpc_cleanup() self._tpc_cleanup()
def _abort(self): def _abort(self):
...@@ -438,6 +439,7 @@ class Connection(ExportImport, object): ...@@ -438,6 +439,7 @@ class Connection(ExportImport, object):
del self._added[oid] del self._added[oid]
del obj._p_jar del obj._p_jar
del obj._p_oid del obj._p_oid
self._db.save_oid(oid)
else: else:
# Note: If we invalidate a non-ghostifiable object # Note: If we invalidate a non-ghostifiable object
...@@ -723,6 +725,7 @@ class Connection(ExportImport, object): ...@@ -723,6 +725,7 @@ class Connection(ExportImport, object):
self._creating = {} self._creating = {}
for oid in creating: for oid in creating:
self._db.save_oid(oid)
o = self._cache.get(oid) o = self._cache.get(oid)
if o is not None: if o is not None:
del self._cache[oid] del self._cache[oid]
......
...@@ -384,7 +384,9 @@ class DB(object): ...@@ -384,7 +384,9 @@ class DB(object):
historical_timeout=300, historical_timeout=300,
database_name='unnamed', database_name='unnamed',
databases=None, databases=None,
xrefs=True): xrefs=True,
max_saved_oids=999,
):
"""Create an object database. """Create an object database.
:Parameters: :Parameters:
...@@ -480,6 +482,9 @@ class DB(object): ...@@ -480,6 +482,9 @@ class DB(object):
self._setupUndoMethods() self._setupUndoMethods()
self.history = storage.history self.history = storage.history
self._saved_oids = []
self._max_saved_oids = max_saved_oids
def _setupUndoMethods(self): def _setupUndoMethods(self):
storage = self.storage storage = self.storage
try: try:
...@@ -942,6 +947,19 @@ class DB(object): ...@@ -942,6 +947,19 @@ class DB(object):
return ContextManager(self) return ContextManager(self)
def save_oid(self, oid):
if len(self._saved_oids) < self._max_saved_oids:
self._saved_oids.append(oid)
def new_oid(self):
if self._saved_oids:
try:
return self._saved_oids.pop()
except IndexError:
pass # Hm, threads
return self.storage.new_oid()
class ContextManager: class ContextManager:
"""PEP 343 context manager """PEP 343 context manager
""" """
......
New OIDs get reused if a transaction aborts
===========================================
Historical note:
An OID is a terrible thing to waste.
Seriously: sequential allocation of OIDs could cause problems when
OIDs are used as (or as the basis of) BTree keys. This happened
with Zope 3's intid utility where the object->id mapping uses an
object key based on the OID. We got frequent conflict errors
because, in a site with many users, many objects are added at the
same time and conficts happened when conflicting changes caused
bucket splits.
Reusing an earlier allocated, but discarded OID will allow retries
of transactions to work because they'll use earlier OIDs which won't
tend to conflict with newly allocated ones.
If a transaction is aborted, new OIDs assigned in the transaction are
saved and made available for later transactions.
>>> import ZODB.tests.util, transaction
>>> db = ZODB.tests.util.DB()
>>> tm1 = transaction.TransactionManager()
>>> conn1 = db.open(tm1)
>>> conn1.root.x = ZODB.tests.util.P()
>>> tm1.commit()
>>> conn1.root.x.x = ZODB.tests.util.P()
>>> conn1.root.y = 1
>>> tm2 = transaction.TransactionManager()
>>> conn2 = db.open(tm2)
>>> conn2.root.y = ZODB.tests.util.P()
>>> tm2.commit()
We get a conflict when we try to commit the change to the first connection:
>>> tm1.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ConflictError: ...
>>> tm1.abort()
When we try, we get the same oid we would have gotten on the first transaction:
>>> conn1.root.x.x = ZODB.tests.util.P()
>>> tm1.commit()
>>> conn1.root.x.x._p_oid
'\x00\x00\x00\x00\x00\x00\x00\x03'
We see this more clearly when we use add to assign the oid before the commit:
>>> conn1.root.z = ZODB.tests.util.P()
>>> conn1.add(conn1.root.z)
>>> conn1.root.z._p_oid
'\x00\x00\x00\x00\x00\x00\x00\x04'
>>> tm1.abort()
>>> conn2.root.a = ZODB.tests.util.P()
>>> conn2.add(conn2.root.a)
>>> conn2.root.a._p_oid
'\x00\x00\x00\x00\x00\x00\x00\x04'
...@@ -815,6 +815,7 @@ class StubDatabase: ...@@ -815,6 +815,7 @@ class StubDatabase:
def __init__(self): def __init__(self):
self.storage = StubStorage() self.storage = StubStorage()
self.new_oid = self.storage.new_oid
classFactory = None classFactory = None
database_name = 'stubdatabase' database_name = 'stubdatabase'
...@@ -823,6 +824,8 @@ class StubDatabase: ...@@ -823,6 +824,8 @@ class StubDatabase:
def invalidate(self, transaction, dict_with_oid_keys, connection): def invalidate(self, transaction, dict_with_oid_keys, connection):
pass pass
save_oid = lambda self, oid: None
def test_suite(): def test_suite():
s = unittest.makeSuite(ConnectionDotAdd, 'check') s = unittest.makeSuite(ConnectionDotAdd, 'check')
s.addTest(doctest.DocTestSuite()) s.addTest(doctest.DocTestSuite())
......
...@@ -11,20 +11,21 @@ ...@@ -11,20 +11,21 @@
# FOR A PARTICULAR PURPOSE. # FOR A PARTICULAR PURPOSE.
# #
############################################################################## ##############################################################################
import unittest
import warnings
import ZODB from persistent import Persistent
import ZODB.FileStorage from persistent.mapping import PersistentMapping
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
import ZODB.tests.util
from persistent import Persistent import doctest
from persistent.mapping import PersistentMapping
import transaction import transaction
import unittest
import warnings
import ZODB
import ZODB.FileStorage
import ZODB.MappingStorage
import ZODB.tests.util
class P(Persistent): class P(Persistent):
pass pass
...@@ -631,7 +632,9 @@ class PoisonedObject: ...@@ -631,7 +632,9 @@ class PoisonedObject:
self._p_jar = poisonedjar self._p_jar = poisonedjar
def test_suite(): def test_suite():
return unittest.makeSuite(ZODBTests, 'check') suite = unittest.makeSuite(ZODBTests, 'check')
suite.addTest(doctest.DocFileSuite('new_oids_get_reused_on_abort.test'))
return suite
if __name__ == "__main__": if __name__ == "__main__":
unittest.main(defaultTest="test_suite") unittest.main(defaultTest="test_suite")
...@@ -57,7 +57,7 @@ def pack(db): ...@@ -57,7 +57,7 @@ def pack(db):
class P(persistent.Persistent): class P(persistent.Persistent):
def __init__(self, name): def __init__(self, name=None):
self.name = name self.name = name
def __repr__(self): def __repr__(self):
......
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