tested django-newsletter

This commit is contained in:
Esther Kleinhenz
2018-10-26 15:21:17 +02:00
parent 7ed667d043
commit 811b7c5453
2352 changed files with 448169 additions and 1 deletions
@@ -0,0 +1,109 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""ZODB transfer activity monitoring
"""
import time
from . import utils
class ActivityMonitor(object):
"""ZODB load/store activity monitor
This simple implementation just keeps a small log in memory
and iterates over the log when getActivityAnalysis() is called.
It assumes that log entries are added in chronological sequence.
"""
def __init__(self, history_length=3600):
self.history_length = history_length # Number of seconds
self.log = [] # [(time, loads, stores)]
self.trim_lock = utils.Lock()
def closedConnection(self, conn):
log = self.log
now = time.time()
loads, stores = conn.getTransferCounts(1)
log.append((now, loads, stores))
self.trim(now)
def trim(self, now):
with self.trim_lock:
log = self.log
cutoff = now - self.history_length
n = 0
loglen = len(log)
while n < loglen and log[n][0] < cutoff:
n = n + 1
if n:
del log[:n]
def setHistoryLength(self, history_length):
self.history_length = history_length
self.trim(time.time())
def getHistoryLength(self):
return self.history_length
def getActivityAnalysis(self, start=0, end=0, divisions=10):
res = []
now = time.time()
if start == 0:
start = now - self.history_length
if end == 0:
end = now
for n in range(divisions):
res.append({
'start': start + (end - start) * n / divisions,
'end': start + (end - start) * (n + 1) / divisions,
'loads': 0,
'stores': 0,
'connections': 0,
})
div = res[0]
div_end = div['end']
div_index = 0
connections = 0
total_loads = 0
total_stores = 0
for t, loads, stores in self.log:
if t < start:
# We could use a binary search to find the start.
continue
elif t > end:
# We could use a binary search to find the end also.
break
while t > div_end:
div['loads'] = total_loads
div['stores'] = total_stores
div['connections'] = connections
total_loads = 0
total_stores = 0
connections = 0
div_index = div_index + 1
if div_index < divisions:
div = res[div_index]
div_end = div['end']
connections = connections + 1
total_loads = total_loads + loads
total_stores = total_stores + stores
div['stores'] = div['stores'] + total_stores
div['loads'] = div['loads'] + total_loads
div['connections'] = div['connections'] + connections
return res
@@ -0,0 +1,381 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""Storage base class that is mostly a mistake
The base class here is tightly coupled with its subclasses and
its use is not recommended. It's still here for historical reasons.
"""
from __future__ import print_function
import time
import logging
import sys
from struct import pack as _structpack, unpack as _structunpack
import zope.interface
from persistent.TimeStamp import TimeStamp
import ZODB.interfaces
from . import POSException, utils
from .Connection import TransactionMetaData
from .utils import z64, oid_repr, byte_ord, byte_chr, load_current
from .UndoLogCompatible import UndoLogCompatible
from ._compat import dumps, _protocol, py2_hasattr
log = logging.getLogger("ZODB.BaseStorage")
class BaseStorage(UndoLogCompatible):
"""Base class that supports storage implementations.
XXX Base classes like this are an attractive nuisance. They often
introduce more complexity than they save. While important logic
is implemented here, we should consider exposing it as utility
functions or as objects that can be used through composition.
A subclass must define the following methods:
load()
store()
close()
cleanup()
lastTransaction()
It must override these hooks:
_begin()
_vote()
_abort()
_finish()
_clear_temp()
If it stores multiple revisions, it should implement
loadSerial()
loadBefore()
Each storage will have two locks that are accessed via lock
acquire and release methods bound to the instance. (Yuck.)
_lock_acquire / _lock_release (reentrant)
_commit_lock_acquire / _commit_lock_release
The commit lock is acquired in tpc_begin() and released in
tpc_abort() and tpc_finish(). It is never acquired with the other
lock held.
The other lock appears to protect _oid and _transaction and
perhaps other things. It is always held when load() is called, so
presumably the load() implementation should also acquire the lock.
"""
_transaction=None # Transaction that is being committed
_tstatus=' ' # Transaction status, used for copying data
_is_read_only = False
def __init__(self, name, base=None):
self.__name__= name
log.debug("create storage %s", self.__name__)
# Allocate locks:
self._lock = utils.RLock()
self._commit_lock = utils.Lock()
# Needed by external storages that use this dumb api :(
self._lock_acquire = self._lock.acquire
self._lock_release = self._lock.release
self._commit_lock_acquire = self._commit_lock.acquire
self._commit_lock_release = self._commit_lock.release
t = time.time()
t = self._ts = TimeStamp(*(time.gmtime(t)[:5] + (t%60,)))
self._tid = t.raw()
# ._oid is the highest oid in use (0 is always in use -- it's
# a reserved oid for the root object). Our new_oid() method
# increments it by 1, and returns the result. It's really a
# 64-bit integer stored as an 8-byte big-endian string.
oid = getattr(base, '_oid', None)
if oid is None:
self._oid = z64
else:
self._oid = oid
# In case that conflicts are resolved during store,
# this collects oids to be returned by tpc_vote.
self._resolved = []
def sortKey(self):
"""Return a string that can be used to sort storage instances.
The key must uniquely identify a storage and must be the same
across multiple instantiations of the same storage.
"""
# name may not be sufficient, e.g. ZEO has a user-definable name.
return self.__name__
def getName(self):
return self.__name__
def getSize(self):
return len(self)*300 # WAG!
def history(self, oid, version, length=1, filter=None):
return ()
def new_oid(self):
if self._is_read_only:
raise POSException.ReadOnlyError()
with self._lock:
last = self._oid
d = byte_ord(last[-1])
if d < 255: # fast path for the usual case
last = last[:-1] + byte_chr(d+1)
else: # there's a carry out of the last byte
last_as_long, = _structunpack(">Q", last)
last = _structpack(">Q", last_as_long + 1)
self._oid = last
return last
# Update the maximum oid in use, under protection of a lock. The
# maximum-in-use attribute is changed only if possible_new_max_oid is
# larger than its current value.
def set_max_oid(self, possible_new_max_oid):
with self._lock:
if possible_new_max_oid > self._oid:
self._oid = possible_new_max_oid
def registerDB(self, db):
pass # we don't care
def isReadOnly(self):
return self._is_read_only
def tpc_abort(self, transaction):
with self._lock:
if transaction is not self._transaction:
return
try:
self._abort()
self._clear_temp()
self._transaction = None
finally:
self._commit_lock_release()
def _abort(self):
"""Subclasses should redefine this to supply abort actions"""
pass
def tpc_begin(self, transaction, tid=None, status=' '):
if self._is_read_only:
raise POSException.ReadOnlyError()
with self._lock:
if self._transaction is transaction:
raise POSException.StorageTransactionError(
"Duplicate tpc_begin calls for same transaction")
self._commit_lock.acquire()
with self._lock:
self._transaction = transaction
self._clear_temp()
user = transaction.user
desc = transaction.description
ext = transaction.extension
if ext:
ext = dumps(ext, _protocol)
else:
ext = ""
self._ude = user, desc, ext
if tid is None:
now = time.time()
t = TimeStamp(*(time.gmtime(now)[:5] + (now % 60,)))
self._ts = t = t.laterThan(self._ts)
self._tid = t.raw()
else:
self._ts = TimeStamp(tid)
self._tid = tid
del self._resolved[:]
self._tstatus = status
self._begin(self._tid, user, desc, ext)
def tpc_transaction(self):
return self._transaction
def _begin(self, tid, u, d, e):
"""Subclasses should redefine this to supply transaction start actions.
"""
pass
def tpc_vote(self, transaction):
with self._lock:
if transaction is not self._transaction:
raise POSException.StorageTransactionError(
"tpc_vote called with wrong transaction")
return self._vote()
def _vote(self):
"""Subclasses should redefine this to supply transaction vote actions.
"""
return self._resolved
def tpc_finish(self, transaction, f=None):
# It's important that the storage calls the function we pass
# while it still has its lock. We don't want another thread
# to be able to read any updated data until we've had a chance
# to send an invalidation message to all of the other
# connections!
with self._lock:
if transaction is not self._transaction:
raise POSException.StorageTransactionError(
"tpc_finish called with wrong transaction")
try:
if f is not None:
f(self._tid)
u, d, e = self._ude
self._finish(self._tid, u, d, e)
self._clear_temp()
finally:
self._ude = None
self._transaction = None
self._commit_lock.release()
return self._tid
def _finish(self, tid, u, d, e):
"""Subclasses should redefine this to supply transaction finish actions
"""
pass
def lastTransaction(self):
with self._lock:
return self._ltid
def getTid(self, oid):
with self._lock:
return load_current(self, oid)[1]
def loadSerial(self, oid, serial):
raise POSException.Unsupported(
"Retrieval of historical revisions is not supported")
def loadBefore(self, oid, tid):
"""Return most recent revision of oid before tid committed."""
return None
def copyTransactionsFrom(self, other, verbose=0):
"""Copy transactions from another storage.
This is typically used for converting data from one storage to
another. `other` must have an .iterator() method.
"""
copy(other, self, verbose)
def copy(source, dest, verbose=0):
"""Copy transactions from a source to a destination storage
This is typically used for converting data from one storage to
another. `source` must have an .iterator() method.
"""
_ts = None
ok = 1
preindex = {};
preget = preindex.get
# restore() is a new storage API method which has an identical
# signature to store() except that it does not return anything.
# Semantically, restore() is also identical to store() except that it
# doesn't do the ConflictError or VersionLockError consistency
# checks. The reason to use restore() over store() in this method is
# that store() cannot be used to copy transactions spanning a version
# commit or abort, or over transactional undos.
#
# We'll use restore() if it's available, otherwise we'll fall back to
# using store(). However, if we use store, then
# copyTransactionsFrom() may fail with VersionLockError or
# ConflictError.
restoring = py2_hasattr(dest, 'restore')
fiter = source.iterator()
for transaction in fiter:
tid = transaction.tid
if _ts is None:
_ts = TimeStamp(tid)
else:
t = TimeStamp(tid)
if t <= _ts:
if ok: print(('Time stamps out of order %s, %s' % (_ts, t)))
ok = 0
_ts = t.laterThan(_ts)
tid = _ts.raw()
else:
_ts = t
if not ok:
print(('Time stamps back in order %s' % (t)))
ok = 1
if verbose:
print(_ts)
dest.tpc_begin(transaction, tid, transaction.status)
for r in transaction:
oid = r.oid
if verbose:
print(oid_repr(oid), r.version, len(r.data))
if restoring:
dest.restore(oid, r.tid, r.data, r.version,
r.data_txn, transaction)
else:
pre = preget(oid, None)
dest.store(oid, pre, r.data, r.version, transaction)
preindex[oid] = tid
dest.tpc_vote(transaction)
dest.tpc_finish(transaction)
# defined outside of BaseStorage to facilitate independent reuse.
# just depends on _transaction attr and getTid method.
def checkCurrentSerialInTransaction(self, oid, serial, transaction):
if transaction is not self._transaction:
raise POSException.StorageTransactionError(self, transaction)
committed_tid = self.getTid(oid)
if committed_tid != serial:
raise POSException.ReadConflictError(
oid=oid, serials=(committed_tid, serial))
BaseStorage.checkCurrentSerialInTransaction = checkCurrentSerialInTransaction
@zope.interface.implementer(ZODB.interfaces.IStorageTransactionInformation)
class TransactionRecord(TransactionMetaData):
"""Abstract base class for iterator protocol"""
def __init__(self, tid, status, user, description, extension):
self.tid = tid
self.status = status
TransactionMetaData.__init__(self, user, description, extension)
@zope.interface.implementer(ZODB.interfaces.IStorageRecordInformation)
class DataRecord(object):
"""Abstract base class for iterator protocol"""
version = ''
def __init__(self, oid, tid, data, prev):
self.oid = oid
self.tid = tid
self.data = data
self.data_txn = prev
@@ -0,0 +1,315 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
import logging
import six
import zope.interface
from ZODB.POSException import ConflictError
from ZODB.loglevels import BLATHER
from ZODB._compat import (
BytesIO, PersistentUnpickler, PersistentPickler, _protocol)
# Subtle: Python 2.x has pickle.PicklingError and cPickle.PicklingError,
# and these are unrelated classes! So we shouldn't use pickle.PicklingError,
# since on Python 2, ZODB._compat.pickle is cPickle.
from pickle import PicklingError
logger = logging.getLogger('ZODB.ConflictResolution')
class BadClassName(Exception):
pass
class BadClass(object):
def __init__(self, *args):
self.args = args
def __reduce__(self):
raise BadClassName(*self.args)
_class_cache = {}
_class_cache_get = _class_cache.get
def find_global(*args):
cls = _class_cache_get(args, 0)
if cls == 0:
# Not cached. Try to import
try:
module = __import__(args[0], {}, {}, ['cluck'])
except ImportError:
cls = 1
else:
cls = getattr(module, args[1], 1)
_class_cache[args] = cls
if cls == 1:
logger.log(BLATHER, "Unable to load class", exc_info=True)
if cls == 1:
# Not importable
if (isinstance(args, tuple) and len(args) == 2 and
isinstance(args[0], six.string_types) and
isinstance(args[1], six.string_types)
):
return BadClass(*args)
else:
raise BadClassName(*args)
return cls
def state(self, oid, serial, prfactory, p=''):
p = p or self.loadSerial(oid, serial)
p = self._crs_untransform_record_data(p)
file = BytesIO(p)
unpickler = PersistentUnpickler(
find_global, prfactory.persistent_load, file)
unpickler.load() # skip the class tuple
return unpickler.load()
class IPersistentReference(zope.interface.Interface):
'''public contract for references to persistent objects from an object
with conflicts.'''
oid = zope.interface.Attribute(
'The oid of the persistent object that this reference represents')
database_name = zope.interface.Attribute(
'''The name of the database of the reference, *if* different.
If not different, None.''')
klass = zope.interface.Attribute(
'''class meta data. Presence is not reliable.''')
weak = zope.interface.Attribute(
'''bool: whether this reference is weak''')
def __cmp__(other):
'''if other is equivalent reference, return 0; else raise ValueError.
Equivalent in this case means that oid and database_name are the same.
If either is a weak reference, we only support `is` equivalence, and
otherwise raise a ValueError even if the datbase_names and oids are
the same, rather than guess at the correct semantics.
It is impossible to sort reliably, since the actual persistent
class may have its own comparison, and we have no idea what it is.
We assert that it is reasonably safe to assume that an object is
equivalent to itself, but that's as much as we can say.
We don't compare on 'is other', despite the
PersistentReferenceFactory.data cache, because it is possible to
have two references to the same object that are spelled with different
data (for instance, one with a class and one without).'''
@zope.interface.implementer(IPersistentReference)
class PersistentReference(object):
weak = False
oid = database_name = klass = None
def __init__(self, data):
self.data = data
# see serialize.py, ObjectReader._persistent_load
if isinstance(data, tuple):
self.oid, klass = data
if isinstance(klass, BadClass):
# We can't use the BadClass directly because, if
# resolution succeeds, there's no good way to pickle
# it. Fortunately, a class reference in a persistent
# reference is allowed to be a module+name tuple.
self.data = self.oid, klass.args
elif isinstance(data, (bytes, str)):
self.oid = data
else: # a list
reference_type = data[0]
# 'm' = multi_persistent: (database_name, oid, klass)
# 'n' = multi_oid: (database_name, oid)
# 'w' = persistent weakref: (oid)
# or persistent weakref: (oid, database_name)
# else it is a weakref: reference_type
if reference_type == 'm':
self.database_name, self.oid, klass = data[1]
if isinstance(klass, BadClass):
# see above wrt BadClass
data[1] = self.database_name, self.oid, klass.args
elif reference_type == 'n':
self.database_name, self.oid = data[1]
elif reference_type == 'w':
try:
self.oid, = data[1]
except ValueError:
self.oid, self.database_name = data[1]
self.weak = True
else:
assert len(data) == 1, 'unknown reference format'
self.oid = data[0]
self.weak = True
if not isinstance(self.oid, (bytes, type(None))):
assert isinstance(self.oid, str)
# this happens on Python 3 when all bytes in the oid are < 0x80
self.oid = self.oid.encode('ascii')
def __cmp__(self, other):
if self is other or (
isinstance(other, PersistentReference) and
self.oid == other.oid and
self.database_name == other.database_name and
not self.weak and
not other.weak):
return 0
else:
raise ValueError(
"can't reliably compare against different "
"PersistentReferences")
# Python 3 dropped __cmp__
def __eq__(self, other):
return self.__cmp__(other) == 0
def __ne__(self, other):
return self.__cmp__(other) != 0
def __lt__(self, other):
return self.__cmp__(other) < 0
def __gt__(self, other):
return self.__cmp__(other) > 0
def __le__(self, other):
return self.__cmp__(other) <= 0
def __ge__(self, other):
return self.__cmp__(other) >= 0
def __repr__(self):
return "PR(%s %s)" % (id(self), self.data)
def __getstate__(self):
raise PicklingError("Can't pickle PersistentReference")
@property
def klass(self):
# for tests
data = self.data
if isinstance(data, tuple):
return data[1]
elif isinstance(data, list) and data[0] == 'm':
return data[1][2]
class PersistentReferenceFactory(object):
data = None
def persistent_load(self, ref):
if self.data is None:
self.data = {}
key = tuple(ref) # lists are not hashable; formats are different enough
# even after eliminating list/tuple distinction
r = self.data.get(key, None)
if r is None:
r = PersistentReference(ref)
self.data[key] = r
return r
def persistent_id(object):
if getattr(object, '__class__', 0) is not PersistentReference:
return None
return object.data
_unresolvable = {}
def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle,
committedData=b''):
# class_tuple, old, committed, newstate = ('',''), 0, 0, 0
klass = 'n/a'
try:
prfactory = PersistentReferenceFactory()
newpickle = self._crs_untransform_record_data(newpickle)
file = BytesIO(newpickle)
unpickler = PersistentUnpickler(
find_global, prfactory.persistent_load, file)
meta = unpickler.load()
if isinstance(meta, tuple):
klass = meta[0]
newargs = meta[1] or ()
if isinstance(klass, tuple):
klass = find_global(*klass)
else:
klass = meta
newargs = ()
if klass in _unresolvable:
raise ConflictError
inst = klass.__new__(klass, *newargs)
try:
resolve = inst._p_resolveConflict
except AttributeError:
_unresolvable[klass] = 1
raise ConflictError
oldData = self.loadSerial(oid, oldSerial)
if not committedData:
committedData = self.loadSerial(oid, committedSerial)
newstate = unpickler.load()
old = state(self, oid, oldSerial, prfactory, oldData)
committed = state(self, oid, committedSerial, prfactory, committedData)
resolved = resolve(old, committed, newstate)
file = BytesIO()
pickler = PersistentPickler(persistent_id, file, _protocol)
pickler.dump(meta)
pickler.dump(resolved)
return self._crs_transform_record_data(file.getvalue())
except (ConflictError, BadClassName) as e:
logger.debug(
"Conflict resolution on %s failed with %s: %s",
klass, e.__class__.__name__, str(e))
except:
# If anything else went wrong, catch it here and avoid passing an
# arbitrary exception back to the client. The error here will mask
# the original ConflictError. A client can recover from a
# ConflictError, but not necessarily from other errors. But log
# the error so that any problems can be fixed.
logger.exception(
"Unexpected error while trying to resolve conflict on %s", klass)
raise ConflictError(oid=oid, serials=(committedSerial, oldSerial),
data=newpickle)
class ConflictResolvingStorage(object):
"Mix-in class that provides conflict resolution handling for storages"
tryToResolveConflict = tryToResolveConflict
_crs_transform_record_data = _crs_untransform_record_data = (
lambda self, o: o)
def registerDB(self, wrapper):
self._crs_untransform_record_data = wrapper.untransform_record_data
self._crs_transform_record_data = wrapper.transform_record_data
try:
m = super(ConflictResolvingStorage, self).registerDB
except AttributeError:
pass
else:
m(wrapper)
@@ -0,0 +1,579 @@
===================
Conflict Resolution
===================
Overview
========
Conflict resolution is a way to resolve transaction conflicts that would
otherwise abort a transaction. As such, it risks data integrity in order to
try to avoid throwing away potentially computationally expensive transactions.
The risk of harming data integrity should not be underestimated. Writing
conflict resolution code takes some responsibility for transactional
integrity away from the ZODB, and puts it in the hands of the developer
writing the conflict resolution code.
The current conflict resolution code is implemented with a storage mix-in
found in ZODB/ConflictResolution.py. The idea's proposal, and an explanation
of the interface, can be found here:
http://www.zope.org/Members/jim/ZODB/ApplicationLevelConflictResolution
Here is the most pertinent section, somewhat modified for this document's
use:
A new interface is proposed to allow object authors to provide a method
for resolving conflicts. When a conflict is detected, then the database
checks to see if the class of the object being saved defines the method,
_p_resolveConflict. If the method is defined, then the method is called
on the object. If the method succeeds, then the object change can be
committed, otherwise a ConflictError is raised as usual.
def _p_resolveConflict(oldState, savedState, newState):
Return the state of the object after resolving different changes.
Arguments:
oldState
The state of the object that the changes made by the current
transaction were based on.
The method is permitted to modify this value.
savedState
The state of the object that is currently stored in the
database. This state was written after oldState and reflects
changes made by a transaction that committed before the
current transaction.
The method is permitted to modify this value.
newState
The state after changes made by the current transaction.
The method is not permitted to modify this value.
This method should compute a new state by merging changes
reflected in savedState and newState, relative to oldState.
If the method cannot resolve the changes, then it should raise
ZODB.POSException.ConflictError.
Consider an extremely simple example, a counter::
from persistent import Persistent
class PCounter(Persistent):
'`value` is readonly; increment it with `inc`.'
# Fool BTree checks for sane comparison :/
def __cmp__(self, other):
return object.__cmp__(self, other)
def __lt__(self, other):
return object.__lt__(self, other)
_val = 0
def inc(self):
self._val += 1
@property
def value(self):
return self._val
def _p_resolveConflict(self, oldState, savedState, newState):
oldState['_val'] = (
savedState.get('_val', 0) +
newState.get('_val', 0) -
oldState.get('_val', 0))
return oldState
.. -> src
>>> import ConflictResolution_txt
>>> exec(src, ConflictResolution_txt.__dict__)
>>> PCounter = ConflictResolution_txt.PCounter
>>> PCounter.__module__ = 'ConflictResolution_txt'
By "state", the excerpt above means the value used by __getstate__ and
__setstate__: a dictionary, in most cases. We'll look at more details below,
but let's continue the example above with a simple successful resolution
story.
First we create a storage and a database, and put a PCounter in the database.
>>> import ZODB
>>> db = ZODB.DB('Data.fs')
>>> import transaction
>>> tm_A = transaction.TransactionManager()
>>> conn_A = db.open(transaction_manager=tm_A)
>>> p_A = conn_A.root()['p'] = PCounter()
>>> p_A.value
0
>>> tm_A.commit()
Now get another copy of 'p' so we can make a conflict. Think of `conn_A`
(connection A) as one thread, and `conn_B` (connection B) as a concurrent
thread. `p_A` is a view on the object in the first connection, and `p_B`
is a view on *the same persistent object* in the second connection.
>>> tm_B = transaction.TransactionManager()
>>> conn_B = db.open(transaction_manager=tm_B)
>>> p_B = conn_B.root()['p']
>>> p_B.value
0
>>> p_A._p_oid == p_B._p_oid
True
Now we can make a conflict, and see it resolved.
>>> p_A.inc()
>>> p_A.value
1
>>> p_B.inc()
>>> p_B.value
1
>>> tm_B.commit()
>>> p_B.value
1
>>> tm_A.commit()
>>> p_A.value
2
We need to synchronize connection B, in any of a variety of ways, to see the
change from connection A.
>>> p_B.value
1
>>> trans = tm_B.begin()
>>> p_B.value
2
A very similar class found in real world use is BTrees.Length.Length.
This conflict resolution approach is simple, yet powerful. However, it
has a few caveats and rough edges in practice. The simplicity, then, is
a bit of a disguise. Again, be warned, writing conflict resolution code
means that you claim significant responsibilty for your data integrity.
Because of the rough edges, the current conflict resolution approach is slated
for change (as of this writing, according to Jim Fulton, the ZODB
primary author and maintainer). Others have talked about different approaches
as well (see, for instance, http://www.python.org/~jeremy/weblog/031031c.html).
But for now, the _p_resolveConflict method is what we have.
Caveats and Dangers
===================
Here are caveats for working with this conflict resolution approach.
Each sub-section has a "DANGERS" section that outlines what might happen
if you ignore the warning. We work from the least danger to the most.
Conflict Resolution Is on the Server
------------------------------------
If you are using ZEO or ZRS, be aware that the classes for which you have
conflict resolution code *and* the classes of the non-persistent objects
they reference must be available to import by the *server* (or ZRS
primary).
DANGERS: You think you are going to get conflict resolution, but you won't.
Ignore `self`
-------------
Even though the _p_resolveConflict method has a "self", ignore it.
Don't change it. You make changes by returning the state. This is
effectively a class method.
DANGERS: The changes you make to the instance will be discarded. The
instance is not initialized, so other methods that depend on instance
attributes will not work.
Here's an example of a broken _p_resolveConflict method::
class PCounter2(PCounter):
def __init__(self):
self.data = []
def _p_resolveConflict(self, oldState, savedState, newState):
self.data.append('bad idea')
return super(PCounter2, self)._p_resolveConflict(
oldState, savedState, newState)
.. -> src
>>> exec(src, ConflictResolution_txt.__dict__)
>>> PCounter2 = ConflictResolution_txt.PCounter2
>>> PCounter2.__module__ = 'ConflictResolution_txt'
Now we'll prepare for the conflict again.
>>> p2_A = conn_A.root()['p2'] = PCounter2()
>>> p2_A.value
0
>>> tm_A.commit()
>>> trans = tm_B.begin() # sync
>>> p2_B = conn_B.root()['p2']
>>> p2_B.value
0
>>> p2_A._p_oid == p2_B._p_oid
True
And now we will make a conflict.
>>> p2_A.inc()
>>> p2_A.value
1
>>> p2_B.inc()
>>> p2_B.value
1
>>> tm_B.commit()
>>> p2_B.value
1
>>> tm_A.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ConflictError: database conflict error...
oops!
>>> tm_A.abort()
>>> p2_A.value
1
>>> trans = tm_B.begin()
>>> p2_B.value
1
Watch Out for Persistent Objects in the State
---------------------------------------------
If the object state has a reference to Persistent objects (instances
of classes that inherit from persistent.Persistent) then these references
*will not be loaded and are inaccessible*. Instead, persistent objects
in the state dictionary are ZODB.ConflictResolution.PersistentReference
instances. These objects have the following interface::
class IPersistentReference(zope.interface.Interface):
'''public contract for references to persistent objects from an object
with conflicts.'''
oid = zope.interface.Attribute(
'The oid of the persistent object that this reference represents')
database_name = zope.interface.Attribute(
'''The name of the database of the reference, *if* different.
If not different, None.''')
klass = zope.interface.Attribute(
'''class meta data. Presence is not reliable.''')
weak = zope.interface.Attribute(
'''bool: whether this reference is weak''')
def __cmp__(other):
'''if other is equivalent reference, return 0; else raise ValueError.
Equivalent in this case means that oid and database_name are the same.
If either is a weak reference, we only support `is` equivalence, and
otherwise raise a ValueError even if the datbase_names and oids are
the same, rather than guess at the correct semantics.
It is impossible to sort reliably, since the actual persistent
class may have its own comparison, and we have no idea what it is.
We assert that it is reasonably safe to assume that an object is
equivalent to itself, but that's as much as we can say.
We don't compare on 'is other', despite the
PersistentReferenceFactory.data cache, because it is possible to
have two references to the same object that are spelled with different
data (for instance, one with a class and one without).'''
So let's look at one of these. Let's assume we have three, `old`,
`saved`, and `new`, each representing a persistent reference to the same
object within a _p_resolveConflict call from the oldState, savedState,
and newState [#get_persistent_reference]_. They have an oid, `weak` is
False, and `database_name` is None. `klass` happens to be set but this is
not always the case.
>>> isinstance(new.oid, bytes)
True
>>> new.weak
False
>>> print(new.database_name)
None
>>> new.klass is PCounter
True
There are a few subtleties to highlight here. First, notice that the
database_name is only present if this is a cross-database reference
(see cross-database-references.txt in this directory, and examples
below). The database name and oid is sometimes a reasonable way to
reliably sort Persistent objects (see zope.app.keyreference, for
instance) but if your code compares one PersistentReference with a
database_name and another without, you need to refuse to give an answer
and raise an exception, because you can't know how the unknown
database_name sorts.
We already saw a persistent reference with a database_name of None. Now
let's suppose `new` is an example of a cross-database reference from a
database named '2' [#cross-database]_.
>>> new.database_name
'2'
As seen, the database_name is available for this cross-database reference,
and not for others. References to persistent objects, as defined in
seialize.py, have other variations, such as weak references, which are
handled but not discussed here [#instantiation_test]_
Second, notice the __cmp__ behavior [#cmp_test]_. This is new behavior
after ZODB 3.8 and addresses a serious problem for when persistent
objects are compared in an _p_resolveConflict, such as that in the ZODB
BTrees code. Prior to this change, it was not safe to use Persistent
objects as keys in a BTree. You needed to define a __cmp__ for them to
be sorted reliably out of the context of conflict resolution, but then
during conflict resolution the sorting would be arbitrary, on the basis
of the persistent reference's memory location. This could have lead to
inconsistent state for BTrees (or BTree module buckets or tree sets or sets).
Here's an example of how the new behavior stops potentially incorrect
resolution.
>>> import BTrees
>>> treeset_A = conn_A.root()['treeset'] = BTrees.family32.OI.TreeSet()
>>> tm_A.commit()
>>> trans = tm_B.begin() # sync
>>> treeset_B = conn_B.root()['treeset']
>>> treeset_A.insert(PCounter())
1
>>> treeset_B.insert(PCounter())
1
>>> tm_B.commit()
>>> tm_A.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ConflictError: database conflict error...
>>> tm_A.abort()
Third, note that, even if the persistent object to which the reference refers
changes in the same transaction, the reference is still the same.
DANGERS: subtle and potentially serious. Beyond the two subtleties above,
which should now be addressed, there is a general problem for objects that
are composites of smaller persistent objects--for instance, a BTree, in
which the BTree and each bucket is a persistent object; or a
zc.queue.CompositePersistentQueue, which is a persistent queue of
persistent queues. Consider the following situation. It is actually solved,
but it is a concrete example of what might go wrong.
A BTree (persistent object) has a two buckets (persistent objects). The
second bucket has one persistent object in it. Concurrently, one thread
deletes the one object in the second bucket, which causes the BTree to dump
the bucket; and another thread puts an object in the second bucket. What
happens during conflict resolution? Remember, each persistent object cannot
see the other. From the perspective of the BTree object, it has no
conflicts: one transaction modified it, causing it to lose a bucket; and the
other transaction did not change it. From the perspective of the bucket,
one transaction deleted an object and the other added it: it will resolve
conflicts and say that the bucket has the new object and not the old one.
However, it will be garbage collected, and effectively the addition of the
new object will be lost.
As mentioned, this story is actually solved for BTrees. As
BTrees/MergeTemplate.c explains, whenever savedState or newState for a bucket
shows an empty bucket, the code refuses to resolve the conflict: this avoids
the situation above.
>>> bucket_A = conn_A.root()['bucket'] = BTrees.family32.II.Bucket()
>>> bucket_A[0] = 255
>>> tm_A.commit()
>>> trans = tm_B.begin() # sync
>>> bucket_B = conn_B.root()['bucket']
>>> bucket_B[1] = 254
>>> del bucket_A[0]
>>> tm_B.commit()
>>> tm_A.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ConflictError: database conflict error...
>>> tm_A.abort()
However, the story highlights the kinds of subtle problems that units
made up of multiple composite Persistent objects need to contemplate.
Any structure made up of objects that contain persistent objects with
conflict resolution code, as a catalog index is made up of multiple
BTree Buckets and Sets, each with conflict resolution, needs to think
through these kinds of problems or be faced with potential data
integrity issues.
.. cleanup
>>> db.close()
>>> db1.close()
>>> db2.close()
.. ......... ..
.. FOOTNOTES ..
.. ......... ..
.. [#get_persistent_reference] We'll catch persistent references with a class
mutable.
::
class PCounter3(PCounter):
data = []
def _p_resolveConflict(self, oldState, savedState, newState):
PCounter3.data.append(
(oldState.get('other'),
savedState.get('other'),
newState.get('other')))
return super(PCounter3, self)._p_resolveConflict(
oldState, savedState, newState)
.. -> src
>>> exec(src, ConflictResolution_txt.__dict__)
>>> PCounter3 = ConflictResolution_txt.PCounter3
>>> PCounter3.__module__ = 'ConflictResolution_txt'
>>> p3_A = conn_A.root()['p3'] = PCounter3()
>>> p3_A.other = conn_A.root()['p']
>>> tm_A.commit()
>>> trans = tm_B.begin() # sync
>>> p3_B = conn_B.root()['p3']
>>> p3_A.inc()
>>> p3_B.inc()
>>> tm_B.commit()
>>> tm_A.commit()
>>> old, saved, new = PCounter3.data[-1]
.. [#cross-database] We need a whole different set of databases for this.
See cross-database-references.txt in this directory for a discussion of
what is going on here.
>>> databases = {}
>>> db1 = ZODB.DB('1', databases=databases, database_name='1')
>>> db2 = ZODB.DB('2', databases=databases, database_name='2')
>>> tm_multi_A = transaction.TransactionManager()
>>> conn_1A = db1.open(transaction_manager=tm_multi_A)
>>> conn_2A = conn_1A.get_connection('2')
>>> p4_1A = conn_1A.root()['p4'] = PCounter3()
>>> p5_2A = conn_2A.root()['p5'] = PCounter3()
>>> conn_2A.add(p5_2A)
>>> p4_1A.other = p5_2A
>>> tm_multi_A.commit()
>>> tm_multi_B = transaction.TransactionManager()
>>> conn_1B = db1.open(transaction_manager=tm_multi_B)
>>> p4_1B = conn_1B.root()['p4']
>>> p4_1A.inc()
>>> p4_1B.inc()
>>> tm_multi_B.commit()
>>> tm_multi_A.commit()
>>> old, saved, new = PCounter3.data[-1]
.. [#instantiation_test] We'll simply instantiate PersistentReferences
with examples of types described in ZODB/serialize.py.
>>> from ZODB.ConflictResolution import PersistentReference
>>> ref1 = PersistentReference(b'my_oid')
>>> ref1.oid
'my_oid'
>>> print(ref1.klass)
None
>>> print(ref1.database_name)
None
>>> ref1.weak
False
>>> ref2 = PersistentReference((b'my_oid', 'my_class'))
>>> ref2.oid
'my_oid'
>>> ref2.klass
'my_class'
>>> print(ref2.database_name)
None
>>> ref2.weak
False
>>> ref3 = PersistentReference(['w', (b'my_oid',)])
>>> ref3.oid
'my_oid'
>>> print(ref3.klass)
None
>>> print(ref3.database_name)
None
>>> ref3.weak
True
>>> ref3a = PersistentReference(['w', (b'my_oid', 'other_db')])
>>> ref3a.oid
'my_oid'
>>> print(ref3a.klass)
None
>>> ref3a.database_name
'other_db'
>>> ref3a.weak
True
>>> ref4 = PersistentReference(['m', ('other_db', b'my_oid', 'my_class')])
>>> ref4.oid
'my_oid'
>>> ref4.klass
'my_class'
>>> ref4.database_name
'other_db'
>>> ref4.weak
False
>>> ref5 = PersistentReference(['n', ('other_db', b'my_oid')])
>>> ref5.oid
'my_oid'
>>> print(ref5.klass)
None
>>> ref5.database_name
'other_db'
>>> ref5.weak
False
>>> ref6 = PersistentReference([b'my_oid']) # legacy
>>> ref6.oid
'my_oid'
>>> print(ref6.klass)
None
>>> print(ref6.database_name)
None
>>> ref6.weak
True
.. [#cmp_test] All references are equal to themselves.
>>> ref1 == ref1 and ref2 == ref2 and ref4 == ref4 and ref5 == ref5
True
>>> ref3 == ref3 and ref3a == ref3a and ref6 == ref6 # weak references
True
Non-weak references with the same oid and database_name are equal.
>>> ref1 == ref2 and ref4 == ref5
True
Everything else raises a ValueError: weak references with the same oid and
database, and references with a different database_name or oid.
>>> ref3 == ref6
Traceback (most recent call last):
...
ValueError: can't reliably compare against different PersistentReferences
>>> ref1 == PersistentReference(('another_oid', 'my_class'))
Traceback (most recent call last):
...
ValueError: can't reliably compare against different PersistentReferences
>>> ref4 == PersistentReference(
... ['m', ('another_db', 'my_oid', 'my_class')])
Traceback (most recent call last):
...
ValueError: can't reliably compare against different PersistentReferences
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,447 @@
##############################################################################
#
# Copyright (c) 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
#
##############################################################################
"""Demo ZODB storage
A demo storage supports demos by allowing a volatile changed database
to be layered over a base database.
The base storage must not change.
"""
from __future__ import print_function
import os
import random
import weakref
import tempfile
import ZODB.BaseStorage
import ZODB.blob
import ZODB.interfaces
import ZODB.MappingStorage
import ZODB.POSException
import ZODB.utils
import zope.interface
from .ConflictResolution import ConflictResolvingStorage
from .utils import load_current, maxtid
@zope.interface.implementer(
ZODB.interfaces.IStorage,
ZODB.interfaces.IStorageIteration,
)
class DemoStorage(ConflictResolvingStorage):
"""A storage that stores changes against a read-only base database
This storage was originally meant to support distribution of
application demonstrations with populated read-only databases (on
CDROM) and writable in-memory databases.
Demo storages are extemely convenient for testing where setup of a
base database can be shared by many tests.
Demo storages are also handy for staging appplications where a
read-only snapshot of a production database (often accomplished
using a `beforestorage
<https://pypi.python.org/pypi/zc.beforestorage>`_) is combined
with a changes database implemented with a
:class:`~ZODB.FileStorage.FileStorage.FileStorage`.
"""
def __init__(self, name=None, base=None, changes=None,
close_base_on_close=None, close_changes_on_close=None):
"""Create a demo storage
:param str name: The storage name used by the
:meth:`~ZODB.interfaces.IStorage.getName` and
:meth:`~ZODB.interfaces.IStorage.sortKey` methods.
:param object base: base storage
:param object changes: changes storage
:param bool close_base_on_close: A Flag indicating whether the base
database should be closed when the demo storage is closed.
:param bool close_changes_on_close: A Flag indicating whether the
changes database should be closed when the demo storage is closed.
If a base database isn't provided, a
:class:`~ZODB.MappingStorage.MappingStorage` will be
constructed and used.
If ``close_base_on_close`` isn't specified, it will be ``True`` if
a base database was provided and ``False`` otherwise.
If a changes database isn't provided, a
:class:`~ZODB.MappingStorage.MappingStorage` will be
constructed and used and blob support will be provided using a
temporary blob directory.
If ``close_changes_on_close`` isn't specified, it will be ``True`` if
a changes database was provided and ``False`` otherwise.
"""
if close_base_on_close is None:
if base is None:
base = ZODB.MappingStorage.MappingStorage()
close_base_on_close = False
else:
close_base_on_close = True
elif base is None:
base = ZODB.MappingStorage.MappingStorage()
self.base = base
self.close_base_on_close = close_base_on_close
if changes is None:
self._temporary_changes = True
changes = ZODB.MappingStorage.MappingStorage()
zope.interface.alsoProvides(self, ZODB.interfaces.IBlobStorage)
if close_changes_on_close is None:
close_changes_on_close = False
else:
if ZODB.interfaces.IBlobStorage.providedBy(changes):
zope.interface.alsoProvides(self, ZODB.interfaces.IBlobStorage)
if close_changes_on_close is None:
close_changes_on_close = True
self.changes = changes
self.close_changes_on_close = close_changes_on_close
self._issued_oids = set()
self._stored_oids = set()
self._resolved = []
self._commit_lock = ZODB.utils.Lock()
self._transaction = None
if name is None:
name = 'DemoStorage(%r, %r)' % (base.getName(), changes.getName())
self.__name__ = name
self._copy_methods_from_changes(changes)
self._next_oid = random.randint(1, 1<<62)
def _blobify(self):
if (self._temporary_changes and
isinstance(self.changes, ZODB.MappingStorage.MappingStorage)
):
blob_dir = tempfile.mkdtemp('.demoblobs')
_temporary_blobdirs[
weakref.ref(self, cleanup_temporary_blobdir)
] = blob_dir
self.changes = ZODB.blob.BlobStorage(blob_dir, self.changes)
self._copy_methods_from_changes(self.changes)
return True
def cleanup(self):
self.base.cleanup()
self.changes.cleanup()
__opened = True
def opened(self):
return self.__opened
def close(self):
self.__opened = False
if self.close_base_on_close:
self.base.close()
if self.close_changes_on_close:
self.changes.close()
def _copy_methods_from_changes(self, changes):
for meth in (
'_lock',
'getSize', 'isReadOnly',
'sortKey', 'tpc_transaction',
):
setattr(self, meth, getattr(changes, meth))
supportsUndo = getattr(changes, 'supportsUndo', None)
if supportsUndo is not None and supportsUndo():
for meth in ('supportsUndo', 'undo', 'undoLog', 'undoInfo'):
setattr(self, meth, getattr(changes, meth))
zope.interface.alsoProvides(self, ZODB.interfaces.IStorageUndoable)
lastInvalidations = getattr(changes, 'lastInvalidations', None)
if lastInvalidations is not None:
self.lastInvalidations = lastInvalidations
def getName(self):
return self.__name__
__repr__ = getName
def getTid(self, oid):
try:
return self.changes.getTid(oid)
except ZODB.POSException.POSKeyError:
return self.base.getTid(oid)
def history(self, oid, size=1):
try:
r = self.changes.history(oid, size)
except ZODB.POSException.POSKeyError:
r = []
size -= len(r)
if size:
try:
r += self.base.history(oid, size)
except ZODB.POSException.POSKeyError:
if not r:
raise
return r
def iterator(self, start=None, end=None):
for t in self.base.iterator(start, end):
yield t
for t in self.changes.iterator(start, end):
yield t
def lastTransaction(self):
t = self.changes.lastTransaction()
if t == ZODB.utils.z64:
t = self.base.lastTransaction()
return t
def __len__(self):
return len(self.changes)
# still want load for old clients (e.g. zeo servers)
load = load_current
def loadBefore(self, oid, tid):
try:
result = self.changes.loadBefore(oid, tid)
except ZODB.POSException.POSKeyError:
# The oid isn't in the changes, so defer to base
return self.base.loadBefore(oid, tid)
if result is None:
# The oid *was* in the changes, but there aren't any
# earlier records. Maybe there are in the base.
try:
result = self.base.loadBefore(oid, tid)
except ZODB.POSException.POSKeyError:
# The oid isn't in the base, so None will be the right result
pass
else:
if result and not result[-1]:
# The oid is current in the base. We need to find
# the end tid in the base by fining the first tid
# in the changes. Unfortunately, there isn't an
# api for this, so we have to walk back using
# loadBefore.
if tid == maxtid:
# Special case: we were looking for the
# current value. We won't find anything in
# changes, so we're done.
return result
end_tid = maxtid
t = self.changes.loadBefore(oid, end_tid)
while t:
end_tid = t[1]
t = self.changes.loadBefore(oid, end_tid)
result = result[:2] + (
end_tid if end_tid != maxtid else None,
)
return result
def loadBlob(self, oid, serial):
try:
return self.changes.loadBlob(oid, serial)
except ZODB.POSException.POSKeyError:
try:
return self.base.loadBlob(oid, serial)
except AttributeError:
if not ZODB.interfaces.IBlobStorage.providedBy(self.base):
raise ZODB.POSException.POSKeyError(oid, serial)
raise
except AttributeError:
if self._blobify():
return self.loadBlob(oid, serial)
raise
def openCommittedBlobFile(self, oid, serial, blob=None):
try:
return self.changes.openCommittedBlobFile(oid, serial, blob)
except ZODB.POSException.POSKeyError:
try:
return self.base.openCommittedBlobFile(oid, serial, blob)
except AttributeError:
if not ZODB.interfaces.IBlobStorage.providedBy(self.base):
raise ZODB.POSException.POSKeyError(oid, serial)
raise
except AttributeError:
if self._blobify():
return self.openCommittedBlobFile(oid, serial, blob)
raise
def loadSerial(self, oid, serial):
try:
return self.changes.loadSerial(oid, serial)
except ZODB.POSException.POSKeyError:
return self.base.loadSerial(oid, serial)
def new_oid(self):
with self._lock:
while 1:
oid = ZODB.utils.p64(self._next_oid )
if oid not in self._issued_oids:
try:
load_current(self.changes, oid)
except ZODB.POSException.POSKeyError:
try:
load_current(self.base, oid)
except ZODB.POSException.POSKeyError:
self._next_oid += 1
self._issued_oids.add(oid)
return oid
self._next_oid = random.randint(1, 1<<62)
def pack(self, t, referencesf, gc=None):
if gc is None:
if self._temporary_changes:
return self.changes.pack(t, referencesf)
elif self._temporary_changes:
return self.changes.pack(t, referencesf, gc=gc)
elif gc:
raise TypeError(
"Garbage collection isn't supported"
" when there is a base storage.")
try:
self.changes.pack(t, referencesf, gc=False)
except TypeError as v:
if 'gc' in str(v):
pass # The gc arg isn't supported. Don't pack
raise
def pop(self):
"""Close the changes database and return the base.
"""
self.changes.close()
return self.base
def push(self, changes=None):
"""Create a new demo storage using the storage as a base.
The given changes are used as the changes for the returned
storage and ``False`` is passed as ``close_base_on_close``.
"""
return self.__class__(base=self, changes=changes,
close_base_on_close=False)
def store(self, oid, serial, data, version, transaction):
assert version=='', "versions aren't supported"
if transaction is not self._transaction:
raise ZODB.POSException.StorageTransactionError(self, transaction)
# Since the OID is being used, we don't have to keep up with it any
# more. Save it now so we can forget it later. :)
self._stored_oids.add(oid)
# See if we already have changes for this oid
try:
old = load_current(self, oid)[1]
except ZODB.POSException.POSKeyError:
old = serial
if old != serial:
rdata = self.tryToResolveConflict(oid, old, serial, data)
self.changes.store(oid, old, rdata, '', transaction)
self._resolved.append(oid)
else:
self.changes.store(oid, serial, data, '', transaction)
def storeBlob(self, oid, oldserial, data, blobfilename, version,
transaction):
assert version=='', "versions aren't supported"
if transaction is not self._transaction:
raise ZODB.POSException.StorageTransactionError(self, transaction)
# Since the OID is being used, we don't have to keep up with it any
# more. Save it now so we can forget it later. :)
self._stored_oids.add(oid)
try:
self.changes.storeBlob(
oid, oldserial, data, blobfilename, '', transaction)
except AttributeError:
if not self._blobify():
raise
self.changes.storeBlob(
oid, oldserial, data, blobfilename, '', transaction)
checkCurrentSerialInTransaction = (
ZODB.BaseStorage.checkCurrentSerialInTransaction)
def temporaryDirectory(self):
try:
return self.changes.temporaryDirectory()
except AttributeError:
if self._blobify():
return self.changes.temporaryDirectory()
raise
def tpc_abort(self, transaction):
with self._lock:
if transaction is not self._transaction:
return
self._stored_oids = set()
self._transaction = None
self.changes.tpc_abort(transaction)
self._commit_lock.release()
def tpc_begin(self, transaction, *a, **k):
with self._lock:
# The tid argument exists to support testing.
if transaction is self._transaction:
raise ZODB.POSException.StorageTransactionError(
"Duplicate tpc_begin calls for same transaction")
self._commit_lock.acquire()
with self._lock:
self.changes.tpc_begin(transaction, *a, **k)
self._transaction = transaction
self._stored_oids = set()
del self._resolved[:]
def tpc_vote(self, *a, **k):
if self.changes.tpc_vote(*a, **k):
raise ZODB.POSException.StorageTransactionError(
"Unexpected resolved conflicts")
return self._resolved
def tpc_finish(self, transaction, func = lambda tid: None):
with self._lock:
if (transaction is not self._transaction):
raise ZODB.POSException.StorageTransactionError(
"tpc_finish called with wrong transaction")
self._issued_oids.difference_update(self._stored_oids)
self._stored_oids = set()
self._transaction = None
tid = self.changes.tpc_finish(transaction, func)
self._commit_lock.release()
return tid
_temporary_blobdirs = {}
def cleanup_temporary_blobdir(
ref,
_temporary_blobdirs=_temporary_blobdirs, # Make sure it stays around
):
blob_dir = _temporary_blobdirs.pop(ref, None)
if blob_dir and os.path.exists(blob_dir):
ZODB.blob.remove_committed_dir(blob_dir)
@@ -0,0 +1,468 @@
==========================
DemoStorage demo (doctest)
==========================
DemoStorages provide a way to provide incremental updates to an
existing, base, storage without updating the storage.
.. We need to mess with time to prevent spurious test failures on windows
>>> now = 1231019584.0
>>> def faux_time_time():
... global now
... now += .1
... return now
>>> import time
>>> real_time_time = time.time
>>> if isinstance(time,type):
... time.time = staticmethod(faux_time_time) # Jython
... else:
... time.time = faux_time_time
To see how this works, we'll start by creating a base storage and
puting an object (in addition to the root object) in it:
>>> from ZODB.FileStorage import FileStorage
>>> base = FileStorage('base.fs')
>>> from ZODB.DB import DB
>>> db = DB(base)
>>> from persistent.mapping import PersistentMapping
>>> conn = db.open()
>>> conn.root()['1'] = PersistentMapping({'a': 1, 'b':2})
>>> import transaction
>>> transaction.commit()
>>> db.close()
>>> import os
>>> original_size = os.path.getsize('base.fs')
Now, lets reopen the base storage in read-only mode:
>>> base = FileStorage('base.fs', read_only=True)
And open a new storage to store changes:
>>> changes = FileStorage('changes.fs')
and combine the 2 in a demofilestorage:
>>> from ZODB.DemoStorage import DemoStorage
>>> storage = DemoStorage(base=base, changes=changes)
The storage will assign OIDs in a pseudo-random fashion, but for test
purposes we need to control where they start (since the random seeds
can be different on different platforms):
>>> storage._next_oid = 3553260803050964942
If there are no transactions, the storage reports the lastTransaction
of the base database:
>>> storage.lastTransaction() == base.lastTransaction()
True
Let's add some data:
>>> db = DB(storage)
>>> conn = db.open()
>>> items = sorted(conn.root()['1'].items())
>>> items
[('a', 1), ('b', 2)]
>>> conn.root()['2'] = PersistentMapping({'a': 3, 'b':4})
>>> transaction.commit()
>>> conn.root()['2']['c'] = 5
>>> transaction.commit()
Here we can see that we haven't modified the base storage:
>>> original_size == os.path.getsize('base.fs')
True
But we have modified the changes database:
>>> len(changes)
2
Our lastTransaction reflects the lastTransaction of the changes:
>>> storage.lastTransaction() > base.lastTransaction()
True
>>> storage.lastTransaction() == changes.lastTransaction()
True
Let's walk over some of the methods so we can see how we delegate to
the new underlying storages:
>>> from ZODB.utils import p64, u64
>>> storage.load(p64(0), '') == changes.load(p64(0), '')
True
>>> storage.load(p64(0), '') == base.load(p64(0), '')
False
>>> storage.load(p64(1), '') == base.load(p64(1), '')
True
>>> serial = base.getTid(p64(0))
>>> storage.loadSerial(p64(0), serial) == base.loadSerial(p64(0), serial)
True
>>> serial = changes.getTid(p64(0))
>>> storage.loadSerial(p64(0), serial) == changes.loadSerial(p64(0),
... serial)
True
The object id of the new object is quite random, and typically large:
>>> print(u64(conn.root()['2']._p_oid))
3553260803050964942
Let's look at some other methods:
>>> storage.getName()
"DemoStorage('base.fs', 'changes.fs')"
>>> storage.sortKey() == changes.sortKey()
True
>>> storage.getSize() == changes.getSize()
True
>>> len(storage) == len(changes)
True
Undo methods are simply copied from the changes storage:
>>> [getattr(storage, name) == getattr(changes, name)
... for name in ('supportsUndo', 'undo', 'undoLog', 'undoInfo')
... ]
[True, True, True, True]
>>> db.close()
Closing demo storages
=====================
Normally, when a demo storage is closed, it's base and changes
storage are closed:
>>> from ZODB.MappingStorage import MappingStorage
>>> demo = DemoStorage(base=MappingStorage(), changes=MappingStorage())
>>> demo.close()
>>> demo.base.opened(), demo.changes.opened()
(False, False)
You can pass constructor arguments to control whether the base and
changes storages should be closed when the demo storage is closed:
>>> demo = DemoStorage(
... base=MappingStorage(), changes=MappingStorage(),
... close_base_on_close=False, close_changes_on_close=False,
... )
>>> demo.close()
>>> demo.base.opened(), demo.changes.opened()
(True, True)
Storage Stacking
================
A common use case is to stack demo storages. DemoStorage provides
some helper functions to help with this. The push method, just
creates a new demo storage who's base is the original demo storage:
>>> demo = DemoStorage()
>>> demo2 = demo.push()
>>> demo2.base is demo
True
We can also supply an explicit changes storage, if we wish:
>>> changes = MappingStorage()
>>> demo3 = demo2.push(changes)
>>> demo3.changes is changes, demo3.base is demo2
(True, True)
The pop method closes the changes storage and returns the base
*without* closing it:
>>> demo3.pop() is demo2
True
>>> changes.opened()
False
If storage returned by push is closed, the original storage isn't:
>>> demo3.push().close()
>>> demo2.opened()
True
Blob Support
============
DemoStorage supports Blobs if the changes database supports blobs.
>>> import ZODB.blob
>>> base = ZODB.blob.BlobStorage('base', FileStorage('base.fs'))
>>> db = DB(base)
>>> conn = db.open()
>>> conn.root()['blob'] = ZODB.blob.Blob()
>>> with conn.root()['blob'].open('w') as file:
... _ = file.write(b'state 1')
>>> transaction.commit()
>>> db.close()
>>> base = ZODB.blob.BlobStorage('base',
... FileStorage('base.fs', read_only=True))
>>> changes = ZODB.blob.BlobStorage('changes',
... FileStorage('changes.fs', create=True))
>>> storage = DemoStorage(base=base, changes=changes)
>>> db = DB(storage)
>>> conn = db.open()
>>> with conn.root()['blob'].open() as fp: fp.read()
'state 1'
>>> _ = transaction.begin()
>>> with conn.root()['blob'].open('w') as file:
... _ = file.write(b'state 2')
>>> transaction.commit()
>>> with conn.root()['blob'].open() as fp: fp.read()
'state 2'
>>> storage.temporaryDirectory() == changes.temporaryDirectory()
True
>>> db.close()
It isn't necessary for the base database to support blobs.
>>> base = FileStorage('base.fs', read_only=True)
>>> changes = ZODB.blob.BlobStorage('changes', FileStorage('changes.fs'))
>>> storage = DemoStorage(base=base, changes=changes)
>>> db = DB(storage)
>>> conn = db.open()
>>> with conn.root()['blob'].open() as fp: fp.read()
'state 2'
>>> _ = transaction.begin()
>>> conn.root()['blob2'] = ZODB.blob.Blob()
>>> with conn.root()['blob2'].open('w') as file:
... _ = file.write(b'state 1')
>>> with conn.root()['blob2'].open() as fp: fp.read()
'state 1'
>>> db.close()
If the changes database is created implicitly, it will get a blob
storage wrapped around it when necessary:
>>> base = ZODB.blob.BlobStorage('base',
... FileStorage('base.fs', read_only=True))
>>> storage = DemoStorage(base=base)
>>> type(storage.changes).__name__
'MappingStorage'
>>> db = DB(storage)
>>> conn = db.open()
>>> with conn.root()['blob'].open() as fp: fp.read()
'state 1'
>>> type(storage.changes).__name__
'BlobStorage'
>>> _ = transaction.begin()
>>> with conn.root()['blob'].open('w') as file:
... _ = file.write(b'state 2')
>>> transaction.commit()
>>> with conn.root()['blob'].open() as fp: fp.read()
'state 2'
>>> storage.temporaryDirectory() == storage.changes.temporaryDirectory()
True
>>> db.close()
This works even if we first write a blob rather than read a blob:
>>> base = ZODB.blob.BlobStorage('base',
... FileStorage('base.fs', read_only=True))
>>> storage = DemoStorage(base=base)
>>> type(storage.changes).__name__
'MappingStorage'
>>> db = DB(storage)
>>> conn = db.open()
>>> _ = transaction.begin()
>>> conn.root()['blob'] = ZODB.blob.Blob()
>>> with conn.root()['blob'].open('w') as file:
... _ = file.write(b'state 2')
>>> transaction.commit()
>>> type(storage.changes).__name__
'BlobStorage'
>>> with conn.root()['blob'].open() as fp: fp.read()
'state 2'
>>> storage.temporaryDirectory() == storage.changes.temporaryDirectory()
True
>>> db.close()
.. Check that the temporary directory is gone
For now, it won't go until the storage does.
>>> transaction.abort()
>>> blobdir = storage.temporaryDirectory()
>>> del storage, _
>>> import gc
>>> _ = gc.collect()
>>> import os
>>> os.path.exists(blobdir)
False
ZConfig support
===============
You can configure demo storages using ZConfig, using name, changes,
and base options:
>>> import ZODB.config
>>> storage = ZODB.config.storageFromString("""
... <demostorage>
... </demostorage>
... """)
>>> storage.getName()
"DemoStorage('MappingStorage', 'MappingStorage')"
>>> storage = ZODB.config.storageFromString("""
... <demostorage>
... <filestorage base>
... path base.fs
... </filestorage>
...
... <filestorage changes>
... path changes.fs
... </filestorage>
... </demostorage>
... """)
>>> storage.getName()
"DemoStorage('base.fs', 'changes.fs')"
>>> storage.close()
>>> storage = ZODB.config.storageFromString("""
... <demostorage>
... name bob
... <filestorage>
... path base.fs
... </filestorage>
...
... <filestorage changes>
... path changes.fs
... </filestorage>
... </demostorage>
... """)
>>> storage.getName()
'bob'
>>> storage.base.getName()
'base.fs'
>>> storage.close()
Generating OIDs
===============
When asked for a new OID DemoStorage chooses a value and then
verifies that neither the base or changes storages already contain
that OID. It chooses values sequentially from random starting
points, picking new starting points whenever a chosen value us already
in the changes or base.
Under rare circumstances an OID can be chosen that has already been
handed out, but which hasn't yet been comitted. Lets verify that if
the same OID is chosen twice during a transaction that everything will
still work.
To test this, we need to hack random.randint a bit.
>>> import random
>>> randint = random.randint
>>> rv = 42
>>> def faux_randint(min, max):
... print('called randint')
... global rv
... rv += 1000
... return rv
>>> random.randint = faux_randint
Now, we create a demostorage.
>>> storage = DemoStorage()
called randint
If we ask for an oid, we'll get 1042.
>>> print(u64(storage.new_oid()))
1042
oids are allocated seuentially:
>>> print(u64(storage.new_oid()))
1043
Now, we'll save 1044 in changes so that it has to pick a new one randomly.
>>> t = transaction.get()
>>> ZODB.tests.util.store(storage.changes, 1044)
>>> print(u64(storage.new_oid()))
called randint
2042
Now, we hack rv to 1042 is given out again and we'll save 2043 in base
to force another attempt:
>>> rv -= 1000
>>> ZODB.tests.util.store(storage.changes, 2043)
>>> oid = storage.new_oid()
called randint
called randint
>>> print(u64(oid))
3042
DemoStorage keeps up with the issued OIDs to know when not to reissue them...
>>> oid in storage._issued_oids
True
...but once data is stored with a given OID...
>>> ZODB.tests.util.store(storage, oid)
...there's no need to remember it any longer:
>>> oid in storage._issued_oids
False
>>> storage.close()
.. restore randint
>>> random.randint = randint
.. restore time
>>> time.time = real_time_time
@@ -0,0 +1,208 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""Support for database export and import."""
import logging
import os
from tempfile import TemporaryFile
import six
from ZODB.blob import Blob
from ZODB.interfaces import IBlobStorage
from ZODB.POSException import ExportError
from ZODB.serialize import referencesf
from ZODB.utils import p64, u64, cp, mktemp
from ZODB._compat import PersistentPickler, Unpickler, BytesIO, _protocol
logger = logging.getLogger('ZODB.ExportImport')
class ExportImport(object):
def exportFile(self, oid, f=None, bufsize=64 * 1024):
if f is None:
f = TemporaryFile(prefix="EXP")
elif isinstance(f, six.string_types):
f = open(f,'w+b')
f.write(b'ZEXP')
oids = [oid]
done_oids = {}
done = done_oids.__contains__
load = self._storage.load
supports_blobs = IBlobStorage.providedBy(self._storage)
while oids:
oid = oids.pop(0)
if oid in done_oids:
continue
done_oids[oid] = True
try:
p, serial = load(oid)
except:
logger.debug("broken reference for oid %s", repr(oid),
exc_info=True)
else:
referencesf(p, oids)
f.writelines([oid, p64(len(p)), p])
if supports_blobs:
if not isinstance(self._reader.getGhost(p), Blob):
continue # not a blob
blobfilename = self._storage.loadBlob(oid, serial)
f.write(blob_begin_marker)
f.write(p64(os.stat(blobfilename).st_size))
blobdata = open(blobfilename, "rb")
cp(blobdata, f, bufsize=bufsize)
blobdata.close()
f.write(export_end_marker)
return f
def importFile(self, f, clue='', customImporters=None):
# This is tricky, because we need to work in a transaction!
if isinstance(f, six.string_types):
with open(f, 'rb') as fp:
return self.importFile(fp, clue=clue,
customImporters=customImporters)
magic = f.read(4)
if magic != b'ZEXP':
if customImporters and magic in customImporters:
f.seek(0)
return customImporters[magic](self, f, clue)
raise ExportError("Invalid export header")
t = self.transaction_manager.get()
if clue:
t.note(clue)
return_oid_list = []
self._import = f, return_oid_list
self._register()
t.savepoint(optimistic=True)
# Return the root imported object.
if return_oid_list:
return self.get(return_oid_list[0])
else:
return None
def _importDuringCommit(self, transaction, f, return_oid_list):
"""Import data during two-phase commit.
Invoked by the transaction manager mid commit.
Appends one item, the OID of the first object created,
to return_oid_list.
"""
oids = {}
# IMPORTANT: This code should be consistent with the code in
# serialize.py. It is currently out of date and doesn't handle
# weak references.
def persistent_load(ooid):
"""Remap a persistent id to a new ID and create a ghost for it."""
klass = None
if isinstance(ooid, tuple):
ooid, klass = ooid
if not isinstance(ooid, bytes):
assert isinstance(ooid, str)
# this happens on Python 3 when all bytes in the oid are < 0x80
ooid = ooid.encode('ascii')
if ooid in oids:
oid = oids[ooid]
else:
if klass is None:
oid = self._storage.new_oid()
else:
oid = self._storage.new_oid(), klass
oids[ooid] = oid
return Ghost(oid)
while 1:
header = f.read(16)
if header == export_end_marker:
break
if len(header) != 16:
raise ExportError("Truncated export file")
# Extract header information
ooid = header[:8]
length = u64(header[8:16])
data = f.read(length)
if len(data) != length:
raise ExportError("Truncated export file")
if oids:
oid = oids[ooid]
if isinstance(oid, tuple):
oid = oid[0]
else:
oids[ooid] = oid = self._storage.new_oid()
return_oid_list.append(oid)
if (b'blob' in data and
isinstance(self._reader.getGhost(data), Blob)
):
# Blob support
# Make sure we have a (redundant, overly) blob marker.
if f.read(len(blob_begin_marker)) != blob_begin_marker:
raise ValueError("No data for blob object")
# Copy the blob data to a temporary file
# and remember the name
blob_len = u64(f.read(8))
blob_filename = mktemp(self._storage.temporaryDirectory())
blob_file = open(blob_filename, "wb")
cp(f, blob_file, blob_len)
blob_file.close()
else:
blob_filename = None
pfile = BytesIO(data)
unpickler = Unpickler(pfile)
unpickler.persistent_load = persistent_load
newp = BytesIO()
pickler = PersistentPickler(persistent_id, newp, _protocol)
pickler.dump(unpickler.load())
pickler.dump(unpickler.load())
data = newp.getvalue()
if blob_filename is not None:
self._storage.storeBlob(oid, None, data, blob_filename,
'', transaction)
else:
self._storage.store(oid, None, data, '', transaction)
export_end_marker = b'\377'*16
blob_begin_marker = b'\000BLOBSTART'
class Ghost(object):
__slots__ = ("oid",)
def __init__(self, oid):
self.oid = oid
def persistent_id(obj):
if isinstance(obj, Ghost):
return obj.oid
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
# this is a package
from ZODB.FileStorage.FileStorage import FileStorage, TransactionRecord
from ZODB.FileStorage.FileStorage import FileIterator, Record, packed_version
# BBB Alias for compatibility
RecordIterator = TransactionRecord
@@ -0,0 +1,289 @@
##############################################################################
#
# Copyright (c) 2003 Zope Foundation 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
#
##############################################################################
#
# File-based ZODB storage
#
# Files are arranged as follows.
#
# - The first 4 bytes are a file identifier.
#
# - The rest of the file consists of a sequence of transaction
# "records".
#
# A transaction record consists of:
#
# - 8-byte transaction id, which is also a time stamp.
#
# - 8-byte transaction record length - 8.
#
# - 1-byte status code
# ' ' (a blank) completed transaction that hasn't been packed
# 'p' completed transaction that has been packed
# 'c' checkpoint -- a transaction in progress, at the end of the file;
# it's been thru vote() but not finish(); if finish() completes
# normally, it will be overwritten with a blank; if finish() dies
# (e.g., out of disk space), cleanup code will try to truncate
# the file to chop off this incomplete transaction
# 'u' uncertain; no longer used; was previously used to record something
# about non-transactional undo
#
# - 2-byte length of user name
#
# - 2-byte length of description
#
# - 2-byte length of extension attributes
#
# - user name
#
# - description
#
# - extension attributes
#
# * A sequence of data records
#
# - 8-byte redundant transaction length -8
#
# A data record consists of
#
# - 8-byte oid.
#
# - 8-byte tid, which matches the transaction id in the transaction record.
#
# - 8-byte previous-record file-position.
#
# - 8-byte beginning of transaction record file position.
#
# - 2-bytes with zero values. (Was version length.)
#
# - 8-byte data length
#
# ? data
# (data length > 0)
#
# ? 8-byte position of data record containing data
# (data length == 0)
#
# Note that the lengths and positions are all big-endian.
# Also, the object ids time stamps are big-endian, so comparisons
# are meaningful.
#
# Backpointers
#
# When we undo a record, we don't copy (or delete)
# data. Instead, we write records with back pointers.
import logging
import struct
from ZODB.POSException import POSKeyError
from ZODB.utils import u64, oid_repr, as_bytes
from ZODB._compat import PY3
class CorruptedError(Exception):
pass
class CorruptedDataError(CorruptedError):
def __init__(self, oid=None, buf=None, pos=None):
self.oid = oid
self.buf = buf
self.pos = pos
def __str__(self):
if self.oid:
msg = "Error reading oid %s. Found %r" % (oid_repr(self.oid),
self.buf)
else:
msg = "Error reading unknown oid. Found %r" % self.buf
if self.pos:
msg += " at %d" % self.pos
return msg
# the struct formats for the headers
TRANS_HDR = ">8sQcHHH"
DATA_HDR = ">8s8sQQHQ"
# constants to support various header sizes
TRANS_HDR_LEN = 23
DATA_HDR_LEN = 42
assert struct.calcsize(TRANS_HDR) == TRANS_HDR_LEN
assert struct.calcsize(DATA_HDR) == DATA_HDR_LEN
logger = logging.getLogger('ZODB.FileStorage.format')
class FileStorageFormatter(object):
"""Mixin class that can read and write the low-level format."""
# subclasses must provide _file
_metadata_size = 4
_format_version = "21"
def _read_num(self, pos):
"""Read an 8-byte number."""
self._file.seek(pos)
return u64(self._file.read(8))
def _read_data_header(self, pos, oid=None, _file=None):
"""Return a DataHeader object for data record at pos.
If ois is not None, raise CorruptedDataError if oid passed
does not match oid in file.
"""
if _file is None:
_file = self._file
_file.seek(pos)
s = _file.read(DATA_HDR_LEN)
if len(s) != DATA_HDR_LEN:
raise CorruptedDataError(oid, s, pos)
h = DataHeaderFromString(s)
if oid is not None and oid != h.oid:
raise CorruptedDataError(oid, s, pos)
if not h.plen:
h.back = u64(_file.read(8))
return h
def _read_txn_header(self, pos, tid=None):
self._file.seek(pos)
s = self._file.read(TRANS_HDR_LEN)
if len(s) != TRANS_HDR_LEN:
raise CorruptedDataError(tid, s, pos)
h = TxnHeaderFromString(s)
if tid is not None and tid != h.tid:
raise CorruptedDataError(tid, s, pos)
h.user = self._file.read(h.ulen)
h.descr = self._file.read(h.dlen)
h.ext = self._file.read(h.elen)
return h
def _loadBack_impl(self, oid, back, fail=True, _file=None):
# shared implementation used by various _loadBack methods
#
# If the backpointer ultimately resolves to 0:
# If fail is True, raise KeyError for zero backpointer.
# If fail is False, return the empty data from the record
# with no backpointer.
if _file is None:
_file = self._file
while 1:
if not back:
# If backpointer is 0, object does not currently exist.
raise POSKeyError(oid)
h = self._read_data_header(back, _file=_file)
if h.plen:
return _file.read(h.plen), h.tid, back, h.tloc
if h.back == 0 and not fail:
return None, h.tid, back, h.tloc
back = h.back
def _loadBackTxn(self, oid, back, fail=True):
"""Return data and txn id for backpointer."""
return self._loadBack_impl(oid, back, fail)[:2]
def _loadBackPOS(self, oid, back):
return self._loadBack_impl(oid, back)[2]
def getTxnFromData(self, oid, back):
"""Return transaction id for data at back."""
h = self._read_data_header(back, oid)
return h.tid
def fail(self, pos, msg, *args):
s = ("%s:%s:" + msg) % ((self._name, pos) + args)
logger.error(s)
raise CorruptedError(s)
def checkTxn(self, th, pos):
if th.tid <= self.ltid:
self.fail(pos, "time-stamp reduction: %s <= %s",
oid_repr(th.tid), oid_repr(self.ltid))
self.ltid = th.tid
if th.status == "c":
self.fail(pos, "transaction with checkpoint flag set")
if not th.status in " pu": # recognize " ", "p", and "u" as valid
self.fail(pos, "invalid transaction status: %r", th.status)
if th.tlen < th.headerlen():
self.fail(pos, "invalid transaction header: "
"txnlen (%d) < headerlen(%d)", th.tlen, th.headerlen())
def checkData(self, th, tpos, dh, pos):
if dh.tloc != tpos:
self.fail(pos, "data record does not point to transaction header"
": %d != %d", dh.tloc, tpos)
if pos + dh.recordlen() > tpos + th.tlen:
self.fail(pos, "data record size exceeds transaction size: "
"%d > %d", pos + dh.recordlen(), tpos + th.tlen)
if dh.prev >= pos:
self.fail(pos, "invalid previous pointer: %d", dh.prev)
if dh.back:
if dh.back >= pos:
self.fail(pos, "invalid back pointer: %d", dh.prev)
if dh.plen:
self.fail(pos, "data record has back pointer and data")
def DataHeaderFromString(s):
return DataHeader(*struct.unpack(DATA_HDR, s))
class DataHeader(object):
"""Header for a data record."""
__slots__ = ("oid", "tid", "prev", "tloc", "plen", "back")
def __init__(self, oid, tid, prev, tloc, vlen, plen):
if vlen:
raise ValueError(
"Non-zero version length. Versions aren't supported.")
self.oid = oid
self.tid = tid
self.prev = prev
self.tloc = tloc
self.plen = plen
self.back = 0 # default
def asString(self):
return struct.pack(DATA_HDR, self.oid, self.tid, self.prev,
self.tloc, 0, self.plen)
def recordlen(self):
return DATA_HDR_LEN + (self.plen or 8)
def TxnHeaderFromString(s):
res = TxnHeader(*struct.unpack(TRANS_HDR, s))
if PY3:
res.status = res.status.decode('ascii')
return res
class TxnHeader(object):
"""Header for a transaction record."""
__slots__ = ("tid", "tlen", "status", "user", "descr", "ext",
"ulen", "dlen", "elen")
def __init__(self, tid, tlen, status, ulen, dlen, elen):
self.tid = tid
self.tlen = tlen
self.status = status
self.ulen = ulen
self.dlen = dlen
self.elen = elen
assert elen >= 0
def asString(self):
s = struct.pack(TRANS_HDR, self.tid, self.tlen, as_bytes(self.status),
self.ulen, self.dlen, self.elen)
return b"".join(map(as_bytes, [s, self.user, self.descr, self.ext]))
def headerlen(self):
return TRANS_HDR_LEN + self.ulen + self.dlen + self.elen
@@ -0,0 +1,124 @@
from __future__ import print_function
##############################################################################
#
# Copyright (c) 2003 Zope Foundation 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.
#
##############################################################################
import struct
from ZODB.FileStorage import FileIterator
from ZODB.FileStorage.format import TRANS_HDR, TRANS_HDR_LEN
from ZODB.FileStorage.format import DATA_HDR, DATA_HDR_LEN
from ZODB.TimeStamp import TimeStamp
from ZODB.utils import u64, get_pickle_metadata
def fsdump(path, file=None, with_offset=1):
iter = FileIterator(path)
for i, trans in enumerate(iter):
if with_offset:
print(("Trans #%05d tid=%016x time=%s offset=%d" %
(i, u64(trans.tid), TimeStamp(trans.tid), trans._pos)), file=file)
else:
print(("Trans #%05d tid=%016x time=%s" %
(i, u64(trans.tid), TimeStamp(trans.tid))), file=file)
print((" status=%r user=%r description=%r" %
(trans.status, trans.user, trans.description)), file=file)
for j, rec in enumerate(trans):
if rec.data is None:
fullclass = "undo or abort of object creation"
size = ""
else:
modname, classname = get_pickle_metadata(rec.data)
size = " size=%d" % len(rec.data)
fullclass = "%s.%s" % (modname, classname)
if rec.data_txn:
# It would be nice to print the transaction number
# (i) but it would be expensive to keep track of.
bp = " bp=%016x" % u64(rec.data_txn)
else:
bp = ""
print((" data #%05d oid=%016x%s class=%s%s" %
(j, u64(rec.oid), size, fullclass, bp)), file=file)
iter.close()
def fmt(p64):
# Return a nicely formatted string for a packaged 64-bit value
return "%016x" % u64(p64)
class Dumper(object):
"""A very verbose dumper for debuggin FileStorage problems."""
# TODO: Should revise this class to use FileStorageFormatter.
def __init__(self, path, dest=None):
self.file = open(path, "rb")
self.dest = dest
def dump(self):
fid = self.file.read(4)
print("*" * 60, file=self.dest)
print("file identifier: %r" % fid, file=self.dest)
while self.dump_txn():
pass
def dump_txn(self):
pos = self.file.tell()
h = self.file.read(TRANS_HDR_LEN)
if not h:
return False
tid, tlen, status, ul, dl, el = struct.unpack(TRANS_HDR, h)
end = pos + tlen
print("=" * 60, file=self.dest)
print("offset: %d" % pos, file=self.dest)
print("end pos: %d" % end, file=self.dest)
print("transaction id: %s" % fmt(tid), file=self.dest)
print("trec len: %d" % tlen, file=self.dest)
print("status: %r" % status, file=self.dest)
user = descr = extra = ""
if ul:
user = self.file.read(ul)
if dl:
descr = self.file.read(dl)
if el:
extra = self.file.read(el)
print("user: %r" % user, file=self.dest)
print("description: %r" % descr, file=self.dest)
print("len(extra): %d" % el, file=self.dest)
while self.file.tell() < end:
self.dump_data(pos)
stlen = self.file.read(8)
print("redundant trec len: %d" % u64(stlen), file=self.dest)
return 1
def dump_data(self, tloc):
pos = self.file.tell()
h = self.file.read(DATA_HDR_LEN)
assert len(h) == DATA_HDR_LEN
oid, revid, prev, tloc, vlen, dlen = struct.unpack(DATA_HDR, h)
print("-" * 60, file=self.dest)
print("offset: %d" % pos, file=self.dest)
print("oid: %s" % fmt(oid), file=self.dest)
print("revid: %s" % fmt(revid), file=self.dest)
print("previous record offset: %d" % prev, file=self.dest)
print("transaction offset: %d" % tloc, file=self.dest)
assert not vlen
print("len(data): %d" % dlen, file=self.dest)
self.file.read(dlen)
if not dlen:
sbp = self.file.read(8)
print("backpointer: %d" % u64(sbp), file=self.dest)
def main():
import sys
fsdump(sys.argv[1])
@@ -0,0 +1,200 @@
##############################################################################
#
# Copyright (c) 2004 Zope Foundation 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
#
##############################################################################
from __future__ import print_function
import ZODB.FileStorage
from ZODB.utils import get_pickle_metadata, p64, oid_repr, tid_repr
from ZODB.serialize import get_refs
from ZODB.TimeStamp import TimeStamp
# Extract module.class string from pickle.
def get_class(pickle):
return "%s.%s" % get_pickle_metadata(pickle)
# Shorten a string for display.
def shorten(s, size=50):
if len(s) <= size:
return s
# Stick ... in the middle.
navail = size - 5
nleading = navail // 2
ntrailing = size - nleading
return s[:nleading] + " ... " + s[-ntrailing:]
class Tracer(object):
"""Trace all occurrences of a set of oids in a FileStorage.
Create passing a path to an existing FileStorage.
Call register_oids(oid, ...) one or more times to specify which oids to
investigate.
Call run() to do the analysis. This isn't swift -- it has to read
every byte in the database, in order to find all references.
Call report() to display the results.
"""
def __init__(self, path):
import os
if not os.path.isfile(path):
raise ValueError("must specify an existing FileStorage")
self.path = path
# Map an interesting tid to (status, user, description, pos).
self.tid2info = {}
# List of messages. Each is a tuple of the form
# (oid, tid, string)
# The order in the tuple is important, because it defines the
# sort order for grouping.
self.msgs = []
# The set of interesting oids, specified by register_oid() calls.
# Maps oid to # of revisions.
self.oids = {}
# Maps interesting oid to its module.class name. If a creation
# record for an interesting oid is never seen, it won't appear
# in this mapping.
self.oid2name = {}
def register_oids(self, *oids):
"""
Declare that oids (0 or more) are "interesting".
An oid can be given as a native 8-byte string, or as an
integer.
Info will be gathered about all appearances of this oid in the
entire database, including references.
"""
for oid in oids:
if isinstance(oid, bytes):
assert len(oid) == 8
else:
oid = p64(oid)
self.oids[oid] = 0 # 0 revisions seen so far
def _msg(self, oid, tid, *args):
self.msgs.append( (oid, tid, ' '.join(map(str, args))) )
self._produced_msg = True
def report(self):
"""Show all msgs, grouped by oid and sub-grouped by tid."""
msgs = self.msgs
oids = self.oids
oid2name = self.oid2name
# First determine which oids weren't seen at all, and synthesize msgs
# for them.
NOT_SEEN = "this oid was not defined (no data record for it found)"
for oid in oids:
if oid not in oid2name:
msgs.append( (oid, None, NOT_SEEN) )
msgs.sort() # oids are primary key, tids secondary
current_oid = current_tid = None
for oid, tid, msg in msgs:
if oid != current_oid:
nrev = oids[oid]
revision = "revision" + (nrev != 1 and 's' or '')
name = oid2name.get(oid, "<unknown>")
print("oid", oid_repr(oid), name, nrev, revision)
current_oid = oid
current_tid = None
if msg is NOT_SEEN:
assert tid is None
print(" ", msg)
continue
if tid != current_tid:
current_tid = tid
status, user, description, pos = self.tid2info[tid]
print(" tid %s offset=%d %s" % (tid_repr(tid),
pos,
TimeStamp(tid)))
print(" tid user=%r" % shorten(user))
print(" tid description=%r" % shorten(description))
print(" ", msg)
# Do the analysis.
def run(self):
"""Find all occurrences of the registered oids in the database."""
# Maps oid of a reference to its module.class name.
self._ref2name = {}
for txn in ZODB.FileStorage.FileIterator(self.path):
self._check_trec(txn)
# Process next transaction record.
def _check_trec(self, txn):
# txn has members tid, status, user, description,
# _extension, _pos, _tend, _file, _tpos
self._produced_msg = False
# Map and list for save data records for current transaction.
self._records_map = {}
self._records = []
for drec in txn:
self._save_references(drec)
for drec in self._records:
self._check_drec(drec)
if self._produced_msg:
# Copy txn info for later output.
self.tid2info[txn.tid] = (txn.status, txn.user, txn.description,
txn._tpos)
def _save_references(self, drec):
# drec has members oid, tid, data, data_txn
tid, oid, pick, pos = drec.tid, drec.oid, drec.data, drec.pos
if pick:
if oid in self.oids:
klass = get_class(pick)
self._msg(oid, tid, "new revision", klass, "at", pos)
self.oids[oid] += 1
self.oid2name[oid] = self._ref2name[oid] = klass
self._records_map[oid] = drec
self._records.append(drec)
elif oid in self.oids:
self._msg(oid, tid, "creation undo at", pos)
# Process next data record. If a message is produced, self._produced_msg
# will be set True.
def _check_drec(self, drec):
# drec has members oid, tid, data, data_txn
tid, oid, pick, pos = drec.tid, drec.oid, drec.data, drec.pos
ref2name = self._ref2name
ref2name_get = ref2name.get
records_map_get = self._records_map.get
if pick:
oid_in_oids = oid in self.oids
for ref, klass in get_refs(pick):
if ref in self.oids:
oidclass = ref2name_get(oid, None)
if oidclass is None:
ref2name[oid] = oidclass = get_class(pick)
self._msg(ref, tid, "referenced by", oid_repr(oid),
oidclass, "at", pos)
if oid_in_oids:
if klass is None:
klass = ref2name_get(ref, None)
if klass is None:
r = records_map_get(ref, None)
# For save memory we only save references
# seen in one transaction with interesting
# objects changes. So in some circumstances
# we may still got "<unknown>" class name.
if r is None:
klass = "<unknown>"
else:
ref2name[ref] = klass = get_class(r.data)
elif isinstance(klass, tuple):
ref2name[ref] = klass = "%s.%s" % klass
else:
klass = "%s.%s" % (klass.__module__, klass.__name__)
self._msg(oid, tid, "references", oid_repr(ref), klass,
"at", pos)
@@ -0,0 +1,668 @@
##############################################################################
#
# Copyright (c) 2003 Zope Foundation 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.
#
##############################################################################
"""FileStorage helper to perform pack.
A storage contains an ordered set of object revisions. When a storage
is packed, object revisions that are not reachable as of the pack time
are deleted. The notion of reachability is complicated by
backpointers -- object revisions that point to earlier revisions of
the same object.
An object revisions is reachable at a certain time if it is reachable
from the revision of the root at that time or if it is reachable from
a backpointer after that time.
"""
from ZODB.FileStorage.format import DataHeader, TRANS_HDR_LEN
from ZODB.FileStorage.format import FileStorageFormatter, CorruptedDataError
from ZODB.utils import p64, u64, z64
import binascii
import logging
import os
import ZODB.fsIndex
import ZODB.POSException
logger = logging.getLogger(__name__)
class PackError(ZODB.POSException.POSError):
pass
class PackCopier(FileStorageFormatter):
def __init__(self, f, index, tindex):
self._file = f
self._index = index
self._tindex = tindex
self._pos = None
def _txn_find(self, tid, stop_at_pack):
# _pos always points just past the last transaction
pos = self._pos
while pos > 4:
self._file.seek(pos - 8)
pos = pos - u64(self._file.read(8)) - 8
self._file.seek(pos)
h = self._file.read(TRANS_HDR_LEN) # XXX bytes
_tid = h[:8]
if _tid == tid:
return pos
if stop_at_pack:
if h[16] == 'p':
break
raise PackError("Invalid backpointer transaction id")
def _data_find(self, tpos, oid, data):
# Return backpointer for oid. Must call with the lock held.
# This is a file offset to oid's data record if found, else 0.
# The data records in the transaction at tpos are searched for oid.
# If a data record for oid isn't found, returns 0.
# Else if oid's data record contains a backpointer, that
# backpointer is returned.
# Else oid's data record contains the data, and the file offset of
# oid's data record is returned. This data record should contain
# a pickle identical to the 'data' argument.
# Unclear: If the length of the stored data doesn't match len(data),
# an exception is raised. If the lengths match but the data isn't
# the same, 0 is returned. Why the discrepancy?
h = self._read_txn_header(tpos)
tend = tpos + h.tlen
pos = self._file.tell()
while pos < tend:
h = self._read_data_header(pos)
if h.oid == oid:
# Make sure this looks like the right data record
if h.plen == 0:
# This is also a backpointer. Gotta trust it.
return pos
if h.plen != len(data):
# The expected data doesn't match what's in the
# backpointer. Something is wrong.
logger.error("Mismatch between data and backpointer at %d",
pos)
return 0
_data = self._file.read(h.plen)
if data != _data:
return 0
return pos
pos += h.recordlen()
return 0
def copy(self, oid, serial, data, prev_txn, txnpos, datapos):
prev_pos = self._resolve_backpointer(prev_txn, oid, data)
old = self._index.get(oid, 0)
# Calculate the pos the record will have in the storage.
here = datapos
# And update the temp file index
self._tindex[oid] = here
if prev_pos:
# If there is a valid prev_pos, don't write data.
data = None
if data is None:
dlen = 0
else:
dlen = len(data)
# Write the recovery data record
h = DataHeader(oid, serial, old, txnpos, 0, dlen)
self._file.write(h.asString())
# Write the data or a backpointer
if data is None:
if prev_pos:
self._file.write(p64(prev_pos))
else:
# Write a zero backpointer, which indicates an
# un-creation transaction.
self._file.write(z64)
else:
self._file.write(data)
def setTxnPos(self, pos):
self._pos = pos
def _resolve_backpointer(self, prev_txn, oid, data):
pos = self._file.tell()
try:
prev_pos = 0
if prev_txn is not None:
prev_txn_pos = self._txn_find(prev_txn, 0)
if prev_txn_pos:
prev_pos = self._data_find(prev_txn_pos, oid, data)
return prev_pos
finally:
self._file.seek(pos)
class GC(FileStorageFormatter):
def __init__(self, file, eof, packtime, gc, referencesf):
self._file = file
self._name = file.name
self.eof = eof
self.packtime = packtime
self.gc = gc
# packpos: position of first txn header after pack time
self.packpos = None
# {oid -> current data record position}:
self.oid2curpos = ZODB.fsIndex.fsIndex()
# The set of reachable revisions of each object.
#
# This set as managed using two data structures. The first is
# an fsIndex mapping oids to one data record pos. Since only
# a few objects will have more than one revision, we use this
# efficient data structure to handle the common case. The
# second is a dictionary mapping objects to lists of
# positions; it is used to handle the same number of objects
# for which we must keep multiple revisions.
self.reachable = ZODB.fsIndex.fsIndex()
self.reach_ex = {}
# keep ltid for consistency checks during initial scan
self.ltid = z64
self.referencesf = referencesf
def isReachable(self, oid, pos):
"""Return 1 if revision of `oid` at `pos` is reachable."""
rpos = self.reachable.get(oid)
if rpos is None:
return 0
if rpos == pos:
return 1
return pos in self.reach_ex.get(oid, [])
def findReachable(self):
self.buildPackIndex()
if self.gc:
self.findReachableAtPacktime([z64])
self.findReachableFromFuture()
# These mappings are no longer needed and may consume a lot of
# space.
del self.oid2curpos
else:
self.reachable = self.oid2curpos
def buildPackIndex(self):
pos = 4
# We make the initial assumption that the database has been
# packed before and set unpacked to True only after seeing the
# first record with a status == " ". If we get to the packtime
# and unpacked is still False, we need to watch for a redundant
# pack.
unpacked = False
while pos < self.eof:
th = self._read_txn_header(pos)
if th.tid > self.packtime:
break
self.checkTxn(th, pos)
if th.status != "p":
unpacked = True
tpos = pos
end = pos + th.tlen
pos += th.headerlen()
while pos < end:
dh = self._read_data_header(pos)
self.checkData(th, tpos, dh, pos)
if dh.plen or dh.back:
self.oid2curpos[dh.oid] = pos
else:
if dh.oid in self.oid2curpos:
del self.oid2curpos[dh.oid]
pos += dh.recordlen()
tlen = self._read_num(pos)
if tlen != th.tlen:
self.fail(pos, "redundant transaction length does not "
"match initial transaction length: %d != %d",
tlen, th.tlen)
pos += 8
self.packpos = pos
if unpacked:
return
# check for a redundant pack. If the first record following
# the newly computed packpos has status 'p', then it was
# packed earlier and the current pack is redudant.
try:
th = self._read_txn_header(pos)
except CorruptedDataError as err:
if err.buf != b"":
raise
if th.status == 'p':
# Delayed import to cope with circular imports.
# TODO: put exceptions in a separate module.
from ZODB.FileStorage.FileStorage import RedundantPackWarning
raise RedundantPackWarning(
"The database has already been packed to a later time"
" or no changes have been made since the last pack")
def findReachableAtPacktime(self, roots):
"""Mark all objects reachable from the oids in roots as reachable."""
reachable = self.reachable
oid2curpos = self.oid2curpos
todo = list(roots)
while todo:
oid = todo.pop()
if oid in reachable:
continue
try:
pos = oid2curpos[oid]
except KeyError:
if oid == z64 and len(oid2curpos) == 0:
# special case, pack to before creation time
continue
raise KeyError(oid)
reachable[oid] = pos
for oid in self.findrefs(pos):
if oid not in reachable:
todo.append(oid)
def findReachableFromFuture(self):
# In this pass, the roots are positions of object revisions.
# We add a pos to extra_roots when there is a backpointer to a
# revision that was not current at the packtime. The
# non-current revision could refer to objects that were
# otherwise unreachable at the packtime.
extra_roots = []
pos = self.packpos
while pos < self.eof:
th = self._read_txn_header(pos)
self.checkTxn(th, pos)
tpos = pos
end = pos + th.tlen
pos += th.headerlen()
while pos < end:
dh = self._read_data_header(pos)
self.checkData(th, tpos, dh, pos)
if dh.back and dh.back < self.packpos:
if dh.oid in self.reachable:
L = self.reach_ex.setdefault(dh.oid, [])
if dh.back not in L:
L.append(dh.back)
extra_roots.append(dh.back)
else:
self.reachable[dh.oid] = dh.back
pos += dh.recordlen()
tlen = self._read_num(pos)
if tlen != th.tlen:
self.fail(pos, "redundant transaction length does not "
"match initial transaction length: %d != %d",
tlen, th.tlen)
pos += 8
for pos in extra_roots:
refs = self.findrefs(pos)
self.findReachableAtPacktime(refs)
def findrefs(self, pos):
"""Return a list of oids referenced as of packtime."""
dh = self._read_data_header(pos)
# Chase backpointers until we get to the record with the refs
while dh.back:
dh = self._read_data_header(dh.back)
if dh.plen:
return self.referencesf(self._file.read(dh.plen))
else:
return []
class FileStoragePacker(FileStorageFormatter):
# path is the storage file path.
# stop is the pack time, as a TimeStamp.
# current_size is the storage's _pos. All valid data at the start
# lives before that offset (there may be a checkpoint transaction in
# progress after it).
def __init__(self, storage, referencesf, stop, gc=True):
self._storage = storage
if storage.blob_dir:
self.pack_blobs = True
self.blob_removed = open(
os.path.join(storage.blob_dir, '.removed'), 'wb')
else:
self.pack_blobs = False
self.blob_removed = None
path = storage._file.name
self._name = path
# We open our own handle on the storage so that much of pack can
# proceed in parallel. It's important to close this file at every
# return point, else on Windows the caller won't be able to rename
# or remove the storage file.
self._file = open(path, "rb")
self._path = path
self._stop = stop
self.locked = False
self.file_end = storage.getSize()
self.gc = GC(self._file, self.file_end, self._stop, gc, referencesf)
# The packer needs to acquire the parent's commit lock
# during the copying stage, so the two sets of lock acquire
# and release methods are passed to the constructor.
self._lock = storage._lock
self._commit_lock = storage._commit_lock
# The packer will use several indexes.
# index: oid -> pos
# tindex: oid -> pos, for current txn
# oid2tid: not used by the packer
self.index = ZODB.fsIndex.fsIndex()
self.tindex = {}
self.oid2tid = {}
self.toid2tid = {}
self.toid2tid_delete = {}
self._tfile = None
def close(self):
self._file.close()
if self._tfile is not None:
self._tfile.close()
if self.blob_removed is not None:
self.blob_removed.close()
def pack(self):
# Pack copies all data reachable at the pack time or later.
#
# Copying occurs in two phases. In the first phase, txns
# before the pack time are copied if the contain any reachable
# data. In the second phase, all txns after the pack time
# are copied.
#
# Txn and data records contain pointers to previous records.
# Because these pointers are stored as file offsets, they
# must be updated when we copy data.
# TODO: Should add sanity checking to pack.
self.gc.findReachable()
def close_files_remove():
# blank except: we might be in an IOError situation/handler
# try our best, but don't fail
try:
self._tfile.close()
except:
pass
try:
self._file.close()
except:
pass
try:
os.remove(self._name + ".pack")
except:
pass
if self.blob_removed is not None:
self.blob_removed.close()
# Setup the destination file and copy the metadata.
# TODO: rename from _tfile to something clearer.
self._tfile = open(self._name + ".pack", "w+b")
try:
self._file.seek(0)
self._tfile.write(self._file.read(self._metadata_size))
self._copier = PackCopier(self._tfile, self.index, self.tindex)
ipos, opos = self.copyToPacktime()
except (OSError, IOError):
# most probably ran out of disk space or some other IO error
close_files_remove()
raise # don't succeed silently
assert ipos == self.gc.packpos
if ipos == opos:
# pack didn't free any data. there's no point in continuing.
close_files_remove()
return None
self._commit_lock.acquire()
self.locked = True
try:
with self._lock:
# Re-open the file in unbuffered mode.
# The main thread may write new transactions to the
# file, which creates the possibility that we will
# read a status 'c' transaction into the pack thread's
# stdio buffer even though we're acquiring the commit
# lock. Transactions can still be in progress
# throughout much of packing, and are written to the
# same physical file but via a distinct Python file
# object. The code used to leave off the trailing 0
# argument, and then on every platform except native
# Windows it was observed that we could read stale
# data from the tail end of the file.
self._file.close() # else self.gc keeps the original
# alive & open
self._file = open(self._path, "rb", 0)
self._file.seek(0, 2)
self.file_end = self._file.tell()
if ipos < self.file_end:
self.copyRest(ipos)
# OK, we've copied everything. Now we need to wrap things up.
pos = self._tfile.tell()
self._tfile.flush()
self._tfile.close()
self._file.close()
if self.blob_removed is not None:
self.blob_removed.close()
return pos
except (OSError, IOError):
# most probably ran out of disk space or some other IO error
close_files_remove()
if self.locked:
self._commit_lock.release()
raise # don't succeed silently
except:
if self.locked:
self._commit_lock.release()
raise
def copyToPacktime(self):
offset = 0 # the amount of space freed by packing
pos = self._metadata_size
new_pos = pos
while pos < self.gc.packpos:
th = self._read_txn_header(pos)
new_tpos, pos = self.copyDataRecords(pos, th)
if new_tpos:
new_pos = self._tfile.tell() + 8
tlen = new_pos - new_tpos - 8
# Update the transaction length
self._tfile.seek(new_tpos + 8)
self._tfile.write(p64(tlen))
self._tfile.seek(new_pos - 8)
self._tfile.write(p64(tlen))
tlen = self._read_num(pos)
if tlen != th.tlen:
self.fail(pos, "redundant transaction length does not "
"match initial transaction length: %d != %d",
tlen, th.tlen)
pos += 8
return pos, new_pos
def copyDataRecords(self, pos, th):
"""Copy any current data records between pos and tend.
Returns position of txn header in output file and position
of next record in the input file.
If any data records are copied, also write txn header (th).
"""
copy = 0
new_tpos = 0
tend = pos + th.tlen
pos += th.headerlen()
while pos < tend:
h = self._read_data_header(pos)
if not self.gc.isReachable(h.oid, pos):
if self.pack_blobs:
# We need to find out if this is a blob, so get the data:
if h.plen:
data = self._file.read(h.plen)
else:
data = self.fetchDataViaBackpointer(h.oid, h.back)
if data and self._storage.is_blob_record(data):
# We need to remove the blob record. Maybe we
# need to remove oid:
# But first, we need to make sure the record
# we're looking at isn't a dup of the current
# record. There's a bug in ZEO blob support that causes
# duplicate data records.
rpos = self.gc.reachable.get(h.oid)
is_dup = (rpos
and self._read_data_header(rpos).tid == h.tid)
if not is_dup:
if h.oid not in self.gc.reachable:
self.blob_removed.write(
binascii.hexlify(h.oid)+b'\n')
else:
self.blob_removed.write(
binascii.hexlify(h.oid+h.tid)+b'\n')
pos += h.recordlen()
continue
pos += h.recordlen()
# If we are going to copy any data, we need to copy
# the transaction header. Note that we will need to
# patch up the transaction length when we are done.
if not copy:
th.status = "p"
s = th.asString()
new_tpos = self._tfile.tell()
self._tfile.write(s)
new_pos = new_tpos + len(s)
copy = 1
if h.plen:
data = self._file.read(h.plen)
else:
data = self.fetchDataViaBackpointer(h.oid, h.back)
self.writePackedDataRecord(h, data, new_tpos)
new_pos = self._tfile.tell()
return new_tpos, pos
def fetchDataViaBackpointer(self, oid, back):
"""Return the data for oid via backpointer back
If `back` is 0 or ultimately resolves to 0, return None.
In this case, the transaction undoes the object
creation.
"""
if back == 0:
return None
data, tid = self._loadBackTxn(oid, back, 0)
return data
def writePackedDataRecord(self, h, data, new_tpos):
# Update the header to reflect current information, then write
# it to the output file.
if data is None:
data = b''
h.prev = 0
h.back = 0
h.plen = len(data)
h.tloc = new_tpos
pos = self._tfile.tell()
self.index[h.oid] = pos
self._tfile.write(h.asString())
self._tfile.write(data)
if not data:
# Packed records never have backpointers (?).
# If there is no data, write a z64 backpointer.
# This is a George Bailey event.
self._tfile.write(z64)
def copyRest(self, ipos):
# After the pack time, all data records are copied.
# Copy one txn at a time, using copy() for data.
try:
while 1:
ipos = self.copyOne(ipos)
except CorruptedDataError as err:
# The last call to copyOne() will raise
# CorruptedDataError, because it will attempt to read past
# the end of the file. Double-check that the exception
# occurred for this reason.
self._file.seek(0, 2)
endpos = self._file.tell()
if endpos != err.pos:
raise
def copyOne(self, ipos):
# The call below will raise CorruptedDataError at EOF.
th = self._read_txn_header(ipos)
# Release commit lock while writing to pack file
self._commit_lock.release()
self.locked = False
pos = self._tfile.tell()
self._copier.setTxnPos(pos)
self._tfile.write(th.asString())
tend = ipos + th.tlen
ipos += th.headerlen()
while ipos < tend:
h = self._read_data_header(ipos)
ipos += h.recordlen()
prev_txn = None
if h.plen:
data = self._file.read(h.plen)
else:
data = self.fetchDataViaBackpointer(h.oid, h.back)
if h.back:
prev_txn = self.getTxnFromData(h.oid, h.back)
self._copier.copy(h.oid, h.tid, data, prev_txn,
pos, self._tfile.tell())
tlen = self._tfile.tell() - pos
assert tlen == th.tlen
self._tfile.write(p64(tlen))
ipos += 8
self.index.update(self.tindex)
self.tindex.clear()
self._commit_lock.acquire()
self.locked = True
return ipos
@@ -0,0 +1,77 @@
##############################################################################
#
# Copyright (c) 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.
#
##############################################################################
import zope.interface
class IFileStoragePacker(zope.interface.Interface):
def __call__(storage, referencesf, stop, gc):
r"""Pack the file storage into a new file
:param FileStorage storage: The storage object to be packed
:param callable referencesf: A function that extracts object
references from a pickle bytes string. This is usually
``ZODB.serialize.referencesf``.
:param bytes stop: A transaction id representing the time at
which to stop packing.
:param bool gc: A flag indicating whether garbage collection
should be performed.
The new file will have the same name as the old file with
``.pack`` appended. (The packer can get the old file name via
storage._file.name.) If blobs are supported, if the storages
blob_dir attribute is not None or empty, then a .removed file
must be created in the blob directory. This file contains records of
the form::
(oid+serial).encode('hex')+'\n'
or, of the form::
oid.encode('hex')+'\n'
If packing is unnecessary, or would not change the file, then
no pack or removed files are created None is returned,
otherwise a tuple is returned with:
- the size of the packed file, and
- the packed index
If and only if packing was necessary (non-None) and there was
no error, then the commit lock must be acquired. In addition,
it is up to FileStorage to:
- Rename the .pack file, and
- process the blob_dir/.removed file by removing the blobs
corresponding to the file records.
"""
class IFileStorage(zope.interface.Interface):
packer = zope.interface.Attribute(
"The IFileStoragePacker to be used for packing."
)
_file = zope.interface.Attribute(
"The file object used to access the underlying data."
)
_lock = zope.interface.Attribute(
"The storage lock."
)
_commit_lock = zope.interface.Attribute(
"The storage commit lock."
)
@@ -0,0 +1,182 @@
FileStorage-specific iterator tests
===================================
The FileStorage iterator has some special features that deserve some
special tests.
We'll make some assertions about time, so we'll take it over:
>>> now = 1229959248
>>> def faux_time():
... global now
... now += 0.1
... return now
>>> import time
>>> time_time = time.time
>>> if isinstance(time,type):
... time.time = staticmethod(faux_time) # Jython
... else:
... time.time = faux_time
Commit a bunch of transactions:
>>> import ZODB.FileStorage, transaction
>>> db = ZODB.DB('data.fs')
>>> tids = [db.storage.lastTransaction()]
>>> poss = [db.storage._pos]
>>> conn = db.open()
>>> for i in range(100):
... conn.root()[i] = conn.root().__class__()
... transaction.commit()
... tids.append(db.storage.lastTransaction())
... poss.append(db.storage._pos)
Deciding where to start
-----------------------
By default, we start at the beginning:
>>> it = ZODB.FileStorage.FileIterator('data.fs')
>>> it.next().tid == tids[0]
True
>>> it.close()
The file iterator has an optimization to deal with large files. It
can serarch from either the front or the back of the file, depending
on the starting transaction given. To see this, we'll turn on debug
logging:
>>> import logging, sys
>>> old_log_level = logging.getLogger().getEffectiveLevel()
>>> logging.getLogger().setLevel(logging.DEBUG)
>>> handler = logging.StreamHandler(sys.stdout)
>>> logging.getLogger().addHandler(handler)
If we specify a start transaction, we'll scan forward or backward, as
seems best and set the next record to that:
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[0])
>>> it.next().tid == tids[0]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[1])
... # doctest: +ELLIPSIS
Scan forward data.fs:<OFFSET> looking for ...
>>> it.next().tid == tids[1]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[30])
... # doctest: +ELLIPSIS
Scan forward data.fs:<OFFSET> looking for ...
>>> it.next().tid == tids[30]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[70])
... # doctest: +ELLIPSIS
Scan backward data.fs:<OFFSET> looking for ...
>>> it.next().tid == tids[70]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[-2])
... # doctest: +ELLIPSIS
Scan backward data.fs:<OFFSET> looking for ...
>>> it.next().tid == tids[-2]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[-1])
>>> it.next().tid == tids[-1]
True
>>> it.close()
We can also supply a file position. This can speed up finding the
starting point, or just pick up where another iterator left off:
>>> it = ZODB.FileStorage.FileIterator('data.fs', pos=poss[50])
>>> it.next().tid == tids[51]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[0], pos=4)
>>> it.next().tid == tids[0]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[-1], pos=poss[-2])
>>> it.next().tid == tids[-1]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[50], pos=poss[50])
... # doctest: +ELLIPSIS
Scan backward data.fs:<OFFSET> looking for ...
>>> it.next().tid == tids[50]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[49], pos=poss[50])
... # doctest: +ELLIPSIS
Scan backward data.fs:<OFFSET> looking for ...
>>> it.next().tid == tids[49]
True
>>> it.close()
>>> it = ZODB.FileStorage.FileIterator('data.fs', tids[51], pos=poss[50])
>>> it.next().tid == tids[51]
True
>>> it.close()
>>> logging.getLogger().setLevel(old_log_level)
>>> logging.getLogger().removeHandler(handler)
If a starting transaction is before the first transaction in the file,
then the first transaction is returned.
>>> from ZODB.utils import p64, u64
>>> it = ZODB.FileStorage.FileIterator('data.fs', p64(u64(tids[0])-1))
>>> it.next().tid == tids[0]
True
>>> it.close()
If it is after the last transaction, then iteration be empty:
>>> it = ZODB.FileStorage.FileIterator('data.fs', p64(u64(tids[-1])+1))
>>> list(it)
[]
>>> it.close()
Even if we write more transactions:
>>> it = ZODB.FileStorage.FileIterator('data.fs', p64(u64(tids[-1])+1))
>>> for i in range(10):
... conn.root()[i] = conn.root().__class__()
... transaction.commit()
>>> list(it)
[]
>>> it.close()
.. Cleanup
>>> time.time = time_time
>>> db.close()
@@ -0,0 +1,322 @@
##############################################################################
#
# Copyright (c) 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.
#
##############################################################################
import doctest
import os
import re
import time
import transaction
import unittest
import ZODB.blob
import ZODB.FileStorage
import ZODB.tests.util
from ZODB.Connection import TransactionMetaData
from zope.testing import renormalizing
checker = renormalizing.RENormalizing([
# Python 3 bytes add a "b".
(re.compile("b('.*?')"), r"\1"),
# Python 3 adds module name to exceptions.
(re.compile("ZODB.POSException.POSKeyError"), r"POSKeyError"),
(re.compile("ZODB.FileStorage.FileStorage.FileStorageQuotaError"),
"FileStorageQuotaError"),
(re.compile('data.fs:[0-9]+'), 'data.fs:<OFFSET>'),
])
def pack_keep_old():
"""Should a copy of the database be kept?
The pack_keep_old constructor argument controls whether a .old file (and .old
directory for blobs is kept.)
>>> fs = ZODB.FileStorage.FileStorage('data.fs', blob_dir='blobs')
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> import ZODB.blob
>>> conn.root()[1] = ZODB.blob.Blob()
>>> with conn.root()[1].open('w') as file:
... _ = file.write(b'some data')
>>> conn.root()[2] = ZODB.blob.Blob()
>>> with conn.root()[2].open('w') as file:
... _ = file.write(b'some data')
>>> transaction.commit()
>>> with conn.root()[1].open('w') as file:
... _ = file.write(b'some other data')
>>> del conn.root()[2]
>>> transaction.commit()
>>> old_size = os.stat('data.fs').st_size
>>> def get_blob_size(d):
... result = 0
... for path, dirs, file_names in os.walk(d):
... for file_name in file_names:
... result += os.stat(os.path.join(path, file_name)).st_size
... return result
>>> blob_size = get_blob_size('blobs')
>>> db.pack(time.time()+1)
>>> packed_size = os.stat('data.fs').st_size
>>> packed_size < old_size
True
>>> os.stat('data.fs.old').st_size == old_size
True
>>> packed_blob_size = get_blob_size('blobs')
>>> packed_blob_size < blob_size
True
>>> get_blob_size('blobs.old') == blob_size
True
>>> db.close()
>>> fs = ZODB.FileStorage.FileStorage('data.fs', blob_dir='blobs',
... create=True, pack_keep_old=False)
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> conn.root()[1] = ZODB.blob.Blob()
>>> with conn.root()[1].open('w') as file:
... _ = file.write(b'some data')
>>> conn.root()[2] = ZODB.blob.Blob()
>>> with conn.root()[2].open('w') as file:
... _ = file.write(b'some data')
>>> transaction.commit()
>>> with conn.root()[1].open('w') as file:
... _ = file.write(b'some other data')
>>> del conn.root()[2]
>>> transaction.commit()
>>> db.pack(time.time()+1)
>>> os.stat('data.fs').st_size == packed_size
True
>>> os.path.exists('data.fs.old')
False
>>> get_blob_size('blobs') == packed_blob_size
True
>>> os.path.exists('blobs.old')
False
>>> db.close()
"""
def pack_with_repeated_blob_records():
"""
There is a bug in ZEO that causes duplicate bloc database records
to be written in a blob store operation. (Maybe this has been
fixed by the time you read this, but there might still be
transactions in the wild that have duplicate records.
>>> fs = ZODB.FileStorage.FileStorage('t', blob_dir='bobs')
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> conn.root()[1] = ZODB.blob.Blob()
>>> transaction.commit()
>>> tm = transaction.TransactionManager()
>>> oid = conn.root()[1]._p_oid
>>> from ZODB.utils import load_current
>>> blob_record, oldserial = load_current(fs, oid)
Now, create a transaction with multiple saves:
>>> trans = TransactionMetaData()
>>> fs.tpc_begin(trans)
>>> with open('ablob', 'w') as file:
... _ = file.write('some data')
>>> fs.store(oid, oldserial, blob_record, '', trans)
>>> fs.storeBlob(oid, oldserial, blob_record, 'ablob', '', trans)
>>> _ = fs.tpc_vote(trans)
>>> _ = fs.tpc_finish(trans)
>>> time.sleep(.01)
>>> db.pack()
>>> conn.sync()
>>> with conn.root()[1].open() as fp: fp.read()
'some data'
>>> db.close()
"""
def _save_index():
"""
_save_index can fail for large indexes.
>>> import ZODB.utils
>>> fs = ZODB.FileStorage.FileStorage('data.fs')
>>> t = TransactionMetaData()
>>> fs.tpc_begin(t)
>>> oid = 0
>>> for i in range(5000):
... oid += (1<<16)
... fs.store(ZODB.utils.p64(oid), ZODB.utils.z64, b'x', '', t)
>>> _ = fs.tpc_vote(t)
>>> _ = fs.tpc_finish(t)
>>> import sys
>>> old_limit = sys.getrecursionlimit()
>>> sys.setrecursionlimit(50)
>>> fs._save_index()
Make sure we can restore:
>>> import logging
>>> handler = logging.StreamHandler(sys.stdout)
>>> logger = logging.getLogger('ZODB.FileStorage')
>>> logger.setLevel(logging.DEBUG)
>>> logger.addHandler(handler)
>>> index, pos, tid = fs._restore_index()
>>> index.items() == fs._index.items()
True
>>> pos, tid = fs._pos, fs._tid
cleanup
>>> fs.close()
>>> logger.setLevel(logging.NOTSET)
>>> logger.removeHandler(handler)
>>> sys.setrecursionlimit(old_limit)
"""
def pack_disk_full_copyToPacktime():
"""Recover from a disk full situation by removing the `.pack` file
`copyToPacktime` fails
Add some data
>>> fs = ZODB.FileStorage.FileStorage('data.fs')
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> conn.root()[1] = 'foobar'
>>> transaction.commit()
patch `copyToPacktime` to fail
>>> from ZODB.FileStorage import fspack
>>> save_copyToPacktime = fspack.FileStoragePacker.copyToPacktime
>>> def failing_copyToPacktime(self):
... self._tfile.write(b'somejunkdata')
... raise OSError("No space left on device")
>>> fspack.FileStoragePacker.copyToPacktime = failing_copyToPacktime
pack -- it still raises `OSError`
>>> db.pack(time.time()+1)
Traceback (most recent call last):
...
OSError: No space left on device
`data.fs.pack` must not exist
>>> os.path.exists('data.fs.pack')
False
undo patching
>>> fspack.FileStoragePacker.copyToPacktime = save_copyToPacktime
>>> db.close()
check the data we added
>>> fs = ZODB.FileStorage.FileStorage('data.fs')
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> conn.root()[1]
'foobar'
>>> db.close()
"""
def pack_disk_full_copyRest():
"""Recover from a disk full situation by removing the `.pack` file
`copyRest` fails
Add some data
>>> fs = ZODB.FileStorage.FileStorage('data.fs')
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> conn.root()[1] = 'foobar'
>>> transaction.commit()
patch `copyToPacktime` to add one more transaction
>>> from ZODB.FileStorage import fspack
>>> save_copyToPacktime = fspack.FileStoragePacker.copyToPacktime
>>> def patched_copyToPacktime(self):
... res = save_copyToPacktime(self)
... conn2 = db.open()
... conn2.root()[2] = 'another bar'
... transaction.commit()
... return res
>>> fspack.FileStoragePacker.copyToPacktime = patched_copyToPacktime
patch `copyRest` to fail
>>> save_copyRest = fspack.FileStoragePacker.copyRest
>>> def failing_copyRest(self, ipos):
... self._tfile.write(b'somejunkdata')
... raise OSError("No space left on device")
>>> fspack.FileStoragePacker.copyRest = failing_copyRest
pack -- it still raises `OSError`
>>> db.pack(time.time()+1)
Traceback (most recent call last):
...
OSError: No space left on device
`data.fs.pack` must not exist
>>> os.path.exists('data.fs.pack')
False
undo patching
>>> fspack.FileStoragePacker.copyToPacktime = save_copyToPacktime
>>> fspack.FileStoragePacker.copyRest = save_copyRest
>>> db.close()
check the data we added
>>> fs = ZODB.FileStorage.FileStorage('data.fs')
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> conn.root()[1]
'foobar'
>>> conn.root()[2]
'another bar'
>>> db.close()
"""
def test_suite():
return unittest.TestSuite((
doctest.DocFileSuite(
'zconfig.txt',
'iterator.test',
setUp=ZODB.tests.util.setUp,
tearDown=ZODB.tests.util.tearDown,
checker=checker),
doctest.DocTestSuite(
setUp=ZODB.tests.util.setUp,
tearDown=ZODB.tests.util.tearDown,
checker=checker),
))
@@ -0,0 +1,209 @@
Defining FileStorages using ZConfig
===================================
ZODB provides support for defining many storages, including
FileStorages, using ZConfig. To define a FileStorage, you use a
filestorage section, and define a path:
>>> import ZODB.config
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... </filestorage>
... """)
>>> fs._file.name
'my.fs'
>>> fs.close()
There are a number of options we can provide:
blob-dir
If supplied, the file storage will provide blob support and this
is the name of a directory to hold blob data. The directory will
be created if it doeesn't exist. If no value (or an empty value)
is provided, then no blob support will be provided. (You can still
use a BlobStorage to provide blob support.)
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... blob-dir blobs
... </filestorage>
... """)
>>> fs._file.name
'my.fs'
>>> import os
>>> os.path.basename(fs.blob_dir)
'blobs'
create
Flag that indicates whether the storage should be truncated if
it already exists.
To demonstrate this, we'll first write some data:
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> import ZODB.blob, transaction
>>> conn.root()[1] = ZODB.blob.Blob()
>>> transaction.commit()
>>> db.close()
Then reopen with the create option:
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... blob-dir blobs
... create true
... </filestorage>
... """)
Because the file was truncated, we no-longer have object 0:
>>> fs.load(b'\0'*8)
Traceback (most recent call last):
...
POSKeyError: 0x00
>>> sorted(os.listdir('blobs'))
['.layout', 'tmp']
>>> fs.close()
read-only
If true, only reads may be executed against the storage. Note
that the "pack" operation is not considered a write operation
and is still allowed on a read-only filestorage.
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... read-only true
... </filestorage>
... """)
>>> fs.isReadOnly()
True
>>> fs.close()
quota
Maximum allowed size of the storage file. Operations which
would cause the size of the storage to exceed the quota will
result in a ZODB.FileStorage.FileStorageQuotaError being
raised.
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... quota 10
... </filestorage>
... """)
>>> db = ZODB.DB(fs) # writes object 0
Traceback (most recent call last):
...
FileStorageQuotaError: The storage quota has been exceeded.
>>> fs.close()
packer
The dotten name (dotten module name and object name) of a
packer object. This is used to provide an alternative pack
implementation.
To demonstrate this, we'll create a null packer that just prints
some information about it's arguments:
>>> import six
>>> def packer(storage, referencesf, stop, gc):
... six.print_(referencesf, storage is fs, gc, storage.pack_keep_old)
>>> ZODB.FileStorage.config_demo_printing_packer = packer
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... packer ZODB.FileStorage.config_demo_printing_packer
... </filestorage>
... """)
>>> import time
>>> db = ZODB.DB(fs) # writes object 0
>>> fs.pack(time.time(), 42)
42 True True True
>>> fs.close()
If the packer contains a ':', then the text after the first ':' is
interpreted as an expression. This is handy to pass limited
configuration information to the packer:
>>> def packer_factory(name):
... def packer(storage, referencesf, stop, gc):
... six.print_(repr(name), referencesf, storage is fs,
... gc, storage.pack_keep_old)
... return packer
>>> ZODB.FileStorage.config_demo_printing_packer_factory = packer_factory
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... packer ZODB.FileStorage:config_demo_printing_packer_factory('bob ')
... </filestorage>
... """)
>>> import time
>>> db = ZODB.DB(fs) # writes object 0
>>> fs.pack(time.time(), 42)
'bob ' 42 True True True
>>> fs.close()
pack-gc
If false, then no garbage collection will be performed when
packing. This can make packing go much faster and can avoid
problems when objects are referenced only from other
databases.
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... packer ZODB.FileStorage.config_demo_printing_packer
... pack-gc false
... </filestorage>
... """)
>>> fs.pack(time.time(), 42)
42 True False True
Note that if we pass the gc option to pack, then this will
override the value set in the configuration:
>>> fs.pack(time.time(), 42, gc=True)
42 True True True
>>> fs.close()
pack-keep-old
If false, then old files aren't kept when packing
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... packer ZODB.FileStorage.config_demo_printing_packer
... pack-keep-old false
... </filestorage>
... """)
>>> fs.pack(time.time(), 42)
42 True True False
>>> fs.close()
@@ -0,0 +1,374 @@
##############################################################################
#
# Copyright (c) Zope Foundation 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 simple in-memory mapping-based ZODB storage
This storage provides an example implementation of a fairly full
storage without distracting storage details.
"""
import BTrees
import time
import ZODB.BaseStorage
import ZODB.interfaces
import ZODB.POSException
import ZODB.TimeStamp
import ZODB.utils
import zope.interface
@zope.interface.implementer(
ZODB.interfaces.IStorage,
ZODB.interfaces.IStorageIteration,
)
class MappingStorage(object):
"""In-memory storage implementation
Note that this implementation is somewhat naive and inefficient
with regard to locking. Its implementation is primarily meant to
be a simple illustration of storage implementation. It's also
useful for testing and exploration where scalability and efficiency
are unimportant.
"""
def __init__(self, name='MappingStorage'):
"""Create a mapping storage
The name parameter is used by the
:meth:`~ZODB.interfaces.IStorage.getName` and
:meth:`~ZODB.interfaces.IStorage.sortKey` methods.
"""
self.__name__ = name
self._data = {} # {oid->{tid->pickle}}
self._transactions = BTrees.OOBTree.OOBTree() # {tid->TransactionRecord}
self._ltid = ZODB.utils.z64
self._last_pack = None
self._lock = ZODB.utils.RLock()
self._commit_lock = ZODB.utils.Lock()
self._opened = True
self._transaction = None
self._oid = 0
######################################################################
# Preconditions:
def opened(self):
"""The storage is open
"""
return self._opened
def not_in_transaction(self):
"""The storage is not committing a transaction
"""
return self._transaction is None
#
######################################################################
# testing framework (lame)
def cleanup(self):
pass
# ZODB.interfaces.IStorage
@ZODB.utils.locked
def close(self):
self._opened = False
# ZODB.interfaces.IStorage
def getName(self):
return self.__name__
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def getSize(self):
size = 0
for oid, tid_data in self._data.items():
size += 50
for tid, pickle in tid_data.items():
size += 100+len(pickle)
return size
# ZEO.interfaces.IServeable
@ZODB.utils.locked(opened)
def getTid(self, oid):
tid_data = self._data.get(oid)
if tid_data:
return tid_data.maxKey()
raise ZODB.POSException.POSKeyError(oid)
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def history(self, oid, size=1):
tid_data = self._data.get(oid)
if not tid_data:
raise ZODB.POSException.POSKeyError(oid)
tids = tid_data.keys()[-size:]
tids.reverse()
return [
dict(
time = ZODB.TimeStamp.TimeStamp(tid).timeTime(),
tid = tid,
serial = tid,
user_name = self._transactions[tid].user,
description = self._transactions[tid].description,
extension = self._transactions[tid].extension,
size = len(tid_data[tid])
)
for tid in tids]
# ZODB.interfaces.IStorage
def isReadOnly(self):
return False
# ZODB.interfaces.IStorageIteration
def iterator(self, start=None, end=None):
for transaction_record in self._transactions.values(start, end):
yield transaction_record
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def lastTransaction(self):
return self._ltid
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def __len__(self):
return len(self._data)
load = ZODB.utils.load_current
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def loadBefore(self, oid, tid):
tid_data = self._data.get(oid)
if tid_data:
before = ZODB.utils.u64(tid)
if not before:
return None
before = ZODB.utils.p64(before-1)
tids_before = tid_data.keys(None, before)
if tids_before:
tids_after = tid_data.keys(tid, None)
tid = tids_before[-1]
return (tid_data[tid], tid,
(tids_after and tids_after[0] or None)
)
else:
raise ZODB.POSException.POSKeyError(oid)
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def loadSerial(self, oid, serial):
tid_data = self._data.get(oid)
if tid_data:
try:
return tid_data[serial]
except KeyError:
pass
raise ZODB.POSException.POSKeyError(oid, serial)
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def new_oid(self):
self._oid += 1
return ZODB.utils.p64(self._oid)
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def pack(self, t, referencesf, gc=True):
if not self._data:
return
stop = ZODB.TimeStamp.TimeStamp(*time.gmtime(t)[:5]+(t%60,)).raw()
if self._last_pack is not None and self._last_pack >= stop:
if self._last_pack == stop:
return
raise ValueError("Already packed to a later time")
self._last_pack = stop
transactions = self._transactions
# Step 1, remove old non-current records
for oid, tid_data in self._data.items():
tids_to_remove = tid_data.keys(None, stop)
if tids_to_remove:
tids_to_remove.pop() # Keep the last, if any
if tids_to_remove:
for tid in tids_to_remove:
del tid_data[tid]
if transactions[tid].pack(oid):
del transactions[tid]
if gc:
# Step 2, GC. A simple sweep+copy
new_data = BTrees.OOBTree.OOBTree()
to_copy = set([ZODB.utils.z64])
while to_copy:
oid = to_copy.pop()
tid_data = self._data.pop(oid)
new_data[oid] = tid_data
for pickle in tid_data.values():
for oid in referencesf(pickle):
if oid in new_data:
continue
to_copy.add(oid)
# Remove left over data from transactions
for oid, tid_data in self._data.items():
for tid in tid_data:
if transactions[tid].pack(oid):
del transactions[tid]
self._data.clear()
self._data.update(new_data)
# ZODB.interfaces.IStorage
def registerDB(self, db):
pass
# ZODB.interfaces.IStorage
def sortKey(self):
return self.__name__
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def store(self, oid, serial, data, version, transaction):
assert not version, "Versions are not supported"
if transaction is not self._transaction:
raise ZODB.POSException.StorageTransactionError(self, transaction)
old_tid = None
tid_data = self._data.get(oid)
if tid_data:
old_tid = tid_data.maxKey()
if serial != old_tid:
raise ZODB.POSException.ConflictError(
oid=oid, serials=(old_tid, serial), data=data)
self._tdata[oid] = data
checkCurrentSerialInTransaction = (
ZODB.BaseStorage.checkCurrentSerialInTransaction)
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def tpc_abort(self, transaction):
if transaction is not self._transaction:
return
self._transaction = None
self._commit_lock.release()
# ZODB.interfaces.IStorage
def tpc_begin(self, transaction, tid=None):
with self._lock:
ZODB.utils.check_precondition(self.opened)
# The tid argument exists to support testing.
if transaction is self._transaction:
raise ZODB.POSException.StorageTransactionError(
"Duplicate tpc_begin calls for same transaction")
self._commit_lock.acquire()
with self._lock:
self._transaction = transaction
self._tdata = {}
if tid is None:
if self._transactions:
old_tid = self._transactions.maxKey()
else:
old_tid = None
tid = ZODB.utils.newTid(old_tid)
self._tid = tid
# ZODB.interfaces.IStorage
@ZODB.utils.locked(opened)
def tpc_finish(self, transaction, func = lambda tid: None):
if (transaction is not self._transaction):
raise ZODB.POSException.StorageTransactionError(
"tpc_finish called with wrong transaction")
tid = self._tid
func(tid)
tdata = self._tdata
for oid in tdata:
tid_data = self._data.get(oid)
if tid_data is None:
tid_data = BTrees.OOBTree.OOBucket()
self._data[oid] = tid_data
tid_data[tid] = tdata[oid]
self._ltid = tid
self._transactions[tid] = TransactionRecord(tid, transaction, tdata)
self._transaction = None
del self._tdata
self._commit_lock.release()
return tid
# ZEO.interfaces.IServeable
@ZODB.utils.locked(opened)
def tpc_transaction(self):
return self._transaction
# ZODB.interfaces.IStorage
def tpc_vote(self, transaction):
if transaction is not self._transaction:
raise ZODB.POSException.StorageTransactionError(
"tpc_vote called with wrong transaction")
class TransactionRecord(object):
status = ' '
def __init__(self, tid, transaction, data):
self.tid = tid
self.user = transaction.user
self.description = transaction.description
extension = transaction.extension
self.extension = extension
self.data = data
_extension = property(lambda self: self.extension,
lambda self, v: setattr(self, 'extension', v),
)
def __iter__(self):
for oid, data in self.data.items():
yield DataRecord(oid, self.tid, data)
def pack(self, oid):
self.status = 'p'
del self.data[oid]
return not self.data
@zope.interface.implementer(ZODB.interfaces.IStorageRecordInformation)
class DataRecord(object):
"""Abstract base class for iterator protocol"""
version = ''
data_txn = None
def __init__(self, oid, tid, data):
self.oid = oid
self.tid = tid
self.data = data
def DB(*args, **kw):
return ZODB.DB(MappingStorage(), *args, **kw)
@@ -0,0 +1,333 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""ZODB-defined exceptions
$Id$"""
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
import transaction.interfaces
def _fmt_undo(oid, reason):
s = reason and (": %s" % reason) or ""
return "Undo error %s%s" % (oid_repr(oid), s)
def _recon(class_, state):
err = class_.__new__(class_)
err.__setstate__(state)
return err
_recon.__no_side_effects__ = True
class POSError(Exception):
"""Persistent object system error."""
def __reduce__(self):
# Copy extra data from internal structures
state = self.__dict__.copy()
state['args'] = self.args
return (_recon, (self.__class__, state))
def __setstate__(self, state):
# PyPy doesn't store the 'args' attribute in an instance's
# __dict__; instead, it uses what amounts to a slot. Because
# we customize the pickled representation to just be a dictionary,
# the args would then get lost, leading to unprintable exceptions
# and worse. Manually assign to args from the state to be sure
# this doesn't happen.
super(POSError,self).__setstate__(state)
self.args = state['args']
class POSKeyError(POSError, KeyError):
"""Key not found in database."""
def __str__(self):
return oid_repr(self.args[0])
class ConflictError(POSError, transaction.interfaces.TransientError):
"""Two transactions tried to modify the same object at once.
This transaction should be resubmitted.
Instance attributes:
oid : string
the OID (8-byte packed string) of the object in conflict
class_name : string
the fully-qualified name of that object's class
message : string
a human-readable explanation of the error
serials : (string, string)
a pair of 8-byte packed strings; these are the serial numbers
related to conflict. The first is the revision of object that
is in conflict, the currently committed serial. The second is
the revision the current transaction read when it started.
data : string
The database record that failed to commit, used to put the
class name in the error message.
The caller should pass either object or oid as a keyword argument,
but not both of them. If object is passed, it should be a
persistent object with an _p_oid attribute.
"""
def __init__(self, message=None, object=None, oid=None, serials=None,
data=None):
if message is None:
self.message = "database conflict error"
else:
self.message = message
if object is None:
self.oid = None
self.class_name = None
else:
self.oid = object._p_oid
klass = object.__class__
self.class_name = klass.__module__ + "." + klass.__name__
if oid is not None:
assert self.oid is None
self.oid = oid
if data is not None:
# avoid circular import chain
from ZODB.utils import get_pickle_metadata
self.class_name = '.'.join(get_pickle_metadata(data))
self.serials = serials
def __str__(self):
extras = []
if self.oid:
extras.append("oid %s" % oid_repr(self.oid))
if self.class_name:
extras.append("class %s" % self.class_name)
if self.serials:
current, old = self.serials
extras.append("serial this txn started with %s" %
readable_tid_repr(old))
extras.append("serial currently committed %s" %
readable_tid_repr(current))
if extras:
return "%s (%s)" % (self.message, ", ".join(extras))
else:
return self.message
def get_oid(self):
return self.oid
def get_class_name(self):
return self.class_name
def get_old_serial(self):
return self.serials[1]
def get_new_serial(self):
return self.serials[0]
def get_serials(self):
return self.serials
class ReadConflictError(ConflictError):
"""Conflict detected when object was loaded.
An attempt was made to read an object that has changed in another
transaction (eg. another thread or process).
"""
def __init__(self, message=None, object=None, serials=None, **kw):
if message is None:
message = "database read conflict error"
ConflictError.__init__(self, message=message, object=object,
serials=serials, **kw)
class BTreesConflictError(ConflictError):
"""A special subclass for BTrees conflict errors."""
msgs = [# 0; i2 or i3 bucket split; positions are all -1
'Conflicting bucket split',
# 1; keys the same, but i2 and i3 values differ, and both values
# differ from i1's value
'Conflicting changes',
# 2; i1's value changed in i2, but key+value deleted in i3
'Conflicting delete and change',
# 3; i1's value changed in i3, but key+value deleted in i2
'Conflicting delete and change',
# 4; i1 and i2 both added the same key, or both deleted the
# same key
'Conflicting inserts or deletes',
# 5; i2 and i3 both deleted the same key
'Conflicting deletes',
# 6; i2 and i3 both added the same key
'Conflicting inserts',
# 7; i2 and i3 both deleted the same key, or i2 changed the value
# associated with a key and i3 deleted that key
'Conflicting deletes, or delete and change',
# 8; i2 and i3 both deleted the same key, or i3 changed the value
# associated with a key and i2 deleted that key
'Conflicting deletes, or delete and change',
# 9; i2 and i3 both deleted the same key
'Conflicting deletes',
# 10; i2 and i3 deleted all the keys, and didn't insert any,
# leaving an empty bucket; conflict resolution doesn't have
# enough info to unlink an empty bucket from its containing
# BTree correctly
'Empty bucket from deleting all keys',
# 11; conflicting changes in an internal BTree node
'Conflicting changes in an internal BTree node',
# 12; i2 or i3 was empty
'Empty bucket in a transaction',
# 13; delete of first key, which causes change to parent node
'Delete of first key',
]
def __init__(self, p1, p2, p3, reason):
self.p1 = p1
self.p2 = p2
self.p3 = p3
self.reason = reason
def __repr__(self):
return "BTreesConflictError(%d, %d, %d, %d)" % (self.p1,
self.p2,
self.p3,
self.reason)
def __str__(self):
return "BTrees conflict error at %d/%d/%d: %s" % (
self.p1, self.p2, self.p3, self.msgs[self.reason])
class DanglingReferenceError(POSError, transaction.interfaces.TransactionError):
"""An object has a persistent reference to a missing object.
If an object is stored and it has a reference to another object
that does not exist (for example, it was deleted by pack), this
exception may be raised. Whether a storage supports this feature,
it a quality of implementation issue.
Instance attributes:
referer: oid of the object being written
missing: referenced oid that does not have a corresponding object
"""
def __init__(self, Aoid, Boid):
self.referer = Aoid
self.missing = Boid
def __str__(self):
return "from %s to %s" % (oid_repr(self.referer),
oid_repr(self.missing))
############################################################################
# Only used in storages; versions are no longer supported.
class VersionError(POSError):
"""An error in handling versions occurred."""
class VersionCommitError(VersionError):
"""An invalid combination of versions was used in a version commit."""
class VersionLockError(VersionError, transaction.interfaces.TransactionError):
"""Modification to an object modified in an unsaved version.
An attempt was made to modify an object that has been modified in an
unsaved version.
"""
############################################################################
class UndoError(POSError):
"""An attempt was made to undo a non-undoable transaction."""
def __init__(self, reason, oid=None):
self._reason = reason
self._oid = oid
def __str__(self):
return _fmt_undo(self._oid, self._reason)
class MultipleUndoErrors(UndoError):
"""Several undo errors occurred during a single transaction."""
def __init__(self, errs):
# provide a reason and oid for clients that only look at that
UndoError.__init__(self, *errs[0])
self._errs = errs
def __str__(self):
return "\n".join([_fmt_undo(*pair) for pair in self._errs])
class StorageError(POSError):
"""Base class for storage based exceptions."""
class StorageTransactionError(StorageError):
"""An operation was invoked for an invalid transaction or state."""
class StorageSystemError(StorageError):
"""Panic! Internal storage error!"""
class MountedStorageError(StorageError):
"""Unable to access mounted storage."""
class ReadOnlyError(StorageError):
"""Unable to modify objects in a read-only storage."""
class TransactionTooLargeError(StorageTransactionError):
"""The transaction exhausted some finite storage resource."""
class ExportError(POSError):
"""An export file doesn't have the right format."""
class Unsupported(POSError):
"""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):
"""An object contains an invalid reference to another object.
An invalid reference may be one of:
o A reference to a wrapped persistent object.
o A reference to an object in a different database connection.
TODO: The exception ought to have a member that is the invalid object.
"""
class ConnectionStateError(POSError):
"""A Connection isn't in the required state for an operation.
o An operation such as a load is attempted on a closed connection.
o An attempt to close a connection is made while the connection is
still joined to a transaction (for example, a transaction is in
progress, with uncommitted modifications in the connection).
"""
@@ -0,0 +1,37 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""Provide backward compatibility with storages that only have undoLog()."""
class UndoLogCompatible(object):
def undoInfo(self, first=0, last=-20, specification=None):
if specification:
# filter(desc) returns true iff `desc` is a "superdict"
# of `specification`, meaning that `desc` contains the same
# (key, value) pairs as `specification`, and possibly additional
# (key, value) pairs. Another way to do this might be
# d = desc.copy()
# d.update(specification)
# return d == desc
def filter(desc, spec=specification.items()):
get = desc.get
for k, v in spec:
if get(k, None) != v:
return 0
return 1
else:
filter = None
return self.undoLog(first, last, filter)
@@ -0,0 +1,28 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
import sys
from persistent import TimeStamp
from persistent import list
from persistent import mapping
# Backward compat for old imports.
sys.modules['ZODB.TimeStamp'] = sys.modules['persistent.TimeStamp']
sys.modules['ZODB.PersistentMapping'] = sys.modules['persistent.mapping']
sys.modules['ZODB.PersistentList'] = sys.modules['persistent.list']
del mapping, list, sys
from ZODB.DB import DB, connection
@@ -0,0 +1,160 @@
##############################################################################
#
# Copyright (c) 2013 Zope Foundation 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
#
##############################################################################
import sys
from six import PY3
IS_JYTHON = sys.platform.startswith('java')
_protocol = 3
from zodbpickle import binary
if not PY3:
# Python 2.x
# PyPy's cPickle doesn't have noload, and noload is broken in Python 2.7,
# so we need zodbpickle.
# Get the fastest working version we can (PyPy has no fastpickle)
try:
import zodbpickle.fastpickle as cPickle
except ImportError:
import zodbpickle.pickle as cPickle
Pickler = cPickle.Pickler
Unpickler = cPickle.Unpickler
dump = cPickle.dump
dumps = cPickle.dumps
loads = cPickle.loads
HIGHEST_PROTOCOL = cPickle.HIGHEST_PROTOCOL
IMPORT_MAPPING = {}
NAME_MAPPING = {}
FILESTORAGE_MAGIC = b"FS21"
else:
# Python 3.x: can't use stdlib's pickle because
# http://bugs.python.org/issue6784
import zodbpickle.pickle
HIGHEST_PROTOCOL = 3
from _compat_pickle import IMPORT_MAPPING, NAME_MAPPING
class Pickler(zodbpickle.pickle.Pickler):
def __init__(self, f, protocol=None):
super(Pickler, self).__init__(f, protocol)
class Unpickler(zodbpickle.pickle.Unpickler):
def __init__(self, f):
super(Unpickler, self).__init__(f)
# Py3: Python 3 doesn't allow assignments to find_global,
# instead, find_class can be overridden
find_global = None
def find_class(self, modulename, name):
if self.find_global is None:
return super(Unpickler, self).find_class(modulename, name)
return self.find_global(modulename, name)
def dump(o, f, protocol=None):
return zodbpickle.pickle.dump(o, f, protocol)
def dumps(o, protocol=None):
return zodbpickle.pickle.dumps(o, protocol)
def loads(s):
return zodbpickle.pickle.loads(s, encoding='ASCII', errors='bytes')
FILESTORAGE_MAGIC = b"FS30"
def PersistentPickler(persistent_id, *args, **kwargs):
"""
Returns a :class:`Pickler` that will use the given ``persistent_id``
to get persistent IDs. The remainder of the arguments are passed to the
Pickler itself.
This covers the differences between Python 2 and 3 and PyPy/zodbpickle.
"""
p = Pickler(*args, **kwargs)
if not PY3:
p.inst_persistent_id = persistent_id
# PyPy uses a python implementation of cPickle/zodbpickle in both Python 2
# and Python 3. We can't really detect inst_persistent_id as its
# a magic attribute that's not readable, but it doesn't hurt to
# simply always assign to persistent_id also
p.persistent_id = persistent_id
return p
def PersistentUnpickler(find_global, load_persistent, *args, **kwargs):
"""
Returns a :class:`Unpickler` that will use the given `find_global` function
to locate classes, and the given `load_persistent` function to load
objects from a persistent id.
This covers the differences between Python 2 and 3 and PyPy/zodbpickle.
"""
unpickler = Unpickler(*args, **kwargs)
if find_global is not None:
unpickler.find_global = find_global
try:
unpickler.find_class = find_global # PyPy, zodbpickle, the non-c-accelerated version
except AttributeError:
pass
if load_persistent is not None:
unpickler.persistent_load = load_persistent
return unpickler
try:
# XXX: why not just import BytesIO from io?
from cStringIO import StringIO as BytesIO
except ImportError:
# Python 3.x
from io import BytesIO
try:
# Python 3.x
from base64 import decodebytes, encodebytes
except ImportError:
# Python 2.x
from base64 import decodestring as decodebytes, encodestring as encodebytes
# Python 3.x: ``hasattr()`` swallows only AttributeError.
def py2_hasattr(obj, name):
try:
getattr(obj, name)
except:
return False
return True
try:
# Py2: simply reexport the builtin
long = long
except NameError:
# Py3
long = int
INT_TYPES = (int,)
else:
INT_TYPES = (int, long)
try:
TEXT = unicode
except NameError: #pragma NO COVER Py3k
TEXT = str
def ascii_bytes(x):
if isinstance(x, TEXT):
x = x.encode('ascii')
return x
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,360 @@
##############################################################################
#
# Copyright (c) 2004 Zope Foundation 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.
#
##############################################################################
"""Broken object support
"""
import sys
import persistent
import zope.interface
import ZODB.interfaces
from ZODB._compat import IMPORT_MAPPING
from ZODB._compat import NAME_MAPPING
broken_cache = {}
@zope.interface.implementer(ZODB.interfaces.IBroken)
class Broken(object):
"""Broken object base class
Broken objects are placeholders for objects that can no longer be
created because their class has gone away.
Broken objects don't really do much of anything, except hold their
state. The Broken class is used as a base class for creating
classes in lieu of missing classes::
>>> Atall = type('Atall', (Broken, ), {'__module__': 'not.there'})
The only thing the class can be used for is to create new objects::
>>> Atall()
<broken not.there.Atall instance>
>>> Atall().__Broken_newargs__
()
>>> Atall().__Broken_initargs__
()
>>> Atall(1, 2).__Broken_newargs__
(1, 2)
>>> Atall(1, 2).__Broken_initargs__
(1, 2)
>>> a = Atall.__new__(Atall, 1, 2)
>>> a
<broken not.there.Atall instance>
>>> a.__Broken_newargs__
(1, 2)
>>> a.__Broken_initargs__
You can't modify broken objects::
>>> a.x = 1
Traceback (most recent call last):
...
BrokenModified: Can't change broken objects
But you can set their state::
>>> a.__setstate__({'x': 1, })
You can pickle broken objects::
>>> r = a.__reduce__()
>>> len(r)
3
>>> r[0] is rebuild
True
>>> r[1]
('not.there', 'Atall', 1, 2)
>>> r[2]
{'x': 1}
>>> from ZODB._compat import dumps
>>> from ZODB._compat import loads
>>> from ZODB._compat import _protocol
>>> a2 = loads(dumps(a, _protocol))
>>> a2
<broken not.there.Atall instance>
>>> a2.__Broken_newargs__
(1, 2)
>>> a2.__Broken_initargs__
>>> a2.__Broken_state__
{'x': 1}
Cleanup::
>>> broken_cache.clear()
"""
__Broken_state__ = __Broken_initargs__ = None
__name__ = 'broken object'
def __new__(class_, *args):
result = object.__new__(class_)
result.__dict__['__Broken_newargs__'] = args
return result
def __init__(self, *args):
self.__dict__['__Broken_initargs__'] = args
def __reduce__(self):
"""We pickle broken objects in hope of being able to fix them later
"""
return (rebuild,
((self.__class__.__module__, self.__class__.__name__)
+ self.__Broken_newargs__),
self.__Broken_state__,
)
def __setstate__(self, state):
self.__dict__['__Broken_state__'] = state
def __repr__(self):
return "<broken %s.%s instance>" % (
self.__class__.__module__, self.__class__.__name__)
def __setattr__(self, name, value):
raise BrokenModified("Can't change broken objects")
def find_global(modulename, globalname,
# These are *not* optimizations. Callers can override these.
Broken=Broken, type=type,
):
"""Find a global object, returning a broken class if it can't be found.
This function looks up global variable in modules::
>>> import sys
>>> find_global('sys', 'path') is sys.path
True
>>> find_global('__builtin__', 'object') is object
True
If an object can't be found, a broken class is returned::
>>> broken = find_global('ZODB.not.there', 'atall')
>>> issubclass(broken, Broken)
True
>>> broken.__module__
'ZODB.not.there'
>>> broken.__name__
'atall'
Broken classes are cached::
>>> find_global('ZODB.not.there', 'atall') is broken
True
If we "repair" a missing global::
>>> class ZODBnotthere(object):
... atall = []
>>> sys.modules['ZODB.not'] = ZODBnotthere
>>> sys.modules['ZODB.not.there'] = ZODBnotthere
we can then get the repaired value::
>>> find_global('ZODB.not.there', 'atall') is ZODBnotthere.atall
True
Of course, if we break it again::
>>> del sys.modules['ZODB.not']
>>> del sys.modules['ZODB.not.there']
we get the broken value::
>>> find_global('ZODB.not.there', 'atall') is broken
True
Cleanup::
>>> broken_cache.clear()
"""
if (modulename, globalname) in NAME_MAPPING:
modulename, globalname = NAME_MAPPING[(modulename, globalname)]
if modulename in IMPORT_MAPPING:
modulename = IMPORT_MAPPING[modulename]
# short circuit common case:
try:
return getattr(sys.modules[modulename], globalname)
except (AttributeError, KeyError):
pass
try:
__import__(modulename)
except ImportError:
pass
else:
module = sys.modules[modulename]
try:
return getattr(module, globalname)
except AttributeError:
pass
try:
return broken_cache[(modulename, globalname)]
except KeyError:
pass
class_ = type(globalname, (Broken, ), {'__module__': modulename})
broken_cache[(modulename, globalname)] = class_
return class_
def rebuild(modulename, globalname, *args):
"""Recreate a broken object, possibly recreating the missing class
This functions unpickles broken objects::
>>> broken = rebuild('ZODB.notthere', 'atall', 1, 2)
>>> broken
<broken ZODB.notthere.atall instance>
>>> broken.__Broken_newargs__
(1, 2)
If we "repair" the brokenness::
>>> class notthere(object): # fake notthere module
... class atall(object):
... def __new__(self, *args):
... ob = object.__new__(self)
... ob.args = args
... return ob
... def __repr__(self):
... return 'atall %s %s' % self.args
>>> sys.modules['ZODB.notthere'] = notthere
>>> rebuild('ZODB.notthere', 'atall', 1, 2)
atall 1 2
>>> del sys.modules['ZODB.notthere']
Cleanup::
>>> broken_cache.clear()
"""
class_ = find_global(modulename, globalname)
return class_.__new__(class_, *args)
class BrokenModified(TypeError):
"""Attempt to modify a broken object
"""
class PersistentBroken(Broken, persistent.Persistent):
r"""Persistent broken objects
Persistent broken objects are used for broken objects that are
also persistent. In addition to having to track the original
object data, they need to handle persistent meta data.
Persistent broken classes are created from existing broken classes
using the persistentBroken, function::
>>> Atall = type('Atall', (Broken, ), {'__module__': 'not.there'})
>>> PAtall = persistentBroken(Atall)
(Note that we always get the *same* persistent broken class
for a given broken class::
>>> persistentBroken(Atall) is PAtall
True
)
Persistent broken classes work a lot like broken classes::
>>> a = PAtall.__new__(PAtall, 1, 2)
>>> a
<persistent broken not.there.Atall instance None>
>>> a.__Broken_newargs__
(1, 2)
>>> a.__Broken_initargs__
>>> a.x = 1
Traceback (most recent call last):
...
BrokenModified: Can't change broken objects
Unlike regular broken objects, persistent broken objects keep
track of persistence meta data:
>>> a._p_oid = '\0\0\0\0****'
>>> a
<persistent broken not.there.Atall instance '\x00\x00\x00\x00****'>
and persistent broken objects aren't directly picklable:
>>> a.__reduce__() # doctest: +NORMALIZE_WHITESPACE
Traceback (most recent call last):
...
BrokenModified:
<persistent broken not.there.Atall instance '\x00\x00\x00\x00****'>
but you can get their state:
>>> a.__setstate__({'y': 2})
>>> a.__getstate__()
{'y': 2}
Cleanup::
>>> broken_cache.clear()
"""
def __new__(class_, *args):
result = persistent.Persistent.__new__(class_)
result.__dict__['__Broken_newargs__'] = args
return result
def __reduce__(self, *args):
raise BrokenModified(self)
def __getstate__(self):
return self.__Broken_state__
def __setattr__(self, name, value):
if name.startswith('_p_'):
persistent.Persistent.__setattr__(self, name, value)
else:
raise BrokenModified("Can't change broken objects")
def __repr__(self):
return "<persistent broken %s.%s instance %r>" % (
self.__class__.__module__, self.__class__.__name__,
self._p_oid)
def __getnewargs__(self):
return self.__Broken_newargs__
def persistentBroken(class_):
try:
return class_.__dict__['__Broken_Persistent__']
except KeyError:
class_.__Broken_Persistent__ = (
type(class_.__name__,
(PersistentBroken, class_),
{'__module__': class_.__module__},
)
)
return class_.__dict__['__Broken_Persistent__']
@@ -0,0 +1,199 @@
=======================
Collabortation Diagrams
=======================
This file contains several collaboration diagrams for the ZODB.
Simple fetch, modify, commit
============================
Participants
------------
- ``DB``: ``ZODB.DB.DB``
- ``C``: ``ZODB.Connection.Connection``
- ``S``: ``ZODB.FileStorage.FileStorage``
- ``T``: ``transaction.interfaces.ITransaction``
- ``TM``: ``transaction.interfaces.ITransactionManager``
- ``o1``, ``o2``, ...: pre-existing persistent objects
Scenario
--------
::
DB.open()
create C
TM.registerSynch(C)
TM.begin()
create T
C.get(1) # fetches o1
C.get(2) # fetches o2
C.get(3) # fetches o3
o1.modify() # anything that modifies o1
C.register(o1)
T.join(C)
o2.modify()
C.register(o2)
# T.join(C) does not happen again
o1.modify()
# C.register(o1) doesn't happen again, because o1 was already
# in the changed state.
T.commit()
C.beforeCompletion(T)
C.tpc_begin(T)
S.tpc_begin(T)
C.commit(T)
S.store(1, ..., T)
S.store(2, ..., T)
# o3 is not stored, because it wasn't modified
C.tpc_vote(T)
S.tpc_vote(T)
C.tpc_finish(T)
S.tpc_finish(T, f) # f is a callback function, which arranges
# to call DB.invalidate (next)
DB.invalidate(tid, {1: 1, 2: 1}, C)
C2.invalidate(tid, {1: 1, 2: 1}) # for all connections
# C2 to DB, where C2
# is not C
TM.free(T)
C.afterCompletion(T)
C._flush_invalidations()
# Processes invalidations that may have come in from other
# transactions.
Simple fetch, modify, abort
===========================
Participants
------------
- ``DB``: ``ZODB.DB.DB``
- ``C``: ``ZODB.Connection.Connection``
- ``S``: ``ZODB.FileStorage.FileStorage``
- ``T``: ``transaction.interfaces.ITransaction``
- ``TM``: ``transaction.interfaces.ITransactionManager``
- ``o1``, ``o2``, ...: pre-existing persistent objects
Scenario
--------
::
DB.open()
create C
TM.registerSynch(C)
TM.begin()
create T
C.get(1) # fetches o1
C.get(2) # fetches o2
C.get(3) # fetches o3
o1.modify() # anything that modifies o1
C.register(o1)
T.join(C)
o2.modify()
C.register(o2)
# T.join(C) does not happen again
o1.modify()
# C.register(o1) doesn't happen again, because o1 was already
# in the changed state.
T.abort()
C.beforeCompletion(T)
C.abort(T)
C._cache.invalidate(1) # toss changes to o1
C._cache.invalidate(2) # toss changes to o2
# o3 wasn't modified, and its cache entry isn't invalidated.
TM.free(T)
C.afterCompletion(T)
C._flush_invalidations()
# Processes invalidations that may have come in from other
# transactions.
Rollback of a savepoint
=======================
Participants
------------
- ``T``: ``transaction.interfaces.ITransaction``
- ``o1``, ``o2``, ``o3``: some persistent objects
- ``C1``, ``C2``, ``C3``: resource managers
- ``S1``, ``S2``: Transaction savepoint objects
- ``s11``, ``s21``, ``s22``: resource-manager savepoints
Scenario
--------
::
create T
o1.modify()
C1.regisiter(o1)
T.join(C1)
T.savepoint()
C1.savepoint()
return s11
return S1 = Savepoint(T, [r11])
o1.modify()
C1.regisiter(o1)
o2.modify()
C2.regisiter(o2)
T.join(C2)
T.savepoint()
C1.savepoint()
return s21
C2.savepoint()
return s22
return S2 = Savepoint(T, [r21, r22])
o3.modify()
C3.regisiter(o3)
T.join(C3)
S1.rollback()
S2.rollback()
T.discard()
C1.discard()
C2.discard()
C3.discard()
o3.invalidate()
S2.discard()
s21.discard() # roll back changes since previous, which is r11
C1.discard(s21)
o1.invalidate()
# truncates temporary storage to s21's position
s22.discard() # roll back changes since previous, which is r11
C1.discard(s22)
o2.invalidate()
# truncates temporary storage to beginning, because
# s22 was the first savepoint. (Perhaps conection
# savepoints record the log position before the
# data were written, which is 0 in this case.
T.commit()
C1.beforeCompletion(T)
C2.beforeCompletion(T)
C3.beforeCompletion(T)
C1.tpc_begin(T)
S1.tpc_begin(T)
C2.tpc_begin(T)
C3.tpc_begin(T)
C1.commit(T)
S1.store(1, ..., T)
C2.commit(T)
C3.commit(T)
C1.tpc_vote(T)
S1.tpc_vote(T)
C2.tpc_vote(T)
C3.tpc_vote(T)
C1.tpc_finish(T)
S1.tpc_finish(T, f) # f is a callback function, which arranges
c# to call DB.invalidate (next)
DB.invalidate(tid, {1: 1}, C)
TM.free(T)
C1.afterCompletion(T)
C1._flush_invalidations()
C2.afterCompletion(T)
C2._flush_invalidations()
C3.afterCompletion(T)
C3._flush_invalidations()
@@ -0,0 +1,342 @@
<component prefix="ZODB.config">
<!-- TODO needs descriptions for everything -->
<abstracttype name="ZODB.storage"/>
<abstracttype name="ZODB.database"/>
<sectiontype name="filestorage" datatype=".FileStorage"
implements="ZODB.storage">
<key name="path" required="yes" datatype="existing-dirpath">
<description>
Path name to the main storage file. The names for
supplemental files, including index and lock files, will be
computed from this.
</description>
</key>
<key name="blob-dir" required="no" datatype="existing-dirpath">
<description>
If supplied, the file storage will provide blob support and this
is the name of a directory to hold blob data. The directory will
be created if it doesn't exist. If no value (or an empty value)
is provided, then no blob support will be provided. (You can still
use a BlobStorage to provide blob support.)
</description>
</key>
<key name="create" datatype="boolean">
<description>
Flag that indicates whether the storage should be truncated if
it already exists.
</description>
</key>
<key name="read-only" datatype="boolean">
<description>
If true, only reads may be executed against the storage. Note
that the "pack" operation is not considered a write operation
and is still allowed on a read-only filestorage.
</description>
</key>
<key name="quota" datatype="byte-size">
<description>
Maximum allowed size of the storage file. Operations which
would cause the size of the storage to exceed the quota will
result in a ZODB.FileStorage.FileStorageQuotaError being
raised.
</description>
</key>
<key name="packer" datatype="string">
<description>
The dotted name (dotted module name and object name) of a
packer object. This is used to provide an alternative pack
implementation.
</description>
</key>
<key name="pack-gc" datatype="boolean" default="true">
<description>
If false, then no garbage collection will be performed when
packing. This can make packing go much faster and can avoid
problems when objects are referenced only from other
databases.
</description>
</key>
<key name="pack-keep-old" datatype="boolean" default="true">
<description>
If true, a copy of the database before packing is kept in a
".old" file.
</description>
</key>
</sectiontype>
<sectiontype name="mappingstorage" datatype=".MappingStorage"
implements="ZODB.storage">
<key name="name" default="Mapping Storage">
<description>
The storage name, used by the
:meth:`~ZODB.interfaces.IStorage.getName` and
:meth:`~ZODB.interfaces.IStorage.sortKey` methods.
</description>
</key>
</sectiontype>
<!-- The BDB storages probably need to be revised somewhat still.
The extension relationship seems a little odd.
-->
<sectiontype name="fullstorage" datatype=".BDBFullStorage"
implements="ZODB.storage">
<key name="envdir" required="yes" />
<key name="interval" datatype="time-interval" default="2m" />
<key name="kbyte" datatype="integer" default="0" />
<key name="min" datatype="integer" default="0" />
<key name="logdir" />
<key name="cachesize" datatype="byte-size" default="128MB" />
<key name="frequency" datatype="time-interval" default="0" />
<key name="packtime" datatype="time-interval" default="4h" />
<key name="gcpack" datatype="integer" default="0" />
<key name="read-only" datatype="boolean" default="off"/>
</sectiontype>
<sectiontype name="minimalstorage" datatype=".BDBMinimalStorage"
implements="ZODB.storage" extends="fullstorage"/>
<sectiontype name="zeoclient" datatype=".ZEOClient"
implements="ZODB.storage">
<multikey name="server" datatype="socket-connection-address" required="yes"/>
<key name="blob-dir" required="no">
<description>
Path name to the blob cache directory.
</description>
</key>
<key name="shared-blob-dir" required="no" default="no"
datatype="boolean">
<description>
Tells whether the cache is a shared writable directory
and that the ZEO protocol should not transfer the file
but only the filename when committing.
</description>
</key>
<key name="blob-cache-size" required="no" datatype="byte-size">
<description>
Maximum size of the ZEO blob cache, in bytes. If not set, then
the cache size isn't checked and the blob directory will
grow without bound.
This option is ignored if shared_blob_dir is true.
</description>
</key>
<key name="blob-cache-size-check" required="no" datatype="integer">
<description>
ZEO check size as percent of blob_cache_size. The ZEO
cache size will be checked when this many bytes have been
loaded into the cache. Defaults to 10% of the blob cache
size. This option is ignored if shared_blob_dir is true.
</description>
</key>
<key name="storage" default="1">
<description>
The name of the storage that the client wants to use. If the
ZEO server serves more than one storage, the client selects
the storage it wants to use by name. The default name is '1',
which is also the default name for the ZEO server.
</description>
</key>
<key name="cache-size" datatype="byte-size" default="20MB">
<description>
The maximum size of the client cache, in bytes, KB or MB.
</description>
</key>
<key name="name" default="">
<description>
The storage name. If unspecified, the address of the server
will be used as the name.
</description>
</key>
<key name="client">
<description>
Enables persistent cache files. The string passed here is
used to construct the cache filenames. If it is not
specified, the client creates a temporary cache that will
only be used by the current object.
</description>
</key>
<key name="var">
<description>
The directory where persistent cache files are stored. By
default cache files, if they are persistent, are stored in
the current directory.
</description>
</key>
<key name="min-disconnect-poll" datatype="integer" default="5">
<description>
The minimum delay in seconds between attempts to connect to
the server, in seconds. Defaults to 5 seconds.
</description>
</key>
<key name="max-disconnect-poll" datatype="integer" default="300">
<description>
The maximum delay in seconds between attempts to connect to
the server, in seconds. Defaults to 300 seconds.
</description>
</key>
<key name="wait" datatype="boolean" default="on">
<description>
A boolean indicating whether the constructor should wait
for the client to connect to the server and verify the cache
before returning. The default is true.
</description>
</key>
<key name="read-only" datatype="boolean" default="off">
<description>
A flag indicating whether this should be a read-only storage,
defaulting to false (i.e. writing is allowed by default).
</description>
</key>
<key name="read-only-fallback" datatype="boolean" default="off">
<description>
A flag indicating whether a read-only remote storage should be
acceptable as a fall-back when no writable storages are
available. Defaults to false. At most one of read_only and
read_only_fallback should be true.
</description>
</key>
<key name="username" required="no">
<description>
The authentication user name of the server.
</description>
</key>
<key name="password" required="no">
<description>
The authentication password of the server.
</description>
</key>
<key name="realm" required="no">
<description>
The authentication realm of the server. Some authentication
schemes use a realm to identify the logic set of user names
that are accepted by this server.
</description>
</key>
<!-- DM 2008-05-15: added -->
<key name="drop-cache-rather-verify" datatype="boolean" default="off">
<description>
A flag indicating whether the client cache should be dropped
instead of an expensive verification.
</description>
</key>
<key name="client-label" required="no">
<description>
A label for the client in server logs
</description>
</key>
</sectiontype>
<sectiontype name="demostorage" datatype=".DemoStorage"
implements="ZODB.storage">
<key name="name">
<description>
The storage name, used by the
:meth:`~ZODB.interfaces.IStorage.getName` and
:meth:`~ZODB.interfaces.IStorage.sortKey` methods.
</description>
</key>
<multisection type="ZODB.storage" name="*" attribute="factories" />
</sectiontype>
<sectiontype name="zodb" datatype=".ZODBDatabase"
implements="ZODB.database">
<section type="ZODB.storage" name="*" attribute="storage"/>
<key name="cache-size" datatype="integer" default="5000">
<description>
Target size, in number of objects, of each connection's
object cache.
</description>
</key>
<key name="cache-size-bytes" datatype="byte-size" default="0">
<description>
Target size, in total estimated size for objects, of each connection's
object cache.
"0" means no limit.
</description>
</key>
<key name="large-record-size" datatype="byte-size" default="16MB">
<description>
When object records are saved
that are larger than this, a warning is issued,
suggesting that blobs should be used instead.
</description>
</key>
<key name="pool-size" datatype="integer" default="7">
<description>
The expected maximum number of simultaneously open connections.
There is no hard limit (as many connections as are requested
will be opened, until system resources are exhausted). Exceeding
pool-size connections causes a warning message to be logged,
and exceeding twice pool-size connections causes a critical
message to be logged.
</description>
</key>
<key name="pool-timeout" datatype="time-interval">
<description>
The minimum interval that an unused (non-historical)
connection should be kept.
</description>
</key>
<key name="historical-pool-size" datatype="integer" default="3">
<description>
The expected maximum total number of historical connections
simultaneously open.
</description>
</key>
<key name="historical-cache-size" datatype="integer" default="1000">
<description>
Target size, in number of objects, of each historical connection's
object cache.
</description>
</key>
<key name="historical-cache-size-bytes" datatype="byte-size" default="0">
<description>
Target size, in total estimated size of objects, of each historical connection's
object cache.
</description>
</key>
<key name="historical-timeout" datatype="time-interval"
default="5m">
<description>
The minimum interval that an unused historical connection should be
kept.
</description>
</key>
<key name="database-name">
<description>
When multi-databases are in use, this is the name given to this
database in the collection. The name must be unique across all
databases in the collection. The collection must also be given
a mapping from its databases' names to their databases, but that
cannot be specified in a ZODB config file. Applications using
multi-databases typical supply a way to configure the mapping in
their own config files, using the "databases" parameter of a DB
constructor.
</description>
</key>
<key name="allow-implicit-cross-references" datatype="boolean">
<description>
If set to false, implicit cross references (the only kind
currently possible) are disallowed.
</description>
</key>
</sectiontype>
<sectiontype name="blobstorage" datatype=".BlobStorage"
implements="ZODB.storage">
<key name="blob-dir" required="yes">
<description>
Path name to the blob storage directory.
</description>
</key>
<section type="ZODB.storage" name="*" attribute="base"/>
</sectiontype>
</component>
@@ -0,0 +1,274 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""Open database and storage from a configuration."""
import os
import ZConfig
import ZODB
try:
from cStringIO import StringIO
except ImportError:
# Py3
from io import StringIO
db_schema_path = os.path.join(ZODB.__path__[0], "config.xml")
_db_schema = None
s_schema_path = os.path.join(ZODB.__path__[0], "storage.xml")
_s_schema = None
def getDbSchema():
global _db_schema
if _db_schema is None:
_db_schema = ZConfig.loadSchema(db_schema_path)
return _db_schema
def getStorageSchema():
global _s_schema
if _s_schema is None:
_s_schema = ZConfig.loadSchema(s_schema_path)
return _s_schema
def databaseFromString(s):
"""Create a database from a database-configuration string.
The string must contain one or more :ref:`zodb
<database-text-configuration>` sections.
The database defined by the first section is returned.
If :ref:`more than one zodb section is provided
<multidatabase-text-configuration>`, a multi-database
configuration will be created and all of the databases will be
available in the returned database's ``databases`` attribute.
"""
return databaseFromFile(StringIO(s))
def databaseFromFile(f):
"""Create a database from a file object that provides configuration.
See :func:`databaseFromString`.
"""
config, handle = ZConfig.loadConfigFile(getDbSchema(), f)
return databaseFromConfig(config.database)
def databaseFromURL(url):
"""Load a database from URL (or file name) that provides configuration.
See :func:`databaseFromString`.
"""
config, handler = ZConfig.loadConfig(getDbSchema(), url)
return databaseFromConfig(config.database)
def databaseFromConfig(database_factories):
databases = {}
first = None
for factory in database_factories:
db = factory.open(databases)
if first is None:
first = db
return first
def storageFromString(s):
"""Create a storage from a storage-configuration string.
"""
return storageFromFile(StringIO(s))
def storageFromFile(f):
"""Create a storage from a file object providing storage-configuration.
"""
config, handle = ZConfig.loadConfigFile(getStorageSchema(), f)
return storageFromConfig(config.storage)
def storageFromURL(url):
"""\
Create a storage from a URL (or file name) providing storage-configuration.
"""
config, handler = ZConfig.loadConfig(getStorageSchema(), url)
return storageFromConfig(config.storage)
def storageFromConfig(section):
return section.open()
class BaseConfig(object):
"""Object representing a configured storage or database.
Methods:
open() -- open and return the configured object
Attributes:
name -- name of the storage
"""
def __init__(self, config):
self.config = config
self.name = config.getSectionName()
def open(self, database_name='unnamed', databases=None):
"""Open and return the storage object."""
raise NotImplementedError
class ZODBDatabase(BaseConfig):
def open(self, databases=None):
section = self.config
storage = section.storage.open()
options = {}
def _option(name, oname=None):
v = getattr(section, name)
if v is not None:
if oname is None:
oname = name
options[oname] = v
_option('pool_timeout')
_option('allow_implicit_cross_references', 'xrefs')
_option('large_record_size')
try:
return ZODB.DB(
storage,
pool_size=section.pool_size,
cache_size=section.cache_size,
cache_size_bytes=section.cache_size_bytes,
historical_pool_size=section.historical_pool_size,
historical_cache_size=section.historical_cache_size,
historical_cache_size_bytes=section.historical_cache_size_bytes,
historical_timeout=section.historical_timeout,
database_name=section.database_name or self.name or '',
databases=databases,
**options)
except:
storage.close()
raise
class MappingStorage(BaseConfig):
def open(self):
from ZODB.MappingStorage import MappingStorage
return MappingStorage(self.config.name)
class DemoStorage(BaseConfig):
def open(self):
base = changes = None
for factory in self.config.factories:
if factory.name == 'changes':
changes = factory.open()
else:
if base is None:
base = factory.open()
else:
raise ValueError("Too many base storages defined!")
from ZODB.DemoStorage import DemoStorage
return DemoStorage(self.config.name, base=base, changes=changes)
class FileStorage(BaseConfig):
def open(self):
from ZODB.FileStorage import FileStorage
config = self.config
options = {}
if getattr(config, 'packer', None):
packer = config.packer
if ':' in packer:
m, expr = packer.split(':', 1)
m = __import__(m, {}, {}, ['*'])
options['packer'] = eval(expr, m.__dict__)
else:
m, name = config.packer.rsplit('.', 1)
m = __import__(m, {}, {}, ['*'])
options['packer'] = getattr(m, name)
for name in ('blob_dir', 'create', 'read_only', 'quota', 'pack_gc',
'pack_keep_old'):
v = getattr(config, name, self)
if v is not self:
options[name] = v
return FileStorage(config.path, **options)
class BlobStorage(BaseConfig):
def open(self):
from ZODB.blob import BlobStorage
base = self.config.base.open()
return BlobStorage(self.config.blob_dir, base)
class ZEOClient(BaseConfig):
def open(self):
from ZEO.ClientStorage import ClientStorage
# config.server is a multikey of socket-connection-address values
# where the value is a socket family, address tuple.
L = [server.address for server in self.config.server]
options = {}
if self.config.blob_cache_size is not None:
options['blob_cache_size'] = self.config.blob_cache_size
if self.config.blob_cache_size_check is not None:
options['blob_cache_size_check'] = self.config.blob_cache_size_check
if self.config.client_label is not None:
options['client_label'] = self.config.client_label
return ClientStorage(
L,
blob_dir=self.config.blob_dir,
shared_blob_dir=self.config.shared_blob_dir,
storage=self.config.storage,
cache_size=self.config.cache_size,
name=self.config.name,
client=self.config.client,
var=self.config.var,
min_disconnect_poll=self.config.min_disconnect_poll,
max_disconnect_poll=self.config.max_disconnect_poll,
wait=self.config.wait,
read_only=self.config.read_only,
read_only_fallback=self.config.read_only_fallback,
drop_cache_rather_verify=self.config.drop_cache_rather_verify,
username=self.config.username,
password=self.config.password,
realm=self.config.realm,
**options)
class BDBStorage(BaseConfig):
def open(self):
from BDBStorage.BerkeleyBase import BerkeleyConfig
storageclass = self.get_storageclass()
bconf = BerkeleyConfig()
for name in dir(BerkeleyConfig):
if name.startswith('_'):
continue
setattr(bconf, name, getattr(self.config, name))
return storageclass(self.config.envdir, config=bconf)
class BDBMinimalStorage(BDBStorage):
def get_storageclass(self):
import BDBStorage.BDBMinimalStorage
return BDBStorage.BDBMinimalStorage.BDBMinimalStorage
class BDBFullStorage(BDBStorage):
def get_storageclass(self):
import BDBStorage.BDBFullStorage
return BDBStorage.BDBFullStorage.BDBFullStorage
@@ -0,0 +1,8 @@
<schema prefix="ZODB.config">
<import package="ZODB"/>
<multisection type="ZODB.database" name="*" attribute="database"
required="yes" />
</schema>
@@ -0,0 +1,34 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
import persistent.mapping
class fixer(object):
def __of__(self, parent):
def __setstate__(state, self=parent):
self._container=state
del self.__setstate__
return __setstate__
fixer=fixer()
class hack(object): pass
hack=hack()
def __basicnew__():
r=persistent.mapping.PersistentMapping()
r.__setstate__=fixer
return r
hack.__basicnew__=__basicnew__
@@ -0,0 +1,200 @@
=========================
Cross-Database References
=========================
Persistent references to objects in different databases within a
multi-database are allowed.
Lets set up a multi-database with 2 databases:
>>> import ZODB.tests.util, transaction, persistent
>>> databases = {}
>>> db1 = ZODB.tests.util.DB(databases=databases, database_name='1')
>>> db2 = ZODB.tests.util.DB(databases=databases, database_name='2')
And create a persistent object in the first database:
>>> tm = transaction.TransactionManager()
>>> conn1 = db1.open(transaction_manager=tm)
>>> p1 = MyClass()
>>> conn1.root()['p'] = p1
>>> tm.commit()
First, we get a connection to the second database. We get the second
connection using the first connection's `get_connection` method. This
is important. When using multiple databases, we need to make sure we
use a consistent set of connections so that the objects in the
connection caches are connected in a consistent manner.
>>> conn2 = conn1.get_connection('2')
Now, we'll create a second persistent object in the second database.
We'll have a reference to the first object:
>>> p2 = MyClass()
>>> conn2.root()['p'] = p2
>>> p2.p1 = p1
>>> tm.commit()
Now, let's open a separate connection to database 2. We use it to
read `p2`, use `p2` to get to `p1`, and verify that it is in database 1:
>>> conn = db2.open()
>>> p2x = conn.root()['p']
>>> p1x = p2x.p1
>>> p2x is p2, p2x._p_oid == p2._p_oid, p2x._p_jar.db() is db2
(False, True, True)
>>> p1x is p1, p1x._p_oid == p1._p_oid, p1x._p_jar.db() is db1
(False, True, True)
It isn't valid to create references outside a multi database:
>>> db3 = ZODB.tests.util.DB()
>>> conn3 = db3.open(transaction_manager=tm)
>>> p3 = MyClass()
>>> conn3.root()['p'] = p3
>>> tm.commit()
>>> p2.p3 = p3
>>> tm.commit() # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Traceback (most recent call last):
...
InvalidObjectReference:
('Attempt to store an object from a foreign database connection',
<Connection at ...>,
<ZODB.tests.testcrossdatabasereferences.MyClass...>)
>>> tm.abort()
Databases for new objects
-------------------------
Objects are normally added to a database by making them reachable from
an object already in the database. This is unambiguous when there is
only one database. With multiple databases, it is not so clear what
happens. Consider:
>>> p4 = MyClass()
>>> p1.p4 = p4
>>> p2.p4 = p4
In this example, the new object is reachable from both `p1` in database
1 and `p2` in database 2. If we commit, which database should `p4` end up
in? This sort of ambiguity could lead to subtle bugs. For that reason,
an error is generated if we commit changes when new objects are
reachable from multiple databases:
>>> tm.commit() # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Traceback (most recent call last):
...
InvalidObjectReference:
("A new object is reachable from multiple databases. Won't try to
guess which one was correct!",
<Connection at ...>,
<ZODB.tests.testcrossdatabasereferences.MyClass...>)
>>> tm.abort()
To resolve this ambiguity, we can commit before an object becomes
reachable from multiple databases.
>>> p4 = MyClass()
>>> p1.p4 = p4
>>> tm.commit()
>>> p2.p4 = p4
>>> tm.commit()
>>> p4._p_jar.db().database_name
'1'
This doesn't work with a savepoint:
>>> p5 = MyClass()
>>> p1.p5 = p5
>>> s = tm.savepoint()
>>> p2.p5 = p5
>>> tm.commit() # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Traceback (most recent call last):
...
InvalidObjectReference:
("A new object is reachable from multiple databases. Won't try to guess
which one was correct!",
<Connection at ...>,
<ZODB.tests.testcrossdatabasereferences.MyClass...>)
>>> tm.abort()
(Maybe it should.)
We can disambiguate this situation by using the connection add method
to explicitly say what database an object belongs to:
>>> p5 = MyClass()
>>> p1.p5 = p5
>>> p2.p5 = p5
>>> conn1.add(p5)
>>> tm.commit()
>>> p5._p_jar.db().database_name
'1'
This the most explicit and thus the best way, when practical, to avoid
the ambiguity.
Dissallowing implicit cross-database references
-----------------------------------------------
The database contructor accepts a xrefs keyword argument that defaults
to True. If False is passed, the implicit cross database references
are disallowed. (Note that currently, implicit cross references are
the only kind of cross references allowed.)
>>> databases = {}
>>> db1 = ZODB.tests.util.DB(databases=databases, database_name='1')
>>> db2 = ZODB.tests.util.DB(databases=databases, database_name='2',
... xrefs=False)
In this example, we allow cross-references from db1 to db2, but not
the other way around.
>>> c1 = db1.open()
>>> c2 = c1.get_connection('2')
>>> c1.root.x = c2.root()
>>> transaction.commit()
>>> c2.root.x = c1.root()
>>> transaction.commit() # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Traceback (most recent call last):
...
InvalidObjectReference:
("Database '2' doesn't allow implicit cross-database references",
<Connection at ...>,
{'x': {}})
>>> transaction.abort()
NOTE
----
This implementation is incomplete. It allows creating and using
cross-database references, however, there are a number of facilities
missing:
cross-database garbage collection
Garbage collection is done on a database by database basis.
If an object on a database only has references to it from other
databases, then the object will be garbage collected when its
database is packed. The cross-database references to it will be
broken.
cross-database undo
Undo is only applied to a single database. Fixing this for
multiple databases is going to be extremely difficult. Undo
currently poses consistency problems, so it is not (or should not
be) widely used.
Cross-database aware (tolerant) export/import
The export/import facility needs to be aware, at least, of cross-database
references.
@@ -0,0 +1,18 @@
##############################################################################
#
# Copyright Zope Foundation 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.
#
##############################################################################
try:
from zope.event import notify
except ImportError:
notify = lambda event: None
@@ -0,0 +1,10 @@
Event support
Sometimes, you want to react when ZODB does certain things. In the
past, ZODB provided ad hoc hook functions for this. Going forward,
ZODB will use an event mechanism. ZODB.event.notify is called with
events of interest.
If zope.event is installed, then ZODB.event.notify is simply an alias
for zope.event. If zope.event isn't installed, then ZODB.event is a
noop.
@@ -0,0 +1,280 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Implement an OID to File-position (long integer) mapping."""
# To save space, we do two things:
#
# 1. We split the keys (OIDS) into 6-byte prefixes and 2-byte suffixes.
# We use the prefixes as keys in a mapping from prefix to mappings
# of suffix to data:
#
# data is {prefix -> {suffix -> data}}
#
# 2. We limit the data size to 48 bits. This should allow databases
# as large as 256 terabytes.
#
# Most of the space is consumed by items in the mappings from 2-byte
# suffix to 6-byte data. This should reduce the overall memory usage to
# 8-16 bytes per OID.
#
# Because
# - the mapping from suffix to data contains at most 65535 entries,
# - this is an in-memory data structure
# - new keys are inserted sequentially,
# we use a BTree bucket instead of a full BTree to store the results.
#
# We use p64 to convert integers to 8-byte strings and lop off the two
# high-order bytes when saving. On loading data, we add the leading
# bytes back before using u64 to convert the data back to (long)
# integers.
import struct
from BTrees.fsBTree import fsBucket
from BTrees.OOBTree import OOBTree
import six
from ZODB._compat import INT_TYPES
from ZODB._compat import Pickler
from ZODB._compat import Unpickler
from ZODB._compat import _protocol
# convert between numbers and six-byte strings
def num2str(n):
return struct.pack(">Q", n)[2:]
def str2num(s):
return struct.unpack(">Q", b"\000\000" + s)[0]
def prefix_plus_one(s):
num = str2num(s)
return num2str(num + 1)
def prefix_minus_one(s):
num = str2num(s)
return num2str(num - 1)
def ensure_bytes(s):
# on Python 3 we might pickle bytes and unpickle unicode strings
return s.encode('ascii') if not isinstance(s, bytes) else s
class fsIndex(object):
def __init__(self, data=None):
self._data = OOBTree()
if data:
self.update(data)
def __getstate__(self):
return dict(
state_version = 1,
_data = [(k, v.toString())
for (k, v) in six.iteritems(self._data)
]
)
def __setstate__(self, state):
version = state.pop('state_version', 0)
getattr(self, '_setstate_%s' % version)(state)
def _setstate_0(self, state):
self.__dict__.clear()
self.__dict__.update(state)
self._data = OOBTree([
(ensure_bytes(k), v)
for (k, v) in self._data.items()
])
def _setstate_1(self, state):
self._data = OOBTree([
(ensure_bytes(k), fsBucket().fromString(ensure_bytes(v)))
for (k, v) in state['_data']
])
def __getitem__(self, key):
assert isinstance(key, bytes)
return str2num(self._data[key[:6]][key[6:]])
def save(self, pos, fname):
with open(fname, 'wb') as f:
pickler = Pickler(f, _protocol)
pickler.fast = True
pickler.dump(pos)
for k, v in six.iteritems(self._data):
pickler.dump((k, v.toString()))
pickler.dump(None)
@classmethod
def load(class_, fname):
with open(fname, 'rb') as f:
unpickler = Unpickler(f)
pos = unpickler.load()
if not isinstance(pos, INT_TYPES):
# NB: this might contain OIDs that got unpickled
# into Unicode strings on Python 3; hope the caller
# will pipe the result to fsIndex().update() to normalize
# the keys
return pos # Old format
index = class_()
data = index._data
while 1:
v = unpickler.load()
if not v:
break
k, v = v
data[ensure_bytes(k)] = fsBucket().fromString(ensure_bytes(v))
return dict(pos=pos, index=index)
def get(self, key, default=None):
assert isinstance(key, bytes)
tree = self._data.get(key[:6], default)
if tree is default:
return default
v = tree.get(key[6:], default)
if v is default:
return default
return str2num(v)
def __setitem__(self, key, value):
assert isinstance(key, bytes)
value = num2str(value)
treekey = key[:6]
tree = self._data.get(treekey)
if tree is None:
tree = fsBucket()
self._data[treekey] = tree
tree[key[6:]] = value
def __delitem__(self, key):
assert isinstance(key, bytes)
treekey = key[:6]
tree = self._data.get(treekey)
if tree is None:
raise KeyError(key)
del tree[key[6:]]
if not tree:
del self._data[treekey]
def __len__(self):
r = 0
for tree in six.itervalues(self._data):
r += len(tree)
return r
def update(self, mapping):
for k, v in mapping.items():
self[ensure_bytes(k)] = v
def has_key(self, key):
v = self.get(key, self)
return v is not self
def __contains__(self, key):
assert isinstance(key, bytes)
tree = self._data.get(key[:6])
if tree is None:
return False
v = tree.get(key[6:], None)
if v is None:
return False
return True
def clear(self):
self._data.clear()
def __iter__(self):
for prefix, tree in six.iteritems(self._data):
for suffix in tree:
yield prefix + suffix
iterkeys = __iter__
def keys(self):
return list(self.iterkeys())
def iteritems(self):
for prefix, tree in six.iteritems(self._data):
for suffix, value in six.iteritems(tree):
yield (prefix + suffix, str2num(value))
def items(self):
return list(self.iteritems())
def itervalues(self):
for tree in six.itervalues(self._data):
for value in six.itervalues(tree):
yield str2num(value)
def values(self):
return list(self.itervalues())
# Comment below applies for the following minKey and maxKey methods
#
# Obscure: what if `tree` is actually empty? We're relying here on
# that this class doesn't implement __delitem__: once a key gets
# into an fsIndex, the only way it can go away is by invoking
# clear(). Therefore nothing in _data.values() is ever empty.
#
# Note that because `tree` is an fsBTree, its minKey()/maxKey() methods are
# very efficient.
def minKey(self, key=None):
if key is None:
smallest_prefix = self._data.minKey()
else:
smallest_prefix = self._data.minKey(key[:6])
tree = self._data[smallest_prefix]
assert tree
if key is None:
smallest_suffix = tree.minKey()
else:
try:
smallest_suffix = tree.minKey(key[6:])
except ValueError: # 'empty tree' (no suffix >= arg)
next_prefix = prefix_plus_one(smallest_prefix)
smallest_prefix = self._data.minKey(next_prefix)
tree = self._data[smallest_prefix]
assert tree
smallest_suffix = tree.minKey()
return smallest_prefix + smallest_suffix
def maxKey(self, key=None):
if key is None:
biggest_prefix = self._data.maxKey()
else:
biggest_prefix = self._data.maxKey(key[:6])
tree = self._data[biggest_prefix]
assert tree
if key is None:
biggest_suffix = tree.maxKey()
else:
try:
biggest_suffix = tree.maxKey(key[6:])
except ValueError: # 'empty tree' (no suffix <= arg)
next_prefix = prefix_minus_one(biggest_prefix)
biggest_prefix = self._data.maxKey(next_prefix)
tree = self._data[biggest_prefix]
assert tree
biggest_suffix = tree.maxKey()
return biggest_prefix + biggest_suffix
@@ -0,0 +1,393 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""Simple script for repairing damaged FileStorage files.
Usage: %s [-f] [-v level] [-p] [-P seconds] input output
Recover data from a FileStorage data file, skipping over damaged data. Any
damaged data will be lost. This could lead to useless output if critical
data is lost.
Options:
-f
Overwrite output file even if it exists.
-v level
Set the verbosity level:
0 -- show progress indicator (default)
1 -- show transaction times and sizes
2 -- show transaction times and sizes, and show object (record)
ids, versions, and sizes
-p
Copy partial transactions. If a data record in the middle of a
transaction is bad, the data up to the bad data are packed. The
output record is marked as packed. If this option is not used,
transactions with any bad data are skipped.
-P t
Pack data to t seconds in the past. Note that if the "-p" option is
used, then t should be 0.
Important: The ZODB package must be importable. You may need to adjust
PYTHONPATH accordingly.
"""
from __future__ import print_function
# Algorithm:
#
# position to start of input
# while 1:
# if end of file:
# break
# try:
# copy_transaction
# except:
# scan for transaction
# continue
import sys
import os
import getopt
import time
from struct import unpack
try:
import ZODB
except ImportError:
if os.path.exists('ZODB'):
sys.path.append('.')
elif os.path.exists('FileStorage.py'):
sys.path.append('..')
import ZODB
import ZODB.FileStorage
from ZODB.utils import u64, as_text
from ZODB.FileStorage import TransactionRecord
from ZODB._compat import loads
from persistent.TimeStamp import TimeStamp
def die(mess='', show_docstring=False):
if mess:
print(mess + '\n', file=sys.stderr)
if show_docstring:
print(__doc__ % sys.argv[0], file=sys.stderr)
sys.exit(1)
class ErrorFound(Exception):
pass
def error(mess, *args):
raise ErrorFound(mess % args)
def read_txn_header(f, pos, file_size, outp, ltid):
# Read the transaction record
f.seek(pos)
h = f.read(23)
if len(h) < 23:
raise EOFError
tid, stl, status, ul, dl, el = unpack(">8s8scHHH",h)
status = as_text(status)
tl = u64(stl)
if pos + (tl + 8) > file_size:
error("bad transaction length at %s", pos)
if tl < (23 + ul + dl + el):
error("invalid transaction length, %s, at %s", tl, pos)
if ltid and tid < ltid:
error("time-stamp reducation %s < %s, at %s", u64(tid), u64(ltid), pos)
if status == "c":
truncate(f, pos, file_size, outp)
raise EOFError
if status not in " up":
error("invalid status, %r, at %s", status, pos)
tpos = pos
tend = tpos + tl
if status == "u":
# Undone transaction, skip it
f.seek(tend)
h = f.read(8)
if h != stl:
error("inconsistent transaction length at %s", pos)
pos = tend + 8
return pos, None, tid
pos = tpos+(23+ul+dl+el)
user = f.read(ul)
description = f.read(dl)
if el:
try: e = loads(f.read(el))
except: e = {}
else: e = {}
result = TransactionRecord(tid, status, user, description, e, pos, tend,
f, tpos)
pos = tend
# Read the (intentionally redundant) transaction length
f.seek(pos)
h = f.read(8)
if h != stl:
error("redundant transaction length check failed at %s", pos)
pos += 8
return pos, result, tid
def truncate(f, pos, file_size, outp):
"""Copy data from pos to end of f to a .trNNN file."""
# _trname is global so that the test suite can know the path too (in
# order to delete the file when the test ends).
global _trname
i = 0
while 1:
_trname = outp + ".tr%d" % i
if os.path.exists(_trname):
i += 1
else:
break
tr = open(_trname, "wb")
copy(f, tr, file_size - pos)
f.seek(pos)
tr.close()
def copy(src, dst, n):
while n:
buf = src.read(8096)
if not buf:
break
if len(buf) > n:
buf = buf[:n]
dst.write(buf)
n -= len(buf)
def scan(f, pos):
"""Return a potential transaction location following pos in f.
This routine scans forward from pos looking for the last data
record in a transaction. A period '.' always occurs at the end of
a pickle, and an 8-byte transaction length follows the last
pickle. If a period is followed by a plausible 8-byte transaction
length, assume that we have found the end of a transaction.
The caller should try to verify that the returned location is
actually a transaction header.
"""
while 1:
f.seek(pos)
data = f.read(8096)
if not data:
return 0
s = 0
while 1:
l = data.find(b".", s)
if l < 0:
pos += len(data)
break
# If we are less than 8 bytes from the end of the
# string, we need to read more data.
s = l + 1
if s > len(data) - 8:
pos += l
break
tl = u64(data[s:s+8])
if tl < pos:
return pos + s + 8
def iprogress(i):
if i % 2:
print(".", end=' ')
else:
print((i/2) % 10, end=' ')
sys.stdout.flush()
def progress(p):
for i in range(p):
iprogress(i)
def main():
try:
opts, args = getopt.getopt(sys.argv[1:], "fv:pP:")
except getopt.error as msg:
die(str(msg), show_docstring=True)
if len(args) != 2:
die("two positional arguments required", show_docstring=True)
inp, outp = args
force = partial = False
verbose = 0
pack = None
for opt, v in opts:
if opt == "-v":
verbose = int(v)
elif opt == "-p":
partial = True
elif opt == "-f":
force = True
elif opt == "-P":
pack = time.time() - float(v)
recover(inp, outp, verbose, partial, force, pack)
def recover(inp, outp, verbose=0, partial=False, force=False, pack=None):
print("Recovering", inp, "into", outp)
if os.path.exists(outp) and not force:
die("%s exists" % outp)
f = open(inp, "rb")
if f.read(4) != ZODB.FileStorage.packed_version:
die("input is not a file storage")
f.seek(0,2)
file_size = f.tell()
ofs = ZODB.FileStorage.FileStorage(outp, create=1)
_ts = None
ok = 1
prog1 = 0
undone = 0
pos = 4
ltid = None
while pos:
try:
npos, txn, tid = read_txn_header(f, pos, file_size, outp, ltid)
except EOFError:
break
except (KeyboardInterrupt, SystemExit):
raise
except Exception as err:
print("error reading txn header:", err)
if not verbose:
progress(prog1)
pos = scan(f, pos)
if verbose > 1:
print("looking for valid txn header at", pos)
continue
ltid = tid
if txn is None:
undone = undone + npos - pos
pos = npos
continue
else:
pos = npos
tid = txn.tid
if _ts is None:
_ts = TimeStamp(tid)
else:
t = TimeStamp(tid)
if t <= _ts:
if ok:
print(("Time stamps out of order %s, %s" % (_ts, t)))
ok = 0
_ts = t.laterThan(_ts)
tid = _ts.raw()
else:
_ts = t
if not ok:
print(("Time stamps back in order %s" % (t)))
ok = 1
ofs.tpc_begin(txn, tid, txn.status)
if verbose:
print("begin", pos, _ts, end=' ')
if verbose > 1:
print()
sys.stdout.flush()
nrec = 0
try:
for r in txn:
if verbose > 1:
if r.data is None:
l = "bp"
else:
l = len(r.data)
print("%7d %s %s" % (u64(r.oid), l))
ofs.restore(r.oid, r.tid, r.data, '', r.data_txn,
txn)
nrec += 1
except (KeyboardInterrupt, SystemExit):
raise
except Exception as err:
if partial and nrec:
ofs._status = "p"
ofs.tpc_vote(txn)
ofs.tpc_finish(txn)
if verbose:
print("partial")
else:
ofs.tpc_abort(txn)
print("error copying transaction:", err)
if not verbose:
progress(prog1)
pos = scan(f, pos)
if verbose > 1:
print("looking for valid txn header at", pos)
else:
ofs.tpc_vote(txn)
ofs.tpc_finish(txn)
if verbose:
print("finish")
sys.stdout.flush()
if not verbose:
prog = pos * 20 / file_size
while prog > prog1:
prog1 = prog1 + 1
iprogress(prog1)
bad = file_size - undone - ofs._pos
print("\n%s bytes removed during recovery" % bad)
if undone:
print("%s bytes of undone transaction data were skipped" % undone)
if pack is not None:
print("Packing ...")
from ZODB.serialize import referencesf
ofs.pack(pack, referencesf)
ofs.close()
f.close()
if __name__ == "__main__":
main()
@@ -0,0 +1,145 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""Tools for using FileStorage data files.
TODO: This module needs tests.
Caution: This file needs to be kept in sync with FileStorage.py.
"""
import struct
from ZODB.FileStorage.format import TRANS_HDR, DATA_HDR, TRANS_HDR_LEN
from ZODB.FileStorage.format import DATA_HDR_LEN
from ZODB.utils import u64
from ZODB._compat import loads
from persistent.TimeStamp import TimeStamp
class TxnHeader(object):
"""Object representing a transaction record header.
Attribute Position Value
--------- -------- -----
tid 0- 8 transaction id
length 8-16 length of entire transaction record - 8
status 16-17 status of transaction (' ', 'u', 'p'?)
user_len 17-19 length of user field (pack code H)
descr_len 19-21 length of description field (pack code H)
ext_len 21-23 length of extensions (pack code H)
"""
def __init__(self, file, pos):
self._file = file
self._pos = pos
self._read_header()
def _read_header(self):
self._file.seek(self._pos)
self._hdr = self._file.read(TRANS_HDR_LEN)
(self.tid, self.length, self.status, self.user_len, self.descr_len,
self.ext_len) = struct.unpack(TRANS_HDR, self._hdr)
def read_meta(self):
"""Load user, descr, and ext attributes."""
self.user = ""
self.descr = ""
self.ext = {}
if not (self.user_len or self.descr_len or self.ext_len):
return
self._file.seek(self._pos + TRANS_HDR_LEN)
if self.user_len:
self.user = self._file.read(self.user_len)
if self.descr_len:
self.descr = self._file.read(self.descr_len)
if self.ext_len:
self._ext = self._file.read(self.ext_len)
self.ext = loads(self._ext)
def get_offset(self):
return self._pos
def __len__(self):
return TRANS_HDR_LEN + self.user_len + self.descr_len + self.ext_len
def get_data_offset(self):
return self._pos + len(self)
def get_timestamp(self):
return TimeStamp(self.tid)
def get_raw_data(self):
data_off = self.get_data_offset()
data_len = self.length - (data_off - self._pos)
self._file.seek(data_off)
return self._file.read(data_len)
def next_txn(self):
off = self._pos + self.length + 8
self._file.seek(off)
s = self._file.read(8)
if not s:
return None
return TxnHeader(self._file, off)
def prev_txn(self):
if self._pos == 4:
return None
self._file.seek(self._pos - 8)
tlen = u64(self._file.read(8))
return TxnHeader(self._file, self._pos - (tlen + 8))
class DataHeader(object):
"""Object representing a data record header.
Attribute Position Value
--------- -------- -----
oid 0- 8 object id
serial 8-16 object serial numver
prev_rec_pos 16-24 position of previous data record for object
txn_pos 24-32 position of txn header
version_len 32-34 length of version (always 0)
data_len 34-42 length of data
"""
def __init__(self, file, pos):
self._file = file
self._pos = pos
self._read_header()
def _read_header(self):
self._file.seek(self._pos)
self._hdr = self._file.read(DATA_HDR_LEN)
# always read the longer header, just in case
(self.oid, self.serial, prev_rec_pos, txn_pos, vlen, data_len
) = struct.unpack(DATA_HDR, self._hdr[:DATA_HDR_LEN])
assert not vlen
self.prev_rec_pos = u64(prev_rec_pos)
self.txn_pos = u64(txn_pos)
self.data_len = u64(data_len)
def next_offset(self):
"""Return offset of next record."""
off = self._pos + self.data_len
off += DATA_HDR_LEN
if self.data_len == 0:
off += 8 # backpointer
return off
def prev_txn(f):
"""Return transaction located before current file position."""
f.seek(-8, 1)
tlen = u64(f.read(8)) + 8
return TxnHeader(f, f.tell() - tlen)
@@ -0,0 +1,332 @@
======================
Historical Connections
======================
.. We need to mess with time to prevent spurious test failures on windows
>>> _now = 1231019584.0
>>> def faux_time_time():
... global _now
... _now += .001 # must be less than 0.01
... return _now
>>> import time
>>> real_time_time = time.time
>>> real_time_sleep = time.sleep
>>> def faux_time_sleep(amt):
... global _now
... _now += amt
>>> if isinstance(time,type):
... time.time = staticmethod(faux_time_time) # Jython
... time.sleep = faux_time_sleep
... else:
... time.time = faux_time_time
... time.sleep = faux_time_sleep
>>> def utcnow():
... mus = (int(_now % 1 * 1000000), )
... return datetime.datetime(*time.gmtime(_now)[:6] + mus)
Usage
=====
A database can be opened with a read-only, historical connection when given
a specific transaction or datetime. This can enable full-context application
level conflict resolution, historical exploration and preparation for reverts,
or even the use of a historical database revision as "production" while
development continues on a "development" head.
A database can be opened historically ``at`` or ``before`` a given transaction
serial or datetime. Here's a simple example. It should work with any storage
that supports ``loadBefore``.
We'll begin our example with a fairly standard set up. We
- make a storage and a database;
- open a normal connection;
- modify the database through the connection;
- commit a transaction, remembering the time in UTC;
- modify the database again; and
- commit a transaction.
>>> import ZODB.MappingStorage
>>> db = ZODB.MappingStorage.DB()
>>> conn = db.open()
>>> import persistent.mapping
>>> conn.root()['first'] = persistent.mapping.PersistentMapping(count=0)
>>> import transaction
>>> transaction.commit()
We wait for some time to pass, record he time, and then make some other changes.
>>> import time
>>> time.sleep(.01)
>>> import datetime
>>> now = utcnow()
>>> time.sleep(.01)
>>> root = conn.root()
>>> root['second'] = persistent.mapping.PersistentMapping()
>>> root['first']['count'] += 1
>>> transaction.commit()
Now we will show a historical connection. We'll open one using the ``now``
value we generated above, and then demonstrate that the state of the original
connection, at the mutable head of the database, is different than the
historical state.
>>> transaction1 = transaction.TransactionManager()
>>> historical_conn = db.open(transaction_manager=transaction1, at=now)
>>> sorted(conn.root().keys())
['first', 'second']
>>> conn.root()['first']['count']
1
>>> sorted(historical_conn.root().keys())
['first']
>>> historical_conn.root()['first']['count']
0
Moreover, the historical connection cannot commit changes.
>>> historical_conn.root()['first']['count'] += 1
>>> historical_conn.root()['first']['count']
1
>>> transaction1.commit()
Traceback (most recent call last):
...
ReadOnlyHistoryError
>>> transaction1.abort()
>>> historical_conn.root()['first']['count']
0
(It is because of the mutable behavior outside of transactional semantics that
we must have a separate connection, and associated object cache, per thread,
even though the semantics should be readonly.)
As demonstrated, a timezone-naive datetime will be interpreted as UTC. You
can also pass a timezone-aware datetime or a serial (transaction id).
Here's opening with a serial--the serial of the root at the time of the first
commit.
>>> historical_serial = historical_conn.root()._p_serial
>>> historical_conn.close()
>>> historical_conn = db.open(transaction_manager=transaction1,
... at=historical_serial)
>>> sorted(historical_conn.root().keys())
['first']
>>> historical_conn.root()['first']['count']
0
>>> historical_conn.close()
We've shown the ``at`` argument. You can also ask to look ``before`` a datetime
or serial. (It's an error to pass both [#not_both]_) In this example, we're
looking at the database immediately prior to the most recent change to the
root.
>>> serial = conn.root()._p_serial
>>> historical_conn = db.open(
... transaction_manager=transaction1, before=serial)
>>> sorted(historical_conn.root().keys())
['first']
>>> historical_conn.root()['first']['count']
0
In fact, ``at`` arguments are translated into ``before`` values because the
underlying mechanism is a storage's loadBefore method. When you look at a
connection's ``before`` attribute, it is normalized into a ``before`` serial,
no matter what you pass into ``db.open``.
>>> print(conn.before)
None
>>> historical_conn.before == serial
True
>>> conn.close()
Configuration
=============
Like normal connections, the database lets you set how many total historical
connections can be active without generating a warning, and
how many objects should be kept in each historical connection's object cache.
>>> db.getHistoricalPoolSize()
3
>>> db.setHistoricalPoolSize(4)
>>> db.getHistoricalPoolSize()
4
>>> db.getHistoricalCacheSize()
1000
>>> db.setHistoricalCacheSize(2000)
>>> db.getHistoricalCacheSize()
2000
In addition, you can specify the minimum number of seconds that an unused
historical connection should be kept.
>>> db.getHistoricalTimeout()
300
>>> db.setHistoricalTimeout(400)
>>> db.getHistoricalTimeout()
400
All three of these values can be specified in a ZConfig file.
>>> import ZODB.config
>>> db2 = ZODB.config.databaseFromString('''
... <zodb>
... <mappingstorage/>
... historical-pool-size 3
... historical-cache-size 1500
... historical-timeout 6m
... </zodb>
... ''')
>>> db2.getHistoricalPoolSize()
3
>>> db2.getHistoricalCacheSize()
1500
>>> db2.getHistoricalTimeout()
360
The pool lets us reuse connections. To see this, we'll open some
connections, close them, and then open them again:
>>> conns1 = [db2.open(before=serial) for i in range(4)]
>>> _ = [c.close() for c in conns1]
>>> conns2 = [db2.open(before=serial) for i in range(4)]
Now let's look at what we got. The first connection in conns 2 is the
last connection in conns1, because it was the last connection closed.
>>> conns2[0] is conns1[-1]
True
Also for the next two:
>>> (conns2[1] is conns1[-2]), (conns2[2] is conns1[-3])
(True, True)
But not for the last:
>>> conns2[3] is conns1[-4]
False
Because the pool size was set to 3.
Connections are also discarded if they haven't been used in a while.
To see this, let's close two of the connections:
>>> conns2[0].close(); conns2[1].close()
We'l also set the historical timeout to be very low:
>>> db2.setHistoricalTimeout(.01)
>>> time.sleep(.1)
>>> conns2[2].close(); conns2[3].close()
Now, when we open 4 connections:
>>> conns1 = [db2.open(before=serial) for i in range(4)]
We'll see that only the last 2 connections from conn2 are in the
result:
>>> [c in conns1 for c in conns2]
[False, False, True, True]
If you change the historical cache size, that changes the size of the
persistent cache on our connection.
>>> historical_conn._cache.cache_size
2000
>>> db.setHistoricalCacheSize(1500)
>>> historical_conn._cache.cache_size
1500
Invalidations
=============
Invalidations are ignored for historical connections. This is another white box
test.
>>> historical_conn = db.open(
... transaction_manager=transaction1, at=serial)
>>> conn = db.open()
>>> sorted(conn.root().keys())
['first', 'second']
>>> conn.root()['first']['count']
1
>>> sorted(historical_conn.root().keys())
['first', 'second']
>>> historical_conn.root()['first']['count']
1
>>> conn.root()['first']['count'] += 1
>>> conn.root()['third'] = persistent.mapping.PersistentMapping()
>>> transaction.commit()
>>> historical_conn.close()
Note that if you try to open an historical connection to a time in the future,
you will get an error.
>>> historical_conn = db.open(
... at=utcnow()+datetime.timedelta(1))
Traceback (most recent call last):
...
ValueError: cannot open an historical connection in the future.
Warnings
========
First, if you use datetimes to get a historical connection, be aware that the
conversion from datetime to transaction id has some pitfalls. Generally, the
transaction ids in the database are only as time-accurate as the system clock
was when the transaction id was created. Moreover, leap seconds are handled
somewhat naively in the ZODB (largely because they are handled naively in Unix/
POSIX time) so any minute that contains a leap second may contain serials that
are a bit off. This is not generally a problem for the ZODB, because serials
are guaranteed to increase, but it does highlight the fact that serials are not
guaranteed to be accurately connected to time. Generally, they are about as
reliable as time.time.
Second, historical connections currently introduce potentially wide variance in
memory requirements for the applications. Since you can open up many
connections to different serials, and each gets their own pool, you may collect
quite a few connections. For now, at least, if you use this feature you need to
be particularly careful of your memory usage. Get rid of pools when you know
you can, and reuse the exact same values for ``at`` or ``before`` when
possible. If historical connections are used for conflict resolution, these
connections will probably be temporary--not saved in a pool--so that the extra
memory usage would also be brief and unlikely to overlap.
.. cleanup
>>> db.close()
>>> db2.close()
.. restore time
>>> time.time = real_time_time
>>> time.sleep = real_time_sleep
.. ......... ..
.. Footnotes ..
.. ......... ..
.. [#not_both] It is an error to try and pass both `at` and `before`.
>>> historical_conn = db.open(
... transaction_manager=transaction1, at=now, before=historical_serial)
Traceback (most recent call last):
...
ValueError: can only pass zero or one of `at` and `before`
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
##############################################################################
#
# Copyright (c) 2004 Zope Foundation 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
#
##############################################################################
"""Supplies custom logging levels BLATHER and TRACE.
$Revision: 1.1 $
"""
import logging
__all__ = ["BLATHER", "TRACE"]
# In the days of zLOG, there were 7 standard log levels, and ZODB/ZEO used
# all of them. Here's how they map to the logging package's 5 standard
# levels:
#
# zLOG logging
# ------------- ---------------
# PANIC (300) FATAL, CRITICAL (50)
# ERROR (200) ERROR (40)
# WARNING, PROBLEM (100) WARN (30)
# INFO (0) INFO (20)
# BLATHER (-100) none -- defined here as BLATHER (15)
# DEBUG (-200) DEBUG (10)
# TRACE (-300) none -- defined here as TRACE (5)
#
# TRACE is used by ZEO for extremely verbose trace output, enabled only
# when chasing bottom-level communications bugs. It really should be at
# a lower level than DEBUG.
#
# BLATHER is a harder call, and various instances could probably be folded
# into INFO or DEBUG without real harm.
BLATHER = 15
TRACE = 5
logging.addLevelName(BLATHER, "BLATHER")
logging.addLevelName(TRACE, "TRACE")
@@ -0,0 +1,266 @@
"""Adapt IStorage objects to IMVCCStorage
This is a largely internal implementation of ZODB, especially DB and
Connection. It takes the MVCC implementation involving invalidations
and start time and moves it into a storage adapter. This allows ZODB
to treat Relstoage and other storages in pretty much the same way and
also simplifies the implementation of the DB and Connection classes.
"""
import zope.interface
from . import interfaces, serialize, POSException
from .utils import p64, u64, Lock
class Base(object):
_copy_methods = (
'getName', 'getSize', 'history', 'lastTransaction', 'sortKey',
'loadBlob', 'openCommittedBlobFile',
'isReadOnly', 'supportsUndo', 'undoLog', 'undoInfo',
'temporaryDirectory',
)
def __init__(self, storage):
self._storage = storage
if interfaces.IBlobStorage.providedBy(storage):
zope.interface.alsoProvides(self, interfaces.IBlobStorage)
def __getattr__(self, name):
if name in self._copy_methods:
m = getattr(self._storage, name)
setattr(self, name, m)
return m
raise AttributeError(name)
def __len__(self):
return len(self._storage)
class MVCCAdapter(Base):
def __init__(self, storage):
Base.__init__(self, storage)
self._instances = set()
self._lock = Lock()
if hasattr(storage, 'registerDB'):
storage.registerDB(self)
def new_instance(self):
instance = MVCCAdapterInstance(self)
with self._lock:
self._instances.add(instance)
return instance
def before_instance(self, before=None):
return HistoricalStorageAdapter(self._storage, before)
def undo_instance(self):
return UndoAdapterInstance(self)
def _release(self, instance):
with self._lock:
self._instances.remove(instance)
closed = False
def close(self):
if not self.closed:
self.closed = True
self._storage.close()
del self._instances
del self._storage
def invalidateCache(self):
with self._lock:
for instance in self._instances:
instance._invalidateCache()
def invalidate(self, transaction_id, oids):
with self._lock:
for instance in self._instances:
instance._invalidate(oids)
def _invalidate_finish(self, oids, committing_instance):
with self._lock:
for instance in self._instances:
if instance is not committing_instance:
instance._invalidate(oids)
references = serialize.referencesf
transform_record_data = untransform_record_data = lambda self, data: data
def pack(self, pack_time, referencesf):
return self._storage.pack(pack_time, referencesf)
class MVCCAdapterInstance(Base):
_copy_methods = Base._copy_methods + (
'loadSerial', 'new_oid', 'tpc_vote',
'checkCurrentSerialInTransaction', 'tpc_abort',
)
def __init__(self, base):
self._base = base
Base.__init__(self, base._storage)
self._lock = Lock()
self._invalidations = set()
self._start = None # Transaction start time
self._sync = getattr(self._storage, 'sync', lambda : None)
def release(self):
self._base._release(self)
close = release
def _invalidateCache(self):
with self._lock:
self._invalidations = None
def _invalidate(self, oids):
with self._lock:
try:
self._invalidations.update(oids)
except AttributeError:
if self._invalidations is not None:
raise
def sync(self, force=True):
if force:
self._sync()
def poll_invalidations(self):
self._start = p64(u64(self._storage.lastTransaction()) + 1)
with self._lock:
if self._invalidations is None:
self._invalidations = set()
return None
else:
result = list(self._invalidations)
self._invalidations.clear()
return result
def load(self, oid):
assert self._start is not None
r = self._storage.loadBefore(oid, self._start)
if r is None:
raise POSException.ReadConflictError(repr(oid))
return r[:2]
def prefetch(self, oids):
try:
self._storage.prefetch(oids, self._start)
except AttributeError:
if not hasattr(self._storage, 'prefetch'):
self.prefetch = lambda *a: None
else:
raise
_modified = None # Used to keep track of oids modified within a
# transaction, so we can invalidate them later.
def tpc_begin(self, transaction):
self._storage.tpc_begin(transaction)
self._modified = set()
def store(self, oid, serial, data, version, transaction):
self._storage.store(oid, serial, data, version, transaction)
self._modified.add(oid)
def storeBlob(self, oid, serial, data, blobfilename, version, transaction):
self._storage.storeBlob(
oid, serial, data, blobfilename, '', transaction)
self._modified.add(oid)
def tpc_finish(self, transaction, func = lambda tid: None):
modified = self._modified
self._modified = None
def invalidate_finish(tid):
self._base._invalidate_finish(modified, self)
func(tid)
return self._storage.tpc_finish(transaction, invalidate_finish)
def read_only_writer(self, *a, **kw):
raise POSException.ReadOnlyError
class HistoricalStorageAdapter(Base):
"""Adapt a storage to a historical storage
"""
_copy_methods = Base._copy_methods + (
'loadSerial', 'tpc_begin', 'tpc_finish', 'tpc_abort', 'tpc_vote',
'checkCurrentSerialInTransaction',
)
def __init__(self, storage, before=None):
Base.__init__(self, storage)
self._before = before
def isReadOnly(self):
return True
def supportsUndo(self):
return False
def release(self):
try:
release = self._storage.release
except AttributeError:
pass
else:
release()
close = release
def sync(self, force=True):
pass
def poll_invalidations(self):
return []
new_oid = pack = store = read_only_writer
def load(self, oid, version=''):
r = self._storage.loadBefore(oid, self._before)
if r is None:
raise POSException.POSKeyError(oid)
return r[:2]
class UndoAdapterInstance(Base):
_copy_methods = Base._copy_methods + (
'tpc_abort',
)
def __init__(self, base):
self._base = base
Base.__init__(self, base._storage)
def release(self):
pass
close = release
def tpc_begin(self, transaction):
self._storage.tpc_begin(transaction)
self._undone = set()
def undo(self, transaction_id, transaction):
result = self._storage.undo(transaction_id, transaction)
if result:
self._undone.update(result[1])
return result
def tpc_vote(self, transaction):
result = self._storage.tpc_vote(transaction)
if result:
self._undone.update(result)
def tpc_finish(self, transaction, func = lambda tid: None):
def invalidate_finish(tid):
self._base._invalidate_finish(self._undone, None)
func(tid)
self._storage.tpc_finish(transaction, invalidate_finish)
@@ -0,0 +1,227 @@
##############################################################################
#
# Copyright (c) 2004 Zope Foundation 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.
#
##############################################################################
"""Persistent Class Support
$Id$
"""
# Notes:
#
# Persistent classes are non-ghostable. This has some interesting
# ramifications:
#
# - When an object is invalidated, it must reload its state
#
# - When an object is loaded from the database, its state must be
# loaded. Unfortunately, there isn't a clear signal when an object is
# loaded from the database. This should probably be fixed.
#
# In the mean time, we need to infer. This should be viewed as a
# short term hack.
#
# Here's the strategy we'll use:
#
# - We'll have a need to be loaded flag that we'll set in
# __new__, through an extra argument.
#
# - When setting _p_oid and _p_jar, if both are set and we need to be
# loaded, then we'll load out state.
#
# - We'll use _p_changed is None to indicate that we're in this state.
#
class _p_DataDescr(object):
# Descr used as base for _p_ data. Data are stored in
# _p_class_dict.
def __init__(self, name):
self.__name__ = name
def __get__(self, inst, cls):
if inst is None:
return self
if '__global_persistent_class_not_stored_in_DB__' in inst.__dict__:
raise AttributeError(self.__name__)
return inst._p_class_dict.get(self.__name__)
def __set__(self, inst, v):
inst._p_class_dict[self.__name__] = v
def __delete__(self, inst):
raise AttributeError(self.__name__)
class _p_oid_or_jar_Descr(_p_DataDescr):
# Special descr for _p_oid and _p_jar that loads
# state when set if both are set and _p_changed is None
#
# See notes above
def __set__(self, inst, v):
get = inst._p_class_dict.get
if v == get(self.__name__):
return
inst._p_class_dict[self.__name__] = v
jar = get('_p_jar')
if (jar is not None
and get('_p_oid') is not None
and get('_p_changed') is None
):
jar.setstate(inst)
class _p_ChangedDescr(object):
# descriptor to handle special weird semantics of _p_changed
def __get__(self, inst, cls):
if inst is None:
return self
return inst._p_class_dict['_p_changed']
def __set__(self, inst, v):
if v is None:
return
inst._p_class_dict['_p_changed'] = bool(v)
def __delete__(self, inst):
inst._p_invalidate()
class _p_MethodDescr(object):
"""Provide unassignable class attributes
"""
def __init__(self, func):
self.func = func
def __get__(self, inst, cls):
if inst is None:
return cls
return self.func.__get__(inst, cls)
def __set__(self, inst, v):
raise AttributeError(self.__name__)
def __delete__(self, inst):
raise AttributeError(self.__name__)
special_class_descrs = '__dict__', '__weakref__'
class PersistentMetaClass(type):
_p_jar = _p_oid_or_jar_Descr('_p_jar')
_p_oid = _p_oid_or_jar_Descr('_p_oid')
_p_changed = _p_ChangedDescr()
_p_serial = _p_DataDescr('_p_serial')
def __new__(self, name, bases, cdict, _p_changed=False):
cdict = dict([(k, v) for (k, v) in cdict.items()
if not k.startswith('_p_')])
cdict['_p_class_dict'] = {'_p_changed': _p_changed}
return super(PersistentMetaClass, self).__new__(
self, name, bases, cdict)
def __getnewargs__(self):
return self.__name__, self.__bases__, {}, None
__getnewargs__ = _p_MethodDescr(__getnewargs__)
def _p_maybeupdate(self, name):
get = self._p_class_dict.get
data_manager = get('_p_jar')
if (
(data_manager is not None)
and
(get('_p_oid') is not None)
and
(get('_p_changed') == False)
):
self._p_changed = True
data_manager.register(self)
def __setattr__(self, name, v):
if not ((name.startswith('_p_') or name.startswith('_v'))):
self._p_maybeupdate(name)
super(PersistentMetaClass, self).__setattr__(name, v)
def __delattr__(self, name):
if not ((name.startswith('_p_') or name.startswith('_v'))):
self._p_maybeupdate(name)
super(PersistentMetaClass, self).__delattr__(name)
def _p_deactivate(self):
# persistent classes can't be ghosts
pass
_p_deactivate = _p_MethodDescr(_p_deactivate)
def _p_invalidate(self):
# reset state
self._p_class_dict['_p_changed'] = None
self._p_jar.setstate(self)
_p_invalidate = _p_MethodDescr(_p_invalidate)
def __getstate__(self):
return (self.__bases__,
dict([(k, v) for (k, v) in self.__dict__.items()
if not (k.startswith('_p_')
or k.startswith('_v_')
or k in special_class_descrs
)
]),
)
__getstate__ = _p_MethodDescr(__getstate__)
def __setstate__(self, state):
bases, cdict = state
if self.__bases__ != bases:
# __getnewargs__ should've taken care of that
raise AssertionError(self.__bases__, '!=', bases)
cdict = dict([(k, v) for (k, v) in cdict.items()
if not k.startswith('_p_')])
_p_class_dict = self._p_class_dict
self._p_class_dict = {}
to_remove = [k for k in self.__dict__
if ((k not in cdict)
and
(k not in special_class_descrs)
and
(k != '_p_class_dict')
)]
for k in to_remove:
delattr(self, k)
for k, v in cdict.items():
setattr(self, k, v)
self._p_class_dict = _p_class_dict
self._p_changed = False
__setstate__ = _p_MethodDescr(__setstate__)
def _p_activate(self):
self._p_jar.setstate(self)
_p_activate = _p_MethodDescr(_p_activate)
@@ -0,0 +1,291 @@
==================
Persistent Classes
==================
NOTE: persistent classes are EXPERIMENTAL and, in some sense,
incomplete. This module exists largely to test changes made to
support Zope 2 ZClasses, with their historical flaws.
The persistentclass module provides a meta class that can be used to implement
persistent classes.
Persistent classes have the following properties:
- They cannot be turned into ghosts
- They can only contain picklable subobjects
- They don't live in regular file-system modules
Let's look at an example:
>>> def __init__(self, name):
... self.name = name
>>> def foo(self):
... return self.name, self.kind
>>> import ZODB.persistentclass
>>> C = ZODB.persistentclass.PersistentMetaClass(
... 'C', (object, ), dict(
... __init__ = __init__,
... __module__ = '__zodb__',
... foo = foo,
... kind = 'sample',
... ))
This example is obviously a bit contrived. In particular, we defined
the methods outside of the class. Why? Because all of the items in a
persistent class must be picklable. We defined the methods as global
functions to make them picklable.
Also note that we explicitly set the module. Persistent classes don't
live in normal Python modules. Rather, they live in the database. We
use information in ``__module__`` to record where in the database. When
we want to use a database, we will need to supply a custom class
factory to load instances of the class.
The class we created works a lot like other persistent objects. It
has standard standard persistent attributes:
>>> C._p_oid
>>> C._p_jar
>>> C._p_serial
>>> C._p_changed
False
Because we haven't saved the object, the jar, oid, and serial are all
None and it's not changed.
We can create and use instances of the class:
>>> c = C('first')
>>> c.foo()
('first', 'sample')
We can modify the class and none of the persistent attributes will
change because the object hasn't been saved.
>>> import six
>>> def bar(self):
... six.print_('bar', self.name)
>>> C.bar = bar
>>> c.bar()
bar first
>>> C._p_oid
>>> C._p_jar
>>> C._p_serial
>>> C._p_changed
False
Now, we can store the class in a database. We're going to use an
explicit transaction manager so that we can show parallel transactions
without having to use threads.
>>> import transaction
>>> tm = transaction.TransactionManager()
>>> connection = some_database.open(transaction_manager=tm)
>>> connection.root()['C'] = C
>>> tm.commit()
Now, if we look at the persistence variables, we'll see that they have
values:
>>> C._p_oid
'\x00\x00\x00\x00\x00\x00\x00\x01'
>>> C._p_jar is not None
True
>>> C._p_serial is not None
True
>>> C._p_changed
False
Now, if we modify the class:
>>> def baz(self):
... six.print_('baz', self.name)
>>> C.baz = baz
>>> c.baz()
baz first
We'll see that the class has changed:
>>> C._p_changed
True
If we abort the transaction:
>>> tm.abort()
Then the class will return to it's prior state:
>>> c.baz()
Traceback (most recent call last):
...
AttributeError: 'C' object has no attribute 'baz'
>>> c.bar()
bar first
We can open another connection and access the class there.
>>> tm2 = transaction.TransactionManager()
>>> connection2 = some_database.open(transaction_manager=tm2)
>>> C2 = connection2.root()['C']
>>> c2 = C2('other')
>>> c2.bar()
bar other
If we make changes without committing them:
>>> C.bar = baz
>>> c.bar()
baz first
>>> C is C2
False
Other connections are unaffected:
>>> connection2.sync()
>>> c2.bar()
bar other
Until we commit:
>>> tm.commit()
>>> connection2.sync()
>>> c2.bar()
baz other
Similarly, we don't see changes made in other connections:
>>> C2.color = 'red'
>>> tm2.commit()
>>> c.color
Traceback (most recent call last):
...
AttributeError: 'C' object has no attribute 'color'
until we sync:
>>> connection.sync()
>>> c.color
'red'
Instances of Persistent Classes
-------------------------------
We can, of course, store instances of persistent classes in the
database:
>>> c.color = 'blue'
>>> connection.root()['c'] = c
>>> tm.commit()
>>> connection2.sync()
>>> connection2.root()['c'].color
'blue'
NOTE: If a non-persistent instance of a persistent class is copied,
the class may be copied as well. This is usually not the desired
result.
Persistent instances of persistent classes
------------------------------------------
Persistent instances of persistent classes are handled differently
than normal instances. When we copy a persistent instances of a
persistent class, we want to avoid copying the class.
Lets create a persistent class that subclasses Persistent:
>>> import persistent
>>> class P(persistent.Persistent, C):
... __module__ = '__zodb__'
... color = 'green'
>>> connection.root()['P'] = P
>>> import persistent.mapping
>>> connection.root()['obs'] = persistent.mapping.PersistentMapping()
>>> p = P('p')
>>> connection.root()['obs']['p'] = p
>>> tm.commit()
You might be wondering why we didn't just stick 'p' into the root
object. We created an intermediate persistent object instead. We are
storing persistent classes in the root object. To create a ghost for a
persistent instance of a persistent class, we need to be able to be
able to access the root object and it must be loaded first. If the
instance was in the root object, we'd be unable to create it while
loading the root object.
Now, if we try to load it, we get a broken object:
>>> connection2.sync()
>>> connection2.root()['obs']['p']
<persistent broken __zodb__.P instance '\x00\x00\x00\x00\x00\x00\x00\x04'>
because the module, `__zodb__` can't be loaded. We need to provide a
class factory that knows about this special module. Here we'll supply a
sample class factory that looks up a class name in the database root
if the module is `__zodb__`. It falls back to the normal class lookup
for other modules:
>>> from ZODB.broken import find_global
>>> def classFactory(connection, modulename, globalname):
... if modulename == '__zodb__':
... return connection.root()[globalname]
... return find_global(modulename, globalname)
>>> some_database.classFactory = classFactory
Normally, the classFactory should be set before a database is opened.
We'll reopen the connections we're using. We'll assign the old
connections to a variable first to prevent getting them from the
connection pool:
>>> old = connection, connection2
>>> connection = some_database.open(transaction_manager=tm)
>>> connection2 = some_database.open(transaction_manager=tm2)
Now, we can read the object:
>>> connection2.root()['obs']['p'].color
'green'
>>> connection2.root()['obs']['p'].color = 'blue'
>>> tm2.commit()
>>> connection.sync()
>>> p = connection.root()['obs']['p']
>>> p.color
'blue'
Copying
-------
If we copy an instance via export/import, the copy and the original
share the same class:
>>> file = connection.exportFile(p._p_oid)
>>> _ = file.seek(0)
>>> cp = connection.importFile(file)
>>> file.close()
>>> cp.color
'blue'
>>> cp is not p
True
>>> cp.__class__ is p.__class__
True
>>> tm.abort()
XXX test abort of import
@@ -0,0 +1,107 @@
This directory contains a collection of utilities for managing ZODB
databases. Some are more useful than others. If you install ZODB
using distutils ("python setup.py install"), a few of these will be installed.
Unless otherwise noted, these scripts are invoked with the name of the
Data.fs file as their only argument. Example: checkbtrees.py data.fs.
analyze.py -- a transaction analyzer for FileStorage
Reports on the data in a FileStorage. The report is organized by
class. It shows total data, as well as separate reports for current
and historical revisions of objects.
checkbtrees.py -- checks BTrees in a FileStorage for corruption
Attempts to find all the BTrees contained in a Data.fs, calls their
_check() methods, and runs them through BTrees.check.check().
fsdump.py -- summarize FileStorage contents, one line per revision
Prints a report of FileStorage contents, with one line for each
transaction and one line for each data record in that transaction.
Includes time stamps, file positions, and class names.
fsoids.py -- trace all uses of specified oids in a FileStorage
For heavy debugging.
A set of oids is specified by text file listing and/or command line.
A report is generated showing all uses of these oids in the database:
all new-revision creation/modifications, all references from all
revisions of other objects, and all creation undos.
fstest.py -- simple consistency checker for FileStorage
usage: fstest.py [-v] data.fs
The fstest tool will scan all the data in a FileStorage and report an
error if it finds any corrupt transaction data. The tool will print a
message when the first error is detected an exit.
The tool accepts one or more -v arguments. If a single -v is used, it
will print a line of text for each transaction record it encounters.
If two -v arguments are used, it will also print a line of text for
each object. The objects for a transaction will be printed before the
transaction itself.
Note: It does not check the consistency of the object pickles. It is
possible for the damage to occur only in the part of the file that
stores object pickles. Those errors will go undetected.
space.py -- report space used by objects in a FileStorage
usage: space.py [-v] data.fs
This ignores revisions and versions.
netspace.py -- hackish attempt to report on size of objects
usage: netspace.py [-P | -v] data.fs
-P: do a pack first
-v: print info for all objects, even if a traversal path isn't found
Traverses objects from the database root and attempts to calculate
size of object, including all reachable subobjects.
repozo.py -- incremental backup utility for FileStorage
Run the script with the -h option to see usage details.
timeout.py -- script to test transaction timeout
usage: timeout.py address delay [storage-name]
This script connects to a storage, begins a transaction, calls store()
and tpc_vote(), and then sleeps forever. This should trigger the
transaction timeout feature of the server.
zodbload.py -- exercise ZODB under a heavy synthesized Zope-like load
See the module docstring for details. Note that this script requires
Zope. New in ZODB3 3.1.4.
fsrefs.py -- check FileStorage for dangling references
fstail.py -- display the most recent transactions in a FileStorage
usage: fstail.py [-n nxtn] data.fs
The most recent ntxn transactions are displayed, to stdout.
Optional argument -n specifies ntxn, and defaults to 10.
migrate.py -- do a storage migration and gather statistics
See the module docstring for details.
@@ -0,0 +1 @@
#
@@ -0,0 +1,147 @@
#!/usr/bin/env python
# Based on a transaction analyzer by Matt Kromer.
from __future__ import print_function
import sys
from ZODB.FileStorage import FileStorage
from ZODB._compat import PersistentUnpickler, BytesIO
class FakeError(Exception):
def __init__(self, module, name):
Exception.__init__(self)
self.module = module
self.name = name
def fake_find_class(module, name):
raise FakeError(module, name)
def FakeUnpickler(f):
unpickler = PersistentUnpickler(fake_find_class, None, f)
return unpickler
class Report(object):
def __init__(self):
self.OIDMAP = {}
self.TYPEMAP = {}
self.TYPESIZE = {}
self.FREEMAP = {}
self.USEDMAP = {}
self.TIDS = 0
self.OIDS = 0
self.DBYTES = 0
self.COIDS = 0
self.CBYTES = 0
self.FOIDS = 0
self.FBYTES = 0
def shorten(s, n):
l = len(s)
if l <= n:
return s
while len(s) + 3 > n: # account for ...
i = s.find(".")
if i == -1:
# In the worst case, just return the rightmost n bytes
return s[-n:]
else:
s = s[i + 1:]
l = len(s)
return "..." + s
def report(rep):
print("Processed %d records in %d transactions" % (rep.OIDS, rep.TIDS))
print("Average record size is %7.2f bytes" % (rep.DBYTES * 1.0 / rep.OIDS))
print(("Average transaction size is %7.2f bytes" %
(rep.DBYTES * 1.0 / rep.TIDS)))
print("Types used:")
fmt = "%-46s %7s %9s %6s %7s"
fmtp = "%-46s %7d %9d %5.1f%% %7.2f" # per-class format
fmts = "%46s %7d %8dk %5.1f%% %7.2f" # summary format
print(fmt % ("Class Name", "Count", "TBytes", "Pct", "AvgSize"))
print(fmt % ('-'*46, '-'*7, '-'*9, '-'*5, '-'*7))
typemap = sorted(rep.TYPEMAP)
cumpct = 0.0
for t in typemap:
pct = rep.TYPESIZE[t] * 100.0 / rep.DBYTES
cumpct += pct
print(fmtp % (shorten(t, 46), rep.TYPEMAP[t], rep.TYPESIZE[t],
pct, rep.TYPESIZE[t] * 1.0 / rep.TYPEMAP[t]))
print(fmt % ('='*46, '='*7, '='*9, '='*5, '='*7))
print("%46s %7d %9s %6s %6.2fk" % ('Total Transactions', rep.TIDS, ' ',
' ', rep.DBYTES * 1.0 / rep.TIDS / 1024.0))
print(fmts % ('Total Records', rep.OIDS, rep.DBYTES / 1024.0, cumpct,
rep.DBYTES * 1.0 / rep.OIDS))
print(fmts % ('Current Objects', rep.COIDS, rep.CBYTES / 1024.0,
rep.CBYTES * 100.0 / rep.DBYTES,
rep.CBYTES * 1.0 / rep.COIDS))
if rep.FOIDS:
print(fmts % ('Old Objects', rep.FOIDS, rep.FBYTES / 1024.0,
rep.FBYTES * 100.0 / rep.DBYTES,
rep.FBYTES * 1.0 / rep.FOIDS))
def analyze(path):
fs = FileStorage(path, read_only=1)
fsi = fs.iterator()
report = Report()
for txn in fsi:
analyze_trans(report, txn)
return report
def analyze_trans(report, txn):
report.TIDS += 1
for rec in txn:
analyze_rec(report, rec)
def get_type(record):
try:
unpickled = FakeUnpickler(BytesIO(record.data)).load()
except FakeError as err:
return "%s.%s" % (err.module, err.name)
classinfo = unpickled[0]
if isinstance(classinfo, tuple):
mod, klass = classinfo
return "%s.%s" % (mod, klass)
else:
return str(classinfo)
def analyze_rec(report, record):
oid = record.oid
report.OIDS += 1
if record.data is None:
# No pickle -- aborted version or undo of object creation.
return
try:
size = len(record.data) # Ignores various overhead
report.DBYTES += size
if oid not in report.OIDMAP:
type = get_type(record)
report.OIDMAP[oid] = type
report.USEDMAP[oid] = size
report.COIDS += 1
report.CBYTES += size
else:
type = report.OIDMAP[oid]
fsize = report.USEDMAP[oid]
report.FREEMAP[oid] = report.FREEMAP.get(oid, 0) + fsize
report.USEDMAP[oid] = size
report.FOIDS += 1
report.FBYTES += fsize
report.CBYTES += size - fsize
report.TYPEMAP[type] = report.TYPEMAP.get(type, 0) + 1
report.TYPESIZE[type] = report.TYPESIZE.get(type, 0) + size
except Exception as err:
print(err)
if __name__ == "__main__":
path = sys.argv[1]
report(analyze(path))
@@ -0,0 +1,120 @@
#!/usr/bin/env python
"""Check the consistency of BTrees in a Data.fs
usage: checkbtrees.py data.fs
Try to find all the BTrees in a Data.fs, call their _check() methods,
and run them through BTrees.check.check().
"""
from __future__ import print_function
import ZODB
from ZODB.FileStorage import FileStorage
from BTrees.check import check
# Set of oids we've already visited. Since the object structure is
# a general graph, this is needed to prevent unbounded paths in the
# presence of cycles. It's also helpful in eliminating redundant
# checking when a BTree is pointed to by many objects.
oids_seen = {}
# Append (obj, path) to L if and only if obj is a persistent object
# and we haven't seen it before.
def add_if_new_persistent(L, obj, path):
global oids_seen
getattr(obj, '_', None) # unghostify
if hasattr(obj, '_p_oid'):
oid = obj._p_oid
if oid not in oids_seen:
L.append((obj, path))
oids_seen[oid] = 1
def get_subobjects(obj):
getattr(obj, '_', None) # unghostify
sub = []
try:
attrs = obj.__dict__.items()
except AttributeError:
attrs = ()
for pair in attrs:
sub.append(pair)
# what if it is a mapping?
try:
items = obj.items()
except AttributeError:
items = ()
for k, v in items:
if not isinstance(k, int):
sub.append(("<key>", k))
if not isinstance(v, int):
sub.append(("[%s]" % repr(k), v))
# what if it is a sequence?
i = 0
while 1:
try:
elt = obj[i]
except:
break
sub.append(("[%d]" % i, elt))
i += 1
return sub
def main(fname=None):
if fname is None:
import sys
try:
fname, = sys.argv[1:]
except:
print(__doc__)
sys.exit(2)
fs = FileStorage(fname, read_only=1)
cn = ZODB.DB(fs).open()
rt = cn.root()
todo = []
add_if_new_persistent(todo, rt, '')
found = 0
while todo:
obj, path = todo.pop(0)
found += 1
if not path:
print("<root>", repr(obj))
else:
print(path, repr(obj))
mod = str(obj.__class__.__module__)
if mod.startswith("BTrees"):
if hasattr(obj, "_check"):
try:
obj._check()
except AssertionError as msg:
print("*" * 60)
print(msg)
print("*" * 60)
try:
check(obj)
except AssertionError as msg:
print("*" * 60)
print(msg)
print("*" * 60)
if found % 100 == 0:
cn.cacheMinimize()
for k, v in get_subobjects(obj):
if k.startswith('['):
# getitem
newpath = "%s%s" % (path, k)
else:
newpath = "%s.%s" % (path, k)
add_if_new_persistent(todo, v, newpath)
print("total", len(fs._index), "found", found)
if __name__ == "__main__":
main()
@@ -0,0 +1,79 @@
#!/usr/bin/python
##############################################################################
#
# Copyright (c) 2004 Zope Foundation 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
#
##############################################################################
"""FileStorage oid-tracer.
usage: fsoids.py [-f oid_file] Data.fs [oid]...
Display information about all occurrences of specified oids in a FileStorage.
This is meant for heavy debugging.
This includes all revisions of the oids, all objects referenced by the
oids, and all revisions of all objects referring to the oids.
If specified, oid_file is an input text file, containing one oid per
line. oids are specified as integers, in any of Python's integer
notations (typically like 0x341a). One or more oids can also be specified
on the command line.
The output is grouped by oid, from smallest to largest, and sub-grouped
by transaction, from oldest to newest.
This will not alter the FileStorage, but running against a live FileStorage
is not recommended (spurious error messages may result).
See testfsoids.py for a tutorial doctest.
"""
from __future__ import print_function
import sys
from ZODB.FileStorage.fsoids import Tracer
def usage():
print(__doc__)
def main():
import getopt
try:
opts, args = getopt.getopt(sys.argv[1:], 'f:')
if not args:
usage()
raise ValueError("Must specify a FileStorage")
path = None
for k, v in opts:
if k == '-f':
path = v
except (getopt.error, ValueError):
usage()
raise
c = Tracer(args[0])
for oid in args[1:]:
as_int = int(oid, 0) # 0 == auto-detect base
c.register_oids(as_int)
if path is not None:
for line in open(path):
as_int = int(line, 0)
c.register_oids(as_int)
if not c.oids:
raise ValueError("no oids specified")
c.run()
c.report()
if __name__ == "__main__":
main()
@@ -0,0 +1,154 @@
#!/usr/bin/env python
##############################################################################
#
# Copyright (c) 2002 Zope Foundation 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
#
##############################################################################
"""Check FileStorage for dangling references.
usage: fsrefs.py [-v] data.fs
fsrefs.py checks object sanity by trying to load the current revision of
every object O in the database, and also verifies that every object
directly reachable from each such O exists in the database.
It's hard to explain exactly what it does because it relies on undocumented
features in Python's cPickle module: many of the crucial steps of loading
an object are taken, but application objects aren't actually created. This
saves a lot of time, and allows fsrefs to be run even if the code
implementing the object classes isn't available.
A read-only connection to the specified FileStorage is made, but it is not
recommended to run fsrefs against a live FileStorage. Because a live
FileStorage is mutating while fsrefs runs, it's not possible for fsrefs to
get a wholly consistent view of the database across the entire time fsrefs
is running; spurious error messages may result.
fsrefs doesn't normally produce any output. If an object fails to load, the
oid of the object is given in a message saying so, and if -v was specified
then the traceback corresponding to the load failure is also displayed
(this is the only effect of the -v flag).
Three other kinds of errors are also detected, when an object O loads OK,
and directly refers to a persistent object P but there's a problem with P:
- If P doesn't exist in the database, a message saying so is displayed.
The unsatisifiable reference to P is often called a "dangling
reference"; P is called "missing" in the error output.
- If the current state of the database is such that P's creation has
been undone, then P can't be loaded either. This is also a kind of
dangling reference, but is identified as "object creation was undone".
- If P can't be loaded (but does exist in the database), a message saying
that O refers to an object that can't be loaded is displayed.
fsrefs also (indirectly) checks that the .index file is sane, because
fsrefs uses the index to get its idea of what constitutes "all the objects
in the database".
Note these limitations: because fsrefs only looks at the current revision
of objects, it does not attempt to load objects in versions, or non-current
revisions of objects; therefore fsrefs cannot find problems in versions or
in non-current revisions.
"""
from __future__ import print_function
import traceback
from ZODB.FileStorage import FileStorage
from ZODB.TimeStamp import TimeStamp
from ZODB.utils import u64, oid_repr, get_pickle_metadata, load_current
from ZODB.serialize import get_refs
from ZODB.POSException import POSKeyError
# There's a problem with oid. 'data' is its pickle, and 'serial' its
# serial number. 'missing' is a list of (oid, class, reason) triples,
# explaining what the problem(s) is(are).
def report(oid, data, serial, missing):
from_mod, from_class = get_pickle_metadata(data)
if len(missing) > 1:
plural = "s"
else:
plural = ""
ts = TimeStamp(serial)
print("oid %s %s.%s" % (hex(u64(oid)), from_mod, from_class))
print("last updated: %s, tid=%s" % (ts, hex(u64(serial))))
print("refers to invalid object%s:" % plural)
for oid, info, reason in missing:
if isinstance(info, tuple):
description = "%s.%s" % info
else:
description = str(info)
print("\toid %s %s: %r" % (oid_repr(oid), reason, description))
print()
def main(path=None):
verbose = 0
if path is None:
import sys
import getopt
opts, args = getopt.getopt(sys.argv[1:], "v")
for k, v in opts:
if k == "-v":
verbose += 1
path, = args
fs = FileStorage(path, read_only=1)
# Set of oids in the index that failed to load due to POSKeyError.
# This is what happens if undo is applied to the transaction creating
# the object (the oid is still in the index, but its current data
# record has a backpointer of 0, and POSKeyError is raised then
# because of that backpointer).
undone = {}
# Set of oids that were present in the index but failed to load.
# This does not include oids in undone.
noload = {}
for oid in fs._index.keys():
try:
data, serial = load_current(fs, oid)
except (KeyboardInterrupt, SystemExit):
raise
except POSKeyError:
undone[oid] = 1
except:
if verbose:
traceback.print_exc()
noload[oid] = 1
inactive = noload.copy()
inactive.update(undone)
for oid in fs._index.keys():
if oid in inactive:
continue
data, serial = load_current(fs, oid)
refs = get_refs(data)
missing = [] # contains 3-tuples of oid, klass-metadata, reason
for ref, klass in refs:
if klass is None:
klass = '<unknown>'
if ref not in fs._index:
missing.append((ref, klass, "missing"))
if ref in noload:
missing.append((ref, klass, "failed to load"))
if ref in undone:
missing.append((ref, klass, "object creation was undone"))
if missing:
report(oid, data, serial, missing)
if __name__ == "__main__":
main()
@@ -0,0 +1,203 @@
#!/usr/bin/env python2
"""Print details statistics from fsdump output."""
from __future__ import print_function
import re
import sys
import six
from six.moves import filter
rx_txn = re.compile("tid=([0-9a-f]+).*size=(\d+)")
rx_data = re.compile("oid=([0-9a-f]+) class=(\S+) size=(\d+)")
def sort_byhsize(seq, reverse=False):
L = [(v.size(), k, v) for k, v in seq]
L.sort()
if reverse:
L.reverse()
return [(k, v) for n, k, v in L]
class Histogram(dict):
def add(self, size):
self[size] = self.get(size, 0) + 1
def size(self):
return sum(six.itervalues(self))
def mean(self):
product = sum([k * v for k, v in six.iteritems(self)])
return product / self.size()
def median(self):
# close enough?
n = self.size() / 2
L = self.keys()
L.sort()
L.reverse()
while 1:
k = L.pop()
if self[k] > n:
return k
n -= self[k]
def mode(self):
mode = 0
value = 0
for k, v in six.iteritems(self):
if v > value:
value = v
mode = k
return mode
def make_bins(self, binsize):
maxkey = max(six.iterkeys(self))
self.binsize = binsize
self.bins = [0] * (1 + maxkey / binsize)
for k, v in six.iteritems(self):
b = k / binsize
self.bins[b] += v
def report(self, name, binsize=50, usebins=False, gaps=True, skip=True):
if usebins:
# Use existing bins with whatever size they have
binsize = self.binsize
else:
# Make new bins
self.make_bins(binsize)
maxval = max(self.bins)
# Print up to 40 dots for a value
dot = max(maxval / 40, 1)
tot = sum(self.bins)
print(name)
print("Total", tot, end=' ')
print("Median", self.median(), end=' ')
print("Mean", self.mean(), end=' ')
print("Mode", self.mode(), end=' ')
print("Max", max(self))
print("One * represents", dot)
gap = False
cum = 0
for i, n in enumerate(self.bins):
if gaps and (not n or (skip and not n / dot)):
if not gap:
print(" ...")
gap = True
continue
gap = False
p = 100 * n / tot
cum += n
pc = 100 * cum / tot
print("%6d %6d %3d%% %3d%% %s" % (
i * binsize, n, p, pc, "*" * (n / dot)))
print()
def class_detail(class_size):
# summary of classes
fmt = "%5s %6s %6s %6s %-50.50s"
labels = ["num", "median", "mean", "mode", "class"]
print(fmt % tuple(labels))
print(fmt % tuple(["-" * len(s) for s in labels]))
for klass, h in sort_byhsize(six.iteritems(class_size)):
print(fmt % (h.size(), h.median(), h.mean(), h.mode(), klass))
print()
# per class details
for klass, h in sort_byhsize(six.iteritems(class_size), reverse=True):
h.make_bins(50)
if len(filter(None, h.bins)) == 1:
continue
h.report("Object size for %s" % klass, usebins=True)
def revision_detail(lifetimes, classes):
# Report per-class details for any object modified more than once
for name, oids in six.iteritems(classes):
h = Histogram()
keep = False
for oid in dict.fromkeys(oids, 1):
L = lifetimes.get(oid)
n = len(L)
h.add(n)
if n > 1:
keep = True
if keep:
h.report("Number of revisions for %s" % name, binsize=10)
def main(path=None):
if path is None:
path = sys.argv[1]
txn_objects = Histogram() # histogram of txn size in objects
txn_bytes = Histogram() # histogram of txn size in bytes
obj_size = Histogram() # histogram of object size
n_updates = Histogram() # oid -> num updates
n_classes = Histogram() # class -> num objects
lifetimes = {} # oid -> list of tids
class_size = {} # class -> histogram of object size
classes = {} # class -> list of oids
MAX = 0
objects = 0
tid = None
f = open(path, "rb")
for i, line in enumerate(f):
if MAX and i > MAX:
break
if line.startswith(" data"):
m = rx_data.search(line)
if not m:
continue
oid, klass, size = m.groups()
size = int(size)
obj_size.add(size)
n_updates.add(oid)
n_classes.add(klass)
h = class_size.get(klass)
if h is None:
h = class_size[klass] = Histogram()
h.add(size)
L = lifetimes.setdefault(oid, [])
L.append(tid)
L = classes.setdefault(klass, [])
L.append(oid)
objects += 1
elif line.startswith("Trans"):
if tid is not None:
txn_objects.add(objects)
m = rx_txn.search(line)
if not m:
continue
tid, size = m.groups()
size = int(size)
objects = 0
txn_bytes.add(size)
f.close()
print("Summary: %d txns, %d objects, %d revisions" % (
txn_objects.size(), len(n_updates), n_updates.size()))
print()
txn_bytes.report("Transaction size (bytes)", binsize=1024)
txn_objects.report("Transaction size (objects)", binsize=10)
obj_size.report("Object size", binsize=128)
# object lifetime info
h = Histogram()
for k, v in lifetimes.items():
h.add(len(v))
h.report("Number of revisions", binsize=10, skip=False)
# details about revisions
revision_detail(lifetimes, classes)
class_detail(class_size)
if __name__ == "__main__":
main()
@@ -0,0 +1,55 @@
#!/usr/bin/env python
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""Tool to dump the last few transactions from a FileStorage."""
from __future__ import print_function
from ZODB.fstools import prev_txn
import binascii
import getopt
import sys
try:
from hashlib import sha1
except ImportError:
from sha import sha as sha1
def main(path, ntxn):
with open(path, "rb") as f:
f.seek(0, 2)
th = prev_txn(f)
i = ntxn
while th and i > 0:
hash = sha1(th.get_raw_data()).digest()
l = len(str(th.get_timestamp())) + 1
th.read_meta()
print("%s: hash=%s" % (th.get_timestamp(),
binascii.hexlify(hash).decode()))
print(("user=%r description=%r length=%d offset=%d (+%d)"
% (th.user, th.descr, th.length, th.get_offset(), len(th))))
print()
th = th.prev_txn()
i -= 1
def Main():
ntxn = 10
opts, args = getopt.getopt(sys.argv[1:], "n:")
path, = args
for k, v in opts:
if k == '-n':
ntxn = int(v)
main(path, ntxn)
if __name__ == "__main__":
Main()
@@ -0,0 +1,225 @@
#!/usr/bin/env python
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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
#
##############################################################################
"""Simple consistency checker for FileStorage.
usage: fstest.py [-v] data.fs
The fstest tool will scan all the data in a FileStorage and report an
error if it finds any corrupt transaction data. The tool will print a
message when the first error is detected, then exit.
The tool accepts one or more -v arguments. If a single -v is used, it
will print a line of text for each transaction record it encounters.
If two -v arguments are used, it will also print a line of text for
each object. The objects for a transaction will be printed before the
transaction itself.
Note: It does not check the consistency of the object pickles. It is
possible for the damage to occur only in the part of the file that
stores object pickles. Those errors will go undetected.
"""
from __future__ import print_function
# The implementation is based closely on the read_index() function in
# ZODB.FileStorage. If anything about the FileStorage layout changes,
# this file will need to be udpated.
import binascii
import struct
import sys
from ZODB._compat import FILESTORAGE_MAGIC
class FormatError(ValueError):
"""There is a problem with the format of the FileStorage."""
class Status(object):
checkpoint = b'c'
undone = b'u'
packed_version = FILESTORAGE_MAGIC
TREC_HDR_LEN = 23
DREC_HDR_LEN = 42
VERBOSE = 0
def hexify(s):
r"""Format an 8-bit string as hex
>>> hexify(b'\x00\xff\xaa\xcc')
'0x00ffaacc'
"""
return '0x' + binascii.hexlify(s).decode()
def chatter(msg, level=1):
if VERBOSE >= level:
sys.stdout.write(msg)
def U64(v):
"""Unpack an 8-byte string as a 64-bit long"""
h, l = struct.unpack(">II", v)
if h:
return (h << 32) + l
else:
return l
def check(path):
with open(path, 'rb') as file:
file.seek(0, 2)
file_size = file.tell()
if file_size == 0:
raise FormatError("empty file")
file.seek(0)
if file.read(4) != packed_version:
raise FormatError("invalid file header")
pos = 4
tid = b'\000' * 8 # lowest possible tid to start
i = 0
while pos:
_pos = pos
pos, tid = check_trec(path, file, pos, tid, file_size)
if tid is not None:
chatter("%10d: transaction tid %s #%d \n" %
(_pos, hexify(tid), i))
i = i + 1
def check_trec(path, file, pos, ltid, file_size):
"""Read an individual transaction record from file.
Returns the pos of the next transaction and the transaction id.
It also leaves the file pointer set to pos. The path argument is
used for generating error messages.
"""
h = file.read(TREC_HDR_LEN) #XXX must be bytes under Py3k
if not h:
return None, None
if len(h) != TREC_HDR_LEN:
raise FormatError("%s truncated at %s" % (path, pos))
tid, stl, status, ul, dl, el = struct.unpack(">8s8scHHH", h)
tmeta_len = TREC_HDR_LEN + ul + dl + el
if tid <= ltid:
raise FormatError("%s time-stamp reduction at %s: %s <= %s" %
(path, pos, hexify(tid), hexify(ltid)))
ltid = tid
tl = U64(stl) # transaction record length - 8
if pos + tl + 8 > file_size:
raise FormatError("%s truncated possibly because of"
" damaged records at %s" % (path, pos))
if status == Status.checkpoint:
raise FormatError("%s checkpoint flag was not cleared at %s"
% (path, pos))
if status not in b' up':
raise FormatError("%s has invalid status '%s' at %s" %
(path, status, pos))
if tmeta_len > tl:
raise FormatError("%s has an invalid transaction header"
" at %s" % (path, pos))
tpos = pos
tend = tpos + tl
if status != Status.undone:
pos = tpos + tmeta_len
file.read(ul + dl + el) # skip transaction metadata
i = 0
while pos < tend:
_pos = pos
pos, oid = check_drec(path, file, pos, tpos, tid)
if pos > tend:
raise FormatError("%s has data records that extend beyond"
" the transaction record; end at %s" %
(path, pos))
chatter("%10d: object oid %s #%d\n" % (_pos, hexify(oid), i),
level=2)
i = i + 1
file.seek(tend)
rtl = file.read(8)
if rtl != stl:
raise FormatError("%s has inconsistent transaction length"
" for undone transaction at %s" % (path, pos))
pos = tend + 8
return pos, tid
def check_drec(path, file, pos, tpos, tid):
"""Check a data record for the current transaction record"""
h = file.read(DREC_HDR_LEN)
if len(h) != DREC_HDR_LEN:
raise FormatError("%s truncated at %s" % (path, pos))
oid, serial, _prev, _tloc, vlen, _plen = (
struct.unpack(">8s8s8s8sH8s", h))
prev = U64(_prev)
tloc = U64(_tloc)
plen = U64(_plen)
dlen = DREC_HDR_LEN + (plen or 8)
if vlen:
dlen = dlen + 16 + vlen
file.seek(8, 1)
pv = U64(file.read(8))
file.seek(vlen, 1) # skip the version data
if tloc != tpos:
raise FormatError("%s data record exceeds transaction record "
"at %s: tloc %d != tpos %d" %
(path, pos, tloc, tpos))
pos = pos + dlen
if plen:
file.seek(plen, 1)
else:
file.seek(8, 1)
# _loadBack() ?
return pos, oid
def usage():
sys.exit(__doc__)
def main(args=None):
if args is None:
args = sys.argv[1:]
import getopt
global VERBOSE
try:
opts, args = getopt.getopt(args, 'v')
if len(args) != 1:
raise ValueError("expected one argument")
for k, v in opts:
if k == '-v':
VERBOSE = VERBOSE + 1
except (getopt.error, ValueError):
usage()
try:
check(args[0])
except FormatError as msg:
sys.exit(msg)
chatter("no errors detected")
if __name__ == "__main__":
main()
@@ -0,0 +1,177 @@
"""Verify that fstest.py can find errors.
Note: To run this test script fstest.py must be on your PYTHONPATH.
"""
from cStringIO import StringIO
import re
import struct
import unittest
import ZODB.tests.util
import fstest
from fstest import FormatError, U64
class TestCorruptedFS(ZODB.tests.util.TestCase):
f = open('test-checker.fs', 'rb')
datafs = f.read()
f.close()
del f
def setUp(self):
ZODB.tests.util.TestCase.setUp(self)
self._temp = 'Data.fs'
self._file = open(self._temp, 'wb')
def tearDown(self):
if not self._file.closed:
self._file.close()
ZODB.tests.util.TestCase.tearDown(self)
def noError(self):
if not self._file.closed:
self._file.close()
fstest.check(self._temp)
def detectsError(self, rx):
if not self._file.closed:
self._file.close()
try:
fstest.check(self._temp)
except FormatError as msg:
mo = re.search(rx, str(msg))
self.assertFalse(mo is None, "unexpected error: %s" % msg)
else:
self.fail("fstest did not detect corruption")
def getHeader(self):
buf = self._datafs.read(16)
if not buf:
return 0, ''
tl = U64(buf[8:])
return tl, buf
def copyTransactions(self, n):
"""Copy at most n transactions from the good data"""
f = self._datafs = StringIO(self.datafs)
self._file.write(f.read(4))
for i in range(n):
tl, data = self.getHeader()
if not tl:
return
self._file.write(data)
rec = f.read(tl - 8)
self._file.write(rec)
def testGood(self):
self._file.write(self.datafs)
self.noError()
def testTwoTransactions(self):
self.copyTransactions(2)
self.noError()
def testEmptyFile(self):
self.detectsError("empty file")
def testInvalidHeader(self):
self._file.write('SF12')
self.detectsError("invalid file header")
def testTruncatedTransaction(self):
self._file.write(self.datafs[:4+22])
self.detectsError("truncated")
def testCheckpointFlag(self):
self.copyTransactions(2)
tl, data = self.getHeader()
assert tl > 0, "ran out of good transaction data"
self._file.write(data)
self._file.write('c')
self._file.write(self._datafs.read(tl - 9))
self.detectsError("checkpoint flag")
def testInvalidStatus(self):
self.copyTransactions(2)
tl, data = self.getHeader()
assert tl > 0, "ran out of good transaction data"
self._file.write(data)
self._file.write('Z')
self._file.write(self._datafs.read(tl - 9))
self.detectsError("invalid status")
def testTruncatedRecord(self):
self.copyTransactions(3)
tl, data = self.getHeader()
assert tl > 0, "ran out of good transaction data"
self._file.write(data)
buf = self._datafs.read(tl / 2)
self._file.write(buf)
self.detectsError("truncated possibly")
def testBadLength(self):
self.copyTransactions(2)
tl, data = self.getHeader()
assert tl > 0, "ran out of good transaction data"
self._file.write(data)
buf = self._datafs.read(tl - 8)
self._file.write(buf[0])
assert tl <= 1<<16, "can't use this transaction for this test"
self._file.write("\777\777")
self._file.write(buf[3:])
self.detectsError("invalid transaction header")
def testDecreasingTimestamps(self):
self.copyTransactions(0)
tl, data = self.getHeader()
buf = self._datafs.read(tl - 8)
t1 = data + buf
tl, data = self.getHeader()
buf = self._datafs.read(tl - 8)
t2 = data + buf
self._file.write(t2[:8] + t1[8:])
self._file.write(t1[:8] + t2[8:])
self.detectsError("time-stamp")
def testTruncatedData(self):
# This test must re-write the transaction header length in
# order to trigger the error in check_drec(). If it doesn't,
# the truncated data record would also caught a truncated
# transaction record.
self.copyTransactions(1)
tl, data = self.getHeader()
pos = self._file.tell()
self._file.write(data)
buf = self._datafs.read(tl - 8)
hdr = buf[:15]
ul, dl, el = struct.unpack(">HHH", hdr[-6:])
self._file.write(buf[:15 + ul + dl + el])
data = buf[15 + ul + dl + el:]
self._file.write(data[:24])
self._file.seek(pos + 8, 0)
newlen = struct.pack(">II", 0, tl - (len(data) - 24))
self._file.write(newlen)
self.detectsError("truncated at")
def testBadDataLength(self):
self.copyTransactions(1)
tl, data = self.getHeader()
self._file.write(data)
buf = self._datafs.read(tl - 8)
hdr = buf[:7]
# write the transaction meta data
ul, dl, el = struct.unpack(">HHH", hdr[-6:])
self._file.write(buf[:7 + ul + dl + el])
# write the first part of the data header
data = buf[7 + ul + dl + el:]
self._file.write(data[:24])
self._file.write("\000" * 4 + "\077" + "\000" * 3)
self._file.write(data[32:])
self.detectsError("record exceeds transaction")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,358 @@
#!/usr/bin/env python
##############################################################################
#
# Copyright (c) 2001, 2002, 2003 Zope Foundation 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 script to gather statistics while doing a storage migration.
This is very similar to a standard storage's copyTransactionsFrom() method,
except that it's geared to run as a script, and it collects useful pieces of
information as it's working. This script can be used to stress test a storage
since it blasts transactions at it as fast as possible. You can get a good
sense of the performance of a storage by running this script.
Actually it just counts the size of pickles in the transaction via the
iterator protocol, so storage overheads aren't counted.
Usage: %(PROGRAM)s [options] [source-storage-args] [destination-storage-args]
Options:
-S sourcetype
--stype=sourcetype
This is the name of a recognized type for the source database. Use -T
to print out the known types. Defaults to "file".
-D desttype
--dtype=desttype
This is the name of the recognized type for the destination database.
Use -T to print out the known types. Defaults to "file".
-o filename
--output=filename
Print results in filename, otherwise stdout.
-m txncount
--max=txncount
Stop after committing txncount transactions.
-k txncount
--skip=txncount
Skip the first txncount transactions.
-p/--profile
Turn on specialized profiling.
-t/--timestamps
Print tids as timestamps.
-T/--storage_types
Print all the recognized storage types and exit.
-v/--verbose
Turns on verbose output. Multiple -v options increase the verbosity.
-h/--help
Print this message and exit.
Positional arguments:
source-storage-args:
Semicolon separated list of arguments for the source storage, as
key=val pairs. E.g. "file_name=Data.fs;read_only=1"
destination-storage-args:
Comma separated list of arguments for the source storage, as key=val
pairs. E.g. "name=full;frequency=3600"
"""
from __future__ import print_function
import re
import sys
import time
import getopt
import marshal
import profile
from ZODB import utils
from ZODB import StorageTypes
from ZODB.TimeStamp import TimeStamp
PROGRAM = sys.argv[0]
ZERO = '\0'*8
def usage(code, msg=''):
print(__doc__ % globals(), file=sys.stderr)
if msg:
print(msg, file=sys.stderr)
sys.exit(code)
def error(code, msg):
print(msg, file=sys.stderr)
print("use --help for usage message")
sys.exit(code)
def main():
try:
opts, args = getopt.getopt(
sys.argv[1:],
'hvo:pm:k:D:S:Tt',
['help', 'verbose',
'output=', 'profile', 'storage_types',
'max=', 'skip=', 'dtype=', 'stype=', 'timestamps'])
except getopt.error as msg:
error(2, msg)
class Options(object):
stype = 'FileStorage'
dtype = 'FileStorage'
verbose = 0
outfile = None
profilep = False
maxtxn = -1
skiptxn = -1
timestamps = False
options = Options()
for opt, arg in opts:
if opt in ('-h', '--help'):
usage(0)
elif opt in ('-v', '--verbose'):
options.verbose += 1
elif opt in ('-T', '--storage_types'):
print_types()
sys.exit(0)
elif opt in ('-S', '--stype'):
options.stype = arg
elif opt in ('-D', '--dtype'):
options.dtype = arg
elif opt in ('-o', '--output'):
options.outfile = arg
elif opt in ('-p', '--profile'):
options.profilep = True
elif opt in ('-m', '--max'):
options.maxtxn = int(arg)
elif opt in ('-k', '--skip'):
options.skiptxn = int(arg)
elif opt in ('-t', '--timestamps'):
options.timestamps = True
if len(args) > 2:
error(2, "too many arguments")
srckws = {}
if len(args) > 0:
srcargs = args[0]
for kv in re.split(r';\s*', srcargs):
key, val = kv.split('=')
srckws[key] = val
destkws = {}
if len(args) > 1:
destargs = args[1]
for kv in re.split(r';\s*', destargs):
key, val = kv.split('=')
destkws[key] = val
if options.stype not in StorageTypes.storage_types.keys():
usage(2, 'Source database type must be provided')
if options.dtype not in StorageTypes.storage_types.keys():
usage(2, 'Destination database type must be provided')
# Open the output file
if options.outfile is None:
options.outfp = sys.stdout
options.outclosep = False
else:
options.outfp = open(options.outfile, 'w')
options.outclosep = True
if options.verbose > 0:
print('Opening source database...')
modname, sconv = StorageTypes.storage_types[options.stype]
kw = sconv(**srckws)
__import__(modname)
sclass = getattr(sys.modules[modname], options.stype)
srcdb = sclass(**kw)
if options.verbose > 0:
print('Opening destination database...')
modname, dconv = StorageTypes.storage_types[options.dtype]
kw = dconv(**destkws)
__import__(modname)
dclass = getattr(sys.modules[modname], options.dtype)
dstdb = dclass(**kw)
try:
t0 = time.time()
doit(srcdb, dstdb, options)
t1 = time.time()
if options.verbose > 0:
print('Migration time: %8.3f' % (t1-t0))
finally:
# Done
srcdb.close()
dstdb.close()
if options.outclosep:
options.outfp.close()
def doit(srcdb, dstdb, options):
outfp = options.outfp
profilep = options.profilep
verbose = options.verbose
# some global information
largest_pickle = 0
largest_txn_in_size = 0
largest_txn_in_objects = 0
total_pickle_size = 0
total_object_count = 0
# Ripped from BaseStorage.copyTransactionsFrom()
ts = None
ok = True
prevrevids = {}
counter = 0
skipper = 0
if options.timestamps:
print("%4s. %26s %6s %8s %5s %5s %5s %5s %5s" % (
"NUM", "TID AS TIMESTAMP", "OBJS", "BYTES",
# Does anybody know what these times mean?
"t4-t0", "t1-t0", "t2-t1", "t3-t2", "t4-t3"))
else:
print("%4s. %20s %6s %8s %6s %6s %6s %6s %6s" % (
"NUM", "TRANSACTION ID", "OBJS", "BYTES",
# Does anybody know what these times mean?
"t4-t0", "t1-t0", "t2-t1", "t3-t2", "t4-t3"))
for txn in srcdb.iterator():
skipper += 1
if skipper <= options.skiptxn:
continue
counter += 1
if counter > options.maxtxn >= 0:
break
tid = txn.tid
if ts is None:
ts = TimeStamp(tid)
else:
t = TimeStamp(tid)
if t <= ts:
if ok:
print((
'Time stamps are out of order %s, %s' % (ts, t)), file=sys.stderr)
ok = False
ts = t.laterThan(ts)
tid = ts.raw()
else:
ts = t
if not ok:
print((
'Time stamps are back in order %s' % t), file=sys.stderr)
ok = True
if verbose > 1:
print(ts)
prof = None
if profilep and (counter % 100) == 0:
prof = profile.Profile()
objects = 0
size = 0
newrevids = RevidAccumulator()
t0 = time.time()
dstdb.tpc_begin(txn, tid, txn.status)
t1 = time.time()
for r in txn:
oid = r.oid
objects += 1
thissize = len(r.data)
size += thissize
if thissize > largest_pickle:
largest_pickle = thissize
if verbose > 1:
if not r.version:
vstr = 'norev'
else:
vstr = r.version
print(utils.U64(oid), vstr, len(r.data))
oldrevid = prevrevids.get(oid, ZERO)
result = dstdb.store(oid, oldrevid, r.data, r.version, txn)
newrevids.store(oid, result)
t2 = time.time()
result = dstdb.tpc_vote(txn)
t3 = time.time()
newrevids.tpc_vote(result)
prevrevids.update(newrevids.get_dict())
# Profile every 100 transactions
if prof:
prof.runcall(dstdb.tpc_finish, txn)
else:
dstdb.tpc_finish(txn)
t4 = time.time()
# record the results
if objects > largest_txn_in_objects:
largest_txn_in_objects = objects
if size > largest_txn_in_size:
largest_txn_in_size = size
if options.timestamps:
tidstr = str(TimeStamp(tid))
format = "%4d. %26s %6d %8d %5.3f %5.3f %5.3f %5.3f %5.3f"
else:
tidstr = utils.U64(tid)
format = "%4d. %20s %6d %8d %6.4f %6.4f %6.4f %6.4f %6.4f"
print(format % (skipper, tidstr, objects, size,
t4-t0, t1-t0, t2-t1, t3-t2, t4-t3), file=outfp)
total_pickle_size += size
total_object_count += objects
if prof:
prof.create_stats()
fp = open('profile-%02d.txt' % (counter / 100), 'wb')
marshal.dump(prof.stats, fp)
fp.close()
print("Largest pickle: %8d" % largest_pickle, file=outfp)
print("Largest transaction: %8d" % largest_txn_in_size, file=outfp)
print("Largest object count: %8d" % largest_txn_in_objects, file=outfp)
print("Total pickle size: %14d" % total_pickle_size, file=outfp)
print("Total object count: %8d" % total_object_count, file=outfp)
# helper to deal with differences between old-style store() return and
# new-style store() return that supports ZEO
class RevidAccumulator(object):
def __init__(self):
self.data = {}
def _update_from_list(self, list):
for oid, serial in list:
if not isinstance(serial, str):
raise serial
self.data[oid] = serial
def store(self, oid, result):
if isinstance(result, str):
self.data[oid] = result
elif result is not None:
self._update_from_list(result)
def tpc_vote(self, result):
if result is not None:
self._update_from_list(result)
def get_dict(self):
return self.data
if __name__ == '__main__':
main()
@@ -0,0 +1,81 @@
##############################################################################
#
# Copyright (c) 2008 Zope Foundation 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 script to migrate a blob directory into a different layout.
"""
from __future__ import print_function
import logging
import optparse
import os
import shutil
from ZODB.blob import FilesystemHelper
from ZODB.utils import oid_repr
def link_or_copy(f1, f2):
try:
os.link(f1, f2)
except OSError:
shutil.copy(f1, f2)
# Check if we actually have link
try:
os.link
except AttributeError:
link_or_copy = shutil.copy
def migrate(source, dest, layout):
source_fsh = FilesystemHelper(source)
source_fsh.create()
dest_fsh = FilesystemHelper(dest, layout)
dest_fsh.create()
print("Migrating blob data from `%s` (%s) to `%s` (%s)" % (
source, source_fsh.layout_name, dest, dest_fsh.layout_name))
for oid, path in source_fsh.listOIDs():
dest_path = dest_fsh.getPathForOID(oid, create=True)
files = os.listdir(path)
for file in files:
source_file = os.path.join(path, file)
dest_file = os.path.join(dest_path, file)
link_or_copy(source_file, dest_file)
print("\tOID: %s - %s files " % (oid_repr(oid), len(files)))
def main(source=None, dest=None, layout="bushy"):
usage = "usage: %prog [options] <source> <dest> <layout>"
description = ("Create the new directory <dest> and migrate all blob "
"data <source> to <dest> while using the new <layout> for "
"<dest>")
parser = optparse.OptionParser(usage=usage, description=description)
parser.add_option("-l", "--layout",
default=layout, type='choice',
choices=['bushy', 'lawn'],
help="Define the layout to use for the new directory "
"(bushy or lawn). Default: %default")
options, args = parser.parse_args()
if not len(args) == 2:
parser.error("source and destination must be given")
logging.getLogger().addHandler(logging.StreamHandler())
logging.getLogger().setLevel(0)
source, dest = args
migrate(source, dest, options.layout)
if __name__ == '__main__':
main()
@@ -0,0 +1,126 @@
#!/usr/bin/env python
"""Report on the net size of objects counting subobjects.
usage: netspace.py [-P | -v] data.fs
-P: do a pack first
-v: print info for all objects, even if a traversal path isn't found
"""
from __future__ import print_function
import ZODB
from ZODB.FileStorage import FileStorage
from ZODB.utils import U64, get_pickle_metadata, load_current
from ZODB.serialize import referencesf
from six.moves import filter
def find_paths(root, maxdist):
"""Find Python attribute traversal paths for objects to maxdist distance.
Starting at a root object, traverse attributes up to distance levels
from the root, looking for persistent objects. Return a dict
mapping oids to traversal paths.
TODO: Assumes that the keys of the root are not themselves
persistent objects.
TODO: Doesn't traverse containers.
"""
paths = {}
# Handle the root as a special case because it's a dict
objs = []
for k, v in root.items():
oid = getattr(v, '_p_oid', None)
objs.append((k, v, oid, 0))
for path, obj, oid, dist in objs:
if oid is not None:
paths[oid] = path
if dist < maxdist:
getattr(obj, 'foo', None) # unghostify
try:
items = obj.__dict__.items()
except AttributeError:
continue
for k, v in items:
oid = getattr(v, '_p_oid', None)
objs.append(("%s.%s" % (path, k), v, oid, dist + 1))
return paths
def main(path):
fs = FileStorage(path, read_only=1)
if PACK:
fs.pack()
db = ZODB.DB(fs)
rt = db.open().root()
paths = find_paths(rt, 3)
def total_size(oid):
cache = {}
cache_size = 1000
def _total_size(oid, seen):
v = cache.get(oid)
if v is not None:
return v
data, serialno = load_current(fs, oid)
size = len(data)
for suboid in referencesf(data):
if suboid in seen:
continue
seen[suboid] = 1
size += _total_size(suboid, seen)
cache[oid] = size
if len(cache) == cache_size:
cache.popitem()
return size
return _total_size(oid, {})
keys = fs._index.keys()
keys.sort()
keys.reverse()
if not VERBOSE:
# If not running verbosely, don't print an entry for an object
# unless it has an entry in paths.
keys = filter(paths.has_key, keys)
fmt = "%8s %5d %8d %s %s.%s"
for oid in keys:
data, serialno = load_current(fs, oid)
mod, klass = get_pickle_metadata(data)
refs = referencesf(data)
path = paths.get(oid, '-')
print(fmt % (U64(oid), len(data), total_size(oid), path, mod, klass))
def Main():
import sys
import getopt
global PACK
global VERBOSE
PACK = 0
VERBOSE = 0
try:
opts, args = getopt.getopt(sys.argv[1:], 'Pv')
path, = args
except getopt.error as err:
print(err)
print(__doc__)
sys.exit(2)
except ValueError:
print("expected one argument, got", len(args))
print(__doc__)
sys.exit(2)
for o, v in opts:
if o == '-P':
PACK = 1
if o == '-v':
VERBOSE += 1
main(path)
if __name__ == "__main__":
Main()
@@ -0,0 +1,27 @@
##############################################################################
#
# Copyright (c) 2005 Zope Foundation 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.
#
##############################################################################
"""Compute a table of object id referrers
$Id$
"""
from ZODB.serialize import referencesf
def referrers(storage):
result = {}
for transaction in storage.iterator():
for record in transaction:
for oid in referencesf(record.data):
result.setdefault(oid, []).append((record.oid, record.tid))
return result
@@ -0,0 +1,747 @@
#!/usr/bin/env python
# repozo.py -- incremental and full backups of a Data.fs file.
#
# Originally written by Anthony Baxter
# Significantly modified by Barry Warsaw
"""repozo.py -- incremental and full backups of a Data.fs file and index.
Usage: %(program)s [options]
Where:
Exactly one of -B, -R, or -V must be specified:
-B / --backup
Backup current ZODB file.
-R / --recover
Restore a ZODB file from a backup.
-V / --verify
Verify backup integrity.
-v / --verbose
Verbose mode.
-h / --help
Print this text and exit.
-r dir
--repository=dir
Repository directory containing the backup files. This argument
is required. The directory must already exist. You should not
edit the files in this directory, or add your own files to it.
Options for -B/--backup:
-f file
--file=file
Source Data.fs file. This argument is required.
-F / --full
Force a full backup. By default, an incremental backup is made
if possible (e.g., if a pack has occurred since the last
incremental backup, a full backup is necessary).
-Q / --quick
Verify via md5 checksum only the last incremental written. This
significantly reduces the disk i/o at the (theoretical) cost of
inconsistency. This is a probabilistic way of determining whether
a full backup is necessary.
-z / --gzip
Compress with gzip the backup files. Uses the default zlib
compression level. By default, gzip compression is not used.
-k / --kill-old-on-full
If a full backup is created, remove any prior full or incremental
backup files (and associated metadata files) from the repository
directory.
Options for -R/--recover:
-D str
--date=str
Recover state as of this date. Specify UTC (not local) time.
yyyy-mm-dd[-hh[-mm[-ss]]]
By default, current time is used.
-o filename
--output=filename
Write recovered ZODB to given file. By default, the file is
written to stdout.
Note: for the stdout case, the index file will **not** be restored
automatically.
Options for -V/--verify:
-Q / --quick
Verify file sizes only (skip md5 checksums).
"""
from __future__ import print_function
import os
import shutil
import sys
from six.moves import filter
from hashlib import md5
import gzip
import time
import errno
import getopt
from ZODB.FileStorage import FileStorage
program = sys.argv[0]
BACKUP = 1
RECOVER = 2
VERIFY = 3
COMMASPACE = ', '
READCHUNK = 16 * 1024
VERBOSE = False
class WouldOverwriteFiles(Exception):
pass
class NoFiles(Exception):
pass
class _GzipCloser(object):
def __init__(self, fqn, mode):
self._opened = gzip.open(fqn, mode)
def __enter__(self):
return self._opened
def __exit__(self, exc_type, exc_value, traceback):
self._opened.close()
def usage(code, msg=''):
outfp = sys.stderr
if code == 0:
outfp = sys.stdout
print(__doc__ % globals(), file=outfp)
if msg:
print(msg, file=outfp)
sys.exit(code)
def log(msg, *args):
if VERBOSE:
# Use stderr here so that -v flag works with -R and no -o
print(msg % args, file=sys.stderr)
def error(msg, *args):
print(msg % args, file=sys.stderr)
def parseargs(argv):
global VERBOSE
try:
opts, args = getopt.getopt(argv, 'BRVvhr:f:FQzkD:o:',
['backup',
'recover',
'verify',
'verbose',
'help',
'repository=',
'file=',
'full',
'quick',
'gzip',
'kill-old-on-full',
'date=',
'output=',
])
except getopt.error as msg:
usage(1, msg)
class Options(object):
mode = None # BACKUP, RECOVER or VERIFY
file = None # name of input Data.fs file
repository = None # name of directory holding backups
full = False # True forces full backup
date = None # -D argument, if any
output = None # where to write recovered data; None = stdout
quick = False # -Q flag state
gzip = False # -z flag state
killold = False # -k flag state
options = Options()
for opt, arg in opts:
if opt in ('-h', '--help'):
usage(0)
elif opt in ('-v', '--verbose'):
VERBOSE = True
elif opt in ('-R', '--recover'):
if options.mode is not None:
usage(1, '-B, -R, and -V are mutually exclusive')
options.mode = RECOVER
elif opt in ('-B', '--backup'):
if options.mode is not None:
usage(1, '-B, -R, and -V are mutually exclusive')
options.mode = BACKUP
elif opt in ('-V', '--verify'):
if options.mode is not None:
usage(1, '-B, -R, and -V are mutually exclusive')
options.mode = VERIFY
elif opt in ('-Q', '--quick'):
options.quick = True
elif opt in ('-f', '--file'):
options.file = arg
elif opt in ('-r', '--repository'):
options.repository = arg
elif opt in ('-F', '--full'):
options.full = True
elif opt in ('-D', '--date'):
options.date = arg
elif opt in ('-o', '--output'):
options.output = arg
elif opt in ('-z', '--gzip'):
options.gzip = True
elif opt in ('-k', '--kill-old-on-full'):
options.killold = True
else:
assert False, (opt, arg)
# Any other arguments are invalid
if args:
usage(1, 'Invalid arguments: ' + COMMASPACE.join(args))
# Sanity checks
if options.mode is None:
usage(1, 'Either --backup, --recover or --verify is required')
if options.repository is None:
usage(1, '--repository is required')
if options.mode == BACKUP:
if options.date is not None:
log('--date option is ignored in backup mode')
options.date = None
if options.output is not None:
log('--output option is ignored in backup mode')
options.output = None
elif options.mode == RECOVER:
if options.file is not None:
log('--file option is ignored in recover mode')
options.file = None
if options.killold:
log('--kill-old-on-full option is ignored in recover mode')
options.killold = False
else:
assert options.mode == VERIFY
if options.date is not None:
log("--date option is ignored in verify mode")
options.date = None
if options.output is not None:
log('--output option is ignored in verify mode')
options.output = None
if options.full:
log('--full option is ignored in verify mode')
options.full = False
if options.gzip:
log('--gzip option is ignored in verify mode')
options.gzip = False
if options.file is not None:
log('--file option is ignored in verify mode')
options.file = None
if options.killold:
log('--kill-old-on-full option is ignored in verify mode')
options.killold = False
return options
# afile is a Python file object, or created by gzip.open(). The latter
# doesn't have a fileno() method, so to fsync it we need to reach into
# its underlying file object.
def fsync(afile):
afile.flush()
fileobject = getattr(afile, 'fileobj', afile)
os.fsync(fileobject.fileno())
# Read bytes (no more than n, or to EOF if n is None) in chunks from the
# current position in file fp. Pass each chunk as an argument to func().
# Return the total number of bytes read == the total number of bytes
# passed in all to func(). Leaves the file position just after the
# last byte read.
def dofile(func, fp, n=None):
bytesread = 0
while n is None or n > 0:
if n is None:
todo = READCHUNK
else:
todo = min(READCHUNK, n)
data = fp.read(todo)
if not data:
break
func(data)
nread = len(data)
bytesread += nread
if n is not None:
n -= nread
return bytesread
def checksum(fp, n):
# Checksum the first n bytes of the specified file
sum = md5()
def func(data):
sum.update(data)
dofile(func, fp, n)
return sum.hexdigest()
def file_size(fp):
# Compute number of bytes that can be read from fp
def func(data):
pass
return dofile(func, fp, None)
def checksum_and_size(fp):
# Checksum and return it with the size of the file
sum = md5()
def func(data):
sum.update(data)
size = dofile(func, fp, None)
return sum.hexdigest(), size
def copyfile(options, dst, start, n):
# Copy bytes from file src, to file dst, starting at offset start, for n
# length of bytes. For robustness, we first write, flush and fsync
# to a temp file, then rename the temp file at the end.
sum = md5()
ifp = open(options.file, 'rb')
ifp.seek(start)
tempname = os.path.join(os.path.dirname(dst), 'tmp.tmp')
if options.gzip:
ofp = gzip.open(tempname, 'wb')
else:
ofp = open(tempname, 'wb')
def func(data):
sum.update(data)
ofp.write(data)
ndone = dofile(func, ifp, n)
assert ndone == n
ifp.close()
fsync(ofp)
ofp.close()
os.rename(tempname, dst)
return sum.hexdigest()
def concat(files, ofp=None):
# Concatenate a bunch of files from the repository, output to 'ofp' if
# given. Return the number of bytes written and the md5 checksum of the
# bytes.
sum = md5()
def func(data):
sum.update(data)
if ofp:
ofp.write(data)
bytesread = 0
for f in files:
# Auto uncompress
if f.endswith('fsz'):
ifp = gzip.open(f, 'rb')
else:
ifp = open(f, 'rb')
bytesread += dofile(func, ifp)
ifp.close()
if ofp:
ofp.close()
return bytesread, sum.hexdigest()
def gen_filedate(options):
return getattr(options, 'test_now', time.gmtime()[:6])
def gen_filename(options, ext=None, now=None):
if ext is None:
if options.full:
ext = '.fs'
else:
ext = '.deltafs'
if options.gzip:
ext += 'z'
# Hook for testing
if now is None:
now = gen_filedate(options)
t = now + (ext,)
return '%04d-%02d-%02d-%02d-%02d-%02d%s' % t
# Return a list of files needed to reproduce state at time options.date.
# This is a list, in chronological order, of the .fs[z] and .deltafs[z]
# files, from the time of the most recent full backup preceding
# options.date, up to options.date.
import re
is_data_file = re.compile(r'\d{4}(?:-\d\d){5}\.(?:delta)?fsz?$').match
del re
def find_files(options):
when = options.date
if not when:
when = gen_filename(options, ext='')
log('looking for files between last full backup and %s...', when)
# newest file first
all = sorted(
filter(is_data_file, os.listdir(options.repository)), reverse=True)
# Find the last full backup before date, then include all the
# incrementals between that full backup and "when".
needed = []
for fname in all:
root, ext = os.path.splitext(fname)
if root <= when:
needed.append(fname)
if ext in ('.fs', '.fsz'):
break
# Make the file names relative to the repository directory
needed = [os.path.join(options.repository, f) for f in needed]
# Restore back to chronological order
needed.reverse()
if needed:
log('files needed to recover state as of %s:', when)
for f in needed:
log('\t%s', f)
else:
log('no files found')
return needed
# Scan the .dat file corresponding to the last full backup performed.
# Return
#
# filename, startpos, endpos, checksum
#
# of the last incremental. If there is no .dat file, or the .dat file
# is empty, return
#
# None, None, None, None
def scandat(repofiles):
fullfile = repofiles[0]
datfile = os.path.splitext(fullfile)[0] + '.dat'
fn = startpos = endpos = sum = None # assume .dat file missing or empty
try:
fp = open(datfile)
except IOError as e:
if e.errno != errno.ENOENT:
raise
else:
# We only care about the last one.
lines = fp.readlines()
fp.close()
if lines:
fn, startpos, endpos, sum = lines[-1].split()
startpos = int(startpos)
endpos = int(endpos)
return fn, startpos, endpos, sum
def delete_old_backups(options):
# Delete all full backup files except for the most recent full backup file
all = sorted(filter(is_data_file, os.listdir(options.repository)))
deletable = []
full = []
for fname in all:
root, ext = os.path.splitext(fname)
if ext in ('.fs', '.fsz'):
full.append(fname)
if ext in ('.fs', '.fsz', '.deltafs', '.deltafsz'):
deletable.append(fname)
# keep most recent full
if not full:
return
recentfull = full.pop(-1)
deletable.remove(recentfull)
root, ext = os.path.splitext(recentfull)
dat = root + '.dat'
if dat in deletable:
deletable.remove(dat)
index = root + '.index'
if index in deletable:
deletable.remove(index)
for fname in deletable:
log('removing old backup file %s (and .dat / .index)', fname)
root, ext = os.path.splitext(fname)
try:
os.unlink(os.path.join(options.repository, root + '.dat'))
except OSError:
pass
try:
os.unlink(os.path.join(options.repository, root + '.index'))
except OSError:
pass
os.unlink(os.path.join(options.repository, fname))
def do_full_backup(options):
options.full = True
tnow = gen_filedate(options)
dest = os.path.join(options.repository, gen_filename(options, now=tnow))
if os.path.exists(dest):
raise WouldOverwriteFiles('Cannot overwrite existing file: %s' % dest)
# Find the file position of the last completed transaction.
fs = FileStorage(options.file, read_only=True)
# Note that the FileStorage ctor calls read_index() which scans the file
# and returns "the position just after the last valid transaction record".
# getSize() then returns this position, which is exactly what we want,
# because we only want to copy stuff from the beginning of the file to the
# last valid transaction record.
pos = fs.getSize()
# Save the storage index into the repository
index_file = os.path.join(options.repository,
gen_filename(options, '.index', tnow))
log('writing index')
fs._index.save(pos, index_file)
fs.close()
log('writing full backup: %s bytes to %s', pos, dest)
sum = copyfile(options, dest, 0, pos)
# Write the data file for this full backup
datfile = os.path.splitext(dest)[0] + '.dat'
fp = open(datfile, 'w')
print(dest, 0, pos, sum, file=fp)
fp.flush()
os.fsync(fp.fileno())
fp.close()
if options.killold:
delete_old_backups(options)
def do_incremental_backup(options, reposz, repofiles):
options.full = False
tnow = gen_filedate(options)
dest = os.path.join(options.repository, gen_filename(options, now=tnow))
if os.path.exists(dest):
raise WouldOverwriteFiles('Cannot overwrite existing file: %s' % dest)
# Find the file position of the last completed transaction.
fs = FileStorage(options.file, read_only=True)
# Note that the FileStorage ctor calls read_index() which scans the file
# and returns "the position just after the last valid transaction record".
# getSize() then returns this position, which is exactly what we want,
# because we only want to copy stuff from the beginning of the file to the
# last valid transaction record.
pos = fs.getSize()
log('writing index')
index_file = os.path.join(options.repository,
gen_filename(options, '.index', tnow))
fs._index.save(pos, index_file)
fs.close()
log('writing incremental: %s bytes to %s', pos-reposz, dest)
sum = copyfile(options, dest, reposz, pos - reposz)
# The first file in repofiles points to the last full backup. Use this to
# get the .dat file and append the information for this incrementatl to
# that file.
fullfile = repofiles[0]
datfile = os.path.splitext(fullfile)[0] + '.dat'
# This .dat file better exist. Let the exception percolate if not.
fp = open(datfile, 'a')
print(dest, reposz, pos, sum, file=fp)
fp.flush()
os.fsync(fp.fileno())
fp.close()
def do_backup(options):
repofiles = find_files(options)
# See if we need to do a full backup
if options.full or not repofiles:
log('doing a full backup')
do_full_backup(options)
return
srcsz = os.path.getsize(options.file)
if options.quick:
fn, startpos, endpos, sum = scandat(repofiles)
# If the .dat file was missing, or was empty, do a full backup
if (fn, startpos, endpos, sum) == (None, None, None, None):
log('missing or empty .dat file (full backup)')
do_full_backup(options)
return
# Has the file shrunk, possibly because of a pack?
if srcsz < endpos:
log('file shrunk, possibly because of a pack (full backup)')
do_full_backup(options)
return
# Now check the md5 sum of the source file, from the last
# incremental's start and stop positions.
srcfp = open(options.file, 'rb')
srcfp.seek(startpos)
srcsum = checksum(srcfp, endpos-startpos)
srcfp.close()
log('last incremental file: %s', fn)
log('last incremental checksum: %s', sum)
log('source checksum range: [%s..%s], sum: %s',
startpos, endpos, srcsum)
if sum == srcsum:
if srcsz == endpos:
log('No changes, nothing to do')
return
log('doing incremental, starting at: %s', endpos)
do_incremental_backup(options, endpos, repofiles)
return
else:
# This was is much slower, and more disk i/o intensive, but it's also
# more accurate since it checks the actual existing files instead of
# the information in the .dat file.
#
# See if we can do an incremental, based on the files that already
# exist. This call of concat() will not write an output file.
reposz, reposum = concat(repofiles)
log('repository state: %s bytes, md5: %s', reposz, reposum)
# Get the md5 checksum of the source file, up to two file positions:
# the entire size of the file, and up to the file position of the last
# incremental backup.
srcfp = open(options.file, 'rb')
srcsum = checksum(srcfp, srcsz)
srcfp.seek(0)
srcsum_backedup = checksum(srcfp, reposz)
srcfp.close()
log('current state : %s bytes, md5: %s', srcsz, srcsum)
log('backed up state : %s bytes, md5: %s', reposz, srcsum_backedup)
# Has nothing changed?
if srcsz == reposz and srcsum == reposum:
log('No changes, nothing to do')
return
# Has the file shrunk, probably because of a pack?
if srcsz < reposz:
log('file shrunk, possibly because of a pack (full backup)')
do_full_backup(options)
return
# The source file is larger than the repository. If the md5 checksums
# match, then we know we can do an incremental backup. If they don't,
# then perhaps the file was packed at some point (or a
# non-transactional undo was performed, but this is deprecated). Only
# do a full backup if forced to.
if reposum == srcsum_backedup:
log('doing incremental, starting at: %s', reposz)
do_incremental_backup(options, reposz, repofiles)
return
# The checksums don't match, meaning the front of the source file has
# changed. We'll need to do a full backup in that case.
log('file changed, possibly because of a pack (full backup)')
do_full_backup(options)
def do_recover(options):
# Find the first full backup at or before the specified date
repofiles = find_files(options)
if not repofiles:
if options.date:
raise NoFiles('No files in repository before %s', options.date)
else:
raise NoFiles('No files in repository')
if options.output is None:
log('Recovering file to stdout')
outfp = sys.stdout
else:
log('Recovering file to %s', options.output)
outfp = open(options.output, 'wb')
reposz, reposum = concat(repofiles, outfp)
if outfp != sys.stdout:
outfp.close()
log('Recovered %s bytes, md5: %s', reposz, reposum)
if options.output is not None:
last_base = os.path.splitext(repofiles[-1])[0]
source_index = '%s.index' % last_base
target_index = '%s.index' % options.output
if os.path.exists(source_index):
log('Restoring index file %s to %s', source_index, target_index)
shutil.copyfile(source_index, target_index)
else:
log('No index file to restore: %s', source_index)
def do_verify(options):
# Verify the sizes and checksums of all files mentioned in the .dat file
repofiles = find_files(options)
if not repofiles:
raise NoFiles('No files in repository')
datfile = os.path.splitext(repofiles[0])[0] + '.dat'
with open(datfile) as fp:
for line in fp:
fn, startpos, endpos, sum = line.split()
startpos = int(startpos)
endpos = int(endpos)
filename = os.path.join(options.repository,
os.path.basename(fn))
expected_size = endpos - startpos
log("Verifying %s", filename)
try:
if filename.endswith('fsz'):
actual_sum, size = get_checksum_and_size_of_gzipped_file(filename, options.quick)
when_uncompressed = ' (when uncompressed)'
else:
actual_sum, size = get_checksum_and_size_of_file(filename, options.quick)
when_uncompressed = ''
except IOError:
error("%s is missing", filename)
continue
if size != expected_size:
error("%s is %d bytes%s, should be %d bytes", filename,
size, when_uncompressed, expected_size)
elif not options.quick:
if actual_sum != sum:
error("%s has checksum %s%s instead of %s", filename,
actual_sum, when_uncompressed, sum)
def get_checksum_and_size_of_gzipped_file(filename, quick):
with _GzipCloser(filename, 'rb') as fp:
if quick:
return None, file_size(fp)
else:
return checksum_and_size(fp)
def get_checksum_and_size_of_file(filename, quick):
with open(filename, 'rb') as fp:
fp.seek(0, 2)
actual_size = fp.tell()
if quick:
actual_sum = None
else:
fp.seek(0)
actual_sum = checksum(fp, actual_size)
return actual_sum, actual_size
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
options = parseargs(argv)
if options.mode == BACKUP:
try:
do_backup(options)
except WouldOverwriteFiles as e:
sys.exit(str(e))
elif options.mode == RECOVER:
try:
do_recover(options)
except NoFiles as e:
sys.exit(str(e))
else:
assert options.mode == VERIFY
try:
do_verify(options)
except NoFiles as e:
sys.exit(str(e))
if __name__ == '__main__':
main()
@@ -0,0 +1,60 @@
#!/usr/bin/env python
"""Report on the space used by objects in a storage.
usage: space.py data.fs
The current implementation only supports FileStorage.
Current limitations / simplifications: Ignores revisions and versions.
"""
from __future__ import print_function
from ZODB.FileStorage import FileStorage
from ZODB.utils import U64, get_pickle_metadata, load_current
import six
def run(path, v=0):
fs = FileStorage(path, read_only=1)
# break into the file implementation
if hasattr(fs._index, 'iterkeys'):
iter = six.iterkeys(fs._index)
else:
iter = fs._index.keys()
totals = {}
for oid in iter:
data, serialno = load_current(fs, oid)
mod, klass = get_pickle_metadata(data)
key = "%s.%s" % (mod, klass)
bytes, count = totals.get(key, (0, 0))
bytes += len(data)
count += 1
totals[key] = bytes, count
if v:
print("%8s %5d %s" % (U64(oid), len(data), key))
L = totals.items()
L.sort(lambda a, b: cmp(a[1], b[1]))
L.reverse()
print("Totals per object class:")
for key, (bytes, count) in L:
print("%8d %8d %s" % (count, bytes, key))
def main():
import sys
import getopt
try:
opts, args = getopt.getopt(sys.argv[1:], "v")
except getopt.error as msg:
print(msg)
print("usage: space.py [-v] Data.fs")
sys.exit(2)
if len(args) != 1:
print("usage: space.py [-v] Data.fs")
sys.exit(2)
v = 0
for o, a in opts:
if o == "-v":
v += 1
path = args[0]
run(path, v)
if __name__ == "__main__":
main()
@@ -0,0 +1,41 @@
====================
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(suffix='.fs')
>>> 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=132 offset=162 (+23)
<BLANKLINE>
2007-11-10 15:18:48.543001: hash=b16422d09fabdb45d4e4325e4b42d7d6f021d3c3
user='' description='initial database creation' length=150 offset=4 (+48)
<BLANKLINE>
Now clean up the storage again:
>>> import os
>>> connection1.close()
>>> base_storage.close()
>>> os.unlink(storagefile)
>>> os.unlink(storagefile+'.index')
>>> os.unlink(storagefile+'.lock')
>>> os.unlink(storagefile+'.tmp')
@@ -0,0 +1,43 @@
Getting Object Referrers
========================
The referrers module provides a way to get object referrers. It
provides a referrers method that takes an iterable storage object. It
returns a dictionary mapping object ids to lists of referrer object
versions, which each version is a tuple an object id nd serial
nummber.
To see how this works, we'll create a small database:
>>> import transaction
>>> from persistent.mapping import PersistentMapping
>>> from ZODB.FileStorage import FileStorage
>>> from ZODB.DB import DB
>>> import os, tempfile
>>> dest = tempfile.mkdtemp()
>>> fs = FileStorage(os.path.join(dest, 'Data.fs'))
>>> db = DB(fs)
>>> conn = db.open()
>>> conn.root()['a'] = PersistentMapping()
>>> conn.root()['b'] = PersistentMapping()
>>> transaction.commit()
>>> roid = conn.root()._p_oid
>>> aoid = conn.root()['a']._p_oid
>>> boid = conn.root()['b']._p_oid
>>> s1 = conn.root()['b']._p_serial
>>> conn.root()['a']['b'] = conn.root()['b']
>>> transaction.commit()
>>> s2 = conn.root()['a']._p_serial
Now we'll get the storage and compute the referrers:
>>> import ZODB.scripts.referrers
>>> referrers = ZODB.scripts.referrers.referrers(fs)
>>> referrers[boid] == [(roid, s1), (aoid, s2)]
True
.. Cleanup
>>> db.close()
@@ -0,0 +1,48 @@
##############################################################################
#
# Copyright (c) 2004 Zope Foundation 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.
#
##############################################################################
import doctest
import re
import unittest
import ZODB.tests.util
import zope.testing.renormalizing
checker = zope.testing.renormalizing.RENormalizing([
(re.compile(
r'[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'),
# Python 3 bytes add a "b".
(re.compile("b('.*?')"), r"\1"),
(re.compile('b(".*?")'), r"\1"),
# Python 3 produces larger pickles, even when we use zodbpickle :(
# this changes all the offsets and sizes in fstail.txt
(re.compile("user='' description='' "
r"length=[0-9]+ offset=[0-9]+ \(\+23\)"),
"user='' description='' "
"length=<LENGTH> offset=<OFFSET> (+23)"),
(re.compile("user='' description='initial database creation' "
r"length=[0-9]+ offset=4 \(\+48\)"),
"user='' description='initial database creation' "
"length=<LENGTH> offset=4 (+48)"),
])
def test_suite():
return unittest.TestSuite((
doctest.DocFileSuite(
'referrers.txt',
'fstail.txt',
setUp=ZODB.tests.util.setUp, tearDown=ZODB.tests.util.tearDown,
checker=checker),
))
@@ -0,0 +1,55 @@
##############################################################################
#
# Copyright (c) 2010 Zope Foundation 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.
#
##############################################################################
import doctest
import re
import unittest
import ZODB
from zope.testing import setupstack
from zope.testing.renormalizing import RENormalizing
def test_fstest_verbose():
r"""
>>> db = ZODB.DB('data.fs')
>>> db.close()
>>> import ZODB.scripts.fstest
>>> ZODB.scripts.fstest.main(['data.fs'])
>>> ZODB.scripts.fstest.main(['data.fs'])
>>> ZODB.scripts.fstest.main(['-v', 'data.fs'])
... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
4: transaction tid ... #0
no errors detected
>>> ZODB.scripts.fstest.main(['-vvv', 'data.fs'])
... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
52: object oid 0x0000000000000000 #0
4: transaction tid ... #0
no errors detected
"""
def test_suite():
checker = RENormalizing([
# Python 3 drops the u'' prefix on unicode strings
(re.compile(r"u('[^']*')"), r"\1"),
])
return unittest.TestSuite([
doctest.DocTestSuite('ZODB.scripts.fstest', checker=checker),
doctest.DocTestSuite(setUp=setupstack.setUpDirectory,
tearDown=setupstack.tearDown),
])
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,835 @@
#!/usr/bin/env python
##############################################################################
#
# Copyright (c) 2003 Zope Foundation 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.
#
##############################################################################
"""Test script for testing ZODB under a heavy zope-like load.
Note that, to be as realistic as possible with ZEO, you should run this
script multiple times, to simulate multiple clients.
Here's how this works.
The script starts some number of threads. Each thread, sequentially
executes jobs. There is a job producer that produces jobs.
Input data are provided by a mail producer that hands out message from
a mailbox.
Execution continues until there is an error, which will normally occur
when the mailbox is exhausted.
Command-line options are used to provide job definitions. Job
definitions have perameters of the form name=value. Jobs have 2
standard parameters:
frequency=integer
The frequency of the job. The default is 1.
sleep=float
The number os seconds to sleep before performing the job. The
default is 0.
Usage: loadmail2 [options]
Options:
-edit [frequency=integer] [sleep=float]
Define an edit job. An edit job edits a random already-saved
email message, deleting and inserting a random number of words.
After editing the message, the message is (re)cataloged.
-insert [number=int] [frequency=integer] [sleep=float]
Insert some number of email messages.
-index [number=int] [frequency=integer] [sleep=float]
Insert and index (catalog) some number of email messages.
-search [terms='word1 word2 ...'] [frequency=integer] [sleep=float]
Search the catalog. A query is givem with one or more terms as
would be entered into a typical seach box. If no query is
given, then queries will be randomly selected based on a set of
built-in word list.
-setup
Set up the database. This will delete any existing Data.fs
file. (Of course, this may have no effect, if there is a
custom_zodb that defined a different storage.) It also adds a
mail folder and a catalog.
-options file
Read options from the given file. Th efile should be a python
source file that defines a sequence of options named 'options'.
-threads n
Specify the number of threads to execute. If not specified (< 2),
then jobs are run in a single (main) thread.
-mbox filename
Specify the mailbox for getting input data.
There is a (lame) syntax for providing options within the
filename. The filename may be followed by up to 3 integers,
min, max, and start:
-mbox 'foo.mbox 0 100 10000'
The messages from min to max will be read from the mailbox.
They will be assigned message numbers starting with start.
So, in the example above, we read the first hundred messages
and assign thgem message numbers starting with 10001.
The maxmum can be given as a negative number, in which case, it
specifies the number of messages to read.
The start defaults to the minimum. The following two options:
-mbox 'foo.mbox 300 400 300'
and
-mbox 'foo.mbox 300 -100'
are equivalent
"""
from __future__ import print_function
import mailbox
import math
import os
import random
import re
import sys
import threading
import time
import transaction
class JobProducer(object):
def __init__(self):
self.jobs = []
def add(self, callable, frequency, sleep, repeatp=0):
self.jobs.extend([(callable, sleep, repeatp)] * int(frequency))
random.shuffle(self.jobs)
def next(self):
factory, sleep, repeatp = random.choice(self.jobs)
time.sleep(sleep)
callable, args = factory.create()
return factory, callable, args, repeatp
def __nonzero__(self):
return not not self.jobs
class MBox(object):
def __init__(self, filename):
if ' ' in filename:
filename = filename.split()
if len(filename) < 4:
filename += [0, 0, -1][-(4-len(filename)):]
filename, min, max, start = filename
min = int(min)
max = int(max)
start = int(start)
if start < 0:
start = min
if max < 0:
# negative max is treated as a count
self._max = start - max
elif max > 0:
self._max = start + max - min
else:
self._max = 0
else:
self._max = 0
min = start = 0
if filename.endswith('.bz2'):
f = os.popen("bunzip2 <"+filename, 'r')
filename = filename[-4:]
else:
f = open(filename)
self._mbox = mb = mailbox.UnixMailbox(f)
self.number = start
while min:
next(mb)
min -= 1
self._lock = threading.Lock()
self.__name__ = os.path.splitext(os.path.split(filename)[1])[0]
self._max = max
def next(self):
with self.lock:
if self._max > 0 and self.number >= self._max:
raise IndexError(self.number + 1)
message = next(self._mbox)
message.body = message.fp.read()
message.headers = list(message.headers)
self.number += 1
message.number = self.number
message.mbox = self.__name__
return message
bins = 9973
#bins = 11
def mailfolder(app, mboxname, number):
mail = getattr(app, mboxname, None)
if mail is None:
app.manage_addFolder(mboxname)
mail = getattr(app, mboxname)
from BTrees.Length import Length
mail.length = Length()
for i in range(bins):
mail.manage_addFolder('b'+str(i))
bin = hash(str(number))%bins
return getattr(mail, 'b'+str(bin))
def VmSize():
try:
with open('/proc/%s/status' % os.getpid()) as f:
lines = f.readlines()
except:
return 0
else:
l = list(filter(lambda l: l[:7] == 'VmSize:', lines))
if l:
l = l[0][7:].strip().split()[0]
return int(l)
return 0
def setup(lib_python):
try:
os.remove(os.path.join(lib_python, '..', '..', 'var', 'Data.fs'))
except:
pass
import Zope2
import Products
import AccessControl.SecurityManagement
app=Zope2.app()
Products.ZCatalog.ZCatalog.manage_addZCatalog(app, 'cat', '')
from Products.ZCTextIndex.ZCTextIndex import PLexicon
from Products.ZCTextIndex.Lexicon import Splitter, CaseNormalizer
app.cat._setObject('lex',
PLexicon('lex', '', Splitter(), CaseNormalizer())
)
class extra(object):
doc_attr = 'PrincipiaSearchSource'
lexicon_id = 'lex'
index_type = 'Okapi BM25 Rank'
app.cat.addIndex('PrincipiaSearchSource', 'ZCTextIndex', extra)
transaction.commit()
system = AccessControl.SpecialUsers.system
AccessControl.SecurityManagement.newSecurityManager(None, system)
app._p_jar.close()
def do(db, f, args):
"""Do something in a transaction, retrying of necessary
Measure the speed of both the compurartion and the commit
"""
from ZODB.POSException import ConflictError
wcomp = ccomp = wcommit = ccommit = 0.0
rconflicts = wconflicts = 0
start = time.time()
while 1:
connection = db.open()
try:
transaction.begin()
t=time.time()
c=time.clock()
try:
try:
r = f(connection, *args)
except ConflictError:
rconflicts += 1
transaction.abort()
continue
finally:
wcomp += time.time() - t
ccomp += time.clock() - c
t=time.time()
c=time.clock()
try:
try:
transaction.commit()
break
except ConflictError:
wconflicts += 1
transaction.abort()
continue
finally:
wcommit += time.time() - t
ccommit += time.clock() - c
finally:
connection.close()
return start, wcomp, ccomp, rconflicts, wconflicts, wcommit, ccommit, r
def run1(tid, db, factory, job, args):
(start, wcomp, ccomp, rconflicts, wconflicts, wcommit, ccommit, r
) = do(db, job, args)
start = "%.4d-%.2d-%.2d %.2d:%.2d:%.2d" % time.localtime(start)[:6]
print("%s %s %8.3g %8.3g %s %s\t%8.3g %8.3g %s %r" % (
start, tid, wcomp, ccomp, rconflicts, wconflicts, wcommit, ccommit,
factory.__name__, r))
def run(jobs, tid=b''):
import Zope2
while 1:
factory, job, args, repeatp = next(jobs)
run1(tid, Zope2.DB, factory, job, args)
if repeatp:
while 1:
i = random.randint(0,100)
if i > repeatp:
break
run1(tid, Zope2.DB, factory, job, args)
def index(connection, messages, catalog, max):
app = connection.root()['Application']
for message in messages:
mail = mailfolder(app, message.mbox, message.number)
if max:
# Cheat and use folder implementation secrets
# to avoid having to read the old data
_objects = mail._objects
if len(_objects) >= max:
for d in _objects[:len(_objects)-max+1]:
del mail.__dict__[d['id']]
mail._objects = _objects[len(_objects)-max+1:]
docid = 'm'+str(message.number)
mail.manage_addDTMLDocument(docid, file=message.body)
# increment counted
getattr(app, message.mbox).length.change(1)
doc = mail[docid]
for h in message.headers:
h = h.strip()
l = h.find(':')
if l <= 0:
continue
name = h[:l].lower()
if name=='subject':
name='title'
v = h[l+1:].strip()
type='string'
if name=='title':
doc.manage_changeProperties(title=h)
else:
try:
doc.manage_addProperty(name, v, type)
except:
pass
if catalog:
app.cat.catalog_object(doc)
return message.number
class IndexJob(object):
needs_mbox = 1
catalog = 1
prefix = 'index'
def __init__(self, mbox, number=1, max=0):
self.__name__ = "%s%s_%s" % (self.prefix, number, mbox.__name__)
self.mbox, self.number, self.max = mbox, int(number), int(max)
def create(self):
messages = [next(self.mbox) for i in range(self.number)]
return index, (messages, self.catalog, self.max)
class InsertJob(IndexJob):
catalog = 0
prefix = 'insert'
wordre = re.compile(r'(\w{3,20})')
stop = 'and', 'not'
def edit(connection, mbox, catalog=1):
app = connection.root()['Application']
mail = getattr(app, mbox.__name__, None)
if mail is None:
time.sleep(1)
return "No mailbox %s" % mbox.__name__
nmessages = mail.length()
if nmessages < 2:
time.sleep(1)
return "No messages to edit in %s" % mbox.__name__
# find a message to edit:
while 1:
number = random.randint(1, nmessages-1)
did = 'm' + str(number)
mail = mailfolder(app, mbox.__name__, number)
doc = getattr(mail, did, None)
if doc is not None:
break
text = doc.raw.split()
norig = len(text)
if norig > 10:
ndel = int(math.exp(random.randint(0, int(math.log(norig)))))
nins = int(math.exp(random.randint(0, int(math.log(norig)))))
else:
ndel = 0
nins = 10
for j in range(ndel):
j = random.randint(0,len(text)-1)
word = text[j]
m = wordre.search(word)
if m:
word = m.group(1).lower()
if (word not in wordsd) and word not in stop:
words.append(word)
wordsd[word] = 1
del text[j]
for j in range(nins):
word = random.choice(words)
text.append(word)
doc.raw = ' '.join(text)
if catalog:
app.cat.catalog_object(doc)
return norig, ndel, nins
class EditJob(object):
needs_mbox = 1
prefix = 'edit'
catalog = 1
def __init__(self, mbox):
self.__name__ = "%s_%s" % (self.prefix, mbox.__name__)
self.mbox = mbox
def create(self):
return edit, (self.mbox, self.catalog)
class ModifyJob(EditJob):
prefix = 'modify'
catalog = 0
def search(connection, terms, number):
app = connection.root()['Application']
cat = app.cat
n = 0
for i in number:
term = random.choice(terms)
results = cat(PrincipiaSearchSource=term)
n += len(results)
for result in results:
obj = result.getObject()
# Apparently, there is a bug in Zope that leads obj to be None
# on occasion.
if obj is not None:
obj.getId()
return n
class SearchJob(object):
def __init__(self, terms='', number=10):
if terms:
terms = terms.split()
self.__name__ = "search_" + '_'.join(terms)
self.terms = terms
else:
self.__name__ = 'search'
self.terms = words
number = min(int(number), len(self.terms))
self.number = list(range(number))
def create(self):
return search, (self.terms, self.number)
words=['banishment', 'indirectly', 'imprecise', 'peeks',
'opportunely', 'bribe', 'sufficiently', 'Occidentalized', 'elapsing',
'fermenting', 'listen', 'orphanage', 'younger', 'draperies', 'Ida',
'cuttlefish', 'mastermind', 'Michaels', 'populations', 'lent',
'cater', 'attentional', 'hastiness', 'dragnet', 'mangling',
'scabbards', 'princely', 'star', 'repeat', 'deviation', 'agers',
'fix', 'digital', 'ambitious', 'transit', 'jeeps', 'lighted',
'Prussianizations', 'Kickapoo', 'virtual', 'Andrew', 'generally',
'boatsman', 'amounts', 'promulgation', 'Malay', 'savaging',
'courtesan', 'nursed', 'hungered', 'shiningly', 'ship', 'presides',
'Parke', 'moderns', 'Jonas', 'unenlightening', 'dearth', 'deer',
'domesticates', 'recognize', 'gong', 'penetrating', 'dependents',
'unusually', 'complications', 'Dennis', 'imbalances', 'nightgown',
'attached', 'testaments', 'congresswoman', 'circuits', 'bumpers',
'braver', 'Boreas', 'hauled', 'Howe', 'seethed', 'cult', 'numismatic',
'vitality', 'differences', 'collapsed', 'Sandburg', 'inches', 'head',
'rhythmic', 'opponent', 'blanketer', 'attorneys', 'hen', 'spies',
'indispensably', 'clinical', 'redirection', 'submit', 'catalysts',
'councilwoman', 'kills', 'topologies', 'noxious', 'exactions',
'dashers', 'balanced', 'slider', 'cancerous', 'bathtubs', 'legged',
'respectably', 'crochets', 'absenteeism', 'arcsine', 'facility',
'cleaners', 'bobwhite', 'Hawkins', 'stockade', 'provisional',
'tenants', 'forearms', 'Knowlton', 'commit', 'scornful',
'pediatrician', 'greets', 'clenches', 'trowels', 'accepts',
'Carboloy', 'Glenn', 'Leigh', 'enroll', 'Madison', 'Macon', 'oiling',
'entertainingly', 'super', 'propositional', 'pliers', 'beneficiary',
'hospitable', 'emigration', 'sift', 'sensor', 'reserved',
'colonization', 'shrilled', 'momentously', 'stevedore', 'Shanghaiing',
'schoolmasters', 'shaken', 'biology', 'inclination', 'immoderate',
'stem', 'allegory', 'economical', 'daytime', 'Newell', 'Moscow',
'archeology', 'ported', 'scandals', 'Blackfoot', 'leery', 'kilobit',
'empire', 'obliviousness', 'productions', 'sacrificed', 'ideals',
'enrolling', 'certainties', 'Capsicum', 'Brookdale', 'Markism',
'unkind', 'dyers', 'legislates', 'grotesquely', 'megawords',
'arbitrary', 'laughing', 'wildcats', 'thrower', 'sex', 'devils',
'Wehr', 'ablates', 'consume', 'gossips', 'doorways', 'Shari',
'advanced', 'enumerable', 'existentially', 'stunt', 'auctioneers',
'scheduler', 'blanching', 'petulance', 'perceptibly', 'vapors',
'progressed', 'rains', 'intercom', 'emergency', 'increased',
'fluctuating', 'Krishna', 'silken', 'reformed', 'transformation',
'easter', 'fares', 'comprehensible', 'trespasses', 'hallmark',
'tormenter', 'breastworks', 'brassiere', 'bladders', 'civet', 'death',
'transformer', 'tolerably', 'bugle', 'clergy', 'mantels', 'satin',
'Boswellizes', 'Bloomington', 'notifier', 'Filippo', 'circling',
'unassigned', 'dumbness', 'sentries', 'representativeness', 'souped',
'Klux', 'Kingstown', 'gerund', 'Russell', 'splices', 'bellow',
'bandies', 'beefers', 'cameramen', 'appalled', 'Ionian', 'butterball',
'Portland', 'pleaded', 'admiringly', 'pricks', 'hearty', 'corer',
'deliverable', 'accountably', 'mentors', 'accorded',
'acknowledgement', 'Lawrenceville', 'morphology', 'eucalyptus',
'Rena', 'enchanting', 'tighter', 'scholars', 'graduations', 'edges',
'Latinization', 'proficiency', 'monolithic', 'parenthesizing', 'defy',
'shames', 'enjoyment', 'Purdue', 'disagrees', 'barefoot', 'maims',
'flabbergast', 'dishonorable', 'interpolation', 'fanatics', 'dickens',
'abysses', 'adverse', 'components', 'bowl', 'belong', 'Pipestone',
'trainees', 'paw', 'pigtail', 'feed', 'whore', 'conditioner',
'Volstead', 'voices', 'strain', 'inhabits', 'Edwin', 'discourses',
'deigns', 'cruiser', 'biconvex', 'biking', 'depreciation', 'Harrison',
'Persian', 'stunning', 'agar', 'rope', 'wagoner', 'elections',
'reticulately', 'Cruz', 'pulpits', 'wilt', 'peels', 'plants',
'administerings', 'deepen', 'rubs', 'hence', 'dissension', 'implored',
'bereavement', 'abyss', 'Pennsylvania', 'benevolent', 'corresponding',
'Poseidon', 'inactive', 'butchers', 'Mach', 'woke', 'loading',
'utilizing', 'Hoosier', 'undo', 'Semitization', 'trigger', 'Mouthe',
'mark', 'disgracefully', 'copier', 'futility', 'gondola', 'algebraic',
'lecturers', 'sponged', 'instigators', 'looted', 'ether', 'trust',
'feeblest', 'sequencer', 'disjointness', 'congresses', 'Vicksburg',
'incompatibilities', 'commend', 'Luxembourg', 'reticulation',
'instructively', 'reconstructs', 'bricks', 'attache', 'Englishman',
'provocation', 'roughen', 'cynic', 'plugged', 'scrawls', 'antipode',
'injected', 'Daedalus', 'Burnsides', 'asker', 'confronter',
'merriment', 'disdain', 'thicket', 'stinker', 'great', 'tiers',
'oust', 'antipodes', 'Macintosh', 'tented', 'packages',
'Mediterraneanize', 'hurts', 'orthodontist', 'seeder', 'readying',
'babying', 'Florida', 'Sri', 'buckets', 'complementary',
'cartographer', 'chateaus', 'shaves', 'thinkable', 'Tehran',
'Gordian', 'Angles', 'arguable', 'bureau', 'smallest', 'fans',
'navigated', 'dipole', 'bootleg', 'distinctive', 'minimization',
'absorbed', 'surmised', 'Malawi', 'absorbent', 'close', 'conciseness',
'hopefully', 'declares', 'descent', 'trick', 'portend', 'unable',
'mildly', 'Morse', 'reference', 'scours', 'Caribbean', 'battlers',
'astringency', 'likelier', 'Byronizes', 'econometric', 'grad',
'steak', 'Austrian', 'ban', 'voting', 'Darlington', 'bison', 'Cetus',
'proclaim', 'Gilbertson', 'evictions', 'submittal', 'bearings',
'Gothicizer', 'settings', 'McMahon', 'densities', 'determinants',
'period', 'DeKastere', 'swindle', 'promptness', 'enablers', 'wordy',
'during', 'tables', 'responder', 'baffle', 'phosgene', 'muttering',
'limiters', 'custodian', 'prevented', 'Stouffer', 'waltz', 'Videotex',
'brainstorms', 'alcoholism', 'jab', 'shouldering', 'screening',
'explicitly', 'earner', 'commandment', 'French', 'scrutinizing',
'Gemma', 'capacitive', 'sheriff', 'herbivore', 'Betsey', 'Formosa',
'scorcher', 'font', 'damming', 'soldiers', 'flack', 'Marks',
'unlinking', 'serenely', 'rotating', 'converge', 'celebrities',
'unassailable', 'bawling', 'wording', 'silencing', 'scotch',
'coincided', 'masochists', 'graphs', 'pernicious', 'disease',
'depreciates', 'later', 'torus', 'interject', 'mutated', 'causer',
'messy', 'Bechtel', 'redundantly', 'profoundest', 'autopsy',
'philosophic', 'iterate', 'Poisson', 'horridly', 'silversmith',
'millennium', 'plunder', 'salmon', 'missioner', 'advances', 'provers',
'earthliness', 'manor', 'resurrectors', 'Dahl', 'canto', 'gangrene',
'gabler', 'ashore', 'frictionless', 'expansionism', 'emphasis',
'preservations', 'Duane', 'descend', 'isolated', 'firmware',
'dynamites', 'scrawled', 'cavemen', 'ponder', 'prosperity', 'squaw',
'vulnerable', 'opthalmic', 'Simms', 'unite', 'totallers', 'Waring',
'enforced', 'bridge', 'collecting', 'sublime', 'Moore', 'gobble',
'criticizes', 'daydreams', 'sedate', 'apples', 'Concordia',
'subsequence', 'distill', 'Allan', 'seizure', 'Isadore', 'Lancashire',
'spacings', 'corresponded', 'hobble', 'Boonton', 'genuineness',
'artifact', 'gratuities', 'interviewee', 'Vladimir', 'mailable',
'Bini', 'Kowalewski', 'interprets', 'bereave', 'evacuated', 'friend',
'tourists', 'crunched', 'soothsayer', 'fleetly', 'Romanizations',
'Medicaid', 'persevering', 'flimsy', 'doomsday', 'trillion',
'carcasses', 'guess', 'seersucker', 'ripping', 'affliction',
'wildest', 'spokes', 'sheaths', 'procreate', 'rusticates', 'Schapiro',
'thereafter', 'mistakenly', 'shelf', 'ruination', 'bushel',
'assuredly', 'corrupting', 'federation', 'portmanteau', 'wading',
'incendiary', 'thing', 'wanderers', 'messages', 'Paso', 'reexamined',
'freeings', 'denture', 'potting', 'disturber', 'laborer', 'comrade',
'intercommunicating', 'Pelham', 'reproach', 'Fenton', 'Alva', 'oasis',
'attending', 'cockpit', 'scout', 'Jude', 'gagging', 'jailed',
'crustaceans', 'dirt', 'exquisitely', 'Internet', 'blocker', 'smock',
'Troutman', 'neighboring', 'surprise', 'midscale', 'impart',
'badgering', 'fountain', 'Essen', 'societies', 'redresses',
'afterwards', 'puckering', 'silks', 'Blakey', 'sequel', 'greet',
'basements', 'Aubrey', 'helmsman', 'album', 'wheelers', 'easternmost',
'flock', 'ambassadors', 'astatine', 'supplant', 'gird', 'clockwork',
'foxes', 'rerouting', 'divisional', 'bends', 'spacer',
'physiologically', 'exquisite', 'concerts', 'unbridled', 'crossing',
'rock', 'leatherneck', 'Fortescue', 'reloading', 'Laramie', 'Tim',
'forlorn', 'revert', 'scarcer', 'spigot', 'equality', 'paranormal',
'aggrieves', 'pegs', 'committeewomen', 'documented', 'interrupt',
'emerald', 'Battelle', 'reconverted', 'anticipated', 'prejudices',
'drowsiness', 'trivialities', 'food', 'blackberries', 'Cyclades',
'tourist', 'branching', 'nugget', 'Asilomar', 'repairmen', 'Cowan',
'receptacles', 'nobler', 'Nebraskan', 'territorial', 'chickadee',
'bedbug', 'darted', 'vigilance', 'Octavia', 'summands', 'policemen',
'twirls', 'style', 'outlawing', 'specifiable', 'pang', 'Orpheus',
'epigram', 'Babel', 'butyrate', 'wishing', 'fiendish', 'accentuate',
'much', 'pulsed', 'adorned', 'arbiters', 'counted', 'Afrikaner',
'parameterizes', 'agenda', 'Americanism', 'referenda', 'derived',
'liquidity', 'trembling', 'lordly', 'Agway', 'Dillon', 'propellers',
'statement', 'stickiest', 'thankfully', 'autograph', 'parallel',
'impulse', 'Hamey', 'stylistic', 'disproved', 'inquirer', 'hoisting',
'residues', 'variant', 'colonials', 'dequeued', 'especial', 'Samoa',
'Polaris', 'dismisses', 'surpasses', 'prognosis', 'urinates',
'leaguers', 'ostriches', 'calculative', 'digested', 'divided',
'reconfigurer', 'Lakewood', 'illegalities', 'redundancy',
'approachability', 'masterly', 'cookery', 'crystallized', 'Dunham',
'exclaims', 'mainline', 'Australianizes', 'nationhood', 'pusher',
'ushers', 'paranoia', 'workstations', 'radiance', 'impedes',
'Minotaur', 'cataloging', 'bites', 'fashioning', 'Alsop', 'servants',
'Onondaga', 'paragraph', 'leadings', 'clients', 'Latrobe',
'Cornwallis', 'excitingly', 'calorimetric', 'savior', 'tandem',
'antibiotics', 'excuse', 'brushy', 'selfish', 'naive', 'becomes',
'towers', 'popularizes', 'engender', 'introducing', 'possession',
'slaughtered', 'marginally', 'Packards', 'parabola', 'utopia',
'automata', 'deterrent', 'chocolates', 'objectives', 'clannish',
'aspirin', 'ferociousness', 'primarily', 'armpit', 'handfuls',
'dangle', 'Manila', 'enlivened', 'decrease', 'phylum', 'hardy',
'objectively', 'baskets', 'chaired', 'Sepoy', 'deputy', 'blizzard',
'shootings', 'breathtaking', 'sticking', 'initials', 'epitomized',
'Forrest', 'cellular', 'amatory', 'radioed', 'horrified', 'Neva',
'simultaneous', 'delimiter', 'expulsion', 'Himmler', 'contradiction',
'Remus', 'Franklinizations', 'luggage', 'moisture', 'Jews',
'comptroller', 'brevity', 'contradictions', 'Ohio', 'active',
'babysit', 'China', 'youngest', 'superstition', 'clawing', 'raccoons',
'chose', 'shoreline', 'helmets', 'Jeffersonian', 'papered',
'kindergarten', 'reply', 'succinct', 'split', 'wriggle', 'suitcases',
'nonce', 'grinders', 'anthem', 'showcase', 'maimed', 'blue', 'obeys',
'unreported', 'perusing', 'recalculate', 'rancher', 'demonic',
'Lilliputianize', 'approximation', 'repents', 'yellowness',
'irritates', 'Ferber', 'flashlights', 'booty', 'Neanderthal',
'someday', 'foregoes', 'lingering', 'cloudiness', 'guy', 'consumer',
'Berkowitz', 'relics', 'interpolating', 'reappearing', 'advisements',
'Nolan', 'turrets', 'skeletal', 'skills', 'mammas', 'Winsett',
'wheelings', 'stiffen', 'monkeys', 'plainness', 'braziers', 'Leary',
'advisee', 'jack', 'verb', 'reinterpret', 'geometrical', 'trolleys',
'arboreal', 'overpowered', 'Cuzco', 'poetical', 'admirations',
'Hobbes', 'phonemes', 'Newsweek', 'agitator', 'finally', 'prophets',
'environment', 'easterners', 'precomputed', 'faults', 'rankly',
'swallowing', 'crawl', 'trolley', 'spreading', 'resourceful', 'go',
'demandingly', 'broader', 'spiders', 'Marsha', 'debris', 'operates',
'Dundee', 'alleles', 'crunchier', 'quizzical', 'hanging', 'Fisk']
wordsd = {}
for word in words:
wordsd[word] = 1
def collect_options(args, jobs, options):
while args:
arg = args.pop(0)
if arg.startswith('-'):
name = arg[1:]
if name == 'options':
fname = args.pop(0)
d = {}
with open(fname) as fp:
exec(compile(fp.read(), fname, 'exec'), d)
collect_options(list(d['options']), jobs, options)
elif name in options:
v = args.pop(0)
if options[name] != None:
raise ValueError(
"Duplicate values for %s, %s and %s"
% (name, v, options[name])
)
options[name] = v
elif name == 'setup':
options['setup'] = 1
elif name.capitalize()+'Job' in globals():
job = name
kw = {}
while args and args[0].find("=") > 0:
arg = args.pop(0).split('=')
name, v = arg[0], '='.join(arg[1:])
if name in kw:
raise ValueError(
"Duplicate parameter %s for job %s"
% (name, job)
)
kw[name]=v
if 'frequency' in kw:
frequency = kw['frequency']
del kw['frequency']
else:
frequency = 1
if 'sleep' in kw:
sleep = float(kw['sleep'])
del kw['sleep']
else:
sleep = 0.0001
if 'repeat' in kw:
repeatp = float(kw['repeat'])
del kw['repeat']
else:
repeatp = 0
jobs.append((job, kw, frequency, sleep, repeatp))
else:
raise ValueError("not an option or job", name)
else:
raise ValueError("Expected an option", arg)
def find_lib_python():
for b in os.getcwd(), os.path.split(sys.argv[0])[0]:
for i in range(6):
d = ['..']*i + ['lib', 'python']
p = os.path.join(b, *d)
if os.path.isdir(p):
return p
raise ValueError("Couldn't find lib/python")
def main(args=None):
lib_python = find_lib_python()
sys.path.insert(0, lib_python)
if args is None:
args = sys.argv[1:]
if not args:
print(__doc__)
sys.exit(0)
print(args)
random.seed(hash(tuple(args))) # always use the same for the given args
options = {"mbox": None, "threads": None}
jobdefs = []
collect_options(args, jobdefs, options)
mboxes = {}
if options["mbox"]:
mboxes[options["mbox"]] = MBox(options["mbox"])
# Perform a ZConfig-based Zope initialization:
zetup(os.path.join(lib_python, '..', '..', 'etc', 'zope.conf'))
if 'setup' in options:
setup(lib_python)
else:
import Zope2
Zope2.startup()
jobs = JobProducer()
for job, kw, frequency, sleep, repeatp in jobdefs:
Job = globals()[job.capitalize()+'Job']
if getattr(Job, 'needs_mbox', 0):
if "mbox" not in kw:
if not options["mbox"]:
raise ValueError(
"no mailbox (mbox option) file specified")
kw['mbox'] = mboxes[options["mbox"]]
else:
if not mboxes.has_key[kw["mbox"]]:
mboxes[kw['mbox']] = MBox[kw['mbox']]
kw["mbox"] = mboxes[kw['mbox']]
jobs.add(Job(**kw), frequency, sleep, repeatp)
if not jobs:
print("No jobs to execute")
return
threads = int(options['threads'] or '0')
if threads > 1:
threads = [threading.Thread(target=run, args=(jobs, i), name=str(i))
for i in range(threads)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
else:
run(jobs)
def zetup(configfile_name):
from Zope.Startup.options import ZopeOptions
from Zope.Startup import handlers as h
from App import config
opts = ZopeOptions()
opts.configfile = configfile_name
opts.realize(args=[])
h.handleConfig(opts.configroot, opts.confighandlers)
config.setConfiguration(opts.configroot)
from Zope.Startup import dropPrivileges
dropPrivileges(opts.configroot)
if __name__ == '__main__':
main()
@@ -0,0 +1,714 @@
##############################################################################
#
# Copyright (c) 2003 Zope Foundation 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.
#
##############################################################################
"""Support for ZODB object serialization.
ZODB serializes objects using a custom format based on Python pickles.
When an object is unserialized, it can be loaded as either a ghost or
a real object. A ghost is a persistent object of the appropriate type
but without any state. The first time a ghost is accessed, the
persistence machinery traps access and loads the actual state. A
ghost allows many persistent objects to be loaded while minimizing the
memory consumption of referenced but otherwise unused objects.
Pickle format
-------------
ZODB stores serialized objects using a custom format based on pickle.
Each serialized object has two parts: the class description and the
object state. The class description must provide enough information
to call the class's ``__new__`` and create an empty object. Once the
object exists as a ghost, its state is passed to ``__setstate__``.
The class description can be in a variety of formats, in part to
provide backwards compatibility with earlier versions of Zope. The
four current formats for class description are:
1. type(obj)
2. type(obj), obj.__getnewargs__()
3. (module name, class name), None
7. (module name, class name), obj.__getnewargs__()
The second of these options is used if the object has a __getnewargs__()
method. It is intended to support objects like persistent classes that have
custom C layouts that are determined by arguments to __new__(). The
third and fourth (#3 & #7) apply to instances of a persistent class (which
means the class itself is persistent, not that it's a subclass of
Persistent).
The type object is usually stored using the standard pickle mechanism, which
involves the pickle GLOBAL opcode (giving the type's module and name as
strings). The type may itself be a persistent object, in which case a
persistent reference (see below) is used.
It's unclear what "usually" means in the last paragraph. There are two
useful places to concentrate confusion about exactly which formats exist:
- ObjectReader.getClassName() below returns a dotted "module.class"
string, via actually loading a pickle. This requires that the
implementation of application objects be available.
- ZODB/utils.py's get_pickle_metadata() tries to return the module and
class names (as strings) without importing any application modules or
classes, via analyzing the pickle.
Earlier versions of Zope supported several other kinds of class
descriptions. The current serialization code reads these descriptions, but
does not write them. The three earlier formats are:
4. (module name, class name), __getinitargs__()
5. class, None
6. class, __getinitargs__()
Formats 4 and 6 are used only if the class defines a __getinitargs__()
method, but we really can't tell them apart from formats 7 and 2
(respectively). Formats 5 and 6 are used if the class does not have a
__module__ attribute (I'm not sure when this applies, but I think it occurs
for some but not all ZClasses).
Persistent references
---------------------
When one persistent object pickle refers to another persistent object,
the database uses a persistent reference.
ZODB persistent references are of the form::
oid
A simple object reference.
(oid, class meta data)
A persistent object reference
[reference_type, args]
An extended reference
Extension references come in a number of subforms, based on the
reference types.
The following reference types are defined:
'w'
Persistent weak reference. The arguments consist of an oid
and optionally a database name.
The following are planned for the future:
'n'
Multi-database simple object reference. The arguments consist
of a database name, and an object id.
'm'
Multi-database persistent object reference. The arguments consist
of a database name, an object id, and class meta data.
The following legacy format is also supported.
[oid]
A persistent weak reference
Because the persistent object reference forms include class
information, it is not possible to change the class of a persistent
object for which this form is used. If a transaction changed the
class of an object, a new record with new class metadata would be
written but all the old references would still use the old class. (It
is possible that we could deal with this limitation in the future.)
An object id is used alone when a class requires arguments
to it's __new__ method, which is signalled by the class having a
__getnewargs__ attribute.
A number of legacyforms are defined:
"""
import logging
from persistent import Persistent
from persistent.wref import WeakRefMarker, WeakRef
from ZODB import broken
from ZODB.POSException import InvalidObjectReference
from ZODB._compat import PersistentPickler, PersistentUnpickler, BytesIO
from ZODB._compat import _protocol, binary
_oidtypes = bytes, type(None)
# Might to update or redo coptimizations to reflect weakrefs:
# from ZODB.coptimizations import new_persistent_id
def myhasattr(obj, name, _marker=object()):
"""Make sure we don't mask exceptions like hasattr().
We don't want exceptions other than AttributeError to be masked,
since that too often masks other programming errors.
Three-argument getattr() doesn't mask those, so we use that to
implement our own hasattr() replacement.
"""
return getattr(obj, name, _marker) is not _marker
class ObjectWriter(object):
"""Serializes objects for storage in the database.
The ObjectWriter creates object pickles in the ZODB format. It
also detects new persistent objects reachable from the current
object.
"""
_jar = None
def __init__(self, obj=None):
self._file = BytesIO()
self._p = PersistentPickler(self.persistent_id, self._file, _protocol)
self._stack = []
if obj is not None:
self._stack.append(obj)
jar = obj._p_jar
assert myhasattr(jar, "new_oid")
self._jar = jar
def persistent_id(self, obj):
"""Return the persistent id for obj.
>>> from ZODB.tests.util import P
>>> class DummyJar(object):
... xrefs = True
... def new_oid(self):
... return b'42'
... def db(self):
... return self
... databases = {}
>>> jar = DummyJar()
>>> class O(object):
... _p_jar = jar
>>> writer = ObjectWriter(O)
Normally, object references include the oid and a cached named
reference to the class. Having the class information
available allows fast creation of the ghost, avoiding
requiring an additional database lookup.
>>> bob = P('bob')
>>> oid, cls = writer.persistent_id(bob)
>>> oid
'42'
>>> cls is P
True
To work with Python 3, the oid in the persistent id is of the
zodbpickle binary type:
>>> oid.__class__ is binary
True
If a persistent object does not already have an oid and jar,
these will be assigned by persistent_id():
>>> bob._p_oid
'42'
>>> bob._p_jar is jar
True
If the object already has a persistent id, the id is not changed:
>>> bob._p_oid = b'24'
>>> oid, cls = writer.persistent_id(bob)
>>> oid
'24'
>>> cls is P
True
If the jar doesn't match that of the writer, an error is raised:
>>> bob._p_jar = DummyJar()
>>> writer.persistent_id(bob)
... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Traceback (most recent call last):
...
InvalidObjectReference:
('Attempt to store an object from a foreign database connection',
<ZODB.serialize.DummyJar ...>, P(bob))
Constructor arguments used by __new__(), as returned by
__getnewargs__(), can affect memory allocation, but may also
change over the life of the object. This makes it useless to
cache even the object's class.
>>> class PNewArgs(P):
... def __getnewargs__(self):
... return ()
>>> sam = PNewArgs('sam')
>>> writer.persistent_id(sam)
'42'
>>> sam._p_oid
'42'
>>> sam._p_jar is jar
True
Check that simple objects don't get accused of persistence:
>>> writer.persistent_id(42)
>>> writer.persistent_id(object())
Check that a classic class doesn't get identified improperly:
>>> class ClassicClara(object):
... pass
>>> clara = ClassicClara()
>>> writer.persistent_id(clara)
"""
# Most objects are not persistent. The following cheap test
# identifies most of them. For these, we return None,
# signalling that the object should be pickled normally.
if not isinstance(obj, (Persistent, type, WeakRef)):
# Not persistent, pickle normally
return None
# Any persistent object must have an oid:
try:
oid = obj._p_oid
except AttributeError:
# Not persistent, pickle normally
return None
if not (oid is None or isinstance(oid, bytes)):
# Deserves a closer look:
# Make sure it's not a descriptor
if hasattr(oid, '__get__'):
# The oid is a descriptor. That means obj is a non-persistent
# class whose instances are persistent, so ...
# Not persistent, pickle normally
return None
if oid is WeakRefMarker:
# we have a weakref, see weakref.py
oid = obj.oid
if oid is None:
target = obj() # get the referenced object
oid = target._p_oid
if oid is None:
# Here we are causing the object to be saved in
# the database. One could argue that we shouldn't
# do this, because a weakref should not cause an object
# to be added. We'll be optimistic, though, and
# assume that the object will be added eventually.
oid = self._jar.new_oid()
target._p_jar = self._jar
target._p_oid = oid
self._stack.append(target)
obj.oid = oid
obj.dm = target._p_jar
obj.database_name = obj.dm.db().database_name
oid = binary(oid)
if obj.dm is self._jar:
return ['w', (oid, )]
else:
return ['w', (oid, obj.database_name)]
# Since we have an oid, we have either a persistent instance
# (an instance of Persistent), or a persistent class.
# NOTE! Persistent classes don't (and can't) subclass persistent.
database_name = None
if oid is None:
oid = obj._p_oid = self._jar.new_oid()
obj._p_jar = self._jar
self._stack.append(obj)
elif obj._p_jar is not self._jar:
if not self._jar.db().xrefs:
raise InvalidObjectReference(
"Database %r doesn't allow implicit cross-database "
"references" % self._jar.db().database_name,
self._jar, obj)
try:
otherdb = obj._p_jar.db()
database_name = otherdb.database_name
except AttributeError:
otherdb = self
if self._jar.db().databases.get(database_name) is not otherdb:
raise InvalidObjectReference(
"Attempt to store an object from a foreign "
"database connection", self._jar, obj,
)
if self._jar.get_connection(database_name) is not obj._p_jar:
raise InvalidObjectReference(
"Attempt to store a reference to an object from "
"a separate connection to the same database or "
"multidatabase", self._jar, obj,
)
# OK, we have an object from another database.
# Lets make sure the object ws not *just* loaded.
if obj._p_jar._implicitlyAdding(oid):
raise InvalidObjectReference(
"A new object is reachable from multiple databases. "
"Won't try to guess which one was correct!",
self._jar, obj,
)
oid = binary(oid)
klass = type(obj)
if hasattr(klass, '__getnewargs__'):
# We don't want to save newargs in object refs.
# It's possible that __getnewargs__ is degenerate and
# returns (), but we don't want to have to deghostify
# the object to find out.
# Note that this has the odd effect that, if the class has
# __getnewargs__ of its own, we'll lose the optimization
# of caching the class info.
if database_name is not None:
return ['n', (database_name, oid)]
return oid
# Note that we never get here for persistent classes.
# We'll use direct refs for normal classes.
if database_name is not None:
return ['m', (database_name, oid, klass)]
return oid, klass
def serialize(self, obj):
# We don't use __class__ here, because obj could be a persistent proxy.
# We don't want to be fooled by proxies.
klass = type(obj)
# We want to serialize persistent classes by name if they have
# a non-None non-empty module so as not to have a direct
# ref. This is important when copying. We probably want to
# revisit this in the future.
newargs = getattr(obj, "__getnewargs__", None)
if (isinstance(getattr(klass, '_p_oid', 0), _oidtypes)
and klass.__module__):
# This is a persistent class with a non-empty module. This
# uses pickle format #3 or #7.
klass = klass.__module__, klass.__name__
if newargs is None:
meta = klass, None
else:
meta = klass, newargs()
elif newargs is None:
# Pickle format #1.
meta = klass
else:
# Pickle format #2.
meta = klass, newargs()
return self._dump(meta, obj.__getstate__())
def _dump(self, classmeta, state):
# To reuse the existing BytesIO object, we must reset
# the file position to 0 and truncate the file after the
# new pickle is written.
self._file.seek(0)
self._p.clear_memo()
self._p.dump(classmeta)
self._p.dump(state)
self._file.truncate()
return self._file.getvalue()
def __iter__(self):
return NewObjectIterator(self._stack)
class NewObjectIterator(object):
# The pickler is used as a forward iterator when the connection
# is looking for new objects to pickle.
def __init__(self, stack):
self._stack = stack
def __iter__(self):
return self
def __next__(self):
if self._stack:
elt = self._stack.pop()
return elt
else:
raise StopIteration
next = __next__
class ObjectReader(object):
def __init__(self, conn=None, cache=None, factory=None):
self._conn = conn
self._cache = cache
self._factory = factory
def _get_class(self, module, name):
return self._factory(self._conn, module, name)
def _get_unpickler(self, pickle):
file = BytesIO(pickle)
factory = self._factory
conn = self._conn
def find_global(modulename, name):
return factory(conn, modulename, name)
unpickler = PersistentUnpickler(find_global, self._persistent_load, file)
return unpickler
loaders = {}
def _persistent_load(self, reference):
if isinstance(reference, tuple):
return self.load_persistent(*reference)
elif isinstance(reference, (bytes, str)):
return self.load_oid(reference)
else:
try:
reference_type, args = reference
except ValueError:
# weakref
return self.loaders['w'](self, *reference)
else:
return self.loaders[reference_type](self, *args)
def load_persistent(self, oid, klass):
# Quick instance reference. We know all we need to know
# to create the instance w/o hitting the db, so go for it!
if not isinstance(oid, bytes):
assert isinstance(oid, str)
# this happens on Python 3 when all bytes in the oid are < 0x80
oid = oid.encode('ascii')
obj = self._cache.get(oid, None)
if obj is not None:
return obj
if isinstance(klass, tuple):
klass = self._get_class(*klass)
if issubclass(klass, broken.Broken):
# We got a broken class. We might need to make it
# PersistentBroken
if not issubclass(klass, broken.PersistentBroken):
klass = broken.persistentBroken(klass)
try:
obj = klass.__new__(klass)
except TypeError:
# Couldn't create the instance. Maybe there's more
# current data in the object's actual record!
return self._conn.get(oid)
# TODO: should be done by connection
self._cache.new_ghost(oid, obj)
return obj
def load_multi_persistent(self, database_name, oid, klass):
conn = self._conn.get_connection(database_name)
# TODO, make connection _cache attr public
reader = ObjectReader(conn, conn._cache, self._factory)
return reader.load_persistent(oid, klass)
loaders['m'] = load_multi_persistent
def load_persistent_weakref(self, oid, database_name=None):
if not isinstance(oid, bytes):
assert isinstance(oid, str)
# this happens on Python 3 when all bytes in the oid are < 0x80
oid = oid.encode('ascii')
obj = WeakRef.__new__(WeakRef)
obj.oid = oid
if database_name is None:
obj.dm = self._conn
else:
obj.database_name = database_name
try:
obj.dm = self._conn.get_connection(database_name)
except KeyError:
# XXX Not sure what to do here. It seems wrong to
# fail since this is a weak reference. For now we'll
# just pretend that the target object has gone.
pass
return obj
loaders['w'] = load_persistent_weakref
def load_oid(self, oid):
if not isinstance(oid, bytes):
assert isinstance(oid, str)
# this happens on Python 3 when all bytes in the oid are < 0x80
oid = oid.encode('ascii')
obj = self._cache.get(oid, None)
if obj is not None:
return obj
return self._conn.get(oid)
def load_multi_oid(self, database_name, oid):
conn = self._conn.get_connection(database_name)
# TODO, make connection _cache attr public
reader = ObjectReader(conn, conn._cache, self._factory)
return reader.load_oid(oid)
loaders['n'] = load_multi_oid
def getClassName(self, pickle):
unpickler = self._get_unpickler(pickle)
klass = unpickler.load()
if isinstance(klass, tuple):
klass, args = klass
if isinstance(klass, tuple):
# old style reference
return "%s.%s" % klass
return "%s.%s" % (klass.__module__, klass.__name__)
def getGhost(self, pickle):
unpickler = self._get_unpickler(pickle)
klass = unpickler.load()
if isinstance(klass, tuple):
# Here we have a separate class and args.
# This could be an old record, so the class module ne a named
# refernce
klass, args = klass
if isinstance(klass, tuple):
# Old module_name, class_name tuple
klass = self._get_class(*klass)
if args is None:
args = ()
else:
# Definitely new style direct class reference
args = ()
if issubclass(klass, broken.Broken):
# We got a broken class. We might need to make it
# PersistentBroken
if not issubclass(klass, broken.PersistentBroken):
klass = broken.persistentBroken(klass)
return klass.__new__(klass, *args)
def getState(self, pickle):
unpickler = self._get_unpickler(pickle)
try:
unpickler.load() # skip the class metadata
return unpickler.load()
except EOFError as msg:
log = logging.getLogger("ZODB.serialize")
log.exception("Unpickling error: %r", pickle)
raise
def setGhostState(self, obj, pickle):
state = self.getState(pickle)
obj.__setstate__(state)
def referencesf(p, oids=None):
"""Return a list of object ids found in a pickle
A list may be passed in, in which case, information is
appended to it.
Only ordinary internal references are included.
Weak and multi-database references are not included.
"""
refs = []
u = PersistentUnpickler(None, refs.append, BytesIO(p))
u.noload()
u.noload()
# Now we have a list of referencs. Need to convert to list of
# oids:
if oids is None:
oids = []
for reference in refs:
if isinstance(reference, tuple):
oid = reference[0]
elif isinstance(reference, (bytes, str)):
oid = reference
else:
assert isinstance(reference, list)
continue
if not isinstance(oid, bytes):
assert isinstance(oid, str)
# this happens on Python 3 when all bytes in the oid are < 0x80
oid = oid.encode('ascii')
oids.append(oid)
return oids
oid_klass_loaders = {
'w': lambda oid, database_name=None: None,
}
def get_refs(a_pickle):
"""Return oid and class information for references in a pickle
The result of a list of oid and class information tuples.
If the reference doesn't contain class information, then the
klass information is None.
"""
refs = []
u = PersistentUnpickler(None, refs.append, BytesIO(a_pickle))
u.noload()
u.noload()
# Now we have a list of references. Need to convert to list of
# oids and class info:
result = []
for reference in refs:
if isinstance(reference, tuple):
oid, klass = reference
elif isinstance(reference, (bytes, str)):
data, klass = reference, None
else:
assert isinstance(reference, list)
continue
if not isinstance(oid, bytes):
assert isinstance(oid, str)
# this happens on Python 3 when all bytes in the oid are < 0x80
oid = oid.encode('ascii')
result.append((oid, klass))
return result
@@ -0,0 +1,4 @@
<schema>
<import package="ZODB"/>
<section type="ZODB.storage" name="*" attribute="storage"/>
</schema>
@@ -0,0 +1,51 @@
=========================
Subtransactions in ZODB 3
=========================
ZODB 3 provides limited support for subtransactions. Subtransactions
are nested to *one* level. There are top-level transactions and
subtransactions. When a transaction is committed, a flag is passed
indicating whether it is a subtransaction or a top-level transaction.
Consider the following exampler commit calls:
- ``commit()``
A regular top-level transaction is committed.
- ``commit(1)``
A subtransaction is committed. There is now one subtransaction of
the current top-level transaction.
- ``commit(1)``
A subtransaction is committed. There are now two subtransactions of
the current top-level transaction.
- ``abort(1)``
A subtransaction is aborted. There are still two subtransactions of
the current top-level transaction; work done since the last
``commit(1)`` call is discarded.
- ``commit()``
We now commit a top-level transaction. The work done in the previous
two subtransactions *plus* work done since the last ``abort(1)`` call
is saved.
- ``commit(1)``
A subtransaction is committed. There is now one subtransaction of
the current top-level transaction.
- ``commit(1)``
A subtransaction is committed. There are now two subtransactions of
the current top-level transaction.
- ``abort()``
We now abort a top-level transaction. We discard the work done in
the previous two subtransactions *plus* work done since the last
``commit(1)`` call.
@@ -0,0 +1,392 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Run the basic tests for a storage as described in the official storage API
The most complete and most out-of-date description of the interface is:
http://www.zope.org/Documentation/Developer/Models/ZODB/ZODB_Architecture_Storage_Interface_Info.html
All storages should be able to pass these tests.
"""
from ZODB import POSException
from ZODB.Connection import TransactionMetaData
from ZODB.tests.MinPO import MinPO
from ZODB.tests.StorageTestBase import zodb_unpickle, zodb_pickle
import threading
import time
import zope.interface
import zope.interface.verify
from .. import utils
ZERO = b'\0'*8
class BasicStorage(object):
def checkBasics(self):
self.assertEqual(self._storage.lastTransaction(), ZERO)
t = TransactionMetaData()
self._storage.tpc_begin(t)
self.assertRaises(POSException.StorageTransactionError,
self._storage.tpc_begin, t)
# Aborting is easy
self._storage.tpc_abort(t)
# Test a few expected exceptions when we're doing operations giving a
# different Transaction object than the one we've begun on.
self._storage.tpc_begin(t)
self.assertRaises(
POSException.StorageTransactionError,
self._storage.store,
ZERO, ZERO, b'', '', TransactionMetaData())
self.assertRaises(
POSException.StorageTransactionError,
self._storage.store,
ZERO, 1, b'2', '', TransactionMetaData())
self.assertRaises(
POSException.StorageTransactionError,
self._storage.tpc_vote, TransactionMetaData())
self._storage.tpc_abort(t)
def checkSerialIsNoneForInitialRevision(self):
eq = self.assertEqual
oid = self._storage.new_oid()
txn = TransactionMetaData()
self._storage.tpc_begin(txn)
# Use None for serial. Don't use _dostore() here because that coerces
# serial=None to serial=ZERO.
self._storage.store(oid, None, zodb_pickle(MinPO(11)),
'', txn)
self._storage.tpc_vote(txn)
newrevid = self._storage.tpc_finish(txn)
data, revid = utils.load_current(self._storage, oid)
value = zodb_unpickle(data)
eq(value, MinPO(11))
eq(revid, newrevid)
def checkStore(self):
revid = ZERO
newrevid = self._dostore(revid=None)
# Finish the transaction.
self.assertNotEqual(newrevid, revid)
def checkStoreAndLoad(self):
eq = self.assertEqual
oid = self._storage.new_oid()
self._dostore(oid=oid, data=MinPO(7))
data, revid = utils.load_current(self._storage, oid)
value = zodb_unpickle(data)
eq(value, MinPO(7))
# Now do a bunch of updates to an object
for i in range(13, 22):
revid = self._dostore(oid, revid=revid, data=MinPO(i))
# Now get the latest revision of the object
data, revid = utils.load_current(self._storage, oid)
eq(zodb_unpickle(data), MinPO(21))
def checkConflicts(self):
oid = self._storage.new_oid()
revid1 = self._dostore(oid, data=MinPO(11))
self._dostore(oid, revid=revid1, data=MinPO(12))
self.assertRaises(POSException.ConflictError,
self._dostore,
oid, revid=revid1, data=MinPO(13))
def checkWriteAfterAbort(self):
oid = self._storage.new_oid()
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(oid, ZERO, zodb_pickle(MinPO(5)), '', t)
# Now abort this transaction
self._storage.tpc_abort(t)
# Now start all over again
oid = self._storage.new_oid()
self._dostore(oid=oid, data=MinPO(6))
def checkAbortAfterVote(self):
oid1 = self._storage.new_oid()
revid1 = self._dostore(oid=oid1, data=MinPO(-2))
oid = self._storage.new_oid()
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(oid, ZERO, zodb_pickle(MinPO(5)), '', t)
# Now abort this transaction
self._storage.tpc_vote(t)
self._storage.tpc_abort(t)
# Now start all over again
oid = self._storage.new_oid()
revid = self._dostore(oid=oid, data=MinPO(6))
for oid, revid in [(oid1, revid1), (oid, revid)]:
data, _revid = utils.load_current(self._storage, oid)
self.assertEqual(revid, _revid)
def checkStoreTwoObjects(self):
noteq = self.assertNotEqual
p31, p32, p51, p52 = map(MinPO, (31, 32, 51, 52))
oid1 = self._storage.new_oid()
oid2 = self._storage.new_oid()
noteq(oid1, oid2)
revid1 = self._dostore(oid1, data=p31)
revid2 = self._dostore(oid2, data=p51)
noteq(revid1, revid2)
revid3 = self._dostore(oid1, revid=revid1, data=p32)
revid4 = self._dostore(oid2, revid=revid2, data=p52)
noteq(revid3, revid4)
def checkGetTid(self):
if not hasattr(self._storage, 'getTid'):
return
eq = self.assertEqual
p41, p42 = map(MinPO, (41, 42))
oid = self._storage.new_oid()
self.assertRaises(KeyError, self._storage.getTid, oid)
# Now store a revision
revid1 = self._dostore(oid, data=p41)
eq(revid1, self._storage.getTid(oid))
# And another one
revid2 = self._dostore(oid, revid=revid1, data=p42)
eq(revid2, self._storage.getTid(oid))
def checkLen(self):
# len(storage) reports the number of objects.
# check it is zero when empty
self.assertEqual(len(self._storage),0)
# check it is correct when the storage contains two object.
# len may also be zero, for storages that do not keep track
# of this number
self._dostore(data=MinPO(22))
self._dostore(data=MinPO(23))
self.assertTrue(len(self._storage) in [0,2])
def checkGetSize(self):
self._dostore(data=MinPO(25))
size = self._storage.getSize()
# The storage API doesn't make any claims about what size
# means except that it ought to be printable.
str(size)
def checkNote(self):
oid = self._storage.new_oid()
t = TransactionMetaData()
self._storage.tpc_begin(t)
t.note(u'this is a test')
self._storage.store(oid, ZERO, zodb_pickle(MinPO(5)), '', t)
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
def checkInterfaces(self):
for iface in zope.interface.providedBy(self._storage):
zope.interface.verify.verifyObject(iface, self._storage)
def checkMultipleEmptyTransactions(self):
# There was a bug in handling empty transactions in mapping
# storage that caused the commit lock not to be released. :(
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
t = TransactionMetaData()
self._storage.tpc_begin(t) # Hung here before
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
def _do_store_in_separate_thread(self, oid, revid, voted):
# We'll run the competing trans in a separate thread:
thread = threading.Thread(name='T2',
target=self._dostore, args=(oid,), kwargs=dict(revid=revid))
thread.setDaemon(True)
thread.start()
thread.join(.1)
return thread
def check_checkCurrentSerialInTransaction(self):
oid = b'\0\0\0\0\0\0\0\xf0'
tid = self._dostore(oid)
tid2 = self._dostore(oid, revid=tid)
data = b'cpersistent\nPersistent\nq\x01.N.' # a simple persistent obj
#----------------------------------------------------------------------
# stale read
t = TransactionMetaData()
self._storage.tpc_begin(t)
try:
self._storage.store(b'\0\0\0\0\0\0\0\xf1',
b'\0\0\0\0\0\0\0\0', data, '', t)
self._storage.checkCurrentSerialInTransaction(oid, tid, t)
self._storage.tpc_vote(t)
except POSException.ReadConflictError as v:
self.assertEqual(v.oid, oid)
self.assertEqual(v.serials, (tid2, tid))
else:
if 0: self.assertTrue(False, "No conflict error")
self._storage.tpc_abort(t)
#----------------------------------------------------------------------
# non-stale read, no stress. :)
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(b'\0\0\0\0\0\0\0\xf2',
b'\0\0\0\0\0\0\0\0', data, '', t)
self._storage.checkCurrentSerialInTransaction(oid, tid2, t)
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
#----------------------------------------------------------------------
# non-stale read, competition after vote. The competing
# transaction must produce a tid > this transaction's tid
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(b'\0\0\0\0\0\0\0\xf3',
b'\0\0\0\0\0\0\0\0', data, '', t)
self._storage.checkCurrentSerialInTransaction(oid, tid2, t)
self._storage.tpc_vote(t)
# We'll run the competing trans in a separate thread:
thread = self._do_store_in_separate_thread(oid, tid2, True)
self._storage.tpc_finish(t)
thread.join(33)
tid3 = utils.load_current(self._storage, oid)[1]
self.assertTrue(tid3 >
utils.load_current(
self._storage, b'\0\0\0\0\0\0\0\xf3')[1])
#----------------------------------------------------------------------
# non-stale competing trans after checkCurrentSerialInTransaction
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(b'\0\0\0\0\0\0\0\xf4',
b'\0\0\0\0\0\0\0\0', data, '', t)
self._storage.checkCurrentSerialInTransaction(oid, tid3, t)
thread = self._do_store_in_separate_thread(oid, tid3, False)
# There are 2 possibilities:
# 1. The store happens before this transaction completes,
# in which case, the vote below fails.
# 2. The store happens after this trans, in which case, the
# tid of the object is greater than this transaction's tid.
try:
self._storage.tpc_vote(t)
except POSException.ReadConflictError:
thread.join() # OK :)
else:
self._storage.tpc_finish(t)
thread.join()
tid4 = utils.load_current(self._storage, oid)[1]
self.assertTrue(
tid4 >
utils.load_current(self._storage, b'\0\0\0\0\0\0\0\xf4')[1])
def check_tid_ordering_w_commit(self):
# It's important that storages always give a consistent
# ordering for revisions, tids. This is most likely to fail
# around commit. Here we'll do some basic tests to check this.
# We'll use threads to arrange for ordering to go wrong and
# verify that a storage gets it right.
# First, some initial data.
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(ZERO, ZERO, b'x', '', t)
self._storage.tpc_vote(t)
tids = []
self._storage.tpc_finish(t, lambda tid: tids.append(tid))
# OK, now we'll start a new transaction, take it to finish,
# and then block finish while we do some other operations.
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(ZERO, tids[0], b'y', '', t)
self._storage.tpc_vote(t)
to_join = []
def run_in_thread(func):
t = threading.Thread(target=func)
t.setDaemon(True)
t.start()
to_join.append(t)
started = threading.Event()
finish = threading.Event()
@run_in_thread
def commit():
def callback(tid):
started.set()
tids.append(tid)
finish.wait()
self._storage.tpc_finish(t, callback)
results = {}
started.wait()
attempts = []
attempts_cond = utils.Condition()
def update_attempts():
with attempts_cond:
attempts.append(1)
attempts_cond.notifyAll()
@run_in_thread
def lastTransaction():
update_attempts()
results['lastTransaction'] = self._storage.lastTransaction()
@run_in_thread
def load():
update_attempts()
results['load'] = utils.load_current(self._storage, ZERO)[1]
expected_attempts = 2
if hasattr(self._storage, 'getTid'):
expected_attempts += 1
@run_in_thread
def getTid():
update_attempts()
results['getTid'] = self._storage.getTid(ZERO)
if hasattr(self._storage, 'lastInvalidations'):
expected_attempts += 1
@run_in_thread
def lastInvalidations():
update_attempts()
invals = self._storage.lastInvalidations(1)
if invals:
results['lastInvalidations'] = invals[0][0]
with attempts_cond:
while len(attempts) < expected_attempts:
attempts_cond.wait()
time.sleep(.01) # for good measure :)
finish.set()
for t in to_join:
t.join(1)
self.assertEqual(results.pop('load'), tids[1])
self.assertEqual(results.pop('lastTransaction'), tids[1])
for m, tid in results.items():
self.assertEqual(tid, tids[1])
@@ -0,0 +1,176 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Tests for application-level conflict resolution."""
from ZODB import DB
from ZODB.Connection import TransactionMetaData
from ZODB.POSException import ConflictError, UndoError
from persistent import Persistent
from transaction import TransactionManager
from ZODB.tests.StorageTestBase import zodb_unpickle, zodb_pickle
class PCounter(Persistent):
_value = 0
def __repr__(self):
return "<PCounter %d>" % self._value
def inc(self, n=1):
self._value = self._value + n
def _p_resolveConflict(self, oldState, savedState, newState):
savedDiff = savedState['_value'] - oldState['_value']
newDiff = newState['_value'] - oldState['_value']
oldState['_value'] = oldState['_value'] + savedDiff + newDiff
return oldState
# Insecurity: What if _p_resolveConflict _thinks_ it resolved the
# conflict, but did something wrong?
class PCounter2(PCounter):
def _p_resolveConflict(self, oldState, savedState, newState):
raise ConflictError
class PCounter3(PCounter):
def _p_resolveConflict(self, oldState, savedState, newState):
raise AttributeError("no attribute (testing conflict resolution)")
class PCounter4(PCounter):
def _p_resolveConflict(self, oldState, savedState):
raise RuntimeError("Can't get here; not enough args")
class ConflictResolvingStorage(object):
def checkResolve(self, resolvable=True):
db = DB(self._storage)
t1 = TransactionManager()
c1 = db.open(t1)
o1 = c1.root()['p'] = (PCounter if resolvable else PCounter2)()
o1.inc()
t1.commit()
t2 = TransactionManager()
c2 = db.open(t2)
o2 = c2.root()['p']
o2.inc(2)
t2.commit()
o1.inc(3)
try:
t1.commit()
except ConflictError as err:
self.assertIn(".PCounter2,", str(err))
self.assertEqual(o1._value, 3)
else:
self.assertTrue(resolvable, "Expected ConflictError")
self.assertEqual(o1._value, 6)
t2.begin()
self.assertEqual(o2._value, o1._value)
db.close()
def checkUnresolvable(self):
self.checkResolve(False)
def checkZClassesArentResolved(self):
from ZODB.ConflictResolution import find_global, BadClassName
dummy_class_tuple = ('*foobar', ())
self.assertRaises(BadClassName, find_global, '*foobar', ())
def checkBuggyResolve1(self):
obj = PCounter3()
obj.inc()
oid = self._storage.new_oid()
revid1 = self._dostoreNP(oid, data=zodb_pickle(obj))
obj.inc()
obj.inc()
# The effect of committing two transactions with the same
# pickle is to commit two different transactions relative to
# revid1 that add two to _value.
revid2 = self._dostoreNP(oid, revid=revid1, data=zodb_pickle(obj))
self.assertRaises(ConflictError,
self._dostoreNP,
oid, revid=revid1, data=zodb_pickle(obj))
def checkBuggyResolve2(self):
obj = PCounter4()
obj.inc()
oid = self._storage.new_oid()
revid1 = self._dostoreNP(oid, data=zodb_pickle(obj))
obj.inc()
obj.inc()
# The effect of committing two transactions with the same
# pickle is to commit two different transactions relative to
# revid1 that add two to _value.
revid2 = self._dostoreNP(oid, revid=revid1, data=zodb_pickle(obj))
self.assertRaises(ConflictError,
self._dostoreNP,
oid, revid=revid1, data=zodb_pickle(obj))
class ConflictResolvingTransUndoStorage(object):
def checkUndoConflictResolution(self):
# This test is based on checkNotUndoable in the
# TransactionalUndoStorage test suite. Except here, conflict
# resolution should allow us to undo the transaction anyway.
obj = PCounter()
obj.inc()
oid = self._storage.new_oid()
revid_a = self._dostore(oid, data=obj)
obj.inc()
revid_b = self._dostore(oid, revid=revid_a, data=obj)
obj.inc()
revid_c = self._dostore(oid, revid=revid_b, data=obj)
# Start the undo
info = self._storage.undoInfo()
tid = info[1]['id']
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.undo(tid, t)
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
def checkUndoUnresolvable(self):
# This test is based on checkNotUndoable in the
# TransactionalUndoStorage test suite. Except here, conflict
# resolution should allow us to undo the transaction anyway.
obj = PCounter2()
obj.inc()
oid = self._storage.new_oid()
revid_a = self._dostore(oid, data=obj)
obj.inc()
revid_b = self._dostore(oid, revid=revid_a, data=obj)
obj.inc()
revid_c = self._dostore(oid, revid=revid_b, data=obj)
# Start the undo
info = self._storage.undoInfo()
tid = info[1]['id']
t = TransactionMetaData()
self.assertRaises(UndoError, self._begin_undos_vote, t, tid)
self._storage.tpc_abort(t)
@@ -0,0 +1,75 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Do some minimal tests of data corruption"""
import os
import random
import stat
import ZODB.FileStorage
from ZODB.utils import load_current
from .StorageTestBase import StorageTestBase
class FileStorageCorruptTests(StorageTestBase):
def setUp(self):
StorageTestBase.setUp(self)
self._storage = ZODB.FileStorage.FileStorage('Data.fs', create=1)
def _do_stores(self):
oids = []
for i in range(5):
oid = self._storage.new_oid()
revid = self._dostore(oid)
oids.append((oid, revid))
return oids
def _check_stores(self, oids):
for oid, revid in oids:
data, s_revid = load_current(self._storage, oid)
self.assertEqual(s_revid, revid)
def checkTruncatedIndex(self):
oids = self._do_stores()
self._close()
# truncation the index file
self.assertTrue(os.path.exists('Data.fs.index'))
f = open('Data.fs.index', 'rb+')
f.seek(0, 2)
size = f.tell()
f.seek(size // 2)
f.truncate()
f.close()
self._storage = ZODB.FileStorage.FileStorage('Data.fs')
self._check_stores(oids)
def checkCorruptedIndex(self):
oids = self._do_stores()
self._close()
# truncation the index file
self.assertTrue(os.path.exists('Data.fs.index'))
size = os.stat('Data.fs.index')[stat.ST_SIZE]
f = open('Data.fs.index', 'rb+')
while f.tell() < size:
f.seek(random.randrange(1, size // 10), 1)
f.write(b'\000')
f.close()
self._storage = ZODB.FileStorage.FileStorage('Data.fs')
self._check_stores(oids)
@@ -0,0 +1,55 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Run the history() related tests for a storage.
Any storage that supports the history() method should be able to pass
all these tests.
"""
import sys
from time import time, sleep
from ZODB.tests.MinPO import MinPO
class HistoryStorage(object):
def checkSimpleHistory(self):
self._checkHistory((11, 12, 13))
def _checkHistory(self, data):
start = time()
# Store a couple of revisions of the object
oid = self._storage.new_oid()
self.assertRaises(KeyError,self._storage.history,oid)
revids = [None]
for data in data:
if sys.platform == 'win32':
# time.time() has a precision of 1ms on Windows.
sleep(0.002)
revids.append(self._dostore(oid, revids[-1], MinPO(data)))
revids.reverse()
del revids[-1]
# Now get various snapshots of the object's history
for i in range(1, 1 + len(revids)):
h = self._storage.history(oid, size=i)
self.assertEqual([d['tid'] for d in h], revids[:i])
# Check results are sorted by timestamp, in descending order.
if sys.platform == 'win32':
# Same as above. This is also required in case this method is
# called several times for the same storage.
sleep(0.002)
a = time()
for d in h:
b = a
a = d['time']
self.assertLess(a, b)
self.assertLess(start, a)
@@ -0,0 +1,130 @@
Storage Support for external GC
===============================
A storage that provides IExternalGC supports external garbage
collectors by providing a deleteObject method that transactionally
deletes an object.
A create_storage function is provided that creates a storage.
>>> storage = create_storage()
>>> import ZODB.blob, transaction
>>> db = ZODB.DB(storage)
>>> conn = db.open()
>>> conn.root()[0] = conn.root().__class__()
>>> conn.root()[1] = ZODB.blob.Blob(b'some data')
>>> transaction.commit()
>>> oid0 = conn.root()[0]._p_oid
>>> oid1 = conn.root()[1]._p_oid
>>> del conn.root()[0]
>>> del conn.root()[1]
>>> transaction.commit()
At this point, object 0 and 1 is garbage, but it's still in the storage:
>>> p0, s0 = storage.load(oid0, '')
>>> p1, s1 = storage.load(oid1, '')
The storage is configured not to gc on pack, so even if we pack, these
objects won't go away:
>>> len(storage)
3
>>> import time
>>> db.pack(time.time()+1)
>>> len(storage)
3
>>> p0, s0 = storage.load(oid0, '')
>>> p1, s1 = storage.load(oid1, '')
Now we'll use the new deleteObject API to delete the objects. We can't
go through the database to do this, so we'll have to manage the
transaction ourselves.
>>> from ZODB.Connection import TransactionMetaData
>>> txn = TransactionMetaData()
>>> storage.tpc_begin(txn)
>>> storage.deleteObject(oid0, s0, txn)
>>> storage.deleteObject(oid1, s1, txn)
>>> _ = storage.tpc_vote(txn)
>>> tid = storage.tpc_finish(txn)
>>> tid == storage.lastTransaction()
True
Now if we try to load data for the objects, we get a POSKeyError:
>>> storage.load(oid0, '') # doctest: +ELLIPSIS
Traceback (most recent call last):
...
POSKeyError: ...
>>> storage.load(oid1, '') # doctest: +ELLIPSIS
Traceback (most recent call last):
...
POSKeyError: ...
We can still get the data if we load before the time we deleted.
>>> storage.loadBefore(oid0, conn.root()._p_serial) == (p0, s0, tid)
True
>>> storage.loadBefore(oid1, conn.root()._p_serial) == (p1, s1, tid)
True
>>> with open(storage.loadBlob(oid1, s1)) as fp: fp.read()
'some data'
If we pack, however, the old data will be removed and the data will be
gone:
>>> db.pack(time.time()+1)
>>> len(db.storage)
1
>>> time.sleep(.1)
>>> storage.load(oid0, '') # doctest: +ELLIPSIS
Traceback (most recent call last):
...
POSKeyError: ...
>>> storage.load(oid1, '') # doctest: +ELLIPSIS
Traceback (most recent call last):
...
POSKeyError: ...
>>> storage.loadBefore(oid0, conn.root()._p_serial) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
POSKeyError: ...
>>> storage.loadBefore(oid1, conn.root()._p_serial) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
POSKeyError: ...
>>> storage.loadBlob(oid1, s1) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
POSKeyError: ...
A conflict error is raised if the serial we provide to deleteObject
isn't current:
>>> conn.root()[0] = conn.root().__class__()
>>> transaction.commit()
>>> oid = conn.root()[0]._p_oid
>>> bad_serial = conn.root()[0]._p_serial
>>> conn.root()[0].x = 1
>>> transaction.commit()
>>> txn = TransactionMetaData()
>>> storage.tpc_begin(txn)
>>> storage.deleteObject(oid, bad_serial, txn); storage.tpc_vote(txn)
... # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ConflictError: database conflict error ...
>>> storage.tpc_abort(txn)
>>> storage.close()
@@ -0,0 +1,247 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Run tests against the iterator() interface for storages.
Any storage that supports the iterator() method should be able to pass
all these tests.
"""
from ZODB.Connection import TransactionMetaData
from ZODB.tests.MinPO import MinPO
from ZODB.tests.StorageTestBase import zodb_pickle, zodb_unpickle
from ZODB.utils import U64, p64, load_current
import ZODB.blob
try:
from itertools import izip as zip
except ImportError:
# Py3: zip() already returns an iterable.
pass
class IteratorCompare(object):
def iter_verify(self, txniter, revids, val0):
eq = self.assertEqual
oid = self._oid
val = val0
for reciter, revid in zip(txniter, revids + [None]):
eq(reciter.tid, revid)
for rec in reciter:
eq(rec.oid, oid)
eq(rec.tid, revid)
eq(zodb_unpickle(rec.data), MinPO(val))
val = val + 1
eq(val, val0 + len(revids))
class IteratorStorage(IteratorCompare):
def checkSimpleIteration(self):
# Store a bunch of revisions of a single object
self._oid = oid = self._storage.new_oid()
revid1 = self._dostore(oid, data=MinPO(11))
revid2 = self._dostore(oid, revid=revid1, data=MinPO(12))
revid3 = self._dostore(oid, revid=revid2, data=MinPO(13))
# Now iterate over all the transactions and compare carefully
txniter = self._storage.iterator()
self.iter_verify(txniter, [revid1, revid2, revid3], 11)
def checkUndoZombie(self):
oid = self._storage.new_oid()
revid = self._dostore(oid, data=MinPO(94))
# Get the undo information
info = self._storage.undoInfo()
tid = info[0]['id']
# Undo the creation of the object, rendering it a zombie
t = TransactionMetaData()
self._storage.tpc_begin(t)
oids = self._storage.undo(tid, t)
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
# Now attempt to iterator over the storage
iter = self._storage.iterator()
for txn in iter:
for rec in txn:
pass
# The last transaction performed an undo of the transaction that
# created object oid. (As Barry points out, the object is now in the
# George Bailey state.) Assert that the final data record contains
# None in the data attribute.
self.assertEqual(rec.oid, oid)
self.assertEqual(rec.data, None)
def checkTransactionExtensionFromIterator(self):
oid = self._storage.new_oid()
revid = self._dostore(oid, data=MinPO(1))
iter = self._storage.iterator()
count = 0
for txn in iter:
self.assertEqual(txn.extension, {})
count +=1
self.assertEqual(count, 1)
def checkIterationIntraTransaction(self):
# TODO: Try this test with logging enabled. If you see something
# like
#
# ZODB FS FS21 warn: FileStorageTests.fs truncated, possibly due to
# damaged records at 4
#
# Then the code in FileIterator.next() hasn't yet been fixed.
# Should automate that check.
oid = self._storage.new_oid()
t = TransactionMetaData()
data = zodb_pickle(MinPO(0))
try:
self._storage.tpc_begin(t)
self._storage.store(oid, '\0'*8, data, '', t)
self._storage.tpc_vote(t)
# Don't do tpc_finish yet
it = self._storage.iterator()
for x in it:
pass
finally:
self._storage.tpc_finish(t)
def checkLoad_was_checkLoadEx(self):
oid = self._storage.new_oid()
self._dostore(oid, data=42)
data, tid = load_current(self._storage, oid)
self.assertEqual(zodb_unpickle(data), MinPO(42))
match = False
for txn in self._storage.iterator():
for rec in txn:
if rec.oid == oid and rec.tid == tid:
self.assertEqual(txn.tid, tid)
match = True
if not match:
self.fail("Could not find transaction with matching id")
def checkIterateRepeatedly(self):
self._dostore()
transactions = self._storage.iterator()
self.assertEqual(1, len(list(transactions)))
# The iterator can only be consumed once:
self.assertEqual(0, len(list(transactions)))
def checkIterateRecordsRepeatedly(self):
self._dostore()
it = self._storage.iterator()
tinfo = next(it)
self.assertEqual(1, len(list(tinfo)))
self.assertEqual(1, len(list(tinfo)))
if hasattr(it, 'close'):
it.close()
def checkIterateWhileWriting(self):
self._dostore()
iterator = self._storage.iterator()
# We have one transaction with 1 modified object.
txn_1 = next(iterator)
self.assertEqual(1, len(list(txn_1)))
# We store another transaction with 1 object, the already running
# iterator does not pick this up.
self._dostore()
with self.assertRaises(StopIteration):
next(iterator)
class ExtendedIteratorStorage(IteratorCompare):
def checkExtendedIteration(self):
# Store a bunch of revisions of a single object
self._oid = oid = self._storage.new_oid()
revid1 = self._dostore(oid, data=MinPO(11))
revid2 = self._dostore(oid, revid=revid1, data=MinPO(12))
revid3 = self._dostore(oid, revid=revid2, data=MinPO(13))
revid4 = self._dostore(oid, revid=revid3, data=MinPO(14))
# Note that the end points are included
# Iterate over all of the transactions with explicit start/stop
txniter = self._storage.iterator(revid1, revid4)
self.iter_verify(txniter, [revid1, revid2, revid3, revid4], 11)
# Iterate over some of the transactions with explicit start
txniter = self._storage.iterator(revid3)
self.iter_verify(txniter, [revid3, revid4], 13)
# Iterate over some of the transactions with explicit stop
txniter = self._storage.iterator(None, revid2)
self.iter_verify(txniter, [revid1, revid2], 11)
# Iterate over some of the transactions with explicit start+stop
txniter = self._storage.iterator(revid2, revid3)
self.iter_verify(txniter, [revid2, revid3], 12)
# Specify an upper bound somewhere in between values
revid3a = p64((U64(revid3) + U64(revid4)) // 2)
txniter = self._storage.iterator(revid2, revid3a)
self.iter_verify(txniter, [revid2, revid3], 12)
# Specify a lower bound somewhere in between values.
# revid2 == revid1+1 is very likely on Windows. Adding 1 before
# dividing ensures that "the midpoint" we compute is strictly larger
# than revid1.
revid1a = p64((U64(revid1) + 1 + U64(revid2)) // 2)
assert revid1 < revid1a
txniter = self._storage.iterator(revid1a, revid3a)
self.iter_verify(txniter, [revid2, revid3], 12)
# Specify an empty range
txniter = self._storage.iterator(revid3, revid2)
self.iter_verify(txniter, [], 13)
# Specify a singleton range
txniter = self._storage.iterator(revid3, revid3)
self.iter_verify(txniter, [revid3], 13)
class IteratorDeepCompare(object):
def compare(self, storage1, storage2):
eq = self.assertEqual
iter1 = storage1.iterator()
iter2 = storage2.iterator()
for txn1, txn2 in zip(iter1, iter2):
eq(txn1.tid, txn2.tid)
eq(txn1.status, txn2.status)
eq(txn1.user, txn2.user)
eq(txn1.description, txn2.description)
eq(txn1.extension, txn2.extension)
itxn1 = iter(txn1)
itxn2 = iter(txn2)
for rec1, rec2 in zip(itxn1, itxn2):
eq(rec1.oid, rec2.oid)
eq(rec1.tid, rec2.tid)
eq(rec1.data, rec2.data)
if ZODB.blob.is_blob_record(rec1.data):
try:
fn1 = storage1.loadBlob(rec1.oid, rec1.tid)
except ZODB.POSException.POSKeyError:
self.assertRaises(
ZODB.POSException.POSKeyError,
storage2.loadBlob, rec1.oid, rec1.tid)
else:
fn2 = storage2.loadBlob(rec1.oid, rec1.tid)
self.assertTrue(fn1 != fn2)
with open(fn1, 'rb') as fp1:
with open(fn2, 'rb') as fp2:
eq(fp1.read(), fp2.read())
# Make sure there are no more records left in rec1 and rec2,
# meaning they were the same length.
# Additionally, check that we're backwards compatible to the
# IndexError we used to raise before.
self.assertRaises(StopIteration, next, itxn1)
self.assertRaises(StopIteration, next, itxn2)
# Make sure ther are no more records left in txn1 and txn2, meaning
# they were the same length
self.assertRaises(StopIteration, next, iter1)
self.assertRaises(StopIteration, next, iter2)
@@ -0,0 +1,244 @@
import random
import sys
import threading
import time
from persistent.mapping import PersistentMapping
import six
import transaction
import ZODB
from ZODB.Connection import TransactionMetaData
from ZODB.tests.StorageTestBase import zodb_pickle, zodb_unpickle
from ZODB.tests.MinPO import MinPO
from ZODB.POSException import ConflictError
from ZODB.utils import load_current
SHORT_DELAY = 0.01
class TestThread(threading.Thread):
"""Base class for defining threads that run from unittest.
If the thread exits with an uncaught exception, catch it and
re-raise it when the thread is joined. The re-raise will cause
the test to fail.
The subclass should define a runtest() method instead of a run()
method.
"""
def __init__(self):
threading.Thread.__init__(self)
self._exc_info = None
def run(self):
try:
self.runtest()
except:
self._exc_info = sys.exc_info()
def join(self, timeout=None):
threading.Thread.join(self, timeout)
if self._exc_info:
raise six.reraise(
self._exc_info[0], self._exc_info[1], self._exc_info[2])
class ZODBClientThread(TestThread):
__super_init = TestThread.__init__
def __init__(self, db, test, commits=10, delay=SHORT_DELAY):
self.__super_init()
self.setDaemon(1)
self.db = db
self.test = test
self.commits = commits
self.delay = delay
def runtest(self):
conn = self.db.open()
conn.sync()
root = conn.root()
d = self.get_thread_dict(root)
if d is None:
self.test.fail()
else:
for i in range(self.commits):
self.commit(d, i)
self.test.assertEqual(sorted(d.keys()), list(range(self.commits)))
conn.close()
def commit(self, d, num):
d[num] = time.time()
time.sleep(self.delay)
transaction.commit()
time.sleep(self.delay)
# Return a new PersistentMapping, and store it on the root object under
# the name (.getName()) of the current thread.
def get_thread_dict(self, root):
# This is vicious: multiple threads are slamming changes into the
# root object, then trying to read the root object, simultaneously
# and without any coordination. Conflict errors are rampant. It
# used to go around at most 10 times, but that fairly often failed
# to make progress in the 7-thread tests on some test boxes. Going
# around (at most) 1000 times was enough so that a 100-thread test
# reliably passed on Tim's hyperthreaded WinXP box (but at the
# original 10 retries, the same test reliably failed with 15 threads).
name = self.getName()
MAXRETRIES = 1000
for i in range(MAXRETRIES):
try:
root[name] = PersistentMapping()
transaction.commit()
break
except ConflictError:
root._p_jar.sync()
else:
raise ConflictError("Exceeded %d attempts to store" % MAXRETRIES)
for j in range(MAXRETRIES):
try:
return root.get(name)
except ConflictError:
root._p_jar.sync()
raise ConflictError("Exceeded %d attempts to read" % MAXRETRIES)
class StorageClientThread(TestThread):
__super_init = TestThread.__init__
def __init__(self, storage, test, commits=10, delay=SHORT_DELAY):
self.__super_init()
self.storage = storage
self.test = test
self.commits = commits
self.delay = delay
self.oids = {}
def runtest(self):
for i in range(self.commits):
self.dostore(i)
self.check()
def check(self):
for oid, revid in self.oids.items():
data, serial = load_current(self.storage, oid)
self.test.assertEqual(serial, revid)
obj = zodb_unpickle(data)
self.test.assertEqual(obj.value[0], self.getName())
def pause(self):
time.sleep(self.delay)
def oid(self):
oid = self.storage.new_oid()
self.oids[oid] = None
return oid
def dostore(self, i):
data = zodb_pickle(MinPO((self.getName(), i)))
t = TransactionMetaData()
oid = self.oid()
self.pause()
self.storage.tpc_begin(t)
self.pause()
# Always create a new object, signified by None for revid
self.storage.store(oid, None, data, '', t)
self.pause()
self.storage.tpc_vote(t)
self.pause()
revid = self.storage.tpc_finish(t)
self.pause()
self.oids[oid] = revid
class ExtStorageClientThread(StorageClientThread):
def runtest(self):
# pick some other storage ops to execute, depending in part
# on the features provided by the storage.
names = ["do_load"]
storage = self.storage
try:
supportsUndo = storage.supportsUndo
except AttributeError:
pass
else:
if supportsUndo():
names += ["do_loadSerial", "do_undoLog", "do_iterator"]
ops = [getattr(self, meth) for meth in names]
assert ops, "Didn't find an storage ops in %s" % self.storage
# do a store to guarantee there's at least one oid in self.oids
self.dostore(0)
for i in range(self.commits - 1):
meth = random.choice(ops)
meth()
self.dostore(i)
self.check()
def pick_oid(self):
return random.choice(tuple(self.oids))
def do_load(self):
oid = self.pick_oid()
load_current(self.storage, oid)
def do_loadSerial(self):
oid = self.pick_oid()
self.storage.loadSerial(oid, self.oids[oid])
def do_undoLog(self):
self.storage.undoLog(0, -20)
def do_iterator(self):
try:
iter = self.storage.iterator()
except AttributeError:
# It's hard to detect that a ZEO ClientStorage
# doesn't have this method, but does have all the others.
return
for obj in iter:
pass
class MTStorage(object):
"Test a storage with multiple client threads executing concurrently."
def _checkNThreads(self, n, constructor, *args):
threads = [constructor(*args) for i in range(n)]
for t in threads:
t.start()
for t in threads:
t.join(60)
for t in threads:
self.assertFalse(t.isAlive(),
"thread failed to finish in 60 seconds")
def check2ZODBThreads(self):
db = ZODB.DB(self._storage)
self._checkNThreads(2, ZODBClientThread, db, self)
db.close()
def check7ZODBThreads(self):
db = ZODB.DB(self._storage)
self._checkNThreads(7, ZODBClientThread, db, self)
db.close()
def check2StorageThreads(self):
self._checkNThreads(2, StorageClientThread, self._storage, self)
def check7StorageThreads(self):
self._checkNThreads(7, StorageClientThread, self._storage, self)
def check4ExtStorageThread(self):
self._checkNThreads(4, ExtStorageClientThread, self._storage, self)
@@ -0,0 +1,124 @@
##############################################################################
#
# Copyright (c) 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
#
##############################################################################
"""An extension of MappingStorage that depends on polling.
Each Connection has its own view of the database. Polling updates each
connection's view.
"""
import ZODB.utils
import ZODB.POSException
from ZODB.interfaces import IMVCCStorage
from ZODB.MappingStorage import MappingStorage
from zope.interface import implementer
@implementer(IMVCCStorage)
class MVCCMappingStorage(MappingStorage):
def __init__(self, name="MVCC Mapping Storage"):
MappingStorage.__init__(self, name=name)
# _polled_tid contains the transaction ID at the last poll.
self._polled_tid = b''
self._data_snapshot = None # {oid->(state, tid)}
self._main_lock = self._lock
def new_instance(self):
"""Returns a storage instance that is a view of the same data.
"""
inst = MVCCMappingStorage(name=self.__name__)
# All instances share the same OID data, transaction log, commit lock,
# and OID sequence.
inst._data = self._data
inst._transactions = self._transactions
inst._commit_lock = self._commit_lock
inst.new_oid = self.new_oid
inst.pack = self.pack
inst.loadBefore = self.loadBefore
inst._ltid = self._ltid
inst._main_lock = self._lock
return inst
@ZODB.utils.locked(MappingStorage.opened)
def sync(self, force=False):
self._data_snapshot = None
def release(self):
pass
@ZODB.utils.locked(MappingStorage.opened)
def load(self, oid, version=''):
assert not version, "Versions are not supported"
if self._data_snapshot is None:
self.poll_invalidations()
info = self._data_snapshot.get(oid)
if info:
return info
raise ZODB.POSException.POSKeyError(oid)
def poll_invalidations(self):
"""Poll the storage for changes by other connections.
"""
# prevent changes to _transactions and _data during analysis
with self._main_lock:
if self._transactions:
new_tid = self._transactions.maxKey()
else:
new_tid = ZODB.utils.z64
# Copy the current data into a snapshot. This is obviously
# very inefficient for large storages, but it's good for
# tests.
self._data_snapshot = {}
for oid, tid_data in self._data.items():
if tid_data:
tid = tid_data.maxKey()
self._data_snapshot[oid] = tid_data[tid], tid
if self._polled_tid:
if self._polled_tid not in self._transactions:
# This connection is so old that we can no longer enumerate
# all the changes.
self._polled_tid = new_tid
return None
changed_oids = set()
for tid, txn in self._transactions.items(
self._polled_tid, new_tid,
excludemin=True, excludemax=False):
if txn.status == 'p':
# This transaction has been packed, so it is no longer
# possible to enumerate all changed oids.
self._polled_tid = new_tid
return None
if tid == self._ltid:
# ignore the transaction committed by this connection
continue
changed_oids.update(txn.data.keys())
self._polled_tid = self._ltid = new_tid
return list(changed_oids)
def tpc_finish(self, transaction, func = lambda tid: None):
self._data_snapshot = None
return MappingStorage.tpc_finish(self, transaction, func)
def tpc_abort(self, transaction):
self._data_snapshot = None
MappingStorage.tpc_abort(self, transaction)
def pack(self, t, referencesf, gc=True):
# prevent all concurrent commits during packing
with self._commit_lock:
MappingStorage.pack(self, t, referencesf, gc)
@@ -0,0 +1,50 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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 minimal persistent object to use for tests"""
from persistent import Persistent
class MinPO(Persistent):
def __init__(self, value=None):
self.value = value
def __cmp__(self, aMinPO):
return cmp(self.value, aMinPO.value)
def __hash__(self):
return hash(self.value)
# Py3: Python 3 does not support cmp() anymore. This is insane!!
def __eq__(self, aMinPO):
return self.value == aMinPO.value
def __lt__(self, aMinPO):
return self.value < aMinPO.value
# @functools.total_ordering is not available in 2.6 :-(
def __ne__(self, aMinPO):
return self.value != aMinPO.value
def __gt__(self, aMinPO):
return self.value > aMinPO.value
def __le__(self, aMinPO):
return self.value <= aMinPO.value
def __ge__(self, aMinPO):
return self.value >= aMinPO.value
def __repr__(self):
return "MinPO(%s)" % self.value
@@ -0,0 +1,798 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Run some tests relevant for storages that support pack()."""
from __future__ import print_function
import doctest
import time
from persistent import Persistent
from persistent.mapping import PersistentMapping
from ZODB import DB
from ZODB.POSException import ConflictError, StorageError
from ZODB.serialize import referencesf
from ZODB.tests.MinPO import MinPO
from ZODB.tests.MTStorage import TestThread
from ZODB.tests.StorageTestBase import snooze
from ZODB._compat import (loads, PersistentPickler, Pickler, Unpickler,
BytesIO, _protocol)
import transaction
import ZODB.interfaces
import ZODB.tests.util
from ZODB.tests.util import time_monotonically_increases
import zope.testing.setupstack
from ZODB.utils import load_current
ZERO = b'\0'*8
# This class is for the root object. It must not contain a getoid() method
# (really, attribute). The persistent pickling machinery -- in the dumps()
# function below -- will pickle Root objects as normal, but any attributes
# which reference persistent Object instances will get pickled as persistent
# ids, not as the object's state. This makes the referencesf stuff work,
# because it pickle sniffs for persistent ids (so we have to get those
# persistent ids into the root object's pickle).
class Root(object):
pass
# This is the persistent Object class. Because it has a getoid() method, the
# persistent pickling machinery -- in the dumps() function below -- will
# pickle the oid string instead of the object's actual state. Yee haw, this
# stuff is deep. ;)
class Object(object):
def __init__(self, oid):
self._oid = oid
def getoid(self):
return self._oid
def __setstate__(self, state):
self.__dict__.clear()
self.__dict__.update(state)
if not isinstance(self._oid, bytes):
# Python 3
self._oid = self._oid.encode('ascii')
class C(Persistent):
pass
# Here's where all the magic occurs. Sadly, the pickle module is a bit
# underdocumented, but here's what happens: by setting the persistent_id
# attribute to getpersid() on the pickler, that function gets called for every
# object being pickled. By returning None when the object has no getoid
# attribute, it signals pickle to serialize the object as normal. That's how
# the Root instance gets pickled correctly. But, if the object has a getoid
# attribute, then by returning that method's value, we tell pickle to
# serialize the persistent id of the object instead of the object's state.
# That sets the pickle up for proper sniffing by the referencesf machinery.
# Fun, huh?
def dumps(obj):
def getpersid(obj):
if hasattr(obj, 'getoid'):
return obj.getoid()
return None
s = BytesIO()
p = PersistentPickler(getpersid, s, _protocol)
p.dump(obj)
p.dump(None)
return s.getvalue()
def pdumps(obj):
s = BytesIO()
p = Pickler(s, _protocol)
p.dump(obj)
p.dump(None)
return s.getvalue()
class PackableStorageBase(object):
# We keep a cache of object ids to instances so that the unpickler can
# easily return any persistent object.
@property
def _cache(self):
try:
return self.__cache
except AttributeError:
self.__cache = {}
return self.__cache
def _newobj(self):
# This is a convenience method to create a new persistent Object
# instance. It asks the storage for a new object id, creates the
# instance with the given oid, populates the cache and returns the
# object.
oid = self._storage.new_oid()
obj = Object(oid)
self._cache[obj.getoid()] = obj
return obj
def _makeloader(self):
# This is the other side of the persistent pickling magic. We need a
# custom unpickler to mirror our custom pickler above. By setting the
# persistent_load function of the unpickler to self._cache.get(),
# whenever a persistent id is unpickled, it will actually return the
# Object instance out of the cache. As far as returning a function
# with an argument bound to an instance attribute method, we do it
# this way because it makes the code in the tests more succinct.
#
# BUT! Be careful in your use of loads() vs. pickle.loads(). loads()
# should only be used on the Root object's pickle since it's the only
# special one. All the Object instances should use pickle.loads().
def loads(str, persfunc=self._cache.get):
fp = BytesIO(str)
u = Unpickler(fp)
u.persistent_load = persfunc
return u.load()
return loads
def _initroot(self):
try:
load_current(self._storage, ZERO)
except KeyError:
from ZODB.Connection import TransactionMetaData
file = BytesIO()
p = Pickler(file, _protocol)
p.dump((PersistentMapping, None))
p.dump({'_container': {}})
t = TransactionMetaData()
t.description = u'initial database creation'
self._storage.tpc_begin(t)
self._storage.store(ZERO, None, file.getvalue(), '', t)
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
def _sanity_check(self):
# Iterate over the storage to make sure it's sane.
if not ZODB.interfaces.IStorageIteration.providedBy(self._storage):
return
it = self._storage.iterator()
for txn in it:
for data in txn:
pass
class PackableStorage(PackableStorageBase):
def checkPackEmptyStorage(self):
self._storage.pack(time.time(), referencesf)
def checkPackTomorrow(self):
self._initroot()
self._storage.pack(time.time() + 10000, referencesf)
def checkPackYesterday(self):
self._initroot()
self._storage.pack(time.time() - 10000, referencesf)
def _PackWhileWriting(self, pack_now):
# A storage should allow some reading and writing during
# a pack. This test attempts to exercise locking code
# in the storage to test that it is safe. It generates
# a lot of revisions, so that pack takes a long time.
db = DB(self._storage)
conn = db.open()
root = conn.root()
for i in range(10):
root[i] = MinPO(i)
transaction.commit()
snooze()
packt = time.time()
choices = list(range(10))
for dummy in choices:
for i in choices:
root[i].value = MinPO(i)
transaction.commit()
# How many client threads should we run, and how long should we
# wait for them to finish? Hard to say. Running 4 threads and
# waiting 30 seconds too often left a thread still alive on Tim's
# Win98SE box, during ZEO flavors of this test. Those tend to
# run one thread at a time to completion, and take about 10 seconds
# per thread. There doesn't appear to be a compelling reason to
# run that many threads. Running 3 threads and waiting up to a
# minute seems to work well in practice. The ZEO tests normally
# finish faster than that, and the non-ZEO tests very much faster
# than that.
NUM_LOOP_TRIP = 50
timer = ElapsedTimer(time.time())
threads = [ClientThread(db, choices, NUM_LOOP_TRIP, timer, i)
for i in range(3)]
for t in threads:
t.start()
if pack_now:
db.pack(time.time())
else:
db.pack(packt)
for t in threads:
t.join(60)
liveness = [t.isAlive() for t in threads]
if True in liveness:
# They should have finished by now.
print('Liveness:', liveness)
# Combine the outcomes, and sort by start time.
outcomes = []
for t in threads:
outcomes.extend(t.outcomes)
# each outcome list has as many of these as a loop trip got thru:
# thread_id
# elapsed millis at loop top
# elapsed millis at attempt to assign to self.root[index]
# index into self.root getting replaced
# elapsed millis when outcome known
# 'OK' or 'Conflict'
# True if we got beyond this line, False if it raised an
# exception (one possible Conflict cause):
# self.root[index].value = MinPO(j)
def cmp_by_time(a, b):
return cmp((a[1], a[0]), (b[1], b[0]))
outcomes.sort(cmp_by_time)
counts = [0] * 4
for outcome in outcomes:
n = len(outcome)
assert n >= 2
tid = outcome[0]
print('tid:%d top:%5d' % (tid, outcome[1]), end=' ')
if n > 2:
print('commit:%5d' % outcome[2], end=' ')
if n > 3:
print('index:%2d' % outcome[3], end=' ')
if n > 4:
print('known:%5d' % outcome[4], end=' ')
if n > 5:
print('%8s' % outcome[5], end=' ')
if n > 6:
print('assigned:%5s' % outcome[6], end=' ')
counts[tid] += 1
if counts[tid] == NUM_LOOP_TRIP:
print('thread %d done' % tid, end=' ')
print()
self.fail('a thread is still alive')
self._sanity_check()
db.close()
@time_monotonically_increases
def checkPackWhileWriting(self):
self._PackWhileWriting(pack_now=False)
@time_monotonically_increases
def checkPackNowWhileWriting(self):
self._PackWhileWriting(pack_now=True)
@time_monotonically_increases
def checkPackLotsWhileWriting(self):
# This is like the other pack-while-writing tests, except it packs
# repeatedly until the client thread is done. At the time it was
# introduced, it reliably provoked
# CorruptedError: ... transaction with checkpoint flag set
# in the ZEO flavor of the FileStorage tests.
db = DB(self._storage)
conn = db.open()
root = conn.root()
choices = list(range(10))
for i in choices:
root[i] = MinPO(i)
transaction.commit()
snooze()
packt = time.time()
for dummy in choices:
for i in choices:
root[i].value = MinPO(i)
transaction.commit()
NUM_LOOP_TRIP = 100
timer = ElapsedTimer(time.time())
thread = ClientThread(db, choices, NUM_LOOP_TRIP, timer, 0)
thread.start()
while thread.isAlive():
db.pack(packt)
snooze()
packt = time.time()
thread.join()
self._sanity_check()
db.close()
def checkPackWithMultiDatabaseReferences(self):
databases = {}
db = DB(self._storage, databases=databases, database_name='')
otherdb = ZODB.tests.util.DB(databases=databases, database_name='o')
conn = db.open()
root = conn.root()
root[1] = C()
transaction.commit()
del root[1]
transaction.commit()
root[2] = conn.get_connection('o').root()
transaction.commit()
db.pack(time.time()+1)
# some valid storages always return 0 for len()
self.assertTrue(len(self._storage) in (0, 1))
conn.close()
otherdb.close()
db.close()
def checkPackAllRevisions(self):
self._initroot()
eq = self.assertEqual
raises = self.assertRaises
# Create a `persistent' object
obj = self._newobj()
oid = obj.getoid()
obj.value = 1
# Commit three different revisions
revid1 = self._dostoreNP(oid, data=pdumps(obj))
obj.value = 2
revid2 = self._dostoreNP(oid, revid=revid1, data=pdumps(obj))
obj.value = 3
revid3 = self._dostoreNP(oid, revid=revid2, data=pdumps(obj))
# Now make sure all three revisions can be extracted
data = self._storage.loadSerial(oid, revid1)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 1)
data = self._storage.loadSerial(oid, revid2)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 2)
data = self._storage.loadSerial(oid, revid3)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 3)
# Now pack all transactions; need to sleep a second to make
# sure that the pack time is greater than the last commit time.
now = packtime = time.time()
while packtime <= now:
packtime = time.time()
self._storage.pack(packtime, referencesf)
# All revisions of the object should be gone, since there is no
# reference from the root object to this object.
raises(KeyError, self._storage.loadSerial, oid, revid1)
raises(KeyError, self._storage.loadSerial, oid, revid2)
raises(KeyError, self._storage.loadSerial, oid, revid3)
def checkPackJustOldRevisions(self):
eq = self.assertEqual
raises = self.assertRaises
loads = self._makeloader()
# Create a root object. This can't be an instance of Object,
# otherwise the pickling machinery will serialize it as a persistent
# id and not as an object that contains references (persistent ids) to
# other objects.
root = Root()
# Create a persistent object, with some initial state
obj = self._newobj()
oid = obj.getoid()
# Link the root object to the persistent object, in order to keep the
# persistent object alive. Store the root object.
root.obj = obj
root.value = 0
revid0 = self._dostoreNP(ZERO, data=dumps(root))
# Make sure the root can be retrieved
data, revid = load_current(self._storage, ZERO)
eq(revid, revid0)
eq(loads(data).value, 0)
# Commit three different revisions of the other object
obj.value = 1
revid1 = self._dostoreNP(oid, data=pdumps(obj))
obj.value = 2
revid2 = self._dostoreNP(oid, revid=revid1, data=pdumps(obj))
obj.value = 3
revid3 = self._dostoreNP(oid, revid=revid2, data=pdumps(obj))
# Now make sure all three revisions can be extracted
data = self._storage.loadSerial(oid, revid1)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 1)
data = self._storage.loadSerial(oid, revid2)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 2)
data = self._storage.loadSerial(oid, revid3)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 3)
# Now pack just revisions 1 and 2. The object's current revision
# should stay alive because it's pointed to by the root.
now = packtime = time.time()
while packtime <= now:
packtime = time.time()
self._storage.pack(packtime, referencesf)
# Make sure the revisions are gone, but that object zero and revision
# 3 are still there and correct
data, revid = load_current(self._storage, ZERO)
eq(revid, revid0)
eq(loads(data).value, 0)
raises(KeyError, self._storage.loadSerial, oid, revid1)
raises(KeyError, self._storage.loadSerial, oid, revid2)
data = self._storage.loadSerial(oid, revid3)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 3)
data, revid = load_current(self._storage, oid)
eq(revid, revid3)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 3)
def checkPackOnlyOneObject(self):
eq = self.assertEqual
raises = self.assertRaises
loads = self._makeloader()
# Create a root object. This can't be an instance of Object,
# otherwise the pickling machinery will serialize it as a persistent
# id and not as an object that contains references (persistent ids) to
# other objects.
root = Root()
# Create a persistent object, with some initial state
obj1 = self._newobj()
oid1 = obj1.getoid()
# Create another persistent object, with some initial state.
obj2 = self._newobj()
oid2 = obj2.getoid()
# Link the root object to the persistent objects, in order to keep
# them alive. Store the root object.
root.obj1 = obj1
root.obj2 = obj2
root.value = 0
revid0 = self._dostoreNP(ZERO, data=dumps(root))
# Make sure the root can be retrieved
data, revid = load_current(self._storage, ZERO)
eq(revid, revid0)
eq(loads(data).value, 0)
# Commit three different revisions of the first object
obj1.value = 1
revid1 = self._dostoreNP(oid1, data=pdumps(obj1))
obj1.value = 2
revid2 = self._dostoreNP(oid1, revid=revid1, data=pdumps(obj1))
obj1.value = 3
revid3 = self._dostoreNP(oid1, revid=revid2, data=pdumps(obj1))
# Now make sure all three revisions can be extracted
data = self._storage.loadSerial(oid1, revid1)
pobj = loads(data)
eq(pobj.getoid(), oid1)
eq(pobj.value, 1)
data = self._storage.loadSerial(oid1, revid2)
pobj = loads(data)
eq(pobj.getoid(), oid1)
eq(pobj.value, 2)
data = self._storage.loadSerial(oid1, revid3)
pobj = loads(data)
eq(pobj.getoid(), oid1)
eq(pobj.value, 3)
# Now commit a revision of the second object
obj2.value = 11
revid4 = self._dostoreNP(oid2, data=pdumps(obj2))
# And make sure the revision can be extracted
data = self._storage.loadSerial(oid2, revid4)
pobj = loads(data)
eq(pobj.getoid(), oid2)
eq(pobj.value, 11)
# Now pack just revisions 1 and 2 of object1. Object1's current
# revision should stay alive because it's pointed to by the root, as
# should Object2's current revision.
now = packtime = time.time()
while packtime <= now:
packtime = time.time()
self._storage.pack(packtime, referencesf)
# Make sure the revisions are gone, but that object zero, object2, and
# revision 3 of object1 are still there and correct.
data, revid = load_current(self._storage, ZERO)
eq(revid, revid0)
eq(loads(data).value, 0)
raises(KeyError, self._storage.loadSerial, oid1, revid1)
raises(KeyError, self._storage.loadSerial, oid1, revid2)
data = self._storage.loadSerial(oid1, revid3)
pobj = loads(data)
eq(pobj.getoid(), oid1)
eq(pobj.value, 3)
data, revid = load_current(self._storage, oid1)
eq(revid, revid3)
pobj = loads(data)
eq(pobj.getoid(), oid1)
eq(pobj.value, 3)
data, revid = load_current(self._storage, oid2)
eq(revid, revid4)
eq(loads(data).value, 11)
data = self._storage.loadSerial(oid2, revid4)
pobj = loads(data)
eq(pobj.getoid(), oid2)
eq(pobj.value, 11)
class PackableStorageWithOptionalGC(PackableStorage):
def checkPackAllRevisionsNoGC(self):
self._initroot()
eq = self.assertEqual
raises = self.assertRaises
# Create a `persistent' object
obj = self._newobj()
oid = obj.getoid()
obj.value = 1
# Commit three different revisions
revid1 = self._dostoreNP(oid, data=pdumps(obj))
obj.value = 2
revid2 = self._dostoreNP(oid, revid=revid1, data=pdumps(obj))
obj.value = 3
revid3 = self._dostoreNP(oid, revid=revid2, data=pdumps(obj))
# Now make sure all three revisions can be extracted
data = self._storage.loadSerial(oid, revid1)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 1)
data = self._storage.loadSerial(oid, revid2)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 2)
data = self._storage.loadSerial(oid, revid3)
pobj = loads(data)
eq(pobj.getoid(), oid)
eq(pobj.value, 3)
# Now pack all transactions; need to sleep a second to make
# sure that the pack time is greater than the last commit time.
now = packtime = time.time()
while packtime <= now:
packtime = time.time()
self._storage.pack(packtime, referencesf, gc=False)
# Only old revisions of the object should be gone. We don't gc
raises(KeyError, self._storage.loadSerial, oid, revid1)
raises(KeyError, self._storage.loadSerial, oid, revid2)
self._storage.loadSerial(oid, revid3)
class PackableUndoStorage(PackableStorageBase):
def checkPackUnlinkedFromRoot(self):
eq = self.assertEqual
db = DB(self._storage)
conn = db.open()
root = conn.root()
txn = transaction.get()
txn.note(u'root')
txn.commit()
now = packtime = time.time()
while packtime <= now:
packtime = time.time()
obj = C()
obj.value = 7
root['obj'] = obj
txn = transaction.get()
txn.note(u'root -> o1')
txn.commit()
del root['obj']
txn = transaction.get()
txn.note(u'root -x-> o1')
txn.commit()
self._storage.pack(packtime, referencesf)
log = self._storage.undoLog()
tid = log[0]['id']
db.undo(tid)
txn = transaction.get()
txn.note(u'undo root -x-> o1')
txn.commit()
conn.sync()
eq(root['obj'].value, 7)
@time_monotonically_increases
def checkRedundantPack(self):
# It is an error to perform a pack with a packtime earlier
# than a previous packtime. The storage can't do a full
# traversal as of the packtime, because the previous pack may
# have removed revisions necessary for a full traversal.
# It should be simple to test that a storage error is raised,
# but this test case goes to the trouble of constructing a
# scenario that would lose data if the earlier packtime was
# honored.
self._initroot()
db = DB(self._storage)
conn = db.open()
root = conn.root()
root["d"] = d = PersistentMapping()
transaction.commit()
snooze()
obj = d["obj"] = C()
obj.value = 1
transaction.commit()
snooze()
packt1 = time.time()
lost_oid = obj._p_oid
obj = d["anotherobj"] = C()
obj.value = 2
transaction.commit()
snooze()
packt2 = time.time()
db.pack(packt2)
# BDBStorage allows the second pack, but doesn't lose data.
try:
db.pack(packt1)
except StorageError:
pass
# This object would be removed by the second pack, even though
# it is reachable.
load_current(self._storage, lost_oid)
@time_monotonically_increases(0.1)
def checkPackUndoLog(self):
self._initroot()
# Create a `persistent' object
obj = self._newobj()
oid = obj.getoid()
obj.value = 1
# Commit two different revisions
revid1 = self._dostoreNP(oid, data=pdumps(obj))
obj.value = 2
snooze()
packtime = time.time()
snooze()
self._dostoreNP(oid, revid=revid1, data=pdumps(obj))
# Now pack the first transaction
self.assertEqual(3, len(self._storage.undoLog()))
self._storage.pack(packtime, referencesf)
# The undo log contains only the most resent transaction
self.assertEqual(1, len(self._storage.undoLog()))
def dont_checkPackUndoLogUndoable(self):
# A disabled test. I wanted to test that the content of the
# undo log was consistent, but every storage appears to
# include something slightly different. If the result of this
# method is only used to fill a GUI then this difference
# doesnt matter. Perhaps re-enable this test once we agree
# what should be asserted.
self._initroot()
# Create two `persistent' object
obj1 = self._newobj()
oid1 = obj1.getoid()
obj1.value = 1
obj2 = self._newobj()
oid2 = obj2.getoid()
obj2.value = 2
# Commit the first revision of each of them
revid11 = self._dostoreNP(oid1, data=pdumps(obj1),
description="1-1")
revid22 = self._dostoreNP(oid2, data=pdumps(obj2),
description="2-2")
# remember the time. everything above here will be packed away
snooze()
packtime = time.time()
snooze()
# Commit two revisions of the first object
obj1.value = 3
revid13 = self._dostoreNP(oid1, revid=revid11,
data=pdumps(obj1), description="1-3")
obj1.value = 4
self._dostoreNP(oid1, revid=revid13,
data=pdumps(obj1), description="1-4")
# Commit one revision of the second object
obj2.value = 5
self._dostoreNP(oid2, revid=revid22,
data=pdumps(obj2), description="2-5")
# Now pack
self.assertEqual(6,len(self._storage.undoLog()))
print('\ninitial undoLog was')
for r in self._storage.undoLog(): print(r)
self._storage.pack(packtime, referencesf)
# The undo log contains only two undoable transaction.
print('\nafter packing undoLog was')
for r in self._storage.undoLog(): print(r)
# what can we assert about that?
# A number of these threads are kicked off by _PackWhileWriting(). Their
# purpose is to abuse the database passed to the constructor with lots of
# random write activity while the main thread is packing it.
class ClientThread(TestThread):
def __init__(self, db, choices, loop_trip, timer, thread_id):
TestThread.__init__(self)
self.db = db
self.choices = choices
self.loop_trip = loop_trip
self.millis = timer.elapsed_millis
self.thread_id = thread_id
# list of lists; each list has as many of these as a loop trip
# got thru:
# thread_id
# elapsed millis at loop top
# elapsed millis at attempt
# index into self.root getting replaced
# elapsed millis when outcome known
# 'OK' or 'Conflict'
# True if we got beyond this line, False if it raised an exception:
# self.root[index].value = MinPO(j)
self.outcomes = []
def runtest(self):
from random import choice
conn = self.db.open()
for j in range(self.loop_trip):
assign_worked = False
alist = [self.thread_id, self.millis()]
self.outcomes.append(alist)
try:
index = choice(self.choices)
alist.extend([self.millis(), index])
conn.root()[index].value = MinPO(j)
assign_worked = True
transaction.commit()
alist.append(self.millis())
alist.append('OK')
except ConflictError:
alist.append(self.millis())
alist.append('Conflict')
transaction.abort()
alist.append(assign_worked)
conn.close()
class ElapsedTimer(object):
def __init__(self, start_time):
self.start_time = start_time
def elapsed_millis(self):
return int((time.time() - self.start_time) * 1000)
def IExternalGC_suite(factory):
"""Return a test suite for a generic .
Pass a factory taking a name and a blob directory name.
"""
def setup(test):
ZODB.tests.util.setUp(test)
test.globs['create_storage'] = factory
return doctest.DocFileSuite(
'IExternalGC.test',
setUp=setup, tearDown=ZODB.tests.util.tearDown,
checker=ZODB.tests.util.checker)
@@ -0,0 +1,51 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Test that a storage's values persist across open and close."""
from ZODB.utils import load_current
class PersistentStorage(object):
def checkUpdatesPersist(self):
oids = []
def new_oid_wrapper(l=oids, new_oid=self._storage.new_oid):
oid = new_oid()
l.append(oid)
return oid
self._storage.new_oid = new_oid_wrapper
self._dostore()
oid = self._storage.new_oid()
revid = self._dostore(oid)
oid = self._storage.new_oid()
revid = self._dostore(oid, data=1)
revid = self._dostore(oid, revid, data=2)
self._dostore(oid, revid, data=3)
# keep copies of all the objects
objects = []
for oid in oids:
p, s = load_current(self._storage, oid)
objects.append((oid, '', p, s))
self._storage.close()
self.open()
# keep copies of all the objects
for oid, ver, p, s in objects:
_p, _s = load_current(self._storage, oid)
self.assertEqual(p, _p)
self.assertEqual(s, _s)
@@ -0,0 +1,58 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
from ZODB.Connection import TransactionMetaData
from ZODB.POSException import ReadOnlyError, Unsupported
from ZODB.utils import load_current
class ReadOnlyStorage(object):
def _create_data(self):
# test a read-only storage that already has some data
self.oids = {}
for i in range(10):
oid = self._storage.new_oid()
revid = self._dostore(oid)
self.oids[oid] = revid
def _make_readonly(self):
self._storage.close()
self.open(read_only=True)
self.assertTrue(self._storage.isReadOnly())
def checkReadMethods(self):
self._create_data()
self._make_readonly()
# Note that this doesn't check _all_ read methods.
for oid in self.oids.keys():
data, revid = load_current(self._storage, oid)
self.assertEqual(revid, self.oids[oid])
# Storages without revisions may not have loadSerial().
try:
_data = self._storage.loadSerial(oid, revid)
self.assertEqual(data, _data)
except Unsupported:
pass
def checkWriteMethods(self):
self._make_readonly()
self.assertRaises(ReadOnlyError, self._storage.new_oid)
t = TransactionMetaData()
self.assertRaises(ReadOnlyError, self._storage.tpc_begin, t)
self.assertRaises(ReadOnlyError, self._storage.store,
b'\000' * 8, None, b'', '', t)
self.assertRaises(ReadOnlyError, self._storage.undo,
b'\000' * 8, t)
@@ -0,0 +1,200 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""More recovery and iterator tests."""
import transaction
from ZODB.Connection import TransactionMetaData
from ZODB.tests.IteratorStorage import IteratorDeepCompare
from ZODB.tests.StorageTestBase import MinPO, snooze
from ZODB import DB
from ZODB.serialize import referencesf
from ZODB.utils import load_current
from ZODB.tests.util import time_monotonically_increases
import time
class RecoveryStorage(IteratorDeepCompare):
# Requires a setUp() that creates a self._dst destination storage
def checkSimpleRecovery(self):
oid = self._storage.new_oid()
revid = self._dostore(oid, data=11)
revid = self._dostore(oid, revid=revid, data=12)
revid = self._dostore(oid, revid=revid, data=13)
self._dst.copyTransactionsFrom(self._storage)
self.compare(self._storage, self._dst)
def checkRestoreAcrossPack(self):
db = DB(self._storage)
c = db.open()
r = c.root()
obj = r["obj1"] = MinPO(1)
transaction.commit()
obj = r["obj2"] = MinPO(1)
transaction.commit()
self._dst.copyTransactionsFrom(self._storage)
self._dst.pack(time.time(), referencesf)
self._undo(self._storage.undoInfo()[0]['id'])
# copy the final transaction manually. even though there
# was a pack, the restore() ought to succeed.
it = self._storage.iterator()
# Get the last transaction and its record iterator. Record iterators
# can't be accessed out-of-order, so we need to do this in a bit
# complicated way:
for final in it:
records = list(final)
self._dst.tpc_begin(final, final.tid, final.status)
for r in records:
self._dst.restore(r.oid, r.tid, r.data, '', r.data_txn,
final)
self._dst.tpc_vote(final)
self._dst.tpc_finish(final)
@time_monotonically_increases
def checkPackWithGCOnDestinationAfterRestore(self):
raises = self.assertRaises
db = DB(self._storage)
conn = db.open()
root = conn.root()
root.obj = obj1 = MinPO(1)
txn = transaction.get()
txn.note(u'root -> obj')
txn.commit()
root.obj.obj = obj2 = MinPO(2)
txn = transaction.get()
txn.note(u'root -> obj -> obj')
txn.commit()
del root.obj
txn = transaction.get()
txn.note(u'root -X->')
txn.commit()
# Now copy the transactions to the destination
self._dst.copyTransactionsFrom(self._storage)
# Now pack the destination.
snooze()
self._dst.pack(time.time(), referencesf)
# And check to see that the root object exists, but not the other
# objects.
data, serial = load_current(self._dst, root._p_oid)
raises(KeyError, load_current, self._dst, obj1._p_oid)
raises(KeyError, load_current, self._dst, obj2._p_oid)
def checkRestoreWithMultipleObjectsInUndoRedo(self):
from ZODB.FileStorage import FileStorage
# Undo creates backpointers in (at least) FileStorage. ZODB 3.2.1
# FileStorage._data_find() had an off-by-8 error, neglecting to
# account for the size of the backpointer when searching a
# transaction with multiple data records. The results were
# unpredictable. For example, it could raise a Python exception
# due to passing a negative offset to file.seek(), or could
# claim that a transaction didn't have data for an oid despite
# that it actually did.
#
# The former failure mode was seen in real life, in a ZRS secondary
# doing recovery. On my box today, the second failure mode is
# what happens in this test (with an unpatched _data_find, of
# course). Note that the error can only "bite" if more than one
# data record is in a transaction, and the oid we're looking for
# follows at least one data record with a backpointer.
#
# Unfortunately, _data_find() is a low-level implementation detail,
# and this test does some horrid white-box abuse to test it.
is_filestorage = isinstance(self._storage, FileStorage)
db = DB(self._storage)
c = db.open()
r = c.root()
# Create some objects.
r["obj1"] = MinPO(1)
r["obj2"] = MinPO(1)
transaction.commit()
# Add x attributes to them.
r["obj1"].x = 'x1'
r["obj2"].x = 'x2'
transaction.commit()
r = db.open().root()
self.assertEqual(r["obj1"].x, 'x1')
self.assertEqual(r["obj2"].x, 'x2')
# Dirty tricks.
if is_filestorage:
obj1_oid = r["obj1"]._p_oid
obj2_oid = r["obj2"]._p_oid
# This will be the offset of the next transaction, which
# will contain two backpointers.
pos = self._storage.getSize()
# Undo the attribute creation.
info = self._storage.undoInfo()
tid = info[0]['id']
t = TransactionMetaData()
self._storage.tpc_begin(t)
oids = self._storage.undo(tid, t)
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
r = db.open().root()
self.assertRaises(AttributeError, getattr, r["obj1"], 'x')
self.assertRaises(AttributeError, getattr, r["obj2"], 'x')
if is_filestorage:
# _data_find should find data records for both objects in that
# transaction. Without the patch, the second assert failed
# (it claimed it couldn't find a data record for obj2) on my
# box, but other failure modes were possible.
self.assertTrue(self._storage._data_find(pos, obj1_oid, '') > 0)
self.assertTrue(self._storage._data_find(pos, obj2_oid, '') > 0)
# The offset of the next ("redo") transaction.
pos = self._storage.getSize()
# Undo the undo (restore the attributes).
info = self._storage.undoInfo()
tid = info[0]['id']
t = TransactionMetaData()
self._storage.tpc_begin(t)
oids = self._storage.undo(tid, t)
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
r = db.open().root()
self.assertEqual(r["obj1"].x, 'x1')
self.assertEqual(r["obj2"].x, 'x2')
if is_filestorage:
# Again _data_find should find both objects in this txn, and
# again the second assert failed on my box.
self.assertTrue(self._storage._data_find(pos, obj1_oid, '') > 0)
self.assertTrue(self._storage._data_find(pos, obj2_oid, '') > 0)
# Indirectly provoke .restore(). .restore in turn indirectly
# provokes _data_find too, but not usefully for the purposes of
# the specific bug this test aims at: copyTransactionsFrom() uses
# storage iterators that chase backpointers themselves, and
# return the data they point at instead. The result is that
# _data_find didn't actually see anything dangerous in this
# part of the test.
self._dst.copyTransactionsFrom(self._storage)
self.compare(self._storage, self._dst)
@@ -0,0 +1,175 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Check loadSerial() on storages that support historical revisions."""
from ZODB.Connection import TransactionMetaData
from ZODB.tests.MinPO import MinPO
from ZODB.tests.StorageTestBase import zodb_unpickle, zodb_pickle, snooze
from ZODB.utils import p64, u64, load_current
from ZODB.tests.util import time_monotonically_increases
ZERO = '\0'*8
class RevisionStorage(object):
def checkLoadSerial(self):
oid = self._storage.new_oid()
revid = ZERO
revisions = {}
for i in range(31, 38):
revid = self._dostore(oid, revid=revid, data=MinPO(i))
revisions[revid] = MinPO(i)
# Now make sure all the revisions have the correct value
for revid, value in revisions.items():
data = self._storage.loadSerial(oid, revid)
self.assertEqual(zodb_unpickle(data), value)
@time_monotonically_increases
def checkLoadBefore(self):
# Store 10 revisions of one object and then make sure that we
# can get all the non-current revisions back.
oid = self._storage.new_oid()
revs = []
revid = None
for i in range(10):
# We need to ensure that successive timestamps are at least
# two apart, so that a timestamp exists that's unambiguously
# between successive timestamps. Each call to snooze()
# guarantees that the next timestamp will be at least one
# larger (and probably much more than that) than the previous
# one.
snooze()
snooze()
revid = self._dostore(oid, revid, data=MinPO(i))
revs.append(load_current(self._storage, oid))
prev = u64(revs[0][1])
for i in range(1, 10):
tid = revs[i][1]
cur = u64(tid)
middle = prev + (cur - prev) // 2
assert prev < middle < cur # else the snooze() trick failed
prev = cur
t = self._storage.loadBefore(oid, p64(middle))
self.assertTrue(t is not None)
data, start, end = t
self.assertEqual(revs[i-1][0], data)
self.assertEqual(tid, end)
def checkLoadBeforeEdges(self):
# Check the edges cases for a non-current load.
oid = self._storage.new_oid()
self.assertRaises(KeyError, self._storage.loadBefore,
oid, p64(0))
revid1 = self._dostore(oid, data=MinPO(1))
self.assertEqual(self._storage.loadBefore(oid, p64(0)), None)
self.assertEqual(self._storage.loadBefore(oid, revid1), None)
cur = p64(u64(revid1) + 1)
data, start, end = self._storage.loadBefore(oid, cur)
self.assertEqual(zodb_unpickle(data), MinPO(1))
self.assertEqual(start, revid1)
self.assertEqual(end, None)
revid2 = self._dostore(oid, revid=revid1, data=MinPO(2))
data, start, end = self._storage.loadBefore(oid, cur)
self.assertEqual(zodb_unpickle(data), MinPO(1))
self.assertEqual(start, revid1)
self.assertEqual(end, revid2)
@time_monotonically_increases
def checkLoadBeforeOld(self):
# Look for a very old revision. With the BaseStorage implementation
# this should require multple history() calls.
oid = self._storage.new_oid()
revs = []
revid = None
for i in range(50):
revid = self._dostore(oid, revid, data=MinPO(i))
revs.append(revid)
data, start, end = self._storage.loadBefore(oid, revs[12])
self.assertEqual(zodb_unpickle(data), MinPO(11))
self.assertEqual(start, revs[11])
self.assertEqual(end, revs[12])
# Unsure: Is it okay to assume everyone testing against RevisionStorage
# implements undo?
def checkLoadBeforeUndo(self):
# Do several transactions then undo them.
oid = self._storage.new_oid()
revid = None
for i in range(5):
revid = self._dostore(oid, revid, data=MinPO(i))
revs = []
for i in range(4):
info = self._storage.undoInfo()
tid = info[0]["id"]
# Always undo the most recent txn, so the value will
# alternate between 3 and 4.
self._undo(tid, note="undo %d" % i)
revs.append(load_current(self._storage, oid))
prev_tid = None
for i, (data, tid) in enumerate(revs):
t = self._storage.loadBefore(oid, p64(u64(tid) + 1))
self.assertEqual(data, t[0])
self.assertEqual(tid, t[1])
if prev_tid:
self.assertTrue(prev_tid < t[1])
prev_tid = t[1]
if i < 3:
self.assertEqual(revs[i+1][1], t[2])
else:
self.assertEqual(None, t[2])
def checkLoadBeforeConsecutiveTids(self):
eq = self.assertEqual
oid = self._storage.new_oid()
def helper(tid, revid, x):
data = zodb_pickle(MinPO(x))
t = TransactionMetaData()
try:
self._storage.tpc_begin(t, p64(tid))
self._storage.store(oid, revid, data, '', t)
# Finish the transaction
self._storage.tpc_vote(t)
newrevid = self._storage.tpc_finish(t)
except:
self._storage.tpc_abort(t)
raise
return newrevid
revid1 = helper(1, None, 1)
revid2 = helper(2, revid1, 2)
revid3 = helper(3, revid2, 3)
data, start_tid, end_tid = self._storage.loadBefore(oid, p64(2))
eq(zodb_unpickle(data), MinPO(1))
eq(u64(start_tid), 1)
eq(u64(end_tid), 2)
def checkLoadBeforeCreation(self):
eq = self.assertEqual
oid1 = self._storage.new_oid()
oid2 = self._storage.new_oid()
revid1 = self._dostore(oid1)
revid2 = self._dostore(oid2)
results = self._storage.loadBefore(oid2, revid2)
eq(results, None)
# TODO: There are other edge cases to handle, including pack.
@@ -0,0 +1,183 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Provide a mixin base class for storage tests.
The StorageTestBase class provides basic setUp() and tearDown()
semantics (which you can override), and it also provides a helper
method _dostore() which performs a complete store transaction for a
single object revision.
"""
from __future__ import print_function
import sys
import time
from ZODB.Connection import TransactionMetaData
from ZODB.utils import u64, z64
from ZODB.tests.MinPO import MinPO
from ZODB._compat import PersistentPickler, Unpickler, BytesIO, _protocol
import ZODB.tests.util
ZERO = b'\0'*8
def snooze():
# In Windows, it's possible that two successive time.time() calls return
# the same value. Tim guarantees that time never runs backwards. You
# usually want to call this before you pack a storage, or must make other
# guarantees about increasing timestamps.
now = time.time()
while now == time.time():
time.sleep(0.1)
def _persistent_id(obj):
oid = getattr(obj, "_p_oid", None)
if getattr(oid, "__get__", None) is not None:
return None
else:
return oid
def zodb_pickle(obj):
"""Create a pickle in the format expected by ZODB."""
f = BytesIO()
p = PersistentPickler(_persistent_id, f, _protocol)
klass = obj.__class__
assert not hasattr(obj, '__getinitargs__'), "not ready for constructors"
args = None
mod = getattr(klass, '__module__', None)
if mod is not None:
klass = mod, klass.__name__
state = obj.__getstate__()
p.dump((klass, args))
p.dump(state)
return f.getvalue()
def persistent_load(pid):
# helper for zodb_unpickle
return "ref to %s.%s oid=%s" % (pid[1][0], pid[1][1], u64(pid[0]))
def zodb_unpickle(data):
"""Unpickle an object stored using the format expected by ZODB."""
f = BytesIO(data)
u = Unpickler(f)
u.persistent_load = persistent_load
klass_info = u.load()
if isinstance(klass_info, tuple):
if isinstance(klass_info[0], type):
# Unclear: what is the second part of klass_info?
klass, xxx = klass_info
assert not xxx
else:
if isinstance(klass_info[0], tuple):
modname, klassname = klass_info[0]
else:
modname, klassname = klass_info
if modname == "__main__":
ns = globals()
else:
mod = import_helper(modname)
ns = mod.__dict__
try:
klass = ns[klassname]
except KeyError:
print("can't find %s in %r" % (klassname, ns), file=sys.stderr)
inst = klass()
else:
raise ValueError("expected class info: %s" % repr(klass_info))
state = u.load()
inst.__setstate__(state)
return inst
def import_helper(name):
__import__(name)
return sys.modules[name]
class StorageTestBase(ZODB.tests.util.TestCase):
# It would be simpler if concrete tests didn't need to extend
# setUp() and tearDown().
_storage = None
def _close(self):
# You should override this if closing your storage requires additional
# shutdown operations.
if self._storage is not None:
self._storage.close()
def tearDown(self):
self._close()
ZODB.tests.util.TestCase.tearDown(self)
def _dostore(self, oid=None, revid=None, data=None,
already_pickled=0, user=None, description=None):
"""Do a complete storage transaction. The defaults are:
- oid=None, ask the storage for a new oid
- revid=None, use a revid of ZERO
- data=None, pickle up some arbitrary data (the integer 7)
Returns the object's new revision id.
"""
if oid is None:
oid = self._storage.new_oid()
if revid is None:
revid = ZERO
if data is None:
data = MinPO(7)
if type(data) == int:
data = MinPO(data)
if not already_pickled:
data = zodb_pickle(data)
# Begin the transaction
t = TransactionMetaData()
if user is not None:
t.user = user
if description is not None:
t.description = description
try:
self._storage.tpc_begin(t)
# Store an object
r1 = self._storage.store(oid, revid, data, '', t)
# Finish the transaction
r2 = self._storage.tpc_vote(t)
revid = self._storage.tpc_finish(t)
except:
self._storage.tpc_abort(t)
raise
return revid
def _dostoreNP(self, oid=None, revid=None, data=None,
user=None, description=None):
return self._dostore(oid, revid, data, 1, user, description)
# The following methods depend on optional storage features.
def _undo(self, tid, expected_oids=None, note=None):
# Undo a tid that affects a single object (oid).
# This is very specialized.
t = TransactionMetaData()
t.note(note or u"undo")
self._storage.tpc_begin(t)
undo_result = self._storage.undo(tid, t)
vote_result = self._storage.tpc_vote(t)
if expected_oids is not None:
oids = set(undo_result[1]) if undo_result else set()
if vote_result:
oids.update(vote_result)
self.assertEqual(oids, set(expected_oids))
return self._storage.tpc_finish(t)
@@ -0,0 +1,118 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Test the storage's implemenetation of the storage synchronization spec.
The Synchronization spec
http://www.zope.org/Documentation/Developer/Models/ZODB/
ZODB_Architecture_Storage_Interface_State_Synchronization_Diag.html
It specifies two states committing and non-committing. A storage
starts in the non-committing state. tpc_begin() transfers to the
committting state; tpc_abort() and tpc_finish() transfer back to
non-committing.
Several other methods are only allowed in one state or another. Many
methods allowed only in the committing state require that they apply
to the currently committing transaction.
The spec is silent on a variety of methods that don't appear to modify
the state, e.g. load(), undoLog(), pack(). It's unclear whether there
is a separate set of synchronization rules that apply to these methods
or if the synchronization is implementation dependent, i.e. only what
is need to guarantee a corrected implementation.
The synchronization spec is also silent on whether there is any
contract implied with the caller. If the storage can assume that a
single client is single-threaded and that it will not call, e.g., store()
until after it calls tpc_begin(), the implementation can be
substantially simplified.
New and/or unspecified methods:
tpc_vote(): handled like tpc_abort
undo(): how's that handled?
Methods that have nothing to do with committing/non-committing:
load(), loadSerial(), getName(), getSize(), __len__(), history(),
undoLog(), pack().
Specific questions:
The spec & docs say that undo() takes three arguments, the second
being a transaction. If the specified arg isn't the current
transaction, the undo() should raise StorageTransactionError. This
isn't implemented anywhere. It looks like undo can be called at
anytime.
FileStorage does not allow undo() during a pack. How should this be
tested? Is it a general restriction?
"""
from ZODB.Connection import TransactionMetaData
from ZODB.POSException import StorageTransactionError
OID = "\000" * 8
SERIALNO = "\000" * 8
TID = "\000" * 8
class SynchronizedStorage(object):
def verifyNotCommitting(self, callable, *args):
self.assertRaises(StorageTransactionError, callable, *args)
def verifyWrongTrans(self, callable, *args):
t = TransactionMetaData()
self._storage.tpc_begin(t)
self.assertRaises(StorageTransactionError, callable, *args)
self._storage.tpc_abort(t)
def checkStoreNotCommitting(self):
self.verifyNotCommitting(self._storage.store,
OID, SERIALNO, b"", "", TransactionMetaData())
def checkStoreWrongTrans(self):
self.verifyWrongTrans(self._storage.store,
OID, SERIALNO, b"", "", TransactionMetaData())
def checkAbortNotCommitting(self):
self._storage.tpc_abort(TransactionMetaData())
def checkAbortWrongTrans(self):
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.tpc_abort(TransactionMetaData())
self._storage.tpc_abort(t)
def checkFinishNotCommitting(self):
t = TransactionMetaData()
self.assertRaises(StorageTransactionError,
self._storage.tpc_finish, t)
self._storage.tpc_abort(t)
def checkFinishWrongTrans(self):
t = TransactionMetaData()
self._storage.tpc_begin(t)
self.assertRaises(StorageTransactionError,
self._storage.tpc_finish, TransactionMetaData())
self._storage.tpc_abort(t)
def checkBeginCommitting(self):
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.tpc_abort(t)
# TODO: how to check undo?
@@ -0,0 +1,773 @@
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation 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.
#
##############################################################################
"""Check undo().
Any storage that supports undo() must pass these tests.
"""
import time
from six import PY3
from persistent import Persistent
import transaction
from transaction import Transaction
from ZODB import POSException
from ZODB.Connection import TransactionMetaData
from ZODB.serialize import referencesf
from ZODB.utils import p64, load_current
from ZODB import DB
from ZODB.tests.MinPO import MinPO
from ZODB.tests.StorageTestBase import zodb_pickle, zodb_unpickle
ZERO = '\0'*8
class C(Persistent):
pass
def snooze():
# In Windows, it's possible that two successive time.time() calls return
# the same value. Tim guarantees that time never runs backwards. You
# usually want to call this before you pack a storage, or must make other
# guarantees about increasing timestamps.
now = time.time()
while now == time.time():
time.sleep(0.1)
def listeq(L1, L2):
"""Return True if L1.sort() == L2.sort()
Also support iterators.
"""
return sorted(L1) == sorted(L2)
class TransactionalUndoStorage(object):
def _multi_obj_transaction(self, objs):
t = TransactionMetaData()
self._storage.tpc_begin(t)
for oid, rev, data in objs:
self._storage.store(oid, rev, data, '', t)
self._storage.tpc_vote(t)
return self._storage.tpc_finish(t)
def _iterate(self):
"""Iterate over the storage in its final state."""
# This is testing that the iterator() code works correctly.
# The hasattr() guards against ZEO, which doesn't support iterator.
if not hasattr(self._storage, "iterator"):
return
iter = self._storage.iterator()
for txn in iter:
for rec in txn:
pass
def _begin_undos_vote(self, t, *tids):
self._storage.tpc_begin(t)
oids = set()
for tid in tids:
undo_result = self._storage.undo(tid, t)
if undo_result:
oids.update(undo_result[1])
oids.update(self._storage.tpc_vote(t) or ())
return oids
def undo(self, tid, note=None):
t = TransactionMetaData()
if note is not None:
t.note(note)
oids = self._begin_undos_vote(t, tid)
self._storage.tpc_finish(t)
return oids
def checkSimpleTransactionalUndo(self):
eq = self.assertEqual
oid = self._storage.new_oid()
revid = self._dostore(oid, data=MinPO(23))
revid = self._dostore(oid, revid=revid, data=MinPO(24))
revid = self._dostore(oid, revid=revid, data=MinPO(25))
info = self._storage.undoInfo()
# Now start an undo transaction
self._undo(info[0]["id"], [oid], note="undo1")
data, revid = load_current(self._storage, oid)
eq(zodb_unpickle(data), MinPO(24))
# Do another one
info = self._storage.undoInfo()
self._undo(info[2]["id"], [oid], note="undo2")
data, revid = load_current(self._storage, oid)
eq(zodb_unpickle(data), MinPO(23))
# Try to undo the first record
info = self._storage.undoInfo()
self._undo(info[4]["id"], [oid], note="undo3")
# This should fail since we've undone the object's creation
self.assertRaises(KeyError, load_current, self._storage, oid)
# And now let's try to redo the object's creation
info = self._storage.undoInfo()
self._undo(info[0]["id"], [oid])
data, revid = load_current(self._storage, oid)
eq(zodb_unpickle(data), MinPO(23))
self._iterate()
def checkCreationUndoneGetTid(self):
# create an object
oid = self._storage.new_oid()
self._dostore(oid, data=MinPO(23))
# undo its creation
info = self._storage.undoInfo()
tid = info[0]['id']
self.undo(tid, 'undo1')
# Check that calling getTid on an uncreated object raises a KeyError
# The current version of FileStorage fails this test
self.assertRaises(KeyError, self._storage.getTid, oid)
def checkUndoCreationBranch1(self):
eq = self.assertEqual
oid = self._storage.new_oid()
revid = self._dostore(oid, data=MinPO(11))
revid = self._dostore(oid, revid=revid, data=MinPO(12))
# Undo the last transaction
info = self._storage.undoInfo()
self._undo(info[0]['id'], [oid])
data, revid = load_current(self._storage, oid)
eq(zodb_unpickle(data), MinPO(11))
# Now from here, we can either redo the last undo, or undo the object
# creation. Let's undo the object creation.
info = self._storage.undoInfo()
self._undo(info[2]['id'], [oid])
self.assertRaises(KeyError, load_current, self._storage, oid)
# Loading current data via loadBefore should raise a POSKeyError too:
self.assertRaises(KeyError, self._storage.loadBefore, oid,
b'\x7f\xff\xff\xff\xff\xff\xff\xff')
self._iterate()
def checkUndoCreationBranch2(self):
eq = self.assertEqual
oid = self._storage.new_oid()
revid = self._dostore(oid, data=MinPO(11))
revid = self._dostore(oid, revid=revid, data=MinPO(12))
# Undo the last transaction
info = self._storage.undoInfo()
self._undo(info[0]['id'], [oid])
data, revid = load_current(self._storage, oid)
eq(zodb_unpickle(data), MinPO(11))
# Now from here, we can either redo the last undo, or undo the object
# creation. Let's redo the last undo
info = self._storage.undoInfo()
self._undo(info[0]['id'], [oid])
data, revid = load_current(self._storage, oid)
eq(zodb_unpickle(data), MinPO(12))
self._iterate()
def checkTwoObjectUndo(self):
eq = self.assertEqual
# Convenience
p31, p32, p51, p52 = map(zodb_pickle,
map(MinPO, (31, 32, 51, 52)))
oid1 = self._storage.new_oid()
oid2 = self._storage.new_oid()
revid1 = revid2 = ZERO
# Store two objects in the same transaction
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(oid1, revid1, p31, '', t)
self._storage.store(oid2, revid2, p51, '', t)
# Finish the transaction
self._storage.tpc_vote(t)
tid = self._storage.tpc_finish(t)
# Update those same two objects
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(oid1, tid, p32, '', t)
self._storage.store(oid2, tid, p52, '', t)
# Finish the transaction
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
# Make sure the objects have the current value
data, revid1 = load_current(self._storage, oid1)
eq(zodb_unpickle(data), MinPO(32))
data, revid2 = load_current(self._storage, oid2)
eq(zodb_unpickle(data), MinPO(52))
# Now attempt to undo the transaction containing two objects
info = self._storage.undoInfo()
self._undo(info[0]['id'], [oid1, oid2])
data, revid1 = load_current(self._storage, oid1)
eq(zodb_unpickle(data), MinPO(31))
data, revid2 = load_current(self._storage, oid2)
eq(zodb_unpickle(data), MinPO(51))
self._iterate()
def checkTwoObjectUndoAtOnce(self):
# Convenience
eq = self.assertEqual
unless = self.assertTrue
p30, p31, p32, p50, p51, p52 = map(zodb_pickle,
map(MinPO,
(30, 31, 32, 50, 51, 52)))
oid1 = self._storage.new_oid()
oid2 = self._storage.new_oid()
# Store two objects in the same transaction
tid = self._multi_obj_transaction([(oid1, ZERO, p30),
(oid2, ZERO, p50),
])
# Update those same two objects
tid = self._multi_obj_transaction([(oid1, tid, p31),
(oid2, tid, p51),
])
# Update those same two objects
tid = self._multi_obj_transaction([(oid1, tid, p32),
(oid2, tid, p52),
])
# Make sure the objects have the current value
data, revid1 = load_current(self._storage, oid1)
eq(zodb_unpickle(data), MinPO(32))
data, revid2 = load_current(self._storage, oid2)
eq(zodb_unpickle(data), MinPO(52))
# Now attempt to undo the transaction containing two objects
info = self._storage.undoInfo()
tid = info[0]['id']
tid1 = info[1]['id']
t = TransactionMetaData()
oids = self._begin_undos_vote(t, tid, tid1)
serial = self._storage.tpc_finish(t)
# We may get the finalization stuff called an extra time,
# depending on the implementation.
if serial is None:
self.assertEqual(oids, {oid1, oid2})
data, revid1 = load_current(self._storage, oid1)
eq(zodb_unpickle(data), MinPO(30))
data, revid2 = load_current(self._storage, oid2)
eq(zodb_unpickle(data), MinPO(50))
# Now try to undo the one we just did to undo, whew
info = self._storage.undoInfo()
self._undo(info[0]['id'], [oid1, oid2])
data, revid1 = load_current(self._storage, oid1)
eq(zodb_unpickle(data), MinPO(32))
data, revid2 = load_current(self._storage, oid2)
eq(zodb_unpickle(data), MinPO(52))
self._iterate()
def checkTwoObjectUndoAgain(self):
eq = self.assertEqual
p31, p32, p33, p51, p52, p53 = map(
zodb_pickle,
map(MinPO, (31, 32, 33, 51, 52, 53)))
# Like the above, but the first revision of the objects are stored in
# different transactions.
oid1 = self._storage.new_oid()
oid2 = self._storage.new_oid()
revid1 = self._dostore(oid1, data=p31, already_pickled=1)
revid2 = self._dostore(oid2, data=p51, already_pickled=1)
# Update those same two objects
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(oid1, revid1, p32, '', t)
self._storage.store(oid2, revid2, p52, '', t)
# Finish the transaction
self._storage.tpc_vote(t)
self._storage.tpc_finish(t)
# Now attempt to undo the transaction containing two objects
info = self._storage.undoInfo()
self._undo(info[0]["id"], [oid1, oid2])
data, revid1 = load_current(self._storage, oid1)
eq(zodb_unpickle(data), MinPO(31))
data, revid2 = load_current(self._storage, oid2)
eq(zodb_unpickle(data), MinPO(51))
# Like the above, but this time, the second transaction contains only
# one object.
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(oid1, revid1, p33, '', t)
self._storage.store(oid2, revid2, p53, '', t)
# Finish the transaction
self._storage.tpc_vote(t)
tid = self._storage.tpc_finish(t)
# Update in different transactions
revid1 = self._dostore(oid1, revid=tid, data=MinPO(34))
revid2 = self._dostore(oid2, revid=tid, data=MinPO(54))
# Now attempt to undo the transaction containing two objects
info = self._storage.undoInfo()
self.undo(info[1]['id'])
data, revid1 = load_current(self._storage, oid1)
eq(zodb_unpickle(data), MinPO(33))
data, revid2 = load_current(self._storage, oid2)
eq(zodb_unpickle(data), MinPO(54))
self._iterate()
def checkNotUndoable(self):
eq = self.assertEqual
# Set things up so we've got a transaction that can't be undone
oid = self._storage.new_oid()
revid_a = self._dostore(oid, data=MinPO(51))
revid_b = self._dostore(oid, revid=revid_a, data=MinPO(52))
revid_c = self._dostore(oid, revid=revid_b, data=MinPO(53))
# Start the undo
info = self._storage.undoInfo()
tid = info[1]['id']
t = TransactionMetaData()
self.assertRaises(POSException.UndoError,
self._begin_undos_vote, t, tid)
self._storage.tpc_abort(t)
# Now have more fun: object1 and object2 are in the same transaction,
# which we'll try to undo to, but one of them has since modified in
# different transaction, so the undo should fail.
oid1 = oid
revid1 = revid_c
oid2 = self._storage.new_oid()
revid2 = ZERO
p81, p82, p91, p92 = map(zodb_pickle,
map(MinPO, (81, 82, 91, 92)))
t = TransactionMetaData()
self._storage.tpc_begin(t)
self._storage.store(oid1, revid1, p81, '', t)
self._storage.store(oid2, revid2, p91, '', t)
self._storage.tpc_vote(t)
tid = self._storage.tpc_finish(t)
# Make sure the objects have the expected values
data, revid_11 = load_current(self._storage, oid1)
eq(zodb_unpickle(data), MinPO(81))
data, revid_22 = load_current(self._storage, oid2)
eq(zodb_unpickle(data), MinPO(91))
eq(revid_11, tid)
eq(revid_22, tid)
# Now modify oid2
revid2 = self._dostore(oid2, tid, MinPO(92))
self.assertNotEqual(tid, revid2)
info = self._storage.undoInfo()
tid = info[1]['id']
t = TransactionMetaData()
self.assertRaises(POSException.UndoError,
self._begin_undos_vote, t, tid)
self._storage.tpc_abort(t)
self._iterate()
def checkTransactionalUndoAfterPack(self):
# bwarsaw Date: Thu Mar 28 21:04:43 2002 UTC
# This is a test which should provoke the underlying bug in
# transactionalUndo() on a standby storage. If our hypothesis
# is correct, the bug is in FileStorage, and is caused by
# encoding the file position in the `id' field of the undoLog
# information. Note that Full just encodes the tid, but this
# is a problem for FileStorage (we have a strategy for fixing
# this).
# So, basically, this makes sure that undo info doesn't depend
# on file positions. We change the file positions in an undo
# record by packing.
# Add a few object revisions
oid = b'\0'*8
revid0 = self._dostore(oid, data=MinPO(50))
revid1 = self._dostore(oid, revid=revid0, data=MinPO(51))
snooze()
packtime = time.time()
snooze() # time.time() now distinct from packtime
revid2 = self._dostore(oid, revid=revid1, data=MinPO(52))
self._dostore(oid, revid=revid2, data=MinPO(53))
# Now get the undo log
info = self._storage.undoInfo()
self.assertEqual(len(info), 4)
tid = info[0]['id']
# Now pack just the initial revision of the object. We need the
# second revision otherwise we won't be able to undo the third
# revision!
self._storage.pack(packtime, referencesf)
# Make some basic assertions about the undo information now
info2 = self._storage.undoInfo()
self.assertEqual(len(info2), 2)
# And now attempt to undo the last transaction
undone, = self.undo(tid)
self.assertEqual(undone, oid)
data, revid = load_current(self._storage, oid)
# The object must now be at the second state
self.assertEqual(zodb_unpickle(data), MinPO(52))
self._iterate()
def checkTransactionalUndoAfterPackWithObjectUnlinkFromRoot(self):
eq = self.assertEqual
db = DB(self._storage)
conn = db.open()
try:
root = conn.root()
o1 = C()
o2 = C()
root['obj'] = o1
o1.obj = o2
txn = transaction.get()
txn.note(u'o1 -> o2')
txn.commit()
now = packtime = time.time()
while packtime <= now:
packtime = time.time()
o3 = C()
o2.obj = o3
txn = transaction.get()
txn.note(u'o1 -> o2 -> o3')
txn.commit()
o1.obj = o3
txn = transaction.get()
txn.note(u'o1 -> o3')
txn.commit()
log = self._storage.undoLog()
eq(len(log), 4)
for entry in zip(log, (b'o1 -> o3', b'o1 -> o2 -> o3',
b'o1 -> o2', b'initial database creation')):
eq(entry[0]['description'], entry[1])
self._storage.pack(packtime, referencesf)
log = self._storage.undoLog()
for entry in zip(log, (b'o1 -> o3', b'o1 -> o2 -> o3')):
eq(entry[0]['description'], entry[1])
tid = log[0]['id']
db.undo(tid)
txn = transaction.get()
txn.note(u'undo')
txn.commit()
# undo does a txn-undo, but doesn't invalidate
conn.sync()
log = self._storage.undoLog()
for entry in zip(log, (b'undo', b'o1 -> o3', b'o1 -> o2 -> o3')):
eq(entry[0]['description'], entry[1])
eq(o1.obj, o2)
eq(o1.obj.obj, o3)
self._iterate()
finally:
conn.close()
db.close()
def checkPackAfterUndoDeletion(self):
db = DB(self._storage)
cn = db.open()
try:
root = cn.root()
pack_times = []
def set_pack_time():
pack_times.append(time.time())
snooze()
root["key0"] = MinPO(0)
root["key1"] = MinPO(1)
root["key2"] = MinPO(2)
txn = transaction.get()
txn.note(u"create 3 keys")
txn.commit()
set_pack_time()
del root["key1"]
txn = transaction.get()
txn.note(u"delete 1 key")
txn.commit()
set_pack_time()
root._p_deactivate()
cn.sync()
self.assertTrue(listeq(root.keys(), ["key0", "key2"]))
L = db.undoInfo()
db.undo(L[0]["id"])
txn = transaction.get()
txn.note(u"undo deletion")
txn.commit()
set_pack_time()
root._p_deactivate()
cn.sync()
self.assertTrue(listeq(root.keys(), ["key0", "key1", "key2"]))
for t in pack_times:
self._storage.pack(t, referencesf)
root._p_deactivate()
cn.sync()
self.assertTrue(listeq(root.keys(), ["key0", "key1", "key2"]))
for i in range(3):
obj = root["key%d" % i]
self.assertEqual(obj.value, i)
root.items()
self._inter_pack_pause()
finally:
cn.close()
db.close()
def checkPackAfterUndoManyTimes(self):
db = DB(self._storage)
cn = db.open()
try:
rt = cn.root()
rt["test"] = MinPO(1)
transaction.commit()
rt["test2"] = MinPO(2)
transaction.commit()
rt["test"] = MinPO(3)
txn = transaction.get()
txn.note(u"root of undo")
txn.commit()
packtimes = []
for i in range(10):
L = db.undoInfo()
db.undo(L[0]["id"])
txn = transaction.get()
txn.note(u"undo %d" % i)
txn.commit()
rt._p_deactivate()
cn.sync()
self.assertEqual(rt["test"].value, i % 2 and 3 or 1)
self.assertEqual(rt["test2"].value, 2)
packtimes.append(time.time())
snooze()
for t in packtimes:
self._storage.pack(t, referencesf)
cn.sync()
# TODO: Is _cache supposed to have a clear() method, or not?
# cn._cache.clear()
# The last undo set the value to 3 and pack should
# never change that.
self.assertEqual(rt["test"].value, 3)
self.assertEqual(rt["test2"].value, 2)
self._inter_pack_pause()
finally:
cn.close()
db.close()
def _inter_pack_pause(self):
# DirectoryStorage needs a pause between packs,
# most other storages dont.
pass
def checkTransactionalUndoIterator(self):
# check that data_txn set in iterator makes sense
if not hasattr(self._storage, "iterator"):
return
s = self._storage
BATCHES = 4
OBJECTS = 4
orig = []
for i in range(BATCHES):
t = TransactionMetaData()
tid = p64(i + 1)
s.tpc_begin(t, tid)
for j in range(OBJECTS):
oid = s.new_oid()
obj = MinPO(i * OBJECTS + j)
s.store(oid, None, zodb_pickle(obj), '', t)
orig.append((tid, oid))
s.tpc_vote(t)
s.tpc_finish(t)
orig = [(tid, oid, s.getTid(oid)) for tid, oid in orig]
i = 0
for tid, oid, revid in orig:
self._dostore(oid, revid=revid, data=MinPO(revid),
description="update %s" % i)
# Undo the OBJECTS transactions that modified objects created
# in the ith original transaction.
def undo(i):
info = s.undoInfo()
t = TransactionMetaData()
s.tpc_begin(t)
base = i * OBJECTS + i
for j in range(OBJECTS):
tid = info[base + j]['id']
s.undo(tid, t)
s.tpc_vote(t)
s.tpc_finish(t)
for i in range(BATCHES):
undo(i)
# There are now (2 + OBJECTS) * BATCHES transactions:
# BATCHES original transactions, followed by
# OBJECTS * BATCHES modifications, followed by
# BATCHES undos
transactions = s.iterator()
eq = self.assertEqual
for i in range(BATCHES):
txn = next(transactions)
tid = p64(i + 1)
eq(txn.tid, tid)
L1 = {(rec.oid, rec.tid, rec.data_txn) for rec in txn}
L2 = {(oid, revid, None) for _tid, oid, revid in orig
if _tid == tid}
eq(L1, L2)
for i in range(BATCHES * OBJECTS):
txn = next(transactions)
eq(len([rec for rec in txn if rec.data_txn is None]), 1)
for i in range(BATCHES):
txn = next(transactions)
# The undos are performed in reverse order.
otid = p64(BATCHES - i)
L1 = [(rec.oid, rec.data_txn) for rec in txn]
L2 = [(oid, otid) for _tid, oid, revid in orig
if _tid == otid]
L1.sort()
L2.sort()
eq(L1, L2)
self.assertRaises(StopIteration, next, transactions)
def checkUndoLogMetadata(self):
# test that the metadata is correct in the undo log
t = transaction.get()
t.note(u't1')
t.setExtendedInfo('k2', 'this is transaction metadata')
t.setUser(u'u3',path=u'p3')
db = DB(self._storage)
conn = db.open()
try:
root = conn.root()
o1 = C()
root['obj'] = o1
txn = transaction.get()
txn.commit()
l = self._storage.undoLog()
self.assertEqual(len(l),2)
d = l[0]
self.assertEqual(d['description'], b't1')
self.assertEqual(d['k2'], 'this is transaction metadata')
self.assertEqual(d['user_name'], b'p3 u3')
finally:
conn.close()
db.close()
# A common test body for index tests on undoInfo and undoLog. Before
# ZODB 3.4, they always returned a wrong number of results (one too
# few _or_ too many, depending on how they were called).
def _exercise_info_indices(self, method_name):
db = DB(self._storage)
info_func = getattr(db, method_name)
cn = db.open()
rt = cn.root()
# Do some transactions.
for key in "abcdefghijklmnopqrstuvwxyz":
rt[key] = ord(key)
transaction.commit()
# 26 letters = 26 transactions, + the hidden transaction to make
# the root object, == 27 expected.
allofem = info_func(0, 100000)
self.assertEqual(len(allofem), 27)
# Asking for no more than 100000 should do the same.
redundant = info_func(last=-1000000)
self.assertEqual(allofem, redundant)
# By default, we should get only 20 back.
default = info_func()
self.assertEqual(len(default), 20)
# And they should be the most recent 20.
self.assertEqual(default, allofem[:20])
# If we ask for only one, we should get only the most recent.
fresh = info_func(last=1)
self.assertEqual(len(fresh), 1)
self.assertEqual(fresh[0], allofem[0])
# Another way of asking for only the most recent.
redundant = info_func(last=-1)
self.assertEqual(fresh, redundant)
# Try a slice that doesn't start at 0.
oddball = info_func(first=11, last=17)
self.assertEqual(len(oddball), 17-11)
self.assertEqual(oddball, allofem[11 : 11+len(oddball)])
# And another way to spell the same thing.
redundant = info_func(first=11, last=-6)
self.assertEqual(oddball, redundant)
cn.close()
# Caution: don't close db; the framework does that. If you close
# it here, the ZODB tests still work, but the ZRS RecoveryStorageTests
# fail (closing the DB here in those tests closes the ZRS primary
# before a ZRS secondary even starts, and then the latter can't
# find a server to recover from).
def checkIndicesInUndoInfo(self):
self._exercise_info_indices("undoInfo")
def checkIndicesInUndoLog(self):
self._exercise_info_indices("undoLog")
def checkUndoMultipleConflictResolution(self, reverse=False):
from .ConflictResolution import PCounter
db = DB(self._storage)
cn = db.open()
try:
cn.root.x = PCounter()
transaction.commit()
for i in range(4):
with db.transaction() as conn:
conn.transaction_manager.get().note(
(str if PY3 else unicode)(i))
conn.root.x.inc()
ids = [l['id'] for l in db.undoLog(1, 3)]
if reverse:
ids.reverse()
db.undoMultiple(ids)
transaction.commit()
self.assertEqual(cn.root.x._value, 2)
finally:
cn.close()
db.close()
def checkUndoMultipleConflictResolutionReversed(self):
self.checkUndoMultipleConflictResolution(True)
@@ -0,0 +1 @@
# Having this makes debugging better.
@@ -0,0 +1,198 @@
##############################################################################
#
# Copyright (c) 2005 Zope Foundation 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.
#
##############################################################################
ZODB Blob support
=================
You create a blob like this::
>>> from ZODB.blob import Blob
>>> myblob = Blob()
A blob implements the IBlob interface::
>>> from ZODB.interfaces import IBlob
>>> IBlob.providedBy(myblob)
True
We can open a new blob file for reading, but it won't have any data::
>>> with myblob.open("r") as fp: fp.read()
''
But we can write data to a new Blob by opening it for writing::
>>> f = myblob.open("w")
>>> _ = f.write(b"Hi, Blob!")
If we try to open a Blob again while it is open for writing, we get an error::
>>> myblob.open("r")
Traceback (most recent call last):
...
BlobError: Already opened for writing.
We can close the file::
>>> f.close()
Now we can open it for reading::
>>> f2 = myblob.open("r")
And we get the data back::
>>> f2.read()
'Hi, Blob!'
If we want to, we can open it again::
>>> f3 = myblob.open("r")
>>> f3.read()
'Hi, Blob!'
But we can't open it for writing, while it is opened for reading::
>>> myblob.open("a")
Traceback (most recent call last):
...
BlobError: Already opened for reading.
Before we can write, we have to close the readers::
>>> f2.close()
>>> f3.close()
Now we can open it for writing again and e.g. append data::
>>> f4 = myblob.open("a")
>>> _ = f4.write(b"\nBlob is fine.")
We can't open a blob while it is open for writing:
>>> myblob.open("w")
Traceback (most recent call last):
...
BlobError: Already opened for writing.
>>> myblob.open("r")
Traceback (most recent call last):
...
BlobError: Already opened for writing.
>>> f4.close()
Now we can read it::
>>> f4a = myblob.open("r")
>>> f4a.read()
'Hi, Blob!\nBlob is fine.'
>>> f4a.close()
You shouldn't need to explicitly close a blob unless you hold a reference
to it via a name. If the first line in the following test kept a reference
around via a name, the second call to open it in a writable mode would fail
with a BlobError, but it doesn't::
>>> with myblob.open("r+") as fp: fp.read()
'Hi, Blob!\nBlob is fine.'
>>> f4b = myblob.open("a")
>>> f4b.close()
We can read lines out of the blob too::
>>> f5 = myblob.open("r")
>>> f5.readline()
'Hi, Blob!\n'
>>> f5.readline()
'Blob is fine.'
>>> f5.close()
We can seek to certain positions in a blob and read portions of it::
>>> f6 = myblob.open('r')
>>> _ = f6.seek(4)
>>> int(f6.tell())
4
>>> f6.read(5)
'Blob!'
>>> f6.close()
We can use the object returned by a blob open call as an iterable::
>>> f7 = myblob.open('r')
>>> for line in f7:
... print(line.decode())
Hi, Blob!
<BLANKLINE>
Blob is fine.
>>> f7.close()
We can truncate a blob::
>>> f8 = myblob.open('a')
>>> _ = f8.truncate(0)
>>> f8.close()
>>> f8 = myblob.open('r')
>>> f8.read()
''
>>> f8.close()
Blobs are always opened in binary mode::
>>> f9 = myblob.open("r")
>>> f9.mode
'rb'
>>> f9.close()
Blobs that have not been committed can be opened using any mode,
except for "c"::
>>> import six
>>> from ZODB.blob import BlobError, valid_modes
>>> for mode in valid_modes:
... try:
... f10 = Blob().open(mode)
... except BlobError:
... six.print_('open failed with mode "%s"' % mode)
... else:
... f10.close()
open failed with mode "c"
Some cleanup in this test is needed::
>>> import transaction
>>> transaction.get().abort()
Subclassing Blobs
-----------------
Blobs are not subclassable::
>>> class SubBlob(Blob):
... pass
>>> my_sub_blob = SubBlob()
Traceback (most recent call last):
...
TypeError: Blobs do not support subclassing.
Passing data to the blob constructor
------------------------------------
If you have a small amount of data, you can pass it to the blob
constructor. (This is a convenience, mostly for writing tests.)
>>> myblob = Blob(b'some data')
>>> with myblob.open() as fp: fp.read()
'some data'
@@ -0,0 +1,88 @@
Connection support for Blobs tests
==================================
Connections handle Blobs specially. To demonstrate that, we first need a Blob
with some data:
>>> from ZODB.interfaces import IBlob
>>> from ZODB.blob import Blob
>>> import transaction
>>> blob = Blob()
>>> data = blob.open("w")
>>> _ = data.write(b"I'm a happy Blob.")
>>> data.close()
We also need a database with a blob supporting storage. (We're going to use
FileStorage rather than MappingStorage here because we will want ``loadBefore``
for one of our examples.)
>>> blob_storage = create_storage()
>>> from ZODB.DB import DB
>>> database = DB(blob_storage)
Putting a Blob into a Connection works like every other object:
>>> connection = database.open()
>>> root = connection.root()
>>> root['myblob'] = blob
>>> transaction.commit()
We can also commit a transaction that seats a blob into place without
calling the blob's open method:
>>> nothing = transaction.begin()
>>> anotherblob = Blob()
>>> root['anotherblob'] = anotherblob
>>> nothing = transaction.commit()
Getting stuff out of there works similarly:
>>> transaction2 = transaction.TransactionManager()
>>> connection2 = database.open(transaction_manager=transaction2)
>>> root = connection2.root()
>>> blob2 = root['myblob']
>>> IBlob.providedBy(blob2)
True
>>> with blob2.open("r") as fp: fp.read()
"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(b'I am an ecstatic Blob.')
>>> f.close()
>>> transaction.commit()
>>> with connection3.root()['myblob'].open('r') as fp: fp.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:
>>> from ZODB.MappingStorage import MappingStorage
>>> no_blob_storage = MappingStorage()
>>> database2 = DB(no_blob_storage)
>>> connection2 = database2.open(transaction_manager=transaction2)
>>> root = connection2.root()
>>> root['myblob'] = Blob()
>>> transaction2.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
Unsupported: Storing Blobs in ...
>>> transaction2.abort()
>>> connection2.close()
After testing this, we don't need the storage directory and databases anymore:
>>> transaction.abort()
>>> connection.close()
>>> database.close()
>>> database2.close()
>>> blob_storage.close()
@@ -0,0 +1,132 @@
Consuming existing files
========================
The ZODB Blob implementation allows to import existing files as Blobs within
an O(1) operation we call `consume`::
Let's create a file::
>>> to_import = open('to_import', 'wb')
>>> _ = to_import.write(b"I'm a Blob and I feel fine.")
The file *must* be closed before giving it to consumeFile:
>>> to_import.close()
Now, let's consume this file in a blob by specifying it's name::
>>> from ZODB.blob import Blob
>>> blob = Blob()
>>> blob.consumeFile('to_import')
After the consumeFile operation, the original file has been removed:
>>> import os
>>> os.path.exists('to_import')
False
We now can call open on the blob and read and write the data::
>>> blob_read = blob.open('r')
>>> blob_read.read()
"I'm a Blob and I feel fine."
>>> blob_read.close()
>>> blob_write = blob.open('w')
>>> _ = blob_write.write(b'I was changed.')
>>> blob_write.close()
We can not consume a file when there is a reader or writer around for a blob
already::
>>> with open('to_import', 'wb') as file:
... _ = file.write(b'I am another blob.')
>>> blob_read = blob.open('r')
>>> blob.consumeFile('to_import')
Traceback (most recent call last):
BlobError: Already opened for reading.
>>> blob_read.close()
>>> blob_write = blob.open('w')
>>> blob.consumeFile('to_import')
Traceback (most recent call last):
BlobError: Already opened for writing.
>>> blob_write.close()
Now, after closing all readers and writers we can consume files again::
>>> blob.consumeFile('to_import')
>>> blob_read = blob.open('r')
>>> blob_read.read()
'I am another blob.'
>>> blob_read.close()
Edge cases
==========
There are some edge cases what happens when the link() operation
fails. We simulate this in different states:
Case 1: We don't have uncommitted data, but the link operation fails. We fall
back to try a copy/remove operation that is successfull::
>>> with open('to_import', 'wb') as file:
... _ = file.write(b'Some data.')
>>> def failing_rename(f1, f2):
... if f1 == 'to_import':
... raise OSError("I can't link.")
... os_rename(f1, f2)
>>> blob = Blob()
>>> os_rename = os.rename
>>> os.rename = failing_rename
>>> blob.consumeFile('to_import')
The blob did not have data before, so it shouldn't have data now::
>>> with blob.open('r') as fp: fp.read()
'Some data.'
Case 2: We don't have uncommitted data and both the link operation and the
copy fail. The exception will be re-raised and the target file will not
exist::
>>> blob = Blob()
>>> import ZODB.utils
>>> utils_cp = ZODB.utils.cp
>>> def failing_copy(f1, f2):
... raise OSError("I can't copy.")
>>> ZODB.utils.cp = failing_copy
>>> with open('to_import', 'wb') as file:
... _ = file.write(b'Some data.')
>>> blob.consumeFile('to_import')
Traceback (most recent call last):
OSError: I can't copy.
The blob did not have data before, so it shouldn't have data now::
>>> with blob.open('r') as fp: fp.read()
''
Case 3: We have uncommitted data, but the link and the copy operations fail.
The exception will be re-raised and the target file will exist with the
previous uncomitted data::
>>> blob = Blob()
>>> with blob.open('w') as blob_writing:
... _ = blob_writing.write(b'Uncommitted data')
>>> blob.consumeFile('to_import')
Traceback (most recent call last):
OSError: I can't copy.
The blob did existed before and had uncommitted data, this shouldn't have
changed::
>>> with blob.open('r') as fp: fp.read()
'Uncommitted data'
>>> os.rename = os_rename
>>> ZODB.utils.cp = utils_cp
@@ -0,0 +1,70 @@
Import/export support for blob data
===================================
Set up:
>>> import ZODB.blob, transaction
>>> from persistent.mapping import PersistentMapping
We need an database with an undoing blob supporting storage:
>>> database1 = ZODB.DB(create_storage('1'))
>>> database2 = ZODB.DB(create_storage('2'))
Create our root object for database1:
>>> connection1 = database1.open()
>>> root1 = connection1.root()
Put a couple blob objects in our database1 and on the filesystem:
>>> import time, os
>>> nothing = transaction.begin()
>>> data1 = b'x'*100000
>>> blob1 = ZODB.blob.Blob()
>>> with blob1.open('w') as file:
... _ = file.write(data1)
>>> data2 = b'y'*100000
>>> blob2 = ZODB.blob.Blob()
>>> with blob2.open('w') as file:
... _ = file.write(data2)
>>> d = PersistentMapping({'blob1':blob1, 'blob2':blob2})
>>> root1['blobdata'] = d
>>> transaction.commit()
Export our blobs from a database1 connection:
>>> conn = root1['blobdata']._p_jar
>>> oid = root1['blobdata']._p_oid
>>> exportfile = 'export'
>>> connection1.exportFile(oid, exportfile).close()
Import our exported data into database2:
>>> connection2 = database2.open()
>>> root2 = connection2.root()
>>> nothing = transaction.begin()
>>> data = root2._p_jar.importFile(exportfile)
>>> root2['blobdata'] = data
>>> transaction.commit()
Make sure our data exists:
>>> items1 = root1['blobdata']
>>> items2 = root2['blobdata']
>>> bool(items1.keys() == items2.keys())
True
>>> with items1['blob1'].open() as fp1:
... with items2['blob1'].open() as fp2:
... fp1.read() == fp2.read()
True
>>> with items1['blob2'].open() as fp1:
... with items2['blob2'].open() as fp2:
... fp1.read() == fp2.read()
True
>>> transaction.get().abort()
.. cleanup
>>> database1.close()
>>> database2.close()
@@ -0,0 +1,303 @@
======================
Blob directory layouts
======================
The internal structure of the blob directories is governed by so called
`layouts`. The current default layout is called `bushy`.
The original blob implementation used a layout that we now call `lawn` and
which is still available for backwards compatibility.
Layouts implement two methods: one for computing a relative path for an
OID and one for turning a relative path back into an OID.
Our terminology is roughly the same as used in `DirectoryStorage`.
The `bushy` layout
==================
The bushy layout splits the OID into the 8 byte parts, reverses them and
creates one directory level for each part, named by the hexlified
representation of the byte value. This results in 8 levels of directories, the
leaf directories being used for the revisions of the blobs and at most 256
entries per directory level:
>>> from ZODB.blob import BushyLayout
>>> bushy = BushyLayout()
>>> bushy.oid_to_path(b'\x00\x00\x00\x00\x00\x00\x00\x00')
'0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x00'
>>> bushy.oid_to_path(b'\x00\x00\x00\x00\x00\x00\x00\x01')
'0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x01'
>>> import os
>>> bushy.path_to_oid(os.path.join(
... '0x01', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00'))
'\x01\x00\x00\x00\x00\x00\x00\x00'
>>> bushy.path_to_oid(os.path.join(
... '0xff', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00'))
'\xff\x00\x00\x00\x00\x00\x00\x00'
Paths that do not represent an OID will cause a ValueError:
>>> bushy.path_to_oid('tmp')
Traceback (most recent call last):
ValueError: Not a valid OID path: `tmp`
The `lawn` layout
=================
The lawn layout creates on directory for each blob named by the blob's hex
representation of its OID. This has some limitations on various file systems
like performance penalties or the inability to store more than a given number
of blobs at the same time (e.g. 32k on ext3).
>>> from ZODB.blob import LawnLayout
>>> lawn = LawnLayout()
>>> lawn.oid_to_path(b'\x00\x00\x00\x00\x00\x00\x00\x00')
'0x00'
>>> lawn.oid_to_path(b'\x00\x00\x00\x00\x00\x00\x00\x01')
'0x01'
>>> lawn.path_to_oid('0x01')
'\x00\x00\x00\x00\x00\x00\x00\x01'
Paths that do not represent an OID will cause a ValueError:
>>> lawn.path_to_oid('tmp')
Traceback (most recent call last):
ValueError: Not a valid OID path: `tmp`
>>> lawn.path_to_oid('')
Traceback (most recent call last):
ValueError: Not a valid OID path: ``
Auto-detecting the layout of a directory
========================================
To allow easier migration, we provide an auto-detection feature that analyses a
blob directory and decides for a strategy to use. In general it prefers to
choose the `bushy` layout, except if it determines that the directory has
already been used to create a lawn structure.
>>> from ZODB.blob import auto_layout_select
1. Non-existing directories will trigger a bushy layout:
>>> import os, shutil
>>> auto_layout_select('blobs')
'bushy'
2. Empty directories will trigger a bushy layout too:
>>> os.mkdir('blobs')
>>> auto_layout_select('blobs')
'bushy'
3. If the directory contains a marker for the strategy it will be used:
>>> from ZODB.blob import LAYOUT_MARKER
>>> import os.path
>>> with open(os.path.join('blobs', LAYOUT_MARKER), 'wb') as file:
... _ = file.write(b'bushy')
>>> auto_layout_select('blobs')
'bushy'
>>> with open(os.path.join('blobs', LAYOUT_MARKER), 'wb') as file:
... _ = file.write(b'lawn')
>>> auto_layout_select('blobs')
'lawn'
>>> shutil.rmtree('blobs')
4. If the directory does not contain a marker but other files that are
not hidden, we assume that it was created with an earlier version of
the blob implementation and uses our `lawn` layout:
>>> os.mkdir('blobs')
>>> with open(os.path.join('blobs', '0x0101'), 'wb') as file:
... _ = file.write(b'foo')
>>> auto_layout_select('blobs')
'lawn'
>>> shutil.rmtree('blobs')
5. If the directory contains only hidden files, use the bushy layout:
>>> os.mkdir('blobs')
>>> with open(os.path.join('blobs', '.svn'), 'wb') as file:
... _ = file.write(b'blah')
>>> auto_layout_select('blobs')
'bushy'
>>> shutil.rmtree('blobs')
Directory layout markers
========================
When the file system helper (FSH) is asked to create the directory structure,
it will leave a marker with the choosen layout if no marker exists yet:
>>> from ZODB.blob import FilesystemHelper
>>> blobs = 'blobs'
>>> fsh = FilesystemHelper(blobs)
>>> fsh.layout_name
'bushy'
>>> fsh.create()
>>> with open(os.path.join(blobs, LAYOUT_MARKER), 'rb') as fp: fp.read()
'bushy'
If the FSH finds a marker, then it verifies whether its content matches the
strategy that was chosen. It will raise an exception if we try to work with a
directory that has a different marker than the chosen strategy:
>>> fsh = FilesystemHelper(blobs, 'lawn')
>>> fsh.layout_name
'lawn'
>>> fsh.create() # doctest: +ELLIPSIS
Traceback (most recent call last):
ValueError: Directory layout `lawn` selected for blob directory .../blobs/, but marker found for layout `bushy`
>>> rmtree(blobs)
This function interacts with the automatic detection in the way, that an
unmarked directory will be marked the first time when it is auto-guessed and
the marker will be used in the future:
>>> import ZODB.FileStorage
>>> from ZODB.blob import BlobStorage
>>> datafs = 'data.fs'
>>> base_storage = ZODB.FileStorage.FileStorage(datafs)
>>> os.mkdir(blobs)
>>> with open(os.path.join(blobs, 'foo'), 'wb') as file:
... _ = file.write(b'foo')
>>> blob_storage = BlobStorage(blobs, base_storage)
>>> blob_storage.fshelper.layout_name
'lawn'
>>> with open(os.path.join(blobs, LAYOUT_MARKER), 'rb') as fp: fp.read()
'lawn'
>>> blob_storage = BlobStorage('blobs', base_storage, layout='bushy')
... # doctest: +ELLIPSIS
Traceback (most recent call last):
ValueError: Directory layout `bushy` selected for blob directory .../blobs/, but marker found for layout `lawn`
>>> base_storage.close()
>>> rmtree('blobs')
Migrating between directory layouts
===================================
A script called `migrateblobs.py` is distributed with the ZODB for offline
migration capabilities between different directory layouts. It can migrate any
blob directory layout to any other layout. It leaves the original blob
directory untouched (except from eventually creating a temporary directory and
the storage layout marker).
The migration is accessible as a library function:
>>> from ZODB.scripts.migrateblobs import migrate
Create a `lawn` directory structure and migrate it to the new `bushy` one:
>>> from ZODB.blob import FilesystemHelper
>>> d = 'd'
>>> os.mkdir(d)
>>> old = os.path.join(d, 'old')
>>> old_fsh = FilesystemHelper(old, 'lawn')
>>> old_fsh.create()
>>> blob1 = old_fsh.getPathForOID(7039, create=True)
>>> blob2 = old_fsh.getPathForOID(10, create=True)
>>> blob3 = old_fsh.getPathForOID(7034, create=True)
>>> with open(os.path.join(blob1, 'foo'), 'wb') as file:
... _ = file.write(b'foo')
>>> with open(os.path.join(blob1, 'foo2'), 'wb') as file:
... _ = file.write(b'bar')
>>> with open(os.path.join(blob2, 'foo3'), 'wb') as file:
... _ = file.write(b'baz')
>>> with open(os.path.join(blob2, 'foo4'), 'wb') as file:
... _ = file.write(b'qux')
>>> with open(os.path.join(blob3, 'foo5'), 'wb') as file:
... _ = file.write(b'quux')
>>> with open(os.path.join(blob3, 'foo6'), 'wb') as file:
... _ = file.write(b'corge')
Committed blobs have their permissions set to 000
The migration function is called with the old and the new path and the layout
that shall be used for the new directory:
>>> bushy = os.path.join(d, 'bushy')
>>> migrate(old, bushy, 'bushy') # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
Migrating blob data from `.../old` (lawn) to `.../bushy` (bushy)
OID: 0x0a - 2 files
OID: 0x1b7a - 2 files
OID: 0x1b7f - 2 files
The new directory now contains the same files in different directories, but
with the same sizes and permissions:
>>> lawn_files = {}
>>> for base, dirs, files in os.walk(old):
... for file_name in files:
... lawn_files[file_name] = os.path.join(base, file_name)
>>> bushy_files = {}
>>> for base, dirs, files in os.walk(bushy):
... for file_name in files:
... bushy_files[file_name] = os.path.join(base, file_name)
>>> len(lawn_files) == len(bushy_files)
True
>>> import six
>>> for file_name, lawn_path in sorted(lawn_files.items()):
... if file_name == '.layout':
... continue
... lawn_stat = os.stat(lawn_path)
... bushy_path = bushy_files[file_name]
... bushy_stat = os.stat(bushy_path)
... six.print_(lawn_path, '-->', bushy_path)
... if ((lawn_stat.st_mode, lawn_stat.st_size) !=
... (bushy_stat.st_mode, bushy_stat.st_size)):
... print('oops')
old/0x1b7f/foo --> bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x1b/0x7f/foo
old/0x1b7f/foo2 --> bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x1b/0x7f/foo2
old/0x0a/foo3 --> bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x0a/foo3
old/0x0a/foo4 --> bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x0a/foo4
old/0x1b7a/foo5 --> bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x1b/0x7a/foo5
old/0x1b7a/foo6 --> bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x1b/0x7a/foo6
We can also migrate the bushy layout back to the lawn layout:
>>> lawn = os.path.join(d, 'lawn')
>>> migrate(bushy, lawn, 'lawn')
Migrating blob data from `.../bushy` (bushy) to `.../lawn` (lawn)
OID: 0x0a - 2 files
OID: 0x1b7a - 2 files
OID: 0x1b7f - 2 files
>>> lawn_files = {}
>>> for base, dirs, files in os.walk(lawn):
... for file_name in files:
... lawn_files[file_name] = os.path.join(base, file_name)
>>> len(lawn_files) == len(bushy_files)
True
>>> for file_name, lawn_path in sorted(lawn_files.items()):
... if file_name == '.layout':
... continue
... lawn_stat = os.stat(lawn_path)
... bushy_path = bushy_files[file_name]
... bushy_stat = os.stat(bushy_path)
... six.print_(bushy_path, '-->', lawn_path)
... if ((lawn_stat.st_mode, lawn_stat.st_size) !=
... (bushy_stat.st_mode, bushy_stat.st_size)):
... print('oops')
bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x1b/0x7f/foo --> lawn/0x1b7f/foo
bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x1b/0x7f/foo2 --> lawn/0x1b7f/foo2
bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x0a/foo3 --> lawn/0x0a/foo3
bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x0a/foo4 --> lawn/0x0a/foo4
bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x1b/0x7a/foo5 --> lawn/0x1b7a/foo5
bushy/0x00/0x00/0x00/0x00/0x00/0x00/0x1b/0x7a/foo6 --> lawn/0x1b7a/foo6
>>> rmtree(d)
@@ -0,0 +1,112 @@
Packing support for blob data
=============================
Set up:
>>> from ZODB.serialize import referencesf
>>> from ZODB.blob import Blob
>>> from ZODB import utils
>>> from ZODB.DB import DB
>>> import transaction
A helper method to assure a unique timestamp across multiple platforms:
>>> from ZODB.tests.testblob import new_time
UNDOING
=======
We need a database with an undoing blob supporting storage:
>>> blob_storage = create_storage()
>>> database = DB(blob_storage)
Create our root object:
>>> connection1 = database.open()
>>> root = connection1.root()
Put some revisions of a blob object in our database and on the filesystem:
>>> import os
>>> tids = []
>>> times = []
>>> blob = Blob()
>>> for i in range(5):
... _ = transaction.begin()
... times.append(new_time())
... with blob.open('w') as file:
... _ = file.write(b'this is blob data ' + str(i).encode())
... if i:
... tids.append(blob._p_serial)
... else:
... root['blob'] = blob
... transaction.commit()
>>> blob._p_activate()
>>> tids.append(blob._p_serial)
>>> oid = root['blob']._p_oid
>>> fns = [ blob_storage.fshelper.getBlobFilename(oid, x) for x in tids ]
>>> [ os.path.exists(x) for x in fns ] # no pack
[True, True, True, True, True]
Do a pack to the slightly before the first revision was written:
>>> packtime = times[0]
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[True, True, True, True, True]
Do a pack to the slightly before the second revision was written:
>>> packtime = times[1]
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[True, True, True, True, True]
Do a pack to the slightly before the third revision was written:
>>> packtime = times[2]
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[False, True, True, True, True]
Do a pack to the slightly before the fourth revision was written:
>>> packtime = times[3]
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[False, False, True, True, True]
Do a pack to the slightly before the fifth revision was written:
>>> packtime = times[4]
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[False, False, False, True, True]
Do a pack to now:
>>> packtime = new_time()
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[False, False, False, False, True]
Delete the object and do a pack, it should get rid of the most current
revision as well as the entire directory:
>>> nothing = transaction.begin()
>>> del root['blob']
>>> transaction.commit()
>>> packtime = new_time()
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[False, False, False, False, False]
>>> os.path.exists(os.path.split(fns[0])[0])
False
Clean up our blob directory and database:
>>> database.close()
@@ -0,0 +1,53 @@
=======================================
Temporary directory handling with blobs
=======================================
When creating uncommitted data files for a blob (e.g. by calling
`blob.open('w')`) we need to decide where to create them. The decision depends
on whether the blob is already stored in a database or not.
Case 1: Blobs that are not in a database yet
============================================
Let's create a new blob and open it for writing::
>>> from ZODB.blob import Blob
>>> b = Blob()
>>> w = b.open('w')
The created file is in the default temporary directory::
>>> import tempfile
>>> w.name.startswith(tempfile.gettempdir())
True
>>> w.close()
Case 2: Blobs that are in a database
====================================
For this case we instanciate a blob and add it to a database immediately.
First, we need a datatabase with blob support::
>>> from ZODB.MappingStorage import MappingStorage
>>> from ZODB.blob import BlobStorage
>>> from ZODB.DB import DB
>>> import os.path
>>> base_storage = MappingStorage('test')
>>> blob_dir = os.path.abspath('blobs')
>>> blob_storage = BlobStorage(blob_dir, base_storage)
>>> database = DB(blob_storage)
Now we create a blob and put it in the database. After that we open it for
writing and expect the file to be in the blob temporary directory::
>>> blob = Blob()
>>> connection = database.open()
>>> connection.add(blob)
>>> w = blob.open('w')
>>> w.name.startswith(os.path.join(blob_dir, 'tmp'))
True
>>> w.close()
>>> database.close()
@@ -0,0 +1,418 @@
Transaction support for Blobs
=============================
We need a database with a blob supporting storage::
>>> import ZODB.blob, transaction
>>> blob_dir = 'blobs'
>>> blob_storage = create_storage(blob_dir=blob_dir)
>>> database = ZODB.DB(blob_storage)
>>> connection1 = database.open()
>>> root1 = connection1.root()
Putting a Blob into a Connection works like any other Persistent object::
>>> blob1 = ZODB.blob.Blob()
>>> with blob1.open('w') as file:
... _ = file.write(b'this is blob 1')
>>> root1['blob1'] = blob1
>>> 'blob1' in root1
True
Aborting a blob add leaves the blob unchanged:
>>> transaction.abort()
>>> 'blob1' in root1
False
>>> blob1._p_oid
>>> blob1._p_jar
>>> with blob1.open() as fp:
... fp.read()
'this is blob 1'
It doesn't clear the file because there is no previously committed version:
>>> fname = blob1._p_blob_uncommitted
>>> import os
>>> os.path.exists(fname)
True
Let's put the blob back into the root and commit the change:
>>> root1['blob1'] = blob1
>>> transaction.commit()
Now, if we make a change and abort it, we'll return to the committed
state:
>>> os.path.exists(fname)
False
>>> blob1._p_blob_uncommitted
>>> with blob1.open('w') as file:
... _ = file.write(b'this is new blob 1')
>>> with blob1.open() as fp:
... fp.read()
'this is new blob 1'
>>> fname = blob1._p_blob_uncommitted
>>> os.path.exists(fname)
True
>>> transaction.abort()
>>> os.path.exists(fname)
False
>>> blob1._p_blob_uncommitted
>>> with blob1.open() as fp:
... fp.read()
'this is blob 1'
Opening a blob gives us a filehandle. Getting data out of the
resulting filehandle is accomplished via the filehandle's read method::
>>> connection2 = database.open()
>>> root2 = connection2.root()
>>> blob1a = root2['blob1']
>>> blob1afh1 = blob1a.open("r")
>>> blob1afh1.read()
'this is blob 1'
Let's make another filehandle for read only to blob1a. Each file
handle has a reference to the (same) underlying blob::
>>> blob1afh2 = blob1a.open("r")
>>> blob1afh2.blob is blob1afh1.blob
True
Let's close the first filehandle we got from the blob::
>>> blob1afh1.close()
Let's abort this transaction, and ensure that the filehandles that we
opened are still open::
>>> transaction.abort()
>>> blob1afh2.read()
'this is blob 1'
>>> blob1afh2.close()
If we open a blob for append, writing any number of bytes to the
blobfile should result in the blob being marked "dirty" in the
connection (we just aborted above, so the object should be "clean"
when we start)::
>>> bool(blob1a._p_changed)
False
>>> with blob1a.open('r') as fp:
... fp.read()
'this is blob 1'
>>> with blob1a.open('a') as blob1afh3:
... assert(bool(blob1a._p_changed))
... _ = blob1afh3.write(b'woot!')
>>> blob1afh3.close()
We can open more than one blob object during the course of a single
transaction::
>>> blob2 = ZODB.blob.Blob()
>>> with blob2.open('w') as file:
... _ = file.write(b'this is blob 3')
>>> root2['blob2'] = blob2
>>> transaction.commit()
Since we committed the current transaction above, the aggregate
changes we've made to blob, blob1a (these refer to the same object) and
blob2 (a different object) should be evident::
>>> with blob1.open('r') as fp:
... fp.read()
'this is blob 1woot!'
>>> with blob1a.open('r') as fp:
... fp.read()
'this is blob 1woot!'
>>> with blob2.open('r') as fp:
... fp.read()
'this is blob 3'
We shouldn't be able to persist a blob filehandle at commit time
(although the exception which is raised when an object cannot be
pickled appears to be particulary unhelpful for casual users at the
moment)::
>>> with blob1.open('r') as f:
... root1['wontwork'] = f
... transaction.commit()
Traceback (most recent call last):
...
TypeError: ...
Abort for good measure::
>>> transaction.abort()
Attempting to change a blob simultaneously from two different
connections should result in a write conflict error::
>>> tm1 = transaction.TransactionManager()
>>> tm2 = transaction.TransactionManager()
>>> root3 = database.open(transaction_manager=tm1).root()
>>> root4 = database.open(transaction_manager=tm2).root()
>>> blob1c3 = root3['blob1']
>>> blob1c4 = root4['blob1']
>>> with blob1c3.open('a') as blob1c3fh1:
... _ = blob1c3fh1.write(b'this is from connection 3')
>>> with blob1c4.open('a') as blob1c4fh1:
... _ = blob1c4fh1.write(b'this is from connection 4')
>>> tm1.commit()
>>> with root3['blob1'].open('r') as fp:
... fp.read()
'this is blob 1woot!this is from connection 3'
>>> tm2.commit()
Traceback (most recent call last):
...
ConflictError: database conflict error (oid 0x01, class ZODB.blob.Blob...)
After the conflict, the winning transaction's result is visible on both
connections::
>>> with root3['blob1'].open('r') as fp:
... fp.read()
'this is blob 1woot!this is from connection 3'
>>> tm2.abort()
>>> with root4['blob1'].open('r') as fp:
... fp.read()
'this is blob 1woot!this is from connection 3'
You can't commit a transaction while blob files are open:
>>> f = root3['blob1'].open('w')
>>> tm1.commit()
Traceback (most recent call last):
...
ValueError: Can't commit with opened blobs.
>>> f.close()
>>> tm1.abort()
>>> f = root3['blob1'].open('w')
>>> f.close()
>>> f = root3['blob1'].open('r')
>>> tm1.commit()
Traceback (most recent call last):
...
ValueError: Can't commit with opened blobs.
>>> f.close()
>>> tm1.abort()
Savepoints and Blobs
--------------------
We do support optimistic savepoints:
>>> connection5 = database.open()
>>> root5 = connection5.root()
>>> blob = ZODB.blob.Blob()
>>> with blob.open("w") as blob_fh:
... _ = blob_fh.write(b"I'm a happy blob.")
>>> root5['blob'] = blob
>>> transaction.commit()
>>> with root5['blob'].open("r") as fp:
... fp.read()
"I'm a happy blob."
>>> with root5['blob'].open("a") as blob_fh:
... _ = blob_fh.write(b" And I'm singing.")
>>> with root5['blob'].open("r") as fp:
... fp.read()
"I'm a happy blob. And I'm singing."
>>> savepoint = transaction.savepoint(optimistic=True)
>>> with root5['blob'].open("r") as fp:
... fp.read()
"I'm a happy blob. And I'm singing."
Savepoints store the blobs in temporary directories in the temporary
directory of the blob storage:
>>> len([name for name in os.listdir(os.path.join(blob_dir, 'tmp'))
... if name.startswith('savepoint')])
1
After committing the transaction, the temporary savepoint files are moved to
the committed location again:
>>> transaction.commit()
>>> len([name for name in os.listdir(os.path.join(blob_dir, 'tmp'))
... if name.startswith('savepoint')])
0
We support non-optimistic savepoints too:
>>> with root5['blob'].open("a") as file:
... _ = file.write(b" And I'm dancing.")
>>> with root5['blob'].open("r") as fp:
... fp.read()
"I'm a happy blob. And I'm singing. And I'm dancing."
>>> savepoint = transaction.savepoint()
Again, the savepoint creates a new savepoints directory:
>>> len([name for name in os.listdir(os.path.join(blob_dir, 'tmp'))
... if name.startswith('savepoint')])
1
>>> with root5['blob'].open("w") as file:
... _ = file.write(b" And the weather is beautiful.")
>>> savepoint.rollback()
>>> with root5['blob'].open("r") as fp:
... fp.read()
"I'm a happy blob. And I'm singing. And I'm dancing."
>>> transaction.abort()
The savepoint blob directory gets cleaned up on an abort:
>>> len([name for name in os.listdir(os.path.join(blob_dir, 'tmp'))
... if name.startswith('savepoint')])
0
Reading Blobs outside of a transaction
--------------------------------------
If you want to read from a Blob outside of transaction boundaries (e.g. to
stream a file to the browser), committed method to get the name of a
file that can be opened.
>>> connection6 = database.open()
>>> root6 = connection6.root()
>>> blob = ZODB.blob.Blob()
>>> with blob.open("w") as blob_fh:
... _ = blob_fh.write(b"I'm a happy blob.")
>>> root6['blob'] = blob
>>> transaction.commit()
>>> with open(blob.committed()) as fp:
... fp.read()
"I'm a happy blob."
We can also read committed data by calling open with a 'c' flag:
>>> f = blob.open('c')
This just returns a regular file object:
>>> type(f) == file_type
True
and doesn't prevent us from opening the blob for writing:
>>> with blob.open('w') as file:
... _ = file.write(b'x')
>>> with blob.open() as fp: fp.read()
'x'
>>> f.read()
"I'm a happy blob."
>>> f.close()
>>> transaction.abort()
An exception is raised if we call committed on a blob that has
uncommitted changes:
>>> blob = ZODB.blob.Blob()
>>> blob.committed()
Traceback (most recent call last):
...
BlobError: Uncommitted changes
>>> blob.open('c')
Traceback (most recent call last):
...
BlobError: Uncommitted changes
>>> with blob.open('w') as file:
... _ = file.write(b"I'm a happy blob.")
>>> root6['blob6'] = blob
>>> blob.committed()
Traceback (most recent call last):
...
BlobError: Uncommitted changes
>>> blob.open('c')
Traceback (most recent call last):
...
BlobError: Uncommitted changes
>>> s = transaction.savepoint()
>>> blob.committed()
Traceback (most recent call last):
...
BlobError: Uncommitted changes
>>> blob.open('c')
Traceback (most recent call last):
...
BlobError: Uncommitted changes
>>> transaction.commit()
>>> with open(blob.committed()) as fp:
... fp.read()
"I'm a happy blob."
You can't open a committed blob file for writing:
>>> try:
... open(blob.committed(), 'w') # doctest: +ELLIPSIS
... except:
... # Produces IOError in Py2 and PermissionError in Py3
... print('Error raised.')
Error raised.
tpc_abort
---------
If a transaction is aborted in the middle of 2-phase commit, any data
stored are discarded.
>>> olddata, oldserial = blob_storage.load(blob._p_oid, '')
>>> from ZODB.Connection import TransactionMetaData
>>> t = TransactionMetaData()
>>> blob_storage.tpc_begin(t)
>>> with open('blobfile', 'wb') as file:
... _ = file.write(b'This data should go away')
>>> blob_storage.storeBlob(blob._p_oid, oldserial, olddata, 'blobfile',
... '', t)
>>> new_oid = blob_storage.new_oid()
>>> with open('blobfile2', 'wb') as file:
... _ = file.write(b'This data should go away too')
>>> blob_storage.storeBlob(new_oid, '\0'*8, olddata, 'blobfile2',
... '', t)
>>> bool(blob_storage.tpc_vote(t))
False
>>> blob_storage.tpc_abort(t)
Now, the serial for the existing blob should be the same:
>>> blob_storage.load(blob._p_oid, '') == (olddata, oldserial)
True
The old data should be unaffected:
>>> with open(blob_storage.loadBlob(blob._p_oid, oldserial)) as fp:
... fp.read()
"I'm a happy blob."
Similarly, the new object wasn't added to the storage:
>>> blob_storage.load(new_oid, '')
Traceback (most recent call last):
...
POSKeyError: 0x...
.. clean up
>>> tm1.abort()
>>> tm2.abort()
>>> database.close()
@@ -0,0 +1,164 @@
##############################################################################
#
# Copyright (c) 2005 Zope Foundation 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.
#
##############################################################################
Packing support for blob data
=============================
Set up:
>>> from ZODB.MappingStorage import MappingStorage
>>> from ZODB.serialize import referencesf
>>> from ZODB.blob import Blob, BlobStorage
>>> from ZODB import utils
>>> from ZODB.DB import DB
>>> import transaction
>>> storagefile = 'Data.fs'
>>> blob_dir = 'blobs'
A helper method to assure a unique timestamp across multiple platforms:
>>> from ZODB.tests.testblob import new_time
UNDOING
=======
See blob_packing.txt.
NON-UNDOING
===========
We need an database with a NON-undoing blob supporting storage:
>>> base_storage = MappingStorage('storage')
>>> blob_storage = BlobStorage(blob_dir, base_storage)
>>> database = DB(blob_storage)
Create our root object:
>>> connection1 = database.open()
>>> root = connection1.root()
Put some revisions of a blob object in our database and on the filesystem:
>>> import time, os
>>> tids = []
>>> times = []
>>> nothing = transaction.begin()
>>> times.append(new_time())
>>> blob = Blob()
>>> with blob.open('w') as file:
... _ = file.write(b'this is blob data 0')
>>> root['blob'] = blob
>>> transaction.commit()
>>> tids.append(blob_storage._tid)
>>> nothing = transaction.begin()
>>> times.append(new_time())
>>> with root['blob'].open('w') as file:
... _ = file.write(b'this is blob data 1')
>>> transaction.commit()
>>> tids.append(blob_storage._tid)
>>> nothing = transaction.begin()
>>> times.append(new_time())
>>> with root['blob'].open('w') as file:
... _ = file.write(b'this is blob data 2')
>>> transaction.commit()
>>> tids.append(blob_storage._tid)
>>> nothing = transaction.begin()
>>> times.append(new_time())
>>> with root['blob'].open('w') as file:
... _ = file.write(b'this is blob data 3')
>>> transaction.commit()
>>> tids.append(blob_storage._tid)
>>> nothing = transaction.begin()
>>> times.append(new_time())
>>> with root['blob'].open('w') as file:
... _ = file.write(b'this is blob data 4')
>>> transaction.commit()
>>> tids.append(blob_storage._tid)
>>> oid = root['blob']._p_oid
>>> fns = [ blob_storage.fshelper.getBlobFilename(oid, x) for x in tids ]
>>> [ os.path.exists(x) for x in fns ]
[True, True, True, True, True]
Get our blob filenames for this oid.
>>> fns = [ blob_storage.fshelper.getBlobFilename(oid, x) for x in tids ]
Do a pack to the slightly before the first revision was written:
>>> packtime = times[0]
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[False, False, False, False, True]
Do a pack to now:
>>> packtime = new_time()
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[False, False, False, False, True]
Delete the object and do a pack, it should get rid of the most current
revision as well as the entire directory:
>>> nothing = transaction.begin()
>>> del root['blob']
>>> transaction.commit()
>>> packtime = new_time()
>>> blob_storage.pack(packtime, referencesf)
>>> [ os.path.exists(x) for x in fns ]
[False, False, False, False, False]
>>> os.path.exists(os.path.split(fns[0])[0])
False
Avoiding parallel packs
=======================
Blob packing (similar to FileStorage) can only be run once at a time. For
this, a flag (_blobs_pack_is_in_progress) is set. If the pack method is called
while this flag is set, it will refuse to perform another pack, until the flag
is reset:
>>> blob_storage._blobs_pack_is_in_progress
False
>>> blob_storage._blobs_pack_is_in_progress = True
>>> blob_storage.pack(packtime, referencesf)
Traceback (most recent call last):
BlobStorageError: Already packing
>>> blob_storage._blobs_pack_is_in_progress = False
>>> blob_storage.pack(packtime, referencesf)
We can also see, that the flag is set during the pack, by leveraging the
knowledge that the underlying storage's pack method is also called:
>>> import six
>>> def dummy_pack(time, ref):
... six.print_(
... "_blobs_pack_is_in_progress =",
... blob_storage._blobs_pack_is_in_progress)
... return base_pack(time, ref)
>>> base_pack = base_storage.pack
>>> base_storage.pack = dummy_pack
>>> blob_storage.pack(packtime, referencesf)
_blobs_pack_is_in_progress = True
>>> blob_storage._blobs_pack_is_in_progress
False
>>> base_storage.pack = base_pack
>>> database.close()
@@ -0,0 +1,16 @@
<component>
<sectiontype
name="hexstorage"
datatype="ZODB.tests.hexstorage.ZConfigHex"
implements="ZODB.storage"
>
<section type="ZODB.storage" name="*" attribute="base" required="yes" />
</sectiontype>
<sectiontype
name="serverhexstorage"
datatype="ZODB.tests.hexstorage.ZConfigServerHex"
implements="ZODB.storage"
>
<section type="ZODB.storage" name="*" attribute="base" required="yes" />
</sectiontype>
</component>
@@ -0,0 +1,66 @@
##############################################################################
#
# Copyright (c) 2002 Zope Foundation 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
#
##############################################################################
"""Functional test to produce a dangling reference."""
from __future__ import print_function
import time
import transaction
from ZODB.FileStorage import FileStorage
from ZODB import DB
from persistent import Persistent
class P(Persistent):
pass
def create_dangling_ref(db):
rt = db.open().root()
rt[1] = o1 = P()
transaction.get().note(u"create o1")
transaction.commit()
rt[2] = o2 = P()
transaction.get().note(u"create o2")
transaction.commit()
c = o1.child = P()
transaction.get().note(u"set child on o1")
transaction.commit()
o1.child = P()
transaction.get().note(u"replace child on o1")
transaction.commit()
time.sleep(2)
# The pack should remove the reference to c, because it is no
# longer referenced from o1. But the object still exists and has
# an oid, so a new commit of it won't create a new object.
db.pack()
print(repr(c._p_oid))
o2.child = c
transaction.get().note(u"set child on o2")
transaction.commit()
def main():
fs = FileStorage(u"dangle.fs")
db = DB(fs)
create_dangling_ref(db)
db.close()
if __name__ == "__main__":
main()
@@ -0,0 +1,377 @@
=====================
Connection Management
=====================
Here we exercise the connection management done by the DB class.
>>> from ZODB import DB
>>> from ZODB.MappingStorage import MappingStorage as Storage
Capturing log messages from DB is important for some of the examples:
>>> from zope.testing.loggingsupport import InstalledHandler
>>> handler = InstalledHandler('ZODB.DB')
Create a storage, and wrap it in a DB wrapper:
>>> st = Storage()
>>> db = DB(st)
By default, we can open 7 connections without any log messages:
>>> conns = [db.open() for dummy in range(7)]
>>> handler.records
[]
Open one more, and we get a warning:
>>> conns.append(db.open())
>>> len(handler.records)
1
>>> msg = handler.records[0]
>>> import six
>>> six.print_(msg.name, msg.levelname, msg.getMessage())
ZODB.DB WARNING DB.open() has 8 open connections with a pool_size of 7
Open 6 more, and we get 6 more warnings:
>>> conns.extend([db.open() for dummy in range(6)])
>>> len(conns)
14
>>> len(handler.records)
7
>>> msg = handler.records[-1]
>>> six.print_(msg.name, msg.levelname, msg.getMessage())
ZODB.DB WARNING DB.open() has 14 open connections with a pool_size of 7
Add another, so that it's more than twice the default, and the level
rises to critical:
>>> conns.append(db.open())
>>> len(conns)
15
>>> len(handler.records)
8
>>> msg = handler.records[-1]
>>> six.print_(msg.name, msg.levelname, msg.getMessage())
ZODB.DB CRITICAL DB.open() has 15 open connections with a pool_size of 7
While it's boring, it's important to verify that the same relationships
hold if the default pool size is overridden.
>>> handler.clear()
>>> db.close()
>>> st = Storage()
>>> PS = 2 # smaller pool size
>>> db = DB(st, pool_size=PS)
>>> conns = [db.open() for dummy in range(PS)]
>>> handler.records
[]
A warning for opening one more:
>>> conns.append(db.open())
>>> len(handler.records)
1
>>> msg = handler.records[0]
>>> six.print_(msg.name, msg.levelname, msg.getMessage())
ZODB.DB WARNING DB.open() has 3 open connections with a pool_size of 2
More warnings through 4 connections:
>>> conns.extend([db.open() for dummy in range(PS-1)])
>>> len(conns)
4
>>> len(handler.records)
2
>>> msg = handler.records[-1]
>>> six.print_(msg.name, msg.levelname, msg.getMessage())
ZODB.DB WARNING DB.open() has 4 open connections with a pool_size of 2
And critical for going beyond that:
>>> conns.append(db.open())
>>> len(conns)
5
>>> len(handler.records)
3
>>> msg = handler.records[-1]
>>> six.print_(msg.name, msg.levelname, msg.getMessage())
ZODB.DB CRITICAL DB.open() has 5 open connections with a pool_size of 2
We can change the pool size on the fly:
>>> handler.clear()
>>> db.setPoolSize(6)
>>> conns.append(db.open())
>>> handler.records # no log msg -- the pool is bigger now
[]
>>> conns.append(db.open()) # but one more and there's a warning again
>>> len(handler.records)
1
>>> msg = handler.records[0]
>>> six.print_(msg.name, msg.levelname, msg.getMessage())
ZODB.DB WARNING DB.open() has 7 open connections with a pool_size of 6
Enough of that.
>>> handler.clear()
>>> db.close()
More interesting is the stack-like nature of connection reuse. So long as
we keep opening new connections, and keep them alive, all connections
returned are distinct:
>>> st = Storage()
>>> db = DB(st)
>>> c1 = db.open()
>>> c1.cacheMinimize()
>>> c2 = db.open()
>>> c3 = db.open()
>>> c1 is c2 or c1 is c3 or c2 is c3
False
Let's put some markers on the connections, so we can identify these
specific objects later:
>>> c1.MARKER = 'c1'
>>> c2.MARKER = 'c2'
>>> c3.MARKER = 'c3'
Now explicitly close c1 and c2:
>>> c1.close()
>>> c2.close()
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
all:
>>> pool = db.pool
>>> len(pool.available)
2
>>> len(pool.all)
3
Since we closed c2 last, it's at the top of the available stack, so will
be reused by the next open():
>>> c1 = db.open()
>>> c1.MARKER
'c2'
>>> len(pool.available), len(pool.all)
(1, 3)
>>> c3.close() # now the stack has c3 on top, then c1
>>> c2 = db.open()
>>> c2.MARKER
'c3'
>>> len(pool.available), len(pool.all)
(1, 3)
>>> c3 = db.open()
>>> c3.MARKER
'c1'
>>> len(pool.available), len(pool.all)
(0, 3)
It's a bit more complicated though. The connection pool tries to keep
connections with larger caches at the top of the stack. It does this
by having connections with smaller caches "sink" below connections with
larger caches when they are closed.
To see this, we'll add some objects to the caches:
>>> for i in range(10):
... c1.root()[i] = c1.root().__class__()
>>> import transaction
>>> transaction.commit()
>>> c1._cache.cache_non_ghost_count
11
>>> for i in range(5):
... _ = len(c2.root()[i])
>>> c2._cache.cache_non_ghost_count
6
Now, we'll close the connections and get them back:
>>> c1.close()
>>> c2.close()
>>> c3.close()
We closed c3 last, but c1 is the biggest, so we get c1 on the next
open:
>>> db.open() is c1
True
Similarly, c2 is the next buggest, so we get that next:
>>> db.open() is c2
True
and finally c3:
>>> db.open() is c3
True
What about the 3 in pool.all? We've seen that closing connections doesn't
reduce pool.all, and it would be bad if DB kept connections alive forever.
In fact pool.all is a "weak set" of connections -- it holds weak references
to connections. That alone doesn't keep connection objects alive. The
weak set allows DB's statistics methods to return info about connections
that are still alive.
>>> len(db.cacheDetailSize()) # one result for each connection's cache
3
If a connection object is abandoned (it becomes unreachable), then it
will vanish from pool.all automatically. However, connections are
involved in cycles, so exactly when a connection vanishes from
pool.all isn't predictable. It can be forced (on most platforms but
not Jython) by running gc.collect():
>>> import gc, sys
>>> dummy = gc.collect()
>>> len(pool.all)
3
>>> c3 = None
>>> dummy = gc.collect() # removes c3 from pool.all
>>> len(pool.all) if not sys.platform.startswith("java") else 2
2
Note that c3 is really gone; in particular it didn't get added back to
the stack of available connections by magic:
>>> len(pool.available)
0
Nothing in that last block should have logged any msgs:
>>> handler.records
[]
If "too many" connections are open, then closing one may kick an older
closed one out of the available connection stack.
>>> db.close()
>>> st = Storage()
>>> db = DB(st, pool_size=3)
>>> conns = [db.open() for dummy in range(6)]
>>> conns[0].cacheMinimize()
>>> len(handler.records) # 3 warnings for the "excess" connections
3
>>> pool = db.pool
>>> len(pool.available), len(pool.all)
(0, 6)
Let's mark them:
>>> for i, c in enumerate(conns):
... c.MARKER = i
Closing connections adds them to the stack:
>>> for i in range(3):
... conns[i].close()
>>> len(pool.available), len(pool.all)
(3, 6)
>>> del conns[:3] # leave the ones with MARKERs 3, 4 and 5
Closing another one will purge the one with MARKER 0 from the stack
(since it was the first added to the stack):
>>> [c.MARKER for (t, c) in pool.available]
[0, 1, 2]
>>> conns[0].close() # MARKER 3
>>> len(pool.available), len(pool.all)
(3, 5)
>>> [c.MARKER for (t, c) in pool.available]
[1, 2, 3]
Similarly for the other two:
>>> conns[1].close(); conns[2].close()
>>> len(pool.available), len(pool.all)
(3, 3)
>>> [c.MARKER for (t, c) in pool.available]
[3, 4, 5]
Reducing the pool size may also purge the oldest closed connections:
>>> db.setPoolSize(2) # gets rid of MARKER 3
>>> len(pool.available), len(pool.all)
(2, 2)
>>> [c.MARKER for (t, c) in pool.available]
[4, 5]
Since MARKER 5 is still the last one added to the stack, it will be the
first popped:
>>> c1 = db.open(); c2 = db.open()
>>> c1.MARKER, c2.MARKER
(5, 4)
>>> len(pool.available), len(pool.all)
(0, 2)
Next: when a closed Connection is removed from .available due to exceeding
pool_size, that Connection's cache is cleared (this behavior was new in
ZODB 3.6b6). While user code may still hold a reference to that
Connection, once it vanishes from .available it's really not usable for
anything sensible (it can never be in the open state again). Waiting for
gc to reclaim the Connection and its cache eventually works, but that can
take "a long time" and caches can hold on to many objects, and limited
resources (like RDB connections), for the duration.
>>> db.close()
>>> st = Storage()
>>> db = DB(st, pool_size=2)
>>> conn0 = db.open()
>>> conn0.cacheMinimize(); import gc; _ = gc.collect() # See fix84.rst
>>> len(conn0._cache) # empty now
0
>>> import transaction
>>> conn0.root()['a'] = 1
>>> transaction.commit()
>>> len(conn0._cache) # but now the cache holds the root object
1
Now open more connections so that the total exceeds pool_size (2):
>>> conn1 = db.open(); _ = conn1.root()['a']
>>> conn2 = db.open(); _ = conn2.root()['a']
Note that we accessed the objects in the new connections so they would
be of the same size, so that when they get closed, they don't sink
below conn0.
>>> pool = db.pool
>>> len(pool.all), len(pool.available) # all Connections are in use
(3, 0)
Return pool_size (2) Connections to the pool:
>>> conn0.close()
>>> conn1.close()
>>> len(pool.all), len(pool.available)
(3, 2)
>>> len(conn0._cache) # nothing relevant has changed yet
1
When we close the third connection, conn0 will be booted from .all, and
we expect its cache to be cleared then:
>>> conn2.close()
>>> len(pool.all), len(pool.available)
(2, 2)
>>> len(conn0._cache) # conn0's cache is empty again
0
>>> del conn0, conn1, conn2
Clean up.
>>> st.close()
>>> handler.uninstall()

Some files were not shown because too many files have changed in this diff Show More