Commit bc066b9a authored by Jim Fulton's avatar Jim Fulton

Bugs fixes:

- If a ZEO client process was restarted while invalidating a ZEO cache
  entry, the cache could be left in a stage when there is data marked
  current that should be invalidated, leading to persistent conflict
  errors.

- Corrupted or invalid cache files prevented ZEO clients from
  starting. Now, bad cache files are moved aside.

- Invalidations of object records in ZEO caches, where the
  invalidation transaction ids matched the cached transaction ids
  should have been ignored.
parent 40bbc6b5
...@@ -13,6 +13,18 @@ Bugs Fixed ...@@ -13,6 +13,18 @@ Bugs Fixed
(Thanks to Christian Zagrodnick for chasing this down.) (Thanks to Christian Zagrodnick for chasing this down.)
- If a ZEO client process was restarted while invalidating a ZEO cache
entry, the cache could be left in a stage when there is data marked
current that should be invalidated, leading to persistent conflict
errors.
- Corrupted or invalid cache files prevented ZEO clients from
starting. Now, bad cache files are moved aside.
- Invalidations of object records in ZEO caches, where the
invalidation transaction ids matched the cached transaction ids
should have been ignored.
- On Mac OS X, clients that connected and disconnected quickly could - On Mac OS X, clients that connected and disconnected quickly could
cause a ZEO server to stop accepting connections, due to a failure cause a ZEO server to stop accepting connections, due to a failure
to catch errors in the initial part of the connection process. to catch errors in the initial part of the connection process.
......
...@@ -207,7 +207,24 @@ class ClientCache(object): ...@@ -207,7 +207,24 @@ class ClientCache(object):
self.f.write(magic+z64) self.f.write(magic+z64)
logger.info("created temporary cache file %r", self.f.name) logger.info("created temporary cache file %r", self.f.name)
try:
self._initfile(fsize) self._initfile(fsize)
except:
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, # Statistics: _n_adds, _n_added_bytes,
# _n_evicts, _n_evicted_bytes, # _n_evicts, _n_evicted_bytes,
...@@ -672,6 +689,9 @@ class ClientCache(object): ...@@ -672,6 +689,9 @@ class ClientCache(object):
self._trace(0x1E, oid, tid) self._trace(0x1E, oid, tid)
self._len -= 1 self._len -= 1
else: else:
if tid == saved_tid:
logger.warning("Ignoring invalidation with same tid as current")
return
self.f.seek(ofs+21) self.f.seek(ofs+21)
self.f.write(tid) self.f.write(tid)
self._set_noncurrent(oid, saved_tid, ofs) self._set_noncurrent(oid, saved_tid, ofs)
......
...@@ -488,17 +488,80 @@ __test__ = dict( ...@@ -488,17 +488,80 @@ __test__ = dict(
>>> logger.setLevel(logging.NOTSET) >>> logger.setLevel(logging.NOTSET)
>>> logger.removeHandler(handler) >>> logger.removeHandler(handler)
>>> logger.setLevel(logging.NOTSET)
>>> logger.removeHandler(handler)
>>> cache.close() >>> cache.close()
""", """,
bad_magic_number =
r"""
>>> open('cache', 'w').write("Hi world!")
>>> try: cache = ZEO.cache.ClientCache('cache', 1000)
... except Exception, v: print v
unexpected magic number: 'Hi w'
"""
) )
def invalidations_with_current_tid_dont_wreck_cache():
"""
>>> cache = ZEO.cache.ClientCache('cache', 1000)
>>> cache.store(p64(1), p64(1), None, 'data')
>>> import logging, sys
>>> handler = logging.StreamHandler(sys.stdout)
>>> logging.getLogger().addHandler(handler)
>>> old_level = logging.getLogger().getEffectiveLevel()
>>> logging.getLogger().setLevel(logging.WARNING)
>>> cache.invalidate(p64(1), p64(1))
Ignoring invalidation with same tid as current
>>> cache.close()
>>> cache = ZEO.cache.ClientCache('cache', 1000)
>>> cache.close()
>>> logging.getLogger().removeHandler(handler)
>>> logging.getLogger().setLevel(old_level)
"""
def rename_bad_cache_file():
"""
An attempt to open a bad cache file will cause it to be dropped and recreated.
>>> open('cache', 'w').write('x'*100)
>>> import logging, sys
>>> handler = logging.StreamHandler(sys.stdout)
>>> logging.getLogger().addHandler(handler)
>>> old_level = logging.getLogger().getEffectiveLevel()
>>> logging.getLogger().setLevel(logging.WARNING)
>>> cache = ZEO.cache.ClientCache('cache', 1000) # doctest: +ELLIPSIS
Moving bad cache file to 'cache.bad'.
Traceback (most recent call last):
...
ValueError: unexpected magic number: 'xxxx'
>>> cache.store(p64(1), p64(1), None, 'data')
>>> cache.close()
>>> f = open('cache')
>>> f.seek(0, 2)
>>> f.tell()
1000
>>> f.close()
>>> open('cache', 'w').write('x'*200)
>>> cache = ZEO.cache.ClientCache('cache', 1000) # doctest: +ELLIPSIS
Removing bad cache file: 'cache' (prev bad exists).
Traceback (most recent call last):
...
ValueError: unexpected magic number: 'xxxx'
>>> cache.store(p64(1), p64(1), None, 'data')
>>> cache.close()
>>> f = open('cache')
>>> f.seek(0, 2)
>>> f.tell()
1000
>>> f.close()
>>> f = open('cache.bad')
>>> f.seek(0, 2)
>>> f.tell()
100
>>> f.close()
>>> logging.getLogger().removeHandler(handler)
>>> logging.getLogger().setLevel(old_level)
"""
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(CacheTests)) suite.addTest(unittest.makeSuite(CacheTests))
......
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