Commit 0bd19805 authored by Tim Peters's avatar Tim Peters

Merge pycon-multidb branch (-r 29573:29605).

This introduces a "multi-database" concept (a simplification
of Jim's Wiki proposal), and adds many interface definitions.

Work done during the PyCon 2005 ZODB sprint, by Christian
Theune, Jim Fulton and Tim Peters.
parent 8ad0a16f
...@@ -252,6 +252,12 @@ class BaseStorage(UndoLogCompatible): ...@@ -252,6 +252,12 @@ class BaseStorage(UndoLogCompatible):
pass pass
def tpc_finish(self, transaction, f=None): 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!
self._lock_acquire() self._lock_acquire()
try: try:
if transaction is not self._transaction: if transaction is not self._transaction:
......
This diff is collapsed.
...@@ -27,6 +27,9 @@ from ZODB.serialize import referencesf ...@@ -27,6 +27,9 @@ from ZODB.serialize import referencesf
from ZODB.utils import WeakSet from ZODB.utils import WeakSet
from ZODB.utils import DEPRECATED_ARGUMENT, deprecated36 from ZODB.utils import DEPRECATED_ARGUMENT, deprecated36
from zope.interface import implements
from ZODB.interfaces import IDatabase
import transaction import transaction
logger = logging.getLogger('ZODB.DB') logger = logging.getLogger('ZODB.DB')
...@@ -178,6 +181,7 @@ class DB(object): ...@@ -178,6 +181,7 @@ class DB(object):
setCacheDeactivateAfter, setCacheDeactivateAfter,
getVersionCacheDeactivateAfter, setVersionCacheDeactivateAfter getVersionCacheDeactivateAfter, setVersionCacheDeactivateAfter
""" """
implements(IDatabase)
klass = Connection # Class to use for connections klass = Connection # Class to use for connections
_activity_monitor = None _activity_monitor = None
...@@ -188,6 +192,8 @@ class DB(object): ...@@ -188,6 +192,8 @@ class DB(object):
cache_deactivate_after=DEPRECATED_ARGUMENT, cache_deactivate_after=DEPRECATED_ARGUMENT,
version_pool_size=3, version_pool_size=3,
version_cache_size=100, version_cache_size=100,
database_name='unnamed',
databases=None,
version_cache_deactivate_after=DEPRECATED_ARGUMENT, version_cache_deactivate_after=DEPRECATED_ARGUMENT,
): ):
"""Create an object database. """Create an object database.
...@@ -248,6 +254,16 @@ class DB(object): ...@@ -248,6 +254,16 @@ class DB(object):
storage.tpc_vote(t) storage.tpc_vote(t)
storage.tpc_finish(t) storage.tpc_finish(t)
# Multi-database setup.
if databases is None:
databases = {}
self.databases = databases
self.database_name = database_name
if database_name in databases:
raise ValueError("database_name %r already in databases" %
database_name)
databases[database_name] = self
# Pass through methods: # Pass through methods:
for m in ['history', 'supportsUndo', 'supportsVersions', 'undoLog', for m in ['history', 'supportsUndo', 'supportsVersions', 'undoLog',
'versionEmpty', 'versions']: 'versionEmpty', 'versions']:
...@@ -565,7 +581,7 @@ class DB(object): ...@@ -565,7 +581,7 @@ class DB(object):
def get_info(c): def get_info(c):
# `result`, `time` and `version` are lexically inherited. # `result`, `time` and `version` are lexically inherited.
o = c._opened o = c._opened
d = c._debug_info d = c.getDebugInfo()
if d: if d:
if len(d) == 1: if len(d) == 1:
d = d[0] d = d[0]
......
...@@ -547,6 +547,7 @@ class FileStorage(BaseStorage.BaseStorage, ...@@ -547,6 +547,7 @@ class FileStorage(BaseStorage.BaseStorage,
self._lock_release() self._lock_release()
def load(self, oid, version): def load(self, oid, version):
"""Return pickle data and serial number."""
self._lock_acquire() self._lock_acquire()
try: try:
pos = self._lookup_pos(oid) pos = self._lookup_pos(oid)
......
...@@ -68,16 +68,16 @@ ...@@ -68,16 +68,16 @@
# #
# - 8-byte data length # - 8-byte data length
# #
# ? 8-byte position of non-version data # ? 8-byte position of non-version data record
# (if version length > 0) # (if version length > 0)
# #
# ? 8-byte position of previous record in this version # ? 8-byte position of previous record in this version
# (if version length > 0) # (if version length > 0)
# #
# ? version string # ? version string
# (if version length > 0) # (if version length > 0)
# #
# ? data # ? data
# (data length > 0) # (data length > 0)
# #
# ? 8-byte position of data record containing data # ? 8-byte position of data record containing data
......
...@@ -61,6 +61,9 @@ class TmpStore: ...@@ -61,6 +61,9 @@ class TmpStore:
serial = h[:8] serial = h[:8]
return self._file.read(size), serial return self._file.read(size), serial
def sortKey(self):
return self._storage.sortKey()
# TODO: clarify difference between self._storage & self._db._storage # TODO: clarify difference between self._storage & self._db._storage
def modifiedInVersion(self, oid): def modifiedInVersion(self, oid):
......
This diff is collapsed.
##############################################################################
#
# Copyright (c) 2005 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.
#
##############################################################################
Multi-database tests
====================
Multi-database support adds the ability to tie multiple databases into a
collection. The original proposal is in the fishbowl:
http://www.zope.org/Wikis/ZODB/MultiDatabases/
It was implemented during the PyCon 2005 sprints, but in a simpler form,
by Jim Fulton, Christian Theune,and Tim Peters. Overview:
No private attributes were added, and one new method was introduced.
DB:
- a new .database_name attribute holds the name of this database
- a new .databases attribute maps from database name to DB object; all DBs
in a multi-database collection share the same .databases object
- the DB constructor has new optional arguments with the same names
(database_name= and databases=).
Connection:
- a new .connections attribute maps from database name to a Connection for
the database with that name; the .connections mapping object is also
shared among databases in a collection
- a new .get_connection(database_name) method returns a Connection for a
database in the collection; if a connection is already open, it's returned
(this is the value .connections[database_name]), else a new connection is
opened (and stored as .connections[database_name])
Creating a multi-database starts with creating a named DB:
>>> from ZODB.tests.test_storage import MinimalMemoryStorage
>>> from ZODB import DB
>>> dbmap = {}
>>> db = DB(MinimalMemoryStorage(), database_name='root', databases=dbmap)
The database name is accessible afterwards and in a newly created collection:
>>> db.database_name
'root'
>>> db.databases # doctest: +ELLIPSIS
{'root': <ZODB.DB.DB object at ...>}
>>> db.databases is dbmap
True
Adding another database to the collection works like this:
>>> db2 = DB(MinimalMemoryStorage(),
... database_name='notroot',
... databases=dbmap)
The new db2 now shares the 'databases' dictionary with db and has two entries:
>>> db2.databases is db.databases is dbmap
True
>>> len(db2.databases)
2
>>> names = dbmap.keys(); names.sort(); print names
['notroot', 'root']
It's an error to try to insert a database with a name already in use:
>>> db3 = DB(MinimalMemoryStorage(),
... database_name='root',
... databases=dbmap)
Traceback (most recent call last):
...
ValueError: database_name 'root' already in databases
Because that failed, db.databases wasn't changed:
>>> len(db.databases) # still 2
2
You can (still) get a connection to a database this way:
>>> cn = db.open()
>>> cn # doctest: +ELLIPSIS
<Connection at ...>
This is the only connection in this collection right now:
>>> cn.connections # doctest: +ELLIPSIS
{'root': <Connection at ...>}
Getting a connection to a different database from an existing connection in the
same database collection (this enables 'connection binding' within a given
thread/transaction/context ...):
>>> cn2 = cn.get_connection('notroot')
>>> cn2 # doctest: +ELLIPSIS
<Connection at ...>
Now there are two connections in that collection:
>>> cn2.connections is cn.connections
True
>>> len(cn2.connections)
2
>>> names = cn.connections.keys(); names.sort(); print names
['notroot', 'root']
So long as this database group remains open, the same Connection objects
are returned:
>>> cn.get_connection('root') is cn
True
>>> cn.get_connection('notroot') is cn2
True
>>> cn2.get_connection('root') is cn
True
>>> cn2.get_connection('notroot') is cn2
True
Of course trying to get a connection for a database not in the group raises
an exception:
>>> cn.get_connection('no way')
Traceback (most recent call last):
...
KeyError: 'no way'
Clean up:
>>> for a_db in dbmap.values():
... a_db.close()
...@@ -647,6 +647,8 @@ class StubDatabase: ...@@ -647,6 +647,8 @@ class StubDatabase:
self._storage = StubStorage() self._storage = StubStorage()
classFactory = None classFactory = None
database_name = 'stubdatabase'
databases = {'stubdatabase': database_name}
def invalidate(self, transaction, dict_with_oid_keys, connection): def invalidate(self, transaction, dict_with_oid_keys, connection):
pass pass
......
...@@ -15,4 +15,6 @@ ...@@ -15,4 +15,6 @@
from zope.testing.doctestunit import DocFileSuite from zope.testing.doctestunit import DocFileSuite
def test_suite(): def test_suite():
return DocFileSuite("dbopen.txt") return DocFileSuite("dbopen.txt",
"multidb.txt",
)
...@@ -257,18 +257,35 @@ class IPersistentDataManager(Interface): ...@@ -257,18 +257,35 @@ class IPersistentDataManager(Interface):
def setstate(object): def setstate(object):
"""Load the state for the given object. """Load the state for the given object.
The object should be in the ghost state. The object should be in the ghost state. The object's state will be
The object's state will be set and the object will end up set and the object will end up in the saved state.
in the saved state.
The object must provide the IPersistent interface. The object must provide the IPersistent interface.
""" """
def oldstate(obj, tid):
"""Return copy of 'obj' that was written by transaction 'tid'.
The returned object does not have the typical metadata (_p_jar, _p_oid,
_p_serial) set. I'm not sure how references to other peristent objects
are handled.
Parameters
obj: a persistent object from this Connection.
tid: id of a transaction that wrote an earlier revision.
Raises KeyError if tid does not exist or if tid deleted a revision of
obj.
"""
def register(object): def register(object):
"""Register an IPersistent with the current transaction. """Register an IPersistent with the current transaction.
This method must be called when the object transitions to This method must be called when the object transitions to
the changed state. the changed state.
A subclass could override this method to customize the default
policy of one transaction manager for each thread.
""" """
def mtime(object): def mtime(object):
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment