tested django-newsletter
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Exceptions for ZEO."""
|
||||
|
||||
import transaction.interfaces
|
||||
|
||||
from ZODB.POSException import StorageError
|
||||
|
||||
class ClientStorageError(StorageError):
|
||||
"""An error occurred in the ZEO Client Storage.
|
||||
"""
|
||||
|
||||
class UnrecognizedResult(ClientStorageError):
|
||||
"""A server call returned an unrecognized result.
|
||||
"""
|
||||
|
||||
class ClientDisconnected(ClientStorageError,
|
||||
transaction.interfaces.TransientError):
|
||||
"""The database storage is disconnected from the storage.
|
||||
"""
|
||||
|
||||
class AuthError(StorageError):
|
||||
"""The client provided invalid authentication credentials.
|
||||
"""
|
||||
|
||||
class ProtocolError(ClientStorageError):
|
||||
"""A client contacted a server with an incomparible protocol
|
||||
"""
|
||||
|
||||
class ServerException(ClientStorageError):
|
||||
"""
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 TransactionBuffer store transaction updates until commit or abort.
|
||||
|
||||
A transaction may generate enough data that it is not practical to
|
||||
always hold pending updates in memory. Instead, a TransactionBuffer
|
||||
is used to store the data until a commit or abort.
|
||||
"""
|
||||
|
||||
# A faster implementation might store trans data in memory until it
|
||||
# reaches a certain size.
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import ZODB.blob
|
||||
|
||||
from ZEO._compat import Pickler, Unpickler
|
||||
|
||||
class TransactionBuffer(object):
|
||||
|
||||
# The TransactionBuffer is used by client storage to hold update
|
||||
# data until the tpc_finish(). It is only used by a single
|
||||
# thread, because only one thread can be in the two-phase commit
|
||||
# at one time.
|
||||
|
||||
def __init__(self, connection_generation):
|
||||
self.connection_generation = connection_generation
|
||||
self.file = tempfile.TemporaryFile(suffix=".tbuf")
|
||||
self.count = 0
|
||||
self.size = 0
|
||||
self.blobs = []
|
||||
# It's safe to use a fast pickler because the only objects
|
||||
# stored are builtin types -- strings or None.
|
||||
self.pickler = Pickler(self.file, 1)
|
||||
self.pickler.fast = 1
|
||||
self.server_resolved = set() # {oid}
|
||||
self.client_resolved = {} # {oid -> buffer_record_number}
|
||||
self.exception = None
|
||||
|
||||
def close(self):
|
||||
self.file.close()
|
||||
|
||||
def store(self, oid, data):
|
||||
"""Store oid, version, data for later retrieval"""
|
||||
self.pickler.dump((oid, data))
|
||||
self.count += 1
|
||||
# Estimate per-record cache size
|
||||
self.size = self.size + (data and len(data) or 0) + 31
|
||||
|
||||
def resolve(self, oid, data):
|
||||
"""Record client-resolved data
|
||||
"""
|
||||
self.store(oid, data)
|
||||
self.client_resolved[oid] = self.count - 1
|
||||
|
||||
def server_resolve(self, oid):
|
||||
self.server_resolved.add(oid)
|
||||
|
||||
def storeBlob(self, oid, blobfilename):
|
||||
self.blobs.append((oid, blobfilename))
|
||||
|
||||
def __iter__(self):
|
||||
self.file.seek(0)
|
||||
unpickler = Unpickler(self.file)
|
||||
server_resolved = self.server_resolved
|
||||
client_resolved = self.client_resolved
|
||||
|
||||
# Gaaaa, this is awkward. There can be entries in serials that
|
||||
# aren't in the buffer, because undo. Entries can be repeated
|
||||
# in the buffer, because ZODB. (Maybe this is a bug now, but
|
||||
# it may be a feature later.
|
||||
|
||||
seen = set()
|
||||
for i in range(self.count):
|
||||
oid, data = unpickler.load()
|
||||
if client_resolved.get(oid, i) == i:
|
||||
seen.add(oid)
|
||||
yield oid, data, oid in server_resolved
|
||||
|
||||
# We may have leftover oids because undo
|
||||
for oid in server_resolved:
|
||||
if oid not in seen:
|
||||
yield oid, None, True
|
||||
|
||||
|
||||
# Support ZEO4:
|
||||
|
||||
def serialnos(self, args):
|
||||
for oid in args:
|
||||
if isinstance(oid, bytes):
|
||||
self.server_resolved.add(oid)
|
||||
else:
|
||||
oid, serial = oid
|
||||
if isinstance(serial, Exception):
|
||||
self.exception = serial
|
||||
elif serial == b'rs':
|
||||
self.server_resolved.add(oid)
|
||||
@@ -0,0 +1,90 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""ZEO -- Zope Enterprise Objects.
|
||||
|
||||
See the file README.txt in this directory for an overview.
|
||||
|
||||
ZEO is now part of ZODB; ZODB's home on the web is
|
||||
|
||||
http://wiki.zope.org/ZODB
|
||||
|
||||
"""
|
||||
|
||||
def client(*args, **kw):
|
||||
import ZEO.ClientStorage
|
||||
return ZEO.ClientStorage.ClientStorage(*args, **kw)
|
||||
|
||||
def DB(*args, **kw):
|
||||
s = client(*args, **kw)
|
||||
try:
|
||||
import ZODB
|
||||
return ZODB.DB(s)
|
||||
except Exception:
|
||||
s.close()
|
||||
raise
|
||||
|
||||
def connection(*args, **kw):
|
||||
db = DB(*args, **kw)
|
||||
try:
|
||||
return db.open_then_close_db_when_connection_closes()
|
||||
except Exception:
|
||||
db.close()
|
||||
raise
|
||||
|
||||
def server(path=None, blob_dir=None, storage_conf=None, zeo_conf=None,
|
||||
port=0, threaded=True, **kw):
|
||||
"""Convenience function to start a server for interactive exploration
|
||||
|
||||
This fuction starts a ZEO server, given a storage configuration or
|
||||
a file-storage path and blob directory. You can also supply a ZEO
|
||||
configuration string or a port. If neither a ZEO port or
|
||||
configuration is supplied, a port is chosen randomly.
|
||||
|
||||
The server address and a stop function are returned. The address
|
||||
can be passed to ZEO.ClientStorage.ClientStorage or ZEO.DB to
|
||||
create a client to the server. The stop function can be called
|
||||
without arguments to stop the server.
|
||||
|
||||
Arguments:
|
||||
|
||||
path
|
||||
A file-storage path. This argument is ignored if a storage
|
||||
configuration is supplied.
|
||||
|
||||
blob_dir
|
||||
A blob directory path. This argument is ignored if a storage
|
||||
configuration is supplied.
|
||||
|
||||
storage_conf
|
||||
A storage configuration string. If none is supplied, then at
|
||||
least a file-storage path must be supplied and the storage
|
||||
configuration will be generated from the file-storage path and
|
||||
the blob directory.
|
||||
|
||||
zeo_conf
|
||||
A ZEO server configuration string.
|
||||
|
||||
port
|
||||
If no ZEO configuration is supplied, the one will be computed
|
||||
from the port. If no port is supplied, one will be chosedn
|
||||
dynamically.
|
||||
|
||||
"""
|
||||
import ZEO._forker as forker
|
||||
if storage_conf is None and path is None:
|
||||
storage_conf = '<mappingstorage>\n</mappingstorage>'
|
||||
|
||||
return forker.start_zeo_server(
|
||||
storage_conf, zeo_conf, port, keep=True, path=path,
|
||||
blob_dir=blob_dir, suicide=False, threaded=threaded, **kw)
|
||||
@@ -0,0 +1,67 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Python versions compatiblity
|
||||
"""
|
||||
import sys
|
||||
import platform
|
||||
|
||||
PY3 = sys.version_info[0] >= 3
|
||||
PY32 = sys.version_info[:2] == (3, 2)
|
||||
PYPY = getattr(platform, 'python_implementation', lambda: None)() == 'PyPy'
|
||||
WIN = sys.platform.startswith('win')
|
||||
|
||||
if PY3:
|
||||
from zodbpickle.pickle import Pickler, Unpickler as _Unpickler, dump, dumps, loads
|
||||
class Unpickler(_Unpickler):
|
||||
# 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)
|
||||
else:
|
||||
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
|
||||
|
||||
# String and Bytes IO
|
||||
from ZODB._compat import BytesIO
|
||||
|
||||
if PY3:
|
||||
|
||||
import _thread as thread
|
||||
if PY32:
|
||||
from threading import _get_ident as get_ident
|
||||
else:
|
||||
from threading import get_ident
|
||||
|
||||
|
||||
else:
|
||||
|
||||
import thread
|
||||
from thread import get_ident
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except:
|
||||
from io import StringIO
|
||||
@@ -0,0 +1,292 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Library for forking storage server and connecting client storage"""
|
||||
from __future__ import print_function
|
||||
import gc
|
||||
import os
|
||||
import sys
|
||||
import multiprocessing
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
from six.moves.queue import Empty
|
||||
import six
|
||||
|
||||
from ZEO._compat import StringIO
|
||||
|
||||
logger = logging.getLogger('ZEO.tests.forker')
|
||||
|
||||
DEBUG = os.environ.get('ZEO_TEST_SERVER_DEBUG')
|
||||
|
||||
ZEO4_SERVER = os.environ.get('ZEO4_SERVER')
|
||||
|
||||
class ZEOConfig(object):
|
||||
"""Class to generate ZEO configuration file. """
|
||||
|
||||
def __init__(self, addr, log=None, **options):
|
||||
if log:
|
||||
if isinstance(log, str):
|
||||
self.logpath = log
|
||||
elif isinstance(addr, str):
|
||||
self.logpath = addr+'.log'
|
||||
else:
|
||||
self.logpath = 'server.log'
|
||||
|
||||
if not isinstance(addr, six.string_types):
|
||||
addr = '%s:%s' % addr
|
||||
|
||||
self.log = log
|
||||
self.address = addr
|
||||
self.read_only = None
|
||||
self.loglevel = 'INFO'
|
||||
self.__dict__.update(options)
|
||||
|
||||
def dump(self, f):
|
||||
print("<zeo>", file=f)
|
||||
print("address " + self.address, file=f)
|
||||
if self.read_only is not None:
|
||||
print("read-only", self.read_only and "true" or "false", file=f)
|
||||
|
||||
for name in (
|
||||
'invalidation_queue_size', 'invalidation_age',
|
||||
'transaction_timeout', 'pid_filename', 'msgpack',
|
||||
'ssl_certificate', 'ssl_key', 'client_conflict_resolution',
|
||||
):
|
||||
v = getattr(self, name, None)
|
||||
if v:
|
||||
print(name.replace('_', '-'), v, file=f)
|
||||
|
||||
print("</zeo>", file=f)
|
||||
|
||||
if self.log:
|
||||
print("""
|
||||
<eventlog>
|
||||
level %s
|
||||
<logfile>
|
||||
path %s
|
||||
</logfile>
|
||||
</eventlog>
|
||||
""" % (self.loglevel, self.logpath), file=f)
|
||||
|
||||
def __str__(self):
|
||||
f = StringIO()
|
||||
self.dump(f)
|
||||
return f.getvalue()
|
||||
|
||||
|
||||
def runner(config, qin, qout, timeout=None,
|
||||
debug=False, name=None,
|
||||
keep=False, protocol=None):
|
||||
|
||||
if debug or DEBUG:
|
||||
debug_logging()
|
||||
|
||||
old_protocol = None
|
||||
if protocol:
|
||||
import ZEO.asyncio.server
|
||||
old_protocol = ZEO.asyncio.server.best_protocol_version
|
||||
ZEO.asyncio.server.best_protocol_version = protocol
|
||||
old_protocols = ZEO.asyncio.server.ServerProtocol.protocols
|
||||
ZEO.asyncio.server.ServerProtocol.protocols = tuple(sorted(
|
||||
set(old_protocols) | set([protocol])
|
||||
))
|
||||
|
||||
try:
|
||||
import threading
|
||||
|
||||
if ZEO4_SERVER:
|
||||
# XXX: test dependency. In practice this is
|
||||
# probably ok
|
||||
from ZEO.tests.ZEO4 import runzeo
|
||||
else:
|
||||
from . import runzeo
|
||||
|
||||
options = runzeo.ZEOOptions()
|
||||
options.realize(['-C', config])
|
||||
server = runzeo.ZEOServer(options)
|
||||
globals()[(name if name else 'last') + '_server'] = server
|
||||
server.open_storages()
|
||||
server.clear_socket()
|
||||
server.create_server()
|
||||
logger.debug('SERVER CREATED')
|
||||
if ZEO4_SERVER:
|
||||
qout.put(server.server.addr)
|
||||
else:
|
||||
qout.put(server.server.acceptor.addr)
|
||||
logger.debug('ADDRESS SENT')
|
||||
thread = threading.Thread(
|
||||
target=server.server.loop, kwargs=dict(timeout=.2),
|
||||
name=(None if name is None else name + '-server'),
|
||||
)
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
os.remove(config)
|
||||
|
||||
try:
|
||||
qin.get(timeout=timeout) # wait for shutdown
|
||||
except Empty:
|
||||
pass
|
||||
server.server.close()
|
||||
thread.join(3)
|
||||
|
||||
if not keep:
|
||||
# Try to cleanup storage files
|
||||
for storage in server.server.storages.values():
|
||||
try:
|
||||
storage.cleanup()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
qout.put(thread.is_alive())
|
||||
|
||||
except Exception:
|
||||
logger.exception("In server thread")
|
||||
|
||||
finally:
|
||||
if old_protocol:
|
||||
ZEO.asyncio.server.best_protocol_version = old_protocol
|
||||
ZEO.asyncio.server.ServerProtocol.protocols = old_protocols
|
||||
|
||||
def stop_runner(thread, config, qin, qout, stop_timeout=19, pid=None):
|
||||
qin.put('stop')
|
||||
try:
|
||||
dirty = qout.get(timeout=stop_timeout)
|
||||
except Empty:
|
||||
print("WARNING Couldn't stop server", file=sys.stderr)
|
||||
if hasattr(thread, 'terminate'):
|
||||
thread.terminate()
|
||||
os.waitpid(thread.pid, 0)
|
||||
else:
|
||||
if dirty:
|
||||
print("WARNING SERVER DIDN'T STOP CLEANLY", file=sys.stderr)
|
||||
|
||||
# The runner thread didn't stop. If it was a process,
|
||||
# give it some time to exit
|
||||
if hasattr(thread, 'pid') and thread.pid:
|
||||
os.waitpid(thread.pid, 0)
|
||||
|
||||
thread.join(stop_timeout)
|
||||
|
||||
gc.collect()
|
||||
|
||||
def start_zeo_server(storage_conf=None, zeo_conf=None, port=None, keep=False,
|
||||
path='Data.fs', protocol=None, blob_dir=None,
|
||||
suicide=True, debug=False,
|
||||
threaded=False, start_timeout=33, name=None, log=None,
|
||||
show_config=False):
|
||||
"""Start a ZEO server in a separate process.
|
||||
|
||||
Takes two positional arguments a string containing the storage conf
|
||||
and a ZEOConfig object.
|
||||
|
||||
Returns the ZEO address, the test server address, the pid, and the path
|
||||
to the config file.
|
||||
"""
|
||||
|
||||
if not storage_conf:
|
||||
storage_conf = '<filestorage>\npath %s\n</filestorage>' % path
|
||||
|
||||
if blob_dir:
|
||||
storage_conf = '<blobstorage>\nblob-dir %s\n%s\n</blobstorage>' % (
|
||||
blob_dir, storage_conf)
|
||||
|
||||
if zeo_conf is None or isinstance(zeo_conf, dict):
|
||||
if port is None:
|
||||
port = 0
|
||||
|
||||
if isinstance(port, int):
|
||||
addr = '127.0.0.1', port
|
||||
else:
|
||||
addr = port
|
||||
|
||||
z = ZEOConfig(addr, log=log)
|
||||
if zeo_conf:
|
||||
z.__dict__.update(zeo_conf)
|
||||
zeo_conf = str(z)
|
||||
|
||||
zeo_conf = str(zeo_conf) + '\n\n' + storage_conf
|
||||
if show_config:
|
||||
print(zeo_conf)
|
||||
|
||||
# Store the config info in a temp file.
|
||||
fd, tmpfile = tempfile.mkstemp(".conf", prefix='ZEO_forker', dir=os.getcwd())
|
||||
with os.fdopen(fd, 'w') as fp:
|
||||
fp.write(zeo_conf)
|
||||
|
||||
if threaded:
|
||||
from threading import Thread
|
||||
from six.moves.queue import Queue
|
||||
else:
|
||||
from multiprocessing import Process as Thread
|
||||
Queue = ThreadlessQueue
|
||||
|
||||
qin = Queue()
|
||||
qout = Queue()
|
||||
thread = Thread(
|
||||
target=runner,
|
||||
args=[tmpfile, qin, qout, 999 if suicide else None],
|
||||
kwargs=dict(debug=debug, name=name, protocol=protocol, keep=keep),
|
||||
name=(None if name is None else name + '-server-runner'),
|
||||
)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
try:
|
||||
addr = qout.get(timeout=start_timeout)
|
||||
except Exception:
|
||||
whine("SERVER FAILED TO START")
|
||||
if thread.is_alive():
|
||||
whine("Server thread/process is still running")
|
||||
elif not threaded:
|
||||
whine("Exit status", thread.exitcode)
|
||||
raise
|
||||
|
||||
def stop(stop_timeout=99):
|
||||
stop_runner(thread, tmpfile, qin, qout, stop_timeout)
|
||||
|
||||
return addr, stop
|
||||
|
||||
|
||||
def shutdown_zeo_server(stop):
|
||||
stop()
|
||||
|
||||
|
||||
def debug_logging(logger='ZEO', stream='stderr', level=logging.DEBUG):
|
||||
handler = logging.StreamHandler(getattr(sys, stream))
|
||||
logger = logging.getLogger(logger)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(level)
|
||||
|
||||
def stop():
|
||||
logger.removeHandler(handler)
|
||||
logger.setLevel(logging.NOTSET)
|
||||
|
||||
return stop
|
||||
|
||||
def whine(*message):
|
||||
print(*message, file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
|
||||
class ThreadlessQueue(object):
|
||||
|
||||
def __init__(self):
|
||||
self.cin, self.cout = multiprocessing.Pipe(False)
|
||||
|
||||
def put(self, v):
|
||||
self.cout.send(v)
|
||||
|
||||
def get(self, timeout=None):
|
||||
if self.cin.poll(timeout):
|
||||
return self.cin.recv()
|
||||
else:
|
||||
raise Empty()
|
||||
@@ -0,0 +1,78 @@
|
||||
================================
|
||||
asyncio-based networking for ZEO
|
||||
================================
|
||||
|
||||
This package provides the networking interface for ZEO. It provides a
|
||||
somewhat RPC-like API.
|
||||
|
||||
Notes
|
||||
=====
|
||||
|
||||
Sending data immediately: ayncio vs asyncore
|
||||
--------------------------------------------
|
||||
|
||||
The previous ZEO networking implementation used the ``asyncore`` library.
|
||||
When writing with asyncore, writes were done only from the event loop.
|
||||
This meant that when sending data, code would have to "wake up" the
|
||||
event loop, typically after adding data to some sort of output buffer.
|
||||
|
||||
Asyncio takes an entirely different and saner approach. When an
|
||||
application wants to send data, it writes to a transport. All
|
||||
interactions with a transport (in a correct application) are from the
|
||||
same thread, which is also the thread running any event loop.
|
||||
Transports are always either idle or sending data. When idle, the
|
||||
transport writes to the outout socket immediately. If not all data
|
||||
isn't sent, then it buffers it and becomes sending. If a transport is
|
||||
sending, then we know that the socket isn't ready for more data, so
|
||||
``write`` can just buffer the data. There's no point in waking up the
|
||||
event loop, because the socket will do so when it's ready for more
|
||||
data.
|
||||
|
||||
An exception to the paragraph above occurs when operations cross
|
||||
threads, as occures for most client operations and when a transaction
|
||||
commits on the server and results have to be sent to other clients. In
|
||||
these cases, a call_soon_threadsafe method is used which queues an
|
||||
operation and has to wake up an event loop to process it.
|
||||
|
||||
Server threading
|
||||
----------------
|
||||
|
||||
There are currently two server implementations, an implementation that
|
||||
used a thread per client (and a thread to listen for connections),
|
||||
``ZEO.asyncio.mtacceptor.Acceptor``, and an implementation that uses a
|
||||
single networking thread, ``ZEO.asyncio.server.Acceptor``. The
|
||||
implementation is selected by changing an import in
|
||||
``ZEO.StorageServer``. The currently-used implementation is
|
||||
``ZEO.asyncio.server.Acceptor``, although this sentance is likely to
|
||||
rot, so check the import to be sure. (Maybe this should be configurable.)
|
||||
|
||||
ZEO switched to a multi-threaded implementation several years ago
|
||||
because it was found to improve performance for large databases using
|
||||
magnetic disks. Because client threads are always working on behalf of
|
||||
a single client, there's not really an issue with making blocking
|
||||
calls, such as executing slow I/O operations.
|
||||
|
||||
Initially, the asyncio-based implementation used a multi-threaded
|
||||
server. A simple thread accepted connections and handed accepted
|
||||
sockets to ``create_connection``. This became a problem when SSL was
|
||||
added because ``create_connection`` sets up SSL conections as client
|
||||
connections, and doesn't provide an option to create server
|
||||
connections.
|
||||
|
||||
In response, I created an ``asyncio.Server``-based implementation.
|
||||
This required using a single thread. This was a pretty trivial
|
||||
change, however, it led to the tests becoming unstable to the point
|
||||
that it was impossible to run all tests without some failing. One
|
||||
test was broken due to a ``asyncio.Server`` `bug
|
||||
<http://bugs.python.org/issue27386>`_. It's unclear whether the test
|
||||
instability is due to ``asyncio.Server`` problems or due to latent
|
||||
test (or ZEO) bugs, but even after beating the tests mostly into
|
||||
submission, tests failures are more likely when using
|
||||
``asyncio.Server``. Beatings will continue.
|
||||
|
||||
While fighting test failures using ``asyncio.Server``, the
|
||||
multi-threaded implementation was updated to use a monkey patch to
|
||||
allow it to create SSL server connections. Aside from the real risk of a
|
||||
monkey patch, this works very well.
|
||||
|
||||
Both implementations seem to perform about the same.
|
||||
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,167 @@
|
||||
from .._compat import PY3
|
||||
|
||||
if PY3:
|
||||
import asyncio
|
||||
else:
|
||||
import trollius as asyncio
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from struct import unpack
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
INET_FAMILIES = socket.AF_INET, socket.AF_INET6
|
||||
|
||||
class Protocol(asyncio.Protocol):
|
||||
"""asyncio low-level ZEO base interface
|
||||
"""
|
||||
|
||||
# All of the code in this class runs in a single dedicated
|
||||
# thread. Thus, we can mostly avoid worrying about interleaved
|
||||
# operations.
|
||||
|
||||
# One place where special care was required was in cache setup on
|
||||
# connect. See finish connect below.
|
||||
|
||||
transport = protocol_version = None
|
||||
|
||||
def __init__(self, loop, addr):
|
||||
self.loop = loop
|
||||
self.addr = addr
|
||||
self.input = [] # Input buffer when assembling messages
|
||||
self.output = [] # Output buffer when paused
|
||||
self.paused = [] # Paused indicator, mutable to avoid attr lookup
|
||||
|
||||
# Handle the first message, the protocol handshake, differently
|
||||
self.message_received = self.first_message_received
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
closed = False
|
||||
def close(self):
|
||||
if not self.closed:
|
||||
self.closed = True
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
|
||||
def connection_made(self, transport):
|
||||
logger.info("Connected %s", self)
|
||||
|
||||
|
||||
if sys.version_info < (3, 6):
|
||||
sock = transport.get_extra_info('socket')
|
||||
if sock is not None and sock.family in INET_FAMILIES:
|
||||
# See https://bugs.python.org/issue27456 :(
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
|
||||
self.transport = transport
|
||||
|
||||
paused = self.paused
|
||||
output = self.output
|
||||
append = output.append
|
||||
writelines = transport.writelines
|
||||
from struct import pack
|
||||
|
||||
def write(message):
|
||||
if paused:
|
||||
append(message)
|
||||
else:
|
||||
writelines((pack(">I", len(message)), message))
|
||||
|
||||
self._write = write
|
||||
|
||||
def writeit(data):
|
||||
# Note, don't worry about combining messages. Iters
|
||||
# will be used with blobs, in which case, the individual
|
||||
# messages will be big to begin with.
|
||||
data = iter(data)
|
||||
for message in data:
|
||||
writelines((pack(">I", len(message)), message))
|
||||
if paused:
|
||||
append(data)
|
||||
break
|
||||
|
||||
self._writeit = writeit
|
||||
|
||||
got = 0
|
||||
want = 4
|
||||
getting_size = True
|
||||
def data_received(self, data):
|
||||
|
||||
# Low-level input handler collects data into sized messages.
|
||||
|
||||
# Note that the logic below assume that when new data pushes
|
||||
# us over what we want, we process it in one call until we
|
||||
# need more, because we assume that excess data is all in the
|
||||
# last item of self.input. This is why the exception handling
|
||||
# in the while loop is critical. Without it, an exception
|
||||
# might cause us to exit before processing all of the data we
|
||||
# should, when then causes the logic to be broken in
|
||||
# subsequent calls.
|
||||
|
||||
self.got += len(data)
|
||||
self.input.append(data)
|
||||
while self.got >= self.want:
|
||||
try:
|
||||
extra = self.got - self.want
|
||||
if extra == 0:
|
||||
collected = b''.join(self.input)
|
||||
self.input = []
|
||||
else:
|
||||
input = self.input
|
||||
self.input = [input[-1][-extra:]]
|
||||
input[-1] = input[-1][:-extra]
|
||||
collected = b''.join(input)
|
||||
|
||||
self.got = extra
|
||||
|
||||
if self.getting_size:
|
||||
# we were recieving the message size
|
||||
assert self.want == 4
|
||||
self.want = unpack(">I", collected)[0]
|
||||
self.getting_size = False
|
||||
else:
|
||||
self.want = 4
|
||||
self.getting_size = True
|
||||
self.message_received(collected)
|
||||
except Exception:
|
||||
logger.exception("data_received %s %s %s",
|
||||
self.want, self.got, self.getting_size)
|
||||
|
||||
def first_message_received(self, protocol_version):
|
||||
# Handler for first/handshake message, set up in __init__
|
||||
del self.message_received # use default handler from here on
|
||||
self.finish_connect(protocol_version)
|
||||
|
||||
def call_async(self, method, args):
|
||||
self._write(self.encode(0, True, method, args))
|
||||
|
||||
def call_async_iter(self, it):
|
||||
self._writeit(self.encode(0, True, method, args)
|
||||
for method, args in it)
|
||||
|
||||
def pause_writing(self):
|
||||
self.paused.append(1)
|
||||
|
||||
def resume_writing(self):
|
||||
paused = self.paused
|
||||
del paused[:]
|
||||
output = self.output
|
||||
writelines = self.transport.writelines
|
||||
from struct import pack
|
||||
while output and not paused:
|
||||
message = output.pop(0)
|
||||
if isinstance(message, bytes):
|
||||
writelines((pack(">I", len(message)), message))
|
||||
else:
|
||||
data = message
|
||||
for message in data:
|
||||
writelines((pack(">I", len(message)), message))
|
||||
if paused: # paused again. Put iter back.
|
||||
output.insert(0, data)
|
||||
break
|
||||
|
||||
def get_peername(self):
|
||||
return self.transport.get_extra_info('peername')
|
||||
@@ -0,0 +1,910 @@
|
||||
from ZEO.Exceptions import ClientDisconnected, ServerException
|
||||
import concurrent.futures
|
||||
import functools
|
||||
import logging
|
||||
import random
|
||||
import threading
|
||||
|
||||
import ZODB.event
|
||||
import ZODB.POSException
|
||||
|
||||
import ZEO.Exceptions
|
||||
import ZEO.interfaces
|
||||
|
||||
from . import base
|
||||
from .compat import asyncio, new_event_loop
|
||||
from .marshal import encoder, decoder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Fallback = object()
|
||||
|
||||
local_random = random.Random() # use separate generator to facilitate tests
|
||||
|
||||
def future_generator(func):
|
||||
"""Decorates a generator that generates futures
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def call_generator(*args, **kw):
|
||||
gen = func(*args, **kw)
|
||||
try:
|
||||
f = next(gen)
|
||||
except StopIteration:
|
||||
gen.close()
|
||||
else:
|
||||
def store(gen, future):
|
||||
@future.add_done_callback
|
||||
def _(future):
|
||||
try:
|
||||
try:
|
||||
result = future.result()
|
||||
except Exception as exc:
|
||||
f = gen.throw(exc)
|
||||
else:
|
||||
f = gen.send(result)
|
||||
except StopIteration:
|
||||
gen.close()
|
||||
else:
|
||||
store(gen, f)
|
||||
|
||||
store(gen, f)
|
||||
|
||||
return call_generator
|
||||
|
||||
class Protocol(base.Protocol):
|
||||
"""asyncio low-level ZEO client interface
|
||||
"""
|
||||
|
||||
# All of the code in this class runs in a single dedicated
|
||||
# thread. Thus, we can mostly avoid worrying about interleaved
|
||||
# operations.
|
||||
|
||||
# One place where special care was required was in cache setup on
|
||||
# connect. See finish connect below.
|
||||
|
||||
protocols = b'309', b'310', b'3101', b'4', b'5'
|
||||
|
||||
def __init__(self, loop,
|
||||
addr, client, storage_key, read_only, connect_poll=1,
|
||||
heartbeat_interval=60, ssl=None, ssl_server_hostname=None,
|
||||
credentials=None):
|
||||
"""Create a client interface
|
||||
|
||||
addr is either a host,port tuple or a string file name.
|
||||
|
||||
client is a ClientStorage. It must be thread safe.
|
||||
|
||||
cache is a ZEO.interfaces.IClientCache.
|
||||
"""
|
||||
super(Protocol, self).__init__(loop, addr)
|
||||
self.storage_key = storage_key
|
||||
self.read_only = read_only
|
||||
self.name = "%s(%r, %r, %r)" % (
|
||||
self.__class__.__name__, addr, storage_key, read_only)
|
||||
self.client = client
|
||||
self.connect_poll = connect_poll
|
||||
self.heartbeat_interval = heartbeat_interval
|
||||
self.futures = {} # { message_id -> future }
|
||||
self.ssl = ssl
|
||||
self.ssl_server_hostname = ssl_server_hostname
|
||||
self.credentials = credentials
|
||||
|
||||
self.connect()
|
||||
|
||||
def close(self):
|
||||
if not self.closed:
|
||||
self.closed = True
|
||||
self._connecting.cancel()
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
for future in self.pop_futures():
|
||||
future.set_exception(ClientDisconnected("Closed"))
|
||||
|
||||
def pop_futures(self):
|
||||
# Remove and return futures from self.futures. The caller
|
||||
# will finalize them in some way and callbacks may modify
|
||||
# self.futures.
|
||||
futures = list(self.futures.values())
|
||||
self.futures.clear()
|
||||
return futures
|
||||
|
||||
def protocol_factory(self):
|
||||
return self
|
||||
|
||||
def connect(self):
|
||||
if isinstance(self.addr, tuple):
|
||||
host, port = self.addr
|
||||
cr = self.loop.create_connection(
|
||||
self.protocol_factory, host or '127.0.0.1', port,
|
||||
ssl=self.ssl, server_hostname=self.ssl_server_hostname)
|
||||
else:
|
||||
cr = self.loop.create_unix_connection(
|
||||
self.protocol_factory, self.addr, ssl=self.ssl)
|
||||
|
||||
self._connecting = cr = asyncio.ensure_future(cr, loop=self.loop)
|
||||
|
||||
@cr.add_done_callback
|
||||
def done_connecting(future):
|
||||
if future.exception() is not None:
|
||||
logger.info("Connection to %r failed, retrying, %s",
|
||||
self.addr, future.exception())
|
||||
# keep trying
|
||||
if not self.closed:
|
||||
self.loop.call_later(
|
||||
self.connect_poll + local_random.random(),
|
||||
self.connect,
|
||||
)
|
||||
|
||||
def connection_made(self, transport):
|
||||
super(Protocol, self).connection_made(transport)
|
||||
self.heartbeat(write=False)
|
||||
|
||||
def connection_lost(self, exc):
|
||||
logger.debug('connection_lost %r', exc)
|
||||
self.heartbeat_handle.cancel()
|
||||
if self.closed:
|
||||
for f in self.pop_futures():
|
||||
f.cancel()
|
||||
else:
|
||||
# We have to be careful processing the futures, because
|
||||
# exception callbacks might modufy them.
|
||||
for f in self.pop_futures():
|
||||
f.set_exception(ClientDisconnected(exc or 'connection lost'))
|
||||
self.closed = True
|
||||
self.client.disconnected(self)
|
||||
|
||||
@future_generator
|
||||
def finish_connect(self, protocol_version):
|
||||
# The future implementation we use differs from
|
||||
# asyncio.Future in that callbacks are called immediately,
|
||||
# rather than using the loops call_soon. We want to avoid a
|
||||
# race between invalidations and cache initialization. In
|
||||
# particular, after getting a response from lastTransaction or
|
||||
# getInvalidations, we want to make sure we set the cache's
|
||||
# lastTid before processing (and possibly missing) subsequent
|
||||
# invalidations.
|
||||
|
||||
version = min(protocol_version[1:], self.protocols[-1])
|
||||
if version not in self.protocols:
|
||||
self.client.register_failed(
|
||||
self, ZEO.Exceptions.ProtocolError(protocol_version))
|
||||
return
|
||||
|
||||
self.protocol_version = protocol_version[:1] + version
|
||||
self.encode = encoder(protocol_version)
|
||||
self.decode = decoder(protocol_version)
|
||||
self.heartbeat_bytes = self.encode(-1, 0, '.reply', None)
|
||||
|
||||
self._write(self.protocol_version)
|
||||
|
||||
credentials = (self.credentials,) if self.credentials else ()
|
||||
|
||||
try:
|
||||
try:
|
||||
server_tid = yield self.fut(
|
||||
'register', self.storage_key,
|
||||
self.read_only if self.read_only is not Fallback else False,
|
||||
*credentials)
|
||||
except ZODB.POSException.ReadOnlyError:
|
||||
if self.read_only is Fallback:
|
||||
self.read_only = True
|
||||
server_tid = yield self.fut(
|
||||
'register', self.storage_key, True, *credentials)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
if self.read_only is Fallback:
|
||||
self.read_only = False
|
||||
except Exception as exc:
|
||||
self.client.register_failed(self, exc)
|
||||
else:
|
||||
self.client.registered(self, server_tid)
|
||||
|
||||
exception_type_type = type(Exception)
|
||||
def message_received(self, data):
|
||||
msgid, async_, name, args = self.decode(data)
|
||||
if name == '.reply':
|
||||
future = self.futures.pop(msgid)
|
||||
if async_: # ZEO 5 exception
|
||||
class_, args = args
|
||||
factory = exc_factories.get(class_)
|
||||
if factory:
|
||||
exc = factory(class_, args)
|
||||
if not isinstance(exc, unlogged_exceptions):
|
||||
logger.error("%s from server: %s:%s",
|
||||
self.name, class_, args)
|
||||
else:
|
||||
exc = ServerException(class_, args)
|
||||
future.set_exception(exc)
|
||||
elif (isinstance(args, tuple) and len(args) > 1 and
|
||||
type(args[0]) == self.exception_type_type and
|
||||
issubclass(args[0], Exception)
|
||||
):
|
||||
if not issubclass(args[0], unlogged_exceptions):
|
||||
logger.error("%s from server: %s.%s:%s",
|
||||
self.name,
|
||||
args[0].__module__,
|
||||
args[0].__name__,
|
||||
args[1])
|
||||
future.set_exception(args[1])
|
||||
else:
|
||||
future.set_result(args)
|
||||
else:
|
||||
assert async_ # clients only get async calls
|
||||
if name in self.client_methods:
|
||||
getattr(self.client, name)(*args)
|
||||
else:
|
||||
raise AttributeError(name)
|
||||
|
||||
message_id = 0
|
||||
def call(self, future, method, args):
|
||||
self.message_id += 1
|
||||
self.futures[self.message_id] = future
|
||||
self._write(self.encode(self.message_id, False, method, args))
|
||||
return future
|
||||
|
||||
def fut(self, method, *args):
|
||||
return self.call(Fut(), method, args)
|
||||
|
||||
def load_before(self, oid, tid):
|
||||
# Special-case loadBefore, so we collapse outstanding requests
|
||||
message_id = (oid, tid)
|
||||
future = self.futures.get(message_id)
|
||||
if future is None:
|
||||
future = asyncio.Future(loop=self.loop)
|
||||
self.futures[message_id] = future
|
||||
self._write(
|
||||
self.encode(message_id, False, 'loadBefore', (oid, tid)))
|
||||
return future
|
||||
|
||||
# Methods called by the server.
|
||||
# WARNING WARNING we can't call methods that call back to us
|
||||
# syncronously, as that would lead to DEADLOCK!
|
||||
|
||||
client_methods = (
|
||||
'invalidateTransaction', 'serialnos', 'info',
|
||||
'receiveBlobStart', 'receiveBlobChunk', 'receiveBlobStop',
|
||||
# plus: notify_connected, notify_disconnected
|
||||
)
|
||||
client_delegated = client_methods[2:]
|
||||
|
||||
def heartbeat(self, write=True):
|
||||
if write:
|
||||
self._write(self.heartbeat_bytes)
|
||||
self.heartbeat_handle = self.loop.call_later(
|
||||
self.heartbeat_interval, self.heartbeat)
|
||||
|
||||
def create_Exception(class_, args):
|
||||
return exc_classes[class_](*args)
|
||||
|
||||
def create_ConflictError(class_, args):
|
||||
exc = exc_classes[class_](
|
||||
message = args['message'],
|
||||
oid = args['oid'],
|
||||
serials = args['serials'],
|
||||
)
|
||||
exc.class_name = args.get('class_name')
|
||||
return exc
|
||||
|
||||
def create_BTreesConflictError(class_, args):
|
||||
return ZODB.POSException.BTreesConflictError(
|
||||
p1 = args['p1'],
|
||||
p2 = args['p2'],
|
||||
p3 = args['p3'],
|
||||
reason = args['reason'],
|
||||
)
|
||||
|
||||
def create_MultipleUndoErrors(class_, args):
|
||||
return ZODB.POSException.MultipleUndoErrors(args['_errs'])
|
||||
|
||||
exc_classes = {
|
||||
'builtins.KeyError': KeyError,
|
||||
'builtins.TypeError': TypeError,
|
||||
'exceptions.KeyError': KeyError,
|
||||
'exceptions.TypeError': TypeError,
|
||||
'ZODB.POSException.ConflictError': ZODB.POSException.ConflictError,
|
||||
'ZODB.POSException.POSKeyError': ZODB.POSException.POSKeyError,
|
||||
'ZODB.POSException.ReadConflictError': ZODB.POSException.ReadConflictError,
|
||||
'ZODB.POSException.ReadOnlyError': ZODB.POSException.ReadOnlyError,
|
||||
'ZODB.POSException.StorageTransactionError':
|
||||
ZODB.POSException.StorageTransactionError,
|
||||
}
|
||||
exc_factories = {
|
||||
'builtins.KeyError': create_Exception,
|
||||
'builtins.TypeError': create_Exception,
|
||||
'exceptions.KeyError': create_Exception,
|
||||
'exceptions.TypeError': create_Exception,
|
||||
'ZODB.POSException.BTreesConflictError': create_BTreesConflictError,
|
||||
'ZODB.POSException.ConflictError': create_ConflictError,
|
||||
'ZODB.POSException.MultipleUndoErrors': create_MultipleUndoErrors,
|
||||
'ZODB.POSException.POSKeyError': create_Exception,
|
||||
'ZODB.POSException.ReadConflictError': create_ConflictError,
|
||||
'ZODB.POSException.ReadOnlyError': create_Exception,
|
||||
'ZODB.POSException.StorageTransactionError': create_Exception,
|
||||
}
|
||||
unlogged_exceptions = (ZODB.POSException.POSKeyError,
|
||||
ZODB.POSException.ConflictError)
|
||||
class Client(object):
|
||||
"""asyncio low-level ZEO client interface
|
||||
"""
|
||||
|
||||
# All of the code in this class runs in a single dedicated
|
||||
# thread. Thus, we can mostly avoid worrying about interleaved
|
||||
# operations.
|
||||
|
||||
# One place where special care was required was in cache setup on
|
||||
# connect.
|
||||
|
||||
protocol = None
|
||||
ready = None # Tri-value: None=Never connected, True=connected,
|
||||
# False=Disconnected
|
||||
|
||||
def __init__(self, loop,
|
||||
addrs, client, cache, storage_key, read_only, connect_poll,
|
||||
register_failed_poll=9,
|
||||
ssl=None, ssl_server_hostname=None, credentials=None):
|
||||
"""Create a client interface
|
||||
|
||||
addr is either a host,port tuple or a string file name.
|
||||
|
||||
client is a ClientStorage. It must be thread safe.
|
||||
|
||||
cache is a ZEO.interfaces.IClientCache.
|
||||
"""
|
||||
self.loop = loop
|
||||
self.addrs = addrs
|
||||
self.storage_key = storage_key
|
||||
self.read_only = read_only
|
||||
self.connect_poll = connect_poll
|
||||
self.register_failed_poll = register_failed_poll
|
||||
self.client = client
|
||||
self.ssl = ssl
|
||||
self.ssl_server_hostname = ssl_server_hostname
|
||||
self.credentials = credentials
|
||||
for name in Protocol.client_delegated:
|
||||
setattr(self, name, getattr(client, name))
|
||||
self.cache = cache
|
||||
self.protocols = ()
|
||||
self.disconnected(None)
|
||||
|
||||
# Work around odd behavior of ZEO4 server. It may send
|
||||
# invalidations for transactions later than the result of
|
||||
# getInvalidations. While we support ZEO 4 servers, we'll
|
||||
# need to keep an invalidation queue. :(
|
||||
self.verify_invalidation_queue = []
|
||||
|
||||
def new_addrs(self, addrs):
|
||||
self.addrs = addrs
|
||||
if self.trying_to_connect():
|
||||
self.disconnected(None)
|
||||
|
||||
def trying_to_connect(self):
|
||||
"""Return whether we're trying to connect
|
||||
|
||||
Either because we're disconnected, or because we're connected
|
||||
read-only, but want a writable connection if we can get one.
|
||||
"""
|
||||
return (not self.ready or
|
||||
self.is_read_only() and self.read_only is Fallback)
|
||||
|
||||
closed = False
|
||||
def close(self):
|
||||
if not self.closed:
|
||||
self.closed = True
|
||||
self.ready = False
|
||||
if self.protocol is not None:
|
||||
self.protocol.close()
|
||||
self.cache.close()
|
||||
self._clear_protocols()
|
||||
|
||||
def _clear_protocols(self, protocol=None):
|
||||
for p in self.protocols:
|
||||
if p is not protocol:
|
||||
p.close()
|
||||
self.protocols = ()
|
||||
|
||||
def disconnected(self, protocol=None):
|
||||
logger.debug('disconnected %r %r', self, protocol)
|
||||
if protocol is None or protocol is self.protocol:
|
||||
if protocol is self.protocol and protocol is not None:
|
||||
self.client.notify_disconnected()
|
||||
if self.ready:
|
||||
self.ready = False
|
||||
self.connected = concurrent.futures.Future()
|
||||
self.protocol = None
|
||||
self._clear_protocols()
|
||||
|
||||
if all(p.closed for p in self.protocols):
|
||||
self.try_connecting()
|
||||
|
||||
def upgrade(self, protocol):
|
||||
self.ready = False
|
||||
self.connected = concurrent.futures.Future()
|
||||
self.protocol.close()
|
||||
self.protocol = protocol
|
||||
self._clear_protocols(protocol)
|
||||
|
||||
def try_connecting(self):
|
||||
logger.debug('try_connecting')
|
||||
if not self.closed:
|
||||
self.protocols = [
|
||||
Protocol(self.loop, addr, self,
|
||||
self.storage_key, self.read_only, self.connect_poll,
|
||||
ssl=self.ssl,
|
||||
ssl_server_hostname=self.ssl_server_hostname,
|
||||
credentials=self.credentials,
|
||||
)
|
||||
for addr in self.addrs
|
||||
]
|
||||
|
||||
def registered(self, protocol, server_tid):
|
||||
if self.protocol is None:
|
||||
self.protocol = protocol
|
||||
if not (self.read_only is Fallback and protocol.read_only):
|
||||
# We're happy with this protocol. Tell the others to
|
||||
# stop trying.
|
||||
self._clear_protocols(protocol)
|
||||
self.verify(server_tid)
|
||||
elif (self.read_only is Fallback and not protocol.read_only and
|
||||
self.protocol.read_only):
|
||||
self.upgrade(protocol)
|
||||
self.verify(server_tid)
|
||||
else:
|
||||
protocol.close() # too late, we went home with another
|
||||
|
||||
def register_failed(self, protocol, exc):
|
||||
# A protocol failed registration. That's weird. If they've all
|
||||
# failed, we should try again in a bit.
|
||||
if protocol is not self:
|
||||
protocol.close()
|
||||
logger.exception("Registration or cache validation failed, %s", exc)
|
||||
if (self.protocol is None and not
|
||||
any(not p.closed for p in self.protocols)
|
||||
):
|
||||
self.loop.call_later(
|
||||
self.register_failed_poll + local_random.random(),
|
||||
self.try_connecting)
|
||||
|
||||
verify_result = None # for tests
|
||||
|
||||
@future_generator
|
||||
def verify(self, server_tid):
|
||||
self.verify_invalidation_queue = [] # See comment in init :(
|
||||
|
||||
protocol = self.protocol
|
||||
if server_tid is None:
|
||||
server_tid = yield protocol.fut('lastTransaction')
|
||||
|
||||
try:
|
||||
cache = self.cache
|
||||
if cache:
|
||||
cache_tid = cache.getLastTid()
|
||||
if not cache_tid:
|
||||
self.verify_result = "Non-empty cache w/o tid"
|
||||
logger.error("Non-empty cache w/o tid -- clearing")
|
||||
cache.clear()
|
||||
self.client.invalidateCache()
|
||||
elif cache_tid > server_tid:
|
||||
self.verify_result = "Cache newer than server"
|
||||
logger.critical(
|
||||
'Client has seen newer transactions than server!')
|
||||
raise AssertionError("Server behind client, %r < %r, %s",
|
||||
server_tid, cache_tid, protocol)
|
||||
elif cache_tid == server_tid:
|
||||
self.verify_result = "Cache up to date"
|
||||
else:
|
||||
vdata = yield protocol.fut('getInvalidations', cache_tid)
|
||||
if vdata:
|
||||
self.verify_result = "quick verification"
|
||||
server_tid, oids = vdata
|
||||
for oid in oids:
|
||||
cache.invalidate(oid, None)
|
||||
self.client.invalidateTransaction(server_tid, oids)
|
||||
else:
|
||||
# cache is too old
|
||||
self.verify_result = "cache too old, clearing"
|
||||
try:
|
||||
ZODB.event.notify(
|
||||
ZEO.interfaces.StaleCache(self.client))
|
||||
except Exception:
|
||||
logger.exception("sending StaleCache event")
|
||||
logger.critical(
|
||||
"%s dropping stale cache",
|
||||
getattr(self.client, '__name__', ''),
|
||||
)
|
||||
self.cache.clear()
|
||||
self.client.invalidateCache()
|
||||
else:
|
||||
self.verify_result = "empty cache"
|
||||
|
||||
except Exception as exc:
|
||||
del self.protocol
|
||||
self.register_failed(protocol, exc)
|
||||
else:
|
||||
# The cache is validated and the last tid we got from the server.
|
||||
# Set ready so we apply any invalidations that follow.
|
||||
# We've been ignoring them up to this point.
|
||||
self.cache.setLastTid(server_tid)
|
||||
self.ready = True
|
||||
|
||||
# Gaaaa, ZEO 4 work around. See comment in __init__. :(
|
||||
for tid, oids in self.verify_invalidation_queue:
|
||||
if tid > server_tid:
|
||||
self.invalidateTransaction(tid, oids)
|
||||
self.verify_invalidation_queue = []
|
||||
|
||||
try:
|
||||
info = yield protocol.fut('get_info')
|
||||
except Exception as exc:
|
||||
# This is weird. We were connected and verified our cache, but
|
||||
# Now we errored getting info.
|
||||
|
||||
# XXX Need a test fpr this. The lone before is what we
|
||||
# had, but it's wrong.
|
||||
self.register_failed(self, exc)
|
||||
|
||||
else:
|
||||
self.client.notify_connected(self, info)
|
||||
self.connected.set_result(None)
|
||||
|
||||
def get_peername(self):
|
||||
return self.protocol.get_peername()
|
||||
|
||||
def call_async_threadsafe(self, future, wait_ready, method, args):
|
||||
if self.ready:
|
||||
self.protocol.call_async(method, args)
|
||||
future.set_result(None)
|
||||
else:
|
||||
future.set_exception(ClientDisconnected())
|
||||
|
||||
def call_async_from_same_thread(self, method, *args):
|
||||
return self.protocol.call_async(method, args)
|
||||
|
||||
def call_async_iter_threadsafe(self, future, wait_ready, it):
|
||||
if self.ready:
|
||||
self.protocol.call_async_iter(it)
|
||||
future.set_result(None)
|
||||
else:
|
||||
future.set_exception(ClientDisconnected())
|
||||
|
||||
def _when_ready(self, func, result_future, *args):
|
||||
|
||||
if self.ready is None:
|
||||
# We started without waiting for a connection. (prob tests :( )
|
||||
result_future.set_exception(ClientDisconnected("never connected"))
|
||||
else:
|
||||
@self.connected.add_done_callback
|
||||
def done(future):
|
||||
e = future.exception()
|
||||
if e is not None:
|
||||
future.set_exception(e)
|
||||
else:
|
||||
if self.ready:
|
||||
func(result_future, *args)
|
||||
else:
|
||||
self._when_ready(func, result_future, *args)
|
||||
|
||||
def call_threadsafe(self, future, wait_ready, method, args):
|
||||
if self.ready:
|
||||
self.protocol.call(future, method, args)
|
||||
elif wait_ready:
|
||||
self._when_ready(
|
||||
self.call_threadsafe, future, wait_ready, method, args)
|
||||
else:
|
||||
future.set_exception(ClientDisconnected())
|
||||
|
||||
# Special methods because they update the cache.
|
||||
|
||||
@future_generator
|
||||
def load_before_threadsafe(self, future, wait_ready, oid, tid):
|
||||
data = self.cache.loadBefore(oid, tid)
|
||||
if data is not None:
|
||||
future.set_result(data)
|
||||
elif self.ready:
|
||||
try:
|
||||
data = yield self.protocol.load_before(oid, tid)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
else:
|
||||
future.set_result(data)
|
||||
if data:
|
||||
data, start, end = data
|
||||
self.cache.store(oid, start, end, data)
|
||||
elif wait_ready:
|
||||
self._when_ready(
|
||||
self.load_before_threadsafe, future, wait_ready, oid, tid)
|
||||
else:
|
||||
future.set_exception(ClientDisconnected())
|
||||
|
||||
@future_generator
|
||||
def _prefetch(self, oid, tid):
|
||||
try:
|
||||
data = yield self.protocol.load_before(oid, tid)
|
||||
if data:
|
||||
data, start, end = data
|
||||
self.cache.store(oid, start, end, data)
|
||||
except Exception:
|
||||
logger.exception("prefetch %r %r" % (oid, tid))
|
||||
|
||||
def prefetch(self, future, wait_ready, oids, tid):
|
||||
if self.ready:
|
||||
for oid in oids:
|
||||
if self.cache.loadBefore(oid, tid) is None:
|
||||
self._prefetch(oid, tid)
|
||||
|
||||
future.set_result(None)
|
||||
else:
|
||||
future.set_exception(ClientDisconnected())
|
||||
|
||||
@future_generator
|
||||
def tpc_finish_threadsafe(self, future, wait_ready, tid, updates, f):
|
||||
if self.ready:
|
||||
try:
|
||||
tid = yield self.protocol.fut('tpc_finish', tid)
|
||||
cache = self.cache
|
||||
for oid, data, resolved in updates:
|
||||
cache.invalidate(oid, tid)
|
||||
if data and not resolved:
|
||||
cache.store(oid, tid, None, data)
|
||||
cache.setLastTid(tid)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# At this point, our cache is in an inconsistent
|
||||
# state. We need to reconnect in hopes of
|
||||
# recovering to a consistent state.
|
||||
self.protocol.close()
|
||||
self.disconnected(self.protocol)
|
||||
else:
|
||||
f(tid)
|
||||
future.set_result(tid)
|
||||
else:
|
||||
future.set_exception(ClientDisconnected())
|
||||
|
||||
def close_threadsafe(self, future, _):
|
||||
self.close()
|
||||
future.set_result(None)
|
||||
|
||||
def invalidateTransaction(self, tid, oids):
|
||||
if self.ready:
|
||||
for oid in oids:
|
||||
self.cache.invalidate(oid, tid)
|
||||
self.client.invalidateTransaction(tid, oids)
|
||||
self.cache.setLastTid(tid)
|
||||
else:
|
||||
self.verify_invalidation_queue.append((tid, oids))
|
||||
|
||||
def serialnos(self, serials):
|
||||
# Method called by ZEO4 storage servers.
|
||||
|
||||
# Before delegating, check for errors (likely ConflictErrors)
|
||||
# and invalidate the oids they're associated with. In the
|
||||
# past, this was done by the client, but now we control the
|
||||
# cache and this is our last chance, as the client won't call
|
||||
# back into us when there's an error.
|
||||
for oid in serials:
|
||||
if isinstance(oid, bytes):
|
||||
self.cache.invalidate(oid, None)
|
||||
else:
|
||||
oid, serial = oid
|
||||
if isinstance(serial, Exception) or serial == b'rs':
|
||||
self.cache.invalidate(oid, None)
|
||||
|
||||
self.client.serialnos(serials)
|
||||
|
||||
@property
|
||||
def protocol_version(self):
|
||||
return self.protocol.protocol_version
|
||||
|
||||
def is_read_only(self):
|
||||
try:
|
||||
protocol = self.protocol
|
||||
except AttributeError:
|
||||
return self.read_only
|
||||
else:
|
||||
if protocol is None:
|
||||
return self.read_only
|
||||
else:
|
||||
return protocol.read_only
|
||||
|
||||
class ClientRunner(object):
|
||||
|
||||
def set_options(self, addrs, wrapper, cache, storage_key, read_only,
|
||||
timeout=30, disconnect_poll=1,
|
||||
**kwargs):
|
||||
self.__args = (addrs, wrapper, cache, storage_key, read_only,
|
||||
disconnect_poll)
|
||||
self.__kwargs = kwargs
|
||||
self.timeout = timeout
|
||||
|
||||
def setup_delegation(self, loop):
|
||||
self.loop = loop
|
||||
self.client = Client(loop, *self.__args, **self.__kwargs)
|
||||
self.call_threadsafe = self.client.call_threadsafe
|
||||
self.call_async_threadsafe = self.client.call_async_threadsafe
|
||||
|
||||
from concurrent.futures import Future
|
||||
call_soon_threadsafe = loop.call_soon_threadsafe
|
||||
|
||||
def call(meth, *args, **kw):
|
||||
timeout = kw.pop('timeout', None)
|
||||
assert not kw
|
||||
|
||||
# Some explanation of the code below.
|
||||
# Timeouts on Python 2 are expensive, so we try to avoid
|
||||
# them if we're connected. The 3rd argument below is a
|
||||
# wait flag. If false, and we're disconnected, we fail
|
||||
# immediately. If that happens, then we try again with the
|
||||
# wait flag set to True and wait with the default timeout.
|
||||
result = Future()
|
||||
call_soon_threadsafe(meth, result, timeout is not None, *args)
|
||||
try:
|
||||
return self.wait_for_result(result, timeout)
|
||||
except ClientDisconnected:
|
||||
if timeout is None:
|
||||
result = Future()
|
||||
call_soon_threadsafe(meth, result, True, *args)
|
||||
return self.wait_for_result(result, self.timeout)
|
||||
else:
|
||||
raise
|
||||
|
||||
self.__call = call
|
||||
|
||||
def wait_for_result(self, future, timeout):
|
||||
try:
|
||||
return future.result(timeout)
|
||||
except concurrent.futures.TimeoutError:
|
||||
if not self.client.ready:
|
||||
raise ClientDisconnected("timed out waiting for connection")
|
||||
else:
|
||||
raise
|
||||
|
||||
def call(self, method, *args, **kw):
|
||||
return self.__call(self.call_threadsafe, method, args, **kw)
|
||||
|
||||
def call_future(self, method, *args):
|
||||
# for tests
|
||||
result = concurrent.futures.Future()
|
||||
self.loop.call_soon_threadsafe(
|
||||
self.call_threadsafe, result, True, method, args)
|
||||
return result
|
||||
|
||||
def async_(self, method, *args):
|
||||
return self.__call(self.call_async_threadsafe, method, args)
|
||||
|
||||
def async_iter(self, it):
|
||||
return self.__call(self.client.call_async_iter_threadsafe, it)
|
||||
|
||||
def prefetch(self, oids, tid):
|
||||
return self.__call(self.client.prefetch, oids, tid)
|
||||
|
||||
def load_before(self, oid, tid):
|
||||
return self.__call(self.client.load_before_threadsafe, oid, tid)
|
||||
|
||||
def tpc_finish(self, tid, updates, f):
|
||||
return self.__call(self.client.tpc_finish_threadsafe, tid, updates, f)
|
||||
|
||||
def is_connected(self):
|
||||
return self.client.ready
|
||||
|
||||
def is_read_only(self):
|
||||
try:
|
||||
protocol = self.client.protocol
|
||||
except AttributeError:
|
||||
return True
|
||||
else:
|
||||
if protocol is None:
|
||||
return True
|
||||
else:
|
||||
return protocol.read_only
|
||||
|
||||
def close(self):
|
||||
self.__call(self.client.close_threadsafe)
|
||||
|
||||
# Short circuit from now on. We're closed.
|
||||
def call_closed(*a, **k):
|
||||
raise ClientDisconnected('closed')
|
||||
|
||||
self.__call = call_closed
|
||||
|
||||
def apply_threadsafe(self, future, wait_ready, func, *args):
|
||||
try:
|
||||
future.set_result(func(*args))
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
def new_addrs(self, addrs):
|
||||
# This usually doesn't have an immediate effect, since the
|
||||
# addrs aren't used until the client disconnects.xs
|
||||
self.__call(self.apply_threadsafe, self.client.new_addrs, addrs)
|
||||
|
||||
def wait(self, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
self.wait_for_result(self.client.connected, timeout)
|
||||
|
||||
class ClientThread(ClientRunner):
|
||||
"""Thread wrapper for client interface
|
||||
|
||||
A ClientProtocol is run in a dedicated thread.
|
||||
|
||||
Calls to it are made in a thread-safe fashion.
|
||||
"""
|
||||
|
||||
def __init__(self, addrs, client, cache,
|
||||
storage_key='1', read_only=False, timeout=30,
|
||||
disconnect_poll=1, ssl=None, ssl_server_hostname=None,
|
||||
credentials=None):
|
||||
self.set_options(addrs, client, cache, storage_key, read_only,
|
||||
timeout, disconnect_poll,
|
||||
ssl=ssl, ssl_server_hostname=ssl_server_hostname,
|
||||
credentials=credentials)
|
||||
self.thread = threading.Thread(
|
||||
target=self.run,
|
||||
name="%s zeo client networking thread" % client.__name__,
|
||||
)
|
||||
self.thread.setDaemon(True)
|
||||
self.started = threading.Event()
|
||||
self.thread.start()
|
||||
self.started.wait()
|
||||
if self.exception:
|
||||
raise self.exception
|
||||
|
||||
exception = None
|
||||
def run(self):
|
||||
loop = None
|
||||
try:
|
||||
loop = new_event_loop()
|
||||
self.setup_delegation(loop)
|
||||
self.started.set()
|
||||
loop.run_forever()
|
||||
except Exception as exc:
|
||||
raise
|
||||
logger.exception("Client thread")
|
||||
self.exception = exc
|
||||
finally:
|
||||
if not self.closed:
|
||||
self.closed = True
|
||||
try:
|
||||
if self.client.ready:
|
||||
self.client.ready = False
|
||||
self.client.client.notify_disconnected()
|
||||
except AttributeError:
|
||||
pass
|
||||
logger.critical("Client loop stopped unexpectedly")
|
||||
if loop is not None:
|
||||
loop.close()
|
||||
logger.debug('Stopping client thread')
|
||||
|
||||
closed = False
|
||||
def close(self):
|
||||
if not self.closed:
|
||||
self.closed = True
|
||||
super(ClientThread, self).close()
|
||||
self.loop.call_soon_threadsafe(self.loop.stop)
|
||||
self.thread.join(9)
|
||||
if self.exception:
|
||||
raise self.exception
|
||||
|
||||
class Fut(object):
|
||||
"""Lightweight future that calls it's callback immediately rather than soon
|
||||
"""
|
||||
|
||||
def add_done_callback(self, cb):
|
||||
self.cb = cb
|
||||
|
||||
exc = None
|
||||
def set_exception(self, exc):
|
||||
self.exc = exc
|
||||
self.cb(self)
|
||||
|
||||
def set_result(self, result):
|
||||
self._result = result
|
||||
self.cb(self)
|
||||
|
||||
def result(self):
|
||||
if self.exc:
|
||||
raise self.exc
|
||||
else:
|
||||
return self._result
|
||||
@@ -0,0 +1,10 @@
|
||||
from .._compat import PY3
|
||||
if PY3:
|
||||
import asyncio
|
||||
try:
|
||||
from uvloop import new_event_loop
|
||||
except ImportError:
|
||||
from asyncio import new_event_loop
|
||||
else:
|
||||
import trollius as asyncio
|
||||
from trollius import new_event_loop
|
||||
@@ -0,0 +1,168 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 marshaling ZEO messages
|
||||
|
||||
Not to be confused with marshaling objects in ZODB.
|
||||
|
||||
We currently use pickle. In the future, we may use a
|
||||
Python-independent format, or possibly a minimal pickle subset.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from .._compat import Unpickler, Pickler, BytesIO, PY3, PYPY
|
||||
from ..shortrepr import short_repr
|
||||
|
||||
PY2 = not PY3
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def encoder(protocol, server=False):
|
||||
"""Return a non-thread-safe encoder
|
||||
"""
|
||||
|
||||
if protocol[:1] == b'M':
|
||||
from msgpack import packb
|
||||
default = server_default if server else None
|
||||
def encode(*args):
|
||||
return packb(
|
||||
args, use_bin_type=True, default=default)
|
||||
|
||||
return encode
|
||||
else:
|
||||
assert protocol[:1] == b'Z'
|
||||
|
||||
f = BytesIO()
|
||||
getvalue = f.getvalue
|
||||
seek = f.seek
|
||||
truncate = f.truncate
|
||||
pickler = Pickler(f, 3)
|
||||
pickler.fast = 1
|
||||
dump = pickler.dump
|
||||
def encode(*args):
|
||||
seek(0)
|
||||
truncate()
|
||||
dump(args)
|
||||
return getvalue()
|
||||
|
||||
return encode
|
||||
|
||||
def encode(*args):
|
||||
|
||||
return encoder(b'Z')(*args)
|
||||
|
||||
def decoder(protocol):
|
||||
if protocol[:1] == b'M':
|
||||
from msgpack import unpackb
|
||||
def msgpack_decode(data):
|
||||
"""Decodes msg and returns its parts"""
|
||||
return unpackb(data, encoding='utf-8', use_list=False)
|
||||
return msgpack_decode
|
||||
else:
|
||||
assert protocol[:1] == b'Z'
|
||||
return pickle_decode
|
||||
|
||||
def pickle_decode(msg):
|
||||
"""Decodes msg and returns its parts"""
|
||||
unpickler = Unpickler(BytesIO(msg))
|
||||
unpickler.find_global = find_global
|
||||
try:
|
||||
# PyPy, zodbpickle, the non-c-accelerated version
|
||||
unpickler.find_class = find_global
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return unpickler.load() # msgid, flags, name, args
|
||||
except:
|
||||
logger.error("can't decode message: %s" % short_repr(msg))
|
||||
raise
|
||||
|
||||
def server_decoder(protocol):
|
||||
if protocol[:1] == b'M':
|
||||
return decoder(protocol)
|
||||
else:
|
||||
assert protocol[:1] == b'Z'
|
||||
return pickle_server_decode
|
||||
|
||||
def pickle_server_decode(msg):
|
||||
"""Decodes msg and returns its parts"""
|
||||
unpickler = Unpickler(BytesIO(msg))
|
||||
unpickler.find_global = server_find_global
|
||||
try:
|
||||
# PyPy, zodbpickle, the non-c-accelerated version
|
||||
unpickler.find_class = server_find_global
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return unpickler.load() # msgid, flags, name, args
|
||||
except:
|
||||
logger.error("can't decode message: %s" % short_repr(msg))
|
||||
raise
|
||||
|
||||
def server_default(obj):
|
||||
if isinstance(obj, Exception):
|
||||
return reduce_exception(obj)
|
||||
else:
|
||||
return obj
|
||||
|
||||
def reduce_exception(exc):
|
||||
class_ = exc.__class__
|
||||
class_ = "%s.%s" % (class_.__module__, class_.__name__)
|
||||
return class_, exc.__dict__ or exc.args
|
||||
|
||||
_globals = globals()
|
||||
_silly = ('__doc__',)
|
||||
|
||||
exception_type_type = type(Exception)
|
||||
|
||||
_SAFE_MODULE_NAMES = ('ZopeUndo.Prefix', 'copy_reg', '__builtin__', 'zodbpickle')
|
||||
|
||||
def find_global(module, name):
|
||||
"""Helper for message unpickler"""
|
||||
try:
|
||||
m = __import__(module, _globals, _globals, _silly)
|
||||
except ImportError as msg:
|
||||
raise ImportError("import error %s: %s" % (module, msg))
|
||||
|
||||
try:
|
||||
r = getattr(m, name)
|
||||
except AttributeError:
|
||||
raise ImportError("module %s has no global %s" % (module, name))
|
||||
|
||||
safe = getattr(r, '__no_side_effects__', 0) or (PY2 and module in _SAFE_MODULE_NAMES)
|
||||
if safe:
|
||||
return r
|
||||
|
||||
# TODO: is there a better way to do this?
|
||||
if type(r) == exception_type_type and issubclass(r, Exception):
|
||||
return r
|
||||
|
||||
raise ImportError("Unsafe global: %s.%s" % (module, name))
|
||||
|
||||
def server_find_global(module, name):
|
||||
"""Helper for message unpickler"""
|
||||
if module not in _SAFE_MODULE_NAMES:
|
||||
raise ImportError("Module not allowed: %s" % (module,))
|
||||
|
||||
try:
|
||||
m = __import__(module, _globals, _globals, _silly)
|
||||
except ImportError as msg:
|
||||
raise ImportError("import error %s: %s" % (module, msg))
|
||||
|
||||
try:
|
||||
r = getattr(m, name)
|
||||
except AttributeError:
|
||||
raise ImportError("module %s has no global %s" % (module, name))
|
||||
|
||||
return r
|
||||
@@ -0,0 +1,222 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Multi-threaded server connectin acceptor
|
||||
|
||||
Each connection is run in it's own thread. Testing serveral years ago
|
||||
suggsted that this was a win, but ZODB shootout and another
|
||||
lower-level tests suggest otherwise. It's really unclear, which is
|
||||
why we're keeping this around for now.
|
||||
|
||||
Asyncio doesn't let you accept connections in one thread and handle
|
||||
them in another. To get around this, we have a listener implemented
|
||||
using asyncore, but when we get a connection, we hand the socket to
|
||||
asyncio. This worked well until we added SSL support. (Even then, it
|
||||
worked on Mac OS X for some reason.)
|
||||
|
||||
SSL + non-blocking sockets requires special care, which asyncio
|
||||
provides. Unfortunately, create_connection, assumes it's creating a
|
||||
client connection. It would be easy to fix this,
|
||||
http://bugs.python.org/issue27392, but it's hard to justify the fix to
|
||||
get it accepted, so we won't bother for now. This currently uses a
|
||||
horrible monley patch to work with SSL.
|
||||
|
||||
To use this module, replace::
|
||||
|
||||
from .asyncio.server import Acceptor
|
||||
|
||||
with::
|
||||
|
||||
from .asyncio.mtacceptor import Acceptor
|
||||
|
||||
in ZEO.StorageServer.
|
||||
"""
|
||||
import asyncore
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from .compat import asyncio, new_event_loop
|
||||
from .server import ServerProtocol
|
||||
|
||||
# _has_dualstack: True if the dual-stack sockets are supported
|
||||
try:
|
||||
# Check whether IPv6 sockets can be created
|
||||
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
except (socket.error, AttributeError):
|
||||
_has_dualstack = False
|
||||
else:
|
||||
# Check whether enabling dualstack (disabling v6only) works
|
||||
try:
|
||||
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
|
||||
except (socket.error, AttributeError):
|
||||
_has_dualstack = False
|
||||
else:
|
||||
_has_dualstack = True
|
||||
s.close()
|
||||
del s
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Acceptor(asyncore.dispatcher):
|
||||
"""A server that accepts incoming RPC connections
|
||||
|
||||
And creates a separate thread for each.
|
||||
"""
|
||||
|
||||
def __init__(self, storage_server, addr, ssl, msgpack):
|
||||
self.storage_server = storage_server
|
||||
self.addr = addr
|
||||
self.__socket_map = {}
|
||||
asyncore.dispatcher.__init__(self, map=self.__socket_map)
|
||||
|
||||
self.ssl_context = ssl
|
||||
self.msgpack = msgpack
|
||||
self._open_socket()
|
||||
|
||||
def _open_socket(self):
|
||||
addr = self.addr
|
||||
|
||||
if type(addr) == tuple:
|
||||
if addr[0] == '' and _has_dualstack:
|
||||
# Wildcard listen on all interfaces, both IPv4 and
|
||||
# IPv6 if possible
|
||||
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(
|
||||
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
|
||||
elif ':' in addr[0]:
|
||||
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
if _has_dualstack:
|
||||
# On Linux, IPV6_V6ONLY is off by default.
|
||||
# If the user explicitly asked for IPv6, don't bind to IPv4
|
||||
self.socket.setsockopt(
|
||||
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, True)
|
||||
else:
|
||||
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
else:
|
||||
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
|
||||
self.set_reuse_addr()
|
||||
|
||||
for i in range(25):
|
||||
try:
|
||||
self.bind(addr)
|
||||
except Exception as exc:
|
||||
logger.info("bind on %s failed %s waiting", addr, i)
|
||||
if i == 24:
|
||||
raise
|
||||
else:
|
||||
time.sleep(5)
|
||||
except:
|
||||
logger.exception('binding')
|
||||
raise
|
||||
else:
|
||||
break
|
||||
|
||||
if isinstance(addr, tuple) and addr[1] == 0:
|
||||
self.addr = addr = self.socket.getsockname()[:2]
|
||||
|
||||
logger.info("listening on %s", str(addr))
|
||||
self.listen(5)
|
||||
|
||||
def writable(self):
|
||||
return 0
|
||||
|
||||
def readable(self):
|
||||
return 1
|
||||
|
||||
def handle_accept(self):
|
||||
try:
|
||||
sock, addr = self.accept()
|
||||
except socket.error as msg:
|
||||
logger.info("accepted failed: %s", msg)
|
||||
return
|
||||
|
||||
|
||||
# We could short-circuit the attempt below in some edge cases
|
||||
# and avoid a log message by checking for addr being None.
|
||||
# Unfortunately, our test for the code below,
|
||||
# quick_close_doesnt_kill_server, causes addr to be None and
|
||||
# we'd have to write a test for the non-None case, which is
|
||||
# *even* harder to provoke. :/ So we'll leave things as they
|
||||
# are for now.
|
||||
|
||||
# It might be better to check whether the socket has been
|
||||
# closed, but I don't see a way to do that. :(
|
||||
|
||||
# Drop flow-info from IPv6 addresses
|
||||
if addr: # Sometimes None on Mac. See above.
|
||||
addr = addr[:2]
|
||||
|
||||
try:
|
||||
logger.debug("new connection %s" % (addr,))
|
||||
|
||||
def run():
|
||||
loop = new_event_loop()
|
||||
zs = self.storage_server.create_client_handler()
|
||||
protocol = ServerProtocol(loop, self.addr, zs, self.msgpack)
|
||||
protocol.stop = loop.stop
|
||||
|
||||
if self.ssl_context is None:
|
||||
cr = loop.create_connection((lambda : protocol), sock=sock)
|
||||
else:
|
||||
if hasattr(loop, 'connect_accepted_socket'):
|
||||
cr = loop.connect_accepted_socket(
|
||||
(lambda : protocol), sock, ssl=self.ssl_context)
|
||||
else:
|
||||
#######################################################
|
||||
# XXX See http://bugs.python.org/issue27392 :(
|
||||
_make_ssl_transport = loop._make_ssl_transport
|
||||
def make_ssl_transport(*a, **kw):
|
||||
kw['server_side'] = True
|
||||
return _make_ssl_transport(*a, **kw)
|
||||
loop._make_ssl_transport = make_ssl_transport
|
||||
#
|
||||
#######################################################
|
||||
cr = loop.create_connection(
|
||||
(lambda : protocol), sock=sock,
|
||||
ssl=self.ssl_context,
|
||||
server_hostname=''
|
||||
)
|
||||
|
||||
asyncio.ensure_future(cr, loop=loop)
|
||||
loop.run_forever()
|
||||
loop.close()
|
||||
|
||||
thread = threading.Thread(target=run, name='zeo_client_hander')
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
except Exception:
|
||||
if sock.fileno() in self.__socket_map:
|
||||
del self.__socket_map[sock.fileno()]
|
||||
logger.exception("Error in handle_accept")
|
||||
else:
|
||||
logger.info("connect from %s", repr(addr))
|
||||
|
||||
def loop(self, timeout=30.0):
|
||||
try:
|
||||
asyncore.loop(map=self.__socket_map, timeout=timeout)
|
||||
except Exception:
|
||||
if not self.__closed:
|
||||
raise # Unexpected exc
|
||||
|
||||
logger.debug('acceptor %s loop stopped', self.addr)
|
||||
|
||||
__closed = False
|
||||
def close(self):
|
||||
if not self.__closed:
|
||||
self.__closed = True
|
||||
asyncore.dispatcher.close(self)
|
||||
logger.debug("Closed accepter, %s", len(self.__socket_map))
|
||||
@@ -0,0 +1,285 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
import ZODB.POSException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from ..shortrepr import short_repr
|
||||
|
||||
from . import base
|
||||
from .compat import asyncio, new_event_loop
|
||||
from .marshal import server_decoder, encoder, reduce_exception
|
||||
|
||||
class ServerProtocol(base.Protocol):
|
||||
"""asyncio low-level ZEO server interface
|
||||
"""
|
||||
|
||||
protocols = (b'5', )
|
||||
|
||||
name = 'server protocol'
|
||||
methods = set(('register', ))
|
||||
|
||||
unlogged_exception_types = (
|
||||
ZODB.POSException.POSKeyError,
|
||||
)
|
||||
|
||||
def __init__(self, loop, addr, zeo_storage, msgpack):
|
||||
"""Create a server's client interface
|
||||
"""
|
||||
super(ServerProtocol, self).__init__(loop, addr)
|
||||
self.zeo_storage = zeo_storage
|
||||
|
||||
self.announce_protocol = (
|
||||
(b'M' if msgpack else b'Z') + best_protocol_version
|
||||
)
|
||||
|
||||
closed = False
|
||||
def close(self):
|
||||
logger.debug("Closing server protocol")
|
||||
if not self.closed:
|
||||
self.closed = True
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
|
||||
connected = None # for tests
|
||||
def connection_made(self, transport):
|
||||
self.connected = True
|
||||
super(ServerProtocol, self).connection_made(transport)
|
||||
self._write(self.announce_protocol)
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.connected = False
|
||||
if exc:
|
||||
logger.error("Disconnected %s:%s", exc.__class__.__name__, exc)
|
||||
self.zeo_storage.notify_disconnected()
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
pass # Might be replaced when running a thread per client
|
||||
|
||||
def finish_connect(self, protocol_version):
|
||||
if protocol_version == b'ruok':
|
||||
self._write(json.dumps(self.zeo_storage.ruok()).encode("ascii"))
|
||||
self.close()
|
||||
else:
|
||||
version = protocol_version[1:]
|
||||
if version in self.protocols:
|
||||
logger.info("received handshake %r" %
|
||||
str(protocol_version.decode('ascii')))
|
||||
self.protocol_version = protocol_version
|
||||
self.encode = encoder(protocol_version, True)
|
||||
self.decode = server_decoder(protocol_version)
|
||||
self.zeo_storage.notify_connected(self)
|
||||
else:
|
||||
logger.error("bad handshake %s" % short_repr(protocol_version))
|
||||
self.close()
|
||||
|
||||
def call_soon_threadsafe(self, func, *args):
|
||||
try:
|
||||
self.loop.call_soon_threadsafe(func, *args)
|
||||
except RuntimeError:
|
||||
if self.connected:
|
||||
logger.exception("call_soon_threadsafe failed while connected")
|
||||
|
||||
def message_received(self, message):
|
||||
try:
|
||||
message_id, async_, name, args = self.decode(message)
|
||||
except Exception:
|
||||
logger.exception("Can't deserialize message")
|
||||
self.close()
|
||||
return
|
||||
|
||||
if message_id == -1:
|
||||
return # keep-alive
|
||||
|
||||
if name not in self.methods:
|
||||
logger.error('Invalid method, %r', name)
|
||||
self.close()
|
||||
|
||||
try:
|
||||
result = getattr(self.zeo_storage, name)(*args)
|
||||
except Exception as exc:
|
||||
if not isinstance(exc, self.unlogged_exception_types):
|
||||
logger.exception(
|
||||
"Bad %srequest, %r", 'async ' if async_ else '', name)
|
||||
if async_:
|
||||
return self.close() # No way to recover/cry for help
|
||||
else:
|
||||
return self.send_error(message_id, exc)
|
||||
|
||||
if not async_:
|
||||
self.send_reply(message_id, result)
|
||||
|
||||
def send_reply(self, message_id, result, send_error=False, flag=0):
|
||||
try:
|
||||
result = self.encode(message_id, flag, '.reply', result)
|
||||
except Exception:
|
||||
if isinstance(result, Delay):
|
||||
result.set_sender(message_id, self)
|
||||
return
|
||||
else:
|
||||
logger.exception("Unpicklable response %r", result)
|
||||
if not send_error:
|
||||
self.send_error(
|
||||
message_id,
|
||||
ValueError("Couldn't pickle response"),
|
||||
True)
|
||||
|
||||
self._write(result)
|
||||
|
||||
def send_reply_threadsafe(self, message_id, result):
|
||||
self.loop.call_soon_threadsafe(self.reply, message_id, result)
|
||||
|
||||
def send_error(self, message_id, exc, send_error=False):
|
||||
"""Abstracting here so we can make this cleaner in the future
|
||||
"""
|
||||
self.send_reply(message_id, reduce_exception(exc), send_error, 2)
|
||||
|
||||
def async_(self, method, *args):
|
||||
self.call_async(method, args)
|
||||
|
||||
def async_threadsafe(self, method, *args):
|
||||
self.call_soon_threadsafe(self.call_async, method, args)
|
||||
|
||||
best_protocol_version = os.environ.get(
|
||||
'ZEO_SERVER_PROTOCOL',
|
||||
ServerProtocol.protocols[-1].decode('utf-8')).encode('utf-8')
|
||||
assert best_protocol_version in ServerProtocol.protocols
|
||||
|
||||
def new_connection(loop, addr, socket, zeo_storage, msgpack):
|
||||
protocol = ServerProtocol(loop, addr, zeo_storage, msgpack)
|
||||
cr = loop.create_connection((lambda : protocol), sock=socket)
|
||||
asyncio.ensure_future(cr, loop=loop)
|
||||
|
||||
class Delay(object):
|
||||
"""Used to delay response to client for synchronous calls.
|
||||
|
||||
When a synchronous call is made and the original handler returns
|
||||
without handling the call, it returns a Delay object that prevents
|
||||
the mainloop from sending a response.
|
||||
"""
|
||||
|
||||
msgid = protocol = sent = None
|
||||
|
||||
def set_sender(self, msgid, protocol):
|
||||
self.msgid = msgid
|
||||
self.protocol = protocol
|
||||
|
||||
def reply(self, obj):
|
||||
self.sent = 'reply'
|
||||
if self.protocol:
|
||||
self.protocol.send_reply(self.msgid, obj)
|
||||
|
||||
def error(self, exc_info):
|
||||
self.sent = 'error'
|
||||
logger.error("Error raised in delayed method", exc_info=exc_info)
|
||||
if self.protocol:
|
||||
self.protocol.send_error(self.msgid, exc_info[1])
|
||||
|
||||
def __repr__(self):
|
||||
return "%s[%s, %r, %r, %r]" % (
|
||||
self.__class__.__name__, id(self),
|
||||
self.msgid, self.protocol, self.sent)
|
||||
|
||||
def __reduce__(self):
|
||||
raise TypeError("Can't pickle delays.")
|
||||
|
||||
class Result(Delay):
|
||||
|
||||
def __init__(self, *args):
|
||||
self.args = args
|
||||
|
||||
def set_sender(self, msgid, protocol):
|
||||
reply, callback = self.args
|
||||
protocol.send_reply(msgid, reply)
|
||||
callback()
|
||||
|
||||
class MTDelay(Delay):
|
||||
|
||||
def __init__(self):
|
||||
self.ready = threading.Event()
|
||||
|
||||
def set_sender(self, *args):
|
||||
Delay.set_sender(self, *args)
|
||||
self.ready.set()
|
||||
|
||||
def reply(self, obj):
|
||||
self.ready.wait()
|
||||
self.protocol.call_soon_threadsafe(
|
||||
self.protocol.send_reply, self.msgid, obj)
|
||||
|
||||
def error(self, exc_info):
|
||||
self.ready.wait()
|
||||
self.protocol.call_soon_threadsafe(Delay.error, self, exc_info)
|
||||
|
||||
|
||||
class Acceptor(object):
|
||||
|
||||
def __init__(self, storage_server, addr, ssl, msgpack):
|
||||
self.storage_server = storage_server
|
||||
self.addr = addr
|
||||
self.ssl_context = ssl
|
||||
self.msgpack = msgpack
|
||||
self.event_loop = loop = new_event_loop()
|
||||
|
||||
if isinstance(addr, tuple):
|
||||
cr = loop.create_server(self.factory, addr[0], addr[1],
|
||||
reuse_address=True, ssl=ssl)
|
||||
else:
|
||||
cr = loop.create_unix_server(self.factory, addr, ssl=ssl)
|
||||
|
||||
f = asyncio.ensure_future(cr, loop=loop)
|
||||
server = loop.run_until_complete(f)
|
||||
|
||||
self.server = server
|
||||
if isinstance(addr, tuple) and addr[1] == 0:
|
||||
addrs = [s.getsockname() for s in server.sockets]
|
||||
addrs = [a for a in addrs if len(a) == len(addr)]
|
||||
if addrs:
|
||||
self.addr = addrs[0]
|
||||
else:
|
||||
self.addr = server.sockets[0].getsockname()[:len(addr)]
|
||||
|
||||
logger.info("listening on %s", str(addr))
|
||||
|
||||
def factory(self):
|
||||
try:
|
||||
logger.debug("Accepted connection")
|
||||
zs = self.storage_server.create_client_handler()
|
||||
protocol = ServerProtocol(
|
||||
self.event_loop, self.addr, zs, self.msgpack)
|
||||
except Exception:
|
||||
logger.exception("Failure in protocol factory")
|
||||
|
||||
return protocol
|
||||
|
||||
def loop(self, timeout=None):
|
||||
self.event_loop.run_forever()
|
||||
self.event_loop.close()
|
||||
|
||||
closed = False
|
||||
def close(self):
|
||||
if not self.closed:
|
||||
self.closed = True
|
||||
self.event_loop.call_soon_threadsafe(self._close)
|
||||
|
||||
def _close(self):
|
||||
loop = self.event_loop
|
||||
|
||||
self.server.close()
|
||||
|
||||
f = asyncio.ensure_future(self.server.wait_closed(), loop=loop)
|
||||
@f.add_done_callback
|
||||
def server_closed(f):
|
||||
# stop the loop when the server closes:
|
||||
loop.call_soon(loop.stop)
|
||||
|
||||
def timeout():
|
||||
logger.warning("Timed out closing asyncio.Server")
|
||||
loop.call_soon(loop.stop)
|
||||
|
||||
# But if the server doesn't close in a second, stop the loop anyway.
|
||||
loop.call_later(1, timeout)
|
||||
@@ -0,0 +1,176 @@
|
||||
from .._compat import PY3
|
||||
|
||||
if PY3:
|
||||
import asyncio
|
||||
else:
|
||||
import trollius as asyncio
|
||||
|
||||
try:
|
||||
ConnectionRefusedError
|
||||
except NameError:
|
||||
class ConnectionRefusedError(OSError):
|
||||
pass
|
||||
|
||||
import pprint
|
||||
|
||||
class Loop(object):
|
||||
|
||||
protocol = transport = None
|
||||
|
||||
def __init__(self, addrs=(), debug=True):
|
||||
self.addrs = addrs
|
||||
self.get_debug = lambda : debug
|
||||
self.connecting = {}
|
||||
self.later = []
|
||||
self.exceptions = []
|
||||
|
||||
def call_soon(self, func, *args):
|
||||
func(*args)
|
||||
|
||||
def _connect(self, future, protocol_factory):
|
||||
self.protocol = protocol = protocol_factory()
|
||||
self.transport = transport = Transport(protocol)
|
||||
protocol.connection_made(transport)
|
||||
future.set_result((transport, protocol))
|
||||
|
||||
def connect_connecting(self, addr):
|
||||
future, protocol_factory = self.connecting.pop(addr)
|
||||
self._connect(future, protocol_factory)
|
||||
|
||||
def fail_connecting(self, addr):
|
||||
future, protocol_factory = self.connecting.pop(addr)
|
||||
if not future.cancelled():
|
||||
future.set_exception(ConnectionRefusedError())
|
||||
|
||||
def create_connection(
|
||||
self, protocol_factory, host=None, port=None, sock=None,
|
||||
ssl=None, server_hostname=None
|
||||
):
|
||||
future = asyncio.Future(loop=self)
|
||||
if sock is None:
|
||||
addr = host, port
|
||||
if addr in self.addrs:
|
||||
self._connect(future, protocol_factory)
|
||||
else:
|
||||
self.connecting[addr] = future, protocol_factory
|
||||
else:
|
||||
self._connect(future, protocol_factory)
|
||||
|
||||
return future
|
||||
|
||||
def create_unix_connection(self, protocol_factory, path):
|
||||
future = asyncio.Future(loop=self)
|
||||
if path in self.addrs:
|
||||
self._connect(future, protocol_factory)
|
||||
else:
|
||||
self.connecting[path] = future, protocol_factory
|
||||
|
||||
return future
|
||||
|
||||
def call_soon_threadsafe(self, func, *args):
|
||||
func(*args)
|
||||
return Handle()
|
||||
|
||||
def call_later(self, delay, func, *args):
|
||||
handle = Handle()
|
||||
self.later.append((delay, func, args, handle))
|
||||
return handle
|
||||
|
||||
def call_exception_handler(self, context):
|
||||
self.exceptions.append(context)
|
||||
|
||||
closed = False
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
stopped = False
|
||||
def stop(self):
|
||||
self.stopped = True
|
||||
|
||||
class Handle(object):
|
||||
|
||||
cancelled = False
|
||||
|
||||
def cancel(self):
|
||||
self.cancelled = True
|
||||
|
||||
class Transport(object):
|
||||
|
||||
capacity = 1 << 64
|
||||
paused = False
|
||||
extra = dict(peername='1.2.3.4', sockname=('127.0.0.1', 4200), socket=None)
|
||||
|
||||
def __init__(self, protocol):
|
||||
self.data = []
|
||||
self.protocol = protocol
|
||||
|
||||
def write(self, data):
|
||||
self.data.append(data)
|
||||
self.check_pause()
|
||||
|
||||
def writelines(self, lines):
|
||||
self.data.extend(lines)
|
||||
self.check_pause()
|
||||
|
||||
def check_pause(self):
|
||||
if len(self.data) > self.capacity and not self.paused:
|
||||
self.paused = True
|
||||
self.protocol.pause_writing()
|
||||
|
||||
def pop(self, count=None):
|
||||
if count:
|
||||
r = self.data[:count]
|
||||
del self.data[:count]
|
||||
else:
|
||||
r = self.data[:]
|
||||
del self.data[:]
|
||||
self.check_resume()
|
||||
return r
|
||||
|
||||
def check_resume(self):
|
||||
if len(self.data) < self.capacity and self.paused:
|
||||
self.paused = False
|
||||
self.protocol.resume_writing()
|
||||
|
||||
closed = False
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def get_extra_info(self, name):
|
||||
return self.extra[name]
|
||||
|
||||
class AsyncRPC(object):
|
||||
"""Adapt an asyncio API to an RPC to help hysterical tests
|
||||
"""
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def __getattr__(self, name):
|
||||
return lambda *a, **kw: self.api.call(name, *a, **kw)
|
||||
|
||||
class ClientRunner(object):
|
||||
|
||||
def __init__(self, addr, client, cache, storage, read_only, timeout,
|
||||
**kw):
|
||||
self.addr = addr
|
||||
self.client = client
|
||||
self.cache = cache
|
||||
self.storage = storage
|
||||
self.read_only = read_only
|
||||
self.timeout = timeout,
|
||||
for name in kw:
|
||||
self.__dict__[name] = kw[name]
|
||||
|
||||
def start(self, wait=True):
|
||||
pass
|
||||
|
||||
def call(self, method, *args, **kw):
|
||||
return getattr(self, method)(*args)
|
||||
|
||||
async_ = async_iter = call
|
||||
|
||||
def wait(self, timeout=None):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
@@ -0,0 +1,877 @@
|
||||
from .._compat import PY3
|
||||
|
||||
if PY3:
|
||||
import asyncio
|
||||
else:
|
||||
import trollius as asyncio
|
||||
|
||||
from zope.testing import setupstack
|
||||
from concurrent.futures import Future
|
||||
import mock
|
||||
from ZODB.POSException import ReadOnlyError
|
||||
from ZODB.utils import maxtid
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import struct
|
||||
import unittest
|
||||
|
||||
from ..Exceptions import ClientDisconnected, ProtocolError
|
||||
|
||||
from .testing import Loop
|
||||
from .client import ClientRunner, Fallback
|
||||
from .server import new_connection, best_protocol_version
|
||||
from .marshal import encoder, decoder
|
||||
|
||||
class Base(object):
|
||||
|
||||
enc = b'Z'
|
||||
seq_type = list
|
||||
|
||||
def setUp(self):
|
||||
super(Base, self).setUp()
|
||||
self.encode = encoder(self.enc)
|
||||
self.decode = decoder(self.enc)
|
||||
|
||||
def unsized(self, data, unpickle=False):
|
||||
result = []
|
||||
while data:
|
||||
size, message = data[:2]
|
||||
data = data[2:]
|
||||
self.assertEqual(struct.unpack(">I", size)[0], len(message))
|
||||
if unpickle:
|
||||
message = self.decode(message)
|
||||
result.append(message)
|
||||
|
||||
if len(result) == 1:
|
||||
result = result[0]
|
||||
return result
|
||||
|
||||
def parse(self, data):
|
||||
return self.unsized(data, True)
|
||||
|
||||
target = None
|
||||
def send(self, method, *args, **kw):
|
||||
target = kw.pop('target', self.target)
|
||||
called = kw.pop('called', True)
|
||||
no_output = kw.pop('no_output', True)
|
||||
self.assertFalse(kw)
|
||||
|
||||
self.loop.protocol.data_received(
|
||||
sized(self.encode(0, True, method, args)))
|
||||
if target is not None:
|
||||
target = getattr(target, method)
|
||||
if called:
|
||||
target.assert_called_with(*args)
|
||||
target.reset_mock()
|
||||
else:
|
||||
self.assertFalse(target.called)
|
||||
if no_output:
|
||||
self.assertFalse(self.loop.transport.pop())
|
||||
|
||||
def pop(self, count=None, parse=True):
|
||||
return self.unsized(self.loop.transport.pop(count), parse)
|
||||
|
||||
class ClientTests(Base, setupstack.TestCase, ClientRunner):
|
||||
|
||||
maxDiff = None
|
||||
|
||||
def tearDown(self):
|
||||
self.client.close()
|
||||
super(ClientTests, self)
|
||||
|
||||
def start(self,
|
||||
addrs=(('127.0.0.1', 8200), ), loop_addrs=None,
|
||||
read_only=False,
|
||||
finish_start=False,
|
||||
):
|
||||
# To create a client, we need to specify an address, a client
|
||||
# object and a cache.
|
||||
|
||||
wrapper = mock.Mock()
|
||||
self.target = wrapper
|
||||
cache = MemoryCache()
|
||||
self.set_options(addrs, wrapper, cache, 'TEST', read_only, timeout=1)
|
||||
|
||||
# We can also provide an event loop. We'll use a testing loop
|
||||
# so we don't have to actually make any network connection.
|
||||
loop = Loop(addrs if loop_addrs is None else loop_addrs)
|
||||
self.setup_delegation(loop)
|
||||
self.assertFalse(wrapper.notify_disconnected.called)
|
||||
protocol = loop.protocol
|
||||
transport = loop.transport
|
||||
|
||||
if finish_start:
|
||||
protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.pop(2, False), self.enc + b'3101')
|
||||
self.respond(1, None)
|
||||
self.respond(2, 'a'*8)
|
||||
self.pop(4)
|
||||
self.assertEqual(self.pop(), (3, False, 'get_info', ()))
|
||||
self.respond(3, dict(length=42))
|
||||
|
||||
return (wrapper, cache, self.loop, self.client, protocol, transport)
|
||||
|
||||
def respond(self, message_id, result, async_=False):
|
||||
self.loop.protocol.data_received(
|
||||
sized(self.encode(message_id, async_, '.reply', result)))
|
||||
|
||||
def wait_for_result(self, future, timeout):
|
||||
if future.done() and future.exception() is not None:
|
||||
raise future.exception()
|
||||
return future
|
||||
|
||||
def testClientBasics(self):
|
||||
|
||||
# Here, we'll go through the basic usage of the asyncio ZEO
|
||||
# network client. The client is responsible for the core
|
||||
# functionality of a ZEO client storage. The client storage
|
||||
# is largely just a wrapper around the asyncio client.
|
||||
|
||||
wrapper, cache, loop, client, protocol, transport = self.start()
|
||||
self.assertFalse(wrapper.notify_disconnected.called)
|
||||
|
||||
# The client isn't connected until the server sends it some data.
|
||||
self.assertFalse(client.connected.done() or transport.data)
|
||||
|
||||
# The server sends the client it's protocol. In this case,
|
||||
# it's a very high one. The client will send it's highest that
|
||||
# it can use.
|
||||
protocol.data_received(sized(self.enc + b'99999'))
|
||||
|
||||
# The client sends back a handshake, and registers the
|
||||
# storage, and requests the last transaction.
|
||||
self.assertEqual(self.pop(2, False), self.enc + b'5')
|
||||
self.assertEqual(self.pop(), (1, False, 'register', ('TEST', False)))
|
||||
|
||||
# The client isn't connected until it initializes it's cache:
|
||||
self.assertFalse(client.connected.done() or transport.data)
|
||||
|
||||
# If we try to make calls while the client is *initially*
|
||||
# connecting, we get an error. This is because some dufus
|
||||
# decided to create a client storage without waiting for it to
|
||||
# connect.
|
||||
self.assertRaises(ClientDisconnected, self.call, 'foo', 1, 2)
|
||||
|
||||
# When the client is reconnecting, it's ready flag is set to False and
|
||||
# it queues calls:
|
||||
client.ready = False
|
||||
f1 = self.call('foo', 1, 2)
|
||||
self.assertFalse(f1.done())
|
||||
|
||||
# If we try to make an async call, we get an immediate error:
|
||||
self.assertRaises(ClientDisconnected, self.async_, 'bar', 3, 4)
|
||||
|
||||
# The wrapper object (ClientStorage) hasn't been notified:
|
||||
self.assertFalse(wrapper.notify_connected.called)
|
||||
|
||||
# Let's respond to the register call:
|
||||
self.respond(1, None)
|
||||
|
||||
# The client requests the last transaction:
|
||||
self.assertEqual(self.pop(), (2, False, 'lastTransaction', ()))
|
||||
|
||||
# We respond
|
||||
self.respond(2, 'a'*8)
|
||||
|
||||
# After verification, the client requests info:
|
||||
self.assertEqual(self.pop(), (3, False, 'get_info', ()))
|
||||
self.respond(3, dict(length=42))
|
||||
|
||||
# Now we're connected, the cache was initialized, and the
|
||||
# queued message has been sent:
|
||||
self.assertTrue(client.connected.done())
|
||||
self.assertEqual(cache.getLastTid(), 'a'*8)
|
||||
self.assertEqual(self.pop(), (4, False, 'foo', (1, 2)))
|
||||
|
||||
# The wrapper object (ClientStorage) has been notified:
|
||||
wrapper.notify_connected.assert_called_with(client, {'length': 42})
|
||||
|
||||
self.respond(4, 42)
|
||||
self.assertEqual(f1.result(), 42)
|
||||
|
||||
# Now we can make async calls:
|
||||
f2 = self.async_('bar', 3, 4)
|
||||
self.assertTrue(f2.done() and f2.exception() is None)
|
||||
self.assertEqual(self.pop(), (0, True, 'bar', (3, 4)))
|
||||
|
||||
# Loading objects gets special handling to leverage the cache.
|
||||
loaded = self.load_before(b'1'*8, maxtid)
|
||||
|
||||
# The data wasn't in the cache, so we made a server call:
|
||||
self.assertEqual(self.pop(), ((b'1'*8, maxtid), False, 'loadBefore', (b'1'*8, maxtid)))
|
||||
# Note load_before uses the oid as the message id.
|
||||
self.respond((b'1'*8, maxtid), (b'data', b'a'*8, None))
|
||||
self.assertEqual(loaded.result(), (b'data', b'a'*8, None))
|
||||
|
||||
# If we make another request, it will be satisfied from the cache:
|
||||
loaded = self.load_before(b'1'*8, maxtid)
|
||||
self.assertEqual(loaded.result(), (b'data', b'a'*8, None))
|
||||
self.assertFalse(transport.data)
|
||||
|
||||
# Let's send an invalidation:
|
||||
self.send('invalidateTransaction', b'b'*8, self.seq_type([b'1'*8]))
|
||||
|
||||
# Now, if we try to load current again, we'll make a server request.
|
||||
loaded = self.load_before(b'1'*8, maxtid)
|
||||
|
||||
# Note that if we make another request for the same object,
|
||||
# the requests will be collapsed:
|
||||
loaded2 = self.load_before(b'1'*8, maxtid)
|
||||
|
||||
self.assertEqual(self.pop(), ((b'1'*8, maxtid), False, 'loadBefore', (b'1'*8, maxtid)))
|
||||
self.respond((b'1'*8, maxtid), (b'data2', b'b'*8, None))
|
||||
self.assertEqual(loaded.result(), (b'data2', b'b'*8, None))
|
||||
self.assertEqual(loaded2.result(), (b'data2', b'b'*8, None))
|
||||
|
||||
# Loading non-current data may also be satisfied from cache
|
||||
loaded = self.load_before(b'1'*8, b'b'*8)
|
||||
self.assertEqual(loaded.result(), (b'data', b'a'*8, b'b'*8))
|
||||
self.assertFalse(transport.data)
|
||||
loaded = self.load_before(b'1'*8, b'c'*8)
|
||||
self.assertEqual(loaded.result(), (b'data2', b'b'*8, None))
|
||||
self.assertFalse(transport.data)
|
||||
loaded = self.load_before(b'1'*8, b'_'*8)
|
||||
|
||||
self.assertEqual(self.pop(), ((b'1'*8, b'_'*8), False, 'loadBefore', (b'1'*8, b'_'*8)))
|
||||
self.respond((b'1'*8, b'_'*8), (b'data0', b'^'*8, b'_'*8))
|
||||
self.assertEqual(loaded.result(), (b'data0', b'^'*8, b'_'*8))
|
||||
|
||||
# When committing transactions, we need to update the cache
|
||||
# with committed data. To do this, we pass a (oid, data, resolved)
|
||||
# iteratable to tpc_finish_threadsafe.
|
||||
|
||||
tids = []
|
||||
def finished_cb(tid):
|
||||
tids.append(tid)
|
||||
|
||||
committed = self.tpc_finish(
|
||||
b'd'*8,
|
||||
[(b'2'*8, 'committed 2', False),
|
||||
(b'1'*8, 'committed 3', True),
|
||||
(b'4'*8, 'committed 4', False),
|
||||
],
|
||||
finished_cb)
|
||||
self.assertFalse(committed.done() or
|
||||
cache.load(b'2'*8) or
|
||||
cache.load(b'4'*8))
|
||||
self.assertEqual(cache.load(b'1'*8), (b'data2', b'b'*8))
|
||||
self.assertEqual(self.pop(),
|
||||
(5, False, 'tpc_finish', (b'd'*8,)))
|
||||
self.respond(5, b'e'*8)
|
||||
self.assertEqual(committed.result(), b'e'*8)
|
||||
self.assertEqual(cache.load(b'1'*8), None)
|
||||
self.assertEqual(cache.load(b'2'*8), ('committed 2', b'e'*8))
|
||||
self.assertEqual(cache.load(b'4'*8), ('committed 4', b'e'*8))
|
||||
self.assertEqual(tids.pop(), b'e'*8)
|
||||
|
||||
# If the protocol is disconnected, it will reconnect and will
|
||||
# resolve outstanding requests with exceptions:
|
||||
loaded = self.load_before(b'1'*8, maxtid)
|
||||
f1 = self.call('foo', 1, 2)
|
||||
self.assertFalse(loaded.done() or f1.done())
|
||||
self.assertEqual(
|
||||
self.pop(),
|
||||
[((b'11111111', b'\x7f\xff\xff\xff\xff\xff\xff\xff'),
|
||||
False, 'loadBefore', (b'1'*8, maxtid)),
|
||||
(6, False, 'foo', (1, 2))],
|
||||
)
|
||||
exc = TypeError(43)
|
||||
|
||||
self.assertFalse(wrapper.notify_disconnected.called)
|
||||
wrapper.notify_connected.reset_mock()
|
||||
protocol.connection_lost(exc)
|
||||
wrapper.notify_disconnected.assert_called_with()
|
||||
|
||||
self.assertTrue(isinstance(loaded.exception(), ClientDisconnected))
|
||||
self.assertEqual(loaded.exception().args, (exc,))
|
||||
self.assertTrue(isinstance(f1.exception(), ClientDisconnected))
|
||||
self.assertEqual(f1.exception().args, (exc,))
|
||||
|
||||
# Because we reconnected, a new protocol and transport were created:
|
||||
self.assertTrue(protocol is not loop.protocol)
|
||||
self.assertTrue(transport is not loop.transport)
|
||||
protocol = loop.protocol
|
||||
transport = loop.transport
|
||||
|
||||
# and we have a new incomplete connect future:
|
||||
self.assertFalse(client.connected.done() or transport.data)
|
||||
|
||||
# This time we'll send a lower protocol version. The client
|
||||
# will send it back, because it's lower than the client's
|
||||
# protocol:
|
||||
protocol.data_received(sized(self.enc + b'310'))
|
||||
self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'310')
|
||||
self.assertEqual(self.pop(), (1, False, 'register', ('TEST', False)))
|
||||
self.assertFalse(wrapper.notify_connected.called)
|
||||
|
||||
# If the register response is a tid, then the client won't
|
||||
# request lastTransaction
|
||||
self.respond(1, b'e'*8)
|
||||
self.assertEqual(self.pop(), (2, False, 'get_info', ()))
|
||||
self.respond(2, dict(length=42))
|
||||
|
||||
# Because the server tid matches the cache tid, we're done connecting
|
||||
wrapper.notify_connected.assert_called_with(client, {'length': 42})
|
||||
self.assertTrue(client.connected.done() and not transport.data)
|
||||
self.assertEqual(cache.getLastTid(), b'e'*8)
|
||||
|
||||
# Because we were able to update the cache, we didn't have to
|
||||
# invalidate the database cache:
|
||||
self.assertFalse(wrapper.invalidateTransaction.called)
|
||||
|
||||
# The close method closes the connection and cache:
|
||||
client.close()
|
||||
self.assertTrue(transport.closed and cache.closed)
|
||||
|
||||
# The client doesn't reconnect
|
||||
self.assertEqual(loop.protocol, protocol)
|
||||
self.assertEqual(loop.transport, transport)
|
||||
|
||||
def test_cache_behind(self):
|
||||
wrapper, cache, loop, client, protocol, transport = self.start()
|
||||
|
||||
cache.setLastTid(b'a'*8)
|
||||
cache.store(b'4'*8, b'a'*8, None, '4 data')
|
||||
cache.store(b'2'*8, b'a'*8, None, '2 data')
|
||||
|
||||
self.assertFalse(client.connected.done() or transport.data)
|
||||
protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'3101')
|
||||
self.respond(1, None)
|
||||
self.respond(2, b'e'*8)
|
||||
self.pop(4)
|
||||
|
||||
# We have to verify the cache, so we're not done connecting:
|
||||
self.assertFalse(client.connected.done())
|
||||
self.assertEqual(self.pop(), (3, False, 'getInvalidations', (b'a'*8, )))
|
||||
self.respond(3, (b'e'*8, [b'4'*8]))
|
||||
|
||||
self.assertEqual(self.pop(), (4, False, 'get_info', ()))
|
||||
self.respond(4, dict(length=42))
|
||||
|
||||
# Now that verification is done, we're done connecting
|
||||
self.assertTrue(client.connected.done() and not transport.data)
|
||||
self.assertEqual(cache.getLastTid(), b'e'*8)
|
||||
|
||||
# And the cache has been updated:
|
||||
self.assertEqual(cache.load(b'2'*8),
|
||||
('2 data', b'a'*8)) # unchanged
|
||||
self.assertEqual(cache.load(b'4'*8), None)
|
||||
|
||||
# Because we were able to update the cache, we didn't have to
|
||||
# invalidate the database cache:
|
||||
self.assertFalse(wrapper.invalidateCache.called)
|
||||
|
||||
def test_cache_way_behind(self):
|
||||
wrapper, cache, loop, client, protocol, transport = self.start()
|
||||
|
||||
cache.setLastTid(b'a'*8)
|
||||
cache.store(b'4'*8, b'a'*8, None, '4 data')
|
||||
self.assertTrue(cache)
|
||||
|
||||
self.assertFalse(client.connected.done() or transport.data)
|
||||
protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'3101')
|
||||
self.respond(1, None)
|
||||
self.respond(2, b'e'*8)
|
||||
self.pop(4)
|
||||
|
||||
# We have to verify the cache, so we're not done connecting:
|
||||
self.assertFalse(client.connected.done())
|
||||
self.assertEqual(self.pop(), (3, False, 'getInvalidations', (b'a'*8, )))
|
||||
|
||||
# We respond None, indicating that we're too far out of date:
|
||||
self.respond(3, None)
|
||||
|
||||
self.assertEqual(self.pop(), (4, False, 'get_info', ()))
|
||||
self.respond(4, dict(length=42))
|
||||
|
||||
# Now that verification is done, we're done connecting
|
||||
self.assertTrue(client.connected.done() and not transport.data)
|
||||
self.assertEqual(cache.getLastTid(), b'e'*8)
|
||||
|
||||
# But the cache is now empty and we invalidated the database cache
|
||||
self.assertFalse(cache)
|
||||
wrapper.invalidateCache.assert_called_with()
|
||||
|
||||
def test_multiple_addresses(self):
|
||||
# We can pass multiple addresses to client constructor
|
||||
addrs = [('1.2.3.4', 8200), ('2.2.3.4', 8200)]
|
||||
wrapper, cache, loop, client, protocol, transport = self.start(
|
||||
addrs, ())
|
||||
|
||||
# We haven't connected yet
|
||||
self.assertTrue(protocol is None and transport is None)
|
||||
|
||||
# There are 2 connection attempts outstanding:
|
||||
self.assertEqual(sorted(loop.connecting), addrs)
|
||||
|
||||
# We cause the first one to fail:
|
||||
loop.fail_connecting(addrs[0])
|
||||
self.assertEqual(sorted(loop.connecting), addrs[1:])
|
||||
|
||||
# The failed connection is attempted in the future:
|
||||
delay, func, args, _ = loop.later.pop(0)
|
||||
self.assertTrue(1 <= delay <= 2)
|
||||
func(*args)
|
||||
self.assertEqual(sorted(loop.connecting), addrs)
|
||||
|
||||
# Let's connect the second address
|
||||
loop.connect_connecting(addrs[1])
|
||||
self.assertEqual(sorted(loop.connecting), addrs[:1])
|
||||
protocol = loop.protocol
|
||||
transport = loop.transport
|
||||
protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'3101')
|
||||
self.respond(1, None)
|
||||
|
||||
# Now, when the first connection fails, it won't be retried,
|
||||
# because we're already connected.
|
||||
# (first in later is heartbeat)
|
||||
self.assertEqual(sorted(loop.later[1:]), [])
|
||||
loop.fail_connecting(addrs[0])
|
||||
self.assertEqual(sorted(loop.connecting), [])
|
||||
self.assertEqual(sorted(loop.later[1:]), [])
|
||||
|
||||
def test_bad_server_tid(self):
|
||||
# If in verification we get a server_tid behing the cache's, make sure
|
||||
# we retry the connection later.
|
||||
wrapper, cache, loop, client, protocol, transport = self.start()
|
||||
cache.store(b'4'*8, b'a'*8, None, '4 data')
|
||||
cache.setLastTid('b'*8)
|
||||
protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'3101')
|
||||
self.respond(1, None)
|
||||
self.respond(2, 'a'*8)
|
||||
self.pop()
|
||||
self.assertFalse(client.connected.done() or transport.data)
|
||||
delay, func, args, _ = loop.later.pop(1) # first in later is heartbeat
|
||||
self.assertTrue(8 < delay < 10)
|
||||
self.assertEqual(len(loop.later), 1) # first in later is heartbeat
|
||||
func(*args) # connect again
|
||||
self.assertFalse(protocol is loop.protocol)
|
||||
self.assertFalse(transport is loop.transport)
|
||||
protocol = loop.protocol
|
||||
transport = loop.transport
|
||||
protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'3101')
|
||||
self.respond(1, None)
|
||||
self.respond(2, 'b'*8)
|
||||
self.pop(4)
|
||||
self.assertEqual(self.pop(), (3, False, 'get_info', ()))
|
||||
self.respond(3, dict(length=42))
|
||||
self.assertTrue(client.connected.done() and not transport.data)
|
||||
self.assertTrue(client.ready)
|
||||
|
||||
def test_readonly_fallback(self):
|
||||
addrs = [('1.2.3.4', 8200), ('2.2.3.4', 8200)]
|
||||
wrapper, cache, loop, client, protocol, transport = self.start(
|
||||
addrs, (), read_only=Fallback)
|
||||
|
||||
self.assertTrue(self.is_read_only())
|
||||
|
||||
# We'll treat the first address as read-only and we'll let it connect:
|
||||
loop.connect_connecting(addrs[0])
|
||||
protocol, transport = loop.protocol, loop.transport
|
||||
protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'3101')
|
||||
# We see that the client tried a writable connection:
|
||||
self.assertEqual(self.pop(),
|
||||
(1, False, 'register', ('TEST', False)))
|
||||
# We respond with a read-only exception:
|
||||
self.respond(1, ('ZODB.POSException.ReadOnlyError', ()), True)
|
||||
self.assertTrue(self.is_read_only())
|
||||
|
||||
# The client tries for a read-only connection:
|
||||
self.assertEqual(self.pop(), (2, False, 'register', ('TEST', True)))
|
||||
# We respond with successfully:
|
||||
self.respond(2, None)
|
||||
self.pop(2)
|
||||
self.respond(3, 'b'*8)
|
||||
self.assertTrue(self.is_read_only())
|
||||
|
||||
# At this point, the client is ready and using the protocol,
|
||||
# and the protocol is read-only:
|
||||
self.assertTrue(client.ready)
|
||||
self.assertEqual(client.protocol, protocol)
|
||||
self.assertEqual(protocol.read_only, True)
|
||||
connected = client.connected
|
||||
|
||||
# The client asks for info, and we respond:
|
||||
self.assertEqual(self.pop(), (4, False, 'get_info', ()))
|
||||
self.respond(4, dict(length=42))
|
||||
|
||||
self.assertTrue(connected.done())
|
||||
|
||||
# We connect the second address:
|
||||
loop.connect_connecting(addrs[1])
|
||||
loop.protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.unsized(loop.transport.pop(2)), self.enc + b'3101')
|
||||
self.assertEqual(self.parse(loop.transport.pop()),
|
||||
(1, False, 'register', ('TEST', False)))
|
||||
self.assertTrue(self.is_read_only())
|
||||
|
||||
# We respond and the writable connection succeeds:
|
||||
self.respond(1, None)
|
||||
|
||||
# at this point, a lastTransaction request is emitted:
|
||||
|
||||
self.assertEqual(self.parse(loop.transport.pop()),
|
||||
(2, False, 'lastTransaction', ()))
|
||||
self.assertFalse(self.is_read_only())
|
||||
|
||||
# Now, the original protocol is closed, and the client is
|
||||
# no-longer ready:
|
||||
self.assertFalse(client.ready)
|
||||
self.assertFalse(client.protocol is protocol)
|
||||
self.assertEqual(client.protocol, loop.protocol)
|
||||
self.assertEqual(protocol.closed, True)
|
||||
self.assertTrue(client.connected is not connected)
|
||||
self.assertFalse(client.connected.done())
|
||||
protocol, transport = loop.protocol, loop.transport
|
||||
self.assertEqual(protocol.read_only, False)
|
||||
|
||||
# Now, we finish verification
|
||||
self.respond(2, 'b'*8)
|
||||
self.respond(3, dict(length=42))
|
||||
self.assertTrue(client.ready)
|
||||
self.assertTrue(client.connected.done())
|
||||
|
||||
def test_invalidations_while_verifying(self):
|
||||
# While we're verifying, invalidations are ignored
|
||||
wrapper, cache, loop, client, protocol, transport = self.start()
|
||||
protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'3101')
|
||||
self.respond(1, None)
|
||||
self.pop(4)
|
||||
self.send('invalidateTransaction', b'b'*8, [b'1'*8], called=False)
|
||||
self.respond(2, b'a'*8)
|
||||
self.send('invalidateTransaction', b'c'*8, self.seq_type([b'1'*8]),
|
||||
no_output=False)
|
||||
self.assertEqual(self.pop(), (3, False, 'get_info', ()))
|
||||
|
||||
# We'll disconnect:
|
||||
protocol.connection_lost(Exception("lost"))
|
||||
self.assertTrue(protocol is not loop.protocol)
|
||||
self.assertTrue(transport is not loop.transport)
|
||||
protocol = loop.protocol
|
||||
transport = loop.transport
|
||||
|
||||
# Similarly, invalidations aren't processed while reconnecting:
|
||||
|
||||
protocol.data_received(sized(self.enc + b'3101'))
|
||||
self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'3101')
|
||||
self.respond(1, None)
|
||||
self.pop(4)
|
||||
self.send('invalidateTransaction', b'd'*8, [b'1'*8], called=False)
|
||||
self.respond(2, b'c'*8)
|
||||
self.send('invalidateTransaction', b'e'*8, self.seq_type([b'1'*8]),
|
||||
no_output=False)
|
||||
self.assertEqual(self.pop(), (3, False, 'get_info', ()))
|
||||
|
||||
def test_flow_control(self):
|
||||
# When sending a lot of data (blobs), we don't want to fill up
|
||||
# memory behind a slow socket. Asycio's flow control helper
|
||||
# seems a bit complicated. We'd rather pass an iterator that's
|
||||
# consumed as we can.
|
||||
|
||||
wrapper, cache, loop, client, protocol, transport = self.start(
|
||||
finish_start=True)
|
||||
|
||||
# Give the transport a small capacity:
|
||||
transport.capacity = 2
|
||||
self.async_('foo')
|
||||
self.async_('bar')
|
||||
self.async_('baz')
|
||||
self.async_('splat')
|
||||
|
||||
# The first 2 were sent, but the remaining were queued.
|
||||
self.assertEqual(self.pop(),
|
||||
[(0, True, 'foo', ()), (0, True, 'bar', ())])
|
||||
|
||||
# But popping them allowed sending to resume:
|
||||
self.assertEqual(self.pop(),
|
||||
[(0, True, 'baz', ()), (0, True, 'splat', ())])
|
||||
|
||||
# This is especially handy with iterators:
|
||||
self.async_iter((name, ()) for name in 'abcde')
|
||||
self.assertEqual(self.pop(), [(0, True, 'a', ()), (0, True, 'b', ())])
|
||||
self.assertEqual(self.pop(), [(0, True, 'c', ()), (0, True, 'd', ())])
|
||||
self.assertEqual(self.pop(), (0, True, 'e', ()))
|
||||
self.assertEqual(self.pop(), [])
|
||||
|
||||
def test_bad_protocol(self):
|
||||
wrapper, cache, loop, client, protocol, transport = self.start()
|
||||
with mock.patch("ZEO.asyncio.client.logger.error") as error:
|
||||
self.assertFalse(error.called)
|
||||
protocol.data_received(sized(self.enc + b'200'))
|
||||
self.assertTrue(isinstance(error.call_args[0][1], ProtocolError))
|
||||
|
||||
|
||||
def test_get_peername(self):
|
||||
wrapper, cache, loop, client, protocol, transport = self.start(
|
||||
finish_start=True)
|
||||
self.assertEqual(client.get_peername(), '1.2.3.4')
|
||||
|
||||
def test_call_async_from_same_thread(self):
|
||||
# There are a few (1?) cases where we call into client storage
|
||||
# where it needs to call back asyncronously. Because we're
|
||||
# calling from the same thread, we don't need to use a futurte.
|
||||
wrapper, cache, loop, client, protocol, transport = self.start(
|
||||
finish_start=True)
|
||||
|
||||
client.call_async_from_same_thread('foo', 1)
|
||||
self.assertEqual(self.pop(), (0, True, 'foo', (1, )))
|
||||
|
||||
def test_ClientDisconnected_on_call_timeout(self):
|
||||
wrapper, cache, loop, client, protocol, transport = self.start()
|
||||
self.wait_for_result = super(ClientTests, self).wait_for_result
|
||||
self.assertRaises(ClientDisconnected, self.call, 'foo')
|
||||
client.ready = False
|
||||
self.assertRaises(ClientDisconnected, self.call, 'foo')
|
||||
|
||||
def test_errors_in_data_received(self):
|
||||
# There was a bug in ZEO.async.client.Protocol.data_recieved
|
||||
# that caused it to fail badly if errors were raised while
|
||||
# handling data.
|
||||
|
||||
wrapper, cache, loop, client, protocol, transport =self.start(
|
||||
finish_start=True)
|
||||
|
||||
wrapper.receiveBlobStart.side_effect = ValueError('test')
|
||||
|
||||
chunk = 'x' * 99999
|
||||
try:
|
||||
loop.protocol.data_received(
|
||||
sized(
|
||||
self.encode(0, True, 'receiveBlobStart', ('oid', 'serial'))
|
||||
) +
|
||||
sized(
|
||||
self.encode(
|
||||
0, True, 'receiveBlobChunk', ('oid', 'serial', chunk))
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
loop.protocol.data_received(sized(
|
||||
self.encode(0, True, 'receiveBlobStop', ('oid', 'serial'))
|
||||
))
|
||||
wrapper.receiveBlobChunk.assert_called_with('oid', 'serial', chunk)
|
||||
wrapper.receiveBlobStop.assert_called_with('oid', 'serial')
|
||||
|
||||
def test_heartbeat(self):
|
||||
# Protocols run heartbeats on a configurable (sort of)
|
||||
# heartbeat interval, which defaults to every 60 seconds.
|
||||
wrapper, cache, loop, client, protocol, transport = self.start(
|
||||
finish_start=True)
|
||||
|
||||
delay, func, args, handle = loop.later.pop()
|
||||
self.assertEqual(
|
||||
(delay, func, args, handle),
|
||||
(60, protocol.heartbeat, (), protocol.heartbeat_handle),
|
||||
)
|
||||
self.assertFalse(loop.later or handle.cancelled)
|
||||
|
||||
# The heartbeat function sends heartbeat data and reschedules itself.
|
||||
func()
|
||||
self.assertEqual(self.pop(), (-1, 0, '.reply', None))
|
||||
self.assertTrue(protocol.heartbeat_handle != handle)
|
||||
|
||||
delay, func, args, handle = loop.later.pop()
|
||||
self.assertEqual(
|
||||
(delay, func, args, handle),
|
||||
(60, protocol.heartbeat, (), protocol.heartbeat_handle),
|
||||
)
|
||||
self.assertFalse(loop.later or handle.cancelled)
|
||||
|
||||
# The heartbeat is cancelled when the protocol connection is lost:
|
||||
protocol.connection_lost(None)
|
||||
self.assertTrue(handle.cancelled)
|
||||
|
||||
class MsgpackClientTests(ClientTests):
|
||||
enc = b'M'
|
||||
seq_type = tuple
|
||||
|
||||
class MemoryCache(object):
|
||||
|
||||
def __init__(self):
|
||||
# { oid -> [(start, end, data)] }
|
||||
self.data = collections.defaultdict(list)
|
||||
self.last_tid = None
|
||||
|
||||
clear = __init__
|
||||
|
||||
closed = False
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def load(self, oid):
|
||||
revisions = self.data[oid]
|
||||
if revisions:
|
||||
start, end, data = revisions[-1]
|
||||
if not end:
|
||||
return data, start
|
||||
return None
|
||||
|
||||
def store(self, oid, start_tid, end_tid, data):
|
||||
assert start_tid is not None
|
||||
revisions = self.data[oid]
|
||||
data = (start_tid, end_tid, data)
|
||||
if not revisions or data != revisions[-1]:
|
||||
revisions.append(data)
|
||||
revisions.sort()
|
||||
|
||||
def loadBefore(self, oid, tid):
|
||||
for start, end, data in self.data[oid]:
|
||||
if start < tid and (end is None or end >= tid):
|
||||
return data, start, end
|
||||
|
||||
def invalidate(self, oid, tid):
|
||||
revisions = self.data[oid]
|
||||
if revisions:
|
||||
if tid is None:
|
||||
del revisions[:]
|
||||
else:
|
||||
start, end, data = revisions[-1]
|
||||
if end is None:
|
||||
revisions[-1] = start, tid, data
|
||||
|
||||
def getLastTid(self):
|
||||
return self.last_tid
|
||||
|
||||
def setLastTid(self, tid):
|
||||
self.last_tid = tid
|
||||
|
||||
|
||||
class ServerTests(Base, setupstack.TestCase):
|
||||
|
||||
# The server side of things is pretty simple compared to the
|
||||
# client, because it's the clien't job to make and keep
|
||||
# connections. Servers are pretty passive.
|
||||
|
||||
def connect(self, finish=False):
|
||||
protocol = server_protocol(self.enc == b'M')
|
||||
self.loop = protocol.loop
|
||||
self.target = protocol.zeo_storage
|
||||
if finish:
|
||||
self.assertEqual(self.pop(parse=False),
|
||||
self.enc + best_protocol_version)
|
||||
protocol.data_received(sized(self.enc + b'5'))
|
||||
return protocol
|
||||
|
||||
message_id = 0
|
||||
target = None
|
||||
def call(self, meth, *args, **kw):
|
||||
if kw:
|
||||
expect = kw.pop('expect', self)
|
||||
target = kw.pop('target', self.target)
|
||||
self.assertFalse(kw)
|
||||
|
||||
if target is not None:
|
||||
target = getattr(target, meth)
|
||||
if expect is not self:
|
||||
target.return_value = expect
|
||||
|
||||
self.message_id += 1
|
||||
self.loop.protocol.data_received(
|
||||
sized(self.encode(self.message_id, False, meth, args)))
|
||||
|
||||
if target is not None:
|
||||
target.assert_called_once_with(*args)
|
||||
target.reset_mock()
|
||||
|
||||
if expect is not self:
|
||||
self.assertEqual(self.pop(),
|
||||
(self.message_id, False, '.reply', expect))
|
||||
|
||||
def testServerBasics(self):
|
||||
# A simple listening thread accepts connections. It creats
|
||||
# asyncio connections by calling ZEO.asyncio.new_connection:
|
||||
protocol = self.connect()
|
||||
self.assertFalse(protocol.zeo_storage.notify_connected.called)
|
||||
|
||||
# The server sends it's protocol.
|
||||
self.assertEqual(self.pop(parse=False),
|
||||
self.enc + best_protocol_version)
|
||||
|
||||
# The client sends it's protocol:
|
||||
protocol.data_received(sized(self.enc + b'5'))
|
||||
|
||||
self.assertEqual(protocol.protocol_version, self.enc + b'5')
|
||||
|
||||
protocol.zeo_storage.notify_connected.assert_called_once_with(protocol)
|
||||
|
||||
# The client registers:
|
||||
self.call('register', False, expect=None)
|
||||
|
||||
# It does other things, like, send hearbeats:
|
||||
protocol.data_received(sized(b'(J\xff\xff\xff\xffK\x00U\x06.replyNt.'))
|
||||
|
||||
# The client can make async calls:
|
||||
self.send('register')
|
||||
|
||||
# Let's close the connection
|
||||
self.assertFalse(protocol.zeo_storage.notify_disconnected.called)
|
||||
protocol.connection_lost(None)
|
||||
protocol.zeo_storage.notify_disconnected.assert_called_once_with()
|
||||
|
||||
def test_invalid_methods(self):
|
||||
protocol = self.connect(True)
|
||||
protocol.zeo_storage.notify_connected.assert_called_once_with(protocol)
|
||||
|
||||
# If we try to call a methid that isn't in the protocol's
|
||||
# white list, it will disconnect:
|
||||
self.assertFalse(protocol.loop.transport.closed)
|
||||
self.call('foo', target=None)
|
||||
self.assertTrue(protocol.loop.transport.closed)
|
||||
|
||||
class MsgpackServerTests(ServerTests):
|
||||
enc = b'M'
|
||||
seq_type = tuple
|
||||
|
||||
def server_protocol(msgpack,
|
||||
zeo_storage=None,
|
||||
protocol_version=None,
|
||||
addr=('1.2.3.4', '42'),
|
||||
):
|
||||
if zeo_storage is None:
|
||||
zeo_storage = mock.Mock()
|
||||
loop = Loop()
|
||||
sock = () # anything not None
|
||||
new_connection(loop, addr, sock, zeo_storage, msgpack)
|
||||
if protocol_version:
|
||||
loop.protocol.data_received(sized(protocol_version))
|
||||
return loop.protocol
|
||||
|
||||
def response(*data):
|
||||
return sized(self.encode(*data))
|
||||
|
||||
def sized(message):
|
||||
return struct.pack(">I", len(message)) + message
|
||||
|
||||
class Logging(object):
|
||||
|
||||
def __init__(self, level=logging.ERROR):
|
||||
self.level = level
|
||||
|
||||
def __enter__(self):
|
||||
self.handler = logging.StreamHandler()
|
||||
logging.getLogger().addHandler(self.handler)
|
||||
logging.getLogger().setLevel(self.level)
|
||||
|
||||
def __exit__(self, *args):
|
||||
logging.getLogger().removeHandler(self.handler)
|
||||
logging.getLogger().setLevel(logging.NOTSET)
|
||||
|
||||
|
||||
def test_suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(ClientTests))
|
||||
suite.addTest(unittest.makeSuite(ServerTests))
|
||||
suite.addTest(unittest.makeSuite(MsgpackClientTests))
|
||||
suite.addTest(unittest.makeSuite(MsgpackServerTests))
|
||||
return suite
|
||||
@@ -0,0 +1,835 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
##############################################################################
|
||||
"""Disk-based client cache for ZEO.
|
||||
|
||||
ClientCache exposes an API used by the ZEO client storage. FileCache stores
|
||||
objects on disk using a 2-tuple of oid and tid as key.
|
||||
|
||||
ClientCache's API is similar to a storage API, with methods like load(),
|
||||
store(), and invalidate(). It manages in-memory data structures that allow
|
||||
it to map this richer API onto the simple key-based API of the lower-level
|
||||
FileCache.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from struct import pack, unpack
|
||||
|
||||
import BTrees.LLBTree
|
||||
import BTrees.LOBTree
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import ZODB.fsIndex
|
||||
import zc.lockfile
|
||||
from ZODB.utils import p64, u64, z64, RLock
|
||||
import six
|
||||
from ._compat import PYPY
|
||||
|
||||
logger = logging.getLogger("ZEO.cache")
|
||||
|
||||
# A disk-based cache for ZEO clients.
|
||||
#
|
||||
# This class provides an interface to a persistent, disk-based cache
|
||||
# used by ZEO clients to store copies of database records from the
|
||||
# server.
|
||||
#
|
||||
# The details of the constructor as unspecified at this point.
|
||||
#
|
||||
# Each entry in the cache is valid for a particular range of transaction
|
||||
# ids. The lower bound is the transaction that wrote the data. The
|
||||
# upper bound is the next transaction that wrote a revision of the
|
||||
# object. If the data is current, the upper bound is stored as None;
|
||||
# the data is considered current until an invalidate() call is made.
|
||||
#
|
||||
# It is an error to call store() twice with the same object without an
|
||||
# intervening invalidate() to set the upper bound on the first cache
|
||||
# entry. Perhaps it will be necessary to have a call the removes
|
||||
# something from the cache outright, without keeping a non-current
|
||||
# entry.
|
||||
|
||||
# Cache verification
|
||||
#
|
||||
# When the client is connected to the server, it receives
|
||||
# invalidations every time an object is modified. When the client is
|
||||
# disconnected then reconnects, it must perform cache verification to make
|
||||
# sure its cached data is synchronized with the storage's current state.
|
||||
#
|
||||
# quick verification
|
||||
# full verification
|
||||
#
|
||||
|
||||
|
||||
# FileCache stores a cache in a single on-disk file.
|
||||
#
|
||||
# On-disk cache structure.
|
||||
#
|
||||
# The file begins with a 12-byte header. The first four bytes are the
|
||||
# file's magic number - ZEC3 - indicating zeo cache version 4. The
|
||||
# next eight bytes are the last transaction id.
|
||||
|
||||
magic = b"ZEC3"
|
||||
ZEC_HEADER_SIZE = 12
|
||||
|
||||
# Maximum block size. Note that while we are doing a store, we may
|
||||
# need to write a free block that is almost twice as big. If we die
|
||||
# in the middle of a store, then we need to split the large free records
|
||||
# while opening.
|
||||
max_block_size = (1<<31) - 1
|
||||
|
||||
|
||||
# After the header, the file contains a contiguous sequence of blocks. All
|
||||
# blocks begin with a one-byte status indicator:
|
||||
#
|
||||
# b'a'
|
||||
# Allocated. The block holds an object; the next 4 bytes are >I
|
||||
# format total block size.
|
||||
#
|
||||
# b'f'
|
||||
# Free. The block is free; the next 4 bytes are >I format total
|
||||
# block size.
|
||||
#
|
||||
# b'1', b'2', b'3', b'4'
|
||||
# The block is free, and consists of 1, 2, 3 or 4 bytes total.
|
||||
#
|
||||
# "Total" includes the status byte, and size bytes. There are no
|
||||
# empty (size 0) blocks.
|
||||
|
||||
|
||||
# Allocated blocks have more structure:
|
||||
#
|
||||
# 1 byte allocation status (b'a').
|
||||
# 4 bytes block size, >I format.
|
||||
# 8 byte oid
|
||||
# 8 byte start_tid
|
||||
# 8 byte end_tid
|
||||
# 2 byte version length must be 0
|
||||
# 4 byte data size
|
||||
# data
|
||||
# 8 byte redundant oid for error detection.
|
||||
allocated_record_overhead = 43
|
||||
|
||||
# The cache's currentofs goes around the file, circularly, forever.
|
||||
# It's always the starting offset of some block.
|
||||
#
|
||||
# When a new object is added to the cache, it's stored beginning at
|
||||
# currentofs, and currentofs moves just beyond it. As many contiguous
|
||||
# blocks needed to make enough room for the new object are evicted,
|
||||
# starting at currentofs. Exception: if currentofs is close enough
|
||||
# to the end of the file that the new object can't fit in one
|
||||
# contiguous chunk, currentofs is reset to ZEC_HEADER_SIZE first.
|
||||
|
||||
# Under PyPy, the available dict specializations perform significantly
|
||||
# better (faster) than the pure-Python BTree implementation. They may
|
||||
# use less memory too. And we don't require any of the special BTree features...
|
||||
_current_index_type = ZODB.fsIndex.fsIndex if not PYPY else dict
|
||||
_noncurrent_index_type = BTrees.LOBTree.LOBTree if not PYPY else dict
|
||||
# ...except at this leaf level
|
||||
_noncurrent_bucket_type = BTrees.LLBTree.LLBucket
|
||||
|
||||
class ClientCache(object):
|
||||
"""A simple in-memory cache."""
|
||||
|
||||
# The default size of 200MB makes a lot more sense than the traditional
|
||||
# default of 20MB. The default here is misleading, though, since
|
||||
# ClientStorage is the only user of ClientCache, and it always passes an
|
||||
# explicit size of its own choosing.
|
||||
def __init__(self, path=None, size=200*1024**2, rearrange=.8):
|
||||
|
||||
# - `path`: filepath for the cache file, or None (in which case
|
||||
# a temp file will be created)
|
||||
self.path = path
|
||||
|
||||
# - `maxsize`: total size of the cache file
|
||||
# We set to the minimum size of less than the minimum.
|
||||
size = max(size, ZEC_HEADER_SIZE)
|
||||
self.maxsize = size
|
||||
|
||||
# rearrange: if we read a current record and it's more than
|
||||
# rearrange*size from the end, then copy it forward to keep it
|
||||
# from being evicted.
|
||||
self.rearrange = rearrange * size
|
||||
|
||||
# The number of records in the cache.
|
||||
self._len = 0
|
||||
|
||||
# {oid -> pos}
|
||||
self.current = _current_index_type()
|
||||
|
||||
# {oid -> {tid->pos}}
|
||||
# Note that caches in the wild seem to have very little non-current
|
||||
# data, so this would seem to have little impact on memory consumption.
|
||||
# I wonder if we even need to store non-current data in the cache.
|
||||
self.noncurrent = _noncurrent_index_type()
|
||||
|
||||
# tid for the most recent transaction we know about. This is also
|
||||
# stored near the start of the file.
|
||||
self.tid = z64
|
||||
|
||||
# Always the offset into the file of the start of a block.
|
||||
# New and relocated objects are always written starting at
|
||||
# currentofs.
|
||||
self.currentofs = ZEC_HEADER_SIZE
|
||||
|
||||
self._lock = RLock()
|
||||
|
||||
# self.f is the open file object.
|
||||
# When we're not reusing an existing file, self.f is left None
|
||||
# here -- the scan() method must be called then to open the file
|
||||
# (and it sets self.f).
|
||||
|
||||
fsize = ZEC_HEADER_SIZE
|
||||
if path:
|
||||
self._lock_file = zc.lockfile.LockFile(path + '.lock')
|
||||
if not os.path.exists(path):
|
||||
# Create a small empty file. We'll make it bigger in _initfile.
|
||||
self.f = open(path, 'wb+')
|
||||
self.f.write(magic+z64)
|
||||
logger.info("created persistent cache file %r", path)
|
||||
else:
|
||||
fsize = os.path.getsize(self.path)
|
||||
self.f = open(path, 'rb+')
|
||||
logger.info("reusing persistent cache file %r", path)
|
||||
else:
|
||||
# Create a small empty file. We'll make it bigger in _initfile.
|
||||
self.f = tempfile.TemporaryFile()
|
||||
self.f.write(magic+z64)
|
||||
logger.info("created temporary cache file %r", self.f.name)
|
||||
|
||||
try:
|
||||
self._initfile(fsize)
|
||||
except:
|
||||
self.f.close()
|
||||
if not path:
|
||||
raise # unrecoverable temp file error :(
|
||||
badpath = path+'.bad'
|
||||
if os.path.exists(badpath):
|
||||
logger.critical(
|
||||
'Removing bad cache file: %r (prev bad exists).',
|
||||
path, exc_info=1)
|
||||
os.remove(path)
|
||||
else:
|
||||
logger.critical('Moving bad cache file to %r.',
|
||||
badpath, exc_info=1)
|
||||
os.rename(path, badpath)
|
||||
self.f = open(path, 'wb+')
|
||||
self.f.write(magic+z64)
|
||||
self._initfile(ZEC_HEADER_SIZE)
|
||||
|
||||
# Statistics: _n_adds, _n_added_bytes,
|
||||
# _n_evicts, _n_evicted_bytes,
|
||||
# _n_accesses
|
||||
self.clearStats()
|
||||
|
||||
self._setup_trace(path)
|
||||
|
||||
# Backward compatibility. Client code used to have to use the fc
|
||||
# attr to get to the file cache to get cache stats.
|
||||
@property
|
||||
def fc(self):
|
||||
return self
|
||||
|
||||
def clear(self):
|
||||
with self._lock:
|
||||
self.f.seek(ZEC_HEADER_SIZE)
|
||||
self.f.truncate()
|
||||
self._initfile(ZEC_HEADER_SIZE)
|
||||
|
||||
##
|
||||
# Scan the current contents of the cache file, calling `install`
|
||||
# for each object found in the cache. This method should only
|
||||
# be called once to initialize the cache from disk.
|
||||
def _initfile(self, fsize):
|
||||
maxsize = self.maxsize
|
||||
f = self.f
|
||||
read = f.read
|
||||
seek = f.seek
|
||||
write = f.write
|
||||
seek(0)
|
||||
if read(4) != magic:
|
||||
seek(0)
|
||||
raise ValueError("unexpected magic number: %r" % read(4))
|
||||
self.tid = read(8)
|
||||
if len(self.tid) != 8:
|
||||
raise ValueError("cache file too small -- no tid at start")
|
||||
|
||||
# Populate .filemap and .key2entry to reflect what's currently in the
|
||||
# file, and tell our parent about it too (via the `install` callback).
|
||||
# Remember the location of the largest free block. That seems a
|
||||
# decent place to start currentofs.
|
||||
|
||||
self.current = _current_index_type()
|
||||
self.noncurrent = _noncurrent_index_type()
|
||||
l = 0
|
||||
last = ofs = ZEC_HEADER_SIZE
|
||||
first_free_offset = 0
|
||||
current = self.current
|
||||
status = b' '
|
||||
while ofs < fsize:
|
||||
seek(ofs)
|
||||
status = read(1)
|
||||
if status == b'a':
|
||||
size, oid, start_tid, end_tid, lver = unpack(
|
||||
">I8s8s8sH", read(30))
|
||||
if ofs+size <= maxsize:
|
||||
if end_tid == z64:
|
||||
assert oid not in current, (ofs, f.tell())
|
||||
current[oid] = ofs
|
||||
else:
|
||||
assert start_tid < end_tid, (ofs, f.tell())
|
||||
self._set_noncurrent(oid, start_tid, ofs)
|
||||
assert lver == 0, "Versions aren't supported"
|
||||
l += 1
|
||||
else:
|
||||
# free block
|
||||
if first_free_offset == 0:
|
||||
first_free_offset = ofs
|
||||
if status == b'f':
|
||||
size, = unpack(">I", read(4))
|
||||
if size > max_block_size:
|
||||
# Oops, we either have an old cache, or a we
|
||||
# crashed while storing. Split this block into two.
|
||||
assert size <= max_block_size*2
|
||||
seek(ofs+max_block_size)
|
||||
write(b'f'+pack(">I", size-max_block_size))
|
||||
seek(ofs)
|
||||
write(b'f'+pack(">I", max_block_size))
|
||||
sync(f)
|
||||
elif status in b'1234':
|
||||
size = int(status)
|
||||
else:
|
||||
raise ValueError("unknown status byte value %s in client "
|
||||
"cache file" % 0, hex(ord(status)))
|
||||
|
||||
last = ofs
|
||||
ofs += size
|
||||
|
||||
if ofs >= maxsize:
|
||||
# Oops, the file was bigger before.
|
||||
if ofs > maxsize:
|
||||
# The last record is too big. Replace it with a smaller
|
||||
# free record
|
||||
size = maxsize-last
|
||||
seek(last)
|
||||
if size > 4:
|
||||
write(b'f'+pack(">I", size))
|
||||
else:
|
||||
write("012345"[size].encode())
|
||||
sync(f)
|
||||
ofs = maxsize
|
||||
break
|
||||
|
||||
if fsize < maxsize:
|
||||
assert ofs==fsize
|
||||
# Make sure the OS really saves enough bytes for the file.
|
||||
seek(self.maxsize - 1)
|
||||
write(b'x')
|
||||
|
||||
# add as many free blocks as are needed to fill the space
|
||||
seek(ofs)
|
||||
nfree = maxsize - ZEC_HEADER_SIZE
|
||||
for i in range(0, nfree, max_block_size):
|
||||
block_size = min(max_block_size, nfree-i)
|
||||
write(b'f' + pack(">I", block_size))
|
||||
seek(block_size-5, 1)
|
||||
sync(self.f)
|
||||
|
||||
# There is always data to read and
|
||||
assert last and (status in b' f1234')
|
||||
first_free_offset = last
|
||||
else:
|
||||
assert ofs==maxsize
|
||||
if maxsize < fsize:
|
||||
seek(maxsize)
|
||||
f.truncate()
|
||||
|
||||
# We use the first_free_offset because it is most likelyt the
|
||||
# place where we last wrote.
|
||||
self.currentofs = first_free_offset or ZEC_HEADER_SIZE
|
||||
self._len = l
|
||||
|
||||
def _set_noncurrent(self, oid, tid, ofs):
|
||||
noncurrent_for_oid = self.noncurrent.get(u64(oid))
|
||||
if noncurrent_for_oid is None:
|
||||
noncurrent_for_oid = _noncurrent_bucket_type()
|
||||
self.noncurrent[u64(oid)] = noncurrent_for_oid
|
||||
noncurrent_for_oid[u64(tid)] = ofs
|
||||
|
||||
def _del_noncurrent(self, oid, tid):
|
||||
try:
|
||||
noncurrent_for_oid = self.noncurrent[u64(oid)]
|
||||
del noncurrent_for_oid[u64(tid)]
|
||||
if not noncurrent_for_oid:
|
||||
del self.noncurrent[u64(oid)]
|
||||
except KeyError:
|
||||
logger.error("Couldn't find non-current %r", (oid, tid))
|
||||
|
||||
|
||||
def clearStats(self):
|
||||
self._n_adds = self._n_added_bytes = 0
|
||||
self._n_evicts = self._n_evicted_bytes = 0
|
||||
self._n_accesses = 0
|
||||
|
||||
def getStats(self):
|
||||
return (self._n_adds, self._n_added_bytes,
|
||||
self._n_evicts, self._n_evicted_bytes,
|
||||
self._n_accesses
|
||||
)
|
||||
|
||||
##
|
||||
# The number of objects currently in the cache.
|
||||
def __len__(self):
|
||||
return self._len
|
||||
|
||||
##
|
||||
# Close the underlying file. No methods accessing the cache should be
|
||||
# used after this.
|
||||
def close(self):
|
||||
self._unsetup_trace()
|
||||
f = self.f
|
||||
self.f = None
|
||||
if f is not None:
|
||||
sync(f)
|
||||
f.close()
|
||||
|
||||
if hasattr(self,'_lock_file'):
|
||||
self._lock_file.close()
|
||||
|
||||
##
|
||||
# Evict objects as necessary to free up at least nbytes bytes,
|
||||
# starting at currentofs. If currentofs is closer than nbytes to
|
||||
# the end of the file, currentofs is reset to ZEC_HEADER_SIZE first.
|
||||
# The number of bytes actually freed may be (and probably will be)
|
||||
# greater than nbytes, and is _makeroom's return value. The file is not
|
||||
# altered by _makeroom. filemap and key2entry are updated to reflect the
|
||||
# evictions, and it's the caller's responsibility both to fiddle
|
||||
# the file, and to update filemap, to account for all the space
|
||||
# freed (starting at currentofs when _makeroom returns, and
|
||||
# spanning the number of bytes retured by _makeroom).
|
||||
def _makeroom(self, nbytes):
|
||||
assert 0 < nbytes <= self.maxsize - ZEC_HEADER_SIZE, (
|
||||
nbytes, self.maxsize)
|
||||
if self.currentofs + nbytes > self.maxsize:
|
||||
self.currentofs = ZEC_HEADER_SIZE
|
||||
ofs = self.currentofs
|
||||
seek = self.f.seek
|
||||
read = self.f.read
|
||||
current = self.current
|
||||
while nbytes > 0:
|
||||
seek(ofs)
|
||||
status = read(1)
|
||||
if status == b'a':
|
||||
size, oid, start_tid, end_tid = unpack(">I8s8s8s", read(28))
|
||||
self._n_evicts += 1
|
||||
self._n_evicted_bytes += size
|
||||
if end_tid == z64:
|
||||
del current[oid]
|
||||
else:
|
||||
self._del_noncurrent(oid, start_tid)
|
||||
self._len -= 1
|
||||
else:
|
||||
if status == b'f':
|
||||
size = unpack(">I", read(4))[0]
|
||||
else:
|
||||
assert status in b'1234'
|
||||
size = int(status)
|
||||
ofs += size
|
||||
nbytes -= size
|
||||
return ofs - self.currentofs
|
||||
|
||||
##
|
||||
# Update our idea of the most recent tid. This is stored in the
|
||||
# instance, and also written out near the start of the cache file. The
|
||||
# new tid must be strictly greater than our current idea of the most
|
||||
# recent tid.
|
||||
def setLastTid(self, tid):
|
||||
with self._lock:
|
||||
if (not tid) or (tid == z64):
|
||||
return
|
||||
if (tid <= self.tid) and self._len:
|
||||
if tid == self.tid:
|
||||
return # Be a little forgiving
|
||||
raise ValueError("new last tid (%s) must be greater than "
|
||||
"previous one (%s)"
|
||||
% (u64(tid), u64(self.tid)))
|
||||
assert isinstance(tid, bytes) and len(tid) == 8, tid
|
||||
self.tid = tid
|
||||
self.f.seek(len(magic))
|
||||
self.f.write(tid)
|
||||
self.f.flush()
|
||||
|
||||
##
|
||||
# Return the last transaction seen by the cache.
|
||||
# @return a transaction id
|
||||
# @defreturn string, or 8 nulls if no transaction is yet known
|
||||
def getLastTid(self):
|
||||
with self._lock:
|
||||
return self.tid
|
||||
|
||||
##
|
||||
# Return the current data record for oid.
|
||||
# @param oid object id
|
||||
# @return (data record, serial number, tid), or None if the object is not
|
||||
# in the cache
|
||||
# @defreturn 3-tuple: (string, string, string)
|
||||
def load(self, oid, before_tid=None):
|
||||
with self._lock:
|
||||
ofs = self.current.get(oid)
|
||||
if ofs is None:
|
||||
self._trace(0x20, oid)
|
||||
return None
|
||||
self.f.seek(ofs)
|
||||
read = self.f.read
|
||||
status = read(1)
|
||||
assert status == b'a', (ofs, self.f.tell(), oid)
|
||||
size, saved_oid, tid, end_tid, lver, ldata = unpack(
|
||||
">I8s8s8sHI", read(34))
|
||||
assert saved_oid == oid, (ofs, self.f.tell(), oid, saved_oid)
|
||||
assert end_tid == z64, (ofs, self.f.tell(), oid, tid, end_tid)
|
||||
assert lver == 0, "Versions aren't supported"
|
||||
|
||||
if before_tid and tid >= before_tid:
|
||||
return None
|
||||
|
||||
data = read(ldata)
|
||||
assert len(data) == ldata, (
|
||||
ofs, self.f.tell(), oid, len(data), ldata)
|
||||
|
||||
# WARNING: The following assert changes the file position.
|
||||
# We must not depend on this below or we'll fail in optimized mode.
|
||||
assert read(8) == oid, (ofs, self.f.tell(), oid)
|
||||
|
||||
self._n_accesses += 1
|
||||
self._trace(0x22, oid, tid, end_tid, ldata)
|
||||
|
||||
ofsofs = self.currentofs - ofs
|
||||
if ofsofs < 0:
|
||||
ofsofs += self.maxsize
|
||||
|
||||
if (ofsofs > self.rearrange and
|
||||
self.maxsize > 10*len(data) and
|
||||
size > 4):
|
||||
# The record is far back and might get evicted, but it's
|
||||
# valuable, so move it forward.
|
||||
|
||||
# Remove fromn old loc:
|
||||
del self.current[oid]
|
||||
self.f.seek(ofs)
|
||||
self.f.write(b'f'+pack(">I", size))
|
||||
|
||||
# Write to new location:
|
||||
self._store(oid, tid, None, data, size)
|
||||
|
||||
return data, tid
|
||||
|
||||
##
|
||||
# Return a non-current revision of oid that was current before tid.
|
||||
# @param oid object id
|
||||
# @param tid id of transaction that wrote next revision of oid
|
||||
# @return data record, serial number, start tid, and end tid
|
||||
# @defreturn 4-tuple: (string, string, string, string)
|
||||
def loadBefore(self, oid, before_tid):
|
||||
with self._lock:
|
||||
noncurrent_for_oid = self.noncurrent.get(u64(oid))
|
||||
if noncurrent_for_oid is None:
|
||||
result = self.load(oid, before_tid)
|
||||
if result:
|
||||
return result[0], result[1], None
|
||||
else:
|
||||
self._trace(0x24, oid, "", before_tid)
|
||||
return result
|
||||
|
||||
items = noncurrent_for_oid.items(None, u64(before_tid)-1)
|
||||
if not items:
|
||||
result = self.load(oid, before_tid)
|
||||
if result:
|
||||
return result[0], result[1], None
|
||||
else:
|
||||
self._trace(0x24, oid, "", before_tid)
|
||||
return result
|
||||
|
||||
tid, ofs = items[-1]
|
||||
|
||||
self.f.seek(ofs)
|
||||
read = self.f.read
|
||||
status = read(1)
|
||||
assert status == b'a', (ofs, self.f.tell(), oid, before_tid)
|
||||
size, saved_oid, saved_tid, end_tid, lver, ldata = unpack(
|
||||
">I8s8s8sHI", read(34))
|
||||
assert saved_oid == oid, (ofs, self.f.tell(), oid, saved_oid)
|
||||
assert saved_tid == p64(tid), (
|
||||
ofs, self.f.tell(), oid, saved_tid, tid)
|
||||
assert end_tid != z64, (ofs, self.f.tell(), oid)
|
||||
assert lver == 0, "Versions aren't supported"
|
||||
data = read(ldata)
|
||||
assert len(data) == ldata, (ofs, self.f.tell())
|
||||
|
||||
# WARNING: The following assert changes the file position.
|
||||
# We must not depend on this below or we'll fail in optimized mode.
|
||||
assert read(8) == oid, (ofs, self.f.tell(), oid)
|
||||
|
||||
if end_tid < before_tid:
|
||||
result = self.load(oid, before_tid)
|
||||
if result:
|
||||
return result[0], result[1], None
|
||||
else:
|
||||
self._trace(0x24, oid, "", before_tid)
|
||||
return result
|
||||
|
||||
self._n_accesses += 1
|
||||
self._trace(0x26, oid, "", saved_tid)
|
||||
return data, saved_tid, end_tid
|
||||
|
||||
##
|
||||
# Store a new data record in the cache.
|
||||
# @param oid object id
|
||||
# @param start_tid the id of the transaction that wrote this revision
|
||||
# @param end_tid the id of the transaction that created the next
|
||||
# revision of oid. If end_tid is None, the data is
|
||||
# current.
|
||||
# @param data the actual data
|
||||
def store(self, oid, start_tid, end_tid, data):
|
||||
with self._lock:
|
||||
seek = self.f.seek
|
||||
if end_tid is None:
|
||||
ofs = self.current.get(oid)
|
||||
if ofs:
|
||||
seek(ofs)
|
||||
read = self.f.read
|
||||
status = read(1)
|
||||
assert status == b'a', (ofs, self.f.tell(), oid)
|
||||
size, saved_oid, saved_tid, end_tid = unpack(
|
||||
">I8s8s8s", read(28))
|
||||
assert saved_oid == oid, (
|
||||
ofs, self.f.tell(), oid, saved_oid)
|
||||
assert end_tid == z64, (ofs, self.f.tell(), oid)
|
||||
if saved_tid == start_tid:
|
||||
return
|
||||
raise ValueError("already have current data for oid")
|
||||
else:
|
||||
noncurrent_for_oid = self.noncurrent.get(u64(oid))
|
||||
if noncurrent_for_oid and (
|
||||
u64(start_tid) in noncurrent_for_oid):
|
||||
return
|
||||
|
||||
size = allocated_record_overhead + len(data)
|
||||
|
||||
# A number of cache simulation experiments all concluded that the
|
||||
# 2nd-level ZEO cache got a much higher hit rate if "very large"
|
||||
# objects simply weren't cached. For now, we ignore the request
|
||||
# only if the entire cache file is too small to hold the object.
|
||||
if size >= min(max_block_size, self.maxsize - ZEC_HEADER_SIZE):
|
||||
return
|
||||
|
||||
self._n_adds += 1
|
||||
self._n_added_bytes += size
|
||||
self._len += 1
|
||||
|
||||
self._store(oid, start_tid, end_tid, data, size)
|
||||
|
||||
if end_tid:
|
||||
self._trace(0x54, oid, start_tid, end_tid, dlen=len(data))
|
||||
else:
|
||||
self._trace(0x52, oid, start_tid, dlen=len(data))
|
||||
|
||||
def _store(self, oid, start_tid, end_tid, data, size):
|
||||
# Low-level store used by store and load
|
||||
|
||||
# In the next line, we ask for an extra to make sure we always
|
||||
# have a free block after the new alocated block. This free
|
||||
# block acts as a ring pointer, so that on restart, we start
|
||||
# where we left off.
|
||||
nfreebytes = self._makeroom(size+1)
|
||||
|
||||
assert size <= nfreebytes, (size, nfreebytes)
|
||||
excess = nfreebytes - size
|
||||
# If there's any excess (which is likely), we need to record a
|
||||
# free block following the end of the data record. That isn't
|
||||
# expensive -- it's all a contiguous write.
|
||||
if excess == 0:
|
||||
extra = b''
|
||||
elif excess < 5:
|
||||
extra = "01234"[excess].encode()
|
||||
else:
|
||||
extra = b'f' + pack(">I", excess)
|
||||
|
||||
ofs = self.currentofs
|
||||
seek = self.f.seek
|
||||
seek(ofs)
|
||||
write = self.f.write
|
||||
|
||||
# Before writing data, we'll write a free block for the space freed.
|
||||
# We'll come back with a last atomic write to rewrite the start of the
|
||||
# allocated-block header.
|
||||
write(b'f'+pack(">I", nfreebytes))
|
||||
|
||||
# Now write the rest of the allocation block header and object data.
|
||||
write(pack(">8s8s8sHI", oid, start_tid, end_tid or z64, 0, len(data)))
|
||||
write(data)
|
||||
write(oid)
|
||||
write(extra)
|
||||
|
||||
# Now, we'll go back and rewrite the beginning of the
|
||||
# allocated block header.
|
||||
seek(ofs)
|
||||
write(b'a'+pack(">I", size))
|
||||
|
||||
if end_tid:
|
||||
self._set_noncurrent(oid, start_tid, ofs)
|
||||
else:
|
||||
self.current[oid] = ofs
|
||||
|
||||
self.currentofs += size
|
||||
|
||||
|
||||
##
|
||||
# If `tid` is None,
|
||||
# forget all knowledge of `oid`. (`tid` can be None only for
|
||||
# invalidations generated by startup cache verification.) If `tid`
|
||||
# isn't None, and we had current
|
||||
# data for `oid`, stop believing we have current data, and mark the
|
||||
# data we had as being valid only up to `tid`. In all other cases, do
|
||||
# nothing.
|
||||
#
|
||||
# Paramters:
|
||||
#
|
||||
# - oid object id
|
||||
# - tid the id of the transaction that wrote a new revision of oid,
|
||||
# or None to forget all cached info about oid.
|
||||
def invalidate(self, oid, tid):
|
||||
with self._lock:
|
||||
ofs = self.current.get(oid)
|
||||
if ofs is None:
|
||||
# 0x10 == invalidate (miss)
|
||||
self._trace(0x10, oid, tid)
|
||||
return
|
||||
|
||||
self.f.seek(ofs)
|
||||
read = self.f.read
|
||||
status = read(1)
|
||||
assert status == b'a', (ofs, self.f.tell(), oid)
|
||||
size, saved_oid, saved_tid, end_tid = unpack(">I8s8s8s", read(28))
|
||||
assert saved_oid == oid, (ofs, self.f.tell(), oid, saved_oid)
|
||||
assert end_tid == z64, (ofs, self.f.tell(), oid)
|
||||
del self.current[oid]
|
||||
if tid is None:
|
||||
self.f.seek(ofs)
|
||||
self.f.write(b'f'+pack(">I", size))
|
||||
# 0x1E = invalidate (hit, discarding current or non-current)
|
||||
self._trace(0x1E, oid, tid)
|
||||
self._len -= 1
|
||||
else:
|
||||
if tid == saved_tid:
|
||||
logger.warning(
|
||||
"Ignoring invalidation with same tid as current")
|
||||
return
|
||||
self.f.seek(ofs+21)
|
||||
self.f.write(tid)
|
||||
self._set_noncurrent(oid, saved_tid, ofs)
|
||||
# 0x1C = invalidate (hit, saving non-current)
|
||||
self._trace(0x1C, oid, tid)
|
||||
|
||||
##
|
||||
# Generates (oid, serial) oairs for all objects in the
|
||||
# cache. This generator is used by cache verification.
|
||||
def contents(self):
|
||||
# May need to materialize list instead of iterating;
|
||||
# depends on whether the caller may change the cache.
|
||||
seek = self.f.seek
|
||||
read = self.f.read
|
||||
for oid, ofs in six.iteritems(self.current):
|
||||
seek(ofs)
|
||||
status = read(1)
|
||||
assert status == b'a', (ofs, self.f.tell(), oid)
|
||||
size, saved_oid, tid, end_tid = unpack(">I8s8s8s", read(28))
|
||||
assert saved_oid == oid, (ofs, self.f.tell(), oid, saved_oid)
|
||||
assert end_tid == z64, (ofs, self.f.tell(), oid)
|
||||
yield oid, tid
|
||||
|
||||
def dump(self):
|
||||
from ZODB.utils import oid_repr
|
||||
print("cache size", len(self))
|
||||
L = list(self.contents())
|
||||
L.sort()
|
||||
for oid, tid in L:
|
||||
print(oid_repr(oid), oid_repr(tid))
|
||||
print("dll contents")
|
||||
L = list(self)
|
||||
L.sort(lambda x, y: cmp(x.key, y.key))
|
||||
for x in L:
|
||||
end_tid = x.end_tid or z64
|
||||
print(oid_repr(x.key[0]), oid_repr(x.key[1]), oid_repr(end_tid))
|
||||
print()
|
||||
|
||||
# If `path` isn't None (== we're using a persistent cache file), and
|
||||
# envar ZEO_CACHE_TRACE is set to a non-empty value, try to open
|
||||
# path+'.trace' as a trace file, and store the file object in
|
||||
# self._tracefile. If not, or we can't write to the trace file, disable
|
||||
# tracing by setting self._trace to a dummy function, and set
|
||||
# self._tracefile to None.
|
||||
_tracefile = None
|
||||
def _trace(self, *a, **kw):
|
||||
pass
|
||||
|
||||
def _setup_trace(self, path):
|
||||
_tracefile = None
|
||||
if path and os.environ.get("ZEO_CACHE_TRACE"):
|
||||
tfn = path + ".trace"
|
||||
try:
|
||||
_tracefile = open(tfn, "ab")
|
||||
except IOError as msg:
|
||||
logger.warning("cannot write tracefile %r (%s)", tfn, msg)
|
||||
else:
|
||||
logger.info("opened tracefile %r", tfn)
|
||||
|
||||
if _tracefile is None:
|
||||
return
|
||||
|
||||
now = time.time
|
||||
def _trace(code, oid=b"", tid=z64, end_tid=z64, dlen=0):
|
||||
# The code argument is two hex digits; bits 0 and 7 must be zero.
|
||||
# The first hex digit shows the operation, the second the outcome.
|
||||
# This method has been carefully tuned to be as fast as possible.
|
||||
# Note: when tracing is disabled, this method is hidden by a dummy.
|
||||
encoded = (dlen << 8) + code
|
||||
if tid is None:
|
||||
tid = z64
|
||||
if end_tid is None:
|
||||
end_tid = z64
|
||||
try:
|
||||
_tracefile.write(
|
||||
pack(">iiH8s8s",
|
||||
int(now()), encoded, len(oid), tid, end_tid) + oid,
|
||||
)
|
||||
except:
|
||||
print(repr(tid), repr(end_tid))
|
||||
raise
|
||||
|
||||
self._trace = _trace
|
||||
self._tracefile = _tracefile
|
||||
_trace(0x00)
|
||||
|
||||
def _unsetup_trace(self):
|
||||
if self._tracefile is not None:
|
||||
del self._trace
|
||||
self._tracefile.close()
|
||||
del self._tracefile
|
||||
|
||||
def sync(f):
|
||||
f.flush()
|
||||
|
||||
if hasattr(os, 'fsync'):
|
||||
def sync(f):
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
@@ -0,0 +1,176 @@
|
||||
<component>
|
||||
|
||||
<import package="ZODB"/>
|
||||
|
||||
<sectiontype name="ssl" datatype="ZEO.zconfig.client_ssl">
|
||||
|
||||
<key name="certificate" datatype="existing-dirpath" required="yes">
|
||||
<description>
|
||||
The full path to an SSL certificate file.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="key" datatype="existing-dirpath" required="no">
|
||||
<description>
|
||||
The full path to an SSL key file for the client certificate.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="password-function" required="no">
|
||||
<description>
|
||||
Dotted name of importable function for retrieving a password
|
||||
for the client certificate key.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="authenticate" datatype="existing-dirpath" required="no">
|
||||
<description>
|
||||
Path to a file or directory containing server certificates to be
|
||||
authenticated.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="check-hostname" datatype="boolean" required="no" default="true">
|
||||
<description>
|
||||
Verify the host name in the server certificate is as expected.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="server-hostname" required="no">
|
||||
<description>
|
||||
Host name to use for SSL host name checks.
|
||||
|
||||
If ``check-hostname`` is true then use this as the
|
||||
value to check. If an address is a host/port pair, then this
|
||||
defaults to the host in the address.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="clientstorage" datatype="ZEO.zconfig.ClientStorageConfig"
|
||||
implements="ZODB.storage">
|
||||
|
||||
<section type="ssl" name="*" attribute="ssl" />
|
||||
|
||||
<multikey name="server" datatype="socket-connection-address" required="yes"
|
||||
/>
|
||||
|
||||
<key name="cache-size" datatype="byte-size" default="20MB">
|
||||
<description>
|
||||
The cache size in bytes, KB or MB. This defaults to a 20MB.
|
||||
Optional ``KB`` or ``MB`` suffixes can (and usually are) used to
|
||||
specify units other than bytes.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="cache-path" required="no">
|
||||
<description>
|
||||
The file path of a persistent cache file
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<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="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 fallback 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="server-sync" datatype="boolean" default="off">
|
||||
<description>
|
||||
A flag indicating whether calls to sync() should make a server
|
||||
request, thus causing the storage to wait for any outstanding
|
||||
invalidations. The sync method is called when transactions are
|
||||
explicitly begun.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="wait-timeout" datatype="integer" default="30">
|
||||
<description>
|
||||
How long to wait for an initial connection, defaulting to 30
|
||||
seconds. If an initial connection can't be made within this time
|
||||
limit, then creation of the client storage will fail with a
|
||||
``ZEO.Exceptions.ClientDisconnected`` exception.
|
||||
|
||||
After the initial connection, if the client is disconnected:
|
||||
|
||||
- In-flight server requests will fail with a
|
||||
``ZEO.Exceptions.ClientDisconnected`` exception.
|
||||
|
||||
- New requests will block for up to ``wait_timeout`` waiting for a
|
||||
connection to be established before failing with a
|
||||
``ZEO.Exceptions.ClientDisconnected`` exception.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="client-label" required="no">
|
||||
<description>
|
||||
A label for the client in server logs
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<!-- The following are undocumented, but not gone. :) -->
|
||||
|
||||
<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="name" default="">
|
||||
<description>
|
||||
The storage name. If unspecified, the address of the server
|
||||
will be used as the name.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,27 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""In Python 2.6, the "sha" and "md5" modules have been deprecated
|
||||
in favor of using hashlib for both. This class allows for compatibility
|
||||
between versions."""
|
||||
|
||||
try:
|
||||
import hashlib
|
||||
sha1 = hashlib.sha1
|
||||
new = sha1
|
||||
except ImportError:
|
||||
import sha
|
||||
sha1 = sha.new
|
||||
new = sha1
|
||||
digest_size = sha.digest_size
|
||||
@@ -0,0 +1,111 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2006 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 zope.interface
|
||||
|
||||
class StaleCache(object):
|
||||
"""A ZEO cache is stale and requires verification.
|
||||
"""
|
||||
|
||||
def __init__(self, storage):
|
||||
self.storage = storage
|
||||
|
||||
class IClientCache(zope.interface.Interface):
|
||||
"""Client cache interface.
|
||||
|
||||
Note that caches need to be thread safe.
|
||||
"""
|
||||
|
||||
def close():
|
||||
"""Close the cache
|
||||
"""
|
||||
|
||||
def load(oid):
|
||||
"""Get current data for object
|
||||
|
||||
Returns data and serial, or None.
|
||||
"""
|
||||
|
||||
def __len__():
|
||||
"""Retirn the number of items in the cache.
|
||||
"""
|
||||
|
||||
def store(oid, start_tid, end_tid, data):
|
||||
"""Store data for the object
|
||||
|
||||
The start_tid is the transaction that committed this data.
|
||||
|
||||
The end_tid is the tid of the next transaction that modified
|
||||
the objects, or None if this is the current version.
|
||||
"""
|
||||
|
||||
def loadBefore(oid, tid):
|
||||
"""Load the data for the object last modified before the tid
|
||||
|
||||
Returns the data, and start and end tids.
|
||||
"""
|
||||
|
||||
def invalidate(oid, tid):
|
||||
"""Invalidate data for the object
|
||||
|
||||
If ``tid`` is None, forget all knowledge of `oid`. (``tid``
|
||||
can be None only for invalidations generated by startup cache
|
||||
verification.)
|
||||
|
||||
If ``tid`` isn't None, and we had current data for ``oid``,
|
||||
stop believing we have current data, and mark the data we had
|
||||
as being valid only up to `tid`. In all other cases, do
|
||||
nothing.
|
||||
"""
|
||||
|
||||
def getLastTid():
|
||||
"""Get the last tid seen by the cache
|
||||
|
||||
This is the cached last tid we've seen from the server.
|
||||
|
||||
This method may be called from multiple threads. (It's assumed
|
||||
to be trivial.)
|
||||
"""
|
||||
|
||||
def setLastTid(tid):
|
||||
"""Save the last tid sent by the server
|
||||
"""
|
||||
|
||||
def clear():
|
||||
"""Clear/empty the cache
|
||||
"""
|
||||
|
||||
class IServeable(zope.interface.Interface):
|
||||
"""Interface provided by storages that can be served by ZEO
|
||||
"""
|
||||
|
||||
def tpc_transaction():
|
||||
"""The current transaction being committed.
|
||||
|
||||
If a storage is participating in a two-phase commit, then
|
||||
return the transaction (object) being committed. Otherwise
|
||||
return None.
|
||||
"""
|
||||
|
||||
def lastInvalidations(size):
|
||||
"""Get recent transaction invalidations
|
||||
|
||||
This method is optional and is used to get invalidations
|
||||
performed by the most recent transactions.
|
||||
|
||||
An iterable of up to size entries must be returned, where each
|
||||
entry is a transaction id and a sequence of object-id/empty-string
|
||||
pairs describing the objects written by the
|
||||
transaction, in chronological order.
|
||||
"""
|
||||
@@ -0,0 +1,113 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Monitor behavior of ZEO server and record statistics.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
|
||||
import asyncore
|
||||
import socket
|
||||
import time
|
||||
import logging
|
||||
|
||||
zeo_version = 'unknown'
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
zeo_dist = pkg_resources.working_set.find(
|
||||
pkg_resources.Requirement.parse('ZODB3')
|
||||
)
|
||||
if zeo_dist is not None:
|
||||
zeo_version = zeo_dist.version
|
||||
|
||||
class StorageStats(object):
|
||||
"""Per-storage usage statistics."""
|
||||
|
||||
def __init__(self, connections=None):
|
||||
self.connections = connections
|
||||
self.loads = 0
|
||||
self.stores = 0
|
||||
self.commits = 0
|
||||
self.aborts = 0
|
||||
self.active_txns = 0
|
||||
self.lock_time = None
|
||||
self.conflicts = 0
|
||||
self.conflicts_resolved = 0
|
||||
self.start = time.ctime()
|
||||
|
||||
@property
|
||||
def clients(self):
|
||||
return len(self.connections)
|
||||
|
||||
def parse(self, s):
|
||||
# parse the dump format
|
||||
lines = s.split("\n")
|
||||
for line in lines:
|
||||
field, value = line.split(":", 1)
|
||||
if field == "Server started":
|
||||
self.start = value
|
||||
elif field == "Clients":
|
||||
# Hack because we use this both on the server and on
|
||||
# the client where there are no connections.
|
||||
self.connections = [0] * int(value)
|
||||
elif field == "Clients verifying":
|
||||
self.verifying_clients = int(value)
|
||||
elif field == "Active transactions":
|
||||
self.active_txns = int(value)
|
||||
elif field == "Commit lock held for":
|
||||
# This assumes
|
||||
self.lock_time = time.time() - int(value)
|
||||
elif field == "Commits":
|
||||
self.commits = int(value)
|
||||
elif field == "Aborts":
|
||||
self.aborts = int(value)
|
||||
elif field == "Loads":
|
||||
self.loads = int(value)
|
||||
elif field == "Stores":
|
||||
self.stores = int(value)
|
||||
elif field == "Conflicts":
|
||||
self.conflicts = int(value)
|
||||
elif field == "Conflicts resolved":
|
||||
self.conflicts_resolved = int(value)
|
||||
|
||||
def dump(self, f):
|
||||
print("Server started:", self.start, file=f)
|
||||
print("Clients:", self.clients, file=f)
|
||||
print("Clients verifying:", self.verifying_clients, file=f)
|
||||
print("Active transactions:", self.active_txns, file=f)
|
||||
if self.lock_time:
|
||||
howlong = time.time() - self.lock_time
|
||||
print("Commit lock held for:", int(howlong), file=f)
|
||||
print("Commits:", self.commits, file=f)
|
||||
print("Aborts:", self.aborts, file=f)
|
||||
print("Loads:", self.loads, file=f)
|
||||
print("Stores:", self.stores, file=f)
|
||||
print("Conflicts:", self.conflicts, file=f)
|
||||
print("Conflicts resolved:", self.conflicts_resolved, file=f)
|
||||
@@ -0,0 +1,143 @@
|
||||
from __future__ import print_function
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2011 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.
|
||||
#
|
||||
##############################################################################
|
||||
"""%prog [options] address
|
||||
|
||||
Where the address is an IPV6 address of the form: [addr]:port, an IPV4
|
||||
address of the form: addr:port, or the name of a unix-domain socket file.
|
||||
"""
|
||||
import json
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
NO_TRANSACTION = '0'*16
|
||||
|
||||
nodiff_names = 'active_txns connections waiting'.split()
|
||||
diff_names = 'aborts commits conflicts conflicts_resolved loads stores'.split()
|
||||
|
||||
per_times = dict(seconds=1.0, minutes=60.0, hours=3600.0, days=86400.0)
|
||||
|
||||
def new_metric(metrics, storage_id, name, value):
|
||||
if storage_id == '1':
|
||||
label = name
|
||||
else:
|
||||
if ' ' in storage_id:
|
||||
label = "'%s:%s'" % (storage_id, name)
|
||||
else:
|
||||
label = "%s:%s" % (storage_id, name)
|
||||
metrics.append("%s=%s" % (label, value))
|
||||
|
||||
def result(messages, metrics=(), status=None):
|
||||
if metrics:
|
||||
messages[0] += '|' + metrics[0]
|
||||
if len(metrics) > 1:
|
||||
messages.append('| ' + '\n '.join(metrics[1:]))
|
||||
print('\n'.join(messages))
|
||||
return status
|
||||
|
||||
def error(message):
|
||||
return result((message, ), (), 2)
|
||||
|
||||
def warn(message):
|
||||
return result((message, ), (), 1)
|
||||
|
||||
def check(addr, output_metrics, status, per):
|
||||
m = re.match(r'\[(\S+)\]:(\d+)$', addr)
|
||||
if m:
|
||||
addr = m.group(1), int(m.group(2))
|
||||
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
else:
|
||||
m = re.match(r'(\S+):(\d+)$', addr)
|
||||
if m:
|
||||
addr = m.group(1), int(m.group(2))
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
else:
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.connect(addr)
|
||||
except socket.error as err:
|
||||
return error("Can't connect %s" % err)
|
||||
|
||||
s.sendall(b'\x00\x00\x00\x04ruok')
|
||||
proto = s.recv(struct.unpack(">I", s.recv(4))[0])
|
||||
datas = s.recv(struct.unpack(">I", s.recv(4))[0])
|
||||
s.close()
|
||||
data = json.loads(datas.decode("ascii"))
|
||||
if not data:
|
||||
return warn("No storages")
|
||||
|
||||
metrics = []
|
||||
messages = []
|
||||
level = 0
|
||||
if output_metrics:
|
||||
for storage_id, sdata in sorted(data.items()):
|
||||
for name in nodiff_names:
|
||||
new_metric(metrics, storage_id, name, sdata[name])
|
||||
|
||||
if status:
|
||||
now = time.time()
|
||||
if os.path.exists(status):
|
||||
dt = now - os.stat(status).st_mtime
|
||||
if dt > 0: # sanity :)
|
||||
with open(status) as f: # Read previous
|
||||
old = json.loads(f.read())
|
||||
dt /= per_times[per]
|
||||
for storage_id, sdata in sorted(data.items()):
|
||||
sdata['sameple-time'] = now
|
||||
if storage_id in old:
|
||||
sold = old[storage_id]
|
||||
for name in diff_names:
|
||||
v = (sdata[name] - sold[name]) / dt
|
||||
new_metric(metrics, storage_id, name, v)
|
||||
with open(status, 'w') as f: # save current
|
||||
f.write(json.dumps(data))
|
||||
|
||||
for storage_id, sdata in sorted(data.items()):
|
||||
if sdata['last-transaction'] == NO_TRANSACTION:
|
||||
messages.append("Empty storage %r" % storage_id)
|
||||
level = max(level, 1)
|
||||
if not messages:
|
||||
messages.append('OK')
|
||||
return result(messages, metrics, level or None)
|
||||
|
||||
def main(args=None):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
parser = optparse.OptionParser(__doc__)
|
||||
parser.add_option(
|
||||
'-m', '--output-metrics', action="store_true",
|
||||
help="Output metrics.",
|
||||
)
|
||||
parser.add_option(
|
||||
'-s', '--status-path',
|
||||
help="Path to status file, needed to get rate metrics",
|
||||
)
|
||||
parser.add_option(
|
||||
'-u', '--time-units', type='choice', default='minutes',
|
||||
choices=['seconds', 'minutes', 'hours', 'days'],
|
||||
help="Time unit for rate metrics",
|
||||
)
|
||||
(options, args) = parser.parse_args(args)
|
||||
[addr] = args
|
||||
return check(
|
||||
addr, options.output_metrics, options.status_path, options.time_units)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,151 @@
|
||||
ZEO Nagios plugin
|
||||
=================
|
||||
|
||||
ZEO includes a script that provides a nagios monitor plugin:
|
||||
|
||||
>>> import pkg_resources, time
|
||||
>>> nagios = pkg_resources.load_entry_point(
|
||||
... 'ZEO', 'console_scripts', 'zeo-nagios')
|
||||
|
||||
In it's simplest form, the script just checks if it can get status:
|
||||
|
||||
>>> import ZEO
|
||||
>>> addr, stop = ZEO.server('test.fs', threaded=False)
|
||||
>>> saddr = ':'.join(map(str, addr)) # (host, port) -> host:port
|
||||
|
||||
>>> nagios([saddr])
|
||||
Empty storage u'1'
|
||||
1
|
||||
|
||||
The storage was empty. In that case, the monitor warned as much.
|
||||
|
||||
Let's add some data:
|
||||
|
||||
>>> ZEO.DB(addr).close()
|
||||
>>> nagios([saddr])
|
||||
OK
|
||||
|
||||
If we stop the server, we'll error:
|
||||
|
||||
>>> stop()
|
||||
>>> nagios([saddr])
|
||||
Can't connect [Errno 61] Connection refused
|
||||
2
|
||||
|
||||
Metrics
|
||||
-------
|
||||
|
||||
The monitor will optionally output server metric data. There are 2
|
||||
kinds of metrics it can output, level and rate metric. If we use the
|
||||
-m/--output-metrics option, we'll just get rate metrics:
|
||||
|
||||
>>> addr, stop = ZEO.server('test.fs', threaded=False)
|
||||
>>> saddr = ':'.join(map(str, addr)) # (host, port) -> host:port
|
||||
>>> nagios([saddr, '-m'])
|
||||
OK|active_txns=0
|
||||
| connections=0
|
||||
waiting=0
|
||||
|
||||
We only got the metrics that are levels, like current number of
|
||||
connections. If we want rate metrics, we need to be able to save
|
||||
values from run to run. We need to use the -s/--status-path option to
|
||||
specify the name of a file for status information:
|
||||
|
||||
>>> nagios([saddr, '-m', '-sstatus'])
|
||||
OK|active_txns=0
|
||||
| connections=0
|
||||
waiting=0
|
||||
|
||||
We still didn't get any rate metrics, because we've only run once.
|
||||
Let's actually do something with the database and then make another
|
||||
sample.
|
||||
|
||||
>>> db = ZEO.DB(addr)
|
||||
>>> nagios([saddr, '-m', '-sstatus'])
|
||||
OK|active_txns=0
|
||||
| connections=1
|
||||
waiting=0
|
||||
aborts=0.0
|
||||
commits=0.0
|
||||
conflicts=0.0
|
||||
conflicts_resolved=0.0
|
||||
loads=81.226297803
|
||||
stores=0.0
|
||||
|
||||
Note that this time, we saw that there was a connection.
|
||||
|
||||
The ZEO.nagios module provides a check function that can be used by
|
||||
other monitors (e.g. that get address data from ZooKeeper). It takes:
|
||||
|
||||
- Address string,
|
||||
|
||||
- Metrics flag.
|
||||
|
||||
- Status file name (or None), and
|
||||
|
||||
- Time units for rate metrics
|
||||
|
||||
::
|
||||
|
||||
>>> import ZEO.nagios
|
||||
>>> ZEO.nagios.check(saddr, True, 'status', 'seconds')
|
||||
OK|active_txns=0
|
||||
| connections=1
|
||||
waiting=0
|
||||
aborts=0.0
|
||||
commits=0.0
|
||||
conflicts=0.0
|
||||
conflicts_resolved=0.0
|
||||
loads=0.0
|
||||
stores=0.0
|
||||
|
||||
>>> db.close()
|
||||
>>> stop()
|
||||
|
||||
Multi-storage servers
|
||||
---------------------
|
||||
|
||||
A ZEO server can host multiple servers. (This is a feature that will
|
||||
likely be dropped in the future.) When this is the case, the monitor
|
||||
profixes metrics with a storage id.
|
||||
|
||||
>>> addr, stop = ZEO.server(
|
||||
... storage_conf = """
|
||||
... <mappingstorage first>
|
||||
... </mappingstorage>
|
||||
... <mappingstorage second>
|
||||
... </mappingstorage>
|
||||
... """, threaded=False)
|
||||
>>> saddr = ':'.join(map(str, addr)) # (host, port) -> host:port
|
||||
>>> nagios([saddr, '-m', '-sstatus'])
|
||||
Empty storage u'first'|first:active_txns=0
|
||||
Empty storage u'second'
|
||||
| first:connections=0
|
||||
first:waiting=0
|
||||
second:active_txns=0
|
||||
second:connections=0
|
||||
second:waiting=0
|
||||
1
|
||||
>>> nagios([saddr, '-m', '-sstatus'])
|
||||
Empty storage u'first'|first:active_txns=0
|
||||
Empty storage u'second'
|
||||
| first:connections=0
|
||||
first:waiting=0
|
||||
second:active_txns=0
|
||||
second:connections=0
|
||||
second:waiting=0
|
||||
first:aborts=0.0
|
||||
first:commits=0.0
|
||||
first:conflicts=0.0
|
||||
first:conflicts_resolved=0.0
|
||||
first:loads=42.42
|
||||
first:stores=0.0
|
||||
second:aborts=0.0
|
||||
second:commits=0.0
|
||||
second:conflicts=0.0
|
||||
second:conflicts_resolved=0.0
|
||||
second:loads=42.42
|
||||
second:stores=0.0
|
||||
1
|
||||
|
||||
>>> stop()
|
||||
@@ -0,0 +1,276 @@
|
||||
==============================
|
||||
Response ordering requirements
|
||||
==============================
|
||||
|
||||
ZEO servers are logically concurrent because they serve multiple
|
||||
clients in multiple threads. Because of this, we have to make sure
|
||||
that information about object histories remain consistent.
|
||||
|
||||
An object history is a sequence of object revisions. Each revision has
|
||||
a tid, which is essentially a time stamp.
|
||||
|
||||
We load objects using either ``load``, which returns the current
|
||||
object. or ``loadBefore``, which returns the object before a specific time/tid.
|
||||
|
||||
When we cache revisions, we record the tid and the next/end tid, which
|
||||
may be None. The end tid is important for choosing a revision for
|
||||
``loadBefore``, as well as for determining whether a cached value is
|
||||
current, for ``load``.
|
||||
|
||||
Because the client and server are multi-threaded, the client may see
|
||||
data out of order. Let's consider some scenarios. In these
|
||||
scenarios
|
||||
|
||||
Scenarios
|
||||
=========
|
||||
|
||||
When considering ordering scenarioes, we'll consider 2 different
|
||||
client behaviors, traditional (T) and loadBefore (B).
|
||||
|
||||
The *traditional* behaviors is that used in ZODB 4. It uses the storage
|
||||
``load(oid)`` method to load objects if it hasn't seen an invalidation
|
||||
for the object. If it has seen an invalidation, it uses
|
||||
``loadBefore(oid, START)``, where ``START`` is the transaction time of
|
||||
the first invalidation it's seen. If it hasn't seen an invalidation
|
||||
*for an object*, it uses ``load(oid)`` and then checks again for an
|
||||
invalidation. If it sees an invalidation, then it retries using
|
||||
``loadBefore``. This approach **assumes that invalidations for a tid
|
||||
are returned before loads for a tid**.
|
||||
|
||||
The *loadBefore* behavior, used in ZODB5, always determines
|
||||
transaction start time, ``START`` at the beginning of a transaction by
|
||||
calling the storage's ``sync`` method and then querying the storage's
|
||||
``lastTransaction`` method (and adding 1). It loads objects
|
||||
exclusively using ``loadBefore(oid, START)``.
|
||||
|
||||
Scenario 1, Invalidations seen after loads for transaction
|
||||
----------------------------------------------------------
|
||||
|
||||
This scenario could occur because the commits are for a different
|
||||
client, and a hypothetical; server doesn't block loads while
|
||||
committing, or sends invalidations in a way that might delay them (but
|
||||
not send them out of order).
|
||||
|
||||
T1
|
||||
|
||||
- client starts a transaction
|
||||
|
||||
- client load(O1) gets O1-T1
|
||||
|
||||
- client load(O2)
|
||||
|
||||
- Server commits O2-T2
|
||||
|
||||
- Server loads (O2-T2)
|
||||
|
||||
- Client gets O2-T2, updates the client cache, and completes load
|
||||
|
||||
- Client sees invalidation for O2-T2. If the
|
||||
client is smart, it doesn't update the cache.
|
||||
|
||||
The transaction now has inconsistent data, because it should have
|
||||
loaded whatever O2 was before T2. Because the invalidation came
|
||||
in after O2 was loaded, the load was unaffected.
|
||||
|
||||
B1
|
||||
|
||||
- client starts a transaction. Sets START to T1+1
|
||||
|
||||
- client loadBefore(O1, T1+1) gets O1-T1, T1, None
|
||||
|
||||
- client loadBefore(O2, T1+1)
|
||||
|
||||
- Server commits O2-T2
|
||||
|
||||
- Server loadBefore(O2, T1+1) -> O2-T0-T2
|
||||
|
||||
(assuming that the revision of O2 before T2 was T0)
|
||||
|
||||
- Client gets O2-T0-T2, updates cache.
|
||||
|
||||
- Client sees invalidation for O2-T2. No update to the cache is
|
||||
necessary.
|
||||
|
||||
In this scenario, loadBefore prevents reading incorrect data.
|
||||
|
||||
A variation on this scenario is that client sees invalidations
|
||||
tpc_finish in another thread after loads for the same transaction.
|
||||
|
||||
Scenario 2, Client sees invalidations for later transaction before load result
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
T2
|
||||
|
||||
- client starts a transaction
|
||||
|
||||
- client load(O1) gets O1-T1
|
||||
|
||||
- client load(O2)
|
||||
|
||||
- Server loads (O2-T0)
|
||||
|
||||
- Server commits O2-T2
|
||||
|
||||
- Client sees invalidation for O2-T2. O2 isn't in the cache, so
|
||||
nothing to do.
|
||||
|
||||
- Client gets O2-T0, updates the client cache, and completes load
|
||||
|
||||
The cache is now incorrect. It has O2-T0-None, meaning it thinks
|
||||
O2-T0 is current.
|
||||
|
||||
The transaction is OK, because it got a consistent value for O2.
|
||||
|
||||
B2
|
||||
|
||||
- client starts a transaction. Sets START to T1+1
|
||||
|
||||
- client loadBefore(O1, T1+1) gets O1-T1, T1, None
|
||||
|
||||
- client loadBefore(O2, T1+1)
|
||||
|
||||
- Server loadBefore(O2, T1+1) -> O2-T0-None
|
||||
|
||||
- Server commits O2-T2
|
||||
|
||||
- Client sees invalidation for O2-T2. O2 isn't in the cache, so
|
||||
nothing to do.
|
||||
|
||||
- Client gets O2-T0-None, and completes load
|
||||
|
||||
ZEO 4 doesn't cache loadBefore results with no ending transaction.
|
||||
|
||||
Assume ZEO 5 updates the client cache.
|
||||
|
||||
For ZEO 5, the cache is now incorrect. It has O2-T0-None, meaning
|
||||
it thinks O2-T0 is current.
|
||||
|
||||
The transaction is OK, because it got a consistent value for O2.
|
||||
|
||||
In this case, ``loadBefore`` didn't prevent an invalid cache value.
|
||||
|
||||
Scenario 3, client sees invalidation after lastTransaction result
|
||||
------------------------------------------------------------------
|
||||
|
||||
(This doesn't effect the traditional behavior.)
|
||||
|
||||
B3
|
||||
|
||||
- The client cache has a last tid of T1.
|
||||
|
||||
- ZODB calls sync() then calls lastTransaction. Is so configured,
|
||||
ZEO calls lastTransaction on the server. This is mainly to make a
|
||||
round trip to get in-flight invalidations. We don't necessarily
|
||||
need to use the value. In fact, in protocol 5, we could just add a
|
||||
sync method that just makes a round trip, but does nothing else.
|
||||
|
||||
- Server commits O1-T2, O2-T2.
|
||||
|
||||
- Server reads and returns T2. (It doesn't mater what it returns
|
||||
|
||||
- client sets START to T1+1, because lastTransaction is based on
|
||||
what's in the cache, which is based on invalidations.
|
||||
|
||||
- Client loadBefore(O1, T2+1), finds O1-T1-None in cache and uses
|
||||
it.
|
||||
|
||||
- Client gets invalidation for O1-T2. Updates cache to O1-T1-T2.
|
||||
|
||||
- Client loadBefore(O2, T1+1), gets O2-T1-None
|
||||
|
||||
This is OK, as long as the client doesn't do anything with the
|
||||
lastTransaction result in ``sync``.
|
||||
|
||||
Implementation notes
|
||||
===================
|
||||
|
||||
ZEO 4
|
||||
-----
|
||||
|
||||
The ZEO 4 server sends data to the client in correct order with
|
||||
respect to loads and invalidations (or tpc_finish results). This is a
|
||||
consequence of the fact that invalidations are sent in a callback
|
||||
called when the storage lock is held, blocking loads while committing,
|
||||
and, fact that client requests, for a particular client, are
|
||||
handled by a single thread on the server, and that all output for a
|
||||
client goes through a thread-safe queue.
|
||||
|
||||
Invalidations are sent from different threads than clients. Outgoing
|
||||
data is queued, however, using Python lists, which are protected by
|
||||
the GIL. This means that the serialization provided though storage
|
||||
locks is preserved by the way that server outputs are queued. **The
|
||||
queueing mechanism is in part a consequence of the way asyncore, used
|
||||
by ZEO4, works.
|
||||
|
||||
In ZEO 4 clients, invalidations and loads are handled by separate
|
||||
threads. This means that even though data arive in order, they may not
|
||||
be processed in order,
|
||||
|
||||
T1
|
||||
The existing servers mitigate this by blocking loads while
|
||||
committing. On the client, this is still a potential issue because loads
|
||||
and invalidations are handled by separate threads, however, locks are
|
||||
used on the client to assure that invalidations are processed before
|
||||
blocked loads complete.
|
||||
|
||||
T2
|
||||
Existing storage servers serialize commits (and thus sending of
|
||||
invalidations) and loads. As with scenario T1, threading on the
|
||||
client can cause load results and invalidations to be processed out
|
||||
of order. To mitigate this, the client uses a load lock to track
|
||||
when loads are invalidated while in flight and doesn't save to the
|
||||
cache when they are. This is bad on multiple levels. It serializes
|
||||
loads even when there are multiple threads. It may prevent writing
|
||||
to the cache unnecessarily, if the invalidation is for a revision
|
||||
before the one that was loaded.
|
||||
|
||||
B2
|
||||
Here, we avoid incorrect returned values and incorrect cache at the
|
||||
cost of caching nothing.
|
||||
|
||||
ZEO 4.2.0 addressed this by using the same locking strategy for
|
||||
``loadBefore`` that was used for ``load``, thus mitigating B2 the
|
||||
same way it mitigates T2.
|
||||
|
||||
ZEO 5
|
||||
-----
|
||||
|
||||
In ZEO(/ZODB) 5, we want to get more concurrency, both on the client,
|
||||
and on the server. On the client, cache invalidations and loads are
|
||||
done by the same thread, which makes things a bit simpler. This let's
|
||||
us get rid of the client load lock and prevents the scenarios above
|
||||
with existing servers.
|
||||
|
||||
On the client, we'd like to stop serializing loads and commits. We'd
|
||||
like commits (tpc_finish calls) to be in flight with loads (and with
|
||||
other commits). In the current protocol, tpc_finish, load and
|
||||
loadBefore are all synchronous calls that are handled by a single
|
||||
thread on the server, so these calls end up being serialized on the
|
||||
server anyway.
|
||||
|
||||
The server-side hndling of invalidations is a bit tricker in ZEO 5
|
||||
because there isn't a thread-safe queue of outgoing messages in ZEO 5
|
||||
as there was in ZEO 4. The natural approach in ZEO 5 would be to use
|
||||
asyncio's ``call_soon_threadsafe`` to send invalidations in a client's
|
||||
thread. This could easily cause invalidations to be sent after loads.
|
||||
As shown above, this isn't a problem for ZODB 5, at least assuming
|
||||
that invalidations arrive in order. This would be a problem for
|
||||
ZODB 4. For this reason, we require ZODB 5 for ZEO 5.
|
||||
|
||||
Note that this approach can't cause invalidations to be sent early,
|
||||
because they could only be sent by the thread that's busy loading, so
|
||||
scenario 2 wouldn't happen.
|
||||
|
||||
B2
|
||||
Because the server send invalidations by calling
|
||||
``call_soon_threadsafe``, it's impoossible for invalidations to be
|
||||
send while a load request is being handled.
|
||||
|
||||
The main server opportunity is allowing commits for separate oids to
|
||||
happen concurrently. This wouldn't effect the invalidation/load
|
||||
ordering though.
|
||||
|
||||
It would be nice not to block loads while making tpc_finish calls, but
|
||||
storages do this anyway now, so there's nothing to be done about it
|
||||
now. Storage locking requirements aren't well specified, and probably
|
||||
should be rethought in light of ZODB5/loadBefore.
|
||||
@@ -0,0 +1,55 @@
|
||||
ZEO Network Protocol (sans authentication)
|
||||
==========================================
|
||||
|
||||
This document describes the ZEO network protocol. It assumes that the
|
||||
optional authentication protocol isn't used. At the lowest
|
||||
level, the protocol consists of sized messages. All communication
|
||||
between the client and server consists of sized messages. A sized
|
||||
message consists of a 4-byte unsigned big-endian content length,
|
||||
followed by the content. There are two subprotocols, for protocol
|
||||
negotiation, and for normal operation. The normal operation protocol
|
||||
is a basic RPC protocol.
|
||||
|
||||
In the protocol negotiation phase, the server sends a protocol
|
||||
identifier to the client. The client chooses a protocol to use to the
|
||||
server. The client or the server can fail if it doesn't like the
|
||||
protocol string sent by the other party. After sending their protocol
|
||||
strings, the client and server switch to RPC mode.
|
||||
|
||||
The RPC protocol uses messages that are pickled tuples consisting of:
|
||||
|
||||
message_id
|
||||
The message id is used to match replies with requests, allowing
|
||||
multiple outstanding synchronous requests.
|
||||
|
||||
async_flag
|
||||
An integer 0 for a regular (2-way) request and 1 for a one-way
|
||||
request. Two-way requests have a reply. One way requests don't.
|
||||
ZRS tries to use as many one-way requests as possible to avoid
|
||||
network round trips.
|
||||
|
||||
name
|
||||
The name of a method to call. If this is the special string
|
||||
".reply", then the message is interpreted as a return from a
|
||||
synchronous call.
|
||||
|
||||
args
|
||||
A tuple of positional arguments or returned values.
|
||||
|
||||
After making a connection and negotiating the protocol, the following
|
||||
interactions occur:
|
||||
|
||||
- The client requests the authentication protocol by calling
|
||||
getAuthProtocol. For this discussion, we'll assume the server
|
||||
returns None. Note that if the server doesn't require
|
||||
authentication, this step is optional.
|
||||
|
||||
- The client calls register passing a storage identifier and a
|
||||
read-only flag. The server doesn't return a value, but it may raise
|
||||
an exception either if the storage doesn't exist, or if the
|
||||
storage is readonly and the read-only flag passed by the client is
|
||||
false.
|
||||
|
||||
At this point, the client and server send each other messages as
|
||||
needed. The client may make regular or one-way calls to the
|
||||
server. The server sends replies and one-way calls to the client.
|
||||
@@ -0,0 +1,399 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Start the ZEO storage server.
|
||||
|
||||
Usage: %s [-C URL] [-a ADDRESS] [-f FILENAME] [-h]
|
||||
|
||||
Options:
|
||||
-C/--configuration URL -- configuration file or URL
|
||||
-a/--address ADDRESS -- server address of the form PORT, HOST:PORT, or PATH
|
||||
(a PATH must contain at least one "/")
|
||||
-f/--filename FILENAME -- filename for FileStorage
|
||||
-t/--timeout TIMEOUT -- transaction timeout in seconds (default no timeout)
|
||||
-h/--help -- print this usage message and exit
|
||||
--pid-file PATH -- relative path to output file containing this process's pid;
|
||||
default $(INSTANCE_HOME)/var/ZEO.pid but only if envar
|
||||
INSTANCE_HOME is defined
|
||||
|
||||
Unless -C is specified, -a and -f are required.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
# The code here is designed to be reused by other, similar servers.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import socket
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
import ZConfig.datatypes
|
||||
from zdaemon.zdoptions import ZDOptions
|
||||
|
||||
logger = logging.getLogger('ZEO.runzeo')
|
||||
_pid = str(os.getpid())
|
||||
|
||||
def log(msg, level=logging.INFO, exc_info=False):
|
||||
"""Internal: generic logging function."""
|
||||
message = "(%s) %s" % (_pid, msg)
|
||||
logger.log(level, message, exc_info=exc_info)
|
||||
|
||||
def parse_binding_address(arg):
|
||||
# Caution: Not part of the official ZConfig API.
|
||||
obj = ZConfig.datatypes.SocketBindingAddress(arg)
|
||||
return obj.family, obj.address
|
||||
|
||||
def windows_shutdown_handler():
|
||||
# Called by the signal mechanism on Windows to perform shutdown.
|
||||
import asyncore
|
||||
asyncore.close_all()
|
||||
|
||||
class ZEOOptionsMixin(object):
|
||||
|
||||
storages = None
|
||||
|
||||
def handle_address(self, arg):
|
||||
self.family, self.address = parse_binding_address(arg)
|
||||
|
||||
def handle_filename(self, arg):
|
||||
from ZODB.config import FileStorage # That's a FileStorage *opener*!
|
||||
class FSConfig(object):
|
||||
def __init__(self, name, path):
|
||||
self._name = name
|
||||
self.path = path
|
||||
self.stop = None
|
||||
def getSectionName(self):
|
||||
return self._name
|
||||
if not self.storages:
|
||||
self.storages = []
|
||||
name = str(1 + len(self.storages))
|
||||
conf = FileStorage(FSConfig(name, arg))
|
||||
self.storages.append(conf)
|
||||
|
||||
testing_exit_immediately = False
|
||||
def handle_test(self, *args):
|
||||
self.testing_exit_immediately = True
|
||||
|
||||
def add_zeo_options(self):
|
||||
self.add(None, None, None, "test", self.handle_test)
|
||||
self.add(None, None, "a:", "address=", self.handle_address)
|
||||
self.add(None, None, "f:", "filename=", self.handle_filename)
|
||||
self.add("family", "zeo.address.family")
|
||||
self.add("address", "zeo.address.address",
|
||||
required="no server address specified; use -a or -C")
|
||||
self.add("read_only", "zeo.read_only", default=0)
|
||||
self.add("client_conflict_resolution",
|
||||
"zeo.client_conflict_resolution",
|
||||
default=0)
|
||||
self.add("msgpack", "zeo.msgpack", default=0)
|
||||
self.add("invalidation_queue_size", "zeo.invalidation_queue_size",
|
||||
default=100)
|
||||
self.add("invalidation_age", "zeo.invalidation_age")
|
||||
self.add("transaction_timeout", "zeo.transaction_timeout",
|
||||
"t:", "timeout=", float)
|
||||
self.add('pid_file', 'zeo.pid_filename',
|
||||
None, 'pid-file=')
|
||||
self.add("ssl", "zeo.ssl")
|
||||
|
||||
class ZEOOptions(ZDOptions, ZEOOptionsMixin):
|
||||
|
||||
__doc__ = __doc__
|
||||
|
||||
logsectionname = "eventlog"
|
||||
schemadir = os.path.dirname(__file__)
|
||||
|
||||
def __init__(self):
|
||||
ZDOptions.__init__(self)
|
||||
self.add_zeo_options()
|
||||
self.add("storages", "storages",
|
||||
required="no storages specified; use -f or -C")
|
||||
|
||||
def realize(self, *a, **k):
|
||||
ZDOptions.realize(self, *a, **k)
|
||||
nunnamed = [s for s in self.storages if s.name is None]
|
||||
if nunnamed:
|
||||
if len(nunnamed) > 1:
|
||||
return self.usage("No more than one storage may be unnamed.")
|
||||
if [s for s in self.storages if s.name == '1']:
|
||||
return self.usage(
|
||||
"Can't have an unnamed storage and a storage named 1.")
|
||||
for s in self.storages:
|
||||
if s.name is None:
|
||||
s.name = '1'
|
||||
break
|
||||
|
||||
|
||||
class ZEOServer(object):
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
self.server = None
|
||||
|
||||
def main(self):
|
||||
self.setup_default_logging()
|
||||
self.check_socket()
|
||||
self.clear_socket()
|
||||
self.make_pidfile()
|
||||
try:
|
||||
self.open_storages()
|
||||
self.setup_signals()
|
||||
self.create_server()
|
||||
self.loop_forever()
|
||||
finally:
|
||||
self.close_server()
|
||||
self.clear_socket()
|
||||
self.remove_pidfile()
|
||||
|
||||
def setup_default_logging(self):
|
||||
if self.options.config_logger is not None:
|
||||
return
|
||||
# No log file is configured; default to stderr.
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.INFO)
|
||||
fmt = logging.Formatter(
|
||||
"------\n%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
"%Y-%m-%dT%H:%M:%S")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(fmt)
|
||||
root.addHandler(handler)
|
||||
|
||||
def check_socket(self):
|
||||
if (isinstance(self.options.address, tuple) and
|
||||
self.options.address[1] is None):
|
||||
self.options.address = self.options.address[0], 0
|
||||
return
|
||||
|
||||
if self.can_connect(self.options.family, self.options.address):
|
||||
self.options.usage("address %s already in use" %
|
||||
repr(self.options.address))
|
||||
|
||||
def can_connect(self, family, address):
|
||||
s = socket.socket(family, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.connect(address)
|
||||
except socket.error:
|
||||
return 0
|
||||
else:
|
||||
s.close()
|
||||
return 1
|
||||
|
||||
def clear_socket(self):
|
||||
if isinstance(self.options.address, six.string_types):
|
||||
try:
|
||||
os.unlink(self.options.address)
|
||||
except os.error:
|
||||
pass
|
||||
|
||||
def open_storages(self):
|
||||
self.storages = {}
|
||||
for opener in self.options.storages:
|
||||
log("opening storage %r using %s"
|
||||
% (opener.name, opener.__class__.__name__))
|
||||
self.storages[opener.name] = opener.open()
|
||||
|
||||
def setup_signals(self):
|
||||
"""Set up signal handlers.
|
||||
|
||||
The signal handler for SIGFOO is a method handle_sigfoo().
|
||||
If no handler method is defined for a signal, the signal
|
||||
action is not changed from its initial value. The handler
|
||||
method is called without additional arguments.
|
||||
"""
|
||||
if os.name != "posix":
|
||||
if os.name == "nt":
|
||||
self.setup_win32_signals()
|
||||
return
|
||||
if hasattr(signal, 'SIGXFSZ'):
|
||||
signal.signal(signal.SIGXFSZ, signal.SIG_IGN) # Special case
|
||||
init_signames()
|
||||
for sig, name in signames.items():
|
||||
method = getattr(self, "handle_" + name.lower(), None)
|
||||
if method is not None:
|
||||
def wrapper(sig_dummy, frame_dummy, method=method):
|
||||
method()
|
||||
signal.signal(sig, wrapper)
|
||||
|
||||
def setup_win32_signals(self):
|
||||
# Borrow the Zope Signals package win32 support, if available.
|
||||
# Signals does a check/log for the availability of pywin32.
|
||||
try:
|
||||
import Signals.Signals
|
||||
except ImportError:
|
||||
logger.debug("Signals package not found. "
|
||||
"Windows-specific signal handler "
|
||||
"will *not* be installed.")
|
||||
return
|
||||
SignalHandler = Signals.Signals.SignalHandler
|
||||
if SignalHandler is not None: # may be None if no pywin32.
|
||||
SignalHandler.registerHandler(signal.SIGTERM,
|
||||
windows_shutdown_handler)
|
||||
SignalHandler.registerHandler(signal.SIGINT,
|
||||
windows_shutdown_handler)
|
||||
SIGUSR2 = 12 # not in signal module on Windows.
|
||||
SignalHandler.registerHandler(SIGUSR2, self.handle_sigusr2)
|
||||
|
||||
def create_server(self):
|
||||
self.server = create_server(self.storages, self.options)
|
||||
|
||||
def loop_forever(self):
|
||||
if self.options.testing_exit_immediately:
|
||||
print("testing exit immediately")
|
||||
else:
|
||||
self.server.loop()
|
||||
|
||||
def close_server(self):
|
||||
if self.server is not None:
|
||||
self.server.close()
|
||||
|
||||
def handle_sigterm(self):
|
||||
log("terminated by SIGTERM")
|
||||
sys.exit(0)
|
||||
|
||||
def handle_sigint(self):
|
||||
log("terminated by SIGINT")
|
||||
sys.exit(0)
|
||||
|
||||
def handle_sighup(self):
|
||||
log("restarted by SIGHUP")
|
||||
sys.exit(1)
|
||||
|
||||
def handle_sigusr2(self):
|
||||
# log rotation signal - do the same as Zope 2.7/2.8...
|
||||
if self.options.config_logger is None or os.name not in ("posix", "nt"):
|
||||
log("received SIGUSR2, but it was not handled!",
|
||||
level=logging.WARNING)
|
||||
return
|
||||
|
||||
loggers = [self.options.config_logger]
|
||||
|
||||
if os.name == "posix":
|
||||
for l in loggers:
|
||||
l.reopen()
|
||||
log("Log files reopened successfully", level=logging.INFO)
|
||||
else: # nt - same rotation code as in Zope's Signals/Signals.py
|
||||
for l in loggers:
|
||||
for f in l.handler_factories:
|
||||
handler = f()
|
||||
if hasattr(handler, 'rotate') and callable(handler.rotate):
|
||||
handler.rotate()
|
||||
log("Log files rotation complete", level=logging.INFO)
|
||||
|
||||
def _get_pidfile(self):
|
||||
pidfile = self.options.pid_file
|
||||
# 'pidfile' is marked as not required.
|
||||
if not pidfile:
|
||||
# Try to find a reasonable location if the pidfile is not
|
||||
# set. If we are running in a Zope environment, we can
|
||||
# safely assume INSTANCE_HOME.
|
||||
instance_home = os.environ.get("INSTANCE_HOME")
|
||||
if not instance_home:
|
||||
# If all our attempts failed, just log a message and
|
||||
# proceed.
|
||||
logger.debug("'pidfile' option not set, and 'INSTANCE_HOME' "
|
||||
"environment variable could not be found. "
|
||||
"Cannot guess pidfile location.")
|
||||
return
|
||||
self.options.pid_file = os.path.join(instance_home,
|
||||
"var", "ZEO.pid")
|
||||
|
||||
def make_pidfile(self):
|
||||
if not self.options.read_only:
|
||||
self._get_pidfile()
|
||||
pidfile = self.options.pid_file
|
||||
if pidfile is None:
|
||||
return
|
||||
pid = os.getpid()
|
||||
try:
|
||||
if os.path.exists(pidfile):
|
||||
os.unlink(pidfile)
|
||||
f = open(pidfile, 'w')
|
||||
print(pid, file=f)
|
||||
f.close()
|
||||
log("created PID file '%s'" % pidfile)
|
||||
except IOError:
|
||||
logger.error("PID file '%s' cannot be opened" % pidfile)
|
||||
|
||||
def remove_pidfile(self):
|
||||
if not self.options.read_only:
|
||||
pidfile = self.options.pid_file
|
||||
if pidfile is None:
|
||||
return
|
||||
try:
|
||||
if os.path.exists(pidfile):
|
||||
os.unlink(pidfile)
|
||||
log("removed PID file '%s'" % pidfile)
|
||||
except IOError:
|
||||
logger.error("PID file '%s' could not be removed" % pidfile)
|
||||
|
||||
|
||||
def create_server(storages, options):
|
||||
from .StorageServer import StorageServer
|
||||
return StorageServer(
|
||||
options.address,
|
||||
storages,
|
||||
read_only = options.read_only,
|
||||
client_conflict_resolution=options.client_conflict_resolution,
|
||||
msgpack=(options.msgpack if isinstance(options.msgpack, bool)
|
||||
else os.environ.get('ZEO_MSGPACK')),
|
||||
invalidation_queue_size = options.invalidation_queue_size,
|
||||
invalidation_age = options.invalidation_age,
|
||||
transaction_timeout = options.transaction_timeout,
|
||||
ssl = options.ssl,
|
||||
)
|
||||
|
||||
|
||||
# Signal names
|
||||
|
||||
signames = None
|
||||
|
||||
def signame(sig):
|
||||
"""Return a symbolic name for a signal.
|
||||
|
||||
Return "signal NNN" if there is no corresponding SIG name in the
|
||||
signal module.
|
||||
"""
|
||||
|
||||
if signames is None:
|
||||
init_signames()
|
||||
return signames.get(sig) or "signal %d" % sig
|
||||
|
||||
def init_signames():
|
||||
global signames
|
||||
signames = {}
|
||||
for name, sig in signal.__dict__.items():
|
||||
k_startswith = getattr(name, "startswith", None)
|
||||
if k_startswith is None:
|
||||
continue
|
||||
if k_startswith("SIG") and not k_startswith("SIG_"):
|
||||
signames[sig] = name
|
||||
|
||||
|
||||
# Main program
|
||||
|
||||
def main(args=None):
|
||||
options = ZEOOptions()
|
||||
options.realize(args)
|
||||
s = ZEOServer(options)
|
||||
s.main()
|
||||
|
||||
def run(args):
|
||||
options = ZEOOptions()
|
||||
options.realize(args)
|
||||
s = ZEOServer(options)
|
||||
s.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,40 @@
|
||||
<schema>
|
||||
|
||||
<!-- note that zeoctl.xml is a closely related schema which should
|
||||
match this schema, but should require the "runner" section -->
|
||||
|
||||
<description>
|
||||
This schema describes the configuration of the ZEO storage server
|
||||
process.
|
||||
</description>
|
||||
|
||||
<!-- Use the storage types defined by ZODB. -->
|
||||
<import package="ZODB"/>
|
||||
|
||||
<!-- Use the ZEO server information structure. -->
|
||||
<import package="ZEO" file="server.xml" />
|
||||
|
||||
<import package="ZConfig.components.logger"/>
|
||||
|
||||
<!-- runner control -->
|
||||
<import package="zdaemon"/>
|
||||
|
||||
|
||||
<section type="zeo" name="*" required="yes" attribute="zeo" />
|
||||
|
||||
<section type="runner" name="*" required="no" attribute="runner" />
|
||||
|
||||
<multisection name="*" type="ZODB.storage"
|
||||
attribute="storages"
|
||||
required="yes">
|
||||
<description>
|
||||
One or more storages that are provided by the ZEO server. The
|
||||
section names are used as the storage names, and must be unique
|
||||
within each ZEO storage server. Traditionally, these names
|
||||
represent small integers starting at '1'.
|
||||
</description>
|
||||
</multisection>
|
||||
|
||||
<section name="*" type="eventlog" attribute="eventlog" required="no" />
|
||||
|
||||
</schema>
|
||||
@@ -0,0 +1,64 @@
|
||||
This directory contains a collection of utilities for working with
|
||||
ZEO. Some are more useful than others. If you install ZODB using
|
||||
distutils ("python setup.py install"), some 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.
|
||||
|
||||
|
||||
parsezeolog.py -- parse BLATHER logs from ZEO server
|
||||
|
||||
This script may be obsolete. It has not been tested against the
|
||||
current log output of the ZEO server.
|
||||
|
||||
Reports on the time and size of transactions committed by a ZEO
|
||||
server, by inspecting log messages at BLATHER level.
|
||||
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
zeopack.py -- pack a ZEO server
|
||||
|
||||
The script connects to a server and calls pack() on a specific
|
||||
storage. See the script for usage details.
|
||||
|
||||
|
||||
zeoreplay.py -- experimental script to replay transactions from a ZEO log
|
||||
|
||||
Like parsezeolog.py, this may be obsolete because it was written
|
||||
against an earlier version of the ZEO server. See the script for
|
||||
usage details.
|
||||
|
||||
|
||||
zeoup.py
|
||||
|
||||
usage: zeoup.py [options]
|
||||
|
||||
The test will connect to a ZEO server, load the root object, and
|
||||
attempt to update the zeoup counter in the root. It will report
|
||||
success if it updates to counter or if it gets a ConflictError. A
|
||||
ConflictError is considered a success, because the client was able to
|
||||
start a transaction.
|
||||
|
||||
See the script for details about the options.
|
||||
|
||||
|
||||
|
||||
zeoserverlog.py -- analyze ZEO server log for performance statistics
|
||||
|
||||
See the module docstring for details; there are a large number of
|
||||
options. New in ZODB3 3.1.4.
|
||||
|
||||
|
||||
zeoqueue.py -- report number of clients currently waiting in the ZEO queue
|
||||
|
||||
See the module docstring for details.
|
||||
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,581 @@
|
||||
#! /usr/bin/env python
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2001-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
|
||||
#
|
||||
##############################################################################
|
||||
"""
|
||||
Cache simulation.
|
||||
|
||||
|
||||
Note:
|
||||
|
||||
- The simulation isn't perfect.
|
||||
|
||||
- The simulation will be far off if the trace file
|
||||
was created starting with a non-empty cache
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import bisect
|
||||
import struct
|
||||
import re
|
||||
import sys
|
||||
import ZEO.cache
|
||||
import argparse
|
||||
|
||||
from ZODB.utils import z64
|
||||
|
||||
from .cache_stats import add_interval_argument
|
||||
from .cache_stats import add_tracefile_argument
|
||||
|
||||
# we assign ctime locally to facilitate test replacement!
|
||||
from time import ctime
|
||||
import six
|
||||
|
||||
|
||||
def main(args=None):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
# Parse options.
|
||||
MB = 1<<20
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--size", "-s",
|
||||
default=20*MB, dest="cachelimit",
|
||||
type=lambda s: int(float(s)*MB),
|
||||
help="cache size in MB (default 20MB)")
|
||||
add_interval_argument(parser)
|
||||
parser.add_argument("--rearrange", "-r",
|
||||
default=0.8, type=float,
|
||||
help="rearrange factor")
|
||||
add_tracefile_argument(parser)
|
||||
|
||||
simclass = CircularCacheSimulation
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
f = options.tracefile
|
||||
interval_step = options.interval
|
||||
|
||||
# Create simulation object.
|
||||
sim = simclass(options.cachelimit, options.rearrange)
|
||||
interval_sim = simclass(options.cachelimit, options.rearrange)
|
||||
|
||||
# Print output header.
|
||||
sim.printheader()
|
||||
|
||||
# Read trace file, simulating cache behavior.
|
||||
f_read = f.read
|
||||
unpack = struct.unpack
|
||||
FMT = ">iiH8s8s"
|
||||
FMT_SIZE = struct.calcsize(FMT)
|
||||
assert FMT_SIZE == 26
|
||||
|
||||
last_interval = None
|
||||
while 1:
|
||||
# Read a record and decode it.
|
||||
r = f_read(FMT_SIZE)
|
||||
if len(r) < FMT_SIZE:
|
||||
break
|
||||
ts, code, oidlen, start_tid, end_tid = unpack(FMT, r)
|
||||
if ts == 0:
|
||||
# Must be a misaligned record caused by a crash; skip 8 bytes
|
||||
# and try again. Why 8? Lost in the mist of history.
|
||||
f.seek(f.tell() - FMT_SIZE + 8)
|
||||
continue
|
||||
oid = f_read(oidlen)
|
||||
if len(oid) < oidlen:
|
||||
break
|
||||
# Decode the code.
|
||||
dlen, version, code = ((code & 0x7fffff00) >> 8,
|
||||
code & 0x80,
|
||||
code & 0x7e)
|
||||
# And pass it to the simulation.
|
||||
this_interval = int(ts) // interval_step
|
||||
if this_interval != last_interval:
|
||||
if last_interval is not None:
|
||||
interval_sim.report()
|
||||
interval_sim.restart()
|
||||
if not interval_sim.warm:
|
||||
sim.restart()
|
||||
last_interval = this_interval
|
||||
sim.event(ts, dlen, version, code, oid, start_tid, end_tid)
|
||||
interval_sim.event(ts, dlen, version, code, oid, start_tid, end_tid)
|
||||
|
||||
f.close()
|
||||
# Finish simulation.
|
||||
interval_sim.report()
|
||||
sim.finish()
|
||||
|
||||
class Simulation(object):
|
||||
"""Base class for simulations.
|
||||
|
||||
The driver program calls: event(), printheader(), finish().
|
||||
|
||||
The standard event() method calls these additional methods:
|
||||
write(), load(), inval(), report(), restart(); the standard
|
||||
finish() method also calls report().
|
||||
"""
|
||||
|
||||
def __init__(self, cachelimit, rearrange):
|
||||
self.cachelimit = cachelimit
|
||||
self.rearrange = rearrange
|
||||
# Initialize global statistics.
|
||||
self.epoch = None
|
||||
self.warm = False
|
||||
self.total_loads = 0
|
||||
self.total_hits = 0 # subclass must increment
|
||||
self.total_invals = 0 # subclass must increment
|
||||
self.total_writes = 0
|
||||
if not hasattr(self, "extras"):
|
||||
self.extras = (self.extraname,)
|
||||
self.format = self.format + " %7s" * len(self.extras)
|
||||
# Reset per-run statistics and set up simulation data.
|
||||
self.restart()
|
||||
|
||||
def restart(self):
|
||||
# Reset per-run statistics.
|
||||
self.loads = 0
|
||||
self.hits = 0 # subclass must increment
|
||||
self.invals = 0 # subclass must increment
|
||||
self.writes = 0
|
||||
self.ts0 = None
|
||||
|
||||
def event(self, ts, dlen, _version, code, oid,
|
||||
start_tid, end_tid):
|
||||
# Record first and last timestamp seen.
|
||||
if self.ts0 is None:
|
||||
self.ts0 = ts
|
||||
if self.epoch is None:
|
||||
self.epoch = ts
|
||||
self.ts1 = ts
|
||||
|
||||
# Simulate cache behavior. Caution: the codes in the trace file
|
||||
# record whether the actual cache missed or hit on each load, but
|
||||
# that bears no necessary relationship to whether the simulated cache
|
||||
# will hit or miss. Relatedly, if the actual cache needed to store
|
||||
# an object, the simulated cache may not need to (it may already
|
||||
# have the data).
|
||||
action = code & 0x70
|
||||
if action & 0x20:
|
||||
# Load.
|
||||
self.loads += 1
|
||||
self.total_loads += 1
|
||||
# Asserting that dlen is 0 iff it's a load miss.
|
||||
# assert (dlen == 0) == (code in (0x20, 0x24))
|
||||
self.load(oid, dlen, start_tid, code)
|
||||
elif action & 0x40:
|
||||
# Store.
|
||||
assert dlen
|
||||
self.write(oid, dlen, start_tid, end_tid)
|
||||
elif action & 0x10:
|
||||
# Invalidate.
|
||||
self.inval(oid, start_tid)
|
||||
elif action == 0x00:
|
||||
# Restart.
|
||||
self.restart()
|
||||
else:
|
||||
raise ValueError("unknown trace code 0x%x" % code)
|
||||
|
||||
def write(self, oid, size, start_tid, end_tid):
|
||||
pass
|
||||
|
||||
def load(self, oid, size, start_tid, code):
|
||||
# Must increment .hits and .total_hits as appropriate.
|
||||
pass
|
||||
|
||||
def inval(self, oid, start_tid):
|
||||
# Must increment .invals and .total_invals as appropriate.
|
||||
pass
|
||||
|
||||
format = "%12s %6s %7s %7s %6s %6s %7s"
|
||||
|
||||
# Subclass should override extraname to name known instance variables;
|
||||
# if extraname is 'foo', both self.foo and self.total_foo must exist:
|
||||
extraname = "*** please override ***"
|
||||
|
||||
def printheader(self):
|
||||
print("%s, cache size %s bytes" % (self.__class__.__name__,
|
||||
addcommas(self.cachelimit)))
|
||||
self.extraheader()
|
||||
extranames = tuple([s.upper() for s in self.extras])
|
||||
args = ("START TIME", "DUR.", "LOADS", "HITS",
|
||||
"INVALS", "WRITES", "HITRATE") + extranames
|
||||
print(self.format % args)
|
||||
|
||||
def extraheader(self):
|
||||
pass
|
||||
|
||||
nreports = 0
|
||||
|
||||
def report(self):
|
||||
if not hasattr(self, 'ts1'):
|
||||
return
|
||||
self.nreports += 1
|
||||
args = (ctime(self.ts0)[4:-8],
|
||||
duration(self.ts1 - self.ts0),
|
||||
self.loads, self.hits, self.invals, self.writes,
|
||||
hitrate(self.loads, self.hits))
|
||||
args += tuple([getattr(self, name) for name in self.extras])
|
||||
print(self.format % args)
|
||||
|
||||
def finish(self):
|
||||
# Make sure that the last line of output ends with "OVERALL". This
|
||||
# makes it much easier for another program parsing the output to
|
||||
# find summary statistics.
|
||||
print('-'*74)
|
||||
if self.nreports < 2:
|
||||
self.report()
|
||||
else:
|
||||
self.report()
|
||||
args = (
|
||||
ctime(self.epoch)[4:-8],
|
||||
duration(self.ts1 - self.epoch),
|
||||
self.total_loads,
|
||||
self.total_hits,
|
||||
self.total_invals,
|
||||
self.total_writes,
|
||||
hitrate(self.total_loads, self.total_hits))
|
||||
args += tuple([getattr(self, "total_" + name)
|
||||
for name in self.extras])
|
||||
print(self.format % args)
|
||||
|
||||
|
||||
# For use in CircularCacheSimulation.
|
||||
class CircularCacheEntry(object):
|
||||
__slots__ = (
|
||||
# object key: an (oid, start_tid) pair, where start_tid is the
|
||||
# tid of the transaction that created this revision of oid
|
||||
'key',
|
||||
|
||||
# tid of transaction that created the next revision; z64 iff
|
||||
# this is the current revision
|
||||
'end_tid',
|
||||
|
||||
# Offset from start of file to the object's data record; this
|
||||
# includes all overhead bytes (status byte, size bytes, etc).
|
||||
'offset',
|
||||
)
|
||||
|
||||
def __init__(self, key, end_tid, offset):
|
||||
self.key = key
|
||||
self.end_tid = end_tid
|
||||
self.offset = offset
|
||||
|
||||
from ZEO.cache import ZEC_HEADER_SIZE
|
||||
|
||||
class CircularCacheSimulation(Simulation):
|
||||
"""Simulate the ZEO 3.0 cache."""
|
||||
|
||||
# The cache is managed as a single file with a pointer that
|
||||
# goes around the file, circularly, forever. New objects
|
||||
# are written at the current pointer, evicting whatever was
|
||||
# there previously.
|
||||
|
||||
extras = "evicts", "inuse"
|
||||
|
||||
evicts = 0
|
||||
|
||||
def __init__(self, cachelimit, rearrange):
|
||||
from ZEO import cache
|
||||
|
||||
Simulation.__init__(self, cachelimit, rearrange)
|
||||
self.total_evicts = 0 # number of cache evictions
|
||||
|
||||
# Current offset in file.
|
||||
self.offset = ZEC_HEADER_SIZE
|
||||
|
||||
# Map offset in file to (size, CircularCacheEntry) pair, or to
|
||||
# (size, None) if the offset starts a free block.
|
||||
self.filemap = {ZEC_HEADER_SIZE: (self.cachelimit - ZEC_HEADER_SIZE,
|
||||
None)}
|
||||
# Map key to CircularCacheEntry. A key is an (oid, tid) pair.
|
||||
self.key2entry = {}
|
||||
|
||||
# Map oid to tid of current revision.
|
||||
self.current = {}
|
||||
|
||||
# Map oid to list of (start_tid, end_tid) pairs in sorted order.
|
||||
# Used to find matching key for load of non-current data.
|
||||
self.noncurrent = {}
|
||||
|
||||
# The number of overhead bytes needed to store an object pickle
|
||||
# on disk (all bytes beyond those needed for the object pickle).
|
||||
self.overhead = ZEO.cache.allocated_record_overhead
|
||||
|
||||
# save evictions so we can replay them, if necessary
|
||||
self.evicted = {}
|
||||
|
||||
def restart(self):
|
||||
Simulation.restart(self)
|
||||
if self.evicts:
|
||||
self.warm = True
|
||||
self.evicts = 0
|
||||
self.evicted_hit = self.evicted_miss = 0
|
||||
|
||||
evicted_hit = evicted_miss = 0
|
||||
def load(self, oid, size, tid, code):
|
||||
if (code == 0x20) or (code == 0x22):
|
||||
# Trying to load current revision.
|
||||
if oid in self.current: # else it's a cache miss
|
||||
self.hits += 1
|
||||
self.total_hits += 1
|
||||
|
||||
tid = self.current[oid]
|
||||
entry = self.key2entry[(oid, tid)]
|
||||
offset_offset = self.offset - entry.offset
|
||||
if offset_offset < 0:
|
||||
offset_offset += self.cachelimit
|
||||
assert offset_offset >= 0
|
||||
|
||||
if offset_offset > self.rearrange * self.cachelimit:
|
||||
# we haven't accessed it in a while. Move it forward
|
||||
size = self.filemap[entry.offset][0]
|
||||
self._remove(*entry.key)
|
||||
self.add(oid, size, tid)
|
||||
|
||||
elif oid in self.evicted:
|
||||
size, e = self.evicted[oid]
|
||||
self.write(oid, size, e.key[1], z64, 1)
|
||||
self.evicted_hit += 1
|
||||
else:
|
||||
self.evicted_miss += 1
|
||||
|
||||
return
|
||||
|
||||
# May or may not be trying to load current revision.
|
||||
cur_tid = self.current.get(oid)
|
||||
if cur_tid == tid:
|
||||
self.hits += 1
|
||||
self.total_hits += 1
|
||||
return
|
||||
|
||||
# It's a load for non-current data. Do we know about this oid?
|
||||
L = self.noncurrent.get(oid)
|
||||
if L is None:
|
||||
return # cache miss
|
||||
i = bisect.bisect_left(L, (tid, None))
|
||||
if i == 0:
|
||||
# This tid is smaller than any we know about -- miss.
|
||||
return
|
||||
lo, hi = L[i-1]
|
||||
assert lo < tid
|
||||
if tid > hi:
|
||||
# No data in the right tid range -- miss.
|
||||
return
|
||||
# Cache hit.
|
||||
self.hits += 1
|
||||
self.total_hits += 1
|
||||
|
||||
# (oid, tid) is in the cache. Remove it: take it out of key2entry,
|
||||
# and in `filemap` mark the space it occupied as being free. The
|
||||
# caller is responsible for removing it from `current` or `noncurrent`.
|
||||
def _remove(self, oid, tid):
|
||||
key = oid, tid
|
||||
e = self.key2entry.pop(key)
|
||||
pos = e.offset
|
||||
size, _e = self.filemap[pos]
|
||||
assert e is _e
|
||||
self.filemap[pos] = size, None
|
||||
|
||||
def _remove_noncurrent_revisions(self, oid):
|
||||
noncurrent_list = self.noncurrent.get(oid)
|
||||
if noncurrent_list:
|
||||
self.invals += len(noncurrent_list)
|
||||
self.total_invals += len(noncurrent_list)
|
||||
for start_tid, end_tid in noncurrent_list:
|
||||
self._remove(oid, start_tid)
|
||||
del self.noncurrent[oid]
|
||||
|
||||
def inval(self, oid, tid):
|
||||
if tid == z64:
|
||||
# This is part of startup cache verification: forget everything
|
||||
# about this oid.
|
||||
self._remove_noncurrent_revisions(oid)
|
||||
|
||||
if oid in self.evicted:
|
||||
del self.evicted[oid]
|
||||
|
||||
cur_tid = self.current.get(oid)
|
||||
if cur_tid is None:
|
||||
# We don't have current data, so nothing more to do.
|
||||
return
|
||||
|
||||
# We had current data for oid, but no longer.
|
||||
self.invals += 1
|
||||
self.total_invals += 1
|
||||
del self.current[oid]
|
||||
if tid == z64:
|
||||
# Startup cache verification: forget this oid entirely.
|
||||
self._remove(oid, cur_tid)
|
||||
return
|
||||
|
||||
# Our current data becomes non-current data.
|
||||
# Add the validity range to the list of non-current data for oid.
|
||||
assert cur_tid < tid
|
||||
L = self.noncurrent.setdefault(oid, [])
|
||||
bisect.insort_left(L, (cur_tid, tid))
|
||||
# Update the end of oid's validity range in its CircularCacheEntry.
|
||||
e = self.key2entry[oid, cur_tid]
|
||||
assert e.end_tid == z64
|
||||
e.end_tid = tid
|
||||
|
||||
def write(self, oid, size, start_tid, end_tid, evhit=0):
|
||||
if end_tid == z64:
|
||||
# Storing current revision.
|
||||
if oid in self.current: # we already have it in cache
|
||||
if evhit:
|
||||
import pdb; pdb.set_trace()
|
||||
raise ValueError('WTF')
|
||||
return
|
||||
self.current[oid] = start_tid
|
||||
self.writes += 1
|
||||
self.total_writes += 1
|
||||
self.add(oid, size, start_tid)
|
||||
return
|
||||
if evhit:
|
||||
import pdb; pdb.set_trace()
|
||||
raise ValueError('WTF')
|
||||
# Storing non-current revision.
|
||||
L = self.noncurrent.setdefault(oid, [])
|
||||
p = start_tid, end_tid
|
||||
if p in L:
|
||||
return # we already have it in cache
|
||||
bisect.insort_left(L, p)
|
||||
self.writes += 1
|
||||
self.total_writes += 1
|
||||
self.add(oid, size, start_tid, end_tid)
|
||||
|
||||
# Add `oid` to the cache, evicting objects as needed to make room.
|
||||
# This updates `filemap` and `key2entry`; it's the caller's
|
||||
# responsibilty to update `current` or `noncurrent` appropriately.
|
||||
def add(self, oid, size, start_tid, end_tid=z64):
|
||||
key = oid, start_tid
|
||||
assert key not in self.key2entry
|
||||
size += self.overhead
|
||||
avail = self.makeroom(size+1) # see cache.py
|
||||
e = CircularCacheEntry(key, end_tid, self.offset)
|
||||
self.filemap[self.offset] = size, e
|
||||
self.key2entry[key] = e
|
||||
self.offset += size
|
||||
# All the space made available must be accounted for in filemap.
|
||||
excess = avail - size
|
||||
if excess:
|
||||
self.filemap[self.offset] = excess, None
|
||||
|
||||
# Evict enough objects to make at least `need` contiguous bytes, starting
|
||||
# at `self.offset`, available. Evicted objects are removed from
|
||||
# `filemap`, `key2entry`, `current` and `noncurrent`. The caller is
|
||||
# responsible for adding new entries to `filemap` to account for all
|
||||
# the freed bytes, and for advancing `self.offset`. The number of bytes
|
||||
# freed is the return value, and will be >= need.
|
||||
def makeroom(self, need):
|
||||
if self.offset + need > self.cachelimit:
|
||||
self.offset = ZEC_HEADER_SIZE
|
||||
pos = self.offset
|
||||
while need > 0:
|
||||
assert pos < self.cachelimit
|
||||
size, e = self.filemap.pop(pos)
|
||||
if e: # there is an object here (else it's already free space)
|
||||
self.evicts += 1
|
||||
self.total_evicts += 1
|
||||
assert pos == e.offset
|
||||
_e = self.key2entry.pop(e.key)
|
||||
assert e is _e
|
||||
oid, start_tid = e.key
|
||||
if e.end_tid == z64:
|
||||
del self.current[oid]
|
||||
self.evicted[oid] = size-self.overhead, e
|
||||
else:
|
||||
L = self.noncurrent[oid]
|
||||
L.remove((start_tid, e.end_tid))
|
||||
need -= size
|
||||
pos += size
|
||||
return pos - self.offset # total number of bytes freed
|
||||
|
||||
def report(self):
|
||||
self.check()
|
||||
free = used = total = 0
|
||||
for size, e in six.itervalues(self.filemap):
|
||||
total += size
|
||||
if e:
|
||||
used += size
|
||||
else:
|
||||
free += size
|
||||
|
||||
self.inuse = round(100.0 * used / total, 1)
|
||||
self.total_inuse = self.inuse
|
||||
Simulation.report(self)
|
||||
#print self.evicted_hit, self.evicted_miss
|
||||
|
||||
def check(self):
|
||||
oidcount = 0
|
||||
pos = ZEC_HEADER_SIZE
|
||||
while pos < self.cachelimit:
|
||||
size, e = self.filemap[pos]
|
||||
if e:
|
||||
oidcount += 1
|
||||
assert self.key2entry[e.key].offset == pos
|
||||
pos += size
|
||||
assert oidcount == len(self.key2entry)
|
||||
assert pos == self.cachelimit
|
||||
|
||||
def dump(self):
|
||||
print(len(self.filemap))
|
||||
L = list(self.filemap)
|
||||
L.sort()
|
||||
for k in L:
|
||||
v = self.filemap[k]
|
||||
print(k, v[0], repr(v[1]))
|
||||
|
||||
|
||||
def roundup(size):
|
||||
k = MINSIZE
|
||||
while k < size:
|
||||
k += k
|
||||
return k
|
||||
|
||||
def hitrate(loads, hits):
|
||||
if loads < 1:
|
||||
return 'n/a'
|
||||
return "%5.1f%%" % (100.0 * hits / loads)
|
||||
|
||||
def duration(secs):
|
||||
mm, ss = divmod(secs, 60)
|
||||
hh, mm = divmod(mm, 60)
|
||||
if hh:
|
||||
return "%d:%02d:%02d" % (hh, mm, ss)
|
||||
if mm:
|
||||
return "%d:%02d" % (mm, ss)
|
||||
return "%d" % ss
|
||||
|
||||
nre = re.compile('([=-]?)(\d+)([.]\d*)?').match
|
||||
def addcommas(n):
|
||||
sign, s, d = nre(str(n)).group(1, 2, 3)
|
||||
if d == '.0':
|
||||
d = ''
|
||||
|
||||
result = s[-3:]
|
||||
s = s[:-3]
|
||||
while s:
|
||||
result = s[-3:]+','+result
|
||||
s = s[:-3]
|
||||
|
||||
return (sign or '') + result + (d or '')
|
||||
|
||||
import random
|
||||
|
||||
def maybe(f, p=0.5):
|
||||
if random.random() < p:
|
||||
f()
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,381 @@
|
||||
from __future__ import print_function
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Trace file statistics analyzer.
|
||||
|
||||
File format:
|
||||
|
||||
Each record is 26 bytes, plus a variable number of bytes to store an oid,
|
||||
with the following layout. Numbers are big-endian integers.
|
||||
|
||||
Offset Size Contents
|
||||
|
||||
0 4 timestamp (seconds since 1/1/1970)
|
||||
4 3 data size, in 256-byte increments, rounded up
|
||||
7 1 code (see below)
|
||||
8 2 object id length
|
||||
10 8 start tid
|
||||
18 8 end tid
|
||||
26 variable object id
|
||||
|
||||
The code at offset 7 packs three fields:
|
||||
|
||||
Mask bits Contents
|
||||
|
||||
0x80 1 set if there was a non-empty version string
|
||||
0x7e 6 function and outcome code
|
||||
0x01 1 current cache file (0 or 1)
|
||||
|
||||
The "current cache file" bit is no longer used; it refers to a 2-file
|
||||
cache scheme used before ZODB 3.3.
|
||||
|
||||
The function and outcome codes are documented in detail at the end of
|
||||
this file in the 'explain' dictionary. Note that the keys there (and
|
||||
also the arguments to _trace() in ClientStorage.py) are 'code & 0x7e',
|
||||
i.e. the low bit is always zero.
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import struct
|
||||
import gzip
|
||||
|
||||
# we assign ctime locally to facilitate test replacement!
|
||||
from time import ctime
|
||||
import six
|
||||
|
||||
def add_interval_argument(parser):
|
||||
def _interval(a):
|
||||
interval = int(60 * float(a))
|
||||
if interval <= 0:
|
||||
interval = 60
|
||||
elif interval > 3600:
|
||||
interval = 3600
|
||||
return interval
|
||||
parser.add_argument("--interval", "-i",
|
||||
default=15*60, type=_interval,
|
||||
help="summarizing interval in minutes (default 15; max 60)")
|
||||
|
||||
def add_tracefile_argument(parser):
|
||||
|
||||
class GzipFileType(argparse.FileType):
|
||||
def __init__(self):
|
||||
super(GzipFileType, self).__init__(mode='rb')
|
||||
|
||||
def __call__(self, s):
|
||||
f = super(GzipFileType, self).__call__(s)
|
||||
if s.endswith(".gz"):
|
||||
f = gzip.GzipFile(filename=s, fileobj=f)
|
||||
return f
|
||||
|
||||
parser.add_argument("tracefile", type=GzipFileType(),
|
||||
help="The trace to read; may be gzipped")
|
||||
|
||||
def main(args=None):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
# Parse options
|
||||
parser = argparse.ArgumentParser(description="Trace file statistics analyzer",
|
||||
# Our -h, short for --load-histogram
|
||||
# conflicts with default for help, so we handle
|
||||
# manually.
|
||||
add_help=False)
|
||||
verbose_group = parser.add_mutually_exclusive_group()
|
||||
verbose_group.add_argument('--verbose', '-v',
|
||||
default=False, action='store_true',
|
||||
help="Be verbose; print each record")
|
||||
verbose_group.add_argument('--quiet', '-q',
|
||||
default=False, action='store_true',
|
||||
help="Reduce output; don't print summaries")
|
||||
parser.add_argument("--sizes", '-s',
|
||||
default=False, action="store_true", dest="print_size_histogram",
|
||||
help="print histogram of object sizes")
|
||||
parser.add_argument("--no-stats", '-S',
|
||||
default=True, action="store_false", dest="dostats",
|
||||
help="don't print statistics")
|
||||
parser.add_argument("--load-histogram", "-h",
|
||||
default=False, action="store_true", dest="print_histogram",
|
||||
help="print histogram of object load frequencies")
|
||||
parser.add_argument("--check", "-X",
|
||||
default=False, action="store_true", dest="heuristic",
|
||||
help=" enable heuristic checking for misaligned records: oids > 2**32"
|
||||
" will be rejected; this requires the tracefile to be seekable")
|
||||
add_interval_argument(parser)
|
||||
add_tracefile_argument(parser)
|
||||
|
||||
if '--help' in args:
|
||||
parser.print_help()
|
||||
sys.exit(2)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
f = options.tracefile
|
||||
|
||||
rt0 = time.time()
|
||||
bycode = {} # map code to count of occurrences
|
||||
byinterval = {} # map code to count in current interval
|
||||
records = 0 # number of trace records read
|
||||
versions = 0 # number of trace records with versions
|
||||
datarecords = 0 # number of records with dlen set
|
||||
datasize = 0 # sum of dlen across records with dlen set
|
||||
oids = {} # map oid to number of times it was loaded
|
||||
bysize = {} # map data size to number of loads
|
||||
bysizew = {} # map data size to number of writes
|
||||
total_loads = 0
|
||||
t0 = None # first timestamp seen
|
||||
te = None # most recent timestamp seen
|
||||
h0 = None # timestamp at start of current interval
|
||||
he = None # timestamp at end of current interval
|
||||
thisinterval = None # generally te//interval
|
||||
f_read = f.read
|
||||
unpack = struct.unpack
|
||||
FMT = ">iiH8s8s"
|
||||
FMT_SIZE = struct.calcsize(FMT)
|
||||
assert FMT_SIZE == 26
|
||||
# Read file, gathering statistics, and printing each record if verbose.
|
||||
print(' '*16, "%7s %7s %7s %7s" % ('loads', 'hits', 'inv(h)', 'writes'), end=' ')
|
||||
print('hitrate')
|
||||
try:
|
||||
while 1:
|
||||
r = f_read(FMT_SIZE)
|
||||
if len(r) < FMT_SIZE:
|
||||
break
|
||||
ts, code, oidlen, start_tid, end_tid = unpack(FMT, r)
|
||||
if ts == 0:
|
||||
# Must be a misaligned record caused by a crash.
|
||||
if not options.quiet:
|
||||
print("Skipping 8 bytes at offset", f.tell() - FMT_SIZE)
|
||||
f.seek(f.tell() - FMT_SIZE + 8)
|
||||
continue
|
||||
oid = f_read(oidlen)
|
||||
if len(oid) < oidlen:
|
||||
break
|
||||
records += 1
|
||||
if t0 is None:
|
||||
t0 = ts
|
||||
thisinterval = t0 // options.interval
|
||||
h0 = he = ts
|
||||
te = ts
|
||||
if ts // options.interval != thisinterval:
|
||||
if not options.quiet:
|
||||
dumpbyinterval(byinterval, h0, he)
|
||||
byinterval = {}
|
||||
thisinterval = ts // options.interval
|
||||
h0 = ts
|
||||
he = ts
|
||||
dlen, code = (code & 0x7fffff00) >> 8, code & 0xff
|
||||
if dlen:
|
||||
datarecords += 1
|
||||
datasize += dlen
|
||||
if code & 0x80:
|
||||
version = 'V'
|
||||
versions += 1
|
||||
else:
|
||||
version = '-'
|
||||
code &= 0x7e
|
||||
bycode[code] = bycode.get(code, 0) + 1
|
||||
byinterval[code] = byinterval.get(code, 0) + 1
|
||||
if dlen:
|
||||
if code & 0x70 == 0x20: # All loads
|
||||
bysize[dlen] = d = bysize.get(dlen) or {}
|
||||
d[oid] = d.get(oid, 0) + 1
|
||||
elif code & 0x70 == 0x50: # All stores
|
||||
bysizew[dlen] = d = bysizew.get(dlen) or {}
|
||||
d[oid] = d.get(oid, 0) + 1
|
||||
if options.verbose:
|
||||
print("%s %02x %s %016x %016x %c%s" % (
|
||||
ctime(ts)[4:-5],
|
||||
code,
|
||||
oid_repr(oid),
|
||||
U64(start_tid),
|
||||
U64(end_tid),
|
||||
version,
|
||||
dlen and (' '+str(dlen)) or ""))
|
||||
if code & 0x70 == 0x20:
|
||||
oids[oid] = oids.get(oid, 0) + 1
|
||||
total_loads += 1
|
||||
elif code == 0x00: # restart
|
||||
if not options.quiet:
|
||||
dumpbyinterval(byinterval, h0, he)
|
||||
byinterval = {}
|
||||
thisinterval = ts // options.interval
|
||||
h0 = he = ts
|
||||
if not options.quiet:
|
||||
print(ctime(ts)[4:-5], end=' ')
|
||||
print('='*20, "Restart", '='*20)
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted. Stats so far:\n")
|
||||
|
||||
end_pos = f.tell()
|
||||
f.close()
|
||||
rte = time.time()
|
||||
if not options.quiet:
|
||||
dumpbyinterval(byinterval, h0, he)
|
||||
|
||||
# Error if nothing was read
|
||||
if not records:
|
||||
print("No records processed", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Print statistics
|
||||
if options.dostats:
|
||||
print()
|
||||
print("Read %s trace records (%s bytes) in %.1f seconds" % (
|
||||
addcommas(records), addcommas(end_pos), rte-rt0))
|
||||
print("Versions: %s records used a version" % addcommas(versions))
|
||||
print("First time: %s" % ctime(t0))
|
||||
print("Last time: %s" % ctime(te))
|
||||
print("Duration: %s seconds" % addcommas(te-t0))
|
||||
print("Data recs: %s (%.1f%%), average size %d bytes" % (
|
||||
addcommas(datarecords),
|
||||
100.0 * datarecords / records,
|
||||
datasize / datarecords))
|
||||
print("Hit rate: %.1f%% (load hits / loads)" % hitrate(bycode))
|
||||
print()
|
||||
codes = sorted(bycode.keys())
|
||||
print("%13s %4s %s" % ("Count", "Code", "Function (action)"))
|
||||
for code in codes:
|
||||
print("%13s %02x %s" % (
|
||||
addcommas(bycode.get(code, 0)),
|
||||
code,
|
||||
explain.get(code) or "*** unknown code ***"))
|
||||
|
||||
# Print histogram.
|
||||
if options.print_histogram:
|
||||
print()
|
||||
print("Histogram of object load frequency")
|
||||
total = len(oids)
|
||||
print("Unique oids: %s" % addcommas(total))
|
||||
print("Total loads: %s" % addcommas(total_loads))
|
||||
s = addcommas(total)
|
||||
width = max(len(s), len("objects"))
|
||||
fmt = "%5d %" + str(width) + "s %5.1f%% %5.1f%% %5.1f%%"
|
||||
hdr = "%5s %" + str(width) + "s %6s %6s %6s"
|
||||
print(hdr % ("loads", "objects", "%obj", "%load", "%cum"))
|
||||
cum = 0.0
|
||||
for binsize, count in histogram(oids):
|
||||
obj_percent = 100.0 * count / total
|
||||
load_percent = 100.0 * count * binsize / total_loads
|
||||
cum += load_percent
|
||||
print(fmt % (binsize, addcommas(count),
|
||||
obj_percent, load_percent, cum))
|
||||
|
||||
# Print size histogram.
|
||||
if options.print_size_histogram:
|
||||
print()
|
||||
print("Histograms of object sizes")
|
||||
print()
|
||||
dumpbysize(bysizew, "written", "writes")
|
||||
dumpbysize(bysize, "loaded", "loads")
|
||||
|
||||
def dumpbysize(bysize, how, how2):
|
||||
print()
|
||||
print("Unique sizes %s: %s" % (how, addcommas(len(bysize))))
|
||||
print("%10s %6s %6s" % ("size", "objs", how2))
|
||||
sizes = sorted(bysize.keys())
|
||||
for size in sizes:
|
||||
loads = 0
|
||||
for n in six.itervalues(bysize[size]):
|
||||
loads += n
|
||||
print("%10s %6d %6d" % (addcommas(size),
|
||||
len(bysize.get(size, "")),
|
||||
loads))
|
||||
|
||||
def dumpbyinterval(byinterval, h0, he):
|
||||
loads = hits = invals = writes = 0
|
||||
for code in byinterval:
|
||||
if code & 0x20:
|
||||
n = byinterval[code]
|
||||
loads += n
|
||||
if code in (0x22, 0x26):
|
||||
hits += n
|
||||
elif code & 0x40:
|
||||
writes += byinterval[code]
|
||||
elif code & 0x10:
|
||||
if code != 0x10:
|
||||
invals += byinterval[code]
|
||||
|
||||
if loads:
|
||||
hr = "%5.1f%%" % (100.0 * hits / loads)
|
||||
else:
|
||||
hr = 'n/a'
|
||||
|
||||
print("%s-%s %7s %7s %7s %7s %7s" % (
|
||||
ctime(h0)[4:-8], ctime(he)[14:-8],
|
||||
loads, hits, invals, writes, hr))
|
||||
|
||||
def hitrate(bycode):
|
||||
loads = hits = 0
|
||||
for code in bycode:
|
||||
if code & 0x70 == 0x20:
|
||||
n = bycode[code]
|
||||
loads += n
|
||||
if code in (0x22, 0x26):
|
||||
hits += n
|
||||
if loads:
|
||||
return 100.0 * hits / loads
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
def histogram(d):
|
||||
bins = {}
|
||||
for v in six.itervalues(d):
|
||||
bins[v] = bins.get(v, 0) + 1
|
||||
L = sorted(bins.items())
|
||||
return L
|
||||
|
||||
def U64(s):
|
||||
return struct.unpack(">Q", s)[0]
|
||||
|
||||
def oid_repr(oid):
|
||||
if isinstance(oid, six.binary_type) and len(oid) == 8:
|
||||
return '%16x' % U64(oid)
|
||||
else:
|
||||
return repr(oid)
|
||||
|
||||
def addcommas(n):
|
||||
sign, s = '', str(n)
|
||||
if s[0] == '-':
|
||||
sign, s = '-', s[1:]
|
||||
i = len(s) - 3
|
||||
while i > 0:
|
||||
s = s[:i] + ',' + s[i:]
|
||||
i -= 3
|
||||
return sign + s
|
||||
|
||||
explain = {
|
||||
# The first hex digit shows the operation, the second the outcome.
|
||||
# If the second digit is in "02468" then it is a 'miss'.
|
||||
# If it is in "ACE" then it is a 'hit'.
|
||||
|
||||
0x00: "_setup_trace (initialization)",
|
||||
|
||||
0x10: "invalidate (miss)",
|
||||
0x1A: "invalidate (hit, version)",
|
||||
0x1C: "invalidate (hit, saving non-current)",
|
||||
# 0x1E can occur during startup verification.
|
||||
0x1E: "invalidate (hit, discarding current or non-current)",
|
||||
|
||||
0x20: "load (miss)",
|
||||
0x22: "load (hit)",
|
||||
0x24: "load (non-current, miss)",
|
||||
0x26: "load (non-current, hit)",
|
||||
|
||||
0x50: "store (version)",
|
||||
0x52: "store (current, non-version)",
|
||||
0x54: "store (non-current)",
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python2.3
|
||||
|
||||
"""Parse the BLATHER logging generated by ZEO2.
|
||||
|
||||
An example of the log format is:
|
||||
2002-04-15T13:05:29 BLATHER(-100) ZEO Server storea(3235680, [714], 235339406490168806) ('10.0.26.30', 45514)
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
rx_time = re.compile('(\d\d\d\d-\d\d-\d\d)T(\d\d:\d\d:\d\d)')
|
||||
|
||||
def parse_time(line):
|
||||
"""Return the time portion of a zLOG line in seconds or None."""
|
||||
mo = rx_time.match(line)
|
||||
if mo is None:
|
||||
return None
|
||||
date, time_ = mo.group(1, 2)
|
||||
date_l = [int(elt) for elt in date.split('-')]
|
||||
time_l = [int(elt) for elt in time_.split(':')]
|
||||
return int(time.mktime(date_l + time_l + [0, 0, 0]))
|
||||
|
||||
rx_meth = re.compile("zrpc:\d+ calling (\w+)\((.*)")
|
||||
|
||||
def parse_method(line):
|
||||
pass
|
||||
|
||||
def parse_line(line):
|
||||
"""Parse a log entry and return time, method info, and client."""
|
||||
t = parse_time(line)
|
||||
if t is None:
|
||||
return None, None
|
||||
mo = rx_meth.search(line)
|
||||
if mo is None:
|
||||
return None, None
|
||||
meth_name = mo.group(1)
|
||||
meth_args = mo.group(2).strip()
|
||||
if meth_args.endswith(')'):
|
||||
meth_args = meth_args[:-1]
|
||||
meth_args = [s.strip() for s in meth_args.split(",")]
|
||||
m = meth_name, tuple(meth_args)
|
||||
return t, m
|
||||
|
||||
class TStats(object):
|
||||
|
||||
counter = 1
|
||||
|
||||
def __init__(self):
|
||||
self.id = TStats.counter
|
||||
TStats.counter += 1
|
||||
|
||||
fields = ("time", "vote", "done", "user", "path")
|
||||
fmt = "%-24s %5s %5s %-15s %s"
|
||||
hdr = fmt % fields
|
||||
|
||||
def report(self):
|
||||
"""Print a report about the transaction"""
|
||||
t = time.ctime(self.begin)
|
||||
if hasattr(self, "vote"):
|
||||
d_vote = self.vote - self.begin
|
||||
else:
|
||||
d_vote = "*"
|
||||
if hasattr(self, "finish"):
|
||||
d_finish = self.finish - self.begin
|
||||
else:
|
||||
d_finish = "*"
|
||||
print(self.fmt % (time.ctime(self.begin), d_vote, d_finish,
|
||||
self.user, self.url))
|
||||
|
||||
class TransactionParser(object):
|
||||
|
||||
def __init__(self):
|
||||
self.txns = {}
|
||||
self.skipped = 0
|
||||
|
||||
def parse(self, line):
|
||||
t, m = parse_line(line)
|
||||
if t is None:
|
||||
return
|
||||
name = m[0]
|
||||
meth = getattr(self, name, None)
|
||||
if meth is not None:
|
||||
meth(t, m[1])
|
||||
|
||||
def tpc_begin(self, time, args):
|
||||
t = TStats()
|
||||
t.begin = time
|
||||
t.user = args[1]
|
||||
t.url = args[2]
|
||||
t.objects = []
|
||||
tid = eval(args[0])
|
||||
self.txns[tid] = t
|
||||
|
||||
def get_txn(self, args):
|
||||
tid = eval(args[0])
|
||||
try:
|
||||
return self.txns[tid]
|
||||
except KeyError:
|
||||
print("uknown tid", repr(tid))
|
||||
return None
|
||||
|
||||
def tpc_finish(self, time, args):
|
||||
t = self.get_txn(args)
|
||||
if t is None:
|
||||
return
|
||||
t.finish = time
|
||||
|
||||
def vote(self, time, args):
|
||||
t = self.get_txn(args)
|
||||
if t is None:
|
||||
return
|
||||
t.vote = time
|
||||
|
||||
def get_txns(self):
|
||||
L = [(t.id, t) for t in self.txns.values()]
|
||||
L.sort()
|
||||
return [t for (id, t) in L]
|
||||
|
||||
if __name__ == "__main__":
|
||||
import fileinput
|
||||
|
||||
p = TransactionParser()
|
||||
i = 0
|
||||
for line in fileinput.input():
|
||||
i += 1
|
||||
try:
|
||||
p.parse(line)
|
||||
except:
|
||||
print("line", i)
|
||||
raise
|
||||
print("Transaction: %d" % len(p.txns))
|
||||
print(TStats.hdr)
|
||||
for txn in p.get_txns():
|
||||
txn.report()
|
||||
@@ -0,0 +1,29 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 doctest, re, unittest
|
||||
from zope.testing import renormalizing
|
||||
|
||||
def test_suite():
|
||||
return unittest.TestSuite((
|
||||
doctest.DocFileSuite(
|
||||
'zeopack.test',
|
||||
checker=renormalizing.RENormalizing([
|
||||
(re.compile('usage: Usage: '), 'Usage: '), # Py 2.4
|
||||
(re.compile('options:'), 'Options:'), # Py 2.4
|
||||
]),
|
||||
globs={'print_function': print_function},
|
||||
),
|
||||
))
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python2.3
|
||||
|
||||
"""Transaction timeout test script.
|
||||
|
||||
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.
|
||||
|
||||
usage: timeout.py address delay [storage-name]
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from ZODB.Connection import TransactionMetaData
|
||||
from ZODB.tests.MinPO import MinPO
|
||||
from ZODB.tests.StorageTestBase import zodb_pickle
|
||||
from ZEO.ClientStorage import ClientStorage
|
||||
|
||||
ZERO = '\0'*8
|
||||
|
||||
def main():
|
||||
if len(sys.argv) not in (3, 4):
|
||||
sys.stderr.write("Usage: timeout.py address delay [storage-name]\n" %
|
||||
sys.argv[0])
|
||||
sys.exit(2)
|
||||
|
||||
hostport = sys.argv[1]
|
||||
delay = float(sys.argv[2])
|
||||
if sys.argv[3:]:
|
||||
name = sys.argv[3]
|
||||
else:
|
||||
name = "1"
|
||||
|
||||
if "/" in hostport:
|
||||
address = hostport
|
||||
else:
|
||||
if ":" in hostport:
|
||||
i = hostport.index(":")
|
||||
host, port = hostport[:i], hostport[i+1:]
|
||||
else:
|
||||
host, port = "", hostport
|
||||
port = int(port)
|
||||
address = (host, port)
|
||||
|
||||
print("Connecting to %s..." % repr(address))
|
||||
storage = ClientStorage(address, name)
|
||||
print("Connected. Now starting a transaction...")
|
||||
|
||||
oid = storage.new_oid()
|
||||
revid = ZERO
|
||||
data = MinPO("timeout.py")
|
||||
pickled_data = zodb_pickle(data)
|
||||
t = TransactionMetaData()
|
||||
t.user = "timeout.py"
|
||||
storage.tpc_begin(t)
|
||||
storage.store(oid, revid, pickled_data, '', t)
|
||||
print("Stored. Now voting...")
|
||||
storage.tpc_vote(t)
|
||||
|
||||
print("Voted; now sleeping %s..." % delay)
|
||||
time.sleep(delay)
|
||||
print("Done.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python2.3
|
||||
|
||||
import logging
|
||||
import optparse
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import ZEO.ClientStorage
|
||||
from six.moves import map
|
||||
from six.moves import zip
|
||||
|
||||
usage = """Usage: %prog [options] [servers]
|
||||
|
||||
Pack one or more storages hosted by ZEO servers.
|
||||
|
||||
The positional arguments specify 0 or more tcp servers to pack, where
|
||||
each is of the form:
|
||||
|
||||
host:port[:name]
|
||||
|
||||
"""
|
||||
|
||||
WAIT = 10 # wait no more than 10 seconds for client to connect
|
||||
|
||||
def _main(args=None, prog=None):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
parser = optparse.OptionParser(usage, prog=prog)
|
||||
|
||||
parser.add_option(
|
||||
"-d", "--days", dest="days", type='int', default=0,
|
||||
help=("Pack objects that are older than this number of days")
|
||||
)
|
||||
|
||||
parser.add_option(
|
||||
"-t", "--time", dest="time",
|
||||
help=("Time of day to pack to of the form: HH[:MM[:SS]]. "
|
||||
"Defaults to current time.")
|
||||
)
|
||||
|
||||
parser.add_option(
|
||||
"-u", "--unix", dest="unix_sockets", action="append",
|
||||
help=("A unix-domain-socket server to connect to, of the form: "
|
||||
"path[:name]")
|
||||
)
|
||||
|
||||
parser.remove_option('-h')
|
||||
parser.add_option(
|
||||
"-h", dest="host",
|
||||
help=("Deprecated: "
|
||||
"Used with the -p and -S options, specified the host to "
|
||||
"connect to.")
|
||||
)
|
||||
|
||||
parser.add_option(
|
||||
"-p", type="int", dest="port",
|
||||
help=("Deprecated: "
|
||||
"Used with the -h and -S options, specifies "
|
||||
"the port to connect to.")
|
||||
)
|
||||
|
||||
parser.add_option(
|
||||
"-S", dest="name", default='1',
|
||||
help=("Deprecated: Used with the -h and -p, options, or with the "
|
||||
"-U option specified the storage name to use. Defaults to 1.")
|
||||
)
|
||||
|
||||
parser.add_option(
|
||||
"-U", dest="unix",
|
||||
help=("Deprecated: Used with the -S option, "
|
||||
"Unix-domain socket to connect to.")
|
||||
)
|
||||
|
||||
if not args:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
def error(message):
|
||||
sys.stderr.write("Error:\n%s\n" % message)
|
||||
sys.exit(1)
|
||||
|
||||
options, args = parser.parse_args(args)
|
||||
|
||||
packt = time.time()
|
||||
if options.time:
|
||||
time_ = list(map(int, options.time.split(':')))
|
||||
if len(time_) == 1:
|
||||
time_ += (0, 0)
|
||||
elif len(time_) == 2:
|
||||
time_ += (0,)
|
||||
elif len(time_) > 3:
|
||||
error("Invalid time value: %r" % options.time)
|
||||
|
||||
packt = time.localtime(packt)
|
||||
packt = time.mktime(packt[:3]+tuple(time_)+packt[6:])
|
||||
|
||||
packt -= options.days * 86400
|
||||
|
||||
servers = []
|
||||
|
||||
if options.host:
|
||||
if not options.port:
|
||||
error("If host (-h) is specified then a port (-p) must be "
|
||||
"specified as well.")
|
||||
servers.append(((options.host, options.port), options.name))
|
||||
elif options.port:
|
||||
servers.append(((socket.gethostname(), options.port), options.name))
|
||||
|
||||
if options.unix:
|
||||
servers.append((options.unix, options.name))
|
||||
|
||||
for server in args:
|
||||
data = server.split(':')
|
||||
if len(data) in (2, 3):
|
||||
host = data[0]
|
||||
try:
|
||||
port = int(data[1])
|
||||
except ValueError:
|
||||
error("Invalid port in server specification: %r" % server)
|
||||
addr = host, port
|
||||
if len(data) == 2:
|
||||
name = '1'
|
||||
else:
|
||||
name = data[2]
|
||||
else:
|
||||
error("Invalid server specification: %r" % server)
|
||||
|
||||
servers.append((addr, name))
|
||||
|
||||
for server in options.unix_sockets or ():
|
||||
data = server.split(':')
|
||||
if len(data) == 1:
|
||||
addr = data[0]
|
||||
name = '1'
|
||||
elif len(data) == 2:
|
||||
addr = data[0]
|
||||
name = data[1]
|
||||
else:
|
||||
error("Invalid server specification: %r" % server)
|
||||
|
||||
servers.append((addr, name))
|
||||
|
||||
if not servers:
|
||||
error("No servers specified.")
|
||||
|
||||
for addr, name in servers:
|
||||
try:
|
||||
cs = ZEO.ClientStorage.ClientStorage(
|
||||
addr, storage=name, wait=False, read_only=1)
|
||||
for i in range(60):
|
||||
if cs.is_connected():
|
||||
break
|
||||
time.sleep(1)
|
||||
else:
|
||||
sys.stderr.write("Couldn't connect to: %r\n"
|
||||
% ((addr, name), ))
|
||||
cs.close()
|
||||
continue
|
||||
cs.pack(packt, wait=True)
|
||||
cs.close()
|
||||
except:
|
||||
traceback.print_exception(*(sys.exc_info()+(99, sys.stderr)))
|
||||
error("Error packing storage %s in %r" % (name, addr))
|
||||
|
||||
def main(*args):
|
||||
root_logger = logging.getLogger()
|
||||
old_level = root_logger.getEffectiveLevel()
|
||||
logging.getLogger().setLevel(logging.WARNING)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(logging.Formatter(
|
||||
"%(name)s %(levelname)s %(message)s"))
|
||||
logging.getLogger().addHandler(handler)
|
||||
try:
|
||||
_main(*args)
|
||||
finally:
|
||||
logging.getLogger().setLevel(old_level)
|
||||
logging.getLogger().removeHandler(handler)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
zeopack
|
||||
=======
|
||||
|
||||
The zeopack script can be used to pack one or more storages. It uses
|
||||
ClientStorage to do this. To test it's behavior, we'll replace the
|
||||
normal ClientStorage with a fake one that echos information we'll want
|
||||
for our test:
|
||||
|
||||
>>> class ClientStorage:
|
||||
... connect_wait = 0
|
||||
... def __init__(self, *args, **kw):
|
||||
... if args[0] == 'bad':
|
||||
... import logging
|
||||
... logging.getLogger('test.ClientStorage').error(
|
||||
... "I hate this address, %r", args[0])
|
||||
... raise ValueError("Bad address")
|
||||
... print("ClientStorage(%s %s)" % (
|
||||
... repr(args)[1:-1],
|
||||
... ', '.join("%s=%r" % i for i in sorted(kw.items())),
|
||||
... ))
|
||||
... def pack(self, t=None, *args, **kw):
|
||||
... now = time.localtime(time.time())
|
||||
... local_midnight = time.mktime(now[:3]+(0, 0, 0)+now[6:])
|
||||
... t -= local_midnight # adjust for tz
|
||||
... t += 86400*7 # add a week to make sure we're positive
|
||||
... print("pack(%r,%s %s)" % (
|
||||
... t, repr(args)[1:-1],
|
||||
... ', '.join("%s=%r" % i for i in sorted(kw.items())),
|
||||
... ))
|
||||
... def is_connected(self):
|
||||
... self.connect_wait -= 1
|
||||
... print('is_connected', self.connect_wait < 0)
|
||||
... return self.connect_wait < 0
|
||||
... def close(self):
|
||||
... print("close()")
|
||||
|
||||
>>> import ZEO
|
||||
>>> ClientStorage_orig = ZEO.ClientStorage.ClientStorage
|
||||
>>> ZEO.ClientStorage.ClientStorage = ClientStorage
|
||||
|
||||
Now, we're ready to try the script:
|
||||
|
||||
>>> from ZEO.scripts.zeopack import main
|
||||
|
||||
If we call it with no arguments, we get help:
|
||||
|
||||
>>> import os; os.environ['COLUMNS'] = '80' # for consistent optparse output
|
||||
>>> main([], 'zeopack')
|
||||
Usage: zeopack [options] [servers]
|
||||
<BLANKLINE>
|
||||
Pack one or more storages hosted by ZEO servers.
|
||||
<BLANKLINE>
|
||||
The positional arguments specify 0 or more tcp servers to pack, where
|
||||
each is of the form:
|
||||
<BLANKLINE>
|
||||
host:port[:name]
|
||||
<BLANKLINE>
|
||||
<BLANKLINE>
|
||||
<BLANKLINE>
|
||||
Options:
|
||||
-d DAYS, --days=DAYS Pack objects that are older than this number of days
|
||||
-t TIME, --time=TIME Time of day to pack to of the form: HH[:MM[:SS]].
|
||||
Defaults to current time.
|
||||
-u UNIX_SOCKETS, --unix=UNIX_SOCKETS
|
||||
A unix-domain-socket server to connect to, of the
|
||||
form: path[:name]
|
||||
-h HOST Deprecated: Used with the -p and -S options, specified
|
||||
the host to connect to.
|
||||
-p PORT Deprecated: Used with the -h and -S options, specifies
|
||||
the port to connect to.
|
||||
-S NAME Deprecated: Used with the -h and -p, options, or with
|
||||
the -U option specified the storage name to use.
|
||||
Defaults to 1.
|
||||
-U UNIX Deprecated: Used with the -S option, Unix-domain
|
||||
socket to connect to.
|
||||
|
||||
Since packing involves time, we'd better have our way with it. Replace
|
||||
time.time() with a function that always returns the same value. The
|
||||
value is timezone dependent.
|
||||
|
||||
>>> import time
|
||||
>>> time_orig = time.time
|
||||
>>> time.time = lambda : time.mktime((2009, 3, 24, 10, 55, 17, 1, 83, -1))
|
||||
>>> sleep_orig = time.sleep
|
||||
>>> def sleep(t):
|
||||
... print('sleep(%r)' % t)
|
||||
>>> time.sleep = sleep
|
||||
|
||||
Normally, we pass one or more TCP server specifications:
|
||||
|
||||
>>> main(["host1:8100", "host1:8100:2"])
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='1', wait=False)
|
||||
is_connected True
|
||||
pack(644117.0, wait=True)
|
||||
close()
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='2', wait=False)
|
||||
is_connected True
|
||||
pack(644117.0, wait=True)
|
||||
close()
|
||||
|
||||
We can also pass unix-domain-sockey servers using the -u option:
|
||||
|
||||
>>> main(["-ufoo", "-ubar:spam", "host1:8100", "host1:8100:2"])
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='1', wait=False)
|
||||
is_connected True
|
||||
pack(644117.0, wait=True)
|
||||
close()
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='2', wait=False)
|
||||
is_connected True
|
||||
pack(644117.0, wait=True)
|
||||
close()
|
||||
ClientStorage('foo', read_only=1, storage='1', wait=False)
|
||||
is_connected True
|
||||
pack(644117.0, wait=True)
|
||||
close()
|
||||
ClientStorage('bar', read_only=1, storage='spam', wait=False)
|
||||
is_connected True
|
||||
pack(644117.0, wait=True)
|
||||
close()
|
||||
|
||||
The -d option causes a pack time the given number of days earlier to
|
||||
be used:
|
||||
|
||||
>>> main(["-ufoo", "-ubar:spam", "-d3", "host1:8100", "host1:8100:2"])
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='1', wait=False)
|
||||
is_connected True
|
||||
pack(384917.0, wait=True)
|
||||
close()
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='2', wait=False)
|
||||
is_connected True
|
||||
pack(384917.0, wait=True)
|
||||
close()
|
||||
ClientStorage('foo', read_only=1, storage='1', wait=False)
|
||||
is_connected True
|
||||
pack(384917.0, wait=True)
|
||||
close()
|
||||
ClientStorage('bar', read_only=1, storage='spam', wait=False)
|
||||
is_connected True
|
||||
pack(384917.0, wait=True)
|
||||
close()
|
||||
|
||||
The -t option allows us to control the time of day:
|
||||
|
||||
>>> main(["-ufoo", "-d3", "-t1:30", "host1:8100:2"])
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='2', wait=False)
|
||||
is_connected True
|
||||
pack(351000.0, wait=True)
|
||||
close()
|
||||
ClientStorage('foo', read_only=1, storage='1', wait=False)
|
||||
is_connected True
|
||||
pack(351000.0, wait=True)
|
||||
close()
|
||||
|
||||
Connection timeout
|
||||
------------------
|
||||
|
||||
The zeopack script tells ClientStorage not to wait for connections
|
||||
before returning from the constructor, but will time out after 60
|
||||
seconds of waiting for a connect.
|
||||
|
||||
>>> ClientStorage.connect_wait = 3
|
||||
>>> main(["-d3", "-t1:30", "host1:8100:2"])
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='2', wait=False)
|
||||
is_connected False
|
||||
sleep(1)
|
||||
is_connected False
|
||||
sleep(1)
|
||||
is_connected False
|
||||
sleep(1)
|
||||
is_connected True
|
||||
pack(351000.0, wait=True)
|
||||
close()
|
||||
|
||||
>>> def call_main(args):
|
||||
... import sys
|
||||
... old_stderr = sys.stderr
|
||||
... sys.stderr = sys.stdout
|
||||
... try:
|
||||
... try:
|
||||
... main(args)
|
||||
... except SystemExit as v:
|
||||
... print("Exited", v)
|
||||
... finally:
|
||||
... sys.stderr = old_stderr
|
||||
|
||||
>>> ClientStorage.connect_wait = 999
|
||||
>>> call_main(["-d3", "-t1:30", "host1:8100", "host1:8100:2"])
|
||||
... # doctest: +ELLIPSIS
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='1', wait=False)
|
||||
is_connected False
|
||||
sleep(1)
|
||||
...
|
||||
is_connected False
|
||||
sleep(1)
|
||||
Couldn't connect to: (('host1', 8100), '1')
|
||||
close()
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='2', wait=False)
|
||||
is_connected False
|
||||
sleep(1)
|
||||
...
|
||||
is_connected False
|
||||
sleep(1)
|
||||
Couldn't connect to: (('host1', 8100), '2')
|
||||
close()
|
||||
|
||||
>>> ClientStorage.connect_wait = 0
|
||||
|
||||
|
||||
Legacy support
|
||||
--------------
|
||||
|
||||
>>> main(["-d3", "-h", "host1", "-p", "8100", "-S", "2"])
|
||||
ClientStorage(('host1', 8100), read_only=1, storage='2', wait=False)
|
||||
is_connected True
|
||||
pack(384917.0, wait=True)
|
||||
close()
|
||||
|
||||
>>> import socket
|
||||
>>> old_gethostname = socket.gethostname
|
||||
>>> socket.gethostname = lambda : 'test.host.com'
|
||||
>>> main(["-d3", "-p", "8100"])
|
||||
ClientStorage(('test.host.com', 8100), read_only=1, storage='1', wait=False)
|
||||
is_connected True
|
||||
pack(384917.0, wait=True)
|
||||
close()
|
||||
>>> socket.gethostname = old_gethostname
|
||||
|
||||
>>> main(["-d3", "-U", "foo/bar", "-S", "2"])
|
||||
ClientStorage('foo/bar', read_only=1, storage='2', wait=False)
|
||||
is_connected True
|
||||
pack(384917.0, wait=True)
|
||||
close()
|
||||
|
||||
Error handling
|
||||
--------------
|
||||
|
||||
>>> call_main(["-d3"])
|
||||
Error:
|
||||
No servers specified.
|
||||
Exited 1
|
||||
|
||||
>>> call_main(["-d3", "a"])
|
||||
Error:
|
||||
Invalid server specification: 'a'
|
||||
Exited 1
|
||||
|
||||
>>> call_main(["-d3", "a:b:c:d"])
|
||||
Error:
|
||||
Invalid server specification: 'a:b:c:d'
|
||||
Exited 1
|
||||
|
||||
>>> call_main(["-d3", "a:b:2"])
|
||||
Error:
|
||||
Invalid port in server specification: 'a:b:2'
|
||||
Exited 1
|
||||
|
||||
>>> call_main(["-d3", "-u", "a:b:2"])
|
||||
Error:
|
||||
Invalid server specification: 'a:b:2'
|
||||
Exited 1
|
||||
|
||||
>>> call_main(["-d3", "-u", "bad"]) # doctest: +ELLIPSIS
|
||||
test.ClientStorage ERROR I hate this address, 'bad'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Bad address
|
||||
Error:
|
||||
Error packing storage 1 in 'bad'
|
||||
Exited 1
|
||||
|
||||
|
||||
|
||||
Note that in the previous example, the first line was output through logging.
|
||||
|
||||
.. tear down
|
||||
|
||||
>>> ZEO.ClientStorage.ClientStorage = ClientStorage_orig
|
||||
>>> time.time = time_orig
|
||||
>>> time.sleep = sleep_orig
|
||||
@@ -0,0 +1,396 @@
|
||||
#!/usr/bin/env python2.3
|
||||
|
||||
"""Report on the number of currently waiting clients in the ZEO queue.
|
||||
|
||||
Usage: %(PROGRAM)s [options] logfile
|
||||
|
||||
Options:
|
||||
-h / --help
|
||||
Print this help text and exit.
|
||||
|
||||
-v / --verbose
|
||||
Verbose output
|
||||
|
||||
-f file
|
||||
--file file
|
||||
Use the specified file to store the incremental state as a pickle. If
|
||||
not given, %(STATEFILE)s is used.
|
||||
|
||||
-r / --reset
|
||||
Reset the state of the tool. This blows away any existing state
|
||||
pickle file and then exits -- it does not parse the file. Use this
|
||||
when you rotate log files so that the next run will parse from the
|
||||
beginning of the file.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import errno
|
||||
import getopt
|
||||
from ZEO._compat import load, dump
|
||||
|
||||
COMMASPACE = ', '
|
||||
STATEFILE = 'zeoqueue.pck'
|
||||
PROGRAM = sys.argv[0]
|
||||
|
||||
|
||||
|
||||
tcre = re.compile(r"""
|
||||
(?P<ymd>
|
||||
\d{4}- # year
|
||||
\d{2}- # month
|
||||
\d{2}) # day
|
||||
T # separator
|
||||
(?P<hms>
|
||||
\d{2}: # hour
|
||||
\d{2}: # minute
|
||||
\d{2}) # second
|
||||
""", re.VERBOSE)
|
||||
|
||||
ccre = re.compile(r"""
|
||||
zrpc-conn:(?P<addr>\d+.\d+.\d+.\d+:\d+)\s+
|
||||
calling\s+
|
||||
(?P<method>
|
||||
\w+) # the method
|
||||
\( # args open paren
|
||||
\' # string quote start
|
||||
(?P<tid>
|
||||
\S+) # first argument -- usually the tid
|
||||
\' # end of string
|
||||
(?P<rest>
|
||||
.*) # rest of line
|
||||
""", re.VERBOSE)
|
||||
|
||||
wcre = re.compile(r'Clients waiting: (?P<num>\d+)')
|
||||
|
||||
|
||||
|
||||
def parse_time(line):
|
||||
"""Return the time portion of a zLOG line in seconds or None."""
|
||||
mo = tcre.match(line)
|
||||
if mo is None:
|
||||
return None
|
||||
date, time_ = mo.group('ymd', 'hms')
|
||||
date_l = [int(elt) for elt in date.split('-')]
|
||||
time_l = [int(elt) for elt in time_.split(':')]
|
||||
return int(time.mktime(date_l + time_l + [0, 0, 0]))
|
||||
|
||||
|
||||
class Txn(object):
|
||||
"""Track status of single transaction."""
|
||||
def __init__(self, tid):
|
||||
self.tid = tid
|
||||
self.hint = None
|
||||
self.begin = None
|
||||
self.vote = None
|
||||
self.abort = None
|
||||
self.finish = None
|
||||
self.voters = []
|
||||
|
||||
def isactive(self):
|
||||
if self.begin and not (self.abort or self.finish):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class Status(object):
|
||||
"""Track status of ZEO server by replaying log records.
|
||||
|
||||
We want to keep track of several events:
|
||||
|
||||
- The last committed transaction.
|
||||
- The last committed or aborted transaction.
|
||||
- The last transaction that got the lock but didn't finish.
|
||||
- The client address doing the first vote of a transaction.
|
||||
- The number of currently active transactions.
|
||||
- The number of reported queued transactions.
|
||||
- Client restarts.
|
||||
- Number of current connections (but this might not be useful).
|
||||
|
||||
We can observe these events by reading the following sorts of log
|
||||
entries:
|
||||
|
||||
2002-12-16T06:16:05 BLATHER(-100) zrpc:12649 calling
|
||||
tpc_begin('\x03I\x90((\xdbp\xd5', '', 'QueueCatal...
|
||||
|
||||
2002-12-16T06:16:06 BLATHER(-100) zrpc:12649 calling
|
||||
vote('\x03I\x90((\xdbp\xd5')
|
||||
|
||||
2002-12-16T06:16:06 BLATHER(-100) zrpc:12649 calling
|
||||
tpc_finish('\x03I\x90((\xdbp\xd5')
|
||||
|
||||
2002-12-16T10:46:10 INFO(0) ZSS:12649:1 Transaction blocked waiting
|
||||
for storage. Clients waiting: 1.
|
||||
|
||||
2002-12-16T06:15:57 BLATHER(-100) zrpc:12649 connect from
|
||||
('10.0.26.54', 48983): <ManagedServerConnection ('10.0.26.54', 48983)>
|
||||
|
||||
2002-12-16T10:30:09 INFO(0) ZSS:12649:1 disconnected
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.lineno = 0
|
||||
self.pos = 0
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.commit = None
|
||||
self.commit_or_abort = None
|
||||
self.last_unfinished = None
|
||||
self.n_active = 0
|
||||
self.n_blocked = 0
|
||||
self.n_conns = 0
|
||||
self.t_restart = None
|
||||
self.txns = {}
|
||||
|
||||
def iscomplete(self):
|
||||
# The status report will always be complete if we encounter an
|
||||
# explicit restart.
|
||||
if self.t_restart is not None:
|
||||
return True
|
||||
# If we haven't seen a restart, assume that seeing a finished
|
||||
# transaction is good enough.
|
||||
return self.commit is not None
|
||||
|
||||
def process_file(self, fp):
|
||||
if self.pos:
|
||||
if VERBOSE:
|
||||
print('seeking to file position', self.pos)
|
||||
fp.seek(self.pos)
|
||||
while True:
|
||||
line = fp.readline()
|
||||
if not line:
|
||||
break
|
||||
self.lineno += 1
|
||||
self.process(line)
|
||||
self.pos = fp.tell()
|
||||
|
||||
def process(self, line):
|
||||
if line.find("calling") != -1:
|
||||
self.process_call(line)
|
||||
elif line.find("connect") != -1:
|
||||
self.process_connect(line)
|
||||
# test for "locked" because word may start with "B" or "b"
|
||||
elif line.find("locked") != -1:
|
||||
self.process_block(line)
|
||||
elif line.find("Starting") != -1:
|
||||
self.process_start(line)
|
||||
|
||||
def process_call(self, line):
|
||||
mo = ccre.search(line)
|
||||
if mo is None:
|
||||
return
|
||||
called_method = mo.group('method')
|
||||
# Exit early if we've got zeoLoad, because it's the most
|
||||
# frequently called method and we don't use it.
|
||||
if called_method == "zeoLoad":
|
||||
return
|
||||
t = parse_time(line)
|
||||
meth = getattr(self, "call_%s" % called_method, None)
|
||||
if meth is None:
|
||||
return
|
||||
client = mo.group('addr')
|
||||
tid = mo.group('tid')
|
||||
rest = mo.group('rest')
|
||||
meth(t, client, tid, rest)
|
||||
|
||||
def process_connect(self, line):
|
||||
pass
|
||||
|
||||
def process_block(self, line):
|
||||
mo = wcre.search(line)
|
||||
if mo is None:
|
||||
# assume that this was a restart message for the last blocked
|
||||
# transaction.
|
||||
self.n_blocked = 0
|
||||
else:
|
||||
self.n_blocked = int(mo.group('num'))
|
||||
|
||||
def process_start(self, line):
|
||||
if line.find("Starting ZEO server") != -1:
|
||||
self.reset()
|
||||
self.t_restart = parse_time(line)
|
||||
|
||||
def call_tpc_begin(self, t, client, tid, rest):
|
||||
txn = Txn(tid)
|
||||
txn.begin = t
|
||||
if rest[0] == ',':
|
||||
i = 1
|
||||
while rest[i].isspace():
|
||||
i += 1
|
||||
rest = rest[i:]
|
||||
txn.hint = rest
|
||||
self.txns[tid] = txn
|
||||
self.n_active += 1
|
||||
self.last_unfinished = txn
|
||||
|
||||
def call_vote(self, t, client, tid, rest):
|
||||
txn = self.txns.get(tid)
|
||||
if txn is None:
|
||||
print("Oops!")
|
||||
txn = self.txns[tid] = Txn(tid)
|
||||
txn.vote = t
|
||||
txn.voters.append(client)
|
||||
|
||||
def call_tpc_abort(self, t, client, tid, rest):
|
||||
txn = self.txns.get(tid)
|
||||
if txn is None:
|
||||
print("Oops!")
|
||||
txn = self.txns[tid] = Txn(tid)
|
||||
txn.abort = t
|
||||
txn.voters = []
|
||||
self.n_active -= 1
|
||||
if self.commit_or_abort:
|
||||
# delete the old transaction
|
||||
try:
|
||||
del self.txns[self.commit_or_abort.tid]
|
||||
except KeyError:
|
||||
pass
|
||||
self.commit_or_abort = txn
|
||||
|
||||
def call_tpc_finish(self, t, client, tid, rest):
|
||||
txn = self.txns.get(tid)
|
||||
if txn is None:
|
||||
print("Oops!")
|
||||
txn = self.txns[tid] = Txn(tid)
|
||||
txn.finish = t
|
||||
txn.voters = []
|
||||
self.n_active -= 1
|
||||
if self.commit:
|
||||
# delete the old transaction
|
||||
try:
|
||||
del self.txns[self.commit.tid]
|
||||
except KeyError:
|
||||
pass
|
||||
if self.commit_or_abort:
|
||||
# delete the old transaction
|
||||
try:
|
||||
del self.txns[self.commit_or_abort.tid]
|
||||
except KeyError:
|
||||
pass
|
||||
self.commit = self.commit_or_abort = txn
|
||||
|
||||
def report(self):
|
||||
print("Blocked transactions:", self.n_blocked)
|
||||
if not VERBOSE:
|
||||
return
|
||||
if self.t_restart:
|
||||
print("Server started:", time.ctime(self.t_restart))
|
||||
|
||||
if self.commit is not None:
|
||||
t = self.commit_or_abort.finish
|
||||
if t is None:
|
||||
t = self.commit_or_abort.abort
|
||||
print("Last finished transaction:", time.ctime(t))
|
||||
|
||||
# the blocked transaction should be the first one that calls vote
|
||||
L = [(txn.begin, txn) for txn in self.txns.values()]
|
||||
L.sort()
|
||||
|
||||
for x, txn in L:
|
||||
if txn.isactive():
|
||||
began = txn.begin
|
||||
if txn.voters:
|
||||
print("Blocked client (first vote):", txn.voters[0])
|
||||
print("Blocked transaction began at:", time.ctime(began))
|
||||
print("Hint:", txn.hint)
|
||||
print("Idle time: %d sec" % int(time.time() - began))
|
||||
break
|
||||
|
||||
|
||||
|
||||
def usage(code, msg=''):
|
||||
print(__doc__ % globals(), file=sys.stderr)
|
||||
if msg:
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def main():
|
||||
global VERBOSE
|
||||
|
||||
VERBOSE = 0
|
||||
file = STATEFILE
|
||||
reset = False
|
||||
# -0 is a secret option used for testing purposes only
|
||||
seek = True
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], 'vhf:r0',
|
||||
['help', 'verbose', 'file=', 'reset'])
|
||||
except getopt.error as msg:
|
||||
usage(1, msg)
|
||||
|
||||
for opt, arg in opts:
|
||||
if opt in ('-h', '--help'):
|
||||
usage(0)
|
||||
elif opt in ('-v', '--verbose'):
|
||||
VERBOSE += 1
|
||||
elif opt in ('-f', '--file'):
|
||||
file = arg
|
||||
elif opt in ('-r', '--reset'):
|
||||
reset = True
|
||||
elif opt == '-0':
|
||||
seek = False
|
||||
|
||||
if reset:
|
||||
# Blow away the existing state file and exit
|
||||
try:
|
||||
os.unlink(file)
|
||||
if VERBOSE:
|
||||
print('removing pickle state file', file)
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
return
|
||||
|
||||
if not args:
|
||||
usage(1, 'logfile is required')
|
||||
if len(args) > 1:
|
||||
usage(1, 'too many arguments: %s' % COMMASPACE.join(args))
|
||||
|
||||
path = args[0]
|
||||
|
||||
# Get the previous status object from the pickle file, if it is available
|
||||
# and if the --reset flag wasn't given.
|
||||
status = None
|
||||
try:
|
||||
statefp = open(file, 'rb')
|
||||
try:
|
||||
status = load(statefp)
|
||||
if VERBOSE:
|
||||
print('reading status from file', file)
|
||||
finally:
|
||||
statefp.close()
|
||||
except IOError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
if status is None:
|
||||
status = Status()
|
||||
if VERBOSE:
|
||||
print('using new status')
|
||||
|
||||
if not seek:
|
||||
status.pos = 0
|
||||
|
||||
fp = open(path, 'rb')
|
||||
try:
|
||||
status.process_file(fp)
|
||||
finally:
|
||||
fp.close()
|
||||
# Save state
|
||||
statefp = open(file, 'wb')
|
||||
dump(status, statefp, 1)
|
||||
statefp.close()
|
||||
# Print the report and return the number of blocked clients in the exit
|
||||
# status code.
|
||||
status.report()
|
||||
sys.exit(status.n_blocked)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python2.3
|
||||
|
||||
"""Parse the BLATHER logging generated by ZEO, and optionally replay it.
|
||||
|
||||
Usage: zeointervals.py [options]
|
||||
|
||||
Options:
|
||||
|
||||
--help / -h
|
||||
Print this message and exit.
|
||||
|
||||
--replay=storage
|
||||
-r storage
|
||||
Replay the parsed transactions through the new storage
|
||||
|
||||
--maxtxn=count
|
||||
-m count
|
||||
Parse no more than count transactions.
|
||||
|
||||
--report / -p
|
||||
Print a report as we're parsing.
|
||||
|
||||
Unlike parsezeolog.py, this script generates timestamps for each transaction,
|
||||
and sub-command in the transaction. We can use this to compare timings with
|
||||
synthesized data.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import getopt
|
||||
import operator
|
||||
# ZEO logs measure wall-clock time so for consistency we need to do the same
|
||||
#from time import clock as now
|
||||
from time import time as now
|
||||
|
||||
from ZODB.FileStorage import FileStorage
|
||||
#from BDBStorage.BDBFullStorage import BDBFullStorage
|
||||
#from Standby.primary import PrimaryStorage
|
||||
#from Standby.config import RS_PORT
|
||||
from ZODB.Connection import TransactionMetaData
|
||||
from ZODB.utils import p64
|
||||
from functools import reduce
|
||||
|
||||
datecre = re.compile('(\d\d\d\d-\d\d-\d\d)T(\d\d:\d\d:\d\d)')
|
||||
methcre = re.compile("ZEO Server (\w+)\((.*)\) \('(.*)', (\d+)")
|
||||
|
||||
class StopParsing(Exception):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def usage(code, msg=''):
|
||||
print(__doc__)
|
||||
if msg:
|
||||
print(msg)
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
|
||||
def parse_time(line):
|
||||
"""Return the time portion of a zLOG line in seconds or None."""
|
||||
mo = datecre.match(line)
|
||||
if mo is None:
|
||||
return None
|
||||
date, time_ = mo.group(1, 2)
|
||||
date_l = [int(elt) for elt in date.split('-')]
|
||||
time_l = [int(elt) for elt in time_.split(':')]
|
||||
return int(time.mktime(date_l + time_l + [0, 0, 0]))
|
||||
|
||||
|
||||
def parse_line(line):
|
||||
"""Parse a log entry and return time, method info, and client."""
|
||||
t = parse_time(line)
|
||||
if t is None:
|
||||
return None, None, None
|
||||
mo = methcre.search(line)
|
||||
if mo is None:
|
||||
return None, None, None
|
||||
meth_name = mo.group(1)
|
||||
meth_args = mo.group(2)
|
||||
meth_args = [s.strip() for s in meth_args.split(',')]
|
||||
m = meth_name, tuple(meth_args)
|
||||
c = mo.group(3), mo.group(4)
|
||||
return t, m, c
|
||||
|
||||
|
||||
|
||||
class StoreStat(object):
|
||||
def __init__(self, when, oid, size):
|
||||
self.when = when
|
||||
self.oid = oid
|
||||
self.size = size
|
||||
|
||||
# Crufty
|
||||
def __getitem__(self, i):
|
||||
if i == 0: return self.oid
|
||||
if i == 1: return self.size
|
||||
raise IndexError
|
||||
|
||||
|
||||
class TxnStat(object):
|
||||
def __init__(self):
|
||||
self._begintime = None
|
||||
self._finishtime = None
|
||||
self._aborttime = None
|
||||
self._url = None
|
||||
self._objects = []
|
||||
|
||||
def tpc_begin(self, when, args, client):
|
||||
self._begintime = when
|
||||
# args are txnid, user, description (looks like it's always a url)
|
||||
self._url = args[2]
|
||||
|
||||
def storea(self, when, args, client):
|
||||
oid = int(args[0])
|
||||
# args[1] is "[numbytes]"
|
||||
size = int(args[1][1:-1])
|
||||
s = StoreStat(when, oid, size)
|
||||
self._objects.append(s)
|
||||
|
||||
def tpc_abort(self, when):
|
||||
self._aborttime = when
|
||||
|
||||
def tpc_finish(self, when):
|
||||
self._finishtime = when
|
||||
|
||||
|
||||
|
||||
# Mapping oid -> revid
|
||||
_revids = {}
|
||||
|
||||
class ReplayTxn(TxnStat):
|
||||
def __init__(self, storage):
|
||||
self._storage = storage
|
||||
self._replaydelta = 0
|
||||
TxnStat.__init__(self)
|
||||
|
||||
def replay(self):
|
||||
ZERO = '\0'*8
|
||||
t0 = now()
|
||||
t = TransactionMetaData()
|
||||
self._storage.tpc_begin(t)
|
||||
for obj in self._objects:
|
||||
oid = obj.oid
|
||||
revid = _revids.get(oid, ZERO)
|
||||
# BAW: simulate a pickle of the given size
|
||||
data = 'x' * obj.size
|
||||
# BAW: ignore versions for now
|
||||
newrevid = self._storage.store(p64(oid), revid, data, '', t)
|
||||
_revids[oid] = newrevid
|
||||
if self._aborttime:
|
||||
self._storage.tpc_abort(t)
|
||||
origdelta = self._aborttime - self._begintime
|
||||
else:
|
||||
self._storage.tpc_vote(t)
|
||||
self._storage.tpc_finish(t)
|
||||
origdelta = self._finishtime - self._begintime
|
||||
t1 = now()
|
||||
# Shows how many seconds behind (positive) or ahead (negative) of the
|
||||
# original reply our local update took
|
||||
self._replaydelta = t1 - t0 - origdelta
|
||||
|
||||
|
||||
|
||||
class ZEOParser(object):
|
||||
def __init__(self, maxtxns=-1, report=1, storage=None):
|
||||
self.__txns = []
|
||||
self.__curtxn = {}
|
||||
self.__skipped = 0
|
||||
self.__maxtxns = maxtxns
|
||||
self.__finishedtxns = 0
|
||||
self.__report = report
|
||||
self.__storage = storage
|
||||
|
||||
def parse(self, line):
|
||||
t, m, c = parse_line(line)
|
||||
if t is None:
|
||||
# Skip this line
|
||||
return
|
||||
name = m[0]
|
||||
meth = getattr(self, name, None)
|
||||
if meth is not None:
|
||||
meth(t, m[1], c)
|
||||
|
||||
def tpc_begin(self, when, args, client):
|
||||
txn = ReplayTxn(self.__storage)
|
||||
self.__curtxn[client] = txn
|
||||
meth = getattr(txn, 'tpc_begin', None)
|
||||
if meth is not None:
|
||||
meth(when, args, client)
|
||||
|
||||
def storea(self, when, args, client):
|
||||
txn = self.__curtxn.get(client)
|
||||
if txn is None:
|
||||
self.__skipped += 1
|
||||
return
|
||||
meth = getattr(txn, 'storea', None)
|
||||
if meth is not None:
|
||||
meth(when, args, client)
|
||||
|
||||
def tpc_finish(self, when, args, client):
|
||||
txn = self.__curtxn.get(client)
|
||||
if txn is None:
|
||||
self.__skipped += 1
|
||||
return
|
||||
meth = getattr(txn, 'tpc_finish', None)
|
||||
if meth is not None:
|
||||
meth(when)
|
||||
if self.__report:
|
||||
self.report(txn)
|
||||
self.__txns.append(txn)
|
||||
self.__curtxn[client] = None
|
||||
self.__finishedtxns += 1
|
||||
if self.__maxtxns > 0 and self.__finishedtxns >= self.__maxtxns:
|
||||
raise StopParsing
|
||||
|
||||
def report(self, txn):
|
||||
"""Print a report about the transaction"""
|
||||
if txn._objects:
|
||||
bytes = reduce(operator.add, [size for oid, size in txn._objects])
|
||||
else:
|
||||
bytes = 0
|
||||
print('%s %s %4d %10d %s %s' % (
|
||||
txn._begintime, txn._finishtime - txn._begintime,
|
||||
len(txn._objects),
|
||||
bytes,
|
||||
time.ctime(txn._begintime),
|
||||
txn._url))
|
||||
|
||||
def replay(self):
|
||||
for txn in self.__txns:
|
||||
txn.replay()
|
||||
# How many fell behind?
|
||||
slower = []
|
||||
faster = []
|
||||
for txn in self.__txns:
|
||||
if txn._replaydelta > 0:
|
||||
slower.append(txn)
|
||||
else:
|
||||
faster.append(txn)
|
||||
print(len(slower), 'laggards,', len(faster), 'on-time or faster')
|
||||
# Find some averages
|
||||
if slower:
|
||||
sum = reduce(operator.add,
|
||||
[txn._replaydelta for txn in slower], 0)
|
||||
print('average slower txn was:', float(sum) / len(slower))
|
||||
if faster:
|
||||
sum = reduce(operator.add,
|
||||
[txn._replaydelta for txn in faster], 0)
|
||||
print('average faster txn was:', float(sum) / len(faster))
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
opts, args = getopt.getopt(
|
||||
sys.argv[1:],
|
||||
'hr:pm:',
|
||||
['help', 'replay=', 'report', 'maxtxns='])
|
||||
except getopt.error as e:
|
||||
usage(1, e)
|
||||
|
||||
if args:
|
||||
usage(1)
|
||||
|
||||
replay = 0
|
||||
maxtxns = -1
|
||||
report = 0
|
||||
storagefile = None
|
||||
for opt, arg in opts:
|
||||
if opt in ('-h', '--help'):
|
||||
usage(0)
|
||||
elif opt in ('-r', '--replay'):
|
||||
replay = 1
|
||||
storagefile = arg
|
||||
elif opt in ('-p', '--report'):
|
||||
report = 1
|
||||
elif opt in ('-m', '--maxtxns'):
|
||||
try:
|
||||
maxtxns = int(arg)
|
||||
except ValueError:
|
||||
usage(1, 'Bad -m argument: %s' % arg)
|
||||
|
||||
if replay:
|
||||
storage = FileStorage(storagefile)
|
||||
#storage = BDBFullStorage(storagefile)
|
||||
#storage = PrimaryStorage('yyz', storage, RS_PORT)
|
||||
t0 = now()
|
||||
p = ZEOParser(maxtxns, report, storage)
|
||||
i = 0
|
||||
while 1:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
break
|
||||
i += 1
|
||||
try:
|
||||
p.parse(line)
|
||||
except StopParsing:
|
||||
break
|
||||
except:
|
||||
print('input file line:', i)
|
||||
raise
|
||||
t1 = now()
|
||||
print('total parse time:', t1-t0)
|
||||
t2 = now()
|
||||
if replay:
|
||||
p.replay()
|
||||
t3 = now()
|
||||
print('total replay time:', t3-t2)
|
||||
print('total time:', t3-t0)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,566 @@
|
||||
#!/usr/bin/env python2.3
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
##############################################################################
|
||||
"""Tools for analyzing ZEO Server logs.
|
||||
|
||||
This script contains a number of commands, implemented by command
|
||||
functions. To run a command, give the command name and it's arguments
|
||||
as arguments to this script.
|
||||
|
||||
Commands:
|
||||
|
||||
blocked_times file threshold
|
||||
|
||||
Output a summary of episodes where thransactions were blocked
|
||||
when the episode lasted at least threshold seconds.
|
||||
|
||||
The file may be a file name or - to read from standard input.
|
||||
The file may also be a command:
|
||||
|
||||
script blocked_times 'bunzip2 <foo.log.bz2' 60
|
||||
|
||||
If the file is a command, it must contain at least a single
|
||||
space.
|
||||
|
||||
The columns of output are:
|
||||
|
||||
- The time the episode started
|
||||
|
||||
- The seconds from the start of the episode until the blocking
|
||||
transaction finished.
|
||||
|
||||
- The client id (host and port) of the blocking transaction.
|
||||
|
||||
- The seconds from the start of the episode until the end of the
|
||||
episode.
|
||||
|
||||
time_calls file threshold
|
||||
|
||||
Time how long calls took. Note that this is normally combined
|
||||
with grep to time just a particulat kind of call:
|
||||
|
||||
script time_calls 'bunzip2 <foo.log.bz2 | grep tpc_finish' 10
|
||||
|
||||
time_trans threshold
|
||||
|
||||
The columns of output are:
|
||||
|
||||
- The time of the call invocation
|
||||
|
||||
- The seconds from the call to the return
|
||||
|
||||
- The client that made the call.
|
||||
|
||||
time_trans file threshold
|
||||
|
||||
Output a summary of transactions that held the global transaction
|
||||
lock for at least threshold seconds. (This is the time from when
|
||||
voting starts until the transaction is completed by the server.)
|
||||
|
||||
The columns of output are:
|
||||
|
||||
- time that the vote started.
|
||||
|
||||
- client id
|
||||
|
||||
- number of objects written / number of objects updated
|
||||
|
||||
- seconds from tpc_begin to vote start
|
||||
|
||||
- seconds spent voting
|
||||
|
||||
- vote status: n=normal, d=delayed, e=error
|
||||
|
||||
- seconds wating between vote return and finish call
|
||||
|
||||
- time spent finishing or 'abort' if the transaction aborted
|
||||
|
||||
minute file
|
||||
|
||||
Compute production statistics by minute
|
||||
|
||||
The columns of output are:
|
||||
|
||||
- date/time
|
||||
|
||||
- Number of active clients
|
||||
|
||||
- number of reads
|
||||
|
||||
- number of stores
|
||||
|
||||
- number of commits (finish)
|
||||
|
||||
- number of aborts
|
||||
|
||||
- number of transactions (commits + aborts)
|
||||
|
||||
Summary statistics are printed at the end
|
||||
|
||||
minutes file
|
||||
|
||||
Show just the summary statistics for production by minute.
|
||||
|
||||
hour file
|
||||
|
||||
Compute production statistics by hour
|
||||
|
||||
hours file
|
||||
|
||||
Show just the summary statistics for production by hour.
|
||||
|
||||
day file
|
||||
|
||||
Compute production statistics by day
|
||||
|
||||
days file
|
||||
|
||||
Show just the summary statistics for production by day.
|
||||
|
||||
verify file
|
||||
|
||||
Compute verification statistics
|
||||
|
||||
The columns of output are:
|
||||
|
||||
- client id
|
||||
- verification start time
|
||||
- number of object's verified
|
||||
- wall time to verify
|
||||
- average miliseconds to verify per object.
|
||||
|
||||
$Id$
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
|
||||
import datetime, sys, re, os
|
||||
from six.moves import map
|
||||
from six.moves import zip
|
||||
|
||||
|
||||
def time(line):
|
||||
d = line[:10]
|
||||
t = line[11:19]
|
||||
y, mo, d = map(int, d.split('-'))
|
||||
h, mi, s = map(int, t.split(':'))
|
||||
return datetime.datetime(y, mo, d, h, mi, s)
|
||||
|
||||
|
||||
def sub(t1, t2):
|
||||
delta = t2 - t1
|
||||
return delta.days*86400.0+delta.seconds+delta.microseconds/1000000.0
|
||||
|
||||
|
||||
|
||||
waitre = re.compile(r'Clients waiting: (\d+)')
|
||||
idre = re.compile(r' ZSS:\d+/(\d+.\d+.\d+.\d+:\d+) ')
|
||||
def blocked_times(args):
|
||||
f, thresh = args
|
||||
|
||||
t1 = t2 = cid = blocking = waiting = 0
|
||||
last_blocking = False
|
||||
|
||||
thresh = int(thresh)
|
||||
|
||||
for line in xopen(f):
|
||||
line = line.strip()
|
||||
|
||||
if line.endswith('Blocked transaction restarted.'):
|
||||
blocking = False
|
||||
waiting = 0
|
||||
else:
|
||||
s = waitre.search(line)
|
||||
if not s:
|
||||
continue
|
||||
waiting = int(s.group(1))
|
||||
blocking = line.find(
|
||||
'Transaction blocked waiting for storage') >= 0
|
||||
|
||||
if blocking and waiting == 1:
|
||||
t1 = time(line)
|
||||
t2 = t1
|
||||
|
||||
if not blocking and last_blocking:
|
||||
last_wait = 0
|
||||
t2 = time(line)
|
||||
cid = idre.search(line).group(1)
|
||||
|
||||
if waiting == 0:
|
||||
d = sub(t1, time(line))
|
||||
if d >= thresh:
|
||||
print(t1, sub(t1, t2), cid, d)
|
||||
t1 = t2 = cid = blocking = waiting = last_wait = max_wait = 0
|
||||
|
||||
last_blocking = blocking
|
||||
|
||||
connidre = re.compile(r' zrpc-conn:(\d+.\d+.\d+.\d+:\d+) ')
|
||||
def time_calls(f):
|
||||
f, thresh = f
|
||||
if f == '-':
|
||||
f = sys.stdin
|
||||
else:
|
||||
f = xopen(f)
|
||||
|
||||
thresh = float(thresh)
|
||||
t1 = None
|
||||
maxd = 0
|
||||
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
|
||||
if ' calling ' in line:
|
||||
t1 = time(line)
|
||||
elif ' returns ' in line and t1 is not None:
|
||||
d = sub(t1, time(line))
|
||||
if d >= thresh:
|
||||
print(t1, d, connidre.search(line).group(1))
|
||||
maxd = max(maxd, d)
|
||||
t1 = None
|
||||
|
||||
print(maxd)
|
||||
|
||||
def xopen(f):
|
||||
if f == '-':
|
||||
return sys.stdin
|
||||
if ' ' in f:
|
||||
return os.popen(f, 'r')
|
||||
return open(f)
|
||||
|
||||
def time_tpc(f):
|
||||
f, thresh = f
|
||||
if f == '-':
|
||||
f = sys.stdin
|
||||
else:
|
||||
f = xopen(f)
|
||||
|
||||
thresh = float(thresh)
|
||||
transactions = {}
|
||||
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
|
||||
if ' calling vote(' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
transactions[cid] = time(line),
|
||||
elif ' vote returns None' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
transactions[cid] += time(line), 'n'
|
||||
elif ' vote() raised' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
transactions[cid] += time(line), 'e'
|
||||
elif ' vote returns ' in line:
|
||||
# delayed, skip
|
||||
cid = connidre.search(line).group(1)
|
||||
transactions[cid] += time(line), 'd'
|
||||
elif ' calling tpc_abort(' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
if cid in transactions:
|
||||
t1, t2, vs = transactions[cid]
|
||||
t = time(line)
|
||||
d = sub(t1, t)
|
||||
if d >= thresh:
|
||||
print('a', t1, cid, sub(t1, t2), vs, sub(t2, t))
|
||||
del transactions[cid]
|
||||
elif ' calling tpc_finish(' in line:
|
||||
if cid in transactions:
|
||||
cid = connidre.search(line).group(1)
|
||||
transactions[cid] += time(line),
|
||||
elif ' tpc_finish returns ' in line:
|
||||
if cid in transactions:
|
||||
t1, t2, vs, t3 = transactions[cid]
|
||||
t = time(line)
|
||||
d = sub(t1, t)
|
||||
if d >= thresh:
|
||||
print('c', t1, cid, sub(t1, t2), vs, sub(t2, t3), sub(t3, t))
|
||||
del transactions[cid]
|
||||
|
||||
|
||||
newobre = re.compile(r"storea\(.*, '\\x00\\x00\\x00\\x00\\x00")
|
||||
def time_trans(f):
|
||||
f, thresh = f
|
||||
if f == '-':
|
||||
f = sys.stdin
|
||||
else:
|
||||
f = xopen(f)
|
||||
|
||||
thresh = float(thresh)
|
||||
transactions = {}
|
||||
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
|
||||
if ' calling tpc_begin(' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
transactions[cid] = time(line), [0, 0]
|
||||
if ' calling storea(' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
if cid in transactions:
|
||||
transactions[cid][1][0] += 1
|
||||
if not newobre.search(line):
|
||||
transactions[cid][1][1] += 1
|
||||
|
||||
elif ' calling vote(' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
if cid in transactions:
|
||||
transactions[cid] += time(line),
|
||||
elif ' vote returns None' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
if cid in transactions:
|
||||
transactions[cid] += time(line), 'n'
|
||||
elif ' vote() raised' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
if cid in transactions:
|
||||
transactions[cid] += time(line), 'e'
|
||||
elif ' vote returns ' in line:
|
||||
# delayed, skip
|
||||
cid = connidre.search(line).group(1)
|
||||
if cid in transactions:
|
||||
transactions[cid] += time(line), 'd'
|
||||
elif ' calling tpc_abort(' in line:
|
||||
cid = connidre.search(line).group(1)
|
||||
if cid in transactions:
|
||||
try:
|
||||
t0, (stores, old), t1, t2, vs = transactions[cid]
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
t = time(line)
|
||||
d = sub(t1, t)
|
||||
if d >= thresh:
|
||||
print(t1, cid, "%s/%s" % (stores, old), \
|
||||
sub(t0, t1), sub(t1, t2), vs, \
|
||||
sub(t2, t), 'abort')
|
||||
del transactions[cid]
|
||||
elif ' calling tpc_finish(' in line:
|
||||
if cid in transactions:
|
||||
cid = connidre.search(line).group(1)
|
||||
transactions[cid] += time(line),
|
||||
elif ' tpc_finish returns ' in line:
|
||||
if cid in transactions:
|
||||
t0, (stores, old), t1, t2, vs, t3 = transactions[cid]
|
||||
t = time(line)
|
||||
d = sub(t1, t)
|
||||
if d >= thresh:
|
||||
print(t1, cid, "%s/%s" % (stores, old), \
|
||||
sub(t0, t1), sub(t1, t2), vs, \
|
||||
sub(t2, t3), sub(t3, t))
|
||||
del transactions[cid]
|
||||
|
||||
def minute(f, slice=16, detail=1, summary=1):
|
||||
f, = f
|
||||
|
||||
if f == '-':
|
||||
f = sys.stdin
|
||||
else:
|
||||
f = xopen(f)
|
||||
|
||||
cols = ["time", "reads", "stores", "commits", "aborts", "txns"]
|
||||
fmt = "%18s %6s %6s %7s %6s %6s"
|
||||
print(fmt % cols)
|
||||
print(fmt % ["-"*len(col) for col in cols])
|
||||
|
||||
mlast = r = s = c = a = cl = None
|
||||
rs = []
|
||||
ss = []
|
||||
cs = []
|
||||
aborts = []
|
||||
ts = []
|
||||
cls = []
|
||||
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if (line.find('returns') > 0
|
||||
or line.find('storea') > 0
|
||||
or line.find('tpc_abort') > 0
|
||||
):
|
||||
client = connidre.search(line).group(1)
|
||||
m = line[:slice]
|
||||
if m != mlast:
|
||||
if mlast:
|
||||
if detail:
|
||||
print(fmt % (mlast, len(cl), r, s, c, a, a+c))
|
||||
cls.append(len(cl))
|
||||
rs.append(r)
|
||||
ss.append(s)
|
||||
cs.append(c)
|
||||
aborts.append(a)
|
||||
ts.append(c+a)
|
||||
mlast = m
|
||||
r = s = c = a = 0
|
||||
cl = {}
|
||||
if line.find('zeoLoad') > 0:
|
||||
r += 1
|
||||
cl[client] = 1
|
||||
elif line.find('storea') > 0:
|
||||
s += 1
|
||||
cl[client] = 1
|
||||
elif line.find('tpc_finish') > 0:
|
||||
c += 1
|
||||
cl[client] = 1
|
||||
elif line.find('tpc_abort') > 0:
|
||||
a += 1
|
||||
cl[client] = 1
|
||||
|
||||
if mlast:
|
||||
if detail:
|
||||
print(fmt % (mlast, len(cl), r, s, c, a, a+c))
|
||||
cls.append(len(cl))
|
||||
rs.append(r)
|
||||
ss.append(s)
|
||||
cs.append(c)
|
||||
aborts.append(a)
|
||||
ts.append(c+a)
|
||||
|
||||
if summary:
|
||||
print()
|
||||
print('Summary: \t', '\t'.join(('min', '10%', '25%', 'med',
|
||||
'75%', '90%', 'max', 'mean')))
|
||||
print("n=%6d\t" % len(cls), '-'*62)
|
||||
print('Clients: \t', '\t'.join(map(str,stats(cls))))
|
||||
print('Reads: \t', '\t'.join(map(str,stats(rs))))
|
||||
print('Stores: \t', '\t'.join(map(str,stats(ss))))
|
||||
print('Commits: \t', '\t'.join(map(str,stats(cs))))
|
||||
print('Aborts: \t', '\t'.join(map(str,stats(aborts))))
|
||||
print('Trans: \t', '\t'.join(map(str,stats(ts))))
|
||||
|
||||
def stats(s):
|
||||
s.sort()
|
||||
min = s[0]
|
||||
max = s[-1]
|
||||
n = len(s)
|
||||
out = [min]
|
||||
ni = n + 1
|
||||
for p in .1, .25, .5, .75, .90:
|
||||
lp = ni*p
|
||||
l = int(lp)
|
||||
if lp < 1 or lp > n:
|
||||
out.append('-')
|
||||
elif abs(lp-l) < .00001:
|
||||
out.append(s[l-1])
|
||||
else:
|
||||
out.append(int(s[l-1] + (lp - l) * (s[l] - s[l-1])))
|
||||
|
||||
mean = 0.0
|
||||
for v in s:
|
||||
mean += v
|
||||
|
||||
out.extend([max, int(mean/n)])
|
||||
|
||||
return out
|
||||
|
||||
def minutes(f):
|
||||
minute(f, 16, detail=0)
|
||||
|
||||
def hour(f):
|
||||
minute(f, 13)
|
||||
|
||||
def day(f):
|
||||
minute(f, 10)
|
||||
|
||||
def hours(f):
|
||||
minute(f, 13, detail=0)
|
||||
|
||||
def days(f):
|
||||
minute(f, 10, detail=0)
|
||||
|
||||
|
||||
new_connection_idre = re.compile(
|
||||
r"new connection \('(\d+.\d+.\d+.\d+)', (\d+)\):")
|
||||
def verify(f):
|
||||
f, = f
|
||||
|
||||
if f == '-':
|
||||
f = sys.stdin
|
||||
else:
|
||||
f = xopen(f)
|
||||
|
||||
t1 = None
|
||||
nv = {}
|
||||
for line in f:
|
||||
if line.find('new connection') > 0:
|
||||
m = new_connection_idre.search(line)
|
||||
cid = "%s:%s" % (m.group(1), m.group(2))
|
||||
nv[cid] = [time(line), 0]
|
||||
elif line.find('calling zeoVerify(') > 0:
|
||||
cid = connidre.search(line).group(1)
|
||||
nv[cid][1] += 1
|
||||
elif line.find('calling endZeoVerify()') > 0:
|
||||
cid = connidre.search(line).group(1)
|
||||
t1, n = nv[cid]
|
||||
if n:
|
||||
d = sub(t1, time(line))
|
||||
print(cid, t1, n, d, n and (d*1000.0/n) or '-')
|
||||
|
||||
def recovery(f):
|
||||
f, = f
|
||||
|
||||
if f == '-':
|
||||
f = sys.stdin
|
||||
else:
|
||||
f = xopen(f)
|
||||
|
||||
last = ''
|
||||
trans = []
|
||||
n = 0
|
||||
for line in f:
|
||||
n += 1
|
||||
if line.find('RecoveryServer') < 0:
|
||||
continue
|
||||
l = line.find('sending transaction ')
|
||||
if l > 0 and last.find('sending transaction ') > 0:
|
||||
trans.append(line[l+20:].strip())
|
||||
else:
|
||||
if trans:
|
||||
if len(trans) > 1:
|
||||
print(" ... %s similar records skipped ..." % (
|
||||
len(trans) - 1))
|
||||
print(n, last.strip())
|
||||
trans=[]
|
||||
print(n, line.strip())
|
||||
last = line
|
||||
|
||||
if len(trans) > 1:
|
||||
print(" ... %s similar records skipped ..." % (
|
||||
len(trans) - 1))
|
||||
print(n, last.strip())
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
globals()[sys.argv[1]](sys.argv[2:])
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python2.3
|
||||
|
||||
"""Make sure a ZEO server is running.
|
||||
|
||||
usage: zeoup.py [options]
|
||||
|
||||
The test will connect to a ZEO server, load the root object, and attempt to
|
||||
update the zeoup counter in the root. It will report success if it updates
|
||||
the counter or if it gets a ConflictError. A ConflictError is considered a
|
||||
success, because the client was able to start a transaction.
|
||||
|
||||
Options:
|
||||
|
||||
-p port -- port to connect to
|
||||
|
||||
-h host -- host to connect to (default is current host)
|
||||
|
||||
-S storage -- storage name (default '1')
|
||||
|
||||
-U path -- Unix-domain socket to connect to
|
||||
|
||||
--nowrite -- Do not update the zeoup counter.
|
||||
|
||||
-1 -- Connect to a ZEO 1.0 server.
|
||||
|
||||
You must specify either -p and -h or -U.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
|
||||
import getopt
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
from persistent.mapping import PersistentMapping
|
||||
import transaction
|
||||
|
||||
import ZODB
|
||||
from ZODB.POSException import ConflictError
|
||||
from ZODB.tests.MinPO import MinPO
|
||||
from ZEO.ClientStorage import ClientStorage
|
||||
|
||||
ZEO_VERSION = 2
|
||||
|
||||
def setup_logging():
|
||||
# Set up logging to stderr which will show messages originating
|
||||
# at severity ERROR or higher.
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.ERROR)
|
||||
fmt = logging.Formatter(
|
||||
"------\n%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
"%Y-%m-%dT%H:%M:%S")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(fmt)
|
||||
root.addHandler(handler)
|
||||
|
||||
def check_server(addr, storage, write):
|
||||
t0 = time.time()
|
||||
if ZEO_VERSION == 2:
|
||||
# TODO: should do retries w/ exponential backoff.
|
||||
cs = ClientStorage(addr, storage=storage, wait=0,
|
||||
read_only=(not write))
|
||||
else:
|
||||
cs = ClientStorage(addr, storage=storage, debug=1,
|
||||
wait_for_server_on_startup=1)
|
||||
# _startup() is an artifact of the way ZEO 1.0 works. The
|
||||
# ClientStorage doesn't get fully initialized until registerDB()
|
||||
# is called. The only thing we care about, though, is that
|
||||
# registerDB() calls _startup().
|
||||
|
||||
if write:
|
||||
db = ZODB.DB(cs)
|
||||
cn = db.open()
|
||||
root = cn.root()
|
||||
try:
|
||||
# We store the data in a special `monitor' dict under the root,
|
||||
# where other tools may also store such heartbeat and bookkeeping
|
||||
# type data.
|
||||
monitor = root.get('monitor')
|
||||
if monitor is None:
|
||||
monitor = root['monitor'] = PersistentMapping()
|
||||
obj = monitor['zeoup'] = monitor.get('zeoup', MinPO(0))
|
||||
obj.value += 1
|
||||
transaction.commit()
|
||||
except ConflictError:
|
||||
pass
|
||||
cn.close()
|
||||
db.close()
|
||||
else:
|
||||
data, serial = cs.load("\0\0\0\0\0\0\0\0", "")
|
||||
cs.close()
|
||||
t1 = time.time()
|
||||
print("Elapsed time: %.2f" % (t1 - t0))
|
||||
|
||||
def usage(exit=1):
|
||||
print(__doc__)
|
||||
print(" ".join(sys.argv))
|
||||
sys.exit(exit)
|
||||
|
||||
def main():
|
||||
host = None
|
||||
port = None
|
||||
unix = None
|
||||
write = 1
|
||||
storage = '1'
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], 'p:h:U:S:1',
|
||||
['nowrite'])
|
||||
for o, a in opts:
|
||||
if o == '-p':
|
||||
port = int(a)
|
||||
elif o == '-h':
|
||||
host = a
|
||||
elif o == '-U':
|
||||
unix = a
|
||||
elif o == '-S':
|
||||
storage = a
|
||||
elif o == '--nowrite':
|
||||
write = 0
|
||||
elif o == '-1':
|
||||
ZEO_VERSION = 1
|
||||
except Exception as err:
|
||||
s = str(err)
|
||||
if s:
|
||||
s = ": " + s
|
||||
print(err.__class__.__name__ + s)
|
||||
usage()
|
||||
|
||||
if unix is not None:
|
||||
addr = unix
|
||||
else:
|
||||
if host is None:
|
||||
host = socket.gethostname()
|
||||
if port is None:
|
||||
usage()
|
||||
addr = host, port
|
||||
|
||||
setup_logging()
|
||||
check_server(addr, storage, write)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as err:
|
||||
s = str(err)
|
||||
if s:
|
||||
s = ": " + s
|
||||
print(err.__class__.__name__ + s)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,133 @@
|
||||
<component>
|
||||
|
||||
<sectiontype name="ssl" datatype="ZEO.zconfig.server_ssl">
|
||||
|
||||
<key name="certificate" datatype="existing-dirpath" required="yes">
|
||||
<description>
|
||||
The full path to an SSL certificate file.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="key" datatype="existing-dirpath" required="no">
|
||||
<description>
|
||||
The full path to an SSL key file for the server certificate.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="password-function" required="no">
|
||||
<description>
|
||||
Dotted name of importable function for retrieving a password
|
||||
for the client certificate key.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="authenticate" required="yes">
|
||||
<description>
|
||||
Path to a file or directory containing client certificates to
|
||||
be authenticated. This can also be - or SIGNED to require
|
||||
signed client certificates.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="zeo">
|
||||
|
||||
<section type="ssl" name="*" attribute="ssl" />
|
||||
|
||||
<description>
|
||||
The content of a ZEO section describe operational parameters
|
||||
of a ZEO server except for the storage(s) to be served.
|
||||
</description>
|
||||
|
||||
<key name="address" datatype="socket-binding-address"
|
||||
required="yes">
|
||||
<description>
|
||||
The address at which the server should listen. This can be in
|
||||
the form 'host:port' to signify a TCP/IP connection or a
|
||||
pathname string to signify a Unix domain socket connection (at
|
||||
least one '/' is required). A hostname may be a DNS name or a
|
||||
dotted IP address. If the hostname is omitted, the platform's
|
||||
default behavior is used when binding the listening socket (''
|
||||
is passed to socket.bind() as the hostname portion of the
|
||||
address).
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="read-only" datatype="boolean"
|
||||
required="no"
|
||||
default="false">
|
||||
<description>
|
||||
Flag indicating whether the server should operate in read-only
|
||||
mode. Defaults to false. Note that even if the server is
|
||||
operating in writable mode, individual storages may still be
|
||||
read-only. But if the server is in read-only mode, no write
|
||||
operations are allowed, even if the storages are writable. Note
|
||||
that pack() is considered a read-only operation.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="invalidation-queue-size" datatype="integer"
|
||||
required="no"
|
||||
default="100">
|
||||
<description>
|
||||
The storage server keeps a queue of the objects modified by the
|
||||
last N transactions, where N == invalidation_queue_size. This
|
||||
queue is used to speed client cache verification when a client
|
||||
disconnects for a short period of time.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="invalidation-age" datatype="float" required="no">
|
||||
<description>
|
||||
The maximum age of a client for which quick-verification
|
||||
invalidations will be provided by iterating over the served
|
||||
storage. This option should only be used if the served storage
|
||||
supports efficient iteration from a starting point near the
|
||||
end of the transaction history (e.g. end of file).
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="transaction-timeout" datatype="integer"
|
||||
required="no">
|
||||
<description>
|
||||
The maximum amount of time to wait for a transaction to commit
|
||||
after acquiring the storage lock, specified in seconds. If the
|
||||
transaction takes too long, the client connection will be closed
|
||||
and the transaction aborted.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="pid-filename" datatype="existing-dirpath"
|
||||
required="no">
|
||||
<description>
|
||||
The full path to the file in which to write the ZEO server's Process ID
|
||||
at startup. If omitted, $INSTANCE/var/ZEO.pid is used.
|
||||
</description>
|
||||
<metadefault>$INSTANCE/var/ZEO.pid (or $clienthome/ZEO.pid)</metadefault>
|
||||
</key>
|
||||
|
||||
<key name="client-conflict-resolution" datatype="boolean"
|
||||
required="no" default="false">
|
||||
<description>
|
||||
Flag indicating whether the server should return conflict
|
||||
errors to the client, for resolution there.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="msgpack" datatype="boolean" required="no">
|
||||
<description>
|
||||
Use msgpack to serialize and de-serialize ZEO protocol messages.
|
||||
|
||||
An advantage of using msgpack for ZEO communication is that
|
||||
it's a tiny bit faster and a ZEO server can support Python 2
|
||||
or Python 3 clients (but not both).
|
||||
|
||||
msgpack can also be enabled by setting the ``ZEO_MSGPACK``
|
||||
environment to a non-empty string.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,56 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
REPR_LIMIT = 60
|
||||
|
||||
def short_repr(obj):
|
||||
"Return an object repr limited to REPR_LIMIT bytes."
|
||||
|
||||
# Some of the objects being repr'd are large strings. A lot of memory
|
||||
# would be wasted to repr them and then truncate, so they are treated
|
||||
# specially in this function.
|
||||
# Also handle short repr of a tuple containing a long string.
|
||||
|
||||
# This strategy works well for arguments to StorageServer methods.
|
||||
# The oid is usually first and will get included in its entirety.
|
||||
# The pickle is near the beginning, too, and you can often fit the
|
||||
# module name in the pickle.
|
||||
|
||||
if isinstance(obj, str):
|
||||
if len(obj) > REPR_LIMIT:
|
||||
r = repr(obj[:REPR_LIMIT])
|
||||
else:
|
||||
r = repr(obj)
|
||||
if len(r) > REPR_LIMIT:
|
||||
r = r[:REPR_LIMIT-4] + '...' + r[-1]
|
||||
return r
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
elts = []
|
||||
size = 0
|
||||
for elt in obj:
|
||||
r = short_repr(elt)
|
||||
elts.append(r)
|
||||
size += len(r)
|
||||
if size > REPR_LIMIT:
|
||||
break
|
||||
if isinstance(obj, tuple):
|
||||
r = "(%s)" % (", ".join(elts))
|
||||
else:
|
||||
r = "[%s]" % (", ".join(elts))
|
||||
else:
|
||||
r = repr(obj)
|
||||
if len(r) > REPR_LIMIT:
|
||||
return r[:REPR_LIMIT] + '...'
|
||||
else:
|
||||
return r
|
||||
@@ -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
|
||||
#
|
||||
##############################################################################
|
||||
"""Tests of the ZEO cache"""
|
||||
|
||||
from ZODB.Connection import TransactionMetaData
|
||||
from ZODB.tests.MinPO import MinPO
|
||||
from ZODB.tests.StorageTestBase import zodb_unpickle
|
||||
|
||||
class TransUndoStorageWithCache(object):
|
||||
|
||||
def checkUndoInvalidation(self):
|
||||
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()
|
||||
if not info:
|
||||
# Preserved this comment, but don't understand it:
|
||||
# "Perhaps we have an old storage implementation that
|
||||
# does do the negative nonsense."
|
||||
info = self._storage.undoInfo(0, 20)
|
||||
tid = info[0]['id']
|
||||
|
||||
# Now start an undo transaction
|
||||
t = TransactionMetaData()
|
||||
t.note(u'undo1')
|
||||
oids = self._begin_undos_vote(t, tid)
|
||||
|
||||
# Make sure this doesn't load invalid data into the cache
|
||||
self._storage.load(oid, '')
|
||||
|
||||
self._storage.tpc_finish(t)
|
||||
|
||||
[uoid] = oids
|
||||
assert uoid == oid
|
||||
data, revid = self._storage.load(oid, '')
|
||||
obj = zodb_unpickle(data)
|
||||
assert obj == MinPO(24)
|
||||
@@ -0,0 +1,180 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Tests of the distributed commit lock."""
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from persistent.TimeStamp import TimeStamp
|
||||
from ZODB.Connection import TransactionMetaData
|
||||
from ZODB.tests.StorageTestBase import zodb_pickle, MinPO
|
||||
|
||||
import ZEO.ClientStorage
|
||||
from ZEO.Exceptions import ClientDisconnected
|
||||
from ZEO.tests.TestThread import TestThread
|
||||
|
||||
ZERO = b'\0'*8
|
||||
|
||||
class DummyDB(object):
|
||||
def invalidate(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
transform_record_data = untransform_record_data = lambda self, data: data
|
||||
|
||||
class WorkerThread(TestThread):
|
||||
|
||||
# run the entire test in a thread so that the blocking call for
|
||||
# tpc_vote() doesn't hang the test suite.
|
||||
|
||||
def __init__(self, test, storage, trans):
|
||||
self.storage = storage
|
||||
self.trans = trans
|
||||
self.ready = threading.Event()
|
||||
TestThread.__init__(self, test)
|
||||
|
||||
def testrun(self):
|
||||
try:
|
||||
self.storage.tpc_begin(self.trans)
|
||||
oid = self.storage.new_oid()
|
||||
p = zodb_pickle(MinPO("c"))
|
||||
self.storage.store(oid, ZERO, p, '', self.trans)
|
||||
oid = self.storage.new_oid()
|
||||
p = zodb_pickle(MinPO("c"))
|
||||
self.storage.store(oid, ZERO, p, '', self.trans)
|
||||
self.myvote()
|
||||
self.storage.tpc_finish(self.trans)
|
||||
except ClientDisconnected:
|
||||
pass
|
||||
|
||||
def myvote(self):
|
||||
# The vote() call is synchronous, which makes it difficult to
|
||||
# coordinate the action of multiple threads that all call
|
||||
# vote(). This method sends the vote call, then sets the
|
||||
# event saying vote was called, then waits for the vote
|
||||
# response.
|
||||
|
||||
future = self.storage._server.call_future('vote', id(self.trans))
|
||||
self.ready.set()
|
||||
future.result(9)
|
||||
|
||||
class CommitLockTests(object):
|
||||
|
||||
NUM_CLIENTS = 5
|
||||
|
||||
# The commit lock tests verify that the storage successfully
|
||||
# blocks and restarts transactions when there is contention for a
|
||||
# single storage. There are a lot of cases to cover.
|
||||
|
||||
# The general flow of these tests is to start a transaction by
|
||||
# getting far enough into 2PC to acquire the commit lock. Then
|
||||
# begin one or more other connections that also want to commit.
|
||||
# This causes the commit lock code to be exercised. Once the
|
||||
# other connections are started, the first transaction completes.
|
||||
|
||||
def _cleanup(self):
|
||||
for store, trans in self._storages:
|
||||
store.tpc_abort(trans)
|
||||
store.close()
|
||||
self._storages = []
|
||||
|
||||
def _start_txn(self):
|
||||
txn = TransactionMetaData()
|
||||
self._storage.tpc_begin(txn)
|
||||
oid = self._storage.new_oid()
|
||||
self._storage.store(oid, ZERO, zodb_pickle(MinPO(1)), '', txn)
|
||||
return oid, txn
|
||||
|
||||
def _begin_threads(self):
|
||||
# Start a second transaction on a different connection without
|
||||
# blocking the test thread. Returns only after each thread has
|
||||
# set it's ready event.
|
||||
self._storages = []
|
||||
self._threads = []
|
||||
|
||||
for i in range(self.NUM_CLIENTS):
|
||||
storage = self._duplicate_client()
|
||||
txn = TransactionMetaData()
|
||||
tid = self._get_timestamp()
|
||||
|
||||
t = WorkerThread(self, storage, txn)
|
||||
self._threads.append(t)
|
||||
t.start()
|
||||
t.ready.wait()
|
||||
|
||||
# Close one of the connections abnormally to test server response
|
||||
if i == 0:
|
||||
storage.close()
|
||||
else:
|
||||
self._storages.append((storage, txn))
|
||||
|
||||
def _finish_threads(self):
|
||||
for t in self._threads:
|
||||
t.cleanup()
|
||||
|
||||
def _duplicate_client(self):
|
||||
"Open another ClientStorage to the same server."
|
||||
# It's hard to find the actual address.
|
||||
# The rpc mgr addr attribute is a list. Each element in the
|
||||
# list is a socket domain (AF_INET, AF_UNIX, etc.) and an
|
||||
# address.
|
||||
addr = self._storage._addr
|
||||
new = ZEO.ClientStorage.ClientStorage(
|
||||
addr, wait=1, **self._client_options())
|
||||
new.registerDB(DummyDB())
|
||||
return new
|
||||
|
||||
def _get_timestamp(self):
|
||||
t = time.time()
|
||||
t = TimeStamp(*time.gmtime(t)[:5]+(t%60,))
|
||||
return repr(t)
|
||||
|
||||
class CommitLockVoteTests(CommitLockTests):
|
||||
|
||||
def checkCommitLockVoteFinish(self):
|
||||
oid, txn = self._start_txn()
|
||||
self._storage.tpc_vote(txn)
|
||||
|
||||
self._begin_threads()
|
||||
|
||||
self._storage.tpc_finish(txn)
|
||||
self._storage.load(oid, '')
|
||||
|
||||
self._finish_threads()
|
||||
|
||||
self._dostore()
|
||||
self._cleanup()
|
||||
|
||||
def checkCommitLockVoteAbort(self):
|
||||
oid, txn = self._start_txn()
|
||||
self._storage.tpc_vote(txn)
|
||||
|
||||
self._begin_threads()
|
||||
|
||||
self._storage.tpc_abort(txn)
|
||||
|
||||
self._finish_threads()
|
||||
|
||||
self._dostore()
|
||||
self._cleanup()
|
||||
|
||||
def checkCommitLockVoteClose(self):
|
||||
oid, txn = self._start_txn()
|
||||
self._storage.tpc_vote(txn)
|
||||
|
||||
self._begin_threads()
|
||||
|
||||
self._storage.close()
|
||||
|
||||
self._finish_threads()
|
||||
self._cleanup()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,511 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 threading
|
||||
import time
|
||||
from random import Random
|
||||
|
||||
import transaction
|
||||
|
||||
from BTrees.check import check, display
|
||||
from BTrees.OOBTree import OOBTree
|
||||
|
||||
from ZEO.tests.TestThread import TestThread
|
||||
|
||||
from ZODB.DB import DB
|
||||
from ZODB.POSException import ReadConflictError, ConflictError
|
||||
|
||||
# The tests here let several threads have a go at one or more database
|
||||
# instances simultaneously. Each thread appends a disjoint (from the
|
||||
# other threads) sequence of increasing integers to an OOBTree, one at
|
||||
# at time (per thread). This provokes lots of conflicts, and BTrees
|
||||
# work hard at conflict resolution too. An OOBTree is used because
|
||||
# that flavor has the smallest maximum bucket size, and so splits buckets
|
||||
# more often than other BTree flavors.
|
||||
#
|
||||
# When these tests were first written, they provoked an amazing number
|
||||
# of obscure timing-related bugs in cache consistency logic, revealed
|
||||
# by failure of the BTree to pass internal consistency checks at the end,
|
||||
# and/or by failure of the BTree to contain all the keys the threads
|
||||
# thought they added (i.e., the keys for which transaction.commit()
|
||||
# did not raise any exception).
|
||||
|
||||
class FailableThread(TestThread):
|
||||
|
||||
# mixin class
|
||||
# subclass must provide
|
||||
# - self.stop attribute (an event)
|
||||
# - self._testrun() method
|
||||
|
||||
# TestThread.run() invokes testrun().
|
||||
def testrun(self):
|
||||
try:
|
||||
self._testrun()
|
||||
except:
|
||||
# Report the failure here to all the other threads, so
|
||||
# that they stop quickly.
|
||||
self.stop.set()
|
||||
raise
|
||||
|
||||
|
||||
class StressTask(object):
|
||||
# Append integers startnum, startnum + step, startnum + 2*step, ...
|
||||
# to 'tree'. If sleep is given, sleep
|
||||
# that long after each append. At the end, instance var .added_keys
|
||||
# is a list of the ints the thread believes it added successfully.
|
||||
def __init__(self, db, threadnum, startnum, step=2, sleep=None):
|
||||
self.db = db
|
||||
self.threadnum = threadnum
|
||||
self.startnum = startnum
|
||||
self.step = step
|
||||
self.sleep = sleep
|
||||
self.added_keys = []
|
||||
self.tm = transaction.TransactionManager()
|
||||
self.cn = self.db.open(transaction_manager=self.tm)
|
||||
self.cn.sync()
|
||||
|
||||
def doStep(self):
|
||||
tree = self.cn.root()["tree"]
|
||||
key = self.startnum
|
||||
tree[key] = self.threadnum
|
||||
|
||||
def commit(self):
|
||||
cn = self.cn
|
||||
key = self.startnum
|
||||
self.tm.get().note(u"add key %s" % key)
|
||||
try:
|
||||
self.tm.get().commit()
|
||||
except ConflictError as msg:
|
||||
self.tm.abort()
|
||||
else:
|
||||
if self.sleep:
|
||||
time.sleep(self.sleep)
|
||||
self.added_keys.append(key)
|
||||
self.startnum += self.step
|
||||
|
||||
def cleanup(self):
|
||||
self.tm.get().abort()
|
||||
self.cn.close()
|
||||
|
||||
def _runTasks(rounds, *tasks):
|
||||
'''run *task* interleaved for *rounds* rounds.'''
|
||||
def commit(run, actions):
|
||||
actions.append(':')
|
||||
for t in run:
|
||||
t.commit()
|
||||
del run[:]
|
||||
r = Random()
|
||||
r.seed(1064589285) # make it deterministic
|
||||
run = []
|
||||
actions = []
|
||||
try:
|
||||
for i in range(rounds):
|
||||
t = r.choice(tasks)
|
||||
if t in run:
|
||||
commit(run, actions)
|
||||
run.append(t)
|
||||
t.doStep()
|
||||
actions.append(repr(t.startnum))
|
||||
commit(run,actions)
|
||||
# stderr.write(' '.join(actions)+'\n')
|
||||
finally:
|
||||
for t in tasks:
|
||||
t.cleanup()
|
||||
|
||||
|
||||
class StressThread(FailableThread):
|
||||
|
||||
# Append integers startnum, startnum + step, startnum + 2*step, ...
|
||||
# to 'tree' until Event stop is set. If sleep is given, sleep
|
||||
# that long after each append. At the end, instance var .added_keys
|
||||
# is a list of the ints the thread believes it added successfully.
|
||||
def __init__(self, testcase, db, stop, threadnum, commitdict,
|
||||
startnum, step=2, sleep=None):
|
||||
TestThread.__init__(self, testcase)
|
||||
self.db = db
|
||||
self.stop = stop
|
||||
self.threadnum = threadnum
|
||||
self.startnum = startnum
|
||||
self.step = step
|
||||
self.sleep = sleep
|
||||
self.added_keys = []
|
||||
self.commitdict = commitdict
|
||||
|
||||
def _testrun(self):
|
||||
tm = transaction.TransactionManager()
|
||||
cn = self.db.open(transaction_manager=tm)
|
||||
while not self.stop.isSet():
|
||||
try:
|
||||
tree = cn.root()["tree"]
|
||||
break
|
||||
except (ConflictError, KeyError):
|
||||
tm.abort()
|
||||
key = self.startnum
|
||||
while not self.stop.isSet():
|
||||
try:
|
||||
tree[key] = self.threadnum
|
||||
tm.get().note(u"add key %s" % key)
|
||||
tm.commit()
|
||||
self.commitdict[self] = 1
|
||||
if self.sleep:
|
||||
time.sleep(self.sleep)
|
||||
except (ReadConflictError, ConflictError) as msg:
|
||||
tm.abort()
|
||||
else:
|
||||
self.added_keys.append(key)
|
||||
key += self.step
|
||||
cn.close()
|
||||
|
||||
class LargeUpdatesThread(FailableThread):
|
||||
|
||||
# A thread that performs a lot of updates. It attempts to modify
|
||||
# more than 25 objects so that it can test code that runs vote
|
||||
# in a separate thread when it modifies more than 25 objects.
|
||||
|
||||
def __init__(self, test, db, stop, threadnum, commitdict, startnum,
|
||||
step=2, sleep=None):
|
||||
TestThread.__init__(self, test)
|
||||
self.db = db
|
||||
self.stop = stop
|
||||
self.threadnum = threadnum
|
||||
self.startnum = startnum
|
||||
self.step = step
|
||||
self.sleep = sleep
|
||||
self.added_keys = []
|
||||
self.commitdict = commitdict
|
||||
|
||||
def _testrun(self):
|
||||
cn = self.db.open()
|
||||
while not self.stop.isSet():
|
||||
try:
|
||||
tree = cn.root()["tree"]
|
||||
break
|
||||
except (ConflictError, KeyError):
|
||||
# print("%d getting tree abort" % self.threadnum)
|
||||
transaction.abort()
|
||||
|
||||
keys_added = {} # set of keys we commit
|
||||
tkeys = []
|
||||
while not self.stop.isSet():
|
||||
|
||||
# The test picks 50 keys spread across many buckets.
|
||||
# self.startnum and self.step ensure that all threads use
|
||||
# disjoint key sets, to minimize conflict errors.
|
||||
|
||||
nkeys = len(tkeys)
|
||||
if nkeys < 50:
|
||||
tkeys = list(range(self.startnum, 3000, self.step))
|
||||
nkeys = len(tkeys)
|
||||
step = max(int(nkeys / 50), 1)
|
||||
keys = [tkeys[i] for i in range(0, nkeys, step)]
|
||||
for key in keys:
|
||||
try:
|
||||
tree[key] = self.threadnum
|
||||
except (ReadConflictError, ConflictError) as msg:
|
||||
# print("%d setting key %s" % (self.threadnum, msg))
|
||||
transaction.abort()
|
||||
break
|
||||
else:
|
||||
# print("%d set #%d" % (self.threadnum, len(keys)))
|
||||
transaction.get().note(u"keys %s" % ", ".join(map(str, keys)))
|
||||
try:
|
||||
transaction.commit()
|
||||
self.commitdict[self] = 1
|
||||
if self.sleep:
|
||||
time.sleep(self.sleep)
|
||||
except ConflictError as msg:
|
||||
# print("%d commit %s" % (self.threadnum, msg))
|
||||
transaction.abort()
|
||||
continue
|
||||
for k in keys:
|
||||
tkeys.remove(k)
|
||||
keys_added[k] = 1
|
||||
self.added_keys = keys_added.keys()
|
||||
cn.close()
|
||||
|
||||
class InvalidationTests(object):
|
||||
|
||||
# Minimum # of seconds the main thread lets the workers run. The
|
||||
# test stops as soon as this much time has elapsed, and all threads
|
||||
# have managed to commit a change.
|
||||
MINTIME = 10
|
||||
|
||||
# Maximum # of seconds the main thread lets the workers run. We
|
||||
# stop after this long has elapsed regardless of whether all threads
|
||||
# have managed to commit a change.
|
||||
MAXTIME = 300
|
||||
|
||||
StressThread = StressThread
|
||||
|
||||
def _check_tree(self, cn, tree):
|
||||
# Make sure the BTree is sane at the C level.
|
||||
retries = 3
|
||||
while retries:
|
||||
retries -= 1
|
||||
try:
|
||||
check(tree)
|
||||
tree._check()
|
||||
except ReadConflictError:
|
||||
if retries:
|
||||
transaction.abort()
|
||||
else:
|
||||
raise
|
||||
except:
|
||||
display(tree)
|
||||
raise
|
||||
|
||||
def _check_threads(self, tree, *threads):
|
||||
# Make sure the thread's view of the world is consistent with
|
||||
# the actual database state.
|
||||
|
||||
expected_keys = []
|
||||
errormsgs = []
|
||||
err = errormsgs.append
|
||||
|
||||
for t in threads:
|
||||
if not t.added_keys:
|
||||
err("thread %d didn't add any keys" % t.threadnum)
|
||||
expected_keys.extend(t.added_keys)
|
||||
expected_keys.sort()
|
||||
|
||||
for i in range(100):
|
||||
tree._p_jar.sync()
|
||||
actual_keys = list(tree.keys())
|
||||
if expected_keys == actual_keys:
|
||||
break
|
||||
time.sleep(.1)
|
||||
else:
|
||||
err("expected keys != actual keys")
|
||||
for k in expected_keys:
|
||||
if k not in actual_keys:
|
||||
err("key %s expected but not in tree" % k)
|
||||
for k in actual_keys:
|
||||
if k not in expected_keys:
|
||||
err("key %s in tree but not expected" % k)
|
||||
|
||||
self.fail('\n'.join(errormsgs))
|
||||
|
||||
def go(self, stop, commitdict, *threads):
|
||||
# Run the threads
|
||||
for t in threads:
|
||||
t.start()
|
||||
delay = self.MINTIME
|
||||
start = time.time()
|
||||
while time.time() - start <= self.MAXTIME:
|
||||
stop.wait(delay)
|
||||
if stop.isSet():
|
||||
# Some thread failed. Stop right now.
|
||||
break
|
||||
delay = 2.0
|
||||
if len(commitdict) >= len(threads):
|
||||
break
|
||||
# Some thread still hasn't managed to commit anything.
|
||||
stop.set()
|
||||
# Give all the threads some time to stop before trying to clean up.
|
||||
# cleanup() will cause the test to fail if some thread ended with
|
||||
# an uncaught exception, and unittest will call the base class
|
||||
# tearDown then immediately, but if other threads are still
|
||||
# running that can lead to a cascade of spurious exceptions.
|
||||
for t in threads:
|
||||
t.join(30)
|
||||
for t in threads:
|
||||
t.cleanup(10)
|
||||
|
||||
def checkConcurrentUpdates2Storages_emulated(self):
|
||||
self._storage = storage1 = self.openClientStorage()
|
||||
db1 = DB(storage1)
|
||||
storage2 = self.openClientStorage()
|
||||
db2 = DB(storage2)
|
||||
|
||||
cn = db1.open()
|
||||
tree = cn.root()["tree"] = OOBTree()
|
||||
transaction.commit()
|
||||
# DM: allow time for invalidations to come in and process them
|
||||
time.sleep(0.1)
|
||||
|
||||
# Run two threads that update the BTree
|
||||
t1 = StressTask(db1, 1, 1,)
|
||||
t2 = StressTask(db2, 2, 2,)
|
||||
_runTasks(100, t1, t2)
|
||||
|
||||
cn.sync()
|
||||
self._check_tree(cn, tree)
|
||||
self._check_threads(tree, t1, t2)
|
||||
|
||||
cn.close()
|
||||
db1.close()
|
||||
db2.close()
|
||||
|
||||
def checkConcurrentUpdates2Storages(self):
|
||||
self._storage = storage1 = self.openClientStorage()
|
||||
db1 = DB(storage1)
|
||||
storage2 = self.openClientStorage()
|
||||
db2 = DB(storage2)
|
||||
stop = threading.Event()
|
||||
|
||||
cn = db1.open()
|
||||
tree = cn.root()["tree"] = OOBTree()
|
||||
transaction.commit()
|
||||
cn.close()
|
||||
|
||||
# Run two threads that update the BTree
|
||||
cd = {}
|
||||
t1 = self.StressThread(self, db1, stop, 1, cd, 1)
|
||||
t2 = self.StressThread(self, db2, stop, 2, cd, 2)
|
||||
self.go(stop, cd, t1, t2)
|
||||
|
||||
while db1.lastTransaction() != db2.lastTransaction():
|
||||
db1._storage.sync()
|
||||
db2._storage.sync()
|
||||
|
||||
cn = db1.open()
|
||||
tree = cn.root()["tree"]
|
||||
self._check_tree(cn, tree)
|
||||
self._check_threads(tree, t1, t2)
|
||||
|
||||
cn.close()
|
||||
db1.close()
|
||||
db2.close()
|
||||
|
||||
def checkConcurrentUpdates19Storages(self):
|
||||
n = 19
|
||||
dbs = [DB(self.openClientStorage()) for i in range(n)]
|
||||
self._storage = dbs[0].storage
|
||||
stop = threading.Event()
|
||||
|
||||
cn = dbs[0].open()
|
||||
tree = cn.root()["tree"] = OOBTree()
|
||||
transaction.commit()
|
||||
cn.close()
|
||||
|
||||
# Run threads that update the BTree
|
||||
cd = {}
|
||||
threads = [self.StressThread(self, dbs[i], stop, i, cd, i, n)
|
||||
for i in range(n)]
|
||||
self.go(stop, cd, *threads)
|
||||
|
||||
while len(set(db.lastTransaction() for db in dbs)) > 1:
|
||||
_ = [db._storage.sync() for db in dbs]
|
||||
|
||||
cn = dbs[0].open()
|
||||
tree = cn.root()["tree"]
|
||||
self._check_tree(cn, tree)
|
||||
self._check_threads(tree, *threads)
|
||||
|
||||
cn.close()
|
||||
_ = [db.close() for db in dbs]
|
||||
|
||||
def checkConcurrentUpdates1Storage(self):
|
||||
self._storage = storage1 = self.openClientStorage()
|
||||
db1 = DB(storage1)
|
||||
stop = threading.Event()
|
||||
|
||||
cn = db1.open()
|
||||
tree = cn.root()["tree"] = OOBTree()
|
||||
transaction.commit()
|
||||
cn.close()
|
||||
|
||||
# Run two threads that update the BTree
|
||||
cd = {}
|
||||
t1 = self.StressThread(self, db1, stop, 1, cd, 1, sleep=0.01)
|
||||
t2 = self.StressThread(self, db1, stop, 2, cd, 2, sleep=0.01)
|
||||
self.go(stop, cd, t1, t2)
|
||||
|
||||
cn = db1.open()
|
||||
tree = cn.root()["tree"]
|
||||
self._check_tree(cn, tree)
|
||||
self._check_threads(tree, t1, t2)
|
||||
|
||||
cn.close()
|
||||
db1.close()
|
||||
|
||||
def checkConcurrentUpdates2StoragesMT(self):
|
||||
self._storage = storage1 = self.openClientStorage()
|
||||
db1 = DB(storage1)
|
||||
db2 = DB(self.openClientStorage())
|
||||
stop = threading.Event()
|
||||
|
||||
cn = db1.open()
|
||||
tree = cn.root()["tree"] = OOBTree()
|
||||
transaction.commit()
|
||||
cn.close()
|
||||
|
||||
# Run three threads that update the BTree.
|
||||
# Two of the threads share a single storage so that it
|
||||
# is possible for both threads to read the same object
|
||||
# at the same time.
|
||||
|
||||
cd = {}
|
||||
t1 = self.StressThread(self, db1, stop, 1, cd, 1, 3)
|
||||
t2 = self.StressThread(self, db2, stop, 2, cd, 2, 3, 0.01)
|
||||
t3 = self.StressThread(self, db2, stop, 3, cd, 3, 3, 0.01)
|
||||
self.go(stop, cd, t1, t2, t3)
|
||||
|
||||
while db1.lastTransaction() != db2.lastTransaction():
|
||||
time.sleep(.1)
|
||||
|
||||
time.sleep(.1)
|
||||
cn = db1.open()
|
||||
tree = cn.root()["tree"]
|
||||
self._check_tree(cn, tree)
|
||||
self._check_threads(tree, t1, t2, t3)
|
||||
|
||||
cn.close()
|
||||
db1.close()
|
||||
db2.close()
|
||||
|
||||
def checkConcurrentLargeUpdates(self):
|
||||
# Use 3 threads like the 2StorageMT test above.
|
||||
self._storage = storage1 = self.openClientStorage()
|
||||
db1 = DB(storage1)
|
||||
db2 = DB(self.openClientStorage())
|
||||
stop = threading.Event()
|
||||
|
||||
cn = db1.open()
|
||||
tree = cn.root()["tree"] = OOBTree()
|
||||
for i in range(0, 3000, 2):
|
||||
tree[i] = 0
|
||||
transaction.commit()
|
||||
cn.close()
|
||||
|
||||
# Run three threads that update the BTree.
|
||||
# Two of the threads share a single storage so that it
|
||||
# is possible for both threads to read the same object
|
||||
# at the same time.
|
||||
|
||||
cd = {}
|
||||
t1 = LargeUpdatesThread(self, db1, stop, 1, cd, 1, 3, 0.02)
|
||||
t2 = LargeUpdatesThread(self, db2, stop, 2, cd, 2, 3, 0.01)
|
||||
t3 = LargeUpdatesThread(self, db2, stop, 3, cd, 3, 3, 0.01)
|
||||
self.go(stop, cd, t1, t2, t3)
|
||||
|
||||
while db1.lastTransaction() != db2.lastTransaction():
|
||||
db1._storage.sync()
|
||||
db2._storage.sync()
|
||||
|
||||
cn = db1.open()
|
||||
tree = cn.root()["tree"]
|
||||
self._check_tree(cn, tree)
|
||||
|
||||
# Purge the tree of the dummy entries mapping to 0.
|
||||
losers = [k for k, v in tree.items() if v == 0]
|
||||
for k in losers:
|
||||
del tree[k]
|
||||
transaction.commit()
|
||||
|
||||
self._check_threads(tree, t1, t2, t3)
|
||||
|
||||
cn.close()
|
||||
db1.close()
|
||||
db2.close()
|
||||
@@ -0,0 +1,223 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""ZEO iterator protocol tests."""
|
||||
|
||||
import transaction
|
||||
import six
|
||||
import gc
|
||||
|
||||
from ZODB.Connection import TransactionMetaData
|
||||
|
||||
from ..asyncio.testing import AsyncRPC
|
||||
|
||||
class IterationTests(object):
|
||||
|
||||
def _assertIteratorIdsEmpty(self):
|
||||
# Account for the need to run a GC collection
|
||||
# under non-refcounted implementations like PyPy
|
||||
# for storage._iterator_gc to fully do its job.
|
||||
# First, confirm that it ran
|
||||
self.assertTrue(self._storage._iterators._last_gc > 0)
|
||||
gc_enabled = gc.isenabled()
|
||||
# make sure there's no race conditions cleaning out the weak refs
|
||||
gc.disable()
|
||||
try:
|
||||
self.assertEqual(0, len(self._storage._iterator_ids))
|
||||
except AssertionError:
|
||||
# Ok, we have ids. That should also mean that the
|
||||
# weak dictionary has the same length.
|
||||
|
||||
self.assertEqual(len(self._storage._iterators),
|
||||
len(self._storage._iterator_ids))
|
||||
# Now if we do a collection and re-ask for iterator_gc
|
||||
# everything goes away as expected.
|
||||
gc.enable()
|
||||
gc.collect()
|
||||
gc.collect() # sometimes PyPy needs it twice to clear weak refs
|
||||
|
||||
self._storage._iterator_gc()
|
||||
|
||||
self.assertEqual(len(self._storage._iterators),
|
||||
len(self._storage._iterator_ids))
|
||||
self.assertEqual(0, len(self._storage._iterator_ids))
|
||||
finally:
|
||||
if gc_enabled:
|
||||
gc.enable()
|
||||
else:
|
||||
gc.disable()
|
||||
|
||||
def checkIteratorGCProtocol(self):
|
||||
# Test garbage collection on protocol level.
|
||||
server = AsyncRPC(self._storage._server)
|
||||
|
||||
iid = server.iterator_start(None, None)
|
||||
# None signals the end of iteration.
|
||||
self.assertEqual(None, server.iterator_next(iid))
|
||||
# The server has disposed the iterator already.
|
||||
self.assertRaises(KeyError, server.iterator_next, iid)
|
||||
|
||||
iid = server.iterator_start(None, None)
|
||||
# This time, we tell the server to throw the iterator away.
|
||||
server.iterator_gc([iid])
|
||||
self.assertRaises(KeyError, server.iterator_next, iid)
|
||||
|
||||
def checkIteratorExhaustionStorage(self):
|
||||
# Test the storage's garbage collection mechanism.
|
||||
self._dostore()
|
||||
iterator = self._storage.iterator()
|
||||
|
||||
# At this point, a wrapping iterator might not have called the CS
|
||||
# iterator yet. We'll consume one item to make sure this happens.
|
||||
six.advance_iterator(iterator)
|
||||
self.assertEqual(1, len(self._storage._iterator_ids))
|
||||
iid = list(self._storage._iterator_ids)[0]
|
||||
self.assertEqual([], list(iterator))
|
||||
self.assertEqual(0, len(self._storage._iterator_ids))
|
||||
|
||||
# The iterator has run through, so the server has already disposed it.
|
||||
self.assertRaises(KeyError, self._storage._call, 'iterator_next', iid)
|
||||
|
||||
def checkIteratorGCSpanTransactions(self):
|
||||
# Keep a hard reference to the iterator so it won't be automatically
|
||||
# garbage collected at the transaction boundary.
|
||||
self._dostore()
|
||||
iterator = self._storage.iterator()
|
||||
self._dostore()
|
||||
# As the iterator was not garbage collected, we can still use it. (We
|
||||
# don't see the transaction we just wrote being picked up, because
|
||||
# iterators only see the state from the point in time when they were
|
||||
# created.)
|
||||
self.assertTrue(list(iterator))
|
||||
|
||||
def checkIteratorGCStorageCommitting(self):
|
||||
# We want the iterator to be garbage-collected, so we don't keep any
|
||||
# hard references to it. The storage tracks its ID, though.
|
||||
|
||||
# The odd little jig we do below arises from the fact that the
|
||||
# CS iterator may not be constructed right away if the CS is wrapped.
|
||||
# We need to actually do some iteration to get the iterator created.
|
||||
# We do a store to make sure the iterator isn't exhausted right away.
|
||||
self._dostore()
|
||||
six.advance_iterator(self._storage.iterator())
|
||||
|
||||
self.assertEqual(1, len(self._storage._iterator_ids))
|
||||
iid = list(self._storage._iterator_ids)[0]
|
||||
|
||||
# GC happens at the transaction boundary. After that, both the storage
|
||||
# and the server have forgotten the iterator.
|
||||
self._storage._iterators._last_gc = -1
|
||||
self._dostore()
|
||||
self._assertIteratorIdsEmpty()
|
||||
self.assertRaises(KeyError, self._storage._call, 'iterator_next', iid)
|
||||
|
||||
def checkIteratorGCStorageTPCAborting(self):
|
||||
# The odd little jig we do below arises from the fact that the
|
||||
# CS iterator may not be constructed right away if the CS is wrapped.
|
||||
# We need to actually do some iteration to get the iterator created.
|
||||
# We do a store to make sure the iterator isn't exhausted right away.
|
||||
self._dostore()
|
||||
six.advance_iterator(self._storage.iterator())
|
||||
|
||||
iid = list(self._storage._iterator_ids)[0]
|
||||
|
||||
t = TransactionMetaData()
|
||||
self._storage._iterators._last_gc = -1
|
||||
self._storage.tpc_begin(t)
|
||||
self._storage.tpc_abort(t)
|
||||
self._assertIteratorIdsEmpty()
|
||||
self.assertRaises(KeyError, self._storage._call, 'iterator_next', iid)
|
||||
|
||||
def checkIteratorGCStorageDisconnect(self):
|
||||
|
||||
# The odd little jig we do below arises from the fact that the
|
||||
# CS iterator may not be constructed right away if the CS is wrapped.
|
||||
# We need to actually do some iteration to get the iterator created.
|
||||
# We do a store to make sure the iterator isn't exhausted right away.
|
||||
self._dostore()
|
||||
six.advance_iterator(self._storage.iterator())
|
||||
|
||||
iid = list(self._storage._iterator_ids)[0]
|
||||
t = TransactionMetaData()
|
||||
self._storage.tpc_begin(t)
|
||||
# Show that after disconnecting, the client side GCs the iterators
|
||||
# as well. I'm calling this directly to avoid accidentally
|
||||
# calling tpc_abort implicitly.
|
||||
self._storage.notify_disconnected()
|
||||
self.assertEqual(0, len(self._storage._iterator_ids))
|
||||
|
||||
def checkIteratorParallel(self):
|
||||
self._dostore()
|
||||
self._dostore()
|
||||
iter1 = self._storage.iterator()
|
||||
iter2 = self._storage.iterator()
|
||||
txn_info1 = six.advance_iterator(iter1)
|
||||
txn_info2 = six.advance_iterator(iter2)
|
||||
self.assertEqual(txn_info1.tid, txn_info2.tid)
|
||||
txn_info1 = six.advance_iterator(iter1)
|
||||
txn_info2 = six.advance_iterator(iter2)
|
||||
self.assertEqual(txn_info1.tid, txn_info2.tid)
|
||||
self.assertRaises(StopIteration, next, iter1)
|
||||
self.assertRaises(StopIteration, next, iter2)
|
||||
|
||||
|
||||
def iterator_sane_after_reconnect():
|
||||
r"""Make sure that iterators are invalidated on disconnect.
|
||||
|
||||
Start a server:
|
||||
|
||||
>>> addr, adminaddr = start_server(
|
||||
... '<filestorage>\npath fs\n</filestorage>', keep=1)
|
||||
|
||||
Open a client storage to it and commit a some transactions:
|
||||
|
||||
>>> import ZEO, ZODB, transaction
|
||||
>>> client = ZEO.client(addr)
|
||||
>>> db = ZODB.DB(client)
|
||||
>>> conn = db.open()
|
||||
>>> for i in range(10):
|
||||
... conn.root().i = i
|
||||
... transaction.commit()
|
||||
|
||||
Create an iterator:
|
||||
|
||||
>>> it = client.iterator()
|
||||
>>> tid1 = it.next().tid
|
||||
|
||||
Restart the storage:
|
||||
|
||||
>>> stop_server(adminaddr)
|
||||
>>> wait_disconnected(client)
|
||||
>>> _ = start_server('<filestorage>\npath fs\n</filestorage>', addr=addr)
|
||||
>>> wait_connected(client)
|
||||
|
||||
Now, we'll create a second iterator:
|
||||
|
||||
>>> it2 = client.iterator()
|
||||
|
||||
If we try to advance the first iterator, we should get an error:
|
||||
|
||||
>>> it.next().tid > tid1
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ClientDisconnected: Disconnected iterator
|
||||
|
||||
The second iterator should be peachy:
|
||||
|
||||
>>> it2.next().tid == tid1
|
||||
True
|
||||
|
||||
Cleanup:
|
||||
|
||||
>>> db.close()
|
||||
"""
|
||||
@@ -0,0 +1,57 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""A Thread base class for use with unittest."""
|
||||
import threading
|
||||
import sys
|
||||
import six
|
||||
|
||||
class TestThread(threading.Thread):
|
||||
"""Base class for defining threads that run from unittest.
|
||||
|
||||
The subclass should define a testrun() method instead of a run()
|
||||
method.
|
||||
|
||||
Call cleanup() when the test is done with the thread, instead of join().
|
||||
If the thread exits with an uncaught exception, it's captured and
|
||||
re-raised when cleanup() is called. cleanup() should be called by
|
||||
the main thread! Trying to tell unittest that a test failed from
|
||||
another thread creates a nightmare of timing-depending cascading
|
||||
failures and missed errors (tracebacks that show up on the screen,
|
||||
but don't cause unittest to believe the test failed).
|
||||
|
||||
cleanup() also joins the thread. If the thread ended without raising
|
||||
an uncaught exception, and the join doesn't succeed in the timeout
|
||||
period, then the test is made to fail with a "Thread still alive"
|
||||
message.
|
||||
"""
|
||||
|
||||
def __init__(self, testcase):
|
||||
threading.Thread.__init__(self)
|
||||
# In case this thread hangs, don't stop Python from exiting.
|
||||
self.setDaemon(1)
|
||||
self._exc_info = None
|
||||
self._testcase = testcase
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.testrun()
|
||||
except:
|
||||
self._exc_info = sys.exc_info()
|
||||
|
||||
def cleanup(self, timeout=15):
|
||||
self.join(timeout)
|
||||
if self._exc_info:
|
||||
six.reraise(self._exc_info[0], self._exc_info[1], self._exc_info[2])
|
||||
if self.isAlive():
|
||||
self._testcase.fail("Thread did not finish: %s" % self)
|
||||
@@ -0,0 +1,132 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Compromising positions involving threads."""
|
||||
|
||||
import threading
|
||||
|
||||
from ZODB.Connection import TransactionMetaData
|
||||
from ZODB.tests.StorageTestBase import zodb_pickle, MinPO
|
||||
import ZEO.Exceptions
|
||||
|
||||
ZERO = '\0'*8
|
||||
|
||||
class BasicThread(threading.Thread):
|
||||
def __init__(self, storage, doNextEvent, threadStartedEvent):
|
||||
self.storage = storage
|
||||
self.trans = TransactionMetaData()
|
||||
self.doNextEvent = doNextEvent
|
||||
self.threadStartedEvent = threadStartedEvent
|
||||
self.gotValueError = 0
|
||||
self.gotDisconnected = 0
|
||||
threading.Thread.__init__(self)
|
||||
self.setDaemon(1)
|
||||
|
||||
def join(self):
|
||||
threading.Thread.join(self, 10)
|
||||
assert not self.isAlive()
|
||||
|
||||
|
||||
class GetsThroughVoteThread(BasicThread):
|
||||
# This thread gets partially through a transaction before it turns
|
||||
# execution over to another thread. We're trying to establish that a
|
||||
# tpc_finish() after a storage has been closed by another thread will get
|
||||
# a ClientStorageError error.
|
||||
#
|
||||
# This class gets does a tpc_begin(), store(), tpc_vote() and is waiting
|
||||
# to do the tpc_finish() when the other thread closes the storage.
|
||||
def run(self):
|
||||
self.storage.tpc_begin(self.trans)
|
||||
oid = self.storage.new_oid()
|
||||
self.storage.store(oid, ZERO, zodb_pickle(MinPO("c")), '', self.trans)
|
||||
self.storage.tpc_vote(self.trans)
|
||||
self.threadStartedEvent.set()
|
||||
self.doNextEvent.wait(10)
|
||||
try:
|
||||
self.storage.tpc_finish(self.trans)
|
||||
except ZEO.Exceptions.ClientStorageError:
|
||||
self.gotValueError = 1
|
||||
self.storage.tpc_abort(self.trans)
|
||||
|
||||
|
||||
class GetsThroughBeginThread(BasicThread):
|
||||
# This class is like the above except that it is intended to be run when
|
||||
# another thread is already in a tpc_begin(). Thus, this thread will
|
||||
# block in the tpc_begin until another thread closes the storage. When
|
||||
# that happens, this one will get disconnected too.
|
||||
def run(self):
|
||||
try:
|
||||
self.storage.tpc_begin(self.trans)
|
||||
except ZEO.Exceptions.ClientStorageError:
|
||||
self.gotValueError = 1
|
||||
|
||||
|
||||
class ThreadTests(object):
|
||||
# Thread 1 should start a transaction, but not get all the way through it.
|
||||
# Main thread should close the connection. Thread 1 should then get
|
||||
# disconnected.
|
||||
def checkDisconnectedOnThread2Close(self):
|
||||
doNextEvent = threading.Event()
|
||||
threadStartedEvent = threading.Event()
|
||||
thread1 = GetsThroughVoteThread(self._storage,
|
||||
doNextEvent, threadStartedEvent)
|
||||
thread1.start()
|
||||
threadStartedEvent.wait(10)
|
||||
self._storage.close()
|
||||
doNextEvent.set()
|
||||
thread1.join()
|
||||
self.assertEqual(thread1.gotValueError, 1)
|
||||
|
||||
# Thread 1 should start a transaction, but not get all the way through
|
||||
# it. While thread 1 is in the middle of the transaction, a second thread
|
||||
# should start a transaction, and it will block in the tcp_begin() --
|
||||
# because thread 1 has acquired the lock in its tpc_begin(). Now the main
|
||||
# thread closes the storage and both sub-threads should get disconnected.
|
||||
def checkSecondBeginFails(self):
|
||||
doNextEvent = threading.Event()
|
||||
threadStartedEvent = threading.Event()
|
||||
thread1 = GetsThroughVoteThread(self._storage,
|
||||
doNextEvent, threadStartedEvent)
|
||||
thread2 = GetsThroughBeginThread(self._storage,
|
||||
doNextEvent, threadStartedEvent)
|
||||
thread1.start()
|
||||
threadStartedEvent.wait(1)
|
||||
thread2.start()
|
||||
self._storage.close()
|
||||
doNextEvent.set()
|
||||
thread1.join()
|
||||
thread2.join()
|
||||
self.assertEqual(thread1.gotValueError, 1)
|
||||
self.assertEqual(thread2.gotValueError, 1)
|
||||
|
||||
# Run a bunch of threads doing small and large stores in parallel
|
||||
def checkMTStores(self):
|
||||
threads = []
|
||||
for i in range(5):
|
||||
t = threading.Thread(target=self.mtstorehelper)
|
||||
threads.append(t)
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join(30)
|
||||
for i in threads:
|
||||
self.assertFalse(t.isAlive())
|
||||
|
||||
# Helper for checkMTStores
|
||||
def mtstorehelper(self):
|
||||
name = threading.currentThread().getName()
|
||||
objs = []
|
||||
for i in range(10):
|
||||
objs.append(MinPO("X" * 200000))
|
||||
objs.append(MinPO("X"))
|
||||
for obj in objs:
|
||||
self._dostore(data=obj)
|
||||
@@ -0,0 +1,10 @@
|
||||
======================
|
||||
Copy of ZEO 4 server
|
||||
======================
|
||||
|
||||
This copy was made by first converting the ZEO 4 server code to use
|
||||
relative imports. The code was tested with ZEO 4 before copying. It
|
||||
was unchanged aside from the relative imports.
|
||||
|
||||
The ZEO 4 server is used for tests if the ZEO4_SERVER environment
|
||||
variable is set to a non-empty value.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,30 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
_auth_modules = {}
|
||||
|
||||
def get_module(name):
|
||||
if name == 'sha':
|
||||
from auth_sha import StorageClass, SHAClient, Database
|
||||
return StorageClass, SHAClient, Database
|
||||
elif name == 'digest':
|
||||
from .auth_digest import StorageClass, DigestClient, DigestDatabase
|
||||
return StorageClass, DigestClient, DigestDatabase
|
||||
else:
|
||||
return _auth_modules.get(name)
|
||||
|
||||
def register_module(name, storage_class, client, db):
|
||||
if name in _auth_modules:
|
||||
raise TypeError("%s is already registred" % name)
|
||||
_auth_modules[name] = storage_class, client, db
|
||||
@@ -0,0 +1,142 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Digest authentication for ZEO
|
||||
|
||||
This authentication mechanism follows the design of HTTP digest
|
||||
authentication (RFC 2069). It is a simple challenge-response protocol
|
||||
that does not send passwords in the clear, but does not offer strong
|
||||
security. The RFC discusses many of the limitations of this kind of
|
||||
protocol.
|
||||
|
||||
Guard the password database as if it contained plaintext passwords.
|
||||
It stores the hash of a username and password. This does not expose
|
||||
the plaintext password, but it is sensitive nonetheless. An attacker
|
||||
with the hash can impersonate the real user. This is a limitation of
|
||||
the simple digest scheme.
|
||||
|
||||
HTTP is a stateless protocol, and ZEO is a stateful protocol. The
|
||||
security requirements are quite different as a result. The HTTP
|
||||
protocol uses a nonce as a challenge. The ZEO protocol requires a
|
||||
separate session key that is used for message authentication. We
|
||||
generate a second nonce for this purpose; the hash of nonce and
|
||||
user/realm/password is used as the session key.
|
||||
|
||||
TODO: I'm not sure if this is a sound approach; SRP would be preferred.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import time
|
||||
|
||||
from .base import Database, Client
|
||||
from ..StorageServer import ZEOStorage
|
||||
from ZEO.Exceptions import AuthError
|
||||
from ..hash import sha1
|
||||
|
||||
def get_random_bytes(n=8):
|
||||
try:
|
||||
b = os.urandom(n)
|
||||
except NotImplementedError:
|
||||
L = [chr(random.randint(0, 255)) for i in range(n)]
|
||||
b = b"".join(L)
|
||||
return b
|
||||
|
||||
def hexdigest(s):
|
||||
return sha1(s.encode()).hexdigest()
|
||||
|
||||
class DigestDatabase(Database):
|
||||
def __init__(self, filename, realm=None):
|
||||
Database.__init__(self, filename, realm)
|
||||
|
||||
# Initialize a key used to build the nonce for a challenge.
|
||||
# We need one key for the lifetime of the server, so it
|
||||
# is convenient to store in on the database.
|
||||
self.noncekey = get_random_bytes(8)
|
||||
|
||||
def _store_password(self, username, password):
|
||||
dig = hexdigest("%s:%s:%s" % (username, self.realm, password))
|
||||
self._users[username] = dig
|
||||
|
||||
def session_key(h_up, nonce):
|
||||
# The hash itself is a bit too short to be a session key.
|
||||
# HMAC wants a 64-byte key. We don't want to use h_up
|
||||
# directly because it would never change over time. Instead
|
||||
# use the hash plus part of h_up.
|
||||
return (sha1(("%s:%s" % (h_up, nonce)).encode('latin-1')).digest() +
|
||||
h_up.encode('utf-8')[:44])
|
||||
|
||||
class StorageClass(ZEOStorage):
|
||||
def set_database(self, database):
|
||||
assert isinstance(database, DigestDatabase)
|
||||
self.database = database
|
||||
self.noncekey = database.noncekey
|
||||
|
||||
def _get_time(self):
|
||||
# Return a string representing the current time.
|
||||
t = int(time.time())
|
||||
return struct.pack("i", t)
|
||||
|
||||
def _get_nonce(self):
|
||||
# RFC 2069 recommends a nonce of the form
|
||||
# H(client-IP ":" time-stamp ":" private-key)
|
||||
dig = sha1()
|
||||
dig.update(str(self.connection.addr).encode('latin-1'))
|
||||
dig.update(self._get_time())
|
||||
dig.update(self.noncekey)
|
||||
return dig.hexdigest()
|
||||
|
||||
def auth_get_challenge(self):
|
||||
"""Return realm, challenge, and nonce."""
|
||||
self._challenge = self._get_nonce()
|
||||
self._key_nonce = self._get_nonce()
|
||||
return self.auth_realm, self._challenge, self._key_nonce
|
||||
|
||||
def auth_response(self, resp):
|
||||
# verify client response
|
||||
user, challenge, response = resp
|
||||
|
||||
# Since zrpc is a stateful protocol, we just store the nonce
|
||||
# we sent to the client. It will need to generate a new
|
||||
# nonce for a new connection anyway.
|
||||
if self._challenge != challenge:
|
||||
raise ValueError("invalid challenge")
|
||||
|
||||
# lookup user in database
|
||||
h_up = self.database.get_password(user)
|
||||
|
||||
# regeneration resp from user, password, and nonce
|
||||
check = hexdigest("%s:%s" % (h_up, challenge))
|
||||
if check == response:
|
||||
self.connection.setSessionKey(session_key(h_up, self._key_nonce))
|
||||
return self._finish_auth(check == response)
|
||||
|
||||
extensions = [auth_get_challenge, auth_response]
|
||||
|
||||
class DigestClient(Client):
|
||||
extensions = ["auth_get_challenge", "auth_response"]
|
||||
|
||||
def start(self, username, realm, password):
|
||||
_realm, challenge, nonce = self.stub.auth_get_challenge()
|
||||
if _realm != realm:
|
||||
raise AuthError("expected realm %r, got realm %r"
|
||||
% (_realm, realm))
|
||||
h_up = hexdigest("%s:%s:%s" % (username, realm, password))
|
||||
|
||||
resp_dig = hexdigest("%s:%s" % (h_up, challenge))
|
||||
result = self.stub.auth_response((username, challenge, resp_dig))
|
||||
if result:
|
||||
return session_key(h_up, nonce)
|
||||
else:
|
||||
return None
|
||||
@@ -0,0 +1,139 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Base classes for defining an authentication protocol.
|
||||
|
||||
Database -- abstract base class for password database
|
||||
Client -- abstract base class for authentication client
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
from ..hash import sha1
|
||||
|
||||
class Client(object):
|
||||
# Subclass should override to list the names of methods that
|
||||
# will be called on the server.
|
||||
extensions = []
|
||||
|
||||
def __init__(self, stub):
|
||||
self.stub = stub
|
||||
for m in self.extensions:
|
||||
setattr(self.stub, m, self.stub.extensionMethod(m))
|
||||
|
||||
def sort(L):
|
||||
"""Sort a list in-place and return it."""
|
||||
L.sort()
|
||||
return L
|
||||
|
||||
class Database(object):
|
||||
"""Abstracts a password database.
|
||||
|
||||
This class is used both in the authentication process (via
|
||||
get_password()) and by client scripts that manage the password
|
||||
database file.
|
||||
|
||||
The password file is a simple, colon-separated text file mapping
|
||||
usernames to password hashes. The hashes are SHA hex digests
|
||||
produced from the password string.
|
||||
"""
|
||||
realm = None
|
||||
def __init__(self, filename, realm=None):
|
||||
"""Creates a new Database
|
||||
|
||||
filename: a string containing the full pathname of
|
||||
the password database file. Must be readable by the user
|
||||
running ZEO. Must be writeable by any client script that
|
||||
accesses the database.
|
||||
|
||||
realm: the realm name (a string)
|
||||
"""
|
||||
self._users = {}
|
||||
self.filename = filename
|
||||
self.load()
|
||||
if realm:
|
||||
if self.realm and self.realm != realm:
|
||||
raise ValueError("Specified realm %r differs from database "
|
||||
"realm %r" % (realm or '', self.realm))
|
||||
else:
|
||||
self.realm = realm
|
||||
|
||||
def save(self, fd=None):
|
||||
filename = self.filename
|
||||
needs_closed = False
|
||||
if not fd:
|
||||
fd = open(filename, 'w')
|
||||
needs_closed = True
|
||||
|
||||
try:
|
||||
if self.realm:
|
||||
print("realm", self.realm, file=fd)
|
||||
|
||||
for username in sorted(self._users.keys()):
|
||||
print("%s: %s" % (username, self._users[username]), file=fd)
|
||||
finally:
|
||||
if needs_closed:
|
||||
fd.close()
|
||||
|
||||
def load(self):
|
||||
filename = self.filename
|
||||
if not filename:
|
||||
return
|
||||
|
||||
if not os.path.exists(filename):
|
||||
return
|
||||
|
||||
with open(filename) as fd:
|
||||
L = fd.readlines()
|
||||
|
||||
if not L:
|
||||
return
|
||||
|
||||
if L[0].startswith("realm "):
|
||||
line = L.pop(0).strip()
|
||||
self.realm = line[len("realm "):]
|
||||
|
||||
for line in L:
|
||||
username, hash = line.strip().split(":", 1)
|
||||
self._users[username] = hash.strip()
|
||||
|
||||
def _store_password(self, username, password):
|
||||
self._users[username] = self.hash(password)
|
||||
|
||||
def get_password(self, username):
|
||||
"""Returns password hash for specified username.
|
||||
|
||||
Callers must check for LookupError, which is raised in
|
||||
the case of a non-existent user specified."""
|
||||
if username not in self._users:
|
||||
raise LookupError("No such user: %s" % username)
|
||||
return self._users[username]
|
||||
|
||||
def hash(self, s):
|
||||
return sha1(s.encode()).hexdigest()
|
||||
|
||||
def add_user(self, username, password):
|
||||
if username in self._users:
|
||||
raise LookupError("User %s already exists" % username)
|
||||
self._store_password(username, password)
|
||||
|
||||
def del_user(self, username):
|
||||
if username not in self._users:
|
||||
raise LookupError("No such user: %s" % username)
|
||||
del self._users[username]
|
||||
|
||||
def change_password(self, username, password):
|
||||
if username not in self._users:
|
||||
raise LookupError("No such user: %s" % username)
|
||||
self._store_password(username, password)
|
||||
@@ -0,0 +1,99 @@
|
||||
"""HMAC (Keyed-Hashing for Message Authentication) Python module.
|
||||
|
||||
Implements the HMAC algorithm as described by RFC 2104.
|
||||
"""
|
||||
from six.moves import map
|
||||
from six.moves import zip
|
||||
|
||||
def _strxor(s1, s2):
|
||||
"""Utility method. XOR the two strings s1 and s2 (must have same length).
|
||||
"""
|
||||
return "".join(map(lambda x, y: chr(ord(x) ^ ord(y)), s1, s2))
|
||||
|
||||
# The size of the digests returned by HMAC depends on the underlying
|
||||
# hashing module used.
|
||||
digest_size = None
|
||||
|
||||
class HMAC(object):
|
||||
"""RFC2104 HMAC class.
|
||||
|
||||
This supports the API for Cryptographic Hash Functions (PEP 247).
|
||||
"""
|
||||
|
||||
def __init__(self, key, msg = None, digestmod = None):
|
||||
"""Create a new HMAC object.
|
||||
|
||||
key: key for the keyed hash object.
|
||||
msg: Initial input for the hash, if provided.
|
||||
digestmod: A module supporting PEP 247. Defaults to the md5 module.
|
||||
"""
|
||||
if digestmod is None:
|
||||
import md5
|
||||
digestmod = md5
|
||||
|
||||
self.digestmod = digestmod
|
||||
self.outer = digestmod.new()
|
||||
self.inner = digestmod.new()
|
||||
self.digest_size = digestmod.digest_size
|
||||
|
||||
blocksize = 64
|
||||
ipad = "\x36" * blocksize
|
||||
opad = "\x5C" * blocksize
|
||||
|
||||
if len(key) > blocksize:
|
||||
key = digestmod.new(key).digest()
|
||||
|
||||
key = key + chr(0) * (blocksize - len(key))
|
||||
self.outer.update(_strxor(key, opad))
|
||||
self.inner.update(_strxor(key, ipad))
|
||||
if msg is not None:
|
||||
self.update(msg)
|
||||
|
||||
## def clear(self):
|
||||
## raise NotImplementedError("clear() method not available in HMAC.")
|
||||
|
||||
def update(self, msg):
|
||||
"""Update this hashing object with the string msg.
|
||||
"""
|
||||
self.inner.update(msg)
|
||||
|
||||
def copy(self):
|
||||
"""Return a separate copy of this hashing object.
|
||||
|
||||
An update to this copy won't affect the original object.
|
||||
"""
|
||||
other = HMAC("")
|
||||
other.digestmod = self.digestmod
|
||||
other.inner = self.inner.copy()
|
||||
other.outer = self.outer.copy()
|
||||
return other
|
||||
|
||||
def digest(self):
|
||||
"""Return the hash value of this hashing object.
|
||||
|
||||
This returns a string containing 8-bit data. The object is
|
||||
not altered in any way by this function; you can continue
|
||||
updating the object after calling this function.
|
||||
"""
|
||||
h = self.outer.copy()
|
||||
h.update(self.inner.digest())
|
||||
return h.digest()
|
||||
|
||||
def hexdigest(self):
|
||||
"""Like digest(), but returns a string of hexadecimal digits instead.
|
||||
"""
|
||||
return "".join([hex(ord(x))[2:].zfill(2)
|
||||
for x in tuple(self.digest())])
|
||||
|
||||
def new(key, msg = None, digestmod = None):
|
||||
"""Create a new hashing object and return it.
|
||||
|
||||
key: The starting key for the hash.
|
||||
msg: if available, will immediately be hashed into the object's starting
|
||||
state.
|
||||
|
||||
You can now feed arbitrary strings into the object using its update()
|
||||
method, and can ask for the hash value at any time by calling its digest()
|
||||
method.
|
||||
"""
|
||||
return HMAC(key, msg, digestmod)
|
||||
@@ -0,0 +1,127 @@
|
||||
<component>
|
||||
|
||||
<sectiontype name="zeo">
|
||||
|
||||
<description>
|
||||
The content of a ZEO section describe operational parameters
|
||||
of a ZEO server except for the storage(s) to be served.
|
||||
</description>
|
||||
|
||||
<key name="address" datatype="socket-binding-address"
|
||||
required="yes">
|
||||
<description>
|
||||
The address at which the server should listen. This can be in
|
||||
the form 'host:port' to signify a TCP/IP connection or a
|
||||
pathname string to signify a Unix domain socket connection (at
|
||||
least one '/' is required). A hostname may be a DNS name or a
|
||||
dotted IP address. If the hostname is omitted, the platform's
|
||||
default behavior is used when binding the listening socket (''
|
||||
is passed to socket.bind() as the hostname portion of the
|
||||
address).
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="read-only" datatype="boolean"
|
||||
required="no"
|
||||
default="false">
|
||||
<description>
|
||||
Flag indicating whether the server should operate in read-only
|
||||
mode. Defaults to false. Note that even if the server is
|
||||
operating in writable mode, individual storages may still be
|
||||
read-only. But if the server is in read-only mode, no write
|
||||
operations are allowed, even if the storages are writable. Note
|
||||
that pack() is considered a read-only operation.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="invalidation-queue-size" datatype="integer"
|
||||
required="no"
|
||||
default="100">
|
||||
<description>
|
||||
The storage server keeps a queue of the objects modified by the
|
||||
last N transactions, where N == invalidation_queue_size. This
|
||||
queue is used to speed client cache verification when a client
|
||||
disconnects for a short period of time.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="invalidation-age" datatype="float" required="no">
|
||||
<description>
|
||||
The maximum age of a client for which quick-verification
|
||||
invalidations will be provided by iterating over the served
|
||||
storage. This option should only be used if the served storage
|
||||
supports efficient iteration from a starting point near the
|
||||
end of the transaction history (e.g. end of file).
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="monitor-address" datatype="socket-binding-address"
|
||||
required="no">
|
||||
<description>
|
||||
The address at which the monitor server should listen. If
|
||||
specified, a monitor server is started. The monitor server
|
||||
provides server statistics in a simple text format. This can
|
||||
be in the form 'host:port' to signify a TCP/IP connection or a
|
||||
pathname string to signify a Unix domain socket connection (at
|
||||
least one '/' is required). A hostname may be a DNS name or a
|
||||
dotted IP address. If the hostname is omitted, the platform's
|
||||
default behavior is used when binding the listening socket (''
|
||||
is passed to socket.bind() as the hostname portion of the
|
||||
address).
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="transaction-timeout" datatype="integer"
|
||||
required="no">
|
||||
<description>
|
||||
The maximum amount of time to wait for a transaction to commit
|
||||
after acquiring the storage lock, specified in seconds. If the
|
||||
transaction takes too long, the client connection will be closed
|
||||
and the transaction aborted.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="authentication-protocol" required="no">
|
||||
<description>
|
||||
The name of the protocol used for authentication. The
|
||||
only protocol provided with ZEO is "digest," but extensions
|
||||
may provide other protocols.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="authentication-database" required="no">
|
||||
<description>
|
||||
The path of the database containing authentication credentials.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="authentication-realm" required="no">
|
||||
<description>
|
||||
The authentication realm of the server. Some authentication
|
||||
schemes use a realm to identify the logical set of usernames
|
||||
that are accepted by this server.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="pid-filename" datatype="existing-dirpath"
|
||||
required="no">
|
||||
<description>
|
||||
The full path to the file in which to write the ZEO server's Process ID
|
||||
at startup. If omitted, $INSTANCE/var/ZEO.pid is used.
|
||||
</description>
|
||||
<metadefault>$INSTANCE/var/ZEO.pid (or $clienthome/ZEO.pid)</metadefault>
|
||||
</key>
|
||||
|
||||
<!-- DM 2006-06-12: added option -->
|
||||
<key name="drop-cache-rather-verify" datatype="boolean"
|
||||
required="no" default="false">
|
||||
<description>
|
||||
indicates that the cache should be dropped rather than
|
||||
verified when the verification optimization is not
|
||||
available (e.g. when the ZEO server restarted).
|
||||
</description>
|
||||
</key>
|
||||
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,27 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""In Python 2.6, the "sha" and "md5" modules have been deprecated
|
||||
in favor of using hashlib for both. This class allows for compatibility
|
||||
between versions."""
|
||||
|
||||
try:
|
||||
import hashlib
|
||||
sha1 = hashlib.sha1
|
||||
new = sha1
|
||||
except ImportError:
|
||||
import sha
|
||||
sha1 = sha.new
|
||||
new = sha1
|
||||
digest_size = sha.digest_size
|
||||
@@ -0,0 +1,190 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Monitor behavior of ZEO server and record statistics.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
|
||||
import asyncore
|
||||
import socket
|
||||
import time
|
||||
import logging
|
||||
|
||||
zeo_version = 'unknown'
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
zeo_dist = pkg_resources.working_set.find(
|
||||
pkg_resources.Requirement.parse('ZODB3')
|
||||
)
|
||||
if zeo_dist is not None:
|
||||
zeo_version = zeo_dist.version
|
||||
|
||||
class StorageStats(object):
|
||||
"""Per-storage usage statistics."""
|
||||
|
||||
def __init__(self, connections=None):
|
||||
self.connections = connections
|
||||
self.loads = 0
|
||||
self.stores = 0
|
||||
self.commits = 0
|
||||
self.aborts = 0
|
||||
self.active_txns = 0
|
||||
self.verifying_clients = 0
|
||||
self.lock_time = None
|
||||
self.conflicts = 0
|
||||
self.conflicts_resolved = 0
|
||||
self.start = time.ctime()
|
||||
|
||||
@property
|
||||
def clients(self):
|
||||
return len(self.connections)
|
||||
|
||||
def parse(self, s):
|
||||
# parse the dump format
|
||||
lines = s.split("\n")
|
||||
for line in lines:
|
||||
field, value = line.split(":", 1)
|
||||
if field == "Server started":
|
||||
self.start = value
|
||||
elif field == "Clients":
|
||||
# Hack because we use this both on the server and on
|
||||
# the client where there are no connections.
|
||||
self.connections = [0] * int(value)
|
||||
elif field == "Clients verifying":
|
||||
self.verifying_clients = int(value)
|
||||
elif field == "Active transactions":
|
||||
self.active_txns = int(value)
|
||||
elif field == "Commit lock held for":
|
||||
# This assumes
|
||||
self.lock_time = time.time() - int(value)
|
||||
elif field == "Commits":
|
||||
self.commits = int(value)
|
||||
elif field == "Aborts":
|
||||
self.aborts = int(value)
|
||||
elif field == "Loads":
|
||||
self.loads = int(value)
|
||||
elif field == "Stores":
|
||||
self.stores = int(value)
|
||||
elif field == "Conflicts":
|
||||
self.conflicts = int(value)
|
||||
elif field == "Conflicts resolved":
|
||||
self.conflicts_resolved = int(value)
|
||||
|
||||
def dump(self, f):
|
||||
print("Server started:", self.start, file=f)
|
||||
print("Clients:", self.clients, file=f)
|
||||
print("Clients verifying:", self.verifying_clients, file=f)
|
||||
print("Active transactions:", self.active_txns, file=f)
|
||||
if self.lock_time:
|
||||
howlong = time.time() - self.lock_time
|
||||
print("Commit lock held for:", int(howlong), file=f)
|
||||
print("Commits:", self.commits, file=f)
|
||||
print("Aborts:", self.aborts, file=f)
|
||||
print("Loads:", self.loads, file=f)
|
||||
print("Stores:", self.stores, file=f)
|
||||
print("Conflicts:", self.conflicts, file=f)
|
||||
print("Conflicts resolved:", self.conflicts_resolved, file=f)
|
||||
|
||||
class StatsClient(asyncore.dispatcher):
|
||||
|
||||
def __init__(self, sock, addr):
|
||||
asyncore.dispatcher.__init__(self, sock)
|
||||
self.buf = []
|
||||
self.closed = 0
|
||||
|
||||
def close(self):
|
||||
self.closed = 1
|
||||
# The socket is closed after all the data is written.
|
||||
# See handle_write().
|
||||
|
||||
def write(self, s):
|
||||
self.buf.append(s)
|
||||
|
||||
def writable(self):
|
||||
return len(self.buf)
|
||||
|
||||
def readable(self):
|
||||
return 0
|
||||
|
||||
def handle_write(self):
|
||||
s = "".join(self.buf)
|
||||
self.buf = []
|
||||
n = self.socket.send(s.encode('ascii'))
|
||||
if n < len(s):
|
||||
self.buf.append(s[:n])
|
||||
|
||||
if self.closed and not self.buf:
|
||||
asyncore.dispatcher.close(self)
|
||||
|
||||
class StatsServer(asyncore.dispatcher):
|
||||
|
||||
StatsConnectionClass = StatsClient
|
||||
|
||||
def __init__(self, addr, stats):
|
||||
asyncore.dispatcher.__init__(self)
|
||||
self.addr = addr
|
||||
self.stats = stats
|
||||
if type(self.addr) == tuple:
|
||||
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
else:
|
||||
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.set_reuse_addr()
|
||||
logger = logging.getLogger('ZEO.monitor')
|
||||
logger.info("listening on %s", repr(self.addr))
|
||||
self.bind(self.addr)
|
||||
self.listen(5)
|
||||
|
||||
def writable(self):
|
||||
return 0
|
||||
|
||||
def readable(self):
|
||||
return 1
|
||||
|
||||
def handle_accept(self):
|
||||
try:
|
||||
sock, addr = self.accept()
|
||||
except socket.error:
|
||||
return
|
||||
f = self.StatsConnectionClass(sock, addr)
|
||||
self.dump(f)
|
||||
f.close()
|
||||
|
||||
def dump(self, f):
|
||||
print("ZEO monitor server version %s" % zeo_version, file=f)
|
||||
print(time.ctime(), file=f)
|
||||
print(file=f)
|
||||
|
||||
L = sorted(self.stats.keys())
|
||||
for k in L:
|
||||
stats = self.stats[k]
|
||||
print("Storage:", k, file=f)
|
||||
stats.dump(f)
|
||||
print(file=f)
|
||||
@@ -0,0 +1,396 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Start the ZEO storage server.
|
||||
|
||||
Usage: %s [-C URL] [-a ADDRESS] [-f FILENAME] [-h]
|
||||
|
||||
Options:
|
||||
-C/--configuration URL -- configuration file or URL
|
||||
-a/--address ADDRESS -- server address of the form PORT, HOST:PORT, or PATH
|
||||
(a PATH must contain at least one "/")
|
||||
-f/--filename FILENAME -- filename for FileStorage
|
||||
-t/--timeout TIMEOUT -- transaction timeout in seconds (default no timeout)
|
||||
-h/--help -- print this usage message and exit
|
||||
-m/--monitor ADDRESS -- address of monitor server ([HOST:]PORT or PATH)
|
||||
--pid-file PATH -- relative path to output file containing this process's pid;
|
||||
default $(INSTANCE_HOME)/var/ZEO.pid but only if envar
|
||||
INSTANCE_HOME is defined
|
||||
|
||||
Unless -C is specified, -a and -f are required.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
|
||||
# The code here is designed to be reused by other, similar servers.
|
||||
# For the forseeable future, it must work under Python 2.1 as well as
|
||||
# 2.2 and above.
|
||||
|
||||
import asyncore
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import socket
|
||||
import logging
|
||||
|
||||
import ZConfig.datatypes
|
||||
from zdaemon.zdoptions import ZDOptions
|
||||
|
||||
logger = logging.getLogger('ZEO.runzeo')
|
||||
_pid = str(os.getpid())
|
||||
|
||||
def log(msg, level=logging.INFO, exc_info=False):
|
||||
"""Internal: generic logging function."""
|
||||
message = "(%s) %s" % (_pid, msg)
|
||||
logger.log(level, message, exc_info=exc_info)
|
||||
|
||||
def parse_binding_address(arg):
|
||||
# Caution: Not part of the official ZConfig API.
|
||||
obj = ZConfig.datatypes.SocketBindingAddress(arg)
|
||||
return obj.family, obj.address
|
||||
|
||||
def windows_shutdown_handler():
|
||||
# Called by the signal mechanism on Windows to perform shutdown.
|
||||
import asyncore
|
||||
asyncore.close_all()
|
||||
|
||||
class ZEOOptionsMixin(object):
|
||||
|
||||
storages = None
|
||||
|
||||
def handle_address(self, arg):
|
||||
self.family, self.address = parse_binding_address(arg)
|
||||
|
||||
def handle_monitor_address(self, arg):
|
||||
self.monitor_family, self.monitor_address = parse_binding_address(arg)
|
||||
|
||||
def handle_filename(self, arg):
|
||||
from ZODB.config import FileStorage # That's a FileStorage *opener*!
|
||||
class FSConfig(object):
|
||||
def __init__(self, name, path):
|
||||
self._name = name
|
||||
self.path = path
|
||||
self.stop = None
|
||||
def getSectionName(self):
|
||||
return self._name
|
||||
if not self.storages:
|
||||
self.storages = []
|
||||
name = str(1 + len(self.storages))
|
||||
conf = FileStorage(FSConfig(name, arg))
|
||||
self.storages.append(conf)
|
||||
|
||||
testing_exit_immediately = False
|
||||
def handle_test(self, *args):
|
||||
self.testing_exit_immediately = True
|
||||
|
||||
def add_zeo_options(self):
|
||||
self.add(None, None, None, "test", self.handle_test)
|
||||
self.add(None, None, "a:", "address=", self.handle_address)
|
||||
self.add(None, None, "f:", "filename=", self.handle_filename)
|
||||
self.add("family", "zeo.address.family")
|
||||
self.add("address", "zeo.address.address",
|
||||
required="no server address specified; use -a or -C")
|
||||
self.add("read_only", "zeo.read_only", default=0)
|
||||
self.add("invalidation_queue_size", "zeo.invalidation_queue_size",
|
||||
default=100)
|
||||
self.add("invalidation_age", "zeo.invalidation_age")
|
||||
self.add("transaction_timeout", "zeo.transaction_timeout",
|
||||
"t:", "timeout=", float)
|
||||
self.add("monitor_address", "zeo.monitor_address.address",
|
||||
"m:", "monitor=", self.handle_monitor_address)
|
||||
self.add('auth_protocol', 'zeo.authentication_protocol',
|
||||
None, 'auth-protocol=', default=None)
|
||||
self.add('auth_database', 'zeo.authentication_database',
|
||||
None, 'auth-database=')
|
||||
self.add('auth_realm', 'zeo.authentication_realm',
|
||||
None, 'auth-realm=')
|
||||
self.add('pid_file', 'zeo.pid_filename',
|
||||
None, 'pid-file=')
|
||||
|
||||
class ZEOOptions(ZDOptions, ZEOOptionsMixin):
|
||||
|
||||
__doc__ = __doc__
|
||||
|
||||
logsectionname = "eventlog"
|
||||
schemadir = os.path.dirname(__file__)
|
||||
|
||||
def __init__(self):
|
||||
ZDOptions.__init__(self)
|
||||
self.add_zeo_options()
|
||||
self.add("storages", "storages",
|
||||
required="no storages specified; use -f or -C")
|
||||
|
||||
def realize(self, *a, **k):
|
||||
ZDOptions.realize(self, *a, **k)
|
||||
nunnamed = [s for s in self.storages if s.name is None]
|
||||
if nunnamed:
|
||||
if len(nunnamed) > 1:
|
||||
return self.usage("No more than one storage may be unnamed.")
|
||||
if [s for s in self.storages if s.name == '1']:
|
||||
return self.usage(
|
||||
"Can't have an unnamed storage and a storage named 1.")
|
||||
for s in self.storages:
|
||||
if s.name is None:
|
||||
s.name = '1'
|
||||
break
|
||||
|
||||
|
||||
class ZEOServer(object):
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
|
||||
def main(self):
|
||||
self.setup_default_logging()
|
||||
self.check_socket()
|
||||
self.clear_socket()
|
||||
self.make_pidfile()
|
||||
try:
|
||||
self.open_storages()
|
||||
self.setup_signals()
|
||||
self.create_server()
|
||||
self.loop_forever()
|
||||
finally:
|
||||
self.server.close()
|
||||
self.clear_socket()
|
||||
self.remove_pidfile()
|
||||
|
||||
def setup_default_logging(self):
|
||||
if self.options.config_logger is not None:
|
||||
return
|
||||
# No log file is configured; default to stderr.
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.INFO)
|
||||
fmt = logging.Formatter(
|
||||
"------\n%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
"%Y-%m-%dT%H:%M:%S")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(fmt)
|
||||
root.addHandler(handler)
|
||||
|
||||
def check_socket(self):
|
||||
if (isinstance(self.options.address, tuple) and
|
||||
self.options.address[1] is None):
|
||||
self.options.address = self.options.address[0], 0
|
||||
return
|
||||
if self.can_connect(self.options.family, self.options.address):
|
||||
self.options.usage("address %s already in use" %
|
||||
repr(self.options.address))
|
||||
|
||||
def can_connect(self, family, address):
|
||||
s = socket.socket(family, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.connect(address)
|
||||
except socket.error:
|
||||
return 0
|
||||
else:
|
||||
s.close()
|
||||
return 1
|
||||
|
||||
def clear_socket(self):
|
||||
if isinstance(self.options.address, type("")):
|
||||
try:
|
||||
os.unlink(self.options.address)
|
||||
except os.error:
|
||||
pass
|
||||
|
||||
def open_storages(self):
|
||||
self.storages = {}
|
||||
for opener in self.options.storages:
|
||||
log("opening storage %r using %s"
|
||||
% (opener.name, opener.__class__.__name__))
|
||||
self.storages[opener.name] = opener.open()
|
||||
|
||||
def setup_signals(self):
|
||||
"""Set up signal handlers.
|
||||
|
||||
The signal handler for SIGFOO is a method handle_sigfoo().
|
||||
If no handler method is defined for a signal, the signal
|
||||
action is not changed from its initial value. The handler
|
||||
method is called without additional arguments.
|
||||
"""
|
||||
if os.name != "posix":
|
||||
if os.name == "nt":
|
||||
self.setup_win32_signals()
|
||||
return
|
||||
if hasattr(signal, 'SIGXFSZ'):
|
||||
signal.signal(signal.SIGXFSZ, signal.SIG_IGN) # Special case
|
||||
init_signames()
|
||||
for sig, name in signames.items():
|
||||
method = getattr(self, "handle_" + name.lower(), None)
|
||||
if method is not None:
|
||||
def wrapper(sig_dummy, frame_dummy, method=method):
|
||||
method()
|
||||
signal.signal(sig, wrapper)
|
||||
|
||||
def setup_win32_signals(self):
|
||||
# Borrow the Zope Signals package win32 support, if available.
|
||||
# Signals does a check/log for the availability of pywin32.
|
||||
try:
|
||||
import Signals.Signals
|
||||
except ImportError:
|
||||
logger.debug("Signals package not found. "
|
||||
"Windows-specific signal handler "
|
||||
"will *not* be installed.")
|
||||
return
|
||||
SignalHandler = Signals.Signals.SignalHandler
|
||||
if SignalHandler is not None: # may be None if no pywin32.
|
||||
SignalHandler.registerHandler(signal.SIGTERM,
|
||||
windows_shutdown_handler)
|
||||
SignalHandler.registerHandler(signal.SIGINT,
|
||||
windows_shutdown_handler)
|
||||
SIGUSR2 = 12 # not in signal module on Windows.
|
||||
SignalHandler.registerHandler(SIGUSR2, self.handle_sigusr2)
|
||||
|
||||
def create_server(self):
|
||||
self.server = create_server(self.storages, self.options)
|
||||
|
||||
def loop_forever(self):
|
||||
if self.options.testing_exit_immediately:
|
||||
print("testing exit immediately")
|
||||
else:
|
||||
self.server.loop()
|
||||
|
||||
def handle_sigterm(self):
|
||||
log("terminated by SIGTERM")
|
||||
sys.exit(0)
|
||||
|
||||
def handle_sigint(self):
|
||||
log("terminated by SIGINT")
|
||||
sys.exit(0)
|
||||
|
||||
def handle_sighup(self):
|
||||
log("restarted by SIGHUP")
|
||||
sys.exit(1)
|
||||
|
||||
def handle_sigusr2(self):
|
||||
# log rotation signal - do the same as Zope 2.7/2.8...
|
||||
if self.options.config_logger is None or os.name not in ("posix", "nt"):
|
||||
log("received SIGUSR2, but it was not handled!",
|
||||
level=logging.WARNING)
|
||||
return
|
||||
|
||||
loggers = [self.options.config_logger]
|
||||
|
||||
if os.name == "posix":
|
||||
for l in loggers:
|
||||
l.reopen()
|
||||
log("Log files reopened successfully", level=logging.INFO)
|
||||
else: # nt - same rotation code as in Zope's Signals/Signals.py
|
||||
for l in loggers:
|
||||
for f in l.handler_factories:
|
||||
handler = f()
|
||||
if hasattr(handler, 'rotate') and callable(handler.rotate):
|
||||
handler.rotate()
|
||||
log("Log files rotation complete", level=logging.INFO)
|
||||
|
||||
def _get_pidfile(self):
|
||||
pidfile = self.options.pid_file
|
||||
# 'pidfile' is marked as not required.
|
||||
if not pidfile:
|
||||
# Try to find a reasonable location if the pidfile is not
|
||||
# set. If we are running in a Zope environment, we can
|
||||
# safely assume INSTANCE_HOME.
|
||||
instance_home = os.environ.get("INSTANCE_HOME")
|
||||
if not instance_home:
|
||||
# If all our attempts failed, just log a message and
|
||||
# proceed.
|
||||
logger.debug("'pidfile' option not set, and 'INSTANCE_HOME' "
|
||||
"environment variable could not be found. "
|
||||
"Cannot guess pidfile location.")
|
||||
return
|
||||
self.options.pid_file = os.path.join(instance_home,
|
||||
"var", "ZEO.pid")
|
||||
|
||||
def make_pidfile(self):
|
||||
if not self.options.read_only:
|
||||
self._get_pidfile()
|
||||
pidfile = self.options.pid_file
|
||||
if pidfile is None:
|
||||
return
|
||||
pid = os.getpid()
|
||||
try:
|
||||
if os.path.exists(pidfile):
|
||||
os.unlink(pidfile)
|
||||
f = open(pidfile, 'w')
|
||||
print(pid, file=f)
|
||||
f.close()
|
||||
log("created PID file '%s'" % pidfile)
|
||||
except IOError:
|
||||
logger.error("PID file '%s' cannot be opened" % pidfile)
|
||||
|
||||
def remove_pidfile(self):
|
||||
if not self.options.read_only:
|
||||
pidfile = self.options.pid_file
|
||||
if pidfile is None:
|
||||
return
|
||||
try:
|
||||
if os.path.exists(pidfile):
|
||||
os.unlink(pidfile)
|
||||
log("removed PID file '%s'" % pidfile)
|
||||
except IOError:
|
||||
logger.error("PID file '%s' could not be removed" % pidfile)
|
||||
|
||||
|
||||
def create_server(storages, options):
|
||||
from .StorageServer import StorageServer
|
||||
return StorageServer(
|
||||
options.address,
|
||||
storages,
|
||||
read_only = options.read_only,
|
||||
invalidation_queue_size = options.invalidation_queue_size,
|
||||
invalidation_age = options.invalidation_age,
|
||||
transaction_timeout = options.transaction_timeout,
|
||||
monitor_address = options.monitor_address,
|
||||
auth_protocol = options.auth_protocol,
|
||||
auth_database = options.auth_database,
|
||||
auth_realm = options.auth_realm,
|
||||
)
|
||||
|
||||
|
||||
# Signal names
|
||||
|
||||
signames = None
|
||||
|
||||
def signame(sig):
|
||||
"""Return a symbolic name for a signal.
|
||||
|
||||
Return "signal NNN" if there is no corresponding SIG name in the
|
||||
signal module.
|
||||
"""
|
||||
|
||||
if signames is None:
|
||||
init_signames()
|
||||
return signames.get(sig) or "signal %d" % sig
|
||||
|
||||
def init_signames():
|
||||
global signames
|
||||
signames = {}
|
||||
for name, sig in signal.__dict__.items():
|
||||
k_startswith = getattr(name, "startswith", None)
|
||||
if k_startswith is None:
|
||||
continue
|
||||
if k_startswith("SIG") and not k_startswith("SIG_"):
|
||||
signames[sig] = name
|
||||
|
||||
|
||||
# Main program
|
||||
|
||||
def main(args=None):
|
||||
options = ZEOOptions()
|
||||
options.realize(args)
|
||||
s = ZEOServer(options)
|
||||
s.main()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,40 @@
|
||||
<schema>
|
||||
|
||||
<!-- note that zeoctl.xml is a closely related schema which should
|
||||
match this schema, but should require the "runner" section -->
|
||||
|
||||
<description>
|
||||
This schema describes the configuration of the ZEO storage server
|
||||
process.
|
||||
</description>
|
||||
|
||||
<!-- Use the storage types defined by ZODB. -->
|
||||
<import package="ZODB"/>
|
||||
|
||||
<!-- Use the ZEO server information structure. -->
|
||||
<import package="ZEO.tests.ZEO4"/>
|
||||
|
||||
<import package="ZConfig.components.logger"/>
|
||||
|
||||
<!-- runner control -->
|
||||
<import package="zdaemon"/>
|
||||
|
||||
|
||||
<section type="zeo" name="*" required="yes" attribute="zeo" />
|
||||
|
||||
<section type="runner" name="*" required="no" attribute="runner" />
|
||||
|
||||
<multisection name="*" type="ZODB.storage"
|
||||
attribute="storages"
|
||||
required="yes">
|
||||
<description>
|
||||
One or more storages that are provided by the ZEO server. The
|
||||
section names are used as the storage names, and must be unique
|
||||
within each ZEO storage server. Traditionally, these names
|
||||
represent small integers starting at '1'.
|
||||
</description>
|
||||
</multisection>
|
||||
|
||||
<section name="*" type="eventlog" attribute="eventlog" required="no" />
|
||||
|
||||
</schema>
|
||||
@@ -0,0 +1,24 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
# zrpc is a package with the following modules
|
||||
# client -- manages connection creation to remote server
|
||||
# connection -- object dispatcher
|
||||
# log -- logging helper
|
||||
# error -- exceptions raised by zrpc
|
||||
# marshal -- internal, handles basic protocol issues
|
||||
# server -- manages incoming connections from remote clients
|
||||
# smac -- sized message async connections
|
||||
# trigger -- medusa's trigger
|
||||
|
||||
# zrpc is not an advertised subpackage of ZEO; its interfaces are internal
|
||||
@@ -0,0 +1,106 @@
|
||||
# This file is a slightly modified copy of Python 2.3's Lib/hmac.py.
|
||||
# This file is under the Python Software Foundation (PSF) license.
|
||||
|
||||
"""HMAC (Keyed-Hashing for Message Authentication) Python module.
|
||||
|
||||
Implements the HMAC algorithm as described by RFC 2104.
|
||||
"""
|
||||
from six.moves import map
|
||||
from six.moves import zip
|
||||
|
||||
def _strxor(s1, s2):
|
||||
"""Utility method. XOR the two strings s1 and s2 (must have same length).
|
||||
"""
|
||||
return "".join(map(lambda x, y: chr(ord(x) ^ ord(y)), s1, s2))
|
||||
|
||||
# The size of the digests returned by HMAC depends on the underlying
|
||||
# hashing module used.
|
||||
digest_size = None
|
||||
|
||||
class HMAC(object):
|
||||
"""RFC2104 HMAC class.
|
||||
|
||||
This supports the API for Cryptographic Hash Functions (PEP 247).
|
||||
"""
|
||||
|
||||
def __init__(self, key, msg = None, digestmod = None):
|
||||
"""Create a new HMAC object.
|
||||
|
||||
key: key for the keyed hash object.
|
||||
msg: Initial input for the hash, if provided.
|
||||
digestmod: A module supporting PEP 247. Defaults to the md5 module.
|
||||
"""
|
||||
if digestmod is None:
|
||||
import md5
|
||||
digestmod = md5
|
||||
|
||||
self.digestmod = digestmod
|
||||
self.outer = digestmod.new()
|
||||
self.inner = digestmod.new()
|
||||
# Python 2.1 and 2.2 differ about the correct spelling
|
||||
try:
|
||||
self.digest_size = digestmod.digestsize
|
||||
except AttributeError:
|
||||
self.digest_size = digestmod.digest_size
|
||||
|
||||
blocksize = 64
|
||||
ipad = "\x36" * blocksize
|
||||
opad = "\x5C" * blocksize
|
||||
|
||||
if len(key) > blocksize:
|
||||
key = digestmod.new(key).digest()
|
||||
|
||||
key = key + chr(0) * (blocksize - len(key))
|
||||
self.outer.update(_strxor(key, opad))
|
||||
self.inner.update(_strxor(key, ipad))
|
||||
if msg is not None:
|
||||
self.update(msg)
|
||||
|
||||
## def clear(self):
|
||||
## raise NotImplementedError("clear() method not available in HMAC.")
|
||||
|
||||
def update(self, msg):
|
||||
"""Update this hashing object with the string msg.
|
||||
"""
|
||||
self.inner.update(msg)
|
||||
|
||||
def copy(self):
|
||||
"""Return a separate copy of this hashing object.
|
||||
|
||||
An update to this copy won't affect the original object.
|
||||
"""
|
||||
other = HMAC("")
|
||||
other.digestmod = self.digestmod
|
||||
other.inner = self.inner.copy()
|
||||
other.outer = self.outer.copy()
|
||||
return other
|
||||
|
||||
def digest(self):
|
||||
"""Return the hash value of this hashing object.
|
||||
|
||||
This returns a string containing 8-bit data. The object is
|
||||
not altered in any way by this function; you can continue
|
||||
updating the object after calling this function.
|
||||
"""
|
||||
h = self.outer.copy()
|
||||
h.update(self.inner.digest())
|
||||
return h.digest()
|
||||
|
||||
def hexdigest(self):
|
||||
"""Like digest(), but returns a string of hexadecimal digits instead.
|
||||
"""
|
||||
return "".join([hex(ord(x))[2:].zfill(2)
|
||||
for x in tuple(self.digest())])
|
||||
|
||||
def new(key, msg = None, digestmod = None):
|
||||
"""Create a new hashing object and return it.
|
||||
|
||||
key: The starting key for the hash.
|
||||
msg: if available, will immediately be hashed into the object's starting
|
||||
state.
|
||||
|
||||
You can now feed arbitrary strings into the object using its update()
|
||||
method, and can ask for the hash value at any time by calling its digest()
|
||||
method.
|
||||
"""
|
||||
return HMAC(key, msg, digestmod)
|
||||
@@ -0,0 +1,655 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 asyncore
|
||||
import errno
|
||||
import logging
|
||||
import select
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from . import trigger
|
||||
|
||||
|
||||
from .connection import ManagedClientConnection
|
||||
from .log import log
|
||||
from .error import DisconnectedError
|
||||
|
||||
from ZODB.POSException import ReadOnlyError
|
||||
from ZODB.loglevels import BLATHER
|
||||
from six.moves import map
|
||||
|
||||
|
||||
def client_timeout():
|
||||
return 30.0
|
||||
|
||||
def client_loop(map):
|
||||
read = asyncore.read
|
||||
write = asyncore.write
|
||||
_exception = asyncore._exception
|
||||
|
||||
while map:
|
||||
try:
|
||||
|
||||
# The next two lines intentionally don't use
|
||||
# iterators. Other threads can close dispatchers, causeing
|
||||
# the socket map to shrink.
|
||||
r = e = map.keys()
|
||||
w = [fd for (fd, obj) in map.items() if obj.writable()]
|
||||
|
||||
try:
|
||||
r, w, e = select.select(r, w, e, client_timeout())
|
||||
except (select.error, RuntimeError) as err:
|
||||
# Python >= 3.3 makes select.error an alias of OSError,
|
||||
# which is not subscriptable but does have the 'errno' attribute
|
||||
err_errno = getattr(err, 'errno', None) or err[0]
|
||||
if err_errno != errno.EINTR:
|
||||
if err_errno == errno.EBADF:
|
||||
|
||||
# If a connection is closed while we are
|
||||
# calling select on it, we can get a bad
|
||||
# file-descriptor error. We'll check for this
|
||||
# case by looking for entries in r and w that
|
||||
# are not in the socket map.
|
||||
|
||||
if [fd for fd in r if fd not in map]:
|
||||
continue
|
||||
if [fd for fd in w if fd not in map]:
|
||||
continue
|
||||
|
||||
# Hm, on Mac OS X, we could get a run time
|
||||
# error and end up here, but retrying select
|
||||
# would work. Let's try:
|
||||
select.select(r, w, e, 0)
|
||||
# we survived, keep going :)
|
||||
continue
|
||||
|
||||
raise
|
||||
else:
|
||||
continue
|
||||
|
||||
if not map:
|
||||
break
|
||||
|
||||
if not (r or w or e):
|
||||
# The line intentionally doesn't use iterators. Other
|
||||
# threads can close dispatchers, causeing the socket
|
||||
# map to shrink.
|
||||
for obj in map.values():
|
||||
if isinstance(obj, ManagedClientConnection):
|
||||
# Send a heartbeat message as a reply to a
|
||||
# non-existent message id.
|
||||
try:
|
||||
obj.send_reply(-1, None)
|
||||
except DisconnectedError:
|
||||
pass
|
||||
continue
|
||||
|
||||
for fd in r:
|
||||
obj = map.get(fd)
|
||||
if obj is None:
|
||||
continue
|
||||
read(obj)
|
||||
|
||||
for fd in w:
|
||||
obj = map.get(fd)
|
||||
if obj is None:
|
||||
continue
|
||||
write(obj)
|
||||
|
||||
for fd in e:
|
||||
obj = map.get(fd)
|
||||
if obj is None:
|
||||
continue
|
||||
_exception(obj)
|
||||
|
||||
except:
|
||||
if map:
|
||||
try:
|
||||
logging.getLogger(__name__+'.client_loop').critical(
|
||||
'A ZEO client loop failed.',
|
||||
exc_info=sys.exc_info())
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
for fd, obj in map.items():
|
||||
if not hasattr(obj, 'mgr'):
|
||||
continue
|
||||
try:
|
||||
obj.mgr.client.close()
|
||||
except:
|
||||
map.pop(fd, None)
|
||||
try:
|
||||
logging.getLogger(__name__+'.client_loop'
|
||||
).critical(
|
||||
"Couldn't close a dispatcher.",
|
||||
exc_info=sys.exc_info())
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionManager(object):
|
||||
"""Keeps a connection up over time"""
|
||||
|
||||
sync_wait = 30
|
||||
|
||||
def __init__(self, addrs, client, tmin=1, tmax=180):
|
||||
self.client = client
|
||||
self._start_asyncore_loop()
|
||||
self.addrlist = self._parse_addrs(addrs)
|
||||
self.tmin = min(tmin, tmax)
|
||||
self.tmax = tmax
|
||||
self.cond = threading.Condition(threading.Lock())
|
||||
self.connection = None # Protected by self.cond
|
||||
self.closed = 0
|
||||
# If thread is not None, then there is a helper thread
|
||||
# attempting to connect.
|
||||
self.thread = None # Protected by self.cond
|
||||
|
||||
def new_addrs(self, addrs):
|
||||
self.addrlist = self._parse_addrs(addrs)
|
||||
|
||||
def _start_asyncore_loop(self):
|
||||
self.map = {}
|
||||
self.trigger = trigger.trigger(self.map)
|
||||
self.loop_thread = threading.Thread(
|
||||
name="%s zeo client networking thread" % self.client.__name__,
|
||||
target=client_loop, args=(self.map,))
|
||||
self.loop_thread.setDaemon(True)
|
||||
self.loop_thread.start()
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s for %s>" % (self.__class__.__name__, self.addrlist)
|
||||
|
||||
def _parse_addrs(self, addrs):
|
||||
# Return a list of (addr_type, addr) pairs.
|
||||
|
||||
# For backwards compatibility (and simplicity?) the
|
||||
# constructor accepts a single address in the addrs argument --
|
||||
# a string for a Unix domain socket or a 2-tuple with a
|
||||
# hostname and port. It can also accept a list of such addresses.
|
||||
|
||||
addr_type = self._guess_type(addrs)
|
||||
if addr_type is not None:
|
||||
return [(addr_type, addrs)]
|
||||
else:
|
||||
addrlist = []
|
||||
for addr in addrs:
|
||||
addr_type = self._guess_type(addr)
|
||||
if addr_type is None:
|
||||
raise ValueError("unknown address in list: %s" % repr(addr))
|
||||
addrlist.append((addr_type, addr))
|
||||
return addrlist
|
||||
|
||||
def _guess_type(self, addr):
|
||||
if isinstance(addr, str):
|
||||
return socket.AF_UNIX
|
||||
|
||||
if (len(addr) == 2
|
||||
and isinstance(addr[0], str)
|
||||
and isinstance(addr[1], int)):
|
||||
return socket.AF_INET # also denotes IPv6
|
||||
|
||||
# not anything I know about
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
"""Prevent ConnectionManager from opening new connections"""
|
||||
self.closed = 1
|
||||
self.cond.acquire()
|
||||
try:
|
||||
t = self.thread
|
||||
self.thread = None
|
||||
finally:
|
||||
self.cond.release()
|
||||
if t is not None:
|
||||
log("CM.close(): stopping and joining thread")
|
||||
t.stop()
|
||||
t.join(30)
|
||||
if t.isAlive():
|
||||
log("CM.close(): self.thread.join() timed out",
|
||||
level=logging.WARNING)
|
||||
|
||||
for fd, obj in list(self.map.items()):
|
||||
if obj is not self.trigger:
|
||||
try:
|
||||
obj.close()
|
||||
except:
|
||||
logging.getLogger(__name__+'.'+self.__class__.__name__
|
||||
).critical(
|
||||
"Couldn't close a dispatcher.",
|
||||
exc_info=sys.exc_info())
|
||||
|
||||
self.map.clear()
|
||||
self.trigger.pull_trigger()
|
||||
try:
|
||||
self.loop_thread.join(9)
|
||||
except RuntimeError:
|
||||
pass # we are the thread :)
|
||||
self.trigger.close()
|
||||
|
||||
def attempt_connect(self):
|
||||
"""Attempt a connection to the server without blocking too long.
|
||||
|
||||
There isn't a crisp definition for too long. When a
|
||||
ClientStorage is created, it attempts to connect to the
|
||||
server. If the server isn't immediately available, it can
|
||||
operate from the cache. This method will start the background
|
||||
connection thread and wait a little while to see if it
|
||||
finishes quickly.
|
||||
"""
|
||||
|
||||
# Will a single attempt take too long?
|
||||
# Answer: it depends -- normally, you'll connect or get a
|
||||
# connection refused error very quickly. Packet-eating
|
||||
# firewalls and other mishaps may cause the connect to take a
|
||||
# long time to time out though. It's also possible that you
|
||||
# connect quickly to a slow server, and the attempt includes
|
||||
# at least one roundtrip to the server (the register() call).
|
||||
# But that's as fast as you can expect it to be.
|
||||
self.connect()
|
||||
self.cond.acquire()
|
||||
try:
|
||||
t = self.thread
|
||||
conn = self.connection
|
||||
finally:
|
||||
self.cond.release()
|
||||
if t is not None and conn is None:
|
||||
event = t.one_attempt
|
||||
event.wait()
|
||||
self.cond.acquire()
|
||||
try:
|
||||
conn = self.connection
|
||||
finally:
|
||||
self.cond.release()
|
||||
return conn is not None
|
||||
|
||||
def connect(self, sync=0):
|
||||
self.cond.acquire()
|
||||
try:
|
||||
if self.connection is not None:
|
||||
return
|
||||
t = self.thread
|
||||
if t is None:
|
||||
log("CM.connect(): starting ConnectThread")
|
||||
self.thread = t = ConnectThread(self, self.client)
|
||||
t.setDaemon(1)
|
||||
t.start()
|
||||
if sync:
|
||||
while self.connection is None and t.isAlive():
|
||||
self.cond.wait(self.sync_wait)
|
||||
if self.connection is None:
|
||||
log("CM.connect(sync=1): still waiting...")
|
||||
assert self.connection is not None
|
||||
finally:
|
||||
self.cond.release()
|
||||
|
||||
def connect_done(self, conn, preferred):
|
||||
# Called by ConnectWrapper.notify_client() after notifying the client
|
||||
log("CM.connect_done(preferred=%s)" % preferred)
|
||||
self.cond.acquire()
|
||||
try:
|
||||
self.connection = conn
|
||||
if preferred:
|
||||
self.thread = None
|
||||
self.cond.notifyAll() # Wake up connect(sync=1)
|
||||
finally:
|
||||
self.cond.release()
|
||||
|
||||
def close_conn(self, conn):
|
||||
# Called by the connection when it is closed
|
||||
self.cond.acquire()
|
||||
try:
|
||||
if conn is not self.connection:
|
||||
# Closing a non-current connection
|
||||
log("CM.close_conn() non-current", level=BLATHER)
|
||||
return
|
||||
log("CM.close_conn()")
|
||||
self.connection = None
|
||||
finally:
|
||||
self.cond.release()
|
||||
self.client.notifyDisconnected()
|
||||
if not self.closed:
|
||||
self.connect()
|
||||
|
||||
def is_connected(self):
|
||||
self.cond.acquire()
|
||||
try:
|
||||
return self.connection is not None
|
||||
finally:
|
||||
self.cond.release()
|
||||
|
||||
# When trying to do a connect on a non-blocking socket, some outcomes
|
||||
# are expected. Set _CONNECT_IN_PROGRESS to the errno value(s) expected
|
||||
# when an initial connect can't complete immediately. Set _CONNECT_OK
|
||||
# to the errno value(s) expected if the connect succeeds *or* if it's
|
||||
# already connected (our code can attempt redundant connects).
|
||||
if hasattr(errno, "WSAEWOULDBLOCK"): # Windows
|
||||
# Caution: The official Winsock docs claim that WSAEALREADY should be
|
||||
# treated as yet another "in progress" indicator, but we've never
|
||||
# seen this.
|
||||
_CONNECT_IN_PROGRESS = (errno.WSAEWOULDBLOCK,)
|
||||
# Win98: WSAEISCONN; Win2K: WSAEINVAL
|
||||
_CONNECT_OK = (0, errno.WSAEISCONN, errno.WSAEINVAL)
|
||||
else: # Unix
|
||||
_CONNECT_IN_PROGRESS = (errno.EINPROGRESS,)
|
||||
_CONNECT_OK = (0, errno.EISCONN)
|
||||
|
||||
class ConnectThread(threading.Thread):
|
||||
"""Thread that tries to connect to server given one or more addresses.
|
||||
|
||||
The thread is passed a ConnectionManager and the manager's client
|
||||
as arguments. It calls testConnection() on the client when a
|
||||
socket connects; that should return 1 or 0 indicating whether this
|
||||
is a preferred or a fallback connection. It may also raise an
|
||||
exception, in which case the connection is abandoned.
|
||||
|
||||
The thread will continue to run, attempting connections, until a
|
||||
preferred connection is seen and successfully handed over to the
|
||||
manager and client.
|
||||
|
||||
As soon as testConnection() finds a preferred connection, or after
|
||||
all sockets have been tried and at least one fallback connection
|
||||
has been seen, notifyConnected(connection) is called on the client
|
||||
and connect_done() on the manager. If this was a preferred
|
||||
connection, the thread then exits; otherwise, it keeps trying
|
||||
until it gets a preferred connection, and then reconnects the
|
||||
client using that connection.
|
||||
|
||||
"""
|
||||
|
||||
__super_init = threading.Thread.__init__
|
||||
|
||||
# We don't expect clients to call any methods of this Thread other
|
||||
# than close() and those defined by the Thread API.
|
||||
|
||||
def __init__(self, mgr, client):
|
||||
self.__super_init(name="Connect(%s)" % mgr.addrlist)
|
||||
self.mgr = mgr
|
||||
self.client = client
|
||||
self.stopped = 0
|
||||
self.one_attempt = threading.Event()
|
||||
# A ConnectThread keeps track of whether it has finished a
|
||||
# call to try_connecting(). This allows the ConnectionManager
|
||||
# to make an attempt to connect right away, but not block for
|
||||
# too long if the server isn't immediately available.
|
||||
|
||||
def stop(self):
|
||||
self.stopped = 1
|
||||
|
||||
def run(self):
|
||||
delay = self.mgr.tmin
|
||||
success = 0
|
||||
# Don't wait too long the first time.
|
||||
# TODO: make timeout configurable?
|
||||
attempt_timeout = 5
|
||||
while not self.stopped:
|
||||
success = self.try_connecting(attempt_timeout)
|
||||
if not self.one_attempt.isSet():
|
||||
self.one_attempt.set()
|
||||
attempt_timeout = 75
|
||||
if success > 0:
|
||||
break
|
||||
time.sleep(delay)
|
||||
if self.mgr.is_connected():
|
||||
log("CT: still trying to replace fallback connection",
|
||||
level=logging.INFO)
|
||||
delay = min(delay*2, self.mgr.tmax)
|
||||
log("CT: exiting thread: %s" % self.getName())
|
||||
|
||||
def try_connecting(self, timeout):
|
||||
"""Try connecting to all self.mgr.addrlist addresses.
|
||||
|
||||
Return 1 if a preferred connection was found; 0 if no
|
||||
connection was found; and -1 if a fallback connection was
|
||||
found.
|
||||
|
||||
If no connection is found within timeout seconds, return 0.
|
||||
"""
|
||||
log("CT: attempting to connect on %d sockets" % len(self.mgr.addrlist))
|
||||
deadline = time.time() + timeout
|
||||
wrappers = self._create_wrappers()
|
||||
for wrap in wrappers.keys():
|
||||
if wrap.state == "notified":
|
||||
return 1
|
||||
try:
|
||||
if time.time() > deadline:
|
||||
return 0
|
||||
r = self._connect_wrappers(wrappers, deadline)
|
||||
if r is not None:
|
||||
return r
|
||||
if time.time() > deadline:
|
||||
return 0
|
||||
r = self._fallback_wrappers(wrappers, deadline)
|
||||
if r is not None:
|
||||
return r
|
||||
# Alas, no luck.
|
||||
assert not wrappers
|
||||
finally:
|
||||
for wrap in wrappers.keys():
|
||||
wrap.close()
|
||||
del wrappers
|
||||
return 0
|
||||
|
||||
def _expand_addrlist(self):
|
||||
for domain, addr in self.mgr.addrlist:
|
||||
# AF_INET really means either IPv4 or IPv6, possibly
|
||||
# indirected by DNS. By design, DNS lookup is deferred
|
||||
# until connections get established, so that DNS
|
||||
# reconfiguration can affect failover
|
||||
if domain == socket.AF_INET:
|
||||
host, port = addr
|
||||
for (family, socktype, proto, cannoname, sockaddr
|
||||
) in socket.getaddrinfo(host or 'localhost', port,
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM
|
||||
): # prune non-TCP results
|
||||
# for IPv6, drop flowinfo, and restrict addresses
|
||||
# to [host]:port
|
||||
yield family, sockaddr[:2]
|
||||
else:
|
||||
yield domain, addr
|
||||
|
||||
def _create_wrappers(self):
|
||||
# Create socket wrappers
|
||||
wrappers = {} # keys are active wrappers
|
||||
for domain, addr in self._expand_addrlist():
|
||||
wrap = ConnectWrapper(domain, addr, self.mgr, self.client)
|
||||
wrap.connect_procedure()
|
||||
if wrap.state == "notified":
|
||||
for w in wrappers.keys():
|
||||
w.close()
|
||||
return {wrap: wrap}
|
||||
if wrap.state != "closed":
|
||||
wrappers[wrap] = wrap
|
||||
return wrappers
|
||||
|
||||
def _connect_wrappers(self, wrappers, deadline):
|
||||
# Next wait until they all actually connect (or fail)
|
||||
# The deadline is necessary, because we'd wait forever if a
|
||||
# sockets never connects or fails.
|
||||
while wrappers:
|
||||
if self.stopped:
|
||||
for wrap in wrappers.keys():
|
||||
wrap.close()
|
||||
return 0
|
||||
# Select connecting wrappers
|
||||
connecting = [wrap
|
||||
for wrap in wrappers.keys()
|
||||
if wrap.state == "connecting"]
|
||||
if not connecting:
|
||||
break
|
||||
if time.time() > deadline:
|
||||
break
|
||||
try:
|
||||
r, w, x = select.select([], connecting, connecting, 1.0)
|
||||
log("CT: select() %d, %d, %d" % tuple(map(len, (r,w,x))))
|
||||
except select.error as msg:
|
||||
log("CT: select failed; msg=%s" % str(msg),
|
||||
level=logging.WARNING)
|
||||
continue
|
||||
# Exceptable wrappers are in trouble; close these suckers
|
||||
for wrap in x:
|
||||
log("CT: closing troubled socket %s" % str(wrap.addr))
|
||||
del wrappers[wrap]
|
||||
wrap.close()
|
||||
# Writable sockets are connected
|
||||
for wrap in w:
|
||||
wrap.connect_procedure()
|
||||
if wrap.state == "notified":
|
||||
del wrappers[wrap] # Don't close this one
|
||||
for wrap in wrappers.keys():
|
||||
wrap.close()
|
||||
return 1
|
||||
if wrap.state == "closed":
|
||||
del wrappers[wrap]
|
||||
|
||||
def _fallback_wrappers(self, wrappers, deadline):
|
||||
# If we've got wrappers left at this point, they're fallback
|
||||
# connections. Try notifying them until one succeeds.
|
||||
for wrap in list(wrappers.keys()):
|
||||
assert wrap.state == "tested" and wrap.preferred == 0
|
||||
if self.mgr.is_connected():
|
||||
wrap.close()
|
||||
else:
|
||||
wrap.notify_client()
|
||||
if wrap.state == "notified":
|
||||
del wrappers[wrap] # Don't close this one
|
||||
for wrap in wrappers.keys():
|
||||
wrap.close()
|
||||
return -1
|
||||
assert wrap.state == "closed"
|
||||
del wrappers[wrap]
|
||||
|
||||
# TODO: should check deadline
|
||||
|
||||
|
||||
class ConnectWrapper(object):
|
||||
"""An object that handles the connection procedure for one socket.
|
||||
|
||||
This is a little state machine with states:
|
||||
closed
|
||||
opened
|
||||
connecting
|
||||
connected
|
||||
tested
|
||||
notified
|
||||
"""
|
||||
|
||||
def __init__(self, domain, addr, mgr, client):
|
||||
"""Store arguments and create non-blocking socket."""
|
||||
self.domain = domain
|
||||
self.addr = addr
|
||||
self.mgr = mgr
|
||||
self.client = client
|
||||
# These attributes are part of the interface
|
||||
self.state = "closed"
|
||||
self.sock = None
|
||||
self.conn = None
|
||||
self.preferred = 0
|
||||
log("CW: attempt to connect to %s" % repr(addr))
|
||||
try:
|
||||
self.sock = socket.socket(domain, socket.SOCK_STREAM)
|
||||
except socket.error as err:
|
||||
log("CW: can't create socket, domain=%s: %s" % (domain, err),
|
||||
level=logging.ERROR)
|
||||
self.close()
|
||||
return
|
||||
self.sock.setblocking(0)
|
||||
self.state = "opened"
|
||||
|
||||
def connect_procedure(self):
|
||||
"""Call sock.connect_ex(addr) and interpret result."""
|
||||
if self.state in ("opened", "connecting"):
|
||||
try:
|
||||
err = self.sock.connect_ex(self.addr)
|
||||
except socket.error as msg:
|
||||
log("CW: connect_ex(%r) failed: %s" % (self.addr, msg),
|
||||
level=logging.ERROR)
|
||||
self.close()
|
||||
return
|
||||
log("CW: connect_ex(%s) returned %s" %
|
||||
(self.addr, errno.errorcode.get(err) or str(err)))
|
||||
if err in _CONNECT_IN_PROGRESS:
|
||||
self.state = "connecting"
|
||||
return
|
||||
if err not in _CONNECT_OK:
|
||||
log("CW: error connecting to %s: %s" %
|
||||
(self.addr, errno.errorcode.get(err) or str(err)),
|
||||
level=logging.WARNING)
|
||||
self.close()
|
||||
return
|
||||
self.state = "connected"
|
||||
if self.state == "connected":
|
||||
self.test_connection()
|
||||
|
||||
def test_connection(self):
|
||||
"""Establish and test a connection at the zrpc level.
|
||||
|
||||
Call the client's testConnection(), giving the client a chance
|
||||
to do app-level check of the connection.
|
||||
"""
|
||||
self.conn = ManagedClientConnection(self.sock, self.addr, self.mgr)
|
||||
self.sock = None # The socket is now owned by the connection
|
||||
try:
|
||||
self.preferred = self.client.testConnection(self.conn)
|
||||
self.state = "tested"
|
||||
except ReadOnlyError:
|
||||
log("CW: ReadOnlyError in testConnection (%s)" % repr(self.addr))
|
||||
self.close()
|
||||
return
|
||||
except:
|
||||
log("CW: error in testConnection (%s)" % repr(self.addr),
|
||||
level=logging.ERROR, exc_info=True)
|
||||
self.close()
|
||||
return
|
||||
if self.preferred:
|
||||
self.notify_client()
|
||||
|
||||
def notify_client(self):
|
||||
"""Call the client's notifyConnected().
|
||||
|
||||
If this succeeds, call the manager's connect_done().
|
||||
|
||||
If the client is already connected, we assume it's a fallback
|
||||
connection, and the new connection must be a preferred
|
||||
connection. The client will close the old connection.
|
||||
"""
|
||||
try:
|
||||
self.client.notifyConnected(self.conn)
|
||||
except:
|
||||
log("CW: error in notifyConnected (%s)" % repr(self.addr),
|
||||
level=logging.ERROR, exc_info=True)
|
||||
self.close()
|
||||
return
|
||||
self.state = "notified"
|
||||
self.mgr.connect_done(self.conn, self.preferred)
|
||||
|
||||
def close(self):
|
||||
"""Close the socket and reset everything."""
|
||||
self.state = "closed"
|
||||
self.mgr = self.client = None
|
||||
self.preferred = 0
|
||||
if self.conn is not None:
|
||||
# Closing the ZRPC connection will eventually close the
|
||||
# socket, somewhere in asyncore. Guido asks: Why do we care?
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
if self.sock is not None:
|
||||
self.sock.close()
|
||||
self.sock = None
|
||||
|
||||
def fileno(self):
|
||||
return self.sock.fileno()
|
||||
@@ -0,0 +1,846 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 asyncore
|
||||
import errno
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import logging
|
||||
from . import marshal
|
||||
from . import trigger
|
||||
|
||||
from . import smac
|
||||
from .error import ZRPCError, DisconnectedError
|
||||
from .log import short_repr, log
|
||||
from ZODB.loglevels import BLATHER, TRACE
|
||||
import ZODB.POSException
|
||||
|
||||
REPLY = ".reply" # message name used for replies
|
||||
|
||||
exception_type_type = type(Exception)
|
||||
|
||||
debug_zrpc = False
|
||||
|
||||
class Delay(object):
|
||||
"""Used to delay response to client for synchronous calls.
|
||||
|
||||
When a synchronous call is made and the original handler returns
|
||||
without handling the call, it returns a Delay object that prevents
|
||||
the mainloop from sending a response.
|
||||
"""
|
||||
|
||||
msgid = conn = sent = None
|
||||
|
||||
def set_sender(self, msgid, conn):
|
||||
self.msgid = msgid
|
||||
self.conn = conn
|
||||
|
||||
def reply(self, obj):
|
||||
self.sent = 'reply'
|
||||
self.conn.send_reply(self.msgid, obj)
|
||||
|
||||
def error(self, exc_info):
|
||||
self.sent = 'error'
|
||||
log("Error raised in delayed method", logging.ERROR, exc_info=exc_info)
|
||||
self.conn.return_error(self.msgid, *exc_info[:2])
|
||||
|
||||
def __repr__(self):
|
||||
return "%s[%s, %r, %r, %r]" % (
|
||||
self.__class__.__name__, id(self), self.msgid, self.conn, self.sent)
|
||||
|
||||
class Result(Delay):
|
||||
|
||||
def __init__(self, *args):
|
||||
self.args = args
|
||||
|
||||
def set_sender(self, msgid, conn):
|
||||
reply, callback = self.args
|
||||
conn.send_reply(msgid, reply, False)
|
||||
callback()
|
||||
|
||||
class MTDelay(Delay):
|
||||
|
||||
def __init__(self):
|
||||
self.ready = threading.Event()
|
||||
|
||||
def set_sender(self, *args):
|
||||
Delay.set_sender(self, *args)
|
||||
self.ready.set()
|
||||
|
||||
def reply(self, obj):
|
||||
self.ready.wait()
|
||||
self.conn.call_from_thread(self.conn.send_reply, self.msgid, obj)
|
||||
|
||||
def error(self, exc_info):
|
||||
self.ready.wait()
|
||||
log("Error raised in delayed method", logging.ERROR, exc_info=exc_info)
|
||||
self.conn.call_from_thread(Delay.error, self, exc_info)
|
||||
|
||||
# PROTOCOL NEGOTIATION
|
||||
#
|
||||
# The code implementing protocol version 2.0.0 (which is deployed
|
||||
# in the field and cannot be changed) *only* talks to peers that
|
||||
# send a handshake indicating protocol version 2.0.0. In that
|
||||
# version, both the client and the server immediately send out
|
||||
# their protocol handshake when a connection is established,
|
||||
# without waiting for their peer, and disconnect when a different
|
||||
# handshake is receive.
|
||||
#
|
||||
# The new protocol uses this to enable new clients to talk to
|
||||
# 2.0.0 servers. In the new protocol:
|
||||
#
|
||||
# The server sends its protocol handshake to the client at once.
|
||||
#
|
||||
# The client waits until it receives the server's protocol handshake
|
||||
# before sending its own handshake. The client sends the lower of its
|
||||
# own protocol version and the server protocol version, allowing it to
|
||||
# talk to servers using later protocol versions (2.0.2 and higher) as
|
||||
# well: the effective protocol used will be the lower of the client
|
||||
# and server protocol. However, this changed in ZODB 3.3.1 (and
|
||||
# should have changed in ZODB 3.3) because an older server doesn't
|
||||
# support MVCC methods required by 3.3 clients.
|
||||
#
|
||||
# [Ugly details: In order to treat the first received message (protocol
|
||||
# handshake) differently than all later messages, both client and server
|
||||
# start by patching their message_input() method to refer to their
|
||||
# recv_handshake() method instead. In addition, the client has to arrange
|
||||
# to queue (delay) outgoing messages until it receives the server's
|
||||
# handshake, so that the first message the client sends to the server is
|
||||
# the client's handshake. This multiply-special treatment of the first
|
||||
# message is delicate, and several asyncore and thread subtleties were
|
||||
# handled unsafely before ZODB 3.2.6.
|
||||
# ]
|
||||
#
|
||||
# The ZEO modules ClientStorage and ServerStub have backwards
|
||||
# compatibility code for dealing with the previous version of the
|
||||
# protocol. The client accepts the old version of some messages,
|
||||
# and will not send new messages when talking to an old server.
|
||||
#
|
||||
# As long as the client hasn't sent its handshake, it can't send
|
||||
# anything else; output messages are queued during this time.
|
||||
# (Output can happen because the connection testing machinery can
|
||||
# start sending requests before the handshake is received.)
|
||||
#
|
||||
# UPGRADING FROM ZEO 2.0.0 TO NEWER VERSIONS:
|
||||
#
|
||||
# Because a new client can talk to an old server, but not vice
|
||||
# versa, all clients should be upgraded before upgrading any
|
||||
# servers. Protocol upgrades beyond 2.0.1 will not have this
|
||||
# restriction, because clients using protocol 2.0.1 or later can
|
||||
# talk to both older and newer servers.
|
||||
#
|
||||
# No compatibility with protocol version 1 is provided.
|
||||
|
||||
# Connection is abstract (it must be derived from). ManagedServerConnection
|
||||
# and ManagedClientConnection are the concrete subclasses. They need to
|
||||
# supply a handshake() method appropriate for their role in protocol
|
||||
# negotiation.
|
||||
|
||||
class Connection(smac.SizedMessageAsyncConnection, object):
|
||||
"""Dispatcher for RPC on object on both sides of socket.
|
||||
|
||||
The connection supports synchronous calls, which expect a return,
|
||||
and asynchronous calls, which do not.
|
||||
|
||||
It uses the Marshaller class to handle encoding and decoding of
|
||||
method calls and arguments. Marshaller uses pickle to encode
|
||||
arbitrary Python objects. The code here doesn't ever see the wire
|
||||
format.
|
||||
|
||||
A Connection is designed for use in a multithreaded application,
|
||||
where a synchronous call must block until a response is ready.
|
||||
|
||||
A socket connection between a client and a server allows either
|
||||
side to invoke methods on the other side. The processes on each
|
||||
end of the socket use a Connection object to manage communication.
|
||||
|
||||
The Connection deals with decoded RPC messages. They are
|
||||
represented as four-tuples containing: msgid, flags, method name,
|
||||
and a tuple of method arguments.
|
||||
|
||||
The msgid starts at zero and is incremented by one each time a
|
||||
method call message is sent. Each side of the connection has a
|
||||
separate msgid state.
|
||||
|
||||
When one side of the connection (the client) calls a method, it
|
||||
sends a message with a new msgid. The other side (the server),
|
||||
replies with a message that has the same msgid, the string
|
||||
".reply" (the global variable REPLY) as the method name, and the
|
||||
actual return value in the args position. Note that each side of
|
||||
the Connection can initiate a call, in which case it will be the
|
||||
client for that particular call.
|
||||
|
||||
The protocol also supports asynchronous calls. The client does
|
||||
not wait for a return value for an asynchronous call.
|
||||
|
||||
If a method call raises an Exception, the exception is propagated
|
||||
back to the client via the REPLY message. The client side will
|
||||
raise any exception it receives instead of returning the value to
|
||||
the caller.
|
||||
"""
|
||||
|
||||
__super_init = smac.SizedMessageAsyncConnection.__init__
|
||||
__super_close = smac.SizedMessageAsyncConnection.close
|
||||
__super_setSessionKey = smac.SizedMessageAsyncConnection.setSessionKey
|
||||
|
||||
# Protocol history:
|
||||
#
|
||||
# Z200 -- Original ZEO 2.0 protocol
|
||||
#
|
||||
# Z201 -- Added invalidateTransaction() to client.
|
||||
# Renamed several client methods.
|
||||
# Added several sever methods:
|
||||
# lastTransaction()
|
||||
# getAuthProtocol() and scheme-specific authentication methods
|
||||
# getExtensionMethods().
|
||||
# getInvalidations().
|
||||
#
|
||||
# Z303 -- named after the ZODB release 3.3
|
||||
# Added methods for MVCC:
|
||||
# loadBefore()
|
||||
# A Z303 client cannot talk to a Z201 server, because the latter
|
||||
# doesn't support MVCC. A Z201 client can talk to a Z303 server,
|
||||
# but because (at least) the type of the root object changed
|
||||
# from ZODB.PersistentMapping to persistent.mapping, the older
|
||||
# client can't actually make progress if a Z303 client created,
|
||||
# or ever modified, the root.
|
||||
#
|
||||
# Z308 -- named after the ZODB release 3.8
|
||||
# Added blob-support server methods:
|
||||
# sendBlob
|
||||
# storeBlobStart
|
||||
# storeBlobChunk
|
||||
# storeBlobEnd
|
||||
# storeBlobShared
|
||||
# Added blob-support client methods:
|
||||
# receiveBlobStart
|
||||
# receiveBlobChunk
|
||||
# receiveBlobStop
|
||||
#
|
||||
# Z309 -- named after the ZODB release 3.9
|
||||
# New server methods:
|
||||
# restorea, iterator_start, iterator_next,
|
||||
# iterator_record_start, iterator_record_next,
|
||||
# iterator_gc
|
||||
#
|
||||
# Z310 -- named after the ZODB release 3.10
|
||||
# New server methods:
|
||||
# undoa
|
||||
# Doesn't support undo for older clients.
|
||||
# Undone oid info returned by vote.
|
||||
#
|
||||
# Z3101 -- checkCurrentSerialInTransaction
|
||||
#
|
||||
# Z4 -- checkCurrentSerialInTransaction
|
||||
# No-longer call load.
|
||||
|
||||
# Protocol variables:
|
||||
# Our preferred protocol.
|
||||
current_protocol = b"Z4"
|
||||
|
||||
# If we're a client, an exhaustive list of the server protocols we
|
||||
# can accept.
|
||||
servers_we_can_talk_to = [b"Z308", b"Z309", b"Z310", b"Z3101",
|
||||
current_protocol]
|
||||
|
||||
# If we're a server, an exhaustive list of the client protocols we
|
||||
# can accept.
|
||||
clients_we_can_talk_to = [
|
||||
b"Z200", b"Z201", b"Z303", b"Z308", b"Z309", b"Z310", b"Z3101",
|
||||
current_protocol]
|
||||
|
||||
# This is pretty excruciating. Details:
|
||||
#
|
||||
# 3.3 server 3.2 client
|
||||
# server sends Z303 to client
|
||||
# client computes min(Z303, Z201) == Z201 as the protocol to use
|
||||
# client sends Z201 to server
|
||||
# OK, because Z201 is in the server's clients_we_can_talk_to
|
||||
#
|
||||
# 3.2 server 3.3 client
|
||||
# server sends Z201 to client
|
||||
# client computes min(Z303, Z201) == Z201 as the protocol to use
|
||||
# Z201 isn't in the client's servers_we_can_talk_to, so client
|
||||
# raises exception
|
||||
#
|
||||
# 3.3 server 3.3 client
|
||||
# server sends Z303 to client
|
||||
# client computes min(Z303, Z303) == Z303 as the protocol to use
|
||||
# Z303 is in the client's servers_we_can_talk_to, so client
|
||||
# sends Z303 to server
|
||||
# OK, because Z303 is in the server's clients_we_can_talk_to
|
||||
|
||||
# Exception types that should not be logged:
|
||||
unlogged_exception_types = ()
|
||||
|
||||
# Client constructor passes b'C' for tag, server constructor b'S'. This
|
||||
# is used in log messages, and to determine whether we can speak with
|
||||
# our peer.
|
||||
def __init__(self, sock, addr, obj, tag, map=None):
|
||||
self.obj = None
|
||||
self.decode = marshal.decode
|
||||
self.encode = marshal.encode
|
||||
self.fast_encode = marshal.fast_encode
|
||||
|
||||
self.closed = False
|
||||
self.peer_protocol_version = None # set in recv_handshake()
|
||||
|
||||
assert tag in b"CS"
|
||||
self.tag = tag
|
||||
self.logger = logging.getLogger('ZEO.zrpc.Connection(%r)' % tag)
|
||||
if isinstance(addr, tuple):
|
||||
self.log_label = "(%s:%d) " % addr
|
||||
else:
|
||||
self.log_label = "(%s) " % addr
|
||||
|
||||
# Supply our own socket map, so that we don't get registered with
|
||||
# the asyncore socket map just yet. The initial protocol messages
|
||||
# are treated very specially, and we dare not get invoked by asyncore
|
||||
# before that special-case setup is complete. Some of that setup
|
||||
# occurs near the end of this constructor, and the rest is done by
|
||||
# a concrete subclass's handshake() method. Unfortunately, because
|
||||
# we ultimately derive from asyncore.dispatcher, it's not possible
|
||||
# to invoke the superclass constructor without asyncore stuffing
|
||||
# us into _some_ socket map.
|
||||
ourmap = {}
|
||||
self.__super_init(sock, addr, map=ourmap)
|
||||
|
||||
# The singleton dict is used in synchronous mode when a method
|
||||
# needs to call into asyncore to try to force some I/O to occur.
|
||||
# The singleton dict is a socket map containing only this object.
|
||||
self._singleton = {self._fileno: self}
|
||||
|
||||
# waiting_for_reply is used internally to indicate whether
|
||||
# a call is in progress. setting a session key is deferred
|
||||
# until after the call returns.
|
||||
self.waiting_for_reply = False
|
||||
self.delay_sesskey = None
|
||||
self.register_object(obj)
|
||||
|
||||
# The first message we see is a protocol handshake. message_input()
|
||||
# is temporarily replaced by recv_handshake() to treat that message
|
||||
# specially. revc_handshake() does "del self.message_input", which
|
||||
# uncovers the normal message_input() method thereafter.
|
||||
self.message_input = self.recv_handshake
|
||||
|
||||
# Server and client need to do different things for protocol
|
||||
# negotiation, and handshake() is implemented differently in each.
|
||||
self.handshake()
|
||||
|
||||
# Now it's safe to register with asyncore's socket map; it was not
|
||||
# safe before message_input was replaced, or before handshake() was
|
||||
# invoked.
|
||||
# Obscure: in Python 2.4, the base asyncore.dispatcher class grew
|
||||
# a ._map attribute, which is used instead of asyncore's global
|
||||
# socket map when ._map isn't None. Because we passed `ourmap` to
|
||||
# the base class constructor above, in 2.4 asyncore believes we want
|
||||
# to use `ourmap` instead of the global socket map -- but we don't.
|
||||
# So we have to replace our ._map with the global socket map, and
|
||||
# update the global socket map with `ourmap`. Replacing our ._map
|
||||
# isn't necessary before Python 2.4, but doesn't hurt then (it just
|
||||
# gives us an unused attribute in 2.3); updating the global socket
|
||||
# map is necessary regardless of Python version.
|
||||
if map is None:
|
||||
map = asyncore.socket_map
|
||||
self._map = map
|
||||
map.update(ourmap)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %s>" % (self.__class__.__name__, self.addr)
|
||||
|
||||
__str__ = __repr__ # Defeat asyncore's dreaded __getattr__
|
||||
|
||||
def log(self, message, level=BLATHER, exc_info=False):
|
||||
self.logger.log(level, self.log_label + message, exc_info=exc_info)
|
||||
|
||||
def close(self):
|
||||
self.mgr.close_conn(self)
|
||||
if self.closed:
|
||||
return
|
||||
self._singleton.clear()
|
||||
self.closed = True
|
||||
self.__super_close()
|
||||
self.trigger.pull_trigger()
|
||||
|
||||
def register_object(self, obj):
|
||||
"""Register obj as the true object to invoke methods on."""
|
||||
self.obj = obj
|
||||
|
||||
# Subclass must implement. handshake() is called by the constructor,
|
||||
# near its end, but before self is added to asyncore's socket map.
|
||||
# When a connection is created the first message sent is a 4-byte
|
||||
# protocol version. This allows the protocol to evolve over time, and
|
||||
# lets servers handle clients using multiple versions of the protocol.
|
||||
# In general, the server's handshake() just needs to send the server's
|
||||
# preferred protocol; the client's also needs to queue (delay) outgoing
|
||||
# messages until it sees the handshake from the server.
|
||||
def handshake(self):
|
||||
raise NotImplementedError
|
||||
|
||||
# Replaces message_input() for the first message received. Records the
|
||||
# protocol sent by the peer in `peer_protocol_version`, restores the
|
||||
# normal message_input() method, and raises an exception if the peer's
|
||||
# protocol is unacceptable. That's all the server needs to do. The
|
||||
# client needs to do additional work in response to the server's
|
||||
# handshake, and extends this method.
|
||||
def recv_handshake(self, proto):
|
||||
# Extended by ManagedClientConnection.
|
||||
del self.message_input # uncover normal-case message_input()
|
||||
self.peer_protocol_version = proto
|
||||
|
||||
if self.tag == b'C':
|
||||
good_protos = self.servers_we_can_talk_to
|
||||
else:
|
||||
assert self.tag == b'S'
|
||||
good_protos = self.clients_we_can_talk_to
|
||||
|
||||
if proto in good_protos:
|
||||
self.log("received handshake %r" % proto, level=logging.INFO)
|
||||
else:
|
||||
self.log("bad handshake %s" % short_repr(proto),
|
||||
level=logging.ERROR)
|
||||
raise ZRPCError("bad handshake %r" % proto)
|
||||
|
||||
def message_input(self, message):
|
||||
"""Decode an incoming message and dispatch it"""
|
||||
# If something goes wrong during decoding, the marshaller
|
||||
# will raise an exception. The exception will ultimately
|
||||
# result in asycnore calling handle_error(), which will
|
||||
# close the connection.
|
||||
msgid, async_, name, args = self.decode(message)
|
||||
|
||||
if debug_zrpc:
|
||||
self.log("recv msg: %s, %s, %s, %s" % (msgid, async_, name,
|
||||
short_repr(args)),
|
||||
level=TRACE)
|
||||
|
||||
if name == 'loadEx':
|
||||
|
||||
# Special case and inline the heck out of load case:
|
||||
try:
|
||||
ret = self.obj.loadEx(*args)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except Exception as msg:
|
||||
if not isinstance(msg, self.unlogged_exception_types):
|
||||
self.log("%s() raised exception: %s" % (name, msg),
|
||||
logging.ERROR, exc_info=True)
|
||||
self.return_error(msgid, *sys.exc_info()[:2])
|
||||
else:
|
||||
try:
|
||||
self.message_output(self.fast_encode(msgid, 0, REPLY, ret))
|
||||
self.poll()
|
||||
except:
|
||||
# Fall back to normal version for better error handling
|
||||
self.send_reply(msgid, ret)
|
||||
|
||||
elif name == REPLY:
|
||||
assert not async_
|
||||
self.handle_reply(msgid, args)
|
||||
else:
|
||||
self.handle_request(msgid, async_, name, args)
|
||||
|
||||
def handle_request(self, msgid, async_, name, args):
|
||||
obj = self.obj
|
||||
|
||||
if name.startswith('_') or not hasattr(obj, name):
|
||||
if obj is None:
|
||||
if debug_zrpc:
|
||||
self.log("no object calling %s%s"
|
||||
% (name, short_repr(args)),
|
||||
level=logging.DEBUG)
|
||||
return
|
||||
|
||||
msg = "Invalid method name: %s on %s" % (name, repr(obj))
|
||||
raise ZRPCError(msg)
|
||||
if debug_zrpc:
|
||||
self.log("calling %s%s" % (name, short_repr(args)),
|
||||
level=logging.DEBUG)
|
||||
|
||||
meth = getattr(obj, name)
|
||||
try:
|
||||
self.waiting_for_reply = True
|
||||
try:
|
||||
ret = meth(*args)
|
||||
finally:
|
||||
self.waiting_for_reply = False
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except Exception as msg:
|
||||
if not isinstance(msg, self.unlogged_exception_types):
|
||||
self.log("%s() raised exception: %s" % (name, msg),
|
||||
logging.ERROR, exc_info=True)
|
||||
error = sys.exc_info()[:2]
|
||||
if async_:
|
||||
self.log("Asynchronous call raised exception: %s" % self,
|
||||
level=logging.ERROR, exc_info=True)
|
||||
else:
|
||||
self.return_error(msgid, *error)
|
||||
return
|
||||
|
||||
if async_:
|
||||
if ret is not None:
|
||||
raise ZRPCError("async method %s returned value %s" %
|
||||
(name, short_repr(ret)))
|
||||
else:
|
||||
if debug_zrpc:
|
||||
self.log("%s returns %s" % (name, short_repr(ret)),
|
||||
logging.DEBUG)
|
||||
if isinstance(ret, Delay):
|
||||
ret.set_sender(msgid, self)
|
||||
else:
|
||||
self.send_reply(msgid, ret, not self.delay_sesskey)
|
||||
|
||||
if self.delay_sesskey:
|
||||
self.__super_setSessionKey(self.delay_sesskey)
|
||||
self.delay_sesskey = None
|
||||
|
||||
def return_error(self, msgid, err_type, err_value):
|
||||
# Note that, ideally, this should be defined soley for
|
||||
# servers, but a test arranges to get it called on
|
||||
# a client. Too much trouble to fix it now. :/
|
||||
|
||||
if not isinstance(err_value, Exception):
|
||||
err_value = err_type, err_value
|
||||
|
||||
# encode() can pass on a wide variety of exceptions from cPickle.
|
||||
# While a bare `except` is generally poor practice, in this case
|
||||
# it's acceptable -- we really do want to catch every exception
|
||||
# cPickle may raise.
|
||||
try:
|
||||
msg = self.encode(msgid, 0, REPLY, (err_type, err_value))
|
||||
except: # see above
|
||||
try:
|
||||
r = short_repr(err_value)
|
||||
except:
|
||||
r = "<unreprable>"
|
||||
err = ZRPCError("Couldn't pickle error %.100s" % r)
|
||||
msg = self.encode(msgid, 0, REPLY, (ZRPCError, err))
|
||||
self.message_output(msg)
|
||||
self.poll()
|
||||
|
||||
def handle_error(self):
|
||||
if sys.exc_info()[0] == SystemExit:
|
||||
raise sys.exc_info()
|
||||
self.log("Error caught in asyncore",
|
||||
level=logging.ERROR, exc_info=True)
|
||||
self.close()
|
||||
|
||||
def setSessionKey(self, key):
|
||||
if self.waiting_for_reply:
|
||||
self.delay_sesskey = key
|
||||
else:
|
||||
self.__super_setSessionKey(key)
|
||||
|
||||
def send_call(self, method, args, async_=False):
|
||||
# send a message and return its msgid
|
||||
if async_:
|
||||
msgid = 0
|
||||
else:
|
||||
msgid = self._new_msgid()
|
||||
|
||||
if debug_zrpc:
|
||||
self.log("send msg: %d, %d, %s, ..." % (msgid, async_, method),
|
||||
level=TRACE)
|
||||
buf = self.encode(msgid, async_, method, args)
|
||||
self.message_output(buf)
|
||||
return msgid
|
||||
|
||||
def callAsync(self, method, *args):
|
||||
if self.closed:
|
||||
raise DisconnectedError()
|
||||
self.send_call(method, args, 1)
|
||||
self.poll()
|
||||
|
||||
def callAsyncNoPoll(self, method, *args):
|
||||
# Like CallAsync but doesn't poll. This exists so that we can
|
||||
# send invalidations atomically to all clients without
|
||||
# allowing any client to sneak in a load request.
|
||||
if self.closed:
|
||||
raise DisconnectedError()
|
||||
self.send_call(method, args, 1)
|
||||
|
||||
def callAsyncNoSend(self, method, *args):
|
||||
# Like CallAsync but doesn't poll. This exists so that we can
|
||||
# send invalidations atomically to all clients without
|
||||
# allowing any client to sneak in a load request.
|
||||
if self.closed:
|
||||
raise DisconnectedError()
|
||||
self.send_call(method, args, 1)
|
||||
self.call_from_thread()
|
||||
|
||||
def callAsyncIterator(self, iterator):
|
||||
"""Queue a sequence of calls using an iterator
|
||||
|
||||
The calls will not be interleaved with other calls from the same
|
||||
client.
|
||||
"""
|
||||
self.message_output(self.encode(0, 1, method, args)
|
||||
for method, args in iterator)
|
||||
|
||||
def handle_reply(self, msgid, ret):
|
||||
assert msgid == -1 and ret is None
|
||||
|
||||
def poll(self):
|
||||
"""Invoke asyncore mainloop to get pending message out."""
|
||||
if debug_zrpc:
|
||||
self.log("poll()", level=TRACE)
|
||||
self.trigger.pull_trigger()
|
||||
|
||||
|
||||
# import cProfile, time
|
||||
|
||||
class ManagedServerConnection(Connection):
|
||||
"""Server-side Connection subclass."""
|
||||
|
||||
# Exception types that should not be logged:
|
||||
unlogged_exception_types = (ZODB.POSException.POSKeyError, )
|
||||
|
||||
def __init__(self, sock, addr, obj, mgr):
|
||||
self.mgr = mgr
|
||||
map = {}
|
||||
Connection.__init__(self, sock, addr, obj, b'S', map=map)
|
||||
|
||||
self.decode = marshal.server_decode
|
||||
|
||||
self.trigger = trigger.trigger(map)
|
||||
self.call_from_thread = self.trigger.pull_trigger
|
||||
|
||||
t = threading.Thread(target=server_loop, args=(map,))
|
||||
t.setName("ManagedServerConnection thread")
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
# self.profile = cProfile.Profile()
|
||||
|
||||
# def message_input(self, message):
|
||||
# self.profile.enable()
|
||||
# try:
|
||||
# Connection.message_input(self, message)
|
||||
# finally:
|
||||
# self.profile.disable()
|
||||
|
||||
def handshake(self):
|
||||
# Send the server's preferred protocol to the client.
|
||||
self.message_output(self.current_protocol)
|
||||
|
||||
def recv_handshake(self, proto):
|
||||
if proto == b'ruok':
|
||||
self.message_output(json.dumps(self.mgr.ruok()).encode("ascii"))
|
||||
self.poll()
|
||||
Connection.close(self)
|
||||
else:
|
||||
Connection.recv_handshake(self, proto)
|
||||
self.obj.notifyConnected(self)
|
||||
|
||||
def close(self):
|
||||
self.obj.notifyDisconnected()
|
||||
Connection.close(self)
|
||||
# self.profile.dump_stats(str(time.time())+'.stats')
|
||||
|
||||
def send_reply(self, msgid, ret, immediately=True):
|
||||
# encode() can pass on a wide variety of exceptions from cPickle.
|
||||
# While a bare `except` is generally poor practice, in this case
|
||||
# it's acceptable -- we really do want to catch every exception
|
||||
# cPickle may raise.
|
||||
try:
|
||||
msg = self.encode(msgid, 0, REPLY, ret)
|
||||
except: # see above
|
||||
try:
|
||||
r = short_repr(ret)
|
||||
except:
|
||||
r = "<unreprable>"
|
||||
err = ZRPCError("Couldn't pickle return %.100s" % r)
|
||||
msg = self.encode(msgid, 0, REPLY, (ZRPCError, err))
|
||||
self.message_output(msg)
|
||||
if immediately:
|
||||
self.poll()
|
||||
|
||||
poll = smac.SizedMessageAsyncConnection.handle_write
|
||||
|
||||
def server_loop(map):
|
||||
while len(map) > 1:
|
||||
try:
|
||||
asyncore.poll(30.0, map)
|
||||
except Exception as v:
|
||||
if v.args[0] != errno.EBADF:
|
||||
raise
|
||||
|
||||
for o in tuple(map.values()):
|
||||
o.close()
|
||||
|
||||
class ManagedClientConnection(Connection):
|
||||
"""Client-side Connection subclass."""
|
||||
__super_init = Connection.__init__
|
||||
base_message_output = Connection.message_output
|
||||
|
||||
def __init__(self, sock, addr, mgr):
|
||||
self.mgr = mgr
|
||||
|
||||
# We can't use the base smac's message_output directly because the
|
||||
# client needs to queue outgoing messages until it's seen the
|
||||
# initial protocol handshake from the server. So we have our own
|
||||
# message_ouput() method, and support for initial queueing. This is
|
||||
# a delicate design, requiring an output mutex to be wholly
|
||||
# thread-safe.
|
||||
# Caution: we must set this up before calling the base class
|
||||
# constructor, because the latter registers us with asyncore;
|
||||
# we need to guarantee that we'll queue outgoing messages before
|
||||
# asyncore learns about us.
|
||||
self.output_lock = threading.Lock()
|
||||
self.queue_output = True
|
||||
self.queued_messages = []
|
||||
|
||||
# msgid_lock guards access to msgid
|
||||
self.msgid = 0
|
||||
self.msgid_lock = threading.Lock()
|
||||
|
||||
# replies_cond is used to block when a synchronous call is
|
||||
# waiting for a response
|
||||
self.replies_cond = threading.Condition()
|
||||
self.replies = {}
|
||||
|
||||
self.__super_init(sock, addr, None, tag=b'C', map=mgr.map)
|
||||
self.trigger = mgr.trigger
|
||||
self.call_from_thread = self.trigger.pull_trigger
|
||||
self.call_from_thread()
|
||||
|
||||
def close(self):
|
||||
Connection.close(self)
|
||||
self.replies_cond.acquire()
|
||||
self.replies_cond.notifyAll()
|
||||
self.replies_cond.release()
|
||||
|
||||
# Our message_ouput() queues messages until recv_handshake() gets the
|
||||
# protocol handshake from the server.
|
||||
def message_output(self, message):
|
||||
self.output_lock.acquire()
|
||||
try:
|
||||
if self.queue_output:
|
||||
self.queued_messages.append(message)
|
||||
else:
|
||||
assert not self.queued_messages
|
||||
self.base_message_output(message)
|
||||
finally:
|
||||
self.output_lock.release()
|
||||
|
||||
def handshake(self):
|
||||
# The client waits to see the server's handshake. Outgoing messages
|
||||
# are queued for the duration. The client will send its own
|
||||
# handshake after the server's handshake is seen, in recv_handshake()
|
||||
# below. It will then send any messages queued while waiting.
|
||||
assert self.queue_output # the constructor already set this
|
||||
|
||||
def recv_handshake(self, proto):
|
||||
# The protocol to use is the older of our and the server's preferred
|
||||
# protocols.
|
||||
proto = min(proto, self.current_protocol)
|
||||
|
||||
# Restore the normal message_input method, and raise an exception
|
||||
# if the protocol version is too old.
|
||||
Connection.recv_handshake(self, proto)
|
||||
|
||||
# Tell the server the protocol in use, then send any messages that
|
||||
# were queued while waiting to hear the server's protocol, and stop
|
||||
# queueing messages.
|
||||
self.output_lock.acquire()
|
||||
try:
|
||||
self.base_message_output(proto)
|
||||
for message in self.queued_messages:
|
||||
self.base_message_output(message)
|
||||
self.queued_messages = []
|
||||
self.queue_output = False
|
||||
finally:
|
||||
self.output_lock.release()
|
||||
|
||||
def _new_msgid(self):
|
||||
self.msgid_lock.acquire()
|
||||
try:
|
||||
msgid = self.msgid
|
||||
self.msgid = self.msgid + 1
|
||||
return msgid
|
||||
finally:
|
||||
self.msgid_lock.release()
|
||||
|
||||
def call(self, method, *args):
|
||||
if self.closed:
|
||||
raise DisconnectedError()
|
||||
msgid = self.send_call(method, args)
|
||||
r_args = self.wait(msgid)
|
||||
if (isinstance(r_args, tuple) and len(r_args) > 1
|
||||
and type(r_args[0]) == exception_type_type
|
||||
and issubclass(r_args[0], Exception)):
|
||||
inst = r_args[1]
|
||||
raise inst # error raised by server
|
||||
else:
|
||||
return r_args
|
||||
|
||||
def wait(self, msgid):
|
||||
"""Invoke asyncore mainloop and wait for reply."""
|
||||
if debug_zrpc:
|
||||
self.log("wait(%d)" % msgid, level=TRACE)
|
||||
|
||||
self.trigger.pull_trigger()
|
||||
|
||||
self.replies_cond.acquire()
|
||||
try:
|
||||
while 1:
|
||||
if self.closed:
|
||||
raise DisconnectedError()
|
||||
reply = self.replies.get(msgid, self)
|
||||
if reply is not self:
|
||||
del self.replies[msgid]
|
||||
if debug_zrpc:
|
||||
self.log("wait(%d): reply=%s" %
|
||||
(msgid, short_repr(reply)), level=TRACE)
|
||||
return reply
|
||||
self.replies_cond.wait()
|
||||
finally:
|
||||
self.replies_cond.release()
|
||||
|
||||
# For testing purposes, it is useful to begin a synchronous call
|
||||
# but not block waiting for its response.
|
||||
|
||||
def _deferred_call(self, method, *args):
|
||||
if self.closed:
|
||||
raise DisconnectedError()
|
||||
msgid = self.send_call(method, args)
|
||||
self.trigger.pull_trigger()
|
||||
return msgid
|
||||
|
||||
def _deferred_wait(self, msgid):
|
||||
r_args = self.wait(msgid)
|
||||
if (isinstance(r_args, tuple)
|
||||
and type(r_args[0]) == exception_type_type
|
||||
and issubclass(r_args[0], Exception)):
|
||||
inst = r_args[1]
|
||||
raise inst # error raised by server
|
||||
else:
|
||||
return r_args
|
||||
|
||||
def handle_reply(self, msgid, args):
|
||||
if debug_zrpc:
|
||||
self.log("recv reply: %s, %s"
|
||||
% (msgid, short_repr(args)), level=TRACE)
|
||||
self.replies_cond.acquire()
|
||||
try:
|
||||
self.replies[msgid] = args
|
||||
self.replies_cond.notifyAll()
|
||||
finally:
|
||||
self.replies_cond.release()
|
||||
|
||||
def send_reply(self, msgid, ret):
|
||||
# Whimper. Used to send heartbeat
|
||||
assert msgid == -1 and ret is None
|
||||
self.message_output(b'(J\xff\xff\xff\xffK\x00U\x06.replyNt.')
|
||||
@@ -0,0 +1,27 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 import POSException
|
||||
from ZEO.Exceptions import ClientDisconnected
|
||||
|
||||
class ZRPCError(POSException.StorageError):
|
||||
pass
|
||||
|
||||
class DisconnectedError(ZRPCError, ClientDisconnected):
|
||||
"""The database storage is disconnected from the storage server.
|
||||
|
||||
The error occurred because a problem in the low-level RPC connection,
|
||||
or because the connection was closed.
|
||||
"""
|
||||
|
||||
# This subclass is raised when zrpc catches the error.
|
||||
@@ -0,0 +1,77 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 os
|
||||
import threading
|
||||
import logging
|
||||
|
||||
from ZODB.loglevels import BLATHER
|
||||
|
||||
LOG_THREAD_ID = 0 # Set this to 1 during heavy debugging
|
||||
|
||||
logger = logging.getLogger('ZEO.zrpc')
|
||||
|
||||
_label = "%s" % os.getpid()
|
||||
|
||||
def new_label():
|
||||
global _label
|
||||
_label = str(os.getpid())
|
||||
|
||||
def log(message, level=BLATHER, label=None, exc_info=False):
|
||||
label = label or _label
|
||||
if LOG_THREAD_ID:
|
||||
label = label + ':' + threading.currentThread().getName()
|
||||
logger.log(level, '(%s) %s' % (label, message), exc_info=exc_info)
|
||||
|
||||
REPR_LIMIT = 60
|
||||
|
||||
def short_repr(obj):
|
||||
"Return an object repr limited to REPR_LIMIT bytes."
|
||||
|
||||
# Some of the objects being repr'd are large strings. A lot of memory
|
||||
# would be wasted to repr them and then truncate, so they are treated
|
||||
# specially in this function.
|
||||
# Also handle short repr of a tuple containing a long string.
|
||||
|
||||
# This strategy works well for arguments to StorageServer methods.
|
||||
# The oid is usually first and will get included in its entirety.
|
||||
# The pickle is near the beginning, too, and you can often fit the
|
||||
# module name in the pickle.
|
||||
|
||||
if isinstance(obj, str):
|
||||
if len(obj) > REPR_LIMIT:
|
||||
r = repr(obj[:REPR_LIMIT])
|
||||
else:
|
||||
r = repr(obj)
|
||||
if len(r) > REPR_LIMIT:
|
||||
r = r[:REPR_LIMIT-4] + '...' + r[-1]
|
||||
return r
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
elts = []
|
||||
size = 0
|
||||
for elt in obj:
|
||||
r = short_repr(elt)
|
||||
elts.append(r)
|
||||
size += len(r)
|
||||
if size > REPR_LIMIT:
|
||||
break
|
||||
if isinstance(obj, tuple):
|
||||
r = "(%s)" % (", ".join(elts))
|
||||
else:
|
||||
r = "[%s]" % (", ".join(elts))
|
||||
else:
|
||||
r = repr(obj)
|
||||
if len(r) > REPR_LIMIT:
|
||||
return r[:REPR_LIMIT] + '...'
|
||||
else:
|
||||
return r
|
||||
@@ -0,0 +1,139 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
|
||||
from ZEO._compat import Unpickler, Pickler, BytesIO, PY3, PYPY
|
||||
from .error import ZRPCError
|
||||
from .log import log, short_repr
|
||||
|
||||
PY2 = not PY3
|
||||
|
||||
def encode(*args): # args: (msgid, flags, name, args)
|
||||
# (We used to have a global pickler, but that's not thread-safe. :-( )
|
||||
|
||||
# It's not thread safe if, in the couse of pickling, we call the
|
||||
# Python interpeter, which releases the GIL.
|
||||
|
||||
# Note that args may contain very large binary pickles already; for
|
||||
# this reason, it's important to use proto 1 (or higher) pickles here
|
||||
# too. For a long time, this used proto 0 pickles, and that can
|
||||
# bloat our pickle to 4x the size (due to high-bit and control bytes
|
||||
# being represented by \xij escapes in proto 0).
|
||||
# Undocumented: cPickle.Pickler accepts a lone protocol argument;
|
||||
# pickle.py does not.
|
||||
# XXX: Py3: Needs optimization.
|
||||
f = BytesIO()
|
||||
pickler = Pickler(f, 3)
|
||||
pickler.fast = 1
|
||||
pickler.dump(args)
|
||||
res = f.getvalue()
|
||||
return res
|
||||
|
||||
|
||||
|
||||
if PY3:
|
||||
# XXX: Py3: Needs optimization.
|
||||
fast_encode = encode
|
||||
elif PYPY:
|
||||
# can't use the python-2 branch, need a new pickler
|
||||
# every time, getvalue() only works once
|
||||
fast_encode = encode
|
||||
else:
|
||||
def fast_encode():
|
||||
# Only use in cases where you *know* the data contains only basic
|
||||
# Python objects
|
||||
pickler = Pickler(1)
|
||||
pickler.fast = 1
|
||||
dump = pickler.dump
|
||||
def fast_encode(*args):
|
||||
return dump(args, 1)
|
||||
return fast_encode
|
||||
fast_encode = fast_encode()
|
||||
|
||||
def decode(msg):
|
||||
"""Decodes msg and returns its parts"""
|
||||
unpickler = Unpickler(BytesIO(msg))
|
||||
unpickler.find_global = find_global
|
||||
try:
|
||||
unpickler.find_class = find_global # PyPy, zodbpickle, the non-c-accelerated version
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return unpickler.load() # msgid, flags, name, args
|
||||
except:
|
||||
log("can't decode message: %s" % short_repr(msg),
|
||||
level=logging.ERROR)
|
||||
raise
|
||||
|
||||
def server_decode(msg):
|
||||
"""Decodes msg and returns its parts"""
|
||||
unpickler = Unpickler(BytesIO(msg))
|
||||
unpickler.find_global = server_find_global
|
||||
try:
|
||||
unpickler.find_class = server_find_global # PyPy, zodbpickle, the non-c-accelerated version
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return unpickler.load() # msgid, flags, name, args
|
||||
except:
|
||||
log("can't decode message: %s" % short_repr(msg),
|
||||
level=logging.ERROR)
|
||||
raise
|
||||
|
||||
_globals = globals()
|
||||
_silly = ('__doc__',)
|
||||
|
||||
exception_type_type = type(Exception)
|
||||
|
||||
_SAFE_MODULE_NAMES = ('ZopeUndo.Prefix', 'copy_reg', '__builtin__', 'zodbpickle')
|
||||
|
||||
def find_global(module, name):
|
||||
"""Helper for message unpickler"""
|
||||
try:
|
||||
m = __import__(module, _globals, _globals, _silly)
|
||||
except ImportError as msg:
|
||||
raise ZRPCError("import error %s: %s" % (module, msg))
|
||||
|
||||
try:
|
||||
r = getattr(m, name)
|
||||
except AttributeError:
|
||||
raise ZRPCError("module %s has no global %s" % (module, name))
|
||||
|
||||
safe = getattr(r, '__no_side_effects__', 0) or (PY2 and module in _SAFE_MODULE_NAMES)
|
||||
if safe:
|
||||
return r
|
||||
|
||||
# TODO: is there a better way to do this?
|
||||
if type(r) == exception_type_type and issubclass(r, Exception):
|
||||
return r
|
||||
|
||||
raise ZRPCError("Unsafe global: %s.%s" % (module, name))
|
||||
|
||||
def server_find_global(module, name):
|
||||
"""Helper for message unpickler"""
|
||||
if module not in _SAFE_MODULE_NAMES:
|
||||
raise ImportError("Module not allowed: %s" % (module,))
|
||||
|
||||
try:
|
||||
m = __import__(module, _globals, _globals, _silly)
|
||||
except ImportError as msg:
|
||||
raise ZRPCError("import error %s: %s" % (module, msg))
|
||||
|
||||
try:
|
||||
r = getattr(m, name)
|
||||
except AttributeError:
|
||||
raise ZRPCError("module %s has no global %s" % (module, name))
|
||||
|
||||
return r
|
||||
@@ -0,0 +1,124 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 asyncore
|
||||
import socket
|
||||
|
||||
# _has_dualstack: True if the dual-stack sockets are supported
|
||||
try:
|
||||
# Check whether IPv6 sockets can be created
|
||||
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
except (socket.error, AttributeError):
|
||||
_has_dualstack = False
|
||||
else:
|
||||
# Check whether enabling dualstack (disabling v6only) works
|
||||
try:
|
||||
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
|
||||
except (socket.error, AttributeError):
|
||||
_has_dualstack = False
|
||||
else:
|
||||
_has_dualstack = True
|
||||
s.close()
|
||||
del s
|
||||
|
||||
from .connection import Connection
|
||||
from .log import log
|
||||
from .log import logger
|
||||
import logging
|
||||
|
||||
# Export the main asyncore loop
|
||||
loop = asyncore.loop
|
||||
|
||||
class Dispatcher(asyncore.dispatcher):
|
||||
"""A server that accepts incoming RPC connections"""
|
||||
__super_init = asyncore.dispatcher.__init__
|
||||
|
||||
def __init__(self, addr, factory=Connection, map=None):
|
||||
self.__super_init(map=map)
|
||||
self.addr = addr
|
||||
self.factory = factory
|
||||
self._open_socket()
|
||||
|
||||
def _open_socket(self):
|
||||
if type(self.addr) == tuple:
|
||||
if self.addr[0] == '' and _has_dualstack:
|
||||
# Wildcard listen on all interfaces, both IPv4 and
|
||||
# IPv6 if possible
|
||||
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(
|
||||
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
|
||||
elif ':' in self.addr[0]:
|
||||
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
if _has_dualstack:
|
||||
# On Linux, IPV6_V6ONLY is off by default.
|
||||
# If the user explicitly asked for IPv6, don't bind to IPv4
|
||||
self.socket.setsockopt(
|
||||
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, True)
|
||||
else:
|
||||
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
else:
|
||||
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.set_reuse_addr()
|
||||
log("listening on %s" % str(self.addr), logging.INFO)
|
||||
|
||||
for i in range(25):
|
||||
try:
|
||||
self.bind(self.addr)
|
||||
except Exception as exc:
|
||||
log("bind failed %s waiting", i)
|
||||
if i == 24:
|
||||
raise
|
||||
else:
|
||||
time.sleep(5)
|
||||
else:
|
||||
break
|
||||
|
||||
self.listen(5)
|
||||
|
||||
def writable(self):
|
||||
return 0
|
||||
|
||||
def readable(self):
|
||||
return 1
|
||||
|
||||
def handle_accept(self):
|
||||
try:
|
||||
sock, addr = self.accept()
|
||||
except socket.error as msg:
|
||||
log("accepted failed: %s" % msg)
|
||||
return
|
||||
|
||||
|
||||
# We could short-circuit the attempt below in some edge cases
|
||||
# and avoid a log message by checking for addr being None.
|
||||
# Unfortunately, our test for the code below,
|
||||
# quick_close_doesnt_kill_server, causes addr to be None and
|
||||
# we'd have to write a test for the non-None case, which is
|
||||
# *even* harder to provoke. :/ So we'll leave things as they
|
||||
# are for now.
|
||||
|
||||
# It might be better to check whether the socket has been
|
||||
# closed, but I don't see a way to do that. :(
|
||||
|
||||
# Drop flow-info from IPv6 addresses
|
||||
if addr: # Sometimes None on Mac. See above.
|
||||
addr = addr[:2]
|
||||
|
||||
try:
|
||||
c = self.factory(sock, addr)
|
||||
except:
|
||||
if sock.fileno() in asyncore.socket_map:
|
||||
del asyncore.socket_map[sock.fileno()]
|
||||
logger.exception("Error in handle_accept")
|
||||
else:
|
||||
log("connect from %s: %s" % (repr(addr), c))
|
||||
@@ -0,0 +1,342 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Sized Message Async Connections.
|
||||
|
||||
This class extends the basic asyncore layer with a record-marking
|
||||
layer. The message_output() method accepts an arbitrary sized string
|
||||
as its argument. It sends over the wire the length of the string
|
||||
encoded using struct.pack('>I') and the string itself. The receiver
|
||||
passes the original string to message_input().
|
||||
|
||||
This layer also supports an optional message authentication code
|
||||
(MAC). If a session key is present, it uses HMAC-SHA-1 to generate a
|
||||
20-byte MAC. If a MAC is present, the high-order bit of the length
|
||||
is set to 1 and the MAC immediately follows the length.
|
||||
"""
|
||||
|
||||
import asyncore
|
||||
import errno
|
||||
import six
|
||||
try:
|
||||
import hmac
|
||||
except ImportError:
|
||||
from . import _hmac as hmac
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
|
||||
from .log import log
|
||||
from .error import DisconnectedError
|
||||
from .. import hash as ZEO_hash
|
||||
|
||||
|
||||
# Use the dictionary to make sure we get the minimum number of errno
|
||||
# entries. We expect that EWOULDBLOCK == EAGAIN on most systems --
|
||||
# or that only one is actually used.
|
||||
|
||||
tmp_dict = {errno.EWOULDBLOCK: 0,
|
||||
errno.EAGAIN: 0,
|
||||
errno.EINTR: 0,
|
||||
}
|
||||
expected_socket_read_errors = tuple(tmp_dict.keys())
|
||||
|
||||
tmp_dict = {errno.EAGAIN: 0,
|
||||
errno.EWOULDBLOCK: 0,
|
||||
errno.ENOBUFS: 0,
|
||||
errno.EINTR: 0,
|
||||
}
|
||||
expected_socket_write_errors = tuple(tmp_dict.keys())
|
||||
del tmp_dict
|
||||
|
||||
# We chose 60000 as the socket limit by looking at the largest strings
|
||||
# that we could pass to send() without blocking.
|
||||
SEND_SIZE = 60000
|
||||
|
||||
MAC_BIT = 0x80000000
|
||||
|
||||
_close_marker = object()
|
||||
|
||||
class SizedMessageAsyncConnection(asyncore.dispatcher):
|
||||
__super_init = asyncore.dispatcher.__init__
|
||||
__super_close = asyncore.dispatcher.close
|
||||
|
||||
__closed = True # Marker indicating that we're closed
|
||||
|
||||
socket = None # to outwit Sam's getattr
|
||||
|
||||
def __init__(self, sock, addr, map=None):
|
||||
self.addr = addr
|
||||
# __input_lock protects __inp, __input_len, __state, __msg_size
|
||||
self.__input_lock = threading.Lock()
|
||||
self.__inp = None # None, a single String, or a list
|
||||
self.__input_len = 0
|
||||
# Instance variables __state, __msg_size and __has_mac work together:
|
||||
# when __state == 0:
|
||||
# __msg_size == 4, and the next thing read is a message size;
|
||||
# __has_mac is set according to the MAC_BIT in the header
|
||||
# when __state == 1:
|
||||
# __msg_size is variable, and the next thing read is a message.
|
||||
# __has_mac indicates if we're in MAC mode or not (and
|
||||
# therefore, if we need to check the mac header)
|
||||
# The next thing read is always of length __msg_size.
|
||||
# The state alternates between 0 and 1.
|
||||
self.__state = 0
|
||||
self.__has_mac = 0
|
||||
self.__msg_size = 4
|
||||
self.__output_messages = []
|
||||
self.__output = []
|
||||
self.__closed = False
|
||||
# Each side of the connection sends and receives messages. A
|
||||
# MAC is generated for each message and depends on each
|
||||
# previous MAC; the state of the MAC generator depends on the
|
||||
# history of operations it has performed. So the MACs must be
|
||||
# generated in the same order they are verified.
|
||||
|
||||
# Each side is guaranteed to receive messages in the order
|
||||
# they are sent, but there is no ordering constraint between
|
||||
# message sends and receives. If the two sides are A and B
|
||||
# and message An indicates the nth message sent by A, then
|
||||
# A1 A2 B1 B2 and A1 B1 B2 A2 are both legitimate total
|
||||
# orderings of the messages.
|
||||
|
||||
# As a result, there must be seperate MAC generators for each
|
||||
# side of the connection. If not, the generator state would
|
||||
# be different after A1 A2 B1 B2 than it would be after
|
||||
# A1 B1 B2 A2; if the generator state was different, the MAC
|
||||
# could not be verified.
|
||||
self.__hmac_send = None
|
||||
self.__hmac_recv = None
|
||||
|
||||
self.__super_init(sock, map)
|
||||
|
||||
# asyncore overwrites addr with the getpeername result
|
||||
# restore our value
|
||||
self.addr = addr
|
||||
|
||||
def setSessionKey(self, sesskey):
|
||||
log("set session key %r" % sesskey)
|
||||
|
||||
# Low-level construction is now delayed until data are sent.
|
||||
# This is to allow use of iterators that generate messages
|
||||
# only when we're ready to do I/O so that we can effeciently
|
||||
# transmit large files. Because we delay messages, we also
|
||||
# have to delay setting the session key to retain proper
|
||||
# ordering.
|
||||
|
||||
# The low-level output queue supports strings, a special close
|
||||
# marker, and iterators. It doesn't support callbacks. We
|
||||
# can create a allback by providing an iterator that doesn't
|
||||
# yield anything.
|
||||
|
||||
# The hack fucntion below is a callback in iterator's
|
||||
# clothing. :) It never yields anything, but is a generator
|
||||
# and thus iterator, because it contains a yield statement.
|
||||
|
||||
def hack():
|
||||
self.__hmac_send = hmac.HMAC(sesskey, digestmod=ZEO_hash)
|
||||
self.__hmac_recv = hmac.HMAC(sesskey, digestmod=ZEO_hash)
|
||||
if False:
|
||||
yield b''
|
||||
|
||||
self.message_output(hack())
|
||||
|
||||
def get_addr(self):
|
||||
return self.addr
|
||||
|
||||
# TODO: avoid expensive getattr calls? Can't remember exactly what
|
||||
# this comment was supposed to mean, but it has something to do
|
||||
# with the way asyncore uses getattr and uses if sock:
|
||||
def __nonzero__(self):
|
||||
return 1
|
||||
|
||||
def handle_read(self):
|
||||
self.__input_lock.acquire()
|
||||
try:
|
||||
# Use a single __inp buffer and integer indexes to make this fast.
|
||||
try:
|
||||
d = self.recv(8192)
|
||||
except socket.error as err:
|
||||
# Python >= 3.3 makes select.error an alias of OSError,
|
||||
# which is not subscriptable but does have the 'errno' attribute
|
||||
err_errno = getattr(err, 'errno', None) or err[0]
|
||||
if err_errno in expected_socket_read_errors:
|
||||
return
|
||||
raise
|
||||
if not d:
|
||||
return
|
||||
|
||||
input_len = self.__input_len + len(d)
|
||||
msg_size = self.__msg_size
|
||||
state = self.__state
|
||||
has_mac = self.__has_mac
|
||||
|
||||
inp = self.__inp
|
||||
if msg_size > input_len:
|
||||
if inp is None:
|
||||
self.__inp = d
|
||||
elif isinstance(self.__inp, six.binary_type):
|
||||
self.__inp = [self.__inp, d]
|
||||
else:
|
||||
self.__inp.append(d)
|
||||
self.__input_len = input_len
|
||||
return # keep waiting for more input
|
||||
|
||||
# load all previous input and d into single string inp
|
||||
if isinstance(inp, six.binary_type):
|
||||
inp = inp + d
|
||||
elif inp is None:
|
||||
inp = d
|
||||
else:
|
||||
inp.append(d)
|
||||
inp = b"".join(inp)
|
||||
|
||||
offset = 0
|
||||
while (offset + msg_size) <= input_len:
|
||||
msg = inp[offset:offset + msg_size]
|
||||
offset = offset + msg_size
|
||||
if not state:
|
||||
msg_size = struct.unpack(">I", msg)[0]
|
||||
has_mac = msg_size & MAC_BIT
|
||||
if has_mac:
|
||||
msg_size ^= MAC_BIT
|
||||
msg_size += 20
|
||||
elif self.__hmac_send:
|
||||
raise ValueError("Received message without MAC")
|
||||
state = 1
|
||||
else:
|
||||
msg_size = 4
|
||||
state = 0
|
||||
# Obscure: We call message_input() with __input_lock
|
||||
# held!!! And message_input() may end up calling
|
||||
# message_output(), which has its own lock. But
|
||||
# message_output() cannot call message_input(), so
|
||||
# the locking order is always consistent, which
|
||||
# prevents deadlock. Also, message_input() may
|
||||
# take a long time, because it can cause an
|
||||
# incoming call to be handled. During all this
|
||||
# time, the __input_lock is held. That's a good
|
||||
# thing, because it serializes incoming calls.
|
||||
if has_mac:
|
||||
mac = msg[:20]
|
||||
msg = msg[20:]
|
||||
if self.__hmac_recv:
|
||||
self.__hmac_recv.update(msg)
|
||||
_mac = self.__hmac_recv.digest()
|
||||
if mac != _mac:
|
||||
raise ValueError("MAC failed: %r != %r"
|
||||
% (_mac, mac))
|
||||
else:
|
||||
log("Received MAC but no session key set")
|
||||
elif self.__hmac_send:
|
||||
raise ValueError("Received message without MAC")
|
||||
self.message_input(msg)
|
||||
|
||||
self.__state = state
|
||||
self.__has_mac = has_mac
|
||||
self.__msg_size = msg_size
|
||||
self.__inp = inp[offset:]
|
||||
self.__input_len = input_len - offset
|
||||
finally:
|
||||
self.__input_lock.release()
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def writable(self):
|
||||
return bool(self.__output_messages or self.__output)
|
||||
|
||||
def should_close(self):
|
||||
self.__output_messages.append(_close_marker)
|
||||
|
||||
def handle_write(self):
|
||||
output = self.__output
|
||||
messages = self.__output_messages
|
||||
while output or messages:
|
||||
|
||||
# Process queued messages until we have enough output
|
||||
size = sum((len(s) for s in output))
|
||||
while (size <= SEND_SIZE) and messages:
|
||||
message = messages[0]
|
||||
if isinstance(message, six.binary_type):
|
||||
size += self.__message_output(messages.pop(0), output)
|
||||
elif isinstance(message, six.text_type):
|
||||
# XXX This can silently lead to data loss and client hangs
|
||||
# if asserts aren't enabled. Encountered this under Python3
|
||||
# and 'ruok' protocol
|
||||
assert False, "Got a unicode message: %s" % repr(message)
|
||||
elif message is _close_marker:
|
||||
del messages[:]
|
||||
del output[:]
|
||||
return self.close()
|
||||
else:
|
||||
try:
|
||||
message = six.advance_iterator(message)
|
||||
except StopIteration:
|
||||
messages.pop(0)
|
||||
else:
|
||||
assert(isinstance(message, six.binary_type))
|
||||
size += self.__message_output(message, output)
|
||||
|
||||
v = b"".join(output)
|
||||
del output[:]
|
||||
|
||||
try:
|
||||
n = self.send(v)
|
||||
except socket.error as err:
|
||||
# Fix for https://bugs.launchpad.net/zodb/+bug/182833
|
||||
# ensure the above mentioned "output" invariant
|
||||
output.insert(0, v)
|
||||
# Python >= 3.3 makes select.error an alias of OSError,
|
||||
# which is not subscriptable but does have the 'errno' attribute
|
||||
err_errno = getattr(err, 'errno', None) or err[0]
|
||||
if err_errno in expected_socket_write_errors:
|
||||
break # we couldn't write anything
|
||||
raise
|
||||
|
||||
if n < len(v):
|
||||
output.append(v[n:])
|
||||
break # we can't write any more
|
||||
|
||||
def handle_close(self):
|
||||
self.close()
|
||||
|
||||
def message_output(self, message):
|
||||
if self.__closed:
|
||||
raise DisconnectedError(
|
||||
"This action is temporarily unavailable.<p>")
|
||||
self.__output_messages.append(message)
|
||||
|
||||
def __message_output(self, message, output):
|
||||
# do two separate appends to avoid copying the message string
|
||||
size = 4
|
||||
if self.__hmac_send:
|
||||
output.append(struct.pack(">I", len(message) | MAC_BIT))
|
||||
self.__hmac_send.update(message)
|
||||
output.append(self.__hmac_send.digest())
|
||||
size += 20
|
||||
else:
|
||||
output.append(struct.pack(">I", len(message)))
|
||||
|
||||
if len(message) <= SEND_SIZE:
|
||||
output.append(message)
|
||||
else:
|
||||
for i in range(0, len(message), SEND_SIZE):
|
||||
output.append(message[i:i+SEND_SIZE])
|
||||
|
||||
return size + len(message)
|
||||
|
||||
def close(self):
|
||||
if not self.__closed:
|
||||
self.__closed = True
|
||||
self.__super_close()
|
||||
@@ -0,0 +1,235 @@
|
||||
from __future__ import print_function
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2001-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
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import asyncore
|
||||
import os
|
||||
import socket
|
||||
import errno
|
||||
|
||||
from ZODB.utils import positive_id
|
||||
from ZEO._compat import thread, get_ident
|
||||
|
||||
# Original comments follow; they're hard to follow in the context of
|
||||
# ZEO's use of triggers. TODO: rewrite from a ZEO perspective.
|
||||
|
||||
# Wake up a call to select() running in the main thread.
|
||||
#
|
||||
# This is useful in a context where you are using Medusa's I/O
|
||||
# subsystem to deliver data, but the data is generated by another
|
||||
# thread. Normally, if Medusa is in the middle of a call to
|
||||
# select(), new output data generated by another thread will have
|
||||
# to sit until the call to select() either times out or returns.
|
||||
# If the trigger is 'pulled' by another thread, it should immediately
|
||||
# generate a READ event on the trigger object, which will force the
|
||||
# select() invocation to return.
|
||||
#
|
||||
# A common use for this facility: letting Medusa manage I/O for a
|
||||
# large number of connections; but routing each request through a
|
||||
# thread chosen from a fixed-size thread pool. When a thread is
|
||||
# acquired, a transaction is performed, but output data is
|
||||
# accumulated into buffers that will be emptied more efficiently
|
||||
# by Medusa. [picture a server that can process database queries
|
||||
# rapidly, but doesn't want to tie up threads waiting to send data
|
||||
# to low-bandwidth connections]
|
||||
#
|
||||
# The other major feature provided by this class is the ability to
|
||||
# move work back into the main thread: if you call pull_trigger()
|
||||
# with a thunk argument, when select() wakes up and receives the
|
||||
# event it will call your thunk from within that thread. The main
|
||||
# purpose of this is to remove the need to wrap thread locks around
|
||||
# Medusa's data structures, which normally do not need them. [To see
|
||||
# why this is true, imagine this scenario: A thread tries to push some
|
||||
# new data onto a channel's outgoing data queue at the same time that
|
||||
# the main thread is trying to remove some]
|
||||
|
||||
class _triggerbase(object):
|
||||
"""OS-independent base class for OS-dependent trigger class."""
|
||||
|
||||
kind = None # subclass must set to "pipe" or "loopback"; used by repr
|
||||
|
||||
def __init__(self):
|
||||
self._closed = False
|
||||
|
||||
# `lock` protects the `thunks` list from being traversed and
|
||||
# appended to simultaneously.
|
||||
self.lock = thread.allocate_lock()
|
||||
|
||||
# List of no-argument callbacks to invoke when the trigger is
|
||||
# pulled. These run in the thread running the asyncore mainloop,
|
||||
# regardless of which thread pulls the trigger.
|
||||
self.thunks = []
|
||||
|
||||
def readable(self):
|
||||
return 1
|
||||
|
||||
def writable(self):
|
||||
return 0
|
||||
|
||||
def handle_connect(self):
|
||||
pass
|
||||
|
||||
def handle_close(self):
|
||||
self.close()
|
||||
|
||||
# Override the asyncore close() method, because it doesn't know about
|
||||
# (so can't close) all the gimmicks we have open. Subclass must
|
||||
# supply a _close() method to do platform-specific closing work. _close()
|
||||
# will be called iff we're not already closed.
|
||||
def close(self):
|
||||
if not self._closed:
|
||||
self._closed = True
|
||||
self.del_channel()
|
||||
self._close() # subclass does OS-specific stuff
|
||||
|
||||
def _close(self): # see close() above; subclass must supply
|
||||
raise NotImplementedError
|
||||
|
||||
def pull_trigger(self, *thunk):
|
||||
if thunk:
|
||||
with self.lock:
|
||||
self.thunks.append(thunk)
|
||||
try:
|
||||
self._physical_pull()
|
||||
except Exception:
|
||||
if not self._closed:
|
||||
raise
|
||||
|
||||
# Subclass must supply _physical_pull, which does whatever the OS
|
||||
# needs to do to provoke the "write" end of the trigger.
|
||||
def _physical_pull(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def handle_read(self):
|
||||
try:
|
||||
self.recv(8192)
|
||||
except socket.error:
|
||||
return
|
||||
|
||||
while 1:
|
||||
with self.lock:
|
||||
if self.thunks:
|
||||
thunk = self.thunks.pop(0)
|
||||
else:
|
||||
return
|
||||
try:
|
||||
thunk[0](*thunk[1:])
|
||||
except:
|
||||
nil, t, v, tbinfo = asyncore.compact_traceback()
|
||||
print(('exception in trigger thunk:'
|
||||
' (%s:%s %s)' % (t, v, tbinfo)))
|
||||
|
||||
def __repr__(self):
|
||||
return '<select-trigger (%s) at %x>' % (self.kind, positive_id(self))
|
||||
|
||||
if os.name == 'posix':
|
||||
|
||||
class trigger(_triggerbase, asyncore.file_dispatcher):
|
||||
kind = "pipe"
|
||||
|
||||
def __init__(self, map=None):
|
||||
_triggerbase.__init__(self)
|
||||
r, self.trigger = os.pipe()
|
||||
asyncore.file_dispatcher.__init__(self, r, map)
|
||||
|
||||
if self.socket.fd != r:
|
||||
# Starting in Python 2.6, the descriptor passed to
|
||||
# file_dispatcher gets duped and assigned to
|
||||
# self.socket.fd. This breals the instantiation semantics and
|
||||
# is a bug imo. I dount it will get fixed, but maybe
|
||||
# it will. Who knows. For that reason, we test for the
|
||||
# fd changing rather than just checking the Python version.
|
||||
os.close(r)
|
||||
|
||||
def _close(self):
|
||||
os.close(self.trigger)
|
||||
asyncore.file_dispatcher.close(self)
|
||||
|
||||
def _physical_pull(self):
|
||||
os.write(self.trigger, b'x')
|
||||
|
||||
else:
|
||||
# Windows version; uses just sockets, because a pipe isn't select'able
|
||||
# on Windows.
|
||||
|
||||
class BindError(Exception):
|
||||
pass
|
||||
|
||||
class trigger(_triggerbase, asyncore.dispatcher):
|
||||
kind = "loopback"
|
||||
|
||||
def __init__(self, map=None):
|
||||
_triggerbase.__init__(self)
|
||||
|
||||
# Get a pair of connected sockets. The trigger is the 'w'
|
||||
# end of the pair, which is connected to 'r'. 'r' is put
|
||||
# in the asyncore socket map. "pulling the trigger" then
|
||||
# means writing something on w, which will wake up r.
|
||||
|
||||
w = socket.socket()
|
||||
# Disable buffering -- pulling the trigger sends 1 byte,
|
||||
# and we want that sent immediately, to wake up asyncore's
|
||||
# select() ASAP.
|
||||
w.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
count = 0
|
||||
while 1:
|
||||
count += 1
|
||||
# Bind to a local port; for efficiency, let the OS pick
|
||||
# a free port for us.
|
||||
# Unfortunately, stress tests showed that we may not
|
||||
# be able to connect to that port ("Address already in
|
||||
# use") despite that the OS picked it. This appears
|
||||
# to be a race bug in the Windows socket implementation.
|
||||
# So we loop until a connect() succeeds (almost always
|
||||
# on the first try). See the long thread at
|
||||
# http://mail.zope.org/pipermail/zope/2005-July/160433.html
|
||||
# for hideous details.
|
||||
a = socket.socket()
|
||||
a.bind(("127.0.0.1", 0))
|
||||
connect_address = a.getsockname() # assigned (host, port) pair
|
||||
a.listen(1)
|
||||
try:
|
||||
w.connect(connect_address)
|
||||
break # success
|
||||
except socket.error as detail:
|
||||
if detail[0] != errno.WSAEADDRINUSE:
|
||||
# "Address already in use" is the only error
|
||||
# I've seen on two WinXP Pro SP2 boxes, under
|
||||
# Pythons 2.3.5 and 2.4.1.
|
||||
raise
|
||||
# (10048, 'Address already in use')
|
||||
# assert count <= 2 # never triggered in Tim's tests
|
||||
if count >= 10: # I've never seen it go above 2
|
||||
a.close()
|
||||
w.close()
|
||||
raise BindError("Cannot bind trigger!")
|
||||
# Close `a` and try again. Note: I originally put a short
|
||||
# sleep() here, but it didn't appear to help or hurt.
|
||||
a.close()
|
||||
|
||||
r, addr = a.accept() # r becomes asyncore's (self.)socket
|
||||
a.close()
|
||||
self.trigger = w
|
||||
asyncore.dispatcher.__init__(self, r, map)
|
||||
|
||||
def _close(self):
|
||||
# self.socket is r, and self.trigger is w, from __init__
|
||||
self.socket.close()
|
||||
self.trigger.close()
|
||||
|
||||
def _physical_pull(self):
|
||||
self.trigger.send('x')
|
||||
@@ -0,0 +1,13 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
@@ -0,0 +1,77 @@
|
||||
ZEO Client Configuration
|
||||
========================
|
||||
|
||||
Here we'll describe (and test) the various ZEO Client configuration
|
||||
options. To facilitate this, we'l start a server that our client can
|
||||
connect to:
|
||||
|
||||
>>> addr, _ = start_server(blob_dir='server-blobs')
|
||||
|
||||
The simplest client configuration specified a server address:
|
||||
|
||||
>>> import ZODB.config
|
||||
>>> storage = ZODB.config.storageFromString("""
|
||||
... <zeoclient>
|
||||
... server %s:%s
|
||||
... </zeoclient>
|
||||
... """ % addr)
|
||||
|
||||
>>> storage.getName(), storage.__class__.__name__
|
||||
... # doctest: +ELLIPSIS
|
||||
("[('127.0.0.1', ...)] (connected)", 'ClientStorage')
|
||||
|
||||
>>> storage.blob_dir
|
||||
>>> storage._storage
|
||||
'1'
|
||||
>>> storage._cache.maxsize
|
||||
20971520
|
||||
>>> storage._cache.path
|
||||
>>> storage._is_read_only
|
||||
False
|
||||
>>> storage._read_only_fallback
|
||||
False
|
||||
>>> storage._blob_cache_size
|
||||
|
||||
>>> storage.close()
|
||||
|
||||
>>> storage = ZODB.config.storageFromString("""
|
||||
... <zeoclient>
|
||||
... server %s:%s
|
||||
... blob-dir blobs
|
||||
... storage 2
|
||||
... cache-size 100
|
||||
... name bob
|
||||
... client cache
|
||||
... read-only true
|
||||
... drop-cache-rather-verify true
|
||||
... blob-cache-size 1000MB
|
||||
... blob-cache-size-check 10
|
||||
... wait false
|
||||
... </zeoclient>
|
||||
... """ % addr)
|
||||
|
||||
|
||||
>>> storage.getName(), storage.__class__.__name__
|
||||
('bob (disconnected)', 'ClientStorage')
|
||||
|
||||
>>> storage.blob_dir
|
||||
'blobs'
|
||||
>>> storage._storage
|
||||
'2'
|
||||
>>> storage._cache.maxsize
|
||||
100
|
||||
>>> import os
|
||||
>>> storage._cache.path == os.path.abspath('cache-2.zec')
|
||||
True
|
||||
|
||||
>>> storage._is_read_only
|
||||
True
|
||||
>>> storage._read_only_fallback
|
||||
False
|
||||
>>> storage._blob_cache_size
|
||||
1048576000
|
||||
|
||||
>>> print(storage._blob_cache_size_check)
|
||||
104857600
|
||||
|
||||
>>> storage.close()
|
||||
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDCDCCAfACCQCbN0hYgirJXTANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
|
||||
VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
|
||||
cyBQdHkgTHRkMCAXDTE3MDcxMTEzNTM0OFoYDzQ3NTUwNjA3MTM1MzQ4WjBFMQsw
|
||||
CQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJu
|
||||
ZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEA9K5RplDXJpZcrkP+2AjaMxXOZ1ANRDY77BdUuAxtKoZIT0Mc9kUNr4TvLab4
|
||||
CLVc5iwwEfir7xPSPEexkZl88IJuqjWS62d11JFlbU62Vq54ZcEYiimnuR1h5zeU
|
||||
zKGqNcMExbTo7jcgSj3FnH4wCUeWWOUEVasnpafL1O4ViT9BuaxWXKD7gnoFSzg6
|
||||
QblzCO2fxknqadLdowTmnyF5EUi2ufaMyY47akZFC8Bf08GnrZtAsENFRHTkZf76
|
||||
YNkivvN+Gnfr60ktiL5HsCarAkjXYqpfi8YpwrlFkqnucqv3VzQwOQsvu94UJScu
|
||||
iD5V9MEc2n3lpk+IrQeaDNmC6QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQDMBjeH
|
||||
tsFkr5GgM3wczmZrCZxq/UEK++L0KGm1k6zGAB54bf01J9QgvBBmlKK7sPC00I/h
|
||||
9MWD4bs4IfQWhyr28mYui+rJl7C9V4mmwY78DqG/gRi/qx+YxhIuuvZ+wyqtAOl9
|
||||
5e5Zn8puT6mJEI23EsBpmRKTyqE8acMZdkjFhVq4Ytxe5foyqNVYoK8PS/RgdUIr
|
||||
ZGHXUhhiCI1W+OrB2/GpeTyTUV4itBBLaW6+DuWYnWL4kljJDx0BsK2UOHvS1mc3
|
||||
mBPJEEqIaWxnaAARe7RPkfOvcWknhfz2oNfDIAhMe22/mY9ka3dwtB9gx+Krpf0M
|
||||
rvtOwosJMZDMBlRC
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA9K5RplDXJpZcrkP+2AjaMxXOZ1ANRDY77BdUuAxtKoZIT0Mc
|
||||
9kUNr4TvLab4CLVc5iwwEfir7xPSPEexkZl88IJuqjWS62d11JFlbU62Vq54ZcEY
|
||||
iimnuR1h5zeUzKGqNcMExbTo7jcgSj3FnH4wCUeWWOUEVasnpafL1O4ViT9BuaxW
|
||||
XKD7gnoFSzg6QblzCO2fxknqadLdowTmnyF5EUi2ufaMyY47akZFC8Bf08GnrZtA
|
||||
sENFRHTkZf76YNkivvN+Gnfr60ktiL5HsCarAkjXYqpfi8YpwrlFkqnucqv3VzQw
|
||||
OQsvu94UJScuiD5V9MEc2n3lpk+IrQeaDNmC6QIDAQABAoIBAErM27MvdYabYvv3
|
||||
V3otwp7pZK8avuOCfPEg9MpLKjhc0tBAYSM8WwG0bvYS3DK1VxAapBtqXQ16jsPU
|
||||
2wj61kIkbbZlKGQEvfXc+Rfgf0eikLXywRDDyT2DKQHpcPjZ11IWK2hRdQAWJC3u
|
||||
EnJT9VVw6BqG8LtL1pQC5wJSQo0xC1sJ/MTr/szLvKRjuYZE7YStpUfV6RYq2KQF
|
||||
7Oa9nPKtxlIbDCa7z4S6y5yiusYrSSFilK0pVSU+9789kGNZMLzKbnGu+YSVB/Bx
|
||||
MLXWRAD8DV9zign255pIU/xI5VKjOwID38JfgdcebV/KeCPu8W6jKKbfUsUCqcjL
|
||||
YjDtHYECgYEA/SaxUoejMOasHppnsAewy/I+DzMuX+KYztqAnzjsuGwRxmxjYyQe
|
||||
w7EidinM3WuloJIBZzA9aULmWjSKOfTsuGm+Mokucbbw9jaWVT6Co3kWrHySInhZ
|
||||
sfTwHKz5ojGBcQD4l06xaVM9utNi6r8wvJijFl5xIsMzc5szEkWs9vkCgYEA9285
|
||||
bGSAAwzUFHVk1pyLKozM2gOtF5rrAUQlWtNVU6K2tw+MKEGara0f+HFZrJZC9Rh2
|
||||
HBm2U9PPt/kJ73HErQG+E6n0jfol8TQ3ZKz3tlSxImh0CiaKLnh4ahf7o8zU16nT
|
||||
XDfu3+Rf11EhORXYfZLmdubfCOD4ZaB2/405N3ECgYEA7b4k0gkoLYi1JJiFwD+4
|
||||
vhBmUAgVCV/ZeoqiOOZRCnITz3GDdVw6uDXm02o2R8wM5Fu6jZo0UmLNyvGEzyFC
|
||||
H37PbM6Am7LfYZuqW6w1LClQLfVfmJfGROZvib65QqWTlvj+fbsdyniuhIJ5Z1Tf
|
||||
BH+kyiEvxyHjdDLRJ9vfsKECgYA8P9MFt7sMAxWpHaS+NUQVyk8fTwHY25oZptRJ
|
||||
t2fxg49mJ90C+GaHn75HKqKhSb1oHNq1oPUqmEreC0AGE/fGAMSd2SZ5Y83VW9eZ
|
||||
JhzzQtAXBsQqrJO9GQyJGOnnSrsRAIM800nRLrS/ozupwM4EVb7UeQcaDF2vsVEI
|
||||
jQS/oQKBgHj26xn7AunX5GS8EYe4GPj4VZehmlnEKONGrPrr25aWkaY4kDJgMLUb
|
||||
AxwIQHbCMm5TMqIxi5l39/O9dxuuGCkOs37j7C3f3VVFuQW1KKyHem9OClgFDZj3
|
||||
tEEk1N3NevrH06NlmAHweHMuJXL8mBvM375zH9tSw5mgG0OMRbnG
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,170 @@
|
||||
Avoiding cache verifification
|
||||
=============================
|
||||
|
||||
For large databases it is common to also use very large ZEO cache
|
||||
files. If a client has beed disconnected for too long, the server
|
||||
can't play back missing invalidations. In this case, the cache is
|
||||
cleared. When this happens, a ZEO.interfaces.StaleCache event is
|
||||
published, largely for backward compatibility.
|
||||
|
||||
ClientStorage used to provide an option to drop it's cache rather than
|
||||
doing verification. This is now the only behavior. Cache
|
||||
verification is no longer supported.
|
||||
|
||||
- Invalidates all object caches
|
||||
|
||||
- Drops or clears it's client cache. (The end result is that the cache
|
||||
is working but empty.)
|
||||
|
||||
- Logs a CRITICAL message.
|
||||
|
||||
Here's an example that shows that this is actually what happens.
|
||||
|
||||
Start a server, create a client to it and commit some data
|
||||
|
||||
>>> addr, admin = start_server(keep=1)
|
||||
>>> import ZEO, transaction
|
||||
>>> db = ZEO.DB(addr, client='cache', name='test')
|
||||
>>> wait_connected(db.storage)
|
||||
>>> conn = db.open()
|
||||
>>> conn.root()[1] = conn.root().__class__()
|
||||
>>> conn.root()[1].x = 1
|
||||
>>> transaction.commit()
|
||||
>>> len(db.storage._cache)
|
||||
3
|
||||
|
||||
Now, we'll stop the server and restart with a different address:
|
||||
|
||||
>>> stop_server(admin)
|
||||
>>> addr2, admin = start_server(keep=1)
|
||||
|
||||
And create another client and write some data to it:
|
||||
|
||||
>>> db2 = ZEO.DB(addr2)
|
||||
>>> wait_connected(db2.storage)
|
||||
>>> conn2 = db2.open()
|
||||
>>> for i in range(5):
|
||||
... conn2.root()[1].x += 1
|
||||
... transaction.commit()
|
||||
>>> db2.close()
|
||||
>>> stop_server(admin)
|
||||
|
||||
Now, we'll restart the server. Before we do that, we'll capture
|
||||
logging and event data:
|
||||
|
||||
>>> import logging, zope.testing.loggingsupport, ZODB.event
|
||||
>>> handler = zope.testing.loggingsupport.InstalledHandler(
|
||||
... 'ZEO', level=logging.ERROR)
|
||||
>>> events = []
|
||||
>>> def event_handler(e):
|
||||
... if hasattr(e, 'storage'):
|
||||
... events.append((
|
||||
... len(e.storage._server.client.cache), str(handler), e.__class__.__name__))
|
||||
|
||||
>>> old_notify = ZODB.event.notify
|
||||
>>> ZODB.event.notify = event_handler
|
||||
|
||||
Note that the event handler is saving away the length of the cache and
|
||||
the state of the log handler. We'll use this to show that the event
|
||||
is generated before the cache is dropped or the message is logged.
|
||||
|
||||
Now, we'll restart the server on the original address:
|
||||
|
||||
>>> _, admin = start_server(zeo_conf=dict(invalidation_queue_size=1),
|
||||
... addr=addr, keep=1)
|
||||
|
||||
>>> wait_connected(db.storage)
|
||||
|
||||
Now, let's verify our assertions above:
|
||||
|
||||
- Publishes a stale-cache event.
|
||||
|
||||
>>> for e in events:
|
||||
... print(e)
|
||||
(3, '', 'StaleCache')
|
||||
|
||||
Note that the length of the cache when the event handler was
|
||||
called waa non-zero. This is because the cache wasn't cleared
|
||||
yet. Similarly, the dropping-cache message hasn't been logged
|
||||
yet.
|
||||
|
||||
>>> del events[:]
|
||||
|
||||
- Drops or clears it's client cache. (The end result is that the cache
|
||||
is working but empty.)
|
||||
|
||||
>>> len(db.storage._cache)
|
||||
0
|
||||
|
||||
- Invalidates all object caches
|
||||
|
||||
>>> transaction.abort()
|
||||
>>> conn.root()._p_changed
|
||||
|
||||
- Logs a CRITICAL message.
|
||||
|
||||
>>> print(handler) # doctest: +ELLIPSIS
|
||||
ZEO... CRITICAL
|
||||
test dropping stale cache
|
||||
|
||||
>>> handler.clear()
|
||||
|
||||
If we access the root object, it'll be loaded from the server:
|
||||
|
||||
>>> conn.root()[1].x
|
||||
6
|
||||
|
||||
Similarly, if we simply disconnect the client, and write data from
|
||||
another client:
|
||||
|
||||
>>> db.close()
|
||||
|
||||
>>> db2 = ZEO.DB(addr)
|
||||
>>> wait_connected(db2.storage)
|
||||
>>> conn2 = db2.open()
|
||||
>>> for i in range(5):
|
||||
... conn2.root()[1].x += 1
|
||||
... transaction.commit()
|
||||
>>> db2.close()
|
||||
|
||||
>>> db = ZEO.DB(addr, drop_cache_rather_verify=True, client='cache',
|
||||
... name='test')
|
||||
>>> wait_connected(db.storage)
|
||||
|
||||
|
||||
- Drops or clears it's client cache. (The end result is that the cache
|
||||
is working but empty.)
|
||||
|
||||
>>> len(db.storage._cache) <= 1
|
||||
True
|
||||
|
||||
(When a database is created, it checks to make sure the root object is
|
||||
in the database, which is why we get 1, rather than 0 objects in the cache.)
|
||||
|
||||
- Publishes a stake-cache event.
|
||||
|
||||
>>> for e in events:
|
||||
... print(e)
|
||||
(2, '', 'StaleCache')
|
||||
|
||||
>>> del events[:]
|
||||
|
||||
- Logs a CRITICAL message.
|
||||
|
||||
>>> print(handler) # doctest: +ELLIPSIS
|
||||
ZEO... CRITICAL
|
||||
test dropping stale cache
|
||||
|
||||
>>> handler.clear()
|
||||
|
||||
If we access the root object, it'll be loaded from the server:
|
||||
|
||||
>>> conn = db.open()
|
||||
>>> conn.root()[1].x
|
||||
11
|
||||
|
||||
.. Cleanup
|
||||
|
||||
>>> db.close()
|
||||
>>> handler.uninstall()
|
||||
>>> ZODB.event.notify = old_notify
|
||||
@@ -0,0 +1,85 @@
|
||||
The storage server can be told to bind to port 0, allowing the OS to
|
||||
pick a port dynamically. For this to be useful, there needs to be a
|
||||
way to tell someone. For this reason, the server posts events to
|
||||
ZODB.notify.
|
||||
|
||||
>>> import ZODB.event
|
||||
>>> old_notify = ZODB.event.notify
|
||||
|
||||
>>> last_event = None
|
||||
>>> def notify(event):
|
||||
... global last_event
|
||||
... last_event = event
|
||||
>>> ZODB.event.notify = notify
|
||||
|
||||
Now, let's start a server and verify that we get a serving event:
|
||||
|
||||
>>> import ZEO
|
||||
>>> addr, stop = ZEO.server()
|
||||
|
||||
>>> isinstance(last_event, ZEO.StorageServer.Serving)
|
||||
True
|
||||
|
||||
>>> last_event.address == addr
|
||||
True
|
||||
|
||||
>>> server = last_event.server
|
||||
>>> server.addr == addr
|
||||
True
|
||||
|
||||
Let's make sure we can connect.
|
||||
|
||||
>>> client = ZEO.client(last_event.address).close()
|
||||
|
||||
If we close the server, we'll get a closed event:
|
||||
|
||||
>>> stop()
|
||||
>>> isinstance(last_event, ZEO.StorageServer.Closed)
|
||||
True
|
||||
>>> last_event.server is server
|
||||
True
|
||||
|
||||
If we pass an empty string as the host part of the server address, we
|
||||
can't really assign a single address, so the server addr attribute is
|
||||
left alone:
|
||||
|
||||
>>> addr, stop = ZEO.server(port=('', 0))
|
||||
>>> isinstance(last_event, ZEO.StorageServer.Serving)
|
||||
True
|
||||
|
||||
>>> last_event.address[1] > 0
|
||||
True
|
||||
|
||||
>>> last_event.server.addr
|
||||
('', 0)
|
||||
|
||||
>>> stop()
|
||||
|
||||
The runzeo module provides some process support, including getting the
|
||||
server configuration via a ZConfig configuration file. To spell a
|
||||
dynamic port using ZConfig, you'd use a hostname by itself. In this
|
||||
case, ZConfig passes None as the port.
|
||||
|
||||
>>> import ZEO.runzeo
|
||||
>>> with open('conf', 'w') as f:
|
||||
... _ = f.write("""
|
||||
... <zeo>
|
||||
... address 127.0.0.1
|
||||
... </zeo>
|
||||
... <mappingstorage>
|
||||
... </mappingstorage>
|
||||
... """)
|
||||
>>> options = ZEO.runzeo.ZEOOptions()
|
||||
>>> options.realize('-C conf'.split())
|
||||
>>> options.address
|
||||
('127.0.0.1', None)
|
||||
|
||||
>>> rs = ZEO.runzeo.ZEOServer(options)
|
||||
>>> rs.check_socket()
|
||||
>>> options.address
|
||||
('127.0.0.1', 0)
|
||||
|
||||
|
||||
.. cleanup
|
||||
|
||||
>>> ZODB.event.notify = old_notify
|
||||
@@ -0,0 +1,207 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
"""Library for forking storage server and connecting client storage"""
|
||||
from __future__ import print_function
|
||||
|
||||
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
import socket
|
||||
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
import ZODB.tests.util
|
||||
import zope.testing.setupstack
|
||||
|
||||
from ZEO._compat import WIN
|
||||
from ZEO import _forker
|
||||
|
||||
logger = logging.getLogger('ZEO.tests.forker')
|
||||
|
||||
DEBUG = _forker.DEBUG
|
||||
|
||||
ZEO4_SERVER = _forker.ZEO4_SERVER
|
||||
|
||||
skip_if_testing_client_against_zeo4 = (
|
||||
(lambda func: None)
|
||||
if ZEO4_SERVER else
|
||||
(lambda func: func)
|
||||
)
|
||||
|
||||
|
||||
ZEOConfig = _forker.ZEOConfig
|
||||
|
||||
|
||||
def encode_format(fmt):
|
||||
# The list of replacements mirrors
|
||||
# ZConfig.components.logger.handlers._control_char_rewrites
|
||||
for xform in (("\n", r"\n"), ("\t", r"\t"), ("\b", r"\b"),
|
||||
("\f", r"\f"), ("\r", r"\r")):
|
||||
fmt = fmt.replace(*xform)
|
||||
return fmt
|
||||
|
||||
runner = _forker.runner
|
||||
|
||||
stop_runner = _forker.stop_runner
|
||||
start_zeo_server = _forker.start_zeo_server
|
||||
|
||||
if WIN:
|
||||
def _quote_arg(s):
|
||||
return '"%s"' % s
|
||||
else:
|
||||
def _quote_arg(s):
|
||||
return s
|
||||
|
||||
shutdown_zeo_server = _forker.shutdown_zeo_server
|
||||
|
||||
def get_port(ignored=None):
|
||||
"""Return a port that is not in use.
|
||||
|
||||
Checks if a port is in use by trying to connect to it. Assumes it
|
||||
is not in use if connect raises an exception. We actually look for
|
||||
2 consective free ports because most of the clients of this
|
||||
function will use the returned port and the next one.
|
||||
|
||||
Raises RuntimeError after 10 tries.
|
||||
"""
|
||||
|
||||
for _i in range(10):
|
||||
port = random.randrange(20000, 30000)
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
try:
|
||||
s.connect(('127.0.0.1', port))
|
||||
except socket.error:
|
||||
pass # Perhaps we should check value of error too.
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
s1.connect(('127.0.0.1', port+1))
|
||||
except socket.error:
|
||||
pass # Perhaps we should check value of error too.
|
||||
else:
|
||||
continue
|
||||
|
||||
return port
|
||||
|
||||
finally:
|
||||
s.close()
|
||||
s1.close()
|
||||
raise RuntimeError("Can't find port")
|
||||
|
||||
def can_connect(port):
|
||||
c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
try:
|
||||
c.connect(('127.0.0.1', port))
|
||||
except socket.error:
|
||||
return False # Perhaps we should check value of error too.
|
||||
else:
|
||||
return True
|
||||
finally:
|
||||
c.close()
|
||||
|
||||
def setUp(test):
|
||||
ZODB.tests.util.setUp(test)
|
||||
|
||||
servers = []
|
||||
|
||||
def start_server(storage_conf=None, zeo_conf=None, port=None, keep=False,
|
||||
addr=None, path='Data.fs', protocol=None, blob_dir=None,
|
||||
suicide=True, debug=False, **kw):
|
||||
"""Start a ZEO server.
|
||||
|
||||
Return the server and admin addresses.
|
||||
"""
|
||||
if port is None:
|
||||
if addr is None:
|
||||
port = 0
|
||||
else:
|
||||
port = addr[1]
|
||||
elif addr is not None:
|
||||
raise TypeError("Can't specify port and addr")
|
||||
addr, stop = start_zeo_server(
|
||||
storage_conf=storage_conf,
|
||||
zeo_conf=zeo_conf,
|
||||
port=port,
|
||||
keep=keep,
|
||||
path=path,
|
||||
protocol=protocol,
|
||||
blob_dir=blob_dir,
|
||||
suicide=suicide,
|
||||
debug=debug,
|
||||
**kw)
|
||||
servers.append(stop)
|
||||
return addr, stop
|
||||
|
||||
test.globs['start_server'] = start_server
|
||||
|
||||
def stop_server(stop):
|
||||
stop()
|
||||
servers.remove(stop)
|
||||
|
||||
test.globs['stop_server'] = stop_server
|
||||
|
||||
def cleanup_servers():
|
||||
for stop in list(servers):
|
||||
stop()
|
||||
|
||||
zope.testing.setupstack.register(test, cleanup_servers)
|
||||
|
||||
test.globs['wait_until'] = wait_until
|
||||
test.globs['wait_connected'] = wait_connected
|
||||
test.globs['wait_disconnected'] = wait_disconnected
|
||||
|
||||
|
||||
def wait_until(label=None, func=None, timeout=30, onfail=None):
|
||||
if label is None:
|
||||
if func is not None:
|
||||
label = func.__name__
|
||||
elif not isinstance(label, six.string_types) and func is None:
|
||||
func = label
|
||||
label = func.__name__
|
||||
|
||||
if func is None:
|
||||
def wait_decorator(f):
|
||||
wait_until(label, f, timeout, onfail)
|
||||
|
||||
return wait_decorator
|
||||
|
||||
giveup = time.time() + timeout
|
||||
while not func():
|
||||
if time.time() > giveup:
|
||||
if onfail is None:
|
||||
raise AssertionError("Timed out waiting for: ", label)
|
||||
else:
|
||||
return onfail()
|
||||
time.sleep(0.01)
|
||||
|
||||
def wait_connected(storage):
|
||||
wait_until("storage is connected", storage.is_connected)
|
||||
|
||||
def wait_disconnected(storage):
|
||||
wait_until("storage is disconnected",
|
||||
lambda: not storage.is_connected())
|
||||
|
||||
|
||||
debug_logging = _forker.debug_logging
|
||||
whine = _forker.whine
|
||||
ThreadlessQueue = _forker.ThreadlessQueue
|
||||
@@ -0,0 +1,124 @@
|
||||
Invalidation age
|
||||
================
|
||||
|
||||
When a ZEO client with a non-empty cache connects to the server, it
|
||||
needs to verify whether the data in its cache is current. It does
|
||||
this in one of 2 ways:
|
||||
|
||||
quick verification
|
||||
It gets a list of invalidations from the server since the last
|
||||
transaction the client has seen and applies those to it's disk and
|
||||
in-memory caches. This is only possible if there haven't been too
|
||||
many transactions since the client was last connected.
|
||||
|
||||
full verification
|
||||
If quick verification isn't possible, the client iterates through
|
||||
it's disk cache asking the server to verify whether each current
|
||||
entry is valid.
|
||||
|
||||
Unfortunately, for large caches, full verification is soooooo not
|
||||
quick that it is impractical. Quick verificatioin is highly
|
||||
desireable.
|
||||
|
||||
To support quick verification, the server keeps a list of recent
|
||||
invalidations. The size of this list is controlled by the
|
||||
invalidation_queue_size parameter. If there is a lot of database
|
||||
activity, the size might need to be quite large to support having
|
||||
clients be disconnected for more than a few minutes. A very large
|
||||
invalidation queue size can use a lot of memory.
|
||||
|
||||
To suppliment the invalidation queue, you can also specify an
|
||||
invalidation_age parameter. When a client connects and presents the
|
||||
last transaction id it has seen, we first check to see if the
|
||||
invalidation queue has that transaction id. It it does, then we send
|
||||
all transactions since that id. Otherwise, we check to see if the
|
||||
difference between storage's last transaction id and the given id is
|
||||
less than or equal to the invalidation age. If it is, then we iterate
|
||||
over the storage, starting with the given id, to get the invalidations
|
||||
since the given id.
|
||||
|
||||
NOTE: This assumes that iterating from a point near the "end" of a
|
||||
database is inexpensive. Don't use this option for a storage for which
|
||||
that is not the case.
|
||||
|
||||
Here's an example. We set up a server, using an
|
||||
invalidation-queue-size of 5:
|
||||
|
||||
>>> addr, admin = start_server(zeo_conf=dict(invalidation_queue_size=5),
|
||||
... keep=True)
|
||||
|
||||
Now, we'll open a client with a persistent cache, set up some data,
|
||||
and then close client:
|
||||
|
||||
>>> import ZEO, transaction
|
||||
>>> db = ZEO.DB(addr, client='test')
|
||||
>>> conn = db.open()
|
||||
>>> for i in range(9):
|
||||
... conn.root()[i] = conn.root().__class__()
|
||||
... conn.root()[i].x = 0
|
||||
>>> transaction.commit()
|
||||
>>> db.close()
|
||||
|
||||
We'll open another client, and commit some transactions:
|
||||
|
||||
>>> db = ZEO.DB(addr)
|
||||
>>> conn = db.open()
|
||||
>>> import transaction
|
||||
>>> for i in range(2):
|
||||
... conn.root()[i].x = 1
|
||||
... transaction.commit()
|
||||
>>> db.close()
|
||||
|
||||
If we reopen the first client, we'll do quick verification.
|
||||
|
||||
>>> db = ZEO.DB(addr, client='test') # doctest: +ELLIPSIS
|
||||
>>> db._storage._server.client.verify_result
|
||||
'quick verification'
|
||||
|
||||
>>> [v.x for v in db.open().root().values()]
|
||||
[1, 1, 0, 0, 0, 0, 0, 0, 0]
|
||||
|
||||
Now, if we disconnect and commit more than 5 transactions, we'll see
|
||||
that we had to clear the cache:
|
||||
|
||||
>>> db.close()
|
||||
>>> db = ZEO.DB(addr)
|
||||
>>> conn = db.open()
|
||||
>>> import transaction
|
||||
>>> for i in range(9):
|
||||
... conn.root()[i].x = 2
|
||||
... transaction.commit()
|
||||
>>> db.close()
|
||||
|
||||
>>> db = ZEO.DB(addr, client='test')
|
||||
>>> db._storage._server.client.verify_result
|
||||
'cache too old, clearing'
|
||||
|
||||
>>> [v.x for v in db.open().root().values()]
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2]
|
||||
|
||||
>>> db.close()
|
||||
|
||||
But if we restart the server with invalidation-age set, we can
|
||||
do quick verification:
|
||||
|
||||
>>> stop_server(admin)
|
||||
>>> addr, admin = start_server(zeo_conf=dict(invalidation_queue_size=5,
|
||||
... invalidation_age=100))
|
||||
>>> db = ZEO.DB(addr)
|
||||
>>> conn = db.open()
|
||||
>>> import transaction
|
||||
>>> for i in range(9):
|
||||
... conn.root()[i].x = 3
|
||||
... transaction.commit()
|
||||
>>> db.close()
|
||||
|
||||
|
||||
>>> db = ZEO.DB(addr, client='test') # doctest: +ELLIPSIS
|
||||
>>> db._storage._server.client.verify_result
|
||||
'quick verification'
|
||||
|
||||
>>> [v.x for v in db.open().root().values()]
|
||||
[3, 3, 3, 3, 3, 3, 3, 3, 3]
|
||||
|
||||
>>> db.close()
|
||||
@@ -0,0 +1,47 @@
|
||||
You can change the address(es) of a client storaage.
|
||||
|
||||
We'll start by setting up a server and connecting to it:
|
||||
|
||||
>>> import ZEO, transaction
|
||||
|
||||
>>> addr, stop = ZEO.server(path='test.fs', threaded=False)
|
||||
>>> conn = ZEO.connection(addr)
|
||||
>>> client = conn.db().storage
|
||||
>>> client.is_connected()
|
||||
True
|
||||
>>> conn.root()
|
||||
{}
|
||||
>>> conn.root.x = 1
|
||||
>>> transaction.commit()
|
||||
|
||||
Now we'll close the server:
|
||||
|
||||
>>> stop()
|
||||
|
||||
And wait for the connectin to notice it's disconnected:
|
||||
|
||||
>>> wait_until(lambda : not client.is_connected())
|
||||
|
||||
Now, we'll restart the server:
|
||||
|
||||
>>> addr, stop = ZEO.server(path='test.fs', threaded=False)
|
||||
|
||||
Update with another client:
|
||||
|
||||
>>> conn2 = ZEO.connection(addr)
|
||||
>>> conn2.root.x += 1
|
||||
>>> transaction.commit()
|
||||
|
||||
Update the connection and wait for connect:
|
||||
|
||||
>>> client.new_addr(addr)
|
||||
>>> wait_until(lambda : client.is_connected())
|
||||
>>> _ = transaction.begin()
|
||||
>>> conn.root()
|
||||
{'x': 2}
|
||||
|
||||
.. cleanup
|
||||
|
||||
>>> conn.close()
|
||||
>>> conn2.close()
|
||||
>>> stop()
|
||||
@@ -0,0 +1,184 @@
|
||||
Test that multiple protocols are supported
|
||||
==========================================
|
||||
|
||||
A full test of all protocols isn't practical. But we'll do a limited
|
||||
test that at least the current and previous protocols are supported in
|
||||
both directions.
|
||||
|
||||
Let's start a Z4 server
|
||||
|
||||
>>> storage_conf = '''
|
||||
... <blobstorage>
|
||||
... blob-dir server-blobs
|
||||
... <filestorage>
|
||||
... path Data.fs
|
||||
... </filestorage>
|
||||
... </blobstorage>
|
||||
... '''
|
||||
|
||||
>>> addr, stop = start_server(
|
||||
... storage_conf, dict(invalidation_queue_size=5), protocol=b'4')
|
||||
|
||||
A current client should be able to connect to a old server:
|
||||
|
||||
>>> import ZEO, ZODB.blob, transaction
|
||||
>>> db = ZEO.DB(addr, client='client', blob_dir='blobs')
|
||||
>>> wait_connected(db.storage)
|
||||
>>> str(db.storage.protocol_version.decode('ascii'))[1:]
|
||||
'4'
|
||||
|
||||
>>> conn = db.open()
|
||||
>>> conn.root().x = 0
|
||||
>>> transaction.commit()
|
||||
>>> len(db.history(conn.root()._p_oid, 99))
|
||||
2
|
||||
|
||||
>>> conn.root()['blob1'] = ZODB.blob.Blob()
|
||||
>>> with conn.root()['blob1'].open('w') as f:
|
||||
... r = f.write(b'blob data 1')
|
||||
>>> transaction.commit()
|
||||
|
||||
>>> db2 = ZEO.DB(addr, blob_dir='server-blobs', shared_blob_dir=True)
|
||||
>>> wait_connected(db2.storage)
|
||||
>>> conn2 = db2.open()
|
||||
>>> for i in range(5):
|
||||
... conn2.root().x += 1
|
||||
... transaction.commit()
|
||||
>>> conn2.root()['blob2'] = ZODB.blob.Blob()
|
||||
>>> with conn2.root()['blob2'].open('w') as f:
|
||||
... r = f.write(b'blob data 2')
|
||||
>>> transaction.commit()
|
||||
|
||||
>>> @wait_until("Get the new data")
|
||||
... def f():
|
||||
... conn.sync()
|
||||
... return conn.root().x == 5
|
||||
|
||||
>>> db.close()
|
||||
|
||||
>>> for i in range(2):
|
||||
... conn2.root().x += 1
|
||||
... transaction.commit()
|
||||
|
||||
>>> db = ZEO.DB(addr, client='client', blob_dir='blobs')
|
||||
>>> wait_connected(db.storage)
|
||||
>>> conn = db.open()
|
||||
>>> conn.root().x
|
||||
7
|
||||
|
||||
>>> db.close()
|
||||
|
||||
>>> for i in range(10):
|
||||
... conn2.root().x += 1
|
||||
... transaction.commit()
|
||||
|
||||
>>> db = ZEO.DB(addr, client='client', blob_dir='blobs')
|
||||
>>> wait_connected(db.storage)
|
||||
>>> conn = db.open()
|
||||
>>> conn.root().x
|
||||
17
|
||||
|
||||
>>> with conn.root()['blob1'].open() as f:
|
||||
... f.read()
|
||||
b'blob data 1'
|
||||
>>> with conn.root()['blob2'].open() as f:
|
||||
... f.read()
|
||||
b'blob data 2'
|
||||
|
||||
>>> db2.close()
|
||||
>>> db.close()
|
||||
>>> stop_server(stop)
|
||||
|
||||
>>> import os, zope.testing.setupstack
|
||||
>>> os.remove('client-1.zec')
|
||||
>>> zope.testing.setupstack.rmtree('blobs')
|
||||
>>> zope.testing.setupstack.rmtree('server-blobs')
|
||||
|
||||
#############################################################################
|
||||
# Note that the ZEO 5.0 server only supports clients that use the Z5 protocol
|
||||
|
||||
# And the other way around:
|
||||
|
||||
# >>> addr, _ = start_server(storage_conf, dict(invalidation_queue_size=5))
|
||||
|
||||
# Note that we'll have to pull some hijinks:
|
||||
|
||||
# >>> db = ZEO.DB(addr, client='client', blob_dir='blobs')
|
||||
# >>> str(db.storage.protocol_version.decode('ascii'))
|
||||
# 'Z4'
|
||||
# >>> wait_connected(db.storage)
|
||||
# >>> conn = db.open()
|
||||
# >>> conn.root().x = 0
|
||||
# >>> transaction.commit()
|
||||
# >>> len(db.history(conn.root()._p_oid, 99))
|
||||
# 2
|
||||
|
||||
# >>> db = ZEO.DB(addr, client='client', blob_dir='blobs')
|
||||
# >>> db.storage.protocol_version
|
||||
# b'Z4'
|
||||
# >>> wait_connected(db.storage)
|
||||
# >>> conn = db.open()
|
||||
# >>> conn.root().x = 0
|
||||
# >>> transaction.commit()
|
||||
# >>> len(db.history(conn.root()._p_oid, 99))
|
||||
# 2
|
||||
|
||||
# >>> conn.root()['blob1'] = ZODB.blob.Blob()
|
||||
# >>> with conn.root()['blob1'].open('w') as f:
|
||||
# ... r = f.write(b'blob data 1')
|
||||
# >>> transaction.commit()
|
||||
|
||||
# >>> db2 = ZEO.DB(addr, blob_dir='server-blobs', shared_blob_dir=True)
|
||||
# >>> wait_connected(db2.storage)
|
||||
# >>> conn2 = db2.open()
|
||||
# >>> for i in range(5):
|
||||
# ... conn2.root().x += 1
|
||||
# ... transaction.commit()
|
||||
# >>> conn2.root()['blob2'] = ZODB.blob.Blob()
|
||||
# >>> with conn2.root()['blob2'].open('w') as f:
|
||||
# ... r = f.write(b'blob data 2')
|
||||
# >>> transaction.commit()
|
||||
|
||||
|
||||
# >>> @wait_until()
|
||||
# ... def x_to_be_5():
|
||||
# ... conn.sync()
|
||||
# ... return conn.root().x == 5
|
||||
|
||||
# >>> db.close()
|
||||
|
||||
# >>> for i in range(2):
|
||||
# ... conn2.root().x += 1
|
||||
# ... transaction.commit()
|
||||
|
||||
# >>> db = ZEO.DB(addr, client='client', blob_dir='blobs')
|
||||
# >>> wait_connected(db.storage)
|
||||
# >>> conn = db.open()
|
||||
# >>> conn.root().x
|
||||
# 7
|
||||
|
||||
# >>> db.close()
|
||||
|
||||
# >>> for i in range(10):
|
||||
# ... conn2.root().x += 1
|
||||
# ... transaction.commit()
|
||||
|
||||
# >>> db = ZEO.DB(addr, client='client', blob_dir='blobs')
|
||||
# >>> wait_connected(db.storage)
|
||||
# >>> conn = db.open()
|
||||
# >>> conn.root().x
|
||||
# 17
|
||||
|
||||
# >>> with conn.root()['blob1'].open() as f:
|
||||
# ... f.read()
|
||||
# b'blob data 1'
|
||||
# >>> with conn.root()['blob2'].open() as f:
|
||||
# ... f.read()
|
||||
# b'blob data 2'
|
||||
|
||||
# >>> db2.close()
|
||||
# >>> db.close()
|
||||
|
||||
# Undo the hijinks:
|
||||
|
||||
# >>> ZEO.asyncio.client.Protocol.protocols = old_protocols
|
||||
@@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDWDCCAkACCQCI2YETV+CDIzANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQGEwJV
|
||||
UzELMAkGA1UECAwCVkExDTALBgNVBAoMBFpPREIxETAPBgNVBAsMCHpvZGIub3Jn
|
||||
MREwDwYDVQQDDAh6b2RiLm9yZzEcMBoGCSqGSIb3DQEJARYNem9kYkB6b2RiLm9y
|
||||
ZzAgFw0xNzA3MTExMzU5NDBaGA80NzU1MDYwNzEzNTk0MFowbTELMAkGA1UEBhMC
|
||||
VVMxCzAJBgNVBAgMAlZBMQ0wCwYDVQQKDARaT0RCMREwDwYDVQQLDAh6b2RiLm9y
|
||||
ZzERMA8GA1UEAwwIem9kYi5vcmcxHDAaBgkqhkiG9w0BCQEWDXpvZGJAem9kYi5v
|
||||
cmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0rlGmUNcmllyuQ/7Y
|
||||
CNozFc5nUA1ENjvsF1S4DG0qhkhPQxz2RQ2vhO8tpvgItVzmLDAR+KvvE9I8R7GR
|
||||
mXzwgm6qNZLrZ3XUkWVtTrZWrnhlwRiKKae5HWHnN5TMoao1wwTFtOjuNyBKPcWc
|
||||
fjAJR5ZY5QRVqyelp8vU7hWJP0G5rFZcoPuCegVLODpBuXMI7Z/GSepp0t2jBOaf
|
||||
IXkRSLa59ozJjjtqRkULwF/Twaetm0CwQ0VEdORl/vpg2SK+834ad+vrSS2Ivkew
|
||||
JqsCSNdiql+LxinCuUWSqe5yq/dXNDA5Cy+73hQlJy6IPlX0wRzafeWmT4itB5oM
|
||||
2YLpAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGKHiLnFViSExRc2b3NBWBZkKIYb
|
||||
gw14xW+PT4BLjL2etoiETGcdSo4lgUnFKw4FB8zF4BiCnSMYpi1yyLA/1ZgphXnS
|
||||
J25ZAbIXFLxMNkC32K/f1WeVLYOaa/u65dzW2bHOXmFwmCYNq45pNC61rch5umuA
|
||||
6kikl+EoNWpouTtkkY/JnfUCeYrLUbzD8mLxgyNFPKpSEbAo7Q0n2bjTtC+Y5GMW
|
||||
8IJNC3i+2tsglVAjUWg3JwD0O0ql73qMh6rzWIdNcVjMv6KSKpqlzOYvqjfaIAQv
|
||||
cvcXPW3RTMd3e245LPMsGwmv3FQ2dgJxhKkf/P3RtNEPrqOhPUCSxonUa+o=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICsjCCAZoCAQAwbTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMQ0wCwYDVQQK
|
||||
DARaT0RCMREwDwYDVQQLDAh6b2RiLm9yZzERMA8GA1UEAwwIem9kYi5vcmcxHDAa
|
||||
BgkqhkiG9w0BCQEWDXpvZGJAem9kYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQD0rlGmUNcmllyuQ/7YCNozFc5nUA1ENjvsF1S4DG0qhkhPQxz2
|
||||
RQ2vhO8tpvgItVzmLDAR+KvvE9I8R7GRmXzwgm6qNZLrZ3XUkWVtTrZWrnhlwRiK
|
||||
Kae5HWHnN5TMoao1wwTFtOjuNyBKPcWcfjAJR5ZY5QRVqyelp8vU7hWJP0G5rFZc
|
||||
oPuCegVLODpBuXMI7Z/GSepp0t2jBOafIXkRSLa59ozJjjtqRkULwF/Twaetm0Cw
|
||||
Q0VEdORl/vpg2SK+834ad+vrSS2IvkewJqsCSNdiql+LxinCuUWSqe5yq/dXNDA5
|
||||
Cy+73hQlJy6IPlX0wRzafeWmT4itB5oM2YLpAgMBAAGgADANBgkqhkiG9w0BAQsF
|
||||
AAOCAQEAVzxIqDiv3evn3LsKrE0HcSkWKnValZz0e4iF96qmstLs2NJa+WsV7p/J
|
||||
Tg8DgUbQ72G9wN10OQld1k06KUd1SEWhOonBX60lGOkqyn6LHassItbwgCmHC5hk
|
||||
qs7h0b56s/gSnxYvN3tAWiRzNxdudFQBB7Ughy2SRN3ChsNDBuRIsJQN2yZtYjXM
|
||||
lZb2J7hZChFGD+L/9Cq6oPhUD+l1aFUv8PvU3jInf/IYyvNQJ3qeYRpOcNR4cnyf
|
||||
6oRJn2b3ypFF/4F4ZiOb6Qocpcg7qBRRqztr4C2MZuDST4/zIBAHfKlUwD1/uo7A
|
||||
BdXUUeM1J1Gaf8GRLSvB8AeZg6/ztA==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA9K5RplDXJpZcrkP+2AjaMxXOZ1ANRDY77BdUuAxtKoZIT0Mc
|
||||
9kUNr4TvLab4CLVc5iwwEfir7xPSPEexkZl88IJuqjWS62d11JFlbU62Vq54ZcEY
|
||||
iimnuR1h5zeUzKGqNcMExbTo7jcgSj3FnH4wCUeWWOUEVasnpafL1O4ViT9BuaxW
|
||||
XKD7gnoFSzg6QblzCO2fxknqadLdowTmnyF5EUi2ufaMyY47akZFC8Bf08GnrZtA
|
||||
sENFRHTkZf76YNkivvN+Gnfr60ktiL5HsCarAkjXYqpfi8YpwrlFkqnucqv3VzQw
|
||||
OQsvu94UJScuiD5V9MEc2n3lpk+IrQeaDNmC6QIDAQABAoIBAErM27MvdYabYvv3
|
||||
V3otwp7pZK8avuOCfPEg9MpLKjhc0tBAYSM8WwG0bvYS3DK1VxAapBtqXQ16jsPU
|
||||
2wj61kIkbbZlKGQEvfXc+Rfgf0eikLXywRDDyT2DKQHpcPjZ11IWK2hRdQAWJC3u
|
||||
EnJT9VVw6BqG8LtL1pQC5wJSQo0xC1sJ/MTr/szLvKRjuYZE7YStpUfV6RYq2KQF
|
||||
7Oa9nPKtxlIbDCa7z4S6y5yiusYrSSFilK0pVSU+9789kGNZMLzKbnGu+YSVB/Bx
|
||||
MLXWRAD8DV9zign255pIU/xI5VKjOwID38JfgdcebV/KeCPu8W6jKKbfUsUCqcjL
|
||||
YjDtHYECgYEA/SaxUoejMOasHppnsAewy/I+DzMuX+KYztqAnzjsuGwRxmxjYyQe
|
||||
w7EidinM3WuloJIBZzA9aULmWjSKOfTsuGm+Mokucbbw9jaWVT6Co3kWrHySInhZ
|
||||
sfTwHKz5ojGBcQD4l06xaVM9utNi6r8wvJijFl5xIsMzc5szEkWs9vkCgYEA9285
|
||||
bGSAAwzUFHVk1pyLKozM2gOtF5rrAUQlWtNVU6K2tw+MKEGara0f+HFZrJZC9Rh2
|
||||
HBm2U9PPt/kJ73HErQG+E6n0jfol8TQ3ZKz3tlSxImh0CiaKLnh4ahf7o8zU16nT
|
||||
XDfu3+Rf11EhORXYfZLmdubfCOD4ZaB2/405N3ECgYEA7b4k0gkoLYi1JJiFwD+4
|
||||
vhBmUAgVCV/ZeoqiOOZRCnITz3GDdVw6uDXm02o2R8wM5Fu6jZo0UmLNyvGEzyFC
|
||||
H37PbM6Am7LfYZuqW6w1LClQLfVfmJfGROZvib65QqWTlvj+fbsdyniuhIJ5Z1Tf
|
||||
BH+kyiEvxyHjdDLRJ9vfsKECgYA8P9MFt7sMAxWpHaS+NUQVyk8fTwHY25oZptRJ
|
||||
t2fxg49mJ90C+GaHn75HKqKhSb1oHNq1oPUqmEreC0AGE/fGAMSd2SZ5Y83VW9eZ
|
||||
JhzzQtAXBsQqrJO9GQyJGOnnSrsRAIM800nRLrS/ozupwM4EVb7UeQcaDF2vsVEI
|
||||
jQS/oQKBgHj26xn7AunX5GS8EYe4GPj4VZehmlnEKONGrPrr25aWkaY4kDJgMLUb
|
||||
AxwIQHbCMm5TMqIxi5l39/O9dxuuGCkOs37j7C3f3VVFuQW1KKyHem9OClgFDZj3
|
||||
tEEk1N3NevrH06NlmAHweHMuJXL8mBvM375zH9tSw5mgG0OMRbnG
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,24 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID8DCCAtigAwIBAgIJALop9P9MBfzLMA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV
|
||||
BAYTAlVTMQswCQYDVQQIEwJWQTENMAsGA1UEChMEWk9EQjERMA8GA1UEAxMIem9k
|
||||
Yi5vcmcxGjAYBgkqhkiG9w0BCQEWC3B3QHpvZGIub3JnMB4XDTE2MDYyMzE1MTAz
|
||||
MVoXDTE3MDYyMzE1MTAzMVowWDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlZBMQ0w
|
||||
CwYDVQQKEwRaT0RCMREwDwYDVQQDEwh6b2RiLm9yZzEaMBgGCSqGSIb3DQEJARYL
|
||||
cHdAem9kYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKw/iw
|
||||
N1EPddU9QQQ+OnCJv9G3rbTOPt4zEbpfTROIHTME3krFKPALrGF2aK+oBpHx3/TZ
|
||||
HN5UvWK/jmGtDL9jekKCAaeAaVIKlESUS6DIxZY+FaO3re/1fbmBNRz8Cnn1raAw
|
||||
/4YZRDPvblooH4Nt5m7uooGAIIDPft3fInhmGboOoIpXc7nMGVGOWXlDN5I9oFmm
|
||||
4vby4CUMy3A/0wnHgTuMNy7Tpjgz2E/1MRAOyWQ7PZYiASs4ycZfas8058O8DI+o
|
||||
rSYyum/czecIz52P6jbx5LWvcKDWac8QbJoHPelthYtxcMHee2+Nh6MWW688CBzq
|
||||
HSeFAdNO3d9kMiFpAgMBAAGjgbwwgbkwHQYDVR0OBBYEFDui1OC2+2z2rHADglk5
|
||||
tGOndxhoMIGJBgNVHSMEgYEwf4AUO6LU4Lb7bPascAOCWTm0Y6d3GGihXKRaMFgx
|
||||
CzAJBgNVBAYTAlVTMQswCQYDVQQIEwJWQTENMAsGA1UEChMEWk9EQjERMA8GA1UE
|
||||
AxMIem9kYi5vcmcxGjAYBgkqhkiG9w0BCQEWC3B3QHpvZGIub3JnggkAuin0/0wF
|
||||
/MswDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAiEYO8MZ3OG8sqy9t
|
||||
AtUZbv0aTsIUzy/QTUKKDUo8qwKNOylqyqGAZV0tZ5eCoqIGFAwRJBBymIizU3zH
|
||||
U1k2MnYZMVi7uYwSy+qwg52+X7GLl/kaAfx8kNvpr274CuZQnLojJS+K8HtH5Pom
|
||||
YD3gTO3OxGS4IS6uf6DD+mf+C9OBnTl47P0HA0/eHBEXVSc2vsv30H/UoW5VbZ6z
|
||||
6TWkoPwSMVhCNRRRif4/eqCLh24/h5b4uvAC+tsrIPQ9If7EsqVTNMCbAkv3ib6g
|
||||
OmaCdbrGkqvD3UVn7i5ci96UZoF80EWNZiwhMdvQtMfOAR4jHQ1pTepJni6JwzZP
|
||||
UMNDpQ==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,30 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,769B900D03925712
|
||||
|
||||
z5M/XkqEC1+PxJ1T3QrUhGG9fPTBsPxKy8WlwIytbMg0RXS0oJBiFgNYp1ktqzGo
|
||||
yT+AdTCRR1hNVX8M5HbV3ksUjKxXKCL3+yaaB6JtGbNRr2qTNwosvxD92nKT/hvN
|
||||
R6rHF6LcO05s8ubs9b9ON/ja7HCx69N5CjBuCbCFHUTlAXkwD9w0ScrxrtfP50EY
|
||||
FOw6LAqhhzq6/KO7c1SJ7k9LYzakhL+nbw5KM9QgBk4WHlmKLbCZIZ5RWvu0F4s5
|
||||
n4qk/BcuXIkbYuEv2kH0nDk5eDfA/dj7xZcMMgL5VFymQzaZLYyj4WuQYXu/7JW/
|
||||
nM/ZWBkZOMaI3vnPTG1hJ9pgjLjQnjfNA/bGWwbLxjCsPmR8yvZS4v2iqdB6X3Vl
|
||||
yJ9aV9r8KoU0PJk3x4v2Zp+RQxgrKSaQw59sXptaXAY3NCRR6ohvq4P5X6UB8r5S
|
||||
vYdoMeVXhX1hzXeMguln7zQInwJhPZqk4wMIV3lTsCqh1eJ7NC2TGCwble+B/ClR
|
||||
KtzuJBzoYPLw44ltnXPEMmVH1Fkh17+QZFgZRJrKGD9PGOAXmnzudsZ1xX9kNnOM
|
||||
JLIT/mzKcqkd8S1n1Xi1x7zGA0Ng5xmKGFe8oDokPJucJO1Ou+hbLDmC0DZUGzr1
|
||||
qqPJ3F/DzZZDTmD/rZF6doPJgFAZvgpVeiWS8/v1qbz/nz13uwXDLjRPgLfcKpmQ
|
||||
4R3V4QlgviDilW61VTZnzV9qAOx4fG6+IwWIGBlrJnfsH/fSCDNlAStc6k12zdun
|
||||
PIIRJBfbEprGig3vRWUoBASReqow1JCN9DaVCX1P27pDKY5oDe+7/HOrQpwhPoya
|
||||
2HEwbKeyY0nCcCXbkWGL1bwEUs/PrJv+61rik4KxOWhKpHWkZLzbozELb44jXrJx
|
||||
e8K8XKz4La2DEjsUYHc31u6T69GBQO9JDEvih15phUWq8ITvDnkHpAg+wYb1JAHD
|
||||
QcqDtAulMvT/ZGN0h7qdwbHMggEsLgCCVPG4iZ5K4cXsMbePFvQqq+o4FTMF+cM5
|
||||
2Dq0wir92U9cH+ooy80LIt5Kp5zqgQZzr73o9MEgwqJocCrx9ZrofKRUmTV+ZU0r
|
||||
w5mfUM47Ctnqia0UNGx6SUs3CHFDPWPbzrAaqGzSvFhzR1MMoL1/rJzP1VSm3Fk3
|
||||
ESWkPrg0J8dcQP/ch9MhH8eoQYyA+2q1vClUbeZLAs5KoHxgi6pSkGYqFhshrA+t
|
||||
2AIrUPDPPDf0PgRoXJrzdVOiNNY1rzyql+0JqDH6DjCVcAADWY+48p9U2YFTd7Je
|
||||
DvnZWihwe0qYGn1AKIkvJ4SR3bQg36etrxhMrMl/8lUn2dnT7GFrhjr9HwCpJwa7
|
||||
8tv150SrQXt3FXZCHb+RMUgoWZDeksDohPiGzXkPU6kaSviZVnRMslyU4ahWp6vC
|
||||
8tYUhb7K6N+is1hYkICNt6zLl2vBDuCDWmiIwopHtnH1kz8bYlp4/GBVaMIgZiCM
|
||||
gM/7+p4YCc++s2sJiQ9+BqPo0zKm3bbSP+fPpeWefQVte9Jx4S36YXU52HsJxBTN
|
||||
WUdHABC+aS2A45I12xMNzOJR6VfxnG6f3JLpt3MkUCEg+898vJGope+TJUhD+aJC
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
##############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Testing the current ZEO implementation is rather hard due to the
|
||||
# architecture, which mixes concerns, especially between application
|
||||
# and networking. Still, it's not as bad as it could be.
|
||||
|
||||
# The 2 most important classes in the architecture are ZEOStorage and
|
||||
# StorageServer. A ZEOStorage is created for each client connection.
|
||||
# The StorageServer maintains data shared or needed for coordination
|
||||
# among clients.
|
||||
|
||||
# The other important part of the architecture is connections.
|
||||
# Connections are used by ZEOStorages to send messages or return data
|
||||
# to clients.
|
||||
|
||||
# Here, we'll try to provide some testing infrastructure to isolate
|
||||
# servers from the network.
|
||||
|
||||
import ZEO.asyncio.tests
|
||||
import ZEO.StorageServer
|
||||
import ZODB.MappingStorage
|
||||
|
||||
class StorageServer(ZEO.StorageServer.StorageServer):
|
||||
|
||||
def __init__(self, addr='test_addr', storages=None, **kw):
|
||||
if storages is None:
|
||||
storages = {'1': ZODB.MappingStorage.MappingStorage()}
|
||||
ZEO.StorageServer.StorageServer.__init__(self, addr, storages, **kw)
|
||||
|
||||
def client(server, name='client'):
|
||||
zs = ZEO.StorageServer.ZEOStorage(server)
|
||||
protocol = ZEO.asyncio.tests.server_protocol(
|
||||
False, zs, protocol_version=b'Z5', addr='test-addr-%s' % name)
|
||||
zs.notify_connected(protocol)
|
||||
zs.register('1', 0)
|
||||
return zs
|
||||
@@ -0,0 +1,222 @@
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
from __future__ import print_function
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
##############################################################################
|
||||
usage="""Test speed of a ZODB storage
|
||||
|
||||
Options:
|
||||
|
||||
-d file The data file to use as input.
|
||||
The default is this script.
|
||||
|
||||
-n n The number of repititions
|
||||
|
||||
-s module A module that defines a 'Storage'
|
||||
attribute, which is an open storage.
|
||||
If not specified, a FileStorage will ne
|
||||
used.
|
||||
|
||||
-z Test compressing data
|
||||
|
||||
-D Run in debug mode
|
||||
|
||||
-L Test loads as well as stores by minimizing
|
||||
the cache after eachrun
|
||||
|
||||
-M Output means only
|
||||
|
||||
-C Run with a persistent client cache
|
||||
|
||||
-U Run ZEO using a Unix domain socket
|
||||
|
||||
-t n Number of concurrent threads to run.
|
||||
"""
|
||||
|
||||
import asyncore
|
||||
import sys, os, getopt, time
|
||||
##sys.path.insert(0, os.getcwd())
|
||||
|
||||
import persistent
|
||||
import transaction
|
||||
import ZODB
|
||||
from ZODB.POSException import ConflictError
|
||||
from ZEO.tests import forker
|
||||
|
||||
class P(persistent.Persistent):
|
||||
pass
|
||||
|
||||
fs_name = "zeo-speed.fs"
|
||||
|
||||
class ZEOExit(asyncore.file_dispatcher):
|
||||
"""Used to exit ZEO.StorageServer when run is done"""
|
||||
def writable(self):
|
||||
return 0
|
||||
def readable(self):
|
||||
return 1
|
||||
def handle_read(self):
|
||||
buf = self.recv(4)
|
||||
assert buf == "done"
|
||||
self.delete_fs()
|
||||
os._exit(0)
|
||||
def handle_close(self):
|
||||
print("Parent process exited unexpectedly")
|
||||
self.delete_fs()
|
||||
os._exit(0)
|
||||
def delete_fs(self):
|
||||
os.unlink(fs_name)
|
||||
os.unlink(fs_name + ".lock")
|
||||
os.unlink(fs_name + ".tmp")
|
||||
|
||||
def work(db, results, nrep, compress, data, detailed, minimize, threadno=None):
|
||||
for j in range(nrep):
|
||||
for r in 1, 10, 100, 1000:
|
||||
t = time.time()
|
||||
conflicts = 0
|
||||
|
||||
jar = db.open()
|
||||
while 1:
|
||||
try:
|
||||
transaction.begin()
|
||||
rt = jar.root()
|
||||
key = 's%s' % r
|
||||
if key in rt:
|
||||
p = rt[key]
|
||||
else:
|
||||
rt[key] = p =P()
|
||||
for i in range(r):
|
||||
v = getattr(p, str(i), P())
|
||||
if compress is not None:
|
||||
v.d = compress(data)
|
||||
else:
|
||||
v.d = data
|
||||
setattr(p, str(i), v)
|
||||
transaction.commit()
|
||||
except ConflictError:
|
||||
conflicts = conflicts + 1
|
||||
else:
|
||||
break
|
||||
jar.close()
|
||||
|
||||
t = time.time() - t
|
||||
if detailed:
|
||||
if threadno is None:
|
||||
print("%s\t%s\t%.4f\t%d" % (j, r, t, conflicts))
|
||||
else:
|
||||
print("%s\t%s\t%.4f\t%d\t%d" % (j, r, t, conflicts,
|
||||
threadno))
|
||||
results[r].append((t, conflicts))
|
||||
rt=d=p=v=None # release all references
|
||||
if minimize:
|
||||
time.sleep(3)
|
||||
jar.cacheMinimize()
|
||||
|
||||
def main(args):
|
||||
opts, args = getopt.getopt(args, 'zd:n:Ds:LMt:U')
|
||||
s = None
|
||||
compress = None
|
||||
data=sys.argv[0]
|
||||
nrep=5
|
||||
minimize=0
|
||||
detailed=1
|
||||
cache = None
|
||||
domain = 'AF_INET'
|
||||
threads = 1
|
||||
for o, v in opts:
|
||||
if o=='-n': nrep = int(v)
|
||||
elif o=='-d': data = v
|
||||
elif o=='-s': s = v
|
||||
elif o=='-z':
|
||||
import zlib
|
||||
compress = zlib.compress
|
||||
elif o=='-L':
|
||||
minimize=1
|
||||
elif o=='-M':
|
||||
detailed=0
|
||||
elif o=='-D':
|
||||
global debug
|
||||
os.environ['STUPID_LOG_FILE']=''
|
||||
os.environ['STUPID_LOG_SEVERITY']='-999'
|
||||
debug = 1
|
||||
elif o == '-C':
|
||||
cache = 'speed'
|
||||
elif o == '-U':
|
||||
domain = 'AF_UNIX'
|
||||
elif o == '-t':
|
||||
threads = int(v)
|
||||
|
||||
zeo_pipe = None
|
||||
if s:
|
||||
s = __import__(s, globals(), globals(), ('__doc__',))
|
||||
s = s.Storage
|
||||
server = None
|
||||
else:
|
||||
s, server, pid = forker.start_zeo("FileStorage",
|
||||
(fs_name, 1), domain=domain)
|
||||
|
||||
data=open(data).read()
|
||||
db=ZODB.DB(s,
|
||||
# disable cache deactivation
|
||||
cache_size=4000,
|
||||
cache_deactivate_after=6000,)
|
||||
|
||||
print("Beginning work...")
|
||||
results={1:[], 10:[], 100:[], 1000:[]}
|
||||
if threads > 1:
|
||||
import threading
|
||||
l = []
|
||||
for i in range(threads):
|
||||
t = threading.Thread(target=work,
|
||||
args=(db, results, nrep, compress, data,
|
||||
detailed, minimize, i))
|
||||
l.append(t)
|
||||
for t in l:
|
||||
t.start()
|
||||
for t in l:
|
||||
t.join()
|
||||
|
||||
else:
|
||||
work(db, results, nrep, compress, data, detailed, minimize)
|
||||
|
||||
if server is not None:
|
||||
server.close()
|
||||
os.waitpid(pid, 0)
|
||||
|
||||
if detailed:
|
||||
print('-'*24)
|
||||
print("num\tmean\tmin\tmax")
|
||||
for r in 1, 10, 100, 1000:
|
||||
times = []
|
||||
for time, conf in results[r]:
|
||||
times.append(time)
|
||||
t = mean(times)
|
||||
print("%d\t%.4f\t%.4f\t%.4f" % (r, t, min(times), max(times)))
|
||||
|
||||
def mean(l):
|
||||
tot = 0
|
||||
for v in l:
|
||||
tot = tot + v
|
||||
return tot / len(l)
|
||||
|
||||
##def compress(s):
|
||||
## c = zlib.compressobj()
|
||||
## o = c.compress(s)
|
||||
## return o + c.flush()
|
||||
|
||||
if __name__=='__main__':
|
||||
main(sys.argv[1:])
|
||||
@@ -0,0 +1,137 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 ZEO client-server stress test to look for leaks.
|
||||
|
||||
The stress test should run in an infinite loop and should involve
|
||||
multiple connections.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
# TODO: This code is currently broken.
|
||||
|
||||
import transaction
|
||||
import ZODB
|
||||
from ZODB.MappingStorage import MappingStorage
|
||||
from ZODB.tests import MinPO
|
||||
from ZEO.ClientStorage import ClientStorage
|
||||
from ZEO.tests import forker
|
||||
|
||||
import os
|
||||
import random
|
||||
|
||||
NUM_TRANSACTIONS_PER_CONN = 10
|
||||
NUM_CONNECTIONS = 10
|
||||
NUM_ROOTS = 20
|
||||
MAX_DEPTH = 20
|
||||
MIN_OBJSIZE = 128
|
||||
MAX_OBJSIZE = 2048
|
||||
|
||||
def an_object():
|
||||
"""Return an object suitable for a PersistentMapping key"""
|
||||
size = random.randrange(MIN_OBJSIZE, MAX_OBJSIZE)
|
||||
if os.path.exists("/dev/urandom"):
|
||||
f = open("/dev/urandom")
|
||||
buf = f.read(size)
|
||||
f.close()
|
||||
return buf
|
||||
else:
|
||||
f = open(MinPO.__file__)
|
||||
l = list(f.read(size))
|
||||
f.close()
|
||||
random.shuffle(l)
|
||||
return "".join(l)
|
||||
|
||||
def setup(cn):
|
||||
"""Initialize the database with some objects"""
|
||||
root = cn.root()
|
||||
for i in range(NUM_ROOTS):
|
||||
prev = an_object()
|
||||
for j in range(random.randrange(1, MAX_DEPTH)):
|
||||
o = MinPO.MinPO(prev)
|
||||
prev = o
|
||||
root[an_object()] = o
|
||||
transaction.commit()
|
||||
cn.close()
|
||||
|
||||
def work(cn):
|
||||
"""Do some work with a transaction"""
|
||||
cn.sync()
|
||||
root = cn.root()
|
||||
obj = random.choice(root.values())
|
||||
# walk down to the bottom
|
||||
while not isinstance(obj.value, str):
|
||||
obj = obj.value
|
||||
obj.value = an_object()
|
||||
transaction.commit()
|
||||
|
||||
def main():
|
||||
# Yuck! Need to cleanup forker so that the API is consistent
|
||||
# across Unix and Windows, at least if that's possible.
|
||||
if os.name == "nt":
|
||||
zaddr, tport, pid = forker.start_zeo_server('MappingStorage', ())
|
||||
def exitserver():
|
||||
import socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect(tport)
|
||||
s.close()
|
||||
else:
|
||||
zaddr = '', random.randrange(20000, 30000)
|
||||
pid, exitobj = forker.start_zeo_server(MappingStorage(), zaddr)
|
||||
def exitserver():
|
||||
exitobj.close()
|
||||
|
||||
while 1:
|
||||
pid = start_child(zaddr)
|
||||
print("started", pid)
|
||||
os.waitpid(pid, 0)
|
||||
|
||||
exitserver()
|
||||
|
||||
def start_child(zaddr):
|
||||
|
||||
pid = os.fork()
|
||||
if pid != 0:
|
||||
return pid
|
||||
try:
|
||||
_start_child(zaddr)
|
||||
finally:
|
||||
os._exit(0)
|
||||
|
||||
def _start_child(zaddr):
|
||||
storage = ClientStorage(zaddr, debug=1, min_disconnect_poll=0.5, wait=1)
|
||||
db = ZODB.DB(storage, pool_size=NUM_CONNECTIONS)
|
||||
setup(db.open())
|
||||
conns = []
|
||||
conn_count = 0
|
||||
|
||||
for i in range(NUM_CONNECTIONS):
|
||||
c = db.open()
|
||||
c.__count = 0
|
||||
conns.append(c)
|
||||
conn_count += 1
|
||||
|
||||
while conn_count < 25:
|
||||
c = random.choice(conns)
|
||||
if c.__count > NUM_TRANSACTIONS_PER_CONN:
|
||||
conns.remove(c)
|
||||
c.close()
|
||||
conn_count += 1
|
||||
c = db.open()
|
||||
c.__count = 0
|
||||
conns.append(c)
|
||||
else:
|
||||
c.__count += 1
|
||||
work(c)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,126 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
|
||||
from zope.testing import setupstack
|
||||
from ZODB.config import storageFromString
|
||||
|
||||
from .forker import start_zeo_server
|
||||
from .threaded import threaded_server_tests
|
||||
|
||||
class ZEOConfigTestBase(setupstack.TestCase):
|
||||
|
||||
setUp = setupstack.setUpDirectory
|
||||
|
||||
def start_server(self, settings='', **kw):
|
||||
|
||||
for name, value in kw.items():
|
||||
settings += '\n%s %s\n' % (name.replace('_', '-'), value)
|
||||
|
||||
zeo_conf = """
|
||||
<zeo>
|
||||
address 127.0.0.1:0
|
||||
%s
|
||||
</zeo>
|
||||
""" % settings
|
||||
return start_zeo_server("<mappingstorage>\n</mappingstorage>\n",
|
||||
zeo_conf, threaded=True)
|
||||
|
||||
def start_client(self, addr, settings='', **kw):
|
||||
settings += '\nserver %s:%s\n' % addr
|
||||
for name, value in kw.items():
|
||||
settings += '\n%s %s\n' % (name.replace('_', '-'), value)
|
||||
return storageFromString(
|
||||
"""
|
||||
%import ZEO
|
||||
|
||||
<clientstorage>
|
||||
{}
|
||||
</clientstorage>
|
||||
""".format(settings))
|
||||
|
||||
def _client_assertions(
|
||||
self, client, addr,
|
||||
connected=True,
|
||||
cache_size=20 * (1<<20),
|
||||
cache_path=None,
|
||||
blob_dir=None,
|
||||
shared_blob_dir=False,
|
||||
blob_cache_size=None,
|
||||
blob_cache_size_check=10,
|
||||
read_only=False,
|
||||
read_only_fallback=False,
|
||||
server_sync=False,
|
||||
wait_timeout=30,
|
||||
client_label=None,
|
||||
storage='1',
|
||||
name=None,
|
||||
):
|
||||
self.assertEqual(client.is_connected(), connected)
|
||||
self.assertEqual(client._addr, [addr])
|
||||
self.assertEqual(client._cache.maxsize, cache_size)
|
||||
|
||||
self.assertEqual(client._cache.path, cache_path)
|
||||
self.assertEqual(client.blob_dir, blob_dir)
|
||||
self.assertEqual(client.shared_blob_dir, shared_blob_dir)
|
||||
self.assertEqual(client._blob_cache_size, blob_cache_size)
|
||||
if blob_cache_size:
|
||||
self.assertEqual(client._blob_cache_size_check,
|
||||
blob_cache_size * blob_cache_size_check // 100)
|
||||
self.assertEqual(client._is_read_only, read_only)
|
||||
self.assertEqual(client._read_only_fallback, read_only_fallback)
|
||||
self.assertEqual(client._server.timeout, wait_timeout)
|
||||
self.assertEqual(client._client_label, client_label)
|
||||
self.assertEqual(client._storage, storage)
|
||||
self.assertEqual(client.__name__,
|
||||
name if name is not None else str(client._addr))
|
||||
|
||||
class ZEOConfigTest(ZEOConfigTestBase):
|
||||
|
||||
def test_default_zeo_config(self, **client_settings):
|
||||
addr, stop = self.start_server()
|
||||
|
||||
client = self.start_client(addr, **client_settings)
|
||||
self._client_assertions(client, addr, **client_settings)
|
||||
|
||||
client.close()
|
||||
stop()
|
||||
|
||||
def test_client_variations(self):
|
||||
|
||||
for name, value in dict(
|
||||
cache_size=4200,
|
||||
cache_path='test',
|
||||
blob_dir='blobs',
|
||||
blob_cache_size=424242,
|
||||
read_only=True,
|
||||
read_only_fallback=True,
|
||||
server_sync=True,
|
||||
wait_timeout=33,
|
||||
client_label='test_client',
|
||||
name='Test'
|
||||
).items():
|
||||
params = {name: value}
|
||||
self.test_default_zeo_config(**params)
|
||||
|
||||
def test_blob_cache_size_check(self):
|
||||
self.test_default_zeo_config(blob_cache_size=424242,
|
||||
blob_cache_size_check=50)
|
||||
|
||||
def test_suite():
|
||||
suite = unittest.makeSuite(ZEOConfigTest)
|
||||
suite.layer = threaded_server_tests
|
||||
return suite
|
||||
@@ -0,0 +1,246 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 setup for ZEO connection logic.
|
||||
|
||||
The actual tests are in ConnectionTests.py; this file provides the
|
||||
platform-dependent scaffolding.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement, print_function
|
||||
|
||||
from ZEO.tests import ConnectionTests, InvalidationTests
|
||||
from zope.testing import setupstack
|
||||
import os
|
||||
if os.environ.get('USE_ZOPE_TESTING_DOCTEST'):
|
||||
from zope.testing import doctest
|
||||
else:
|
||||
import doctest
|
||||
import unittest
|
||||
import ZODB.tests.util
|
||||
|
||||
import ZEO
|
||||
|
||||
from . import forker
|
||||
|
||||
class FileStorageConfig(object):
|
||||
def getConfig(self, path, create, read_only):
|
||||
return """\
|
||||
<filestorage 1>
|
||||
path %s
|
||||
create %s
|
||||
read-only %s
|
||||
</filestorage>""" % (path,
|
||||
create and 'yes' or 'no',
|
||||
read_only and 'yes' or 'no')
|
||||
|
||||
class MappingStorageConfig(object):
|
||||
def getConfig(self, path, create, read_only):
|
||||
return """<mappingstorage 1/>"""
|
||||
|
||||
|
||||
class FileStorageConnectionTests(
|
||||
FileStorageConfig,
|
||||
ConnectionTests.ConnectionTests,
|
||||
InvalidationTests.InvalidationTests
|
||||
):
|
||||
"""FileStorage-specific connection tests."""
|
||||
|
||||
class FileStorageReconnectionTests(
|
||||
FileStorageConfig,
|
||||
ConnectionTests.ReconnectionTests,
|
||||
):
|
||||
"""FileStorage-specific re-connection tests."""
|
||||
# Run this at level 1 because MappingStorage can't do reconnection tests
|
||||
|
||||
class FileStorageInvqTests(
|
||||
FileStorageConfig,
|
||||
ConnectionTests.InvqTests
|
||||
):
|
||||
"""FileStorage-specific invalidation queue tests."""
|
||||
|
||||
class FileStorageTimeoutTests(
|
||||
FileStorageConfig,
|
||||
ConnectionTests.TimeoutTests
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class MappingStorageConnectionTests(
|
||||
MappingStorageConfig,
|
||||
ConnectionTests.ConnectionTests
|
||||
):
|
||||
"""Mapping storage connection tests."""
|
||||
|
||||
# The ReconnectionTests can't work with MappingStorage because it's only an
|
||||
# in-memory storage and has no persistent state.
|
||||
|
||||
class MappingStorageTimeoutTests(
|
||||
MappingStorageConfig,
|
||||
ConnectionTests.TimeoutTests
|
||||
):
|
||||
pass
|
||||
|
||||
class SSLConnectionTests(
|
||||
MappingStorageConfig,
|
||||
ConnectionTests.SSLConnectionTests,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
test_classes = [FileStorageConnectionTests,
|
||||
FileStorageReconnectionTests,
|
||||
FileStorageInvqTests,
|
||||
FileStorageTimeoutTests,
|
||||
MappingStorageConnectionTests,
|
||||
MappingStorageTimeoutTests,
|
||||
]
|
||||
if not forker.ZEO4_SERVER:
|
||||
test_classes.append(SSLConnectionTests)
|
||||
|
||||
def invalidations_while_connecting():
|
||||
r"""
|
||||
As soon as a client registers with a server, it will recieve
|
||||
invalidations from the server. The client must be careful to queue
|
||||
these invalidations until it is ready to deal with them. At the time
|
||||
of the writing of this test, clients weren't careful enough about
|
||||
queing invalidations. This led to cache corruption in the form of
|
||||
both low-level file corruption as well as out-of-date records marked
|
||||
as current.
|
||||
|
||||
This tests tries to provoke this bug by:
|
||||
|
||||
- starting a server
|
||||
|
||||
>>> addr, _ = start_server()
|
||||
|
||||
- opening a client to the server that writes some objects, filling
|
||||
it's cache at the same time,
|
||||
|
||||
>>> import ZEO, ZODB.tests.MinPO, transaction
|
||||
>>> db = ZEO.DB(addr, client='x')
|
||||
>>> conn = db.open()
|
||||
>>> nobs = 1000
|
||||
>>> for i in range(nobs):
|
||||
... conn.root()[i] = ZODB.tests.MinPO.MinPO(0)
|
||||
>>> transaction.commit()
|
||||
|
||||
>>> import zope.testing.loggingsupport, logging
|
||||
>>> handler = zope.testing.loggingsupport.InstalledHandler(
|
||||
... 'ZEO', level=logging.INFO)
|
||||
|
||||
# >>> logging.getLogger('ZEO').debug(
|
||||
# ... 'Initial tid %r' % conn.root()._p_serial)
|
||||
|
||||
- disconnecting the first client (closing it with a persistent cache),
|
||||
|
||||
>>> db.close()
|
||||
|
||||
- starting a second client that writes objects more or less
|
||||
constantly,
|
||||
|
||||
>>> import random, threading, time
|
||||
>>> stop = False
|
||||
>>> db2 = ZEO.DB(addr)
|
||||
>>> tm = transaction.TransactionManager()
|
||||
>>> conn2 = db2.open(transaction_manager=tm)
|
||||
>>> random = random.Random(0)
|
||||
>>> lock = threading.Lock()
|
||||
>>> def run():
|
||||
... while 1:
|
||||
... i = random.randint(0, nobs-1)
|
||||
... if stop:
|
||||
... return
|
||||
... with lock:
|
||||
... conn2.root()[i].value += 1
|
||||
... tm.commit()
|
||||
... #logging.getLogger('ZEO').debug(
|
||||
... # 'COMMIT %s %s %r' % (
|
||||
... # i, conn2.root()[i].value, conn2.root()[i]._p_serial))
|
||||
... time.sleep(0)
|
||||
>>> thread = threading.Thread(target=run)
|
||||
>>> thread.setDaemon(True)
|
||||
>>> thread.start()
|
||||
|
||||
- restarting the first client, and
|
||||
- testing for cache validity.
|
||||
|
||||
>>> bad = False
|
||||
>>> try:
|
||||
... for c in range(10):
|
||||
... time.sleep(.1)
|
||||
... db = ZODB.DB(ZEO.ClientStorage.ClientStorage(addr, client='x'))
|
||||
... with lock:
|
||||
... #logging.getLogger('ZEO').debug('Locked %s' % c)
|
||||
... @wait_until("connected and we have caught up", timeout=199)
|
||||
... def _():
|
||||
... if (db.storage.is_connected()
|
||||
... and db.storage.lastTransaction()
|
||||
... == db.storage._call('lastTransaction')
|
||||
... ):
|
||||
... #logging.getLogger('ZEO').debug(
|
||||
... # 'Connected %r' % db.storage.lastTransaction())
|
||||
... return True
|
||||
...
|
||||
... conn = db.open()
|
||||
... for i in range(1000):
|
||||
... if conn.root()[i].value != conn2.root()[i].value:
|
||||
... print('bad', c, i, conn.root()[i].value, end=" ")
|
||||
... print(conn2.root()[i].value)
|
||||
... bad = True
|
||||
... print('client debug log with lock held')
|
||||
... while handler.records:
|
||||
... record = handler.records.pop(0)
|
||||
... print(record.name, record.levelname, end=' ')
|
||||
... print(handler.format(record))
|
||||
... #if bad:
|
||||
... # with open('server.log') as f:
|
||||
... # print(f.read())
|
||||
... #else:
|
||||
... # logging.getLogger('ZEO').debug('GOOD %s' % c)
|
||||
... db.close()
|
||||
... finally:
|
||||
... stop = True
|
||||
... thread.join(10)
|
||||
|
||||
>>> thread.isAlive()
|
||||
False
|
||||
|
||||
>>> for record in handler.records:
|
||||
... if record.levelno < logging.ERROR:
|
||||
... continue
|
||||
... print(record.name, record.levelname)
|
||||
... print(handler.format(record))
|
||||
|
||||
>>> handler.uninstall()
|
||||
|
||||
>>> db.close()
|
||||
>>> db2.close()
|
||||
"""
|
||||
|
||||
def test_suite():
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
for klass in test_classes:
|
||||
sub = unittest.makeSuite(klass, 'check')
|
||||
sub.layer = ZODB.tests.util.MininalTestLayer(
|
||||
klass.__name__ + ' ZEO Connection Tests')
|
||||
suite.addTest(sub)
|
||||
|
||||
sub = doctest.DocTestSuite(
|
||||
setUp=forker.setUp, tearDown=setupstack.tearDown,
|
||||
)
|
||||
sub.layer = ZODB.tests.util.MininalTestLayer('ZEO Connection DocTests')
|
||||
suite.addTest(sub)
|
||||
|
||||
return suite
|
||||
@@ -0,0 +1,150 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2006 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 unittest
|
||||
|
||||
import ZEO.asyncio.testing
|
||||
|
||||
class FakeStorageBase(object):
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in ('getTid', 'history', 'load', 'loadSerial',
|
||||
'lastTransaction', 'getSize', 'getName', 'supportsUndo',
|
||||
'tpc_transaction'):
|
||||
return lambda *a, **k: None
|
||||
raise AttributeError(name)
|
||||
|
||||
def isReadOnly(self):
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
return 4
|
||||
|
||||
class FakeStorage(FakeStorageBase):
|
||||
|
||||
def record_iternext(self, next=None):
|
||||
if next == None:
|
||||
next = '0'
|
||||
next = str(int(next) + 1)
|
||||
oid = next
|
||||
if next == '4':
|
||||
next = None
|
||||
|
||||
return oid, oid*8, 'data ' + oid, next
|
||||
|
||||
class FakeServer(object):
|
||||
storages = {
|
||||
'1': FakeStorage(),
|
||||
'2': FakeStorageBase(),
|
||||
}
|
||||
lock_managers = storages
|
||||
|
||||
def register_connection(*args):
|
||||
return None, None
|
||||
|
||||
client_conflict_resolution = False
|
||||
|
||||
class FakeConnection(object):
|
||||
protocol_version = b'Z4'
|
||||
addr = 'test'
|
||||
|
||||
call_soon_threadsafe = lambda f, *a: f(*a)
|
||||
async_ = async_threadsafe = None
|
||||
|
||||
def test_server_record_iternext():
|
||||
"""
|
||||
|
||||
On the server, record_iternext calls are simply delegated to the
|
||||
underlying storage.
|
||||
|
||||
>>> import ZEO.StorageServer
|
||||
|
||||
>>> zeo = ZEO.StorageServer.ZEOStorage(FakeServer(), False)
|
||||
>>> zeo.notify_connected(FakeConnection())
|
||||
>>> zeo.register('1', False)
|
||||
|
||||
>>> next = None
|
||||
>>> while 1:
|
||||
... oid, serial, data, next = zeo.record_iternext(next)
|
||||
... print(oid)
|
||||
... if next is None:
|
||||
... break
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
|
||||
The storage info also reflects the fact that record_iternext is supported.
|
||||
|
||||
>>> zeo.get_info()['supports_record_iternext']
|
||||
True
|
||||
|
||||
>>> zeo = ZEO.StorageServer.ZEOStorage(FakeServer(), False)
|
||||
>>> zeo.notify_connected(FakeConnection())
|
||||
>>> zeo.register('2', False)
|
||||
|
||||
>>> zeo.get_info()['supports_record_iternext']
|
||||
False
|
||||
|
||||
"""
|
||||
|
||||
def test_client_record_iternext():
|
||||
"""Test client storage delegation to the network client
|
||||
|
||||
The client simply delegates record_iternext calls to it's server stub.
|
||||
|
||||
There's really no decent way to test ZEO without running too much crazy
|
||||
stuff. I'd rather do a lame test than a really lame test, so here goes.
|
||||
|
||||
First, fake out the connection manager so we can make a connection:
|
||||
|
||||
>>> import ZEO
|
||||
|
||||
>>> class Client(ZEO.asyncio.testing.ClientRunner):
|
||||
...
|
||||
... def record_iternext(self, next=None):
|
||||
... if next == None:
|
||||
... next = '0'
|
||||
... next = str(int(next) + 1)
|
||||
... oid = next
|
||||
... if next == '4':
|
||||
... next = None
|
||||
...
|
||||
... return oid, oid*8, 'data ' + oid, next
|
||||
...
|
||||
|
||||
>>> client = ZEO.client(
|
||||
... '', wait=False, _client_factory=Client)
|
||||
|
||||
Now we'll have our way with it's private _server attr:
|
||||
|
||||
>>> next = None
|
||||
>>> while 1:
|
||||
... oid, serial, data, next = client.record_iternext(next)
|
||||
... print(oid)
|
||||
... if next is None:
|
||||
... break
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
>>> client.close()
|
||||
|
||||
"""
|
||||
|
||||
def test_suite():
|
||||
return doctest.DocTestSuite()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -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
|
||||
#
|
||||
##############################################################################
|
||||
import random
|
||||
import unittest
|
||||
|
||||
from ZEO.TransactionBuffer import TransactionBuffer
|
||||
|
||||
def random_string(size):
|
||||
"""Return a random string of size size."""
|
||||
l = [chr(random.randrange(256)) for i in range(size)]
|
||||
return "".join(l)
|
||||
|
||||
def new_store_data():
|
||||
"""Return arbitrary data to use as argument to store() method."""
|
||||
return random_string(8), random_string(random.randrange(1000))
|
||||
|
||||
def store(tbuf, resolved=False):
|
||||
data = new_store_data()
|
||||
tbuf.store(*data)
|
||||
if resolved:
|
||||
tbuf.server_resolve(data[0])
|
||||
return data
|
||||
|
||||
class TransBufTests(unittest.TestCase):
|
||||
|
||||
def checkTypicalUsage(self):
|
||||
tbuf = TransactionBuffer(0)
|
||||
store(tbuf)
|
||||
store(tbuf)
|
||||
for o in tbuf:
|
||||
pass
|
||||
tbuf.close()
|
||||
|
||||
def checkOrderPreserved(self):
|
||||
tbuf = TransactionBuffer(0)
|
||||
data = []
|
||||
for i in range(10):
|
||||
data.append((store(tbuf), False))
|
||||
data.append((store(tbuf, True), True))
|
||||
|
||||
for i, (oid, d, resolved) in enumerate(tbuf):
|
||||
self.assertEqual((oid, d), data[i][0])
|
||||
self.assertEqual(resolved, data[i][1])
|
||||
tbuf.close()
|
||||
|
||||
def test_suite():
|
||||
return unittest.makeSuite(TransBufTests, 'check')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,513 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
##############################################################################
|
||||
from __future__ import print_function
|
||||
from zope.testing import setupstack, renormalizing
|
||||
import doctest
|
||||
import logging
|
||||
import pprint
|
||||
import re
|
||||
import sys
|
||||
import transaction
|
||||
import unittest
|
||||
import ZEO.StorageServer
|
||||
import ZEO.tests.servertesting
|
||||
import ZODB.blob
|
||||
import ZODB.FileStorage
|
||||
import ZODB.tests.util
|
||||
import ZODB.utils
|
||||
|
||||
def proper_handling_of_blob_conflicts():
|
||||
r"""
|
||||
|
||||
Conflict errors weren't properly handled when storing blobs, the
|
||||
result being that the storage was left in a transaction.
|
||||
|
||||
We originally saw this when restarting a blob transaction, although
|
||||
it doesn't really matter.
|
||||
|
||||
Set up the storage with some initial blob data.
|
||||
|
||||
>>> fs = ZODB.FileStorage.FileStorage('t.fs', blob_dir='t.blobs')
|
||||
>>> db = ZODB.DB(fs)
|
||||
>>> conn = db.open()
|
||||
>>> conn.root.b = ZODB.blob.Blob(b'x')
|
||||
>>> transaction.commit()
|
||||
|
||||
Get the oid and first serial. We'll use the serial later to provide
|
||||
out-of-date data.
|
||||
|
||||
>>> oid = conn.root.b._p_oid
|
||||
>>> serial = conn.root.b._p_serial
|
||||
>>> with conn.root.b.open('w') as file:
|
||||
... _ = file.write(b'y')
|
||||
>>> transaction.commit()
|
||||
>>> data = fs.load(oid)[0]
|
||||
|
||||
Create the server:
|
||||
|
||||
>>> server = ZEO.tests.servertesting.StorageServer('x', {'1': fs})
|
||||
|
||||
And an initial client.
|
||||
|
||||
>>> zs1 = ZEO.tests.servertesting.client(server, 1)
|
||||
>>> zs1.tpc_begin('0', '', '', {})
|
||||
>>> zs1.storea(ZODB.utils.p64(99), ZODB.utils.z64, b'x', '0')
|
||||
>>> _ = zs1.vote('0') # doctest: +ELLIPSIS
|
||||
|
||||
In a second client, we'll try to commit using the old serial. This
|
||||
will conflict. It will be blocked at the vote call.
|
||||
|
||||
>>> zs2 = ZEO.tests.servertesting.client(server, 2)
|
||||
>>> zs2.tpc_begin('1', '', '', {})
|
||||
>>> zs2.storeBlobStart()
|
||||
>>> zs2.storeBlobChunk(b'z')
|
||||
>>> zs2.storeBlobEnd(oid, serial, data, '1')
|
||||
>>> delay = zs2.vote('1')
|
||||
|
||||
>>> class Sender(object):
|
||||
... def send_reply(self, id, reply):
|
||||
... print('reply', id, reply)
|
||||
... def send_error(self, id, err):
|
||||
... print('error', id, err)
|
||||
>>> delay.set_sender(1, Sender())
|
||||
|
||||
>>> logger = logging.getLogger('ZEO')
|
||||
>>> handler = logging.StreamHandler(sys.stdout)
|
||||
>>> logger.setLevel(logging.INFO)
|
||||
>>> logger.addHandler(handler)
|
||||
|
||||
Now, when we abort the transaction for the first client. The second
|
||||
client will be restarted. It will get a conflict error, that is
|
||||
raised to the client:
|
||||
|
||||
>>> zs1.tpc_abort('0') # doctest: +ELLIPSIS
|
||||
Error raised in delayed method
|
||||
Traceback (most recent call last):
|
||||
...ConflictError: ...
|
||||
error 1 database conflict error ...
|
||||
|
||||
The transaction is aborted by the server:
|
||||
|
||||
>>> fs.tpc_transaction() is None
|
||||
True
|
||||
|
||||
>>> zs2.connected
|
||||
True
|
||||
|
||||
>>> logger.setLevel(logging.NOTSET)
|
||||
>>> logger.removeHandler(handler)
|
||||
>>> zs2.tpc_abort('1')
|
||||
>>> fs.close()
|
||||
"""
|
||||
|
||||
def proper_handling_of_errors_in_restart():
|
||||
r"""
|
||||
|
||||
It's critical that if there is an error in vote that the
|
||||
storage isn't left in tpc.
|
||||
|
||||
>>> fs = ZODB.FileStorage.FileStorage('t.fs', blob_dir='t.blobs')
|
||||
>>> server = ZEO.tests.servertesting.StorageServer('x', {'1': fs})
|
||||
|
||||
And an initial client.
|
||||
|
||||
>>> zs1 = ZEO.tests.servertesting.client(server, 1)
|
||||
>>> zs1.tpc_begin('0', '', '', {})
|
||||
>>> zs1.storea(ZODB.utils.p64(99), ZODB.utils.z64, b'x', '0')
|
||||
|
||||
Intentionally break zs1:
|
||||
|
||||
>>> zs1._store = lambda : None
|
||||
>>> _ = zs1.vote('0') # doctest: +ELLIPSIS +IGNORE_EXCEPTION_DETAIL
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: <lambda>() takes no arguments (3 given)
|
||||
|
||||
We're not in a transaction:
|
||||
|
||||
>>> fs.tpc_transaction() is None
|
||||
True
|
||||
|
||||
We can start another client and get the storage lock.
|
||||
|
||||
>>> zs1 = ZEO.tests.servertesting.client(server, 1)
|
||||
>>> zs1.tpc_begin('1', '', '', {})
|
||||
>>> zs1.storea(ZODB.utils.p64(99), ZODB.utils.z64, b'x', '1')
|
||||
>>> _ = zs1.vote('1') # doctest: +ELLIPSIS
|
||||
|
||||
>>> zs1.tpc_finish('1').set_sender(0, zs1.connection)
|
||||
|
||||
>>> fs.close()
|
||||
>>> server.close()
|
||||
"""
|
||||
|
||||
def errors_in_vote_should_clear_lock():
|
||||
"""
|
||||
|
||||
So, we arrange to get an error in vote:
|
||||
|
||||
>>> import ZODB.MappingStorage
|
||||
>>> vote_should_fail = True
|
||||
>>> class MappingStorage(ZODB.MappingStorage.MappingStorage):
|
||||
... def tpc_vote(*args):
|
||||
... if vote_should_fail:
|
||||
... raise ValueError
|
||||
... return ZODB.MappingStorage.MappingStorage.tpc_vote(*args)
|
||||
|
||||
>>> server = ZEO.tests.servertesting.StorageServer(
|
||||
... 'x', {'1': MappingStorage()})
|
||||
>>> zs = ZEO.tests.servertesting.client(server, 1)
|
||||
>>> zs.tpc_begin('0', '', '', {})
|
||||
>>> zs.storea(ZODB.utils.p64(99), ZODB.utils.z64, 'x', '0')
|
||||
|
||||
>>> zs.vote('0')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError
|
||||
|
||||
When we do, the storage server's transaction lock shouldn't be held:
|
||||
|
||||
>>> zs.lock_manager.locked is not None
|
||||
False
|
||||
|
||||
Of course, if vote suceeds, the lock will be held:
|
||||
|
||||
>>> vote_should_fail = False
|
||||
>>> zs.tpc_begin('1', '', '', {})
|
||||
>>> zs.storea(ZODB.utils.p64(99), ZODB.utils.z64, 'x', '1')
|
||||
>>> _ = zs.vote('1') # doctest: +ELLIPSIS
|
||||
|
||||
>>> zs.lock_manager.locked is not None
|
||||
True
|
||||
|
||||
>>> zs.tpc_abort('1')
|
||||
"""
|
||||
|
||||
|
||||
def some_basic_locking_tests():
|
||||
r"""
|
||||
|
||||
>>> itid = 0
|
||||
>>> def start_trans(zs):
|
||||
... global itid
|
||||
... itid += 1
|
||||
... tid = str(itid)
|
||||
... zs.tpc_begin(tid, '', '', {})
|
||||
... zs.storea(ZODB.utils.p64(99), ZODB.utils.z64, 'x', tid)
|
||||
... return tid
|
||||
|
||||
>>> server = ZEO.tests.servertesting.StorageServer()
|
||||
|
||||
>>> handler = logging.StreamHandler(sys.stdout)
|
||||
>>> handler.setFormatter(logging.Formatter(
|
||||
... '%(name)s %(levelname)s\n%(message)s'))
|
||||
>>> logging.getLogger('ZEO').addHandler(handler)
|
||||
>>> logging.getLogger('ZEO').setLevel(logging.DEBUG)
|
||||
|
||||
Work around the fact that ZODB registers level names backwards, which
|
||||
quit working in Python 3.4:
|
||||
|
||||
>>> import logging
|
||||
>>> from ZODB.loglevels import BLATHER
|
||||
>>> logging.addLevelName(BLATHER, "BLATHER")
|
||||
|
||||
We start a transaction and vote, this leads to getting the lock.
|
||||
|
||||
>>> zs1 = ZEO.tests.servertesting.client(server, '1')
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
>>> tid1 = start_trans(zs1)
|
||||
>>> resolved1 = zs1.vote(tid1) # doctest: +ELLIPSIS
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-1) ('1') lock: transactions waiting: 0
|
||||
ZEO.StorageServer BLATHER
|
||||
(test-addr-1) Preparing to commit transaction: 1 objects, ... bytes
|
||||
|
||||
If another client tried to vote, it's lock request will be queued and
|
||||
a delay will be returned:
|
||||
|
||||
>>> zs2 = ZEO.tests.servertesting.client(server, '2')
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
>>> tid2 = start_trans(zs2)
|
||||
>>> delay = zs2.vote(tid2)
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-2) ('1') queue lock: transactions waiting: 1
|
||||
|
||||
>>> delay.set_sender(0, zs2.connection)
|
||||
|
||||
When we end the first transaction, the queued vote gets the lock.
|
||||
|
||||
>>> zs1.tpc_abort(tid1) # doctest: +ELLIPSIS
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-1) ('1') unlock: transactions waiting: 1
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-2) ('1') lock: transactions waiting: 0
|
||||
ZEO.StorageServer BLATHER
|
||||
(test-addr-2) Preparing to commit transaction: 1 objects, ... bytes
|
||||
|
||||
Let's try again with the first client. The vote will be queued:
|
||||
|
||||
>>> tid1 = start_trans(zs1)
|
||||
>>> delay = zs1.vote(tid1)
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-1) ('1') queue lock: transactions waiting: 1
|
||||
|
||||
If the queued transaction is aborted, it will be dequeued:
|
||||
|
||||
>>> zs1.tpc_abort(tid1) # doctest: +ELLIPSIS
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-1) ('1') dequeue lock: transactions waiting: 0
|
||||
|
||||
BTW, voting multiple times will error:
|
||||
|
||||
>>> zs2.vote(tid2)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
StorageTransactionError: Already voting (locked)
|
||||
|
||||
>>> tid1 = start_trans(zs1)
|
||||
>>> delay = zs1.vote(tid1)
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-1) ('1') queue lock: transactions waiting: 1
|
||||
|
||||
>>> delay.set_sender(0, zs1.connection)
|
||||
|
||||
>>> zs1.vote(tid1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
StorageTransactionError: Already voting (waiting)
|
||||
|
||||
Note that the locking activity is logged at debug level to avoid
|
||||
cluttering log files, however, as the number of waiting votes
|
||||
increased, so does the logging level:
|
||||
|
||||
>>> clients = []
|
||||
>>> for i in range(9):
|
||||
... client = ZEO.tests.servertesting.client(server, str(i+10))
|
||||
... tid = start_trans(client)
|
||||
... delay = client.vote(tid)
|
||||
... clients.append(client)
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-10) ('1') queue lock: transactions waiting: 2
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-11) ('1') queue lock: transactions waiting: 3
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
ZEO.StorageServer WARNING
|
||||
(test-addr-12) ('1') queue lock: transactions waiting: 4
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
ZEO.StorageServer WARNING
|
||||
(test-addr-13) ('1') queue lock: transactions waiting: 5
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
ZEO.StorageServer WARNING
|
||||
(test-addr-14) ('1') queue lock: transactions waiting: 6
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
ZEO.StorageServer WARNING
|
||||
(test-addr-15) ('1') queue lock: transactions waiting: 7
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
ZEO.StorageServer WARNING
|
||||
(test-addr-16) ('1') queue lock: transactions waiting: 8
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
ZEO.StorageServer WARNING
|
||||
(test-addr-17) ('1') queue lock: transactions waiting: 9
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
ZEO.StorageServer CRITICAL
|
||||
(test-addr-18) ('1') queue lock: transactions waiting: 10
|
||||
|
||||
If a client with the transaction lock disconnects, it will abort and
|
||||
release the lock and one of the waiting clients will get the lock.
|
||||
|
||||
>>> zs2.notify_disconnected() # doctest: +ELLIPSIS
|
||||
ZEO.StorageServer INFO
|
||||
(test-addr-...) disconnected during locked transaction
|
||||
ZEO.StorageServer CRITICAL
|
||||
(test-addr-...) ('1') unlock: transactions waiting: 10
|
||||
ZEO.StorageServer WARNING
|
||||
(test-addr-...) ('1') lock: transactions waiting: 9
|
||||
ZEO.StorageServer BLATHER
|
||||
(test-addr-...) Preparing to commit transaction: 1 objects, ... bytes
|
||||
|
||||
(In practice, waiting clients won't necessarily get the lock in order.)
|
||||
|
||||
We can find out about the current lock state, and get other server
|
||||
statistics using the server_status method:
|
||||
|
||||
>>> pprint.pprint(zs1.server_status(), width=40)
|
||||
{'aborts': 3,
|
||||
'active_txns': 10,
|
||||
'commits': 0,
|
||||
'conflicts': 0,
|
||||
'conflicts_resolved': 0,
|
||||
'connections': 10,
|
||||
'last-transaction': '0000000000000000',
|
||||
'loads': 0,
|
||||
'lock_time': 1272653598.693882,
|
||||
'start': 'Fri Apr 30 14:53:18 2010',
|
||||
'stores': 13,
|
||||
'timeout-thread-is-alive': 'stub',
|
||||
'waiting': 9}
|
||||
|
||||
If clients disconnect while waiting, they will be dequeued:
|
||||
|
||||
>>> for client in clients:
|
||||
... client.notify_disconnected() # doctest: +ELLIPSIS
|
||||
ZEO.StorageServer INFO
|
||||
(test-addr-10) disconnected during...locked transaction
|
||||
ZEO.StorageServer WARNING
|
||||
(test-addr-10) ('1') ... lock: transactions waiting: ...
|
||||
|
||||
>>> zs1.server_status()['waiting']
|
||||
0
|
||||
|
||||
>>> zs1.tpc_abort(tid1)
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-1) ('1') unlock: transactions waiting: 0
|
||||
|
||||
>>> logging.getLogger('ZEO').setLevel(logging.NOTSET)
|
||||
>>> logging.getLogger('ZEO').removeHandler(handler)
|
||||
>>> server.close()
|
||||
"""
|
||||
|
||||
def lock_sanity_check():
|
||||
r"""
|
||||
On one occasion with 3.10.0a1 in production, we had a case where a
|
||||
transaction lock wasn't released properly. One possibility, fron
|
||||
scant log information, is that the server and ZEOStorage had different
|
||||
ideas about whether the ZEOStorage was locked. The timeout thread
|
||||
properly closed the ZEOStorage's connection, but the ZEOStorage didn't
|
||||
release it's lock, presumably because it thought it wasn't locked. I'm
|
||||
not sure why this happened. I've refactored the logic quite a bit to
|
||||
try to deal with this, but the consequences of this failure are so
|
||||
severe, I'm adding some sanity checking when queueing lock requests.
|
||||
|
||||
Helper to manage transactions:
|
||||
|
||||
>>> itid = 0
|
||||
>>> def start_trans(zs):
|
||||
... global itid
|
||||
... itid += 1
|
||||
... tid = str(itid)
|
||||
... zs.tpc_begin(tid, '', '', {})
|
||||
... zs.storea(ZODB.utils.p64(99), ZODB.utils.z64, 'x', tid)
|
||||
... return tid
|
||||
|
||||
Set up server and logging:
|
||||
|
||||
>>> server = ZEO.tests.servertesting.StorageServer()
|
||||
|
||||
>>> handler = logging.StreamHandler(sys.stdout)
|
||||
>>> handler.setFormatter(logging.Formatter(
|
||||
... '%(name)s %(levelname)s\n%(message)s'))
|
||||
>>> logging.getLogger('ZEO').addHandler(handler)
|
||||
>>> logging.getLogger('ZEO').setLevel(logging.DEBUG)
|
||||
|
||||
Work around the fact that ZODB registers level names backwards, which
|
||||
quit working in Python 3.4:
|
||||
|
||||
>>> import logging
|
||||
>>> from ZODB.loglevels import BLATHER
|
||||
>>> logging.addLevelName(BLATHER, "BLATHER")
|
||||
|
||||
Now, we'll start a transaction, get the lock and then mark the
|
||||
ZEOStorage as closed and see if trying to get a lock cleans it up:
|
||||
|
||||
>>> zs1 = ZEO.tests.servertesting.client(server, '1')
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
>>> tid1 = start_trans(zs1)
|
||||
>>> resolved1 = zs1.vote(tid1) # doctest: +ELLIPSIS
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-1) ('1') lock: transactions waiting: 0
|
||||
ZEO.StorageServer BLATHER
|
||||
(test-addr-1) Preparing to commit transaction: 1 objects, ... bytes
|
||||
|
||||
>>> zs1.connection.connection_lost(None)
|
||||
ZEO.StorageServer INFO
|
||||
(test-addr-1) disconnected during locked transaction
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-1) ('1') unlock: transactions waiting: 0
|
||||
|
||||
>>> zs2 = ZEO.tests.servertesting.client(server, '2')
|
||||
ZEO.asyncio.base INFO
|
||||
Connected server protocol
|
||||
ZEO.asyncio.server INFO
|
||||
received handshake 'Z5'
|
||||
>>> tid2 = start_trans(zs2)
|
||||
>>> resolved2 = zs2.vote(tid2) # doctest: +ELLIPSIS
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-2) ('1') lock: transactions waiting: 0
|
||||
ZEO.StorageServer BLATHER
|
||||
(test-addr-2) Preparing to commit transaction: 1 objects, ... bytes
|
||||
|
||||
>>> zs2.tpc_abort(tid2)
|
||||
ZEO.StorageServer DEBUG
|
||||
(test-addr-2) ('1') unlock: transactions waiting: 0
|
||||
|
||||
>>> logging.getLogger('ZEO').setLevel(logging.NOTSET)
|
||||
>>> logging.getLogger('ZEO').removeHandler(handler)
|
||||
|
||||
>>> server.close()
|
||||
"""
|
||||
|
||||
def test_suite():
|
||||
return unittest.TestSuite((
|
||||
doctest.DocTestSuite(
|
||||
setUp=ZODB.tests.util.setUp, tearDown=setupstack.tearDown,
|
||||
checker=renormalizing.RENormalizing([
|
||||
(re.compile(r'\d+/test-addr'), ''),
|
||||
(re.compile(r"'lock_time': \d+.\d+"), 'lock_time'),
|
||||
(re.compile(r"'start': '[^\n]+'"), 'start'),
|
||||
(re.compile('ZODB.POSException.StorageTransactionError'),
|
||||
'StorageTransactionError'),
|
||||
]),
|
||||
),
|
||||
))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,110 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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 suite for ZEO.runzeo.ZEOOptions."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import ZODB.config
|
||||
|
||||
from ZEO.runzeo import ZEOOptions
|
||||
from zdaemon.tests.testzdoptions import TestZDOptions
|
||||
|
||||
# When a hostname isn't specified in a socket binding address, ZConfig
|
||||
# supplies the empty string.
|
||||
DEFAULT_BINDING_HOST = ""
|
||||
|
||||
class TestZEOOptions(TestZDOptions):
|
||||
|
||||
OptionsClass = ZEOOptions
|
||||
|
||||
input_args = ["-f", "Data.fs", "-a", "5555"]
|
||||
output_opts = [("-f", "Data.fs"), ("-a", "5555")]
|
||||
output_args = []
|
||||
|
||||
configdata = """
|
||||
<zeo>
|
||||
address 5555
|
||||
</zeo>
|
||||
<filestorage fs>
|
||||
path Data.fs
|
||||
</filestorage>
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.tempfilename = tempfile.mktemp()
|
||||
with open(self.tempfilename, "w") as f:
|
||||
f.write(self.configdata)
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
os.remove(self.tempfilename)
|
||||
except os.error:
|
||||
pass
|
||||
|
||||
def test_configure(self):
|
||||
# Hide the base class test_configure
|
||||
pass
|
||||
|
||||
def test_default_help(self): pass # disable silly test w spurious failures
|
||||
|
||||
def test_defaults_with_schema(self):
|
||||
options = self.OptionsClass()
|
||||
options.realize(["-C", self.tempfilename])
|
||||
self.assertEqual(options.address, (DEFAULT_BINDING_HOST, 5555))
|
||||
self.assertEqual(len(options.storages), 1)
|
||||
opener = options.storages[0]
|
||||
self.assertEqual(opener.name, "fs")
|
||||
self.assertEqual(opener.__class__, ZODB.config.FileStorage)
|
||||
self.assertEqual(options.read_only, 0)
|
||||
self.assertEqual(options.transaction_timeout, None)
|
||||
self.assertEqual(options.invalidation_queue_size, 100)
|
||||
|
||||
def test_defaults_without_schema(self):
|
||||
options = self.OptionsClass()
|
||||
options.realize(["-a", "5555", "-f", "Data.fs"])
|
||||
self.assertEqual(options.address, (DEFAULT_BINDING_HOST, 5555))
|
||||
self.assertEqual(len(options.storages), 1)
|
||||
opener = options.storages[0]
|
||||
self.assertEqual(opener.name, "1")
|
||||
self.assertEqual(opener.__class__, ZODB.config.FileStorage)
|
||||
self.assertEqual(opener.config.path, "Data.fs")
|
||||
self.assertEqual(options.read_only, 0)
|
||||
self.assertEqual(options.transaction_timeout, None)
|
||||
self.assertEqual(options.invalidation_queue_size, 100)
|
||||
|
||||
def test_commandline_overrides(self):
|
||||
options = self.OptionsClass()
|
||||
options.realize(["-C", self.tempfilename,
|
||||
"-a", "6666", "-f", "Wisdom.fs"])
|
||||
self.assertEqual(options.address, (DEFAULT_BINDING_HOST, 6666))
|
||||
self.assertEqual(len(options.storages), 1)
|
||||
opener = options.storages[0]
|
||||
self.assertEqual(opener.__class__, ZODB.config.FileStorage)
|
||||
self.assertEqual(opener.config.path, "Wisdom.fs")
|
||||
self.assertEqual(options.read_only, 0)
|
||||
self.assertEqual(options.transaction_timeout, None)
|
||||
self.assertEqual(options.invalidation_queue_size, 100)
|
||||
|
||||
|
||||
def test_suite():
|
||||
suite = unittest.TestSuite()
|
||||
for cls in [TestZEOOptions]:
|
||||
suite.addTest(unittest.makeSuite(cls))
|
||||
return suite
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,179 @@
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import os
|
||||
|
||||
from ZEO._compat import PY3
|
||||
from ZEO.runzeo import ZEOServer
|
||||
|
||||
|
||||
class TestStorageServer(object):
|
||||
|
||||
def __init__(self, fail_create_server):
|
||||
self.called = []
|
||||
if fail_create_server: raise RuntimeError()
|
||||
|
||||
def close(self):
|
||||
self.called.append("close")
|
||||
|
||||
|
||||
class TestZEOServer(ZEOServer):
|
||||
|
||||
def __init__(self, fail_create_server=False, fail_loop_forever=False):
|
||||
ZEOServer.__init__(self, None)
|
||||
self.called = []
|
||||
self.fail_create_server = fail_create_server
|
||||
self.fail_loop_forever = fail_loop_forever
|
||||
|
||||
def setup_default_logging(self):
|
||||
self.called.append("setup_default_logging")
|
||||
|
||||
def check_socket(self):
|
||||
self.called.append("check_socket")
|
||||
|
||||
def clear_socket(self):
|
||||
self.called.append("clear_socket")
|
||||
|
||||
def make_pidfile(self):
|
||||
self.called.append("make_pidfile")
|
||||
|
||||
def open_storages(self):
|
||||
self.called.append("open_storages")
|
||||
|
||||
def setup_signals(self):
|
||||
self.called.append("setup_signals")
|
||||
|
||||
def create_server(self):
|
||||
self.called.append("create_server")
|
||||
self.server = TestStorageServer(self.fail_create_server)
|
||||
|
||||
def loop_forever(self):
|
||||
self.called.append("loop_forever")
|
||||
if self.fail_loop_forever: raise RuntimeError()
|
||||
|
||||
def close_server(self):
|
||||
self.called.append("close_server")
|
||||
ZEOServer.close_server(self)
|
||||
|
||||
def remove_pidfile(self):
|
||||
self.called.append("remove_pidfile")
|
||||
|
||||
|
||||
class AttributeErrorTests(unittest.TestCase):
|
||||
|
||||
def testFailCreateServer(self):
|
||||
#
|
||||
# Fix AttributeError: 'ZEOServer' object has no attribute
|
||||
# 'server' in ZEOServer.main
|
||||
#
|
||||
# Demonstrate the AttributeError
|
||||
zeo = TestZEOServer(fail_create_server=True)
|
||||
self.assertRaises(RuntimeError, zeo.main)
|
||||
|
||||
|
||||
class CloseServerTests(unittest.TestCase):
|
||||
|
||||
def testCallSequence(self):
|
||||
# The close_server hook is called after loop_forever
|
||||
# has returned
|
||||
zeo = TestZEOServer()
|
||||
zeo.main()
|
||||
self.assertEqual(zeo.called, [
|
||||
"setup_default_logging",
|
||||
"check_socket",
|
||||
"clear_socket",
|
||||
"make_pidfile",
|
||||
"open_storages",
|
||||
"setup_signals",
|
||||
"create_server",
|
||||
"loop_forever",
|
||||
"close_server", # New
|
||||
"clear_socket",
|
||||
"remove_pidfile",
|
||||
])
|
||||
# The default implementation closes the storage server
|
||||
self.assertEqual(hasattr(zeo, "server"), True)
|
||||
self.assertEqual(zeo.server.called, ["close"])
|
||||
|
||||
def testFailLoopForever(self):
|
||||
# The close_server hook is called if loop_forever exits
|
||||
# with an exception
|
||||
zeo = TestZEOServer(fail_loop_forever=True)
|
||||
self.assertRaises(RuntimeError, zeo.main)
|
||||
self.assertEqual(zeo.called, [
|
||||
"setup_default_logging",
|
||||
"check_socket",
|
||||
"clear_socket",
|
||||
"make_pidfile",
|
||||
"open_storages",
|
||||
"setup_signals",
|
||||
"create_server",
|
||||
"loop_forever",
|
||||
"close_server",
|
||||
"clear_socket",
|
||||
"remove_pidfile",
|
||||
])
|
||||
# The storage server has been closed
|
||||
self.assertEqual(hasattr(zeo, "server"), True)
|
||||
self.assertEqual(zeo.server.called, ["close"])
|
||||
|
||||
def testFailCreateServer(self):
|
||||
# The close_server hook is called if create_server exits
|
||||
# with an exception
|
||||
zeo = TestZEOServer(fail_create_server=True)
|
||||
self.assertRaises(RuntimeError, zeo.main)
|
||||
self.assertEqual(zeo.called, [
|
||||
"setup_default_logging",
|
||||
"check_socket",
|
||||
"clear_socket",
|
||||
"make_pidfile",
|
||||
"open_storages",
|
||||
"setup_signals",
|
||||
"create_server",
|
||||
"close_server",
|
||||
"clear_socket",
|
||||
"remove_pidfile",
|
||||
])
|
||||
# The server attribute is present but None
|
||||
self.assertEqual(hasattr(zeo, "server"), True)
|
||||
self.assertEqual(zeo.server, None)
|
||||
|
||||
@mock.patch('os.unlink')
|
||||
class TestZEOServerSocket(unittest.TestCase):
|
||||
|
||||
def _unlinked(self, unlink, options):
|
||||
server = ZEOServer(options)
|
||||
server.clear_socket()
|
||||
unlink.assert_called_once()
|
||||
|
||||
def _not_unlinked(self, unlink, options):
|
||||
server = ZEOServer(options)
|
||||
server.clear_socket()
|
||||
unlink.assert_not_called()
|
||||
|
||||
def test_clear_with_native_str(self, unlink):
|
||||
class Options(object):
|
||||
address = "a str that does not exist"
|
||||
self._unlinked(unlink, Options)
|
||||
|
||||
def test_clear_with_unicode_str(self, unlink):
|
||||
class Options(object):
|
||||
address = u"a str that does not exist"
|
||||
self._unlinked(unlink, Options)
|
||||
|
||||
def test_clear_with_bytes(self, unlink):
|
||||
class Options(object):
|
||||
address = b'a byte str that does not exist'
|
||||
|
||||
if PY3:
|
||||
# bytes are not a string type under Py3
|
||||
assertion = self._not_unlinked
|
||||
else:
|
||||
assertion = self._unlinked
|
||||
|
||||
assertion(unlink, Options)
|
||||
|
||||
def test_clear_with_tuple(self, unlink):
|
||||
class Options(object):
|
||||
address = ('abc', 1)
|
||||
self._not_unlinked(unlink, Options)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
"""Clients can pass credentials to a server.
|
||||
|
||||
This is an experimental feature to enable server authentication and
|
||||
authorization.
|
||||
"""
|
||||
from zope.testing import setupstack
|
||||
import unittest
|
||||
|
||||
import ZEO.StorageServer
|
||||
|
||||
from . import forker
|
||||
from .threaded import threaded_server_tests
|
||||
|
||||
@unittest.skipIf(forker.ZEO4_SERVER, "ZEO4 servers don't support SSL")
|
||||
class ClientAuthTests(setupstack.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.setUpDirectory()
|
||||
self.__register = ZEO.StorageServer.ZEOStorage.register
|
||||
|
||||
def tearDown(self):
|
||||
ZEO.StorageServer.ZEOStorage.register = self.__register
|
||||
|
||||
def test_passing_credentials(self):
|
||||
|
||||
# First, we'll temporarily swap the storage server register
|
||||
# method with one that let's is see credentials that were passed:
|
||||
|
||||
creds_log = []
|
||||
|
||||
def register(zs, storage_id, read_only, credentials=self):
|
||||
creds_log.append(credentials)
|
||||
return self.__register(zs, storage_id, read_only)
|
||||
|
||||
ZEO.StorageServer.ZEOStorage.register = register
|
||||
|
||||
# Now start an in process server
|
||||
addr, stop = ZEO.server()
|
||||
|
||||
# If we connect, without providing credentials, then no
|
||||
# credentials will be passed to register:
|
||||
|
||||
client = ZEO.client(addr)
|
||||
|
||||
self.assertEqual(creds_log, [self])
|
||||
client.close()
|
||||
creds_log.pop()
|
||||
|
||||
# But if we pass credentials, they'll be passed to register:
|
||||
creds = dict(user='me', password='123')
|
||||
client = ZEO.client(addr, credentials=creds)
|
||||
self.assertEqual(creds_log, [creds])
|
||||
client.close()
|
||||
|
||||
stop()
|
||||
|
||||
def test_suite():
|
||||
suite = unittest.makeSuite(ClientAuthTests)
|
||||
suite.layer = threaded_server_tests
|
||||
return suite
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user