Commit 694ac459 authored by Jim Fulton's avatar Jim Fulton

tagged

parents f0cbd414 4f9bbb17
What's new in ZODB 3.8.0
========================
General
-------
- The ZODB Storage APIs have been documented and cleaned up.
- ZODB versions are now officially deprecated and support for them
will be removed in ZODB 3.9. (They have been widely recognized as
deprecated for quite a while.)
- Changed the automatic garbage collection when opening a connection to only
apply the garbage collections on those connections in the pool that are
closed. (This fixed issue 113932.)
ZEO
---
- (3.8a1) ZEO's strategoes for avoiding client cache verification were
improved in the case that servers are restarted. Before, if
transactions were committed after the restart, clients that were up
to date or nearly up to date at the time of the restart and then
connected had to verify their caches. Now, it is far more likely
that a client that reconnects soon after a server restart won't have
to verify its cache.
- (3.8a1) Fixed a serious bug that could cause clients that disconnect from and
reconnect to a server to get bad invalidation data if the server
serves multiple storages with active writes.
- (3.8a1) It is now theoretically possible to use a ClientStorage in a storage
server. This might make it possible to offload read load from a
storage server at the cost of increasing write latency. This should
increase write throughput by offloading reads from the final storage
server. This feature is somewhat experimental. It has tests, but
hasn't been used in production.
Transactions
------------
- (3.8a1) Add a doom() and isDoomed() interface to the transaction module.
First step towards the resolution of
http://www.zope.org/Collectors/Zope3-dev/655
A doomed transaction behaves exactly the same way as an active transaction
but raises an error on any attempt to commit it, thus forcing an abort.
Doom is useful in places where abort is unsafe and an exception cannot be
raised. This occurs when the programmer wants the code following the doom to
run but not commit. It is unsafe to abort in these circumstances as a
following get() may implicitly open a new transaction.
Any attempt to commit a doomed transaction will raise a DoomedTransaction
exception.
- (3.8a1) Clean up the ZODB imports in transaction.
Clean up weird import dance with ZODB. This is unnecessary since the
transaction module stopped being imported in ZODB/__init__.py in rev 39622.
- (3.8a1) Support for subtransactions has been removed in favor of
save points.
Blobs
-----
- (3.8b1) Updated the Blob implementation in a number of ways. Some
of these are backward incompatible with 3.8a1:
o The Blob class now lives in ZODB.blob
o The blob openDetached method has been replaced by the committed method.
- (3.8a1) Added new blob feature. See the ZODB/Blobs directory for
documentation.
ZODB now handles (reasonably) large binary objects efficiently. Useful to
use from a few kilobytes to at least multiple hundred megabytes.
BTrees
------
- (3.8a1) Added support for 64-bit integer BTrees as separate types.
(For now, we're retaining compile-time support for making the regular
integer BTrees 64-bit.)
- (3.8a1) Normalize names in modules so that BTrees, Buckets, Sets, and
TreeSets can all be accessed with those names in the modules (e.g.,
BTrees.IOBTree.BTree). This is in addition to the older names (e.g.,
BTrees.IOBTree.IOBTree). This allows easier drop-in replacement, which
can especially be simplify code for packages that want to support both
32-bit and 64-bit BTrees.
- (3.8a1) Describe the interfaces for each module and actually declare
the interfaces for each.
- (3.8a1) Fix module references so klass.__module__ points to the Python
wrapper module, not the C extension.
- (3.8a1) introduce module families, to group all 32-bit and all 64-bit
modules.
What's new in ZODB3 3.7.0 What's new in ZODB3 3.7.0
========================== ==========================
Release date: 2007-04-20 Release date: 2007-04-20
......
What's new in ZODB 3.9.0 What's new on ZODB 3.8.0
======================== ========================
General General
------- -------
- (3.9.0a1) Document conflict resolution (see ZODB/ConflictResolution.txt). - The ZODB Storage APIs have been documented and cleaned up.
- (3.9.0a1) Bugfix the situation in which comparing persistent objects (for - ZODB versions are now officially deprecated and support for them
instance, as members in BTree set or keys of BTree) might cause data will be removed in ZODB 3.9. (They have been widely recognized as
inconsistency during conflict resolution. deprecated for quite a while.)
- (3.9.0a1) Support multidatabase references in conflict resolution. - Changed the automatic garbage collection when opening a connection to only
apply the garbage collections on those connections in the pool that are
- (3.9.0a1) Make it possible to examine oid and (in some situations) database closed. (This fixed issue 113932.)
name of persistent object references during conflict resolution.
- (3.9.0a1) Moved 'transaction' module out of ZODB. - (3.8.0b3) Document conflict resolution (see ZODB/ConflictResolution.txt).
ZODB depends upon this module, but it must be installed separately.
- (3.9.0a1) ZODB installation now requires setuptools. - (3.8.0b3) Bugfix the situation in which comparing persistent objects (for
instance, as members in BTree set or keys of BTree) might cause data
- (3.9.0a1) Added `offset` information to output of `fstail` inconsistency during conflict resolution.
script. Added test harness for this script.
- (3.9.0a1) Fixed bug 153316: persistent and BTrees were using `int` - (3.8.0b3) Support multidatabase references in conflict resolution.
for memory sizes which caused errors on x86_64 Intel Xeon machines
(using 64-bit Linux).
- (3.9.0a1) Removed version support from connections and DB. Versions - (3.8.0b3) Make it possible to examine oid and (in some situations) database
are still in the storages; this is an incremental step. name of persistent object references during conflict resolution.
- (3.9.0a1) Added support for read-only, historical connections based - (3.8.0b3) Added missing data attribute for conflict errors.
on datetimes or serials (TIDs). See
src/ZODB/historical_connections.txt.
- (3.9.0a1) Fixed small bug that the Connection.isReadOnly method didn't - (3.8.0b5) Fixed bug 153316: persistent and BTrees gave errors on x86_64
work after a savepoint. Intel XEON platforms.
ZEO ZEO
--- ---
- (3.9.0a1) Bug #98275: Made ZEO cache more tolerant when invalidating current - (3.8.0b6) Bug #98275: Made ZEO cache more tolerant when invalidating current
versions of objects. versions of objects.
- (3.9.0a1) Fixed a serious bug that could cause client I/O to stop - (3.8.0b4, 3.8.0b5) Fixed a serious bug that could cause client I/O to stop
(hang). This was accomonied by a critical log message along the (hang). This was accomonied by a critical log message along the
lines of: "RuntimeError: dictionary changed size during iteration". lines of: "RuntimeError: dictionary changed size during iteration".
(In b4, the bug was only partially fixed.)
- (3.8a1) ZEO's strategoes for avoiding client cache verification were
improved in the case that servers are restarted. Before, if
transactions were committed after the restart, clients that were up
to date or nearly up to date at the time of the restart and then
connected had to verify their caches. Now, it is far more likely
that a client that reconnects soon after a server restart won't have
to verify its cache.
- (3.8a1) Fixed a serious bug that could cause clients that disconnect from and
reconnect to a server to get bad invalidation data if the server
serves multiple storages with active writes.
- (3.8a1) It is now theoretically possible to use a ClientStorage in a storage
server. This might make it possible to offload read load from a
storage server at the cost of increasing write latency. This should
increase write throughput by offloading reads from the final storage
server. This feature is somewhat experimental. It has tests, but
hasn't been used in production.
Transactions Transactions
------------ ------------
- (3.9.0a1) 'transaction' module is not included in ZODB anymore. It - (3.8a1) Add a doom() and isDoomed() interface to the transaction module.
is now just a ZODB dependency (via setuptools declarations).
First step towards the resolution of
http://www.zope.org/Collectors/Zope3-dev/655
A doomed transaction behaves exactly the same way as an active transaction
but raises an error on any attempt to commit it, thus forcing an abort.
Doom is useful in places where abort is unsafe and an exception cannot be
raised. This occurs when the programmer wants the code following the doom to
run but not commit. It is unsafe to abort in these circumstances as a
following get() may implicitly open a new transaction.
Any attempt to commit a doomed transaction will raise a DoomedTransaction
exception.
- (3.8a1) Clean up the ZODB imports in transaction.
Clean up weird import dance with ZODB. This is unnecessary since the
transaction module stopped being imported in ZODB/__init__.py in rev 39622.
- (3.8a1) Support for subtransactions has been removed in favor of
save points.
Blobs Blobs
----- -----
- (3.9.0a1) Fixed bug #127182: Blobs were subclassable which was not desired. - (3.8b5) Fixed bug #130459: Packing was broken by uncommitted blob data.
- (3.8b4) Fixed bug #127182: Blobs were subclassable which was not desired.
- (3.9.0a1) Fixed bug #126007: tpc_abort had untested code path that was - (3.8b3) Fixed bug #126007: tpc_abort had untested code path that was
broken. broken.
- (3.9.0a1) Fixed bug #129921: getSize() function in BlobStorage could not - (3.8b3) Fixed bug #129921: getSize() function in BlobStorage could not
deal with garbage files deal with garbage files
- (3.9.0a1) Fixed bug in which MVCC would not work for blobs. - (3.8b1) Updated the Blob implementation in a number of ways. Some
of these are backward incompatible with 3.8a1:
o The Blob class now lives in ZODB.blob
o The blob openDetached method has been replaced by the committed method.
- (3.8a1) Added new blob feature. See the ZODB/Blobs directory for
documentation.
ZODB now handles (reasonably) large binary objects efficiently. Useful to
use from a few kilobytes to at least multiple hundred megabytes.
BTrees BTrees
------ ------
- - (3.8a1) Added support for 64-bit integer BTrees as separate types.
(For now, we're retaining compile-time support for making the regular
integer BTrees 64-bit.)
- (3.8a1) Normalize names in modules so that BTrees, Buckets, Sets, and
TreeSets can all be accessed with those names in the modules (e.g.,
BTrees.IOBTree.BTree). This is in addition to the older names (e.g.,
BTrees.IOBTree.IOBTree). This allows easier drop-in replacement, which
can especially be simplify code for packages that want to support both
32-bit and 64-bit BTrees.
- (3.8a1) Describe the interfaces for each module and actually declare
the interfaces for each.
- (3.8a1) Fix module references so klass.__module__ points to the Python
wrapper module, not the C extension.
- (3.8a1) introduce module families, to group all 32-bit and all 64-bit
modules.
[buildout] [buildout]
develop = . transaction develop = .
parts = test scripts parts = test scripts
find-links = http://download.zope.org/distribution/ find-links = http://download.zope.org/distribution/
......
...@@ -72,11 +72,11 @@ configuration should look like this:: ...@@ -72,11 +72,11 @@ configuration should look like this::
<filestorage> <filestorage>
path $INSTANCE/var/Data.fs path $INSTANCE/var/Data.fs
<filestorage> <filestorage>
blob-dir $SERVER/blobs bob-dir $SERVER/blobs
</blobstorage> </blobstorage>
(Remember to manually replace $SERVER and $CLIENT with the exported directory (Remember to manually replace $SERVER and $CLIENT with the exported directory
as accessible by either the ZEO server or the ZEO client.) as accessible bei either the ZEO server or the ZEO client.)
Conclusion Conclusion
---------- ----------
......
...@@ -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 = "3.8.0c1" VERSION = "3.8.0b6"
# The (non-obvious!) choices for the Trove Development Status line: # The (non-obvious!) choices for the Trove Development Status line:
# Development Status :: 5 - Production/Stable # Development Status :: 5 - Production/Stable
...@@ -28,7 +28,6 @@ VERSION = "3.8.0c1" ...@@ -28,7 +28,6 @@ VERSION = "3.8.0c1"
# Development Status :: 3 - Alpha # Development Status :: 3 - Alpha
classifiers = """\ classifiers = """\
Development Status :: 4 - Beta
Intended Audience :: Developers Intended Audience :: Developers
License :: OSI Approved :: Zope Public License License :: OSI Approved :: Zope Public License
Programming Language :: Python Programming Language :: Python
...@@ -38,9 +37,26 @@ Operating System :: Microsoft :: Windows ...@@ -38,9 +37,26 @@ Operating System :: Microsoft :: Windows
Operating System :: Unix Operating System :: Unix
""" """
from setuptools import setup try:
from setuptools import setup
entry_points = """ except ImportError:
from distutils.core import setup
extra = dict(
scripts = ["src/ZODB/scripts/fsdump.py",
"src/ZODB/scripts/fsoids.py",
"src/ZODB/scripts/fsrefs.py",
"src/ZODB/scripts/fstail.py",
"src/ZODB/scripts/fstest.py",
"src/ZODB/scripts/repozo.py",
"src/ZEO/scripts/zeopack.py",
"src/ZEO/scripts/runzeo.py",
"src/ZEO/scripts/zeopasswd.py",
"src/ZEO/scripts/mkzeoinst.py",
"src/ZEO/scripts/zeoctl.py",
],
)
else:
entry_points = """
[console_scripts] [console_scripts]
fsdump = ZODB.FileStorage.fsdump:main fsdump = ZODB.FileStorage.fsdump:main
fsoids = ZODB.scripts.fsoids:main fsoids = ZODB.scripts.fsoids:main
...@@ -53,8 +69,19 @@ entry_points = """ ...@@ -53,8 +69,19 @@ entry_points = """
mkzeoinst = ZEO.mkzeoinst:main mkzeoinst = ZEO.mkzeoinst:main
zeoctl = ZEO.zeoctl:main zeoctl = ZEO.zeoctl:main
""" """
extra = dict(
scripts = [] install_requires = [
'zope.interface',
'zope.proxy',
'zope.testing',
'ZConfig',
'zdaemon',
],
zip_safe = False,
entry_points = entry_points,
include_package_data = True,
)
scripts = []
import glob import glob
import os import os
...@@ -149,6 +176,7 @@ packages = ["BTrees", "BTrees.tests", ...@@ -149,6 +176,7 @@ packages = ["BTrees", "BTrees.tests",
"ZODB", "ZODB.FileStorage", "ZODB.tests", "ZODB", "ZODB.FileStorage", "ZODB.tests",
"ZODB.scripts", "ZODB.scripts",
"persistent", "persistent.tests", "persistent", "persistent.tests",
"transaction", "transaction.tests",
"ThreadedAsync", "ThreadedAsync",
"ZopeUndo", "ZopeUndo.tests", "ZopeUndo", "ZopeUndo.tests",
] ]
...@@ -159,6 +187,8 @@ def copy_other_files(cmd, outputbase): ...@@ -159,6 +187,8 @@ def copy_other_files(cmd, outputbase):
extensions = ["*.conf", "*.xml", "*.txt", "*.sh"] extensions = ["*.conf", "*.xml", "*.txt", "*.sh"]
directories = [ directories = [
"BTrees", "BTrees",
"transaction",
"transaction/tests",
"persistent/tests", "persistent/tests",
"ZEO", "ZEO",
"ZEO/scripts", "ZEO/scripts",
...@@ -210,24 +240,9 @@ class MyDistribution(Distribution): ...@@ -210,24 +240,9 @@ class MyDistribution(Distribution):
self.cmdclass['build_py'] = MyPyBuilder self.cmdclass['build_py'] = MyPyBuilder
self.cmdclass['install_lib'] = MyLibInstaller self.cmdclass['install_lib'] = MyLibInstaller
def alltests():
# use the zope.testing testrunner machinery to find all the
# test suites we've put under ourselves
from zope.testing.testrunner import get_options
from zope.testing.testrunner import find_suites
from zope.testing.testrunner import configure_logging
configure_logging()
from unittest import TestSuite
here = os.path.abspath(os.path.dirname(sys.argv[0]))
args = sys.argv[:]
src = os.path.join(here, 'src')
defaults = ['--test-path', src]
options = get_options(args, defaults)
suites = list(find_suites(options))
return TestSuite(suites)
doclines = __doc__.split("\n") doclines = __doc__.split("\n")
setup(name="ZODB3", setup(name="ZODB3",
version=VERSION, version=VERSION,
maintainer="Zope Corporation", maintainer="Zope Corporation",
...@@ -244,35 +259,4 @@ setup(name="ZODB3", ...@@ -244,35 +259,4 @@ setup(name="ZODB3",
classifiers = filter(None, classifiers.split("\n")), classifiers = filter(None, classifiers.split("\n")),
long_description = "\n".join(doclines[2:]), long_description = "\n".join(doclines[2:]),
distclass = MyDistribution, distclass = MyDistribution,
test_suite="__main__.alltests", # to support "setup.py test" **extra)
tests_require = [
'zope.interface',
'zope.proxy',
'zope.testing',
'transaction',
'zdaemon',
],
install_requires = [
'zope.interface',
'zope.proxy',
'zope.testing',
'ZConfig',
'zdaemon',
'transaction',
],
zip_safe = False,
entry_points = """
[console_scripts]
fsdump = ZODB.FileStorage.fsdump:main
fsoids = ZODB.scripts.fsoids:main
fsrefs = ZODB.scripts.fsrefs:main
fstail = ZODB.scripts.fstail:Main
repozo = ZODB.scripts.repozo:main
zeopack = ZEO.scripts.zeopack:main
runzeo = ZEO.runzeo:main
zeopasswd = ZEO.zeopasswd:main
mkzeoinst = ZEO.mkzeoinst:main
zeoctl = ZEO.zeoctl:main
""",
include_package_data = True,
)
This diff is collapsed.
...@@ -46,7 +46,7 @@ class MappingStorageConfig: ...@@ -46,7 +46,7 @@ class MappingStorageConfig:
def getConfig(self, path, create, read_only): def getConfig(self, path, create, read_only):
return """<mappingstorage 1/>""" return """<mappingstorage 1/>"""
class FileStorageConnectionTests( class FileStorageConnectionTests(
FileStorageConfig, FileStorageConfig,
ConnectionTests.ConnectionTests, ConnectionTests.ConnectionTests,
......
...@@ -14,16 +14,11 @@ ...@@ -14,16 +14,11 @@
"""Basic unit tests for a multi-version client cache.""" """Basic unit tests for a multi-version client cache."""
import os import os
import random
import tempfile import tempfile
import unittest import unittest
import doctest
import string
import sys
import ZEO.cache import ZEO.cache
from ZODB.utils import p64, repr_to_oid from ZODB.utils import p64
n1 = p64(1) n1 = p64(1)
n2 = p64(2) n2 = p64(2)
...@@ -31,60 +26,6 @@ n3 = p64(3) ...@@ -31,60 +26,6 @@ n3 = p64(3)
n4 = p64(4) n4 = p64(4)
n5 = p64(5) n5 = p64(5)
def hexprint(file):
file.seek(0)
data = file.read()
offset = 0
while data:
line, data = data[:16], data[16:]
printable = ""
hex = ""
for character in line:
if character in string.printable and not ord(character) in [12,13,9]:
printable += character
else:
printable += '.'
hex += character.encode('hex') + ' '
hex = hex[:24] + ' ' + hex[24:]
hex = hex.ljust(49)
printable = printable.ljust(16)
print '%08x %s |%s|' % (offset, hex, printable)
offset += 16
class ClientCacheDummy(object):
def __init__(self):
self.objects = {}
def _evicted(self, o):
if o.key in self.objects:
del self.objects[o.key]
def oid(o):
repr = '%016x' % o
return repr_to_oid(repr)
tid = oid
class FileCacheFuzzing(unittest.TestCase):
def testFileCacheFuzzing(self):
cache_dummy = ClientCacheDummy()
fc = ZEO.cache.FileCache(maxsize=5000, fpath=None,
parent=cache_dummy)
for i in xrange(10000):
size = random.randint(0,5500)
obj = ZEO.cache.Object(key=(oid(i), oid(1)), version='',
data='*'*size, start_tid=oid(1),
end_tid=None)
fc.add(obj)
hexprint(fc.f)
fc.close()
class CacheTests(unittest.TestCase): class CacheTests(unittest.TestCase):
def setUp(self): def setUp(self):
...@@ -185,76 +126,6 @@ class CacheTests(unittest.TestCase): ...@@ -185,76 +126,6 @@ class CacheTests(unittest.TestCase):
# TODO: Need to make sure eviction of non-current data # TODO: Need to make sure eviction of non-current data
# and of version data are handled correctly. # and of version data are handled correctly.
def _run_fuzzing(self):
current_tid = 1
current_oid = 1
def log(*args):
#print args
pass
cache = self.fuzzy_cache
objects = self.fuzzy_cache_client.objects
for operation in xrange(10000):
op = random.choice(['add', 'access', 'remove', 'update', 'settid'])
if not objects:
op = 'add'
log(op)
if op == 'add':
current_oid += 1
key = (oid(current_oid), tid(current_tid))
object = ZEO.cache.Object(
key=key, version='', data='*'*random.randint(1,60*1024),
start_tid=tid(current_tid), end_tid=None)
assert key not in objects
log(key, len(object.data), current_tid)
cache.add(object)
if (object.size + ZEO.cache.OBJECT_HEADER_SIZE >
cache.maxsize - ZEO.cache.ZEC3_HEADER_SIZE):
assert key not in cache
else:
objects[key] = object
assert key in cache, key
elif op == 'access':
key = random.choice(objects.keys())
log(key)
object = objects[key]
found = cache.access(key)
assert object.data == found.data
assert object.key == found.key
assert object.size == found.size == (len(object.data)+object.TOTAL_FIXED_SIZE)
elif op == 'remove':
key = random.choice(objects.keys())
log(key)
cache.remove(key)
assert key not in cache
assert key not in objects
elif op == 'update':
key = random.choice(objects.keys())
object = objects[key]
log(key, object.key)
if not object.end_tid:
object.end_tid = tid(current_tid)
log(key, current_tid)
cache.update(object)
elif op == 'settid':
current_tid += 1
log(current_tid)
cache.settid(tid(current_tid))
cache.close()
def testFuzzing(self):
random.seed()
seed = random.randint(0, sys.maxint)
random.seed(seed)
self.fuzzy_cache_client = ClientCacheDummy()
self.fuzzy_cache = ZEO.cache.FileCache(
random.randint(100, 50*1024), None, self.fuzzy_cache_client)
try:
self._run_fuzzing()
except:
print "Error in fuzzing with seed", seed
hexprint(self.fuzzy_cache.f)
raise
def testSerialization(self): def testSerialization(self):
self.cache.store(n1, "", n2, None, "data for n1") self.cache.store(n1, "", n2, None, "data for n1")
self.cache.store(n2, "version", n2, None, "version data for n2") self.cache.store(n2, "version", n2, None, "version data for n2")
...@@ -281,10 +152,5 @@ class CacheTests(unittest.TestCase): ...@@ -281,10 +152,5 @@ class CacheTests(unittest.TestCase):
eq(copy.current, self.cache.current) eq(copy.current, self.cache.current)
eq(copy.noncurrent, self.cache.noncurrent) eq(copy.noncurrent, self.cache.noncurrent)
def test_suite(): def test_suite():
suite = unittest.TestSuite() return unittest.makeSuite(CacheTests)
suite.addTest(unittest.makeSuite(CacheTests))
suite.addTest(unittest.makeSuite(FileCacheFuzzing))
suite.addTest(doctest.DocFileSuite('filecache.txt'))
return suite
...@@ -4,7 +4,7 @@ ZEO Fan Out ...@@ -4,7 +4,7 @@ ZEO Fan Out
We should be able to set up ZEO servers with ZEO clients. Let's see We should be able to set up ZEO servers with ZEO clients. Let's see
if we can make it work. if we can make it work.
We'll use some helper functions. The first is a helper that starts We'll use some helper functions. The first is a helpter that starts
ZEO servers for us and another one that picks ports. ZEO servers for us and another one that picks ports.
We'll start the first server: We'll start the first server:
...@@ -16,7 +16,7 @@ We'll start the first server: ...@@ -16,7 +16,7 @@ We'll start the first server:
... '<filestorage 1>\n path fs\n</filestorage>\n', zconf0, port0) ... '<filestorage 1>\n path fs\n</filestorage>\n', zconf0, port0)
Then we'll start 2 others that use this one: Then we''ll start 2 others that use this one:
>>> port1 = ZEO.tests.testZEO.get_port() >>> port1 = ZEO.tests.testZEO.get_port()
>>> zconf1 = ZEO.tests.forker.ZEOConfig(('', port1)) >>> zconf1 = ZEO.tests.forker.ZEOConfig(('', port1))
......
...@@ -82,7 +82,10 @@ def client_loop(): ...@@ -82,7 +82,10 @@ def client_loop():
continue continue
if not (r or w or e): if not (r or w or e):
for obj in client_map.itervalues(): # The line intentionally doesn't use iterators. Other
# threads can close dispatchers, causeing the socket
# map to shrink.
for obj in client_map.values():
if isinstance(obj, Connection): if isinstance(obj, Connection):
# Send a heartbeat message as a reply to a # Send a heartbeat message as a reply to a
# non-existent message id. # non-existent message id.
......
This diff is collapsed.
This diff is collapsed.
...@@ -47,7 +47,7 @@ class ExportImport: ...@@ -47,7 +47,7 @@ class ExportImport:
continue continue
done_oids[oid] = True done_oids[oid] = True
try: try:
p, serial = load(oid, '') p, serial = load(oid, self._version)
except: except:
logger.debug("broken reference for oid %s", repr(oid), logger.debug("broken reference for oid %s", repr(oid),
exc_info=True) exc_info=True)
...@@ -127,6 +127,8 @@ class ExportImport: ...@@ -127,6 +127,8 @@ class ExportImport:
return Ghost(oid) return Ghost(oid)
version = self._version
while 1: while 1:
header = f.read(16) header = f.read(16)
if header == export_end_marker: if header == export_end_marker:
...@@ -178,9 +180,9 @@ class ExportImport: ...@@ -178,9 +180,9 @@ class ExportImport:
if blob_filename is not None: if blob_filename is not None:
self._storage.storeBlob(oid, None, data, blob_filename, self._storage.storeBlob(oid, None, data, blob_filename,
'', transaction) version, transaction)
else: else:
self._storage.store(oid, None, data, '', transaction) self._storage.store(oid, None, data, version, transaction)
export_end_marker = '\377'*16 export_end_marker = '\377'*16
......
...@@ -17,10 +17,6 @@ $Id$""" ...@@ -17,10 +17,6 @@ $Id$"""
from ZODB.utils import oid_repr, readable_tid_repr from ZODB.utils import oid_repr, readable_tid_repr
# BBB: We moved the two transactions to the transaction package
from transaction.interfaces import TransactionError, TransactionFailedError
def _fmt_undo(oid, reason): def _fmt_undo(oid, reason):
s = reason and (": %s" % reason) or "" s = reason and (": %s" % reason) or ""
return "Undo error %s%s" % (oid_repr(oid), s) return "Undo error %s%s" % (oid_repr(oid), s)
...@@ -48,6 +44,18 @@ class POSKeyError(KeyError, POSError): ...@@ -48,6 +44,18 @@ class POSKeyError(KeyError, POSError):
def __str__(self): def __str__(self):
return oid_repr(self.args[0]) return oid_repr(self.args[0])
class TransactionError(POSError):
"""An error occurred due to normal transaction processing."""
class TransactionFailedError(POSError):
"""Cannot perform an operation on a transaction that previously failed.
An attempt was made to commit a transaction, or to join a transaction,
but this transaction previously raised an exception during an attempt
to commit it. The transaction must be explicitly aborted, either by
invoking abort() on the transaction, or begin() on its transaction
manager.
"""
class ConflictError(TransactionError): class ConflictError(TransactionError):
"""Two transactions tried to modify the same object at once. """Two transactions tried to modify the same object at once.
...@@ -98,12 +106,11 @@ class ConflictError(TransactionError): ...@@ -98,12 +106,11 @@ class ConflictError(TransactionError):
# avoid circular import chain # avoid circular import chain
from ZODB.utils import get_pickle_metadata from ZODB.utils import get_pickle_metadata
self.class_name = "%s.%s" % get_pickle_metadata(data) self.class_name = "%s.%s" % get_pickle_metadata(data)
## else:
## if message != "data read conflict error":
## raise RuntimeError
self.serials = serials self.serials = serials
self.data = data
def __str__(self): def __str__(self):
extras = [] extras = []
if self.oid: if self.oid:
...@@ -234,10 +241,6 @@ class DanglingReferenceError(TransactionError): ...@@ -234,10 +241,6 @@ class DanglingReferenceError(TransactionError):
return "from %s to %s" % (oid_repr(self.referer), return "from %s to %s" % (oid_repr(self.referer),
oid_repr(self.missing)) oid_repr(self.missing))
############################################################################
# Only used in storages; versions are no longer supported.
class VersionError(POSError): class VersionError(POSError):
"""An error in handling versions occurred.""" """An error in handling versions occurred."""
...@@ -250,7 +253,6 @@ class VersionLockError(VersionError, TransactionError): ...@@ -250,7 +253,6 @@ class VersionLockError(VersionError, TransactionError):
An attempt was made to modify an object that has been modified in an An attempt was made to modify an object that has been modified in an
unsaved version. unsaved version.
""" """
############################################################################
class UndoError(POSError): class UndoError(POSError):
"""An attempt was made to undo a non-undoable transaction.""" """An attempt was made to undo a non-undoable transaction."""
...@@ -297,9 +299,6 @@ class ExportError(POSError): ...@@ -297,9 +299,6 @@ class ExportError(POSError):
class Unsupported(POSError): class Unsupported(POSError):
"""A feature was used that is not supported by the storage.""" """A feature was used that is not supported by the storage."""
class ReadOnlyHistoryError(POSError):
"""Unable to add or modify objects in an historical connection."""
class InvalidObjectReference(POSError): class InvalidObjectReference(POSError):
"""An object contains an invalid reference to another object. """An object contains an invalid reference to another object.
......
...@@ -490,6 +490,7 @@ class BlobStorage(SpecificationDecoratorBase): ...@@ -490,6 +490,7 @@ class BlobStorage(SpecificationDecoratorBase):
base_dir = self.fshelper.base_dir base_dir = self.fshelper.base_dir
for oid, oid_path in self.fshelper.listOIDs(): for oid, oid_path in self.fshelper.listOIDs():
files = os.listdir(oid_path) files = os.listdir(oid_path)
for filename in files: for filename in files:
filepath = os.path.join(oid_path, filename) filepath = os.path.join(oid_path, filename)
whatever, serial = self.fshelper.splitBlobFilename(filepath) whatever, serial = self.fshelper.splitBlobFilename(filepath)
......
...@@ -180,22 +180,16 @@ ...@@ -180,22 +180,16 @@
and exceeding twice pool-size connections causes a critical and exceeding twice pool-size connections causes a critical
message to be logged. message to be logged.
</description> </description>
<key name="historical-pool-size" datatype="integer" default="3"/> <key name="version-pool-size" datatype="integer" default="3"/>
<description> <description>
The expected maximum total number of historical connections The expected maximum number of connections simultaneously open
simultaneously open. per version.
</description> </description>
<key name="historical-cache-size" datatype="integer" default="1000"/> <key name="version-cache-size" datatype="integer" default="100"/>
<description> <description>
Target size, in number of objects, of each historical connection's Target size, in number of objects, of each version connection's
object cache. object cache.
</description> </description>
<key name="historical-timeout" datatype="time-interval"
default="5m"/>
<description>
The minimum interval that an unused historical connection should be
kept.
</description>
<key name="database-name" default="unnamed"/> <key name="database-name" default="unnamed"/>
<description> <description>
When multidatabases are in use, this is the name given to this When multidatabases are in use, this is the name given to this
......
...@@ -68,6 +68,7 @@ def storageFromURL(url): ...@@ -68,6 +68,7 @@ def storageFromURL(url):
def storageFromConfig(section): def storageFromConfig(section):
return section.open() return section.open()
class BaseConfig: class BaseConfig:
"""Object representing a configured storage or database. """Object representing a configured storage or database.
...@@ -98,9 +99,8 @@ class ZODBDatabase(BaseConfig): ...@@ -98,9 +99,8 @@ class ZODBDatabase(BaseConfig):
return ZODB.DB(storage, return ZODB.DB(storage,
pool_size=section.pool_size, pool_size=section.pool_size,
cache_size=section.cache_size, cache_size=section.cache_size,
historical_pool_size=section.historical_pool_size, version_pool_size=section.version_pool_size,
historical_cache_size=section.historical_cache_size, version_cache_size=section.version_cache_size,
historical_timeout=section.historical_timeout,
database_name=section.database_name, database_name=section.database_name,
databases=databases) databases=databases)
except: except:
......
This diff is collapsed.
...@@ -34,10 +34,9 @@ class IConnection(Interface): ...@@ -34,10 +34,9 @@ class IConnection(Interface):
loading objects from that Connection. Objects loaded by one loading objects from that Connection. Objects loaded by one
thread should not be used by another thread. thread should not be used by another thread.
A Connection can be frozen to a serial--a transaction id, a single point in A Connection can be associated with a single version when it is
history-- when it is created. By default, a Connection is not associated created. By default, a Connection is not associated with a
with a serial; it uses current data. A Connection frozen to a serial is version; it uses non-version data.
read-only.
Each Connection provides an isolated, consistent view of the Each Connection provides an isolated, consistent view of the
database, by managing independent copies of objects in the database, by managing independent copies of objects in the
...@@ -102,7 +101,8 @@ class IConnection(Interface): ...@@ -102,7 +101,8 @@ class IConnection(Interface):
User Methods: User Methods:
root, get, add, close, db, sync, isReadOnly, cacheGC, root, get, add, close, db, sync, isReadOnly, cacheGC,
cacheFullSweep, cacheMinimize cacheFullSweep, cacheMinimize, getVersion,
modifiedInVersion
Experimental Methods: Experimental Methods:
onCloseCallbacks onCloseCallbacks
...@@ -226,6 +226,9 @@ class IConnection(Interface): ...@@ -226,6 +226,9 @@ class IConnection(Interface):
The root is a persistent.mapping.PersistentMapping. The root is a persistent.mapping.PersistentMapping.
""" """
def getVersion():
"""Returns the version this connection is attached to."""
# Multi-database support. # Multi-database support.
connections = Attribute( connections = Attribute(
...@@ -322,7 +325,7 @@ class IStorageDB(Interface): ...@@ -322,7 +325,7 @@ class IStorageDB(Interface):
there would be so many that it would be inefficient to do so. there would be so many that it would be inefficient to do so.
""" """
def invalidate(transaction_id, oids): def invalidate(transaction_id, oids, version=''):
"""Invalidate object ids committed by the given transaction """Invalidate object ids committed by the given transaction
The oids argument is an iterable of object identifiers. The oids argument is an iterable of object identifiers.
...@@ -353,15 +356,13 @@ class IDatabase(IStorageDB): ...@@ -353,15 +356,13 @@ class IDatabase(IStorageDB):
entry. entry.
""") """)
def open(transaction_manager=None, serial=''): def open(version='', transaction_manager=None):
"""Return an IConnection object for use by application code. """Return an IConnection object for use by application code.
version: the "version" that all changes will be made
in, defaults to no version.
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.
serial: the serial (transaction id) of the database to open.
An empty string (the default) means to open it to the newest
serial. Specifying a serial results in a read-only historical
connection.
Note that the connection pool is managed as a stack, to Note that the connection pool is managed as a stack, to
increase the likelihood that the connection's stack will increase the likelihood that the connection's stack will
...@@ -440,7 +441,7 @@ class IStorage(Interface): ...@@ -440,7 +441,7 @@ class IStorage(Interface):
This is used soley for informational purposes. This is used soley for informational purposes.
""" """
def history(oid, size=1): def history(oid, version, size=1):
"""Return a sequence of history information dictionaries. """Return a sequence of history information dictionaries.
Up to size objects (including no objects) may be returned. Up to size objects (including no objects) may be returned.
...@@ -456,6 +457,10 @@ class IStorage(Interface): ...@@ -456,6 +457,10 @@ class IStorage(Interface):
tid tid
The transaction identifier of the transaction that The transaction identifier of the transaction that
committed the version. committed the version.
version
The version that the revision is in. If the storage
doesn't support versions, then this must be an empty
string.
user_name user_name
The user identifier, if any (or an empty string) of the The user identifier, if any (or an empty string) of the
user on whos behalf the revision was committed. user on whos behalf the revision was committed.
...@@ -486,14 +491,18 @@ class IStorage(Interface): ...@@ -486,14 +491,18 @@ class IStorage(Interface):
This is used soley for informational purposes. This is used soley for informational purposes.
""" """
def load(oid): def load(oid, version):
"""Load data for an object id """Load data for an object id and version
A data record and serial are returned. The serial is a A data record and serial are returned. The serial is a
transaction identifier of the transaction that wrote the data transaction identifier of the transaction that wrote the data
record. record.
A POSKeyError is raised if there is no record for the object id. A POSKeyError is raised if there is no record for the object
id and version.
Storages that don't support versions must ignore the version
argument.
""" """
def loadBefore(oid, tid): def loadBefore(oid, tid):
...@@ -566,7 +575,7 @@ class IStorage(Interface): ...@@ -566,7 +575,7 @@ class IStorage(Interface):
has a reasonable chance of being unique. has a reasonable chance of being unique.
""" """
def store(oid, serial, data, transaction): def store(oid, serial, data, version, transaction):
"""Store data for the object id, oid. """Store data for the object id, oid.
Arguments: Arguments:
...@@ -585,6 +594,11 @@ class IStorage(Interface): ...@@ -585,6 +594,11 @@ class IStorage(Interface):
data data
The data record. This is opaque to the storage. The data record. This is opaque to the storage.
version
The version to store the data is. If the storage doesn't
support versions, this should be an empty string and the
storage is allowed to ignore it.
transaction transaction
A transaction object. This should match the current A transaction object. This should match the current
transaction for the storage, set by tpc_begin. transaction for the storage, set by tpc_begin.
...@@ -693,7 +707,7 @@ class IStorageRestoreable(IStorage): ...@@ -693,7 +707,7 @@ class IStorageRestoreable(IStorage):
# failed to take into account records after the pack time. # failed to take into account records after the pack time.
def restore(oid, serial, data, prev_txn, transaction): def restore(oid, serial, data, version, prev_txn, transaction):
"""Write data already committed in a separate database """Write data already committed in a separate database
The restore method is used when copying data from one database The restore method is used when copying data from one database
...@@ -713,6 +727,9 @@ class IStorageRestoreable(IStorage): ...@@ -713,6 +727,9 @@ class IStorageRestoreable(IStorage):
The record data. This will be None if the transaction The record data. This will be None if the transaction
undid the creation of the object. undid the creation of the object.
version
The version identifier for the record
prev_txn prev_txn
The identifier of a previous transaction that held the The identifier of a previous transaction that held the
object data. The target storage can sometimes use this object data. The target storage can sometimes use this
...@@ -729,6 +746,7 @@ class IStorageRecordInformation(Interface): ...@@ -729,6 +746,7 @@ class IStorageRecordInformation(Interface):
""" """
oid = Attribute("The object id") oid = Attribute("The object id")
version = Attribute("The version")
data = Attribute("The data record") data = Attribute("The data record")
class IStorageTransactionInformation(Interface): class IStorageTransactionInformation(Interface):
...@@ -918,7 +936,7 @@ class IBlob(Interface): ...@@ -918,7 +936,7 @@ class IBlob(Interface):
class IBlobStorage(Interface): class IBlobStorage(Interface):
"""A storage supporting BLOBs.""" """A storage supporting BLOBs."""
def storeBlob(oid, oldserial, data, blob, transaction): def storeBlob(oid, oldserial, data, blob, version, transaction):
"""Stores data that has a BLOB attached.""" """Stores data that has a BLOB attached."""
def loadBlob(oid, serial): def loadBlob(oid, serial):
......
...@@ -33,8 +33,8 @@ def main(path, ntxn): ...@@ -33,8 +33,8 @@ def main(path, ntxn):
th.read_meta() th.read_meta()
print "%s: hash=%s" % (th.get_timestamp(), print "%s: hash=%s" % (th.get_timestamp(),
binascii.hexlify(hash)) binascii.hexlify(hash))
print ("user=%r description=%r length=%d offset=%d" print ("user=%r description=%r length=%d"
% (th.user, th.descr, th.length, th.get_data_offset())) % (th.user, th.descr, th.length))
print print
th = th.prev_txn() th = th.prev_txn()
i -= 1 i -= 1
......
====================
The `fstail` utility
====================
The `fstail` utility shows information for a FileStorage about the last `n`
transactions:
We have to prepare a FileStorage first:
>>> from ZODB.FileStorage import FileStorage
>>> from ZODB.DB import DB
>>> import transaction
>>> from tempfile import mktemp
>>> storagefile = mktemp()
>>> base_storage = FileStorage(storagefile)
>>> database = DB(base_storage)
>>> connection1 = database.open()
>>> root = connection1.root()
>>> root['foo'] = 1
>>> transaction.commit()
Now lets have a look at the last transactions of this FileStorage:
>>> from ZODB.scripts.fstail import main
>>> main(storagefile, 5)
2007-11-10 15:18:48.543001: hash=b16422d09fabdb45d4e4325e4b42d7d6f021d3c3
user='' description='' length=138 offset=191
<BLANKLINE>
2007-11-10 15:18:48.543001: hash=b16422d09fabdb45d4e4325e4b42d7d6f021d3c3
user='' description='initial database creation' length=156 offset=52
<BLANKLINE>
Now clean up the storage again:
>>> import os
>>> base_storage.close()
>>> os.unlink(storagefile)
>>> os.unlink(storagefile+'.index')
>>> os.unlink(storagefile+'.lock')
>>> os.unlink(storagefile+'.tmp')
...@@ -11,21 +11,15 @@ ...@@ -11,21 +11,15 @@
# FOR A PARTICULAR PURPOSE. # FOR A PARTICULAR PURPOSE.
# #
############################################################################## ##############################################################################
"""Test harness for scripts. """XXX short summary goes here.
$Id$ $Id$
""" """
import unittest import unittest
import re from zope.testing import doctest
from zope.testing import doctest, renormalizing
checker = renormalizing.RENormalizing([
(re.compile('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+'),
'2007-11-10 15:18:48.543001'),
(re.compile('hash=[0-9a-f]{40}'),
'hash=b16422d09fabdb45d4e4325e4b42d7d6f021d3c3')])
def test_suite(): def test_suite():
return unittest.TestSuite(( return unittest.TestSuite((
doctest.DocFileSuite('referrers.txt', 'fstail.txt', checker=checker), doctest.DocFileSuite('referrers.txt'),
)) ))
...@@ -371,7 +371,7 @@ class ObjectWriter: ...@@ -371,7 +371,7 @@ class ObjectWriter:
return oid return oid
# Note that we never get here for persistent classes. # Note that we never get here for persistent classes.
# We'll use direct refs for normal classes. # We'll use driect refs for normal classes.
if database_name: if database_name:
return ['m', (database_name, oid, klass)] return ['m', (database_name, oid, klass)]
......
...@@ -394,6 +394,153 @@ class VersionStorage: ...@@ -394,6 +394,153 @@ class VersionStorage:
self._storage.tpc_finish(t) self._storage.tpc_finish(t)
self.assertEqual(oids, [oid]) self.assertEqual(oids, [oid])
def checkPackVersions(self):
db = DB(self._storage)
cn = db.open(version="testversion")
root = cn.root()
obj = root["obj"] = MinPO("obj")
root["obj2"] = MinPO("obj2")
txn = transaction.get()
txn.note("create 2 objs in version")
txn.commit()
obj.value = "77"
txn = transaction.get()
txn.note("modify obj in version")
txn.commit()
# undo the modification to generate a mix of backpointers
# and versions for pack to chase
info = db.undoInfo()
db.undo(info[0]["id"])
txn = transaction.get()
txn.note("undo modification")
txn.commit()
snooze()
self._storage.pack(time.time(), referencesf)
db.commitVersion("testversion")
txn = transaction.get()
txn.note("commit version")
txn.commit()
cn = db.open()
root = cn.root()
root["obj"] = "no version"
txn = transaction.get()
txn.note("modify obj")
txn.commit()
self._storage.pack(time.time(), referencesf)
def checkPackVersionsInPast(self):
db = DB(self._storage)
cn = db.open(version="testversion")
root = cn.root()
obj = root["obj"] = MinPO("obj")
root["obj2"] = MinPO("obj2")
txn = transaction.get()
txn.note("create 2 objs in version")
txn.commit()
obj.value = "77"
txn = transaction.get()
txn.note("modify obj in version")
txn.commit()
t0 = time.time()
snooze()
# undo the modification to generate a mix of backpointers
# and versions for pack to chase
info = db.undoInfo()
db.undo(info[0]["id"])
txn = transaction.get()
txn.note("undo modification")
txn.commit()
self._storage.pack(t0, referencesf)
db.commitVersion("testversion")
txn = transaction.get()
txn.note("commit version")
txn.commit()
cn = db.open()
root = cn.root()
root["obj"] = "no version"
txn = transaction.get()
txn.note("modify obj")
txn.commit()
self._storage.pack(time.time(), referencesf)
def checkPackVersionReachable(self):
db = DB(self._storage)
cn = db.open()
root = cn.root()
names = "a", "b", "c"
for name in names:
root[name] = MinPO(name)
transaction.commit()
for name in names:
cn2 = db.open(version=name)
rt2 = cn2.root()
obj = rt2[name]
obj.value = MinPO("version")
transaction.commit()
cn2.close()
root["d"] = MinPO("d")
transaction.commit()
snooze()
self._storage.pack(time.time(), referencesf)
cn.sync()
# make sure all the non-version data is there
for name, obj in root.items():
self.assertEqual(name, obj.value)
# make sure all the version-data is there,
# and create a new revision in the version
for name in names:
cn2 = db.open(version=name)
rt2 = cn2.root()
obj = rt2[name].value
self.assertEqual(obj.value, "version")
obj.value = "still version"
transaction.commit()
cn2.close()
db.abortVersion("b")
txn = transaction.get()
txn.note("abort version b")
txn.commit()
t = time.time()
snooze()
L = db.undoInfo()
db.undo(L[0]["id"])
txn = transaction.get()
txn.note("undo abort")
txn.commit()
self._storage.pack(t, referencesf)
cn2 = db.open(version="b")
rt2 = cn2.root()
self.assertEqual(rt2["b"].value.value, "still version")
def checkLoadBeforeVersion(self): def checkLoadBeforeVersion(self):
eq = self.assertEqual eq = self.assertEqual
oid = self._storage.new_oid() oid = self._storage.new_oid()
......
...@@ -172,4 +172,3 @@ Blobs are not subclassable:: ...@@ -172,4 +172,3 @@ Blobs are not subclassable::
Traceback (most recent call last): Traceback (most recent call last):
... ...
TypeError: Blobs do not support subclassing. TypeError: Blobs do not support subclassing.
...@@ -15,8 +15,7 @@ ...@@ -15,8 +15,7 @@
Connection support for Blobs tests Connection support for Blobs tests
================================== ==================================
Connections handle Blobs specially. To demonstrate that, we first need a Blob Connections handle Blobs specially. To demonstrate that, we first need a Blob with some data:
with some data:
>>> from ZODB.interfaces import IBlob >>> from ZODB.interfaces import IBlob
>>> from ZODB.blob import Blob >>> from ZODB.blob import Blob
...@@ -26,16 +25,13 @@ with some data: ...@@ -26,16 +25,13 @@ with some data:
>>> data.write("I'm a happy Blob.") >>> data.write("I'm a happy Blob.")
>>> data.close() >>> data.close()
We also need a database with a blob supporting storage. (We're going to use We also need a database with a blob supporting storage:
FileStorage rather than MappingStorage here because we will want ``loadBefore``
for one of our examples.)
>>> import ZODB.FileStorage >>> from ZODB.MappingStorage import MappingStorage
>>> from ZODB.blob import BlobStorage >>> from ZODB.blob import BlobStorage
>>> from ZODB.DB import DB >>> from ZODB.DB import DB
>>> from tempfile import mkdtemp >>> from tempfile import mkdtemp
>>> base_storage = ZODB.FileStorage.FileStorage( >>> base_storage = MappingStorage("test")
... 'BlobTests.fs', create=True)
>>> blob_dir = mkdtemp() >>> blob_dir = mkdtemp()
>>> blob_storage = BlobStorage(blob_dir, base_storage) >>> blob_storage = BlobStorage(blob_dir, base_storage)
>>> database = DB(blob_storage) >>> database = DB(blob_storage)
...@@ -55,55 +51,31 @@ calling the blob's open method: ...@@ -55,55 +51,31 @@ calling the blob's open method:
>>> root['anotherblob'] = anotherblob >>> root['anotherblob'] = anotherblob
>>> nothing = transaction.commit() >>> nothing = transaction.commit()
Getting stuff out of there works similarly: Getting stuff out of there works similar:
>>> transaction2 = transaction.TransactionManager() >>> connection2 = database.open()
>>> connection2 = database.open(transaction_manager=transaction2)
>>> root = connection2.root() >>> root = connection2.root()
>>> blob2 = root['myblob'] >>> blob2 = root['myblob']
>>> IBlob.providedBy(blob2) >>> IBlob.providedBy(blob2)
True True
>>> blob2.open("r").read() >>> blob2.open("r").read()
"I'm a happy Blob." "I'm a happy Blob."
>>> transaction2.abort()
MVCC also works.
>>> transaction3 = transaction.TransactionManager()
>>> connection3 = database.open(transaction_manager=transaction3)
>>> f = connection.root()['myblob'].open('w')
>>> f.write('I am an ecstatic Blob.')
>>> f.close()
>>> transaction.commit()
>>> connection3.root()['myblob'].open('r').read()
"I'm a happy Blob."
>>> transaction2.abort()
>>> transaction3.abort()
>>> connection2.close()
>>> connection3.close()
You can't put blobs into a database that has uses a Non-Blob-Storage, though: You can't put blobs into a database that has uses a Non-Blob-Storage, though:
>>> from ZODB.MappingStorage import MappingStorage
>>> no_blob_storage = MappingStorage() >>> no_blob_storage = MappingStorage()
>>> database2 = DB(no_blob_storage) >>> database2 = DB(no_blob_storage)
>>> connection2 = database2.open(transaction_manager=transaction2) >>> connection3 = database2.open()
>>> root = connection2.root() >>> root = connection3.root()
>>> root['myblob'] = Blob() >>> root['myblob'] = Blob()
>>> transaction2.commit() # doctest: +ELLIPSIS >>> transaction.commit() # doctest: +ELLIPSIS
Traceback (most recent call last): Traceback (most recent call last):
... ...
Unsupported: Storing Blobs in <ZODB.MappingStorage.MappingStorage instance at ...> is not supported. Unsupported: Storing Blobs in <ZODB.MappingStorage.MappingStorage instance at ...> is not supported.
>>> transaction2.abort() While we are testing this, we don't need the storage directory and
>>> connection2.close() databases anymore:
After testing this, we don't need the storage directory and databases anymore:
>>> transaction.abort() >>> transaction.abort()
>>> connection.close()
>>> database.close() >>> database.close()
>>> database2.close() >>> database2.close()
>>> blob_storage.close()
>>> base_storage.cleanup()
...@@ -146,7 +146,7 @@ Reaching into the internals, we can see that db's connection pool now has ...@@ -146,7 +146,7 @@ Reaching into the internals, we can see that db's connection pool now has
two connections available for reuse, and knows about three connections in two connections available for reuse, and knows about three connections in
all: all:
>>> pool = db.pool >>> pool = db._pools['']
>>> len(pool.available) >>> len(pool.available)
2 2
>>> len(pool.all) >>> len(pool.all)
...@@ -219,7 +219,7 @@ closed one out of the available connection stack. ...@@ -219,7 +219,7 @@ closed one out of the available connection stack.
>>> conns = [db.open() for dummy in range(6)] >>> conns = [db.open() for dummy in range(6)]
>>> len(handler.records) # 3 warnings for the "excess" connections >>> len(handler.records) # 3 warnings for the "excess" connections
3 3
>>> pool = db.pool >>> pool = db._pools['']
>>> len(pool.available), len(pool.all) >>> len(pool.available), len(pool.all)
(0, 6) (0, 6)
...@@ -239,12 +239,12 @@ Closing connections adds them to the stack: ...@@ -239,12 +239,12 @@ Closing connections adds them to the stack:
Closing another one will purge the one with MARKER 0 from the stack Closing another one will purge the one with MARKER 0 from the stack
(since it was the first added to the stack): (since it was the first added to the stack):
>>> [c.MARKER for c in pool.available.values()] >>> [c.MARKER for c in pool.available]
[0, 1, 2] [0, 1, 2]
>>> conns[0].close() # MARKER 3 >>> conns[0].close() # MARKER 3
>>> len(pool.available), len(pool.all) >>> len(pool.available), len(pool.all)
(3, 5) (3, 5)
>>> [c.MARKER for c in pool.available.values()] >>> [c.MARKER for c in pool.available]
[1, 2, 3] [1, 2, 3]
Similarly for the other two: Similarly for the other two:
...@@ -252,7 +252,7 @@ Similarly for the other two: ...@@ -252,7 +252,7 @@ Similarly for the other two:
>>> conns[1].close(); conns[2].close() >>> conns[1].close(); conns[2].close()
>>> len(pool.available), len(pool.all) >>> len(pool.available), len(pool.all)
(3, 3) (3, 3)
>>> [c.MARKER for c in pool.available.values()] >>> [c.MARKER for c in pool.available]
[3, 4, 5] [3, 4, 5]
Reducing the pool size may also purge the oldest closed connections: Reducing the pool size may also purge the oldest closed connections:
...@@ -260,7 +260,7 @@ Reducing the pool size may also purge the oldest closed connections: ...@@ -260,7 +260,7 @@ Reducing the pool size may also purge the oldest closed connections:
>>> db.setPoolSize(2) # gets rid of MARKER 3 >>> db.setPoolSize(2) # gets rid of MARKER 3
>>> len(pool.available), len(pool.all) >>> len(pool.available), len(pool.all)
(2, 2) (2, 2)
>>> [c.MARKER for c in pool.available.values()] >>> [c.MARKER for c in pool.available]
[4, 5] [4, 5]
Since MARKER 5 is still the last one added to the stack, it will be the Since MARKER 5 is still the last one added to the stack, it will be the
...@@ -297,7 +297,7 @@ Now open more connections so that the total exceeds pool_size (2): ...@@ -297,7 +297,7 @@ Now open more connections so that the total exceeds pool_size (2):
>>> conn1 = db.open() >>> conn1 = db.open()
>>> conn2 = db.open() >>> conn2 = db.open()
>>> pool = db.pool >>> pool = db._pools['']
>>> len(pool.all), len(pool.available) # all Connections are in use >>> len(pool.all), len(pool.available) # all Connections are in use
(3, 0) (3, 0)
......
...@@ -138,21 +138,6 @@ Verify all the values are as expected: ...@@ -138,21 +138,6 @@ Verify all the values are as expected:
>>> db.close() >>> db.close()
""" """
def testIsReadonly():
"""\
The connection isReadonly method relies on the _storage to have an isReadOnly.
We simply rely on the underlying storage method.
>>> import ZODB.tests.util
>>> db = ZODB.tests.util.DB()
>>> connection = db.open()
>>> root = connection.root()
>>> root['a'] = 1
>>> sp = transaction.savepoint()
>>> connection.isReadOnly()
False
"""
def test_suite(): def test_suite():
return unittest.TestSuite(( return unittest.TestSuite((
doctest.DocFileSuite('testConnectionSavepoint.txt'), doctest.DocFileSuite('testConnectionSavepoint.txt'),
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
import os import os
import time import time
import unittest import unittest
import datetime import warnings
import transaction import transaction
...@@ -35,6 +35,8 @@ class DBTests(unittest.TestCase): ...@@ -35,6 +35,8 @@ class DBTests(unittest.TestCase):
self.__path = os.path.abspath('test.fs') self.__path = os.path.abspath('test.fs')
store = ZODB.FileStorage.FileStorage(self.__path) store = ZODB.FileStorage.FileStorage(self.__path)
self.db = ZODB.DB(store) self.db = ZODB.DB(store)
warnings.filterwarnings(
'ignore', message='Versions are deprecated', module=__name__)
def tearDown(self): def tearDown(self):
self.db.close() self.db.close()
...@@ -42,8 +44,8 @@ class DBTests(unittest.TestCase): ...@@ -42,8 +44,8 @@ class DBTests(unittest.TestCase):
if os.path.exists(self.__path+s): if os.path.exists(self.__path+s):
os.remove(self.__path+s) os.remove(self.__path+s)
def dowork(self): def dowork(self, version=''):
c = self.db.open() c = self.db.open(version)
r = c.root() r = c.root()
o = r[time.time()] = MinPO(0) o = r[time.time()] = MinPO(0)
transaction.commit() transaction.commit()
...@@ -51,16 +53,85 @@ class DBTests(unittest.TestCase): ...@@ -51,16 +53,85 @@ class DBTests(unittest.TestCase):
o.value = MinPO(i) o.value = MinPO(i)
transaction.commit() transaction.commit()
o = o.value o = o.value
serial = o._p_serial
root_serial = r._p_serial
c.close() c.close()
return serial, root_serial
# make sure the basic methods are callable # make sure the basic methods are callable
def testSets(self): def testSets(self):
self.db.setCacheSize(15) self.db.setCacheSize(15)
self.db.setHistoricalCacheSize(15) self.db.setVersionCacheSize(15)
def test_removeVersionPool(self):
# Test that we can remove a version pool
# This is white box because we check some internal data structures
self.dowork()
self.dowork('v2')
c1 = self.db.open('v1')
c1.close() # return to pool
c12 = self.db.open('v1')
c12.close() # return to pool
self.assert_(c1 is c12) # should be same
pools = self.db._pools
self.assertEqual(len(pools), 3)
self.assertEqual(nconn(pools), 3)
self.db.removeVersionPool('v1')
self.assertEqual(len(pools), 2)
self.assertEqual(nconn(pools), 2)
c12 = self.db.open('v1')
c12.close() # return to pool
self.assert_(c1 is not c12) # should be different
self.assertEqual(len(pools), 3)
self.assertEqual(nconn(pools), 3)
def _test_for_leak(self):
self.dowork()
self.dowork('v2')
while 1:
c1 = self.db.open('v1')
self.db.removeVersionPool('v1')
c1.close() # return to pool
def test_removeVersionPool_while_connection_open(self):
# Test that we can remove a version pool
# This is white box because we check some internal data structures
self.dowork()
self.dowork('v2')
c1 = self.db.open('v1')
c1.close() # return to pool
c12 = self.db.open('v1')
self.assert_(c1 is c12) # should be same
pools = self.db._pools
self.assertEqual(len(pools), 3)
self.assertEqual(nconn(pools), 3)
self.db.removeVersionPool('v1')
self.assertEqual(len(pools), 2)
self.assertEqual(nconn(pools), 2)
c12.close() # should leave pools alone
self.assertEqual(len(pools), 2)
self.assertEqual(nconn(pools), 2)
c12 = self.db.open('v1')
c12.close() # return to pool
self.assert_(c1 is not c12) # should be different
self.assertEqual(len(pools), 3)
self.assertEqual(nconn(pools), 3)
def test_references(self): def test_references(self):
......
...@@ -136,6 +136,20 @@ class ZODBTests(unittest.TestCase): ...@@ -136,6 +136,20 @@ class ZODBTests(unittest.TestCase):
def checkExportImportAborted(self): def checkExportImportAborted(self):
self.checkExportImport(abort_it=True) self.checkExportImport(abort_it=True)
def checkVersionOnly(self):
# Make sure the changes to make empty transactions a no-op
# still allow things like abortVersion(). This should work
# because abortVersion() calls tpc_begin() itself.
conn = self._db.open("version")
try:
r = conn.root()
r[1] = 1
transaction.commit()
finally:
conn.close()
self._db.abortVersion("version")
transaction.commit()
def checkResetCache(self): def checkResetCache(self):
# The cache size after a reset should be 0. Note that # The cache size after a reset should be 0. Note that
# _resetCache is not a public API, but the resetCaches() # _resetCache is not a public API, but the resetCaches()
......
############################################################################## ##############################################################################
# #
# Copyright (c) 2007 Zope Corporation and Contributors. # Copyright (c) 2006 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -11,34 +11,27 @@ ...@@ -11,34 +11,27 @@
# FOR A PARTICULAR PURPOSE. # FOR A PARTICULAR PURPOSE.
# #
############################################################################## ##############################################################################
"""Misc tests :)
""" """
$Id$
"""
import unittest import unittest
from zope.testing import doctest, module from zope.testing import doctest
def conflict_error_retains_data_passed():
r"""
def setUp(test): ConflictError can be passed a data record which it claims to retain as
module.setUp(test, 'historical_connections_txt') an attribute.
def tearDown(test): >>> import ZODB.POSException
test.globs['db'].close() >>>
test.globs['db2'].close() >>> ZODB.POSException.ConflictError(data='cM\nC\n').data
test.globs['storage'].close() 'cM\nC\n'
test.globs['storage'].cleanup()
# the DB class masks the module because of __init__ shenanigans """
DB_module = __import__('ZODB.DB', globals(), locals(), ['chicken'])
DB_module.time = test.globs['original_time']
module.tearDown(test)
def test_suite(): def test_suite():
return unittest.TestSuite(( return unittest.TestSuite((
doctest.DocFileSuite('../historical_connections.txt', doctest.DocTestSuite(),
setUp=setUp,
tearDown=tearDown,
optionflags=doctest.INTERPRET_FOOTNOTES,
),
)) ))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
============
Transactions
============
This package contains a generic transaction implementation for Python. It is
mainly used by the ZODB, though.
Note that the data manager API, ``transaction.interfaces.IDataManager``,
is syntactically simple, but semantically complex. The semantics
were not easy to express in the interface. This could probably use
more work. The semantics are presented in detail through examples of
a sample data manager in ``transaction.tests.test_SampleDataManager``.
############################################################################
#
# Copyright (c) 2001, 2002, 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
############################################################################
"""Exported transaction functions.
$Id$
"""
from transaction._transaction import Transaction
from transaction._manager import TransactionManager, ThreadTransactionManager
manager = ThreadTransactionManager()
get = manager.get
begin = manager.begin
commit = manager.commit
abort = manager.abort
doom = manager.doom
isDoomed = manager.isDoomed
savepoint = manager.savepoint
############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
############################################################################
"""A TransactionManager controls transaction boundaries.
It coordinates application code and resource managers, so that they
are associated with the right transaction.
"""
import thread
from ZODB.utils import WeakSet, deprecated37
from transaction._transaction import Transaction
# Used for deprecated arguments. ZODB.utils.DEPRECATED_ARGUMENT was
# too hard to use here, due to the convoluted import dance across
# __init__.py files.
_marker = object()
# We have to remember sets of synch objects, especially Connections.
# But we don't want mere registration with a transaction manager to
# keep a synch object alive forever; in particular, it's common
# practice not to explicitly close Connection objects, and keeping
# a Connection alive keeps a potentially huge number of other objects
# alive (e.g., the cache, and everything reachable from it too).
# Therefore we use "weak sets" internally.
#
# Call the ISynchronizer newTransaction() method on every element of
# WeakSet synchs.
# A transaction manager needs to do this whenever begin() is called.
# Since it would be good if tm.get() returned the new transaction while
# newTransaction() is running, calling this has to be delayed until after
# the transaction manager has done whatever it needs to do to make its
# get() return the new txn.
def _new_transaction(txn, synchs):
if synchs:
synchs.map(lambda s: s.newTransaction(txn))
# Important: we must always pass a WeakSet (even if empty) to the Transaction
# constructor: synchronizers are registered with the TM, but the
# ISynchronizer xyzCompletion() methods are called by Transactions without
# consulting the TM, so we need to pass a mutable collection of synchronizers
# so that Transactions "see" synchronizers that get registered after the
# Transaction object is constructed.
class TransactionManager(object):
def __init__(self):
self._txn = None
self._synchs = WeakSet()
def begin(self):
if self._txn is not None:
self._txn.abort()
txn = self._txn = Transaction(self._synchs, self)
_new_transaction(txn, self._synchs)
return txn
def get(self):
if self._txn is None:
self._txn = Transaction(self._synchs, self)
return self._txn
def free(self, txn):
assert txn is self._txn
self._txn = None
def registerSynch(self, synch):
self._synchs.add(synch)
def unregisterSynch(self, synch):
self._synchs.remove(synch)
def isDoomed(self):
return self.get().isDoomed()
def doom(self):
return self.get().doom()
def commit(self):
return self.get().commit()
def abort(self):
return self.get().abort()
def savepoint(self, optimistic=False):
return self.get().savepoint(optimistic)
class ThreadTransactionManager(TransactionManager):
"""Thread-aware transaction manager.
Each thread is associated with a unique transaction.
"""
def __init__(self):
# _threads maps thread ids to transactions
self._txns = {}
# _synchs maps a thread id to a WeakSet of registered synchronizers.
# The WeakSet is passed to the Transaction constructor, because the
# latter needs to call the synchronizers when it commits.
self._synchs = {}
def begin(self):
tid = thread.get_ident()
txn = self._txns.get(tid)
if txn is not None:
txn.abort()
synchs = self._synchs.get(tid)
if synchs is None:
synchs = self._synchs[tid] = WeakSet()
txn = self._txns[tid] = Transaction(synchs, self)
_new_transaction(txn, synchs)
return txn
def get(self):
tid = thread.get_ident()
txn = self._txns.get(tid)
if txn is None:
synchs = self._synchs.get(tid)
if synchs is None:
synchs = self._synchs[tid] = WeakSet()
txn = self._txns[tid] = Transaction(synchs, self)
return txn
def free(self, txn):
tid = thread.get_ident()
assert txn is self._txns.get(tid)
del self._txns[tid]
def registerSynch(self, synch):
tid = thread.get_ident()
ws = self._synchs.get(tid)
if ws is None:
ws = self._synchs[tid] = WeakSet()
ws.add(synch)
def unregisterSynch(self, synch):
tid = thread.get_ident()
ws = self._synchs[tid]
ws.remove(synch)
This diff is collapsed.
This diff is collapsed.
Savepoints
==========
Savepoints provide a way to save to disk intermediate work done during
a transaction allowing:
- partial transaction (subtransaction) rollback (abort)
- state of saved objects to be freed, freeing on-line memory for other
uses
Savepoints make it possible to write atomic subroutines that don't
make top-level transaction commitments.
Applications
------------
To demonstrate how savepoints work with transactions, we've provided a sample
data manager implementation that provides savepoint support. The primary
purpose of this data manager is to provide code that can be read to understand
how savepoints work. The secondary purpose is to provide support for
demonstrating the correct operation of savepoint support within the
transaction system. This data manager is very simple. It provides flat
storage of named immutable values, like strings and numbers.
>>> import transaction.tests.savepointsample
>>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
>>> dm['name'] = 'bob'
As with other data managers, we can commit changes:
>>> transaction.commit()
>>> dm['name']
'bob'
and abort changes:
>>> dm['name'] = 'sally'
>>> dm['name']
'sally'
>>> transaction.abort()
>>> dm['name']
'bob'
Now, let's look at an application that manages funds for people. It allows
deposits and debits to be entered for multiple people. It accepts a sequence
of entries and generates a sequence of status messages. For each entry, it
applies the change and then validates the user's account. If the user's
account is invalid, we roll back the change for that entry. The success or
failure of an entry is indicated in the output status. First we'll initialize
some accounts:
>>> dm['bob-balance'] = 0.0
>>> dm['bob-credit'] = 0.0
>>> dm['sally-balance'] = 0.0
>>> dm['sally-credit'] = 100.0
>>> transaction.commit()
Now, we'll define a validation function to validate an account:
>>> def validate_account(name):
... if dm[name+'-balance'] + dm[name+'-credit'] < 0:
... raise ValueError('Overdrawn', name)
And a function to apply entries. If the function fails in some unexpected
way, it rolls back all of its changes and prints the error:
>>> def apply_entries(entries):
... savepoint = transaction.savepoint()
... try:
... for name, amount in entries:
... entry_savepoint = transaction.savepoint()
... try:
... dm[name+'-balance'] += amount
... validate_account(name)
... except ValueError, error:
... entry_savepoint.rollback()
... print 'Error', str(error)
... else:
... print 'Updated', name
... except Exception, error:
... savepoint.rollback()
... print 'Unexpected exception', error
Now let's try applying some entries:
>>> apply_entries([
... ('bob', 10.0),
... ('sally', 10.0),
... ('bob', 20.0),
... ('sally', 10.0),
... ('bob', -100.0),
... ('sally', -100.0),
... ])
Updated bob
Updated sally
Updated bob
Updated sally
Error ('Overdrawn', 'bob')
Updated sally
>>> dm['bob-balance']
30.0
>>> dm['sally-balance']
-80.0
If we provide entries that cause an unexpected error:
>>> apply_entries([
... ('bob', 10.0),
... ('sally', 10.0),
... ('bob', '20.0'),
... ('sally', 10.0),
... ])
Updated bob
Updated sally
Unexpected exception unsupported operand type(s) for +=: 'float' and 'str'
Because the apply_entries used a savepoint for the entire function, it was
able to rollback the partial changes without rolling back changes made in the
previous call to ``apply_entries``:
>>> dm['bob-balance']
30.0
>>> dm['sally-balance']
-80.0
If we now abort the outer transactions, the earlier changes will go
away:
>>> transaction.abort()
>>> dm['bob-balance']
0.0
>>> dm['sally-balance']
0.0
Savepoint invalidation
----------------------
A savepoint can be used any number of times:
>>> dm['bob-balance'] = 100.0
>>> dm['bob-balance']
100.0
>>> savepoint = transaction.savepoint()
>>> dm['bob-balance'] = 200.0
>>> dm['bob-balance']
200.0
>>> savepoint.rollback()
>>> dm['bob-balance']
100.0
>>> savepoint.rollback() # redundant, but should be harmless
>>> dm['bob-balance']
100.0
>>> dm['bob-balance'] = 300.0
>>> dm['bob-balance']
300.0
>>> savepoint.rollback()
>>> dm['bob-balance']
100.0
However, using a savepoint invalidates any savepoints that come after it:
>>> dm['bob-balance'] = 200.0
>>> dm['bob-balance']
200.0
>>> savepoint1 = transaction.savepoint()
>>> dm['bob-balance'] = 300.0
>>> dm['bob-balance']
300.0
>>> savepoint2 = transaction.savepoint()
>>> savepoint.rollback()
>>> dm['bob-balance']
100.0
>>> savepoint2.rollback()
Traceback (most recent call last):
...
InvalidSavepointRollbackError
>>> savepoint1.rollback()
Traceback (most recent call last):
...
InvalidSavepointRollbackError
>>> transaction.abort()
Databases without savepoint support
-----------------------------------
Normally it's an error to use savepoints with databases that don't support
savepoints:
>>> dm_no_sp = transaction.tests.savepointsample.SampleDataManager()
>>> dm_no_sp['name'] = 'bob'
>>> transaction.commit()
>>> dm_no_sp['name'] = 'sally'
>>> savepoint = transaction.savepoint()
Traceback (most recent call last):
...
TypeError: ('Savepoints unsupported', {'name': 'bob'})
>>> transaction.abort()
However, a flag can be passed to the transaction savepoint method to indicate
that databases without savepoint support should be tolerated until a savepoint
is rolled back. This allows transactions to proceed if there are no reasons
to roll back:
>>> dm_no_sp['name'] = 'sally'
>>> savepoint = transaction.savepoint(1)
>>> dm_no_sp['name'] = 'sue'
>>> transaction.commit()
>>> dm_no_sp['name']
'sue'
>>> dm_no_sp['name'] = 'sam'
>>> savepoint = transaction.savepoint(1)
>>> savepoint.rollback()
Traceback (most recent call last):
...
TypeError: ('Savepoints unsupported', {'name': 'sam'})
Failures
--------
If a failure occurs when creating or rolling back a savepoint, the transaction
state will be uncertain and the transaction will become uncommitable. From
that point on, most transaction operations, including commit, will fail until
the transaction is aborted.
In the previous example, we got an error when we tried to rollback the
savepoint. If we try to commit the transaction, the commit will fail:
>>> transaction.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TransactionFailedError: An operation previously failed, with traceback:
...
TypeError: ('Savepoints unsupported', {'name': 'sam'})
<BLANKLINE>
We have to abort it to make any progress:
>>> transaction.abort()
Similarly, in our earlier example, where we tried to take a savepoint with a
data manager that didn't support savepoints:
>>> dm_no_sp['name'] = 'sally'
>>> dm['name'] = 'sally'
>>> savepoint = transaction.savepoint()
Traceback (most recent call last):
...
TypeError: ('Savepoints unsupported', {'name': 'sue'})
>>> transaction.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TransactionFailedError: An operation previously failed, with traceback:
...
TypeError: ('Savepoints unsupported', {'name': 'sue'})
<BLANKLINE>
>>> transaction.abort()
After clearing the transaction with an abort, we can get on with new
transactions:
>>> dm_no_sp['name'] = 'sally'
>>> dm['name'] = 'sally'
>>> transaction.commit()
>>> dm_no_sp['name']
'sally'
>>> dm['name']
'sally'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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