770 lines
30 KiB
Python
770 lines
30 KiB
Python
##############################################################################
|
|
#
|
|
# Copyright (c) 2009 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 absolute_import, print_function, division
|
|
|
|
from relstorage.autotemp import AutoTemporaryFile
|
|
from ZODB.utils import p64
|
|
from ZODB.utils import u64
|
|
|
|
from ZODB.POSException import ReadConflictError
|
|
from persistent.timestamp import TimeStamp
|
|
|
|
import BTrees
|
|
|
|
import importlib
|
|
import logging
|
|
import os
|
|
|
|
import threading
|
|
|
|
from zope import interface
|
|
|
|
from relstorage._compat import string_types
|
|
from relstorage._compat import iteritems
|
|
from relstorage._compat import PYPY
|
|
|
|
from relstorage.cache.interfaces import IPersistentCache
|
|
from relstorage.cache import persistence
|
|
from relstorage.cache.local_client import LocalClient
|
|
from relstorage.cache.trace import ZEOTracer
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
class _UsedAfterRelease(object):
|
|
pass
|
|
_UsedAfterRelease = _UsedAfterRelease()
|
|
|
|
|
|
@interface.implementer(IPersistentCache)
|
|
class StorageCache(object):
|
|
"""RelStorage integration with memcached or similar.
|
|
|
|
Holds a list of memcache clients in order from most local to
|
|
most global. The first is a LocalClient, which stores the cache
|
|
in the Python process, but shares the cache between threads.
|
|
"""
|
|
# pylint:disable=too-many-instance-attributes,too-many-public-methods
|
|
|
|
# send_limit: approximate limit on the bytes to buffer before
|
|
# sending to the cache.
|
|
send_limit = 1024 * 1024
|
|
|
|
# queue is an AutoTemporaryFile during transaction commit.
|
|
queue = None
|
|
|
|
# queue_contents is a map of {oid_int: (startpos, endpos)}
|
|
# during transaction commit.
|
|
queue_contents = None
|
|
|
|
# checkpoints, when set, is a tuple containing the integer
|
|
# transaction ID of the two current checkpoints. checkpoint0 is
|
|
# greater than or equal to checkpoint1.
|
|
checkpoints = None
|
|
|
|
# current_tid contains the last polled transaction ID. Invariant:
|
|
# when self.checkpoints is not None, self.delta_after0 has info
|
|
# from all transactions in the range:
|
|
# self.checkpoints[0] < tid <= self.current_tid
|
|
current_tid = 0
|
|
|
|
_tracer = None
|
|
|
|
# An LLBTree uses much less memory than a dict, and is still plenty fast on CPython;
|
|
# it's just as big and slower on PyPy, though.
|
|
_delta_map_type = BTrees.family64.II.BTree if not PYPY else dict
|
|
|
|
|
|
def __init__(self, adapter, options, prefix, local_client=None,
|
|
_tracer=None):
|
|
self.adapter = adapter
|
|
self.options = options
|
|
self.prefix = prefix or ''
|
|
|
|
# checkpoints_key holds the current checkpoints.
|
|
self.checkpoints_key = '%s:checkpoints' % self.prefix
|
|
assert isinstance(self.checkpoints_key, str) # no unicode on Py2
|
|
|
|
# delta_after0 contains {oid: tid} after checkpoint 0
|
|
# and before or at self.current_tid.
|
|
self.delta_after0 = self._delta_map_type()
|
|
|
|
# delta_after1 contains {oid: tid} after checkpoint 1 and
|
|
# before or at checkpoint 0. The content of delta_after1 only
|
|
# changes when checkpoints move.
|
|
self.delta_after1 = self._delta_map_type()
|
|
|
|
# delta_size_limit places an approximate limit on the number of
|
|
# entries in the delta_after maps.
|
|
self.delta_size_limit = options.cache_delta_size_limit
|
|
|
|
self.clients_local_first = []
|
|
if local_client is None:
|
|
self.clients_local_first.append(LocalClient(options, self.prefix))
|
|
else:
|
|
self.clients_local_first.append(local_client)
|
|
|
|
if options.cache_servers:
|
|
module_name = options.cache_module_name
|
|
module = importlib.import_module(module_name)
|
|
servers = options.cache_servers
|
|
if isinstance(servers, string_types):
|
|
servers = servers.split()
|
|
self.clients_local_first.append(module.Client(servers))
|
|
|
|
# self.clients_local_first is in order from local to global caches,
|
|
# while self.clients_global_first is in order from global to local.
|
|
self.clients_global_first = list(reversed(self.clients_local_first))
|
|
|
|
if local_client is None:
|
|
self.restore()
|
|
|
|
if _tracer is None:
|
|
tracefile = persistence.trace_file(options, self.prefix)
|
|
if tracefile:
|
|
_tracer = ZEOTracer(tracefile)
|
|
_tracer.trace(0x00)
|
|
|
|
self._tracer = _tracer
|
|
if hasattr(self._tracer, 'trace_store_current'):
|
|
self._trace = self._tracer.trace
|
|
self._trace_store_current = self._tracer.trace_store_current
|
|
|
|
def __bool__(self):
|
|
return True
|
|
__nonzero__ = __bool__
|
|
|
|
def __len__(self):
|
|
if self.clients_local_first is _UsedAfterRelease:
|
|
return 0
|
|
return len(self.local_client)
|
|
|
|
@property
|
|
def size(self):
|
|
if self.clients_local_first is _UsedAfterRelease:
|
|
return 0
|
|
return self.local_client.size
|
|
|
|
@property
|
|
def limit(self):
|
|
if self.clients_local_first is _UsedAfterRelease:
|
|
return 0
|
|
return self.local_client.limit
|
|
|
|
@property
|
|
def local_client(self):
|
|
"""
|
|
The (shared) local in-memory cache client.
|
|
"""
|
|
return self.clients_local_first[0]
|
|
|
|
def stats(self):
|
|
"""
|
|
Return stats. This is a debugging aid only. The format is undefined and intended
|
|
for human inspection only.
|
|
"""
|
|
try:
|
|
local_client = self.local_client
|
|
except TypeError:
|
|
return {'closed': True}
|
|
else:
|
|
return local_client.stats()
|
|
|
|
def new_instance(self):
|
|
"""Return a copy of this instance sharing the same local client"""
|
|
local_client = self.local_client if self.options.share_local_cache else None
|
|
|
|
cache = type(self)(self.adapter, self.options, self.prefix,
|
|
local_client,
|
|
_tracer=self._tracer or False)
|
|
return cache
|
|
|
|
def release(self):
|
|
"""
|
|
Release resources held by this instance.
|
|
|
|
This is usually memcache connections if they're in use.
|
|
"""
|
|
clients = self.clients_local_first if self.clients_local_first is not _UsedAfterRelease else ()
|
|
for client in clients:
|
|
client.disconnect_all()
|
|
|
|
# Release our clients. If we had a non-shared local cache,
|
|
# this will also allow it to release any memory its holding.
|
|
# Set them to non-iterables to make it obvious if we are used
|
|
# after release.
|
|
self.clients_local_first = _UsedAfterRelease
|
|
self.clients_global_first = _UsedAfterRelease
|
|
|
|
def save(self):
|
|
"""
|
|
Store any persistent client data.
|
|
"""
|
|
if self.options.cache_local_dir and len(self):
|
|
return persistence.save_local_cache(self.options, self.prefix, self)
|
|
|
|
|
|
def write_to_stream(self, stream):
|
|
# We currently don't write anything to the stream, delegating instead
|
|
# just to the local client.
|
|
|
|
# We experimented with trying to save and load chcekpoints and
|
|
# the delta maps, but this turned out to be complex (because
|
|
# the `new_instance`s that have the actual data are released
|
|
# before we are, so their data gets lost, and we have to
|
|
# implement a parent/child relationship to fix that) and no
|
|
# more effective than relying on the default checkpoints we
|
|
# get from polling, if there have been no changes---at least
|
|
# in the case of zodbshootout benchmark (in fact, it was
|
|
# somewhat *slower*, for reasons that aren't fully clear).
|
|
|
|
# Note that if we did want to dump the delta maps, we would
|
|
# need to either wrap them in a dict or dump them pairwise; We
|
|
# can't dump a BTree larger than about 25000 without getting
|
|
# into recursion problems.
|
|
self.local_client.write_to_stream(stream)
|
|
|
|
def get_cache_modification_time_for_stream(self):
|
|
max_tid = 0
|
|
for key in self.local_client:
|
|
parts = key.split(':')
|
|
if len(parts) != 4:
|
|
continue
|
|
tid = int(parts[2])
|
|
max_tid = max(tid, max_tid)
|
|
|
|
if max_tid:
|
|
tid_str = p64(max_tid)
|
|
ts = TimeStamp(tid_str)
|
|
return ts.timeTime()
|
|
|
|
def restore(self):
|
|
options = self.options
|
|
if options.cache_local_dir:
|
|
persistence.load_local_cache(options, self.prefix, self)
|
|
|
|
def read_from_stream(self, stream):
|
|
return self.local_client.read_from_stream(stream)
|
|
|
|
def close(self):
|
|
"""
|
|
Release resources held by this instance, and
|
|
save any persistent data necessary.
|
|
"""
|
|
self.save()
|
|
self.release()
|
|
|
|
if self._tracer:
|
|
# Note we can't do this in release(). Release is called on
|
|
# all instances, while close() is only called on the main one.
|
|
self._tracer.close()
|
|
del self._trace
|
|
del self._trace_store_current
|
|
del self._tracer
|
|
|
|
def clear(self, load_persistent=True):
|
|
"""
|
|
Remove all data from the cache. Called by speed tests.
|
|
|
|
Starting from the introduction of persistent cache files,
|
|
this also results in the local client being repopulated with
|
|
the current set of persistent data. The *load_persistent* keyword can
|
|
be used to control this.
|
|
|
|
.. versionchanged:: 2.0b6
|
|
Added the ``load_persistent`` keyword. This argument is provisional.
|
|
"""
|
|
for client in self.clients_local_first:
|
|
client.flush_all()
|
|
|
|
self.checkpoints = None
|
|
self.delta_after0 = self._delta_map_type()
|
|
self.delta_after1 = self._delta_map_type()
|
|
self.current_tid = 0
|
|
|
|
if load_persistent:
|
|
self.restore()
|
|
|
|
@staticmethod
|
|
def _trace(*_args, **_kwargs): # pylint:disable=method-hidden
|
|
# Dummy method for when we don't do tracing
|
|
return
|
|
|
|
@staticmethod
|
|
def _trace_store_current(_tid_int, _items): # pylint:disable=method-hidden
|
|
# Dummy method for when we don't do tracing
|
|
return
|
|
|
|
def _check_tid_after_load(self, oid_int, actual_tid_int,
|
|
expect_tid_int=None):
|
|
"""Verify the tid of an object loaded from the database is sane."""
|
|
|
|
if actual_tid_int > self.current_tid:
|
|
# Strangely, the database just gave us data from a future
|
|
# transaction. We can't give the data to ZODB because that
|
|
# would be a consistency violation. However, the cause is hard
|
|
# to track down, so issue a ReadConflictError and hope that
|
|
# the application retries successfully.
|
|
msg = ("Got data for OID 0x%(oid_int)x from "
|
|
"future transaction %(actual_tid_int)d (%(got_ts)s). "
|
|
"Current transaction is %(current_tid)d (%(current_ts)s)."
|
|
% {
|
|
'oid_int': oid_int,
|
|
'actual_tid_int': actual_tid_int,
|
|
'current_tid': self.current_tid,
|
|
'got_ts': str(TimeStamp(p64(actual_tid_int))),
|
|
'current_ts': str(TimeStamp(p64(self.current_tid))),
|
|
})
|
|
raise ReadConflictError(msg)
|
|
|
|
if expect_tid_int is not None and actual_tid_int != expect_tid_int:
|
|
# Uh-oh, the cache is inconsistent with the database.
|
|
# Possible causes:
|
|
#
|
|
# - The database MUST provide a snapshot view for each
|
|
# session; this error can occur if that requirement is
|
|
# violated. For example, MySQL's MyISAM engine is not
|
|
# sufficient for the object_state table because MyISAM
|
|
# can not provide a snapshot view. (InnoDB is
|
|
# sufficient.)
|
|
#
|
|
# - Something could be writing to the database out
|
|
# of order, such as a version of RelStorage that
|
|
# acquires a different commit lock.
|
|
#
|
|
# - A software bug. In the past, there was a subtle bug
|
|
# in after_poll() that caused it to ignore the
|
|
# transaction order, leading it to sometimes put the
|
|
# wrong tid in delta_after*.
|
|
cp0, cp1 = self.checkpoints
|
|
|
|
msg = ("Detected an inconsistency "
|
|
"between the RelStorage cache and the database "
|
|
"while loading an object using the delta_after0 dict. "
|
|
"Please verify the database is configured for "
|
|
"ACID compliance and that all clients are using "
|
|
"the same commit lock. "
|
|
"(oid_int=%(oid_int)r, expect_tid_int=%(expect_tid_int)r, "
|
|
"actual_tid_int=%(actual_tid_int)r, "
|
|
"current_tid=%(current_tid)r, cp0=%(cp0)r, cp1=%(cp1)r, "
|
|
"len(delta_after0)=%(lda0)r, len(delta_after1)=%(lda1)r, "
|
|
"pid=%(pid)r, thread_ident=%(thread_ident)r)"
|
|
% {
|
|
'oid_int': oid_int,
|
|
'expect_tid_int': expect_tid_int,
|
|
'actual_tid_int': actual_tid_int,
|
|
'current_tid': self.current_tid,
|
|
'cp0': cp0,
|
|
'cp1': cp1,
|
|
'lda0': len(self.delta_after0),
|
|
'lda1': len(self.delta_after1),
|
|
'pid': os.getpid(),
|
|
'thread_ident': threading.current_thread(),
|
|
})
|
|
raise AssertionError(msg)
|
|
|
|
def load(self, cursor, oid_int):
|
|
"""Load the given object from cache if possible.
|
|
|
|
Fall back to loading from the database.
|
|
"""
|
|
# pylint:disable=too-many-statements,too-many-branches,too-many-locals
|
|
if not self.checkpoints:
|
|
# No poll has occurred yet. For safety, don't use the cache.
|
|
self._trace(0x20, oid_int)
|
|
return self.adapter.mover.load_current(cursor, oid_int)
|
|
|
|
prefix = self.prefix
|
|
|
|
# Get the object from the transaction specified
|
|
# by the following values, in order:
|
|
#
|
|
# 1. delta_after0[oid_int]
|
|
# 2. checkpoints[0]
|
|
# 3. delta_after1[oid_int]
|
|
# 4. checkpoints[1]
|
|
# 5. The database.
|
|
#
|
|
# checkpoints[0] is the preferred location.
|
|
#
|
|
# If delta_after0 contains oid_int, we should not look at any
|
|
# other cache keys, since the tid_int specified in delta_after0
|
|
# replaces all older transaction IDs. Similarly, if
|
|
# delta_after1 contains oid_int, we should not look at
|
|
# checkpoints[1]. Also, when both checkpoints are set to the
|
|
# same transaction ID, we don't need to ask for the same key
|
|
# twice.
|
|
|
|
tid_int = self.delta_after0.get(oid_int)
|
|
if tid_int:
|
|
# This object changed after checkpoint0, so
|
|
# there is only one place to look for its state.
|
|
cachekey = '%s:state:%d:%d' % (prefix, tid_int, oid_int)
|
|
for client in self.clients_local_first:
|
|
cache_data = client.get(cachekey)
|
|
if cache_data and len(cache_data) >= 8:
|
|
# Cache hit.
|
|
# Note that we trace all cache hits, not just the local cache hit.
|
|
# This makes the simulation less useful, but the stats might still have
|
|
# value to people trying different tuning options manually.
|
|
self._trace(0x22, oid_int, tid_int, dlen=len(cache_data) - 8)
|
|
assert cache_data[:8] == p64(tid_int)
|
|
return cache_data[8:], tid_int
|
|
# Cache miss.
|
|
self._trace(0x20, oid_int)
|
|
state, actual_tid_int = self.adapter.mover.load_current(
|
|
cursor, oid_int)
|
|
self._check_tid_after_load(oid_int, actual_tid_int, tid_int)
|
|
|
|
cache_data = p64(tid_int) + (state or b'')
|
|
for client in self.clients_local_first:
|
|
client.set(cachekey, cache_data)
|
|
return state, tid_int
|
|
|
|
# Make a list of cache keys to query. The list will have either
|
|
# 1 or 2 keys.
|
|
cp0, cp1 = self.checkpoints
|
|
cachekeys = []
|
|
cp0_key = '%s:state:%d:%d' % (prefix, cp0, oid_int)
|
|
cachekeys.append(cp0_key)
|
|
da1_key = None
|
|
cp1_key = None
|
|
tid_int = self.delta_after1.get(oid_int)
|
|
if tid_int:
|
|
da1_key = '%s:state:%d:%d' % (prefix, tid_int, oid_int)
|
|
cachekeys.append(da1_key)
|
|
elif cp1 != cp0:
|
|
cp1_key = '%s:state:%d:%d' % (prefix, cp1, oid_int)
|
|
cachekeys.append(cp1_key)
|
|
|
|
for client in self.clients_local_first:
|
|
# Query the cache. Query multiple keys simultaneously to
|
|
# minimize latency.
|
|
response = client.get_multi(cachekeys)
|
|
if response:
|
|
cache_data = response.get(cp0_key)
|
|
if cache_data and len(cache_data) >= 8:
|
|
# Cache hit on the preferred cache key.
|
|
local_client = self.local_client
|
|
if client is not local_client:
|
|
# Copy to the local client.
|
|
local_client.set(cp0_key, cache_data)
|
|
self._trace(0x22, oid_int, u64(cache_data[:8]), dlen=len(cache_data) - 8)
|
|
return cache_data[8:], u64(cache_data[:8])
|
|
|
|
if da1_key:
|
|
cache_data = response.get(da1_key)
|
|
elif cp1_key:
|
|
cache_data = response.get(cp1_key)
|
|
if cache_data and len(cache_data) >= 8:
|
|
# Cache hit, but copy the state to
|
|
# the currently preferred key.
|
|
self._trace(0x22, oid_int, u64(cache_data[:8]), dlen=len(cache_data) - 8)
|
|
for client_to_set in self.clients_local_first:
|
|
client_to_set.set(cp0_key, cache_data)
|
|
return cache_data[8:], u64(cache_data[:8])
|
|
|
|
# Cache miss.
|
|
self._trace(0x20, oid_int)
|
|
state, tid_int = self.adapter.mover.load_current(cursor, oid_int)
|
|
if tid_int:
|
|
self._check_tid_after_load(oid_int, tid_int)
|
|
cache_data = p64(tid_int) + (state or b'')
|
|
# Record this as a store into the cache, ZEO does.
|
|
self._trace(0x52, oid_int, tid_int, dlen=len(state) if state else 0)
|
|
for client in self.clients_local_first:
|
|
client.set(cp0_key, cache_data)
|
|
return state, tid_int
|
|
|
|
|
|
def tpc_begin(self):
|
|
"""Prepare temp space for objects to cache."""
|
|
# start with a fresh in-memory buffer instead of reusing one that might
|
|
# already be spooled to disk.
|
|
self.queue = AutoTemporaryFile()
|
|
self.queue_contents = {}
|
|
|
|
def store_temp(self, oid_int, state):
|
|
"""Queue an object for caching.
|
|
|
|
Typically, we can't actually cache the object yet, because its
|
|
transaction ID is not yet chosen.
|
|
"""
|
|
assert isinstance(state, bytes)
|
|
queue = self.queue
|
|
queue.seek(0, 2) # seek to end
|
|
startpos = queue.tell()
|
|
queue.write(state)
|
|
endpos = queue.tell()
|
|
self.queue_contents[oid_int] = (startpos, endpos)
|
|
|
|
|
|
def _read_temp_state(self, startpos, endpos):
|
|
self.queue.seek(startpos)
|
|
length = endpos - startpos
|
|
state = self.queue.read(length)
|
|
if len(state) != length:
|
|
raise AssertionError("Queued cache data is truncated")
|
|
return state, length
|
|
|
|
def read_temp(self, oid_int):
|
|
"""
|
|
Return the bytes for a previously stored temporary item.
|
|
"""
|
|
startpos, endpos = self.queue_contents[oid_int]
|
|
return self._read_temp_state(startpos, endpos)[0]
|
|
|
|
def send_queue(self, tid):
|
|
"""Now that this tid is known, send all queued objects to the cache"""
|
|
tid_int = u64(tid)
|
|
send_size = 0
|
|
to_send = {}
|
|
prefix = self.prefix
|
|
|
|
# Order the queue by file position, which should help if the
|
|
# file is large and needs to be read sequentially from disk.
|
|
items = [
|
|
(startpos, endpos, oid_int)
|
|
for (oid_int, (startpos, endpos)) in iteritems(self.queue_contents)
|
|
]
|
|
items.sort()
|
|
# Trace these. This is the equivalent of ZEOs
|
|
# ClientStorage._update_cache.
|
|
self._trace_store_current(tid_int, items)
|
|
for startpos, endpos, oid_int in items:
|
|
state, length = self._read_temp_state(startpos, endpos)
|
|
cachekey = '%s:state:%d:%d' % (prefix, tid_int, oid_int)
|
|
item_size = length + len(cachekey)
|
|
if send_size and send_size + item_size >= self.send_limit:
|
|
for client in self.clients_local_first:
|
|
client.set_multi(to_send)
|
|
to_send.clear()
|
|
send_size = 0
|
|
to_send[cachekey] = tid + state
|
|
send_size += item_size
|
|
|
|
if to_send:
|
|
for client in self.clients_local_first:
|
|
client.set_multi(to_send)
|
|
|
|
self.queue_contents.clear()
|
|
self.queue.seek(0)
|
|
|
|
def after_tpc_finish(self, tid):
|
|
"""
|
|
Flush queued changes.
|
|
|
|
This is called after the database commit lock is released,
|
|
but before releasing the storage lock that will allow other
|
|
threads to use this instance.
|
|
"""
|
|
tid_int = u64(tid)
|
|
|
|
if self.checkpoints:
|
|
for oid_int in self.queue_contents:
|
|
# Future cache lookups for oid_int should now use
|
|
# the tid just committed.
|
|
self.delta_after0[oid_int] = tid_int
|
|
|
|
self.send_queue(tid)
|
|
|
|
def clear_temp(self):
|
|
"""Discard all transaction-specific temporary data.
|
|
|
|
Called after transaction finish or abort.
|
|
"""
|
|
self.queue_contents = None
|
|
if self.queue is not None:
|
|
self.queue.close()
|
|
self.queue = None
|
|
|
|
|
|
def after_poll(self, cursor, prev_tid_int, new_tid_int, changes):
|
|
"""Update checkpoint data after a database poll.
|
|
|
|
cursor is connected to a load connection.
|
|
|
|
changes lists all [(oid_int, tid_int)] changed after
|
|
prev_tid_int, up to and including new_tid_int, excluding the
|
|
changes last committed by the associated storage instance.
|
|
changes can be None to indicate too many objects changed
|
|
to list them all.
|
|
|
|
prev_tid_int can be None, in which case the changes
|
|
parameter will be ignored. new_tid_int can not be None.
|
|
"""
|
|
# pylint:disable=too-many-statements,too-many-branches,too-many-locals
|
|
new_checkpoints = None
|
|
for client in self.clients_global_first:
|
|
s = client.get(self.checkpoints_key)
|
|
if s:
|
|
try:
|
|
c0, c1 = s.split()
|
|
c0 = int(c0)
|
|
c1 = int(c1)
|
|
except ValueError:
|
|
# Invalid checkpoint cache value; ignore it.
|
|
pass
|
|
else:
|
|
if c0 >= c1:
|
|
new_checkpoints = (c0, c1)
|
|
break
|
|
|
|
if not new_checkpoints:
|
|
new_checkpoints = (new_tid_int, new_tid_int)
|
|
|
|
if not self.checkpoints:
|
|
# Initialize the checkpoints.
|
|
cache_data = '%d %d' % new_checkpoints
|
|
log.debug("Initializing checkpoints: %s", cache_data)
|
|
else:
|
|
# Suggest reinstatement of the former checkpoints, but
|
|
# use new checkpoints for this instance. Using new
|
|
# checkpoints ensures that we don't build up
|
|
# self.delta_after0 in case the cache is offline.
|
|
cache_data = '%d %d' % self.checkpoints
|
|
log.debug("Reinstating checkpoints: %s", cache_data)
|
|
|
|
cache_data = cache_data.encode("ascii")
|
|
for client in self.clients_global_first:
|
|
client.set(self.checkpoints_key, cache_data)
|
|
|
|
self.checkpoints = new_checkpoints
|
|
self.delta_after0 = self._delta_map_type()
|
|
self.delta_after1 = self._delta_map_type()
|
|
self.current_tid = new_tid_int
|
|
return
|
|
|
|
allow_shift = True
|
|
if new_checkpoints[0] > new_tid_int:
|
|
# checkpoint0 is in a future that this instance can't
|
|
# yet see. Ignore the checkpoint change for now.
|
|
new_checkpoints = self.checkpoints
|
|
if not new_checkpoints:
|
|
new_checkpoints = (new_tid_int, new_tid_int)
|
|
allow_shift = False
|
|
|
|
# We want to keep the current checkpoints for speed, but we
|
|
# have to replace them (to avoid consistency violations)
|
|
# if certain conditions happen (like emptying the ZODB cache).
|
|
if (new_checkpoints == self.checkpoints
|
|
and changes is not None
|
|
and prev_tid_int
|
|
and prev_tid_int <= self.current_tid
|
|
and new_tid_int >= self.current_tid):
|
|
# All the conditions for keeping the checkpoints were met,
|
|
# so just update self.delta_after0 and self.current_tid.
|
|
m = self.delta_after0
|
|
m_get = m.get
|
|
for oid_int, tid_int in changes:
|
|
my_tid_int = m_get(oid_int)
|
|
if my_tid_int is None or tid_int > my_tid_int:
|
|
m[oid_int] = tid_int
|
|
# 0x1E = invalidate (hit, saving non-current)
|
|
self._trace(0x1C, oid_int, tid_int)
|
|
self.current_tid = new_tid_int
|
|
else:
|
|
# We have to replace the checkpoints.
|
|
cp0, cp1 = new_checkpoints
|
|
log.debug("Using new checkpoints: %d %d", cp0, cp1)
|
|
# Use the checkpoints specified by the cache.
|
|
# Rebuild delta_after0 and delta_after1.
|
|
new_delta_after0 = self._delta_map_type()
|
|
new_delta_after1 = self._delta_map_type()
|
|
if cp1 < new_tid_int:
|
|
# poller.list_changes provides an iterator of
|
|
# (oid, tid) where tid > after_tid and tid <= last_tid.
|
|
change_list = self.adapter.poller.list_changes(
|
|
cursor, cp1, new_tid_int)
|
|
|
|
# Make a dictionary that contains, for each oid, the most
|
|
# recent tid listed in changes. This works because sorting the
|
|
# (oid, tid) pairs puts the newest tid at the back, and constructing
|
|
# the dictionary from that sorted list preserves order, keeping the
|
|
# last key that it saw.
|
|
try:
|
|
change_dict = self._delta_map_type(sorted(change_list))
|
|
except TypeError:
|
|
# pg8000 returns a list of lists, not a list of tuples. The
|
|
# BTree constructor is very particular about that. Normally one
|
|
# would use pg8000 on PyPy, where we don't use BTrees, so this shouldn't
|
|
# actually come up in practice.
|
|
change_dict = self._delta_map_type()
|
|
for oid_int, tid_int in sorted(change_list):
|
|
change_dict[oid_int] = tid_int
|
|
|
|
|
|
# Put the changes in new_delta_after*.
|
|
for oid_int, tid_int in change_dict.items():
|
|
# 0x1E = invalidate (hit, saving non-current)
|
|
self._trace(0x1C, oid_int, tid_int)
|
|
if tid_int > cp0:
|
|
new_delta_after0[oid_int] = tid_int
|
|
elif tid_int > cp1:
|
|
new_delta_after1[oid_int] = tid_int
|
|
|
|
self.checkpoints = new_checkpoints
|
|
self.delta_after0 = new_delta_after0
|
|
self.delta_after1 = new_delta_after1
|
|
self.current_tid = new_tid_int
|
|
|
|
if allow_shift and len(self.delta_after0) >= self.delta_size_limit:
|
|
# delta_after0 has reached its limit. The way to
|
|
# shrink it is to shift the checkpoints. Suggest
|
|
# shifted checkpoints for future polls.
|
|
# If delta_after0 is far over the limit (caused by a large
|
|
# transaction), suggest starting new checkpoints instead of
|
|
# shifting.
|
|
oversize = (len(self.delta_after0) >= self.delta_size_limit * 2)
|
|
self._suggest_shifted_checkpoints(new_tid_int, oversize)
|
|
|
|
|
|
def _suggest_shifted_checkpoints(self, tid_int, oversize):
|
|
"""Suggest that future polls use a new pair of checkpoints.
|
|
|
|
This does nothing if another instance has already shifted
|
|
the checkpoints.
|
|
|
|
checkpoint0 shifts to checkpoint1 and the tid just committed
|
|
becomes checkpoint0.
|
|
"""
|
|
cp0, _cp1 = self.checkpoints
|
|
assert tid_int > cp0
|
|
expect = '%d %d' % self.checkpoints
|
|
if oversize:
|
|
# start new checkpoints
|
|
change_to = '%d %d' % (tid_int, tid_int)
|
|
else:
|
|
# shift the existing checkpoints
|
|
change_to = '%d %d' % (tid_int, cp0)
|
|
expect = expect.encode('ascii')
|
|
change_to = change_to.encode('ascii')
|
|
|
|
for client in self.clients_global_first:
|
|
old_value = client.get(self.checkpoints_key)
|
|
if old_value:
|
|
break
|
|
if not old_value or old_value == expect:
|
|
# Shift the checkpoints.
|
|
# Although this is a race with other instances, the race
|
|
# should not matter.
|
|
log.debug("Shifting checkpoints to: %s. len(delta_after0) == %d.",
|
|
change_to, len(self.delta_after0))
|
|
for client in self.clients_global_first:
|
|
client.set(self.checkpoints_key, change_to)
|
|
# The poll code will later see the new checkpoints
|
|
# and update self.checkpoints and self.delta_after(0|1).
|
|
else:
|
|
log.debug("Checkpoints already shifted to %s. "
|
|
"len(delta_after0) == %d.", old_value, len(self.delta_after0))
|