Commit 5e6fd05f authored by Jim Fulton's avatar Jim Fulton

Make the cache thread safe again. :/

There are some cases where we want to touch the cache outside of the
I/O thread. Including:

- ClientStorage wants to to invalidata cache entries if it gets a
  conflict error in voting.

- loadBefore can probably be optimized by checking the cache in the
  client thread. (This will be safe for loadBefore, because the intent
  in practice is never to load current data.
parent 062cbecc
...@@ -33,7 +33,7 @@ import time ...@@ -33,7 +33,7 @@ import time
import ZODB.fsIndex import ZODB.fsIndex
import zc.lockfile import zc.lockfile
from ZODB.utils import p64, u64, z64 from ZODB.utils import p64, u64, z64, RLock
import six import six
from ._compat import PYPY from ._compat import PYPY
...@@ -182,6 +182,8 @@ class ClientCache(object): ...@@ -182,6 +182,8 @@ class ClientCache(object):
# currentofs. # currentofs.
self.currentofs = ZEC_HEADER_SIZE self.currentofs = ZEC_HEADER_SIZE
self._lock = RLock()
# self.f is the open file object. # self.f is the open file object.
# When we're not reusing an existing file, self.f is left None # 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 # here -- the scan() method must be called then to open the file
...@@ -239,6 +241,7 @@ class ClientCache(object): ...@@ -239,6 +241,7 @@ class ClientCache(object):
return self return self
def clear(self): def clear(self):
with self._lock:
self.f.seek(ZEC_HEADER_SIZE) self.f.seek(ZEC_HEADER_SIZE)
self.f.truncate() self.f.truncate()
self._initfile(ZEC_HEADER_SIZE) self._initfile(ZEC_HEADER_SIZE)
...@@ -451,6 +454,7 @@ class ClientCache(object): ...@@ -451,6 +454,7 @@ class ClientCache(object):
# new tid must be strictly greater than our current idea of the most # new tid must be strictly greater than our current idea of the most
# recent tid. # recent tid.
def setLastTid(self, tid): def setLastTid(self, tid):
with self._lock:
if (not tid) or (tid == z64): if (not tid) or (tid == z64):
return return
if (tid <= self.tid) and self._len: if (tid <= self.tid) and self._len:
...@@ -470,6 +474,7 @@ class ClientCache(object): ...@@ -470,6 +474,7 @@ class ClientCache(object):
# @return a transaction id # @return a transaction id
# @defreturn string, or 8 nulls if no transaction is yet known # @defreturn string, or 8 nulls if no transaction is yet known
def getLastTid(self): def getLastTid(self):
with self._lock:
return self.tid return self.tid
## ##
...@@ -479,6 +484,7 @@ class ClientCache(object): ...@@ -479,6 +484,7 @@ class ClientCache(object):
# in the cache # in the cache
# @defreturn 3-tuple: (string, string, string) # @defreturn 3-tuple: (string, string, string)
def load(self, oid, before_tid=None): def load(self, oid, before_tid=None):
with self._lock:
ofs = self.current.get(oid) ofs = self.current.get(oid)
if ofs is None: if ofs is None:
self._trace(0x20, oid) self._trace(0x20, oid)
...@@ -497,7 +503,8 @@ class ClientCache(object): ...@@ -497,7 +503,8 @@ class ClientCache(object):
return None return None
data = read(ldata) data = read(ldata)
assert len(data) == ldata, (ofs, self.f.tell(), oid, len(data), ldata) assert len(data) == ldata, (
ofs, self.f.tell(), oid, len(data), ldata)
# WARNING: The following assert changes the file position. # WARNING: The following assert changes the file position.
# We must not depend on this below or we'll fail in optimized mode. # We must not depend on this below or we'll fail in optimized mode.
...@@ -533,6 +540,7 @@ class ClientCache(object): ...@@ -533,6 +540,7 @@ class ClientCache(object):
# @return data record, serial number, start tid, and end tid # @return data record, serial number, start tid, and end tid
# @defreturn 4-tuple: (string, string, string, string) # @defreturn 4-tuple: (string, string, string, string)
def loadBefore(self, oid, before_tid): def loadBefore(self, oid, before_tid):
with self._lock:
noncurrent_for_oid = self.noncurrent.get(u64(oid)) noncurrent_for_oid = self.noncurrent.get(u64(oid))
if noncurrent_for_oid is None: if noncurrent_for_oid is None:
result = self.load(oid, before_tid) result = self.load(oid, before_tid)
...@@ -560,7 +568,8 @@ class ClientCache(object): ...@@ -560,7 +568,8 @@ class ClientCache(object):
size, saved_oid, saved_tid, end_tid, lver, ldata = unpack( size, saved_oid, saved_tid, end_tid, lver, ldata = unpack(
">I8s8s8sHI", read(34)) ">I8s8s8sHI", read(34))
assert saved_oid == oid, (ofs, self.f.tell(), oid, saved_oid) 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 saved_tid == p64(tid), (
ofs, self.f.tell(), oid, saved_tid, tid)
assert end_tid != z64, (ofs, self.f.tell(), oid) assert end_tid != z64, (ofs, self.f.tell(), oid)
assert lver == 0, "Versions aren't supported" assert lver == 0, "Versions aren't supported"
data = read(ldata) data = read(ldata)
...@@ -591,6 +600,7 @@ class ClientCache(object): ...@@ -591,6 +600,7 @@ class ClientCache(object):
# current. # current.
# @param data the actual data # @param data the actual data
def store(self, oid, start_tid, end_tid, data): def store(self, oid, start_tid, end_tid, data):
with self._lock:
seek = self.f.seek seek = self.f.seek
if end_tid is None: if end_tid is None:
ofs = self.current.get(oid) ofs = self.current.get(oid)
...@@ -601,14 +611,16 @@ class ClientCache(object): ...@@ -601,14 +611,16 @@ class ClientCache(object):
assert status == b'a', (ofs, self.f.tell(), oid) assert status == b'a', (ofs, self.f.tell(), oid)
size, saved_oid, saved_tid, end_tid = unpack( size, saved_oid, saved_tid, end_tid = unpack(
">I8s8s8s", read(28)) ">I8s8s8s", read(28))
assert saved_oid == oid, (ofs, self.f.tell(), oid, saved_oid) assert saved_oid == oid, (
ofs, self.f.tell(), oid, saved_oid)
assert end_tid == z64, (ofs, self.f.tell(), oid) assert end_tid == z64, (ofs, self.f.tell(), oid)
if saved_tid == start_tid: if saved_tid == start_tid:
return return
raise ValueError("already have current data for oid") raise ValueError("already have current data for oid")
else: else:
noncurrent_for_oid = self.noncurrent.get(u64(oid)) noncurrent_for_oid = self.noncurrent.get(u64(oid))
if noncurrent_for_oid and (u64(start_tid) in noncurrent_for_oid): if noncurrent_for_oid and (
u64(start_tid) in noncurrent_for_oid):
return return
size = allocated_record_overhead + len(data) size = allocated_record_overhead + len(data)
...@@ -696,6 +708,7 @@ class ClientCache(object): ...@@ -696,6 +708,7 @@ class ClientCache(object):
# - tid the id of the transaction that wrote a new revision of oid, # - tid the id of the transaction that wrote a new revision of oid,
# or None to forget all cached info about oid. # or None to forget all cached info about oid.
def invalidate(self, oid, tid): def invalidate(self, oid, tid):
with self._lock:
ofs = self.current.get(oid) ofs = self.current.get(oid)
if ofs is None: if ofs is None:
# 0x10 == invalidate (miss) # 0x10 == invalidate (miss)
...@@ -718,7 +731,8 @@ class ClientCache(object): ...@@ -718,7 +731,8 @@ class ClientCache(object):
self._len -= 1 self._len -= 1
else: else:
if tid == saved_tid: if tid == saved_tid:
logger.warning("Ignoring invalidation with same tid as current") logger.warning(
"Ignoring invalidation with same tid as current")
return return
self.f.seek(ofs+21) self.f.seek(ofs+21)
self.f.write(tid) self.f.write(tid)
......
...@@ -24,8 +24,7 @@ class StaleCache(object): ...@@ -24,8 +24,7 @@ class StaleCache(object):
class IClientCache(zope.interface.Interface): class IClientCache(zope.interface.Interface):
"""Client cache interface. """Client cache interface.
Note that caches need not be thread safe, fpr the most part, Note that caches need to be thread safe.
except for getLastTid, which may be called from multiple threads.
""" """
def close(): def close():
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment