Commit a6087fbb authored by Jim Fulton's avatar Jim Fulton

Fixed a bug in the iterator support. Iterators weren't properly

cleaned up on disconnect.

Also avoid unnecessary iterator_gc calls to the server when there are
no iterators to be cleaned up.
parent 2728a1b5
...@@ -624,7 +624,7 @@ class ClientStorage(object): ...@@ -624,7 +624,7 @@ class ClientStorage(object):
self._ready.clear() self._ready.clear()
self._server = disconnected_stub self._server = disconnected_stub
self._midtxn_disconnect = 1 self._midtxn_disconnect = 1
self._iterator_gc() self._iterator_gc(True)
def __len__(self): def __len__(self):
"""Return the size of the storage.""" """Return the size of the storage."""
...@@ -1380,19 +1380,26 @@ class ClientStorage(object): ...@@ -1380,19 +1380,26 @@ class ClientStorage(object):
self._iterators.pop(iid, None) self._iterators.pop(iid, None)
self._iterator_ids.remove(iid) self._iterator_ids.remove(iid)
def _iterator_gc(self): def _iterator_gc(self, disconnected=False):
if not self._iterator_ids:
return
if disconnected:
for i in self._iterators.values():
i._iid = -1
self._iterators.clear()
self._iterator_ids.clear()
return
iids = self._iterator_ids - set(self._iterators) iids = self._iterator_ids - set(self._iterators)
try: if iids:
self._server.iterator_gc(list(iids)) try:
except ClientDisconnected: self._server.iterator_gc(list(iids))
# We could not successfully garbage-collect iterators. except ClientDisconnected:
# The server might have been restarted, so the IIDs might mean # If we get disconnected, all of the iterators on the
# something different now. We simply forget our unused IIDs to # server are thrown away. We should clear ours too:
# avoid gc'ing foreign iterators. return self._iterator_gc(True)
# In the case that the server was not restarted, we accept the self._iterator_ids -= iids
# risk of leaking resources on the ZEO server.
pass
self._iterator_ids -= iids
class TransactionIterator(object): class TransactionIterator(object):
...@@ -1409,6 +1416,9 @@ class TransactionIterator(object): ...@@ -1409,6 +1416,9 @@ class TransactionIterator(object):
if self._ended: if self._ended:
raise ZODB.interfaces.StorageStopIteration() raise ZODB.interfaces.StorageStopIteration()
if self._iid < 0:
raise ClientDisconnected("Disconnected iterator")
tx_data = self._storage._server.iterator_next(self._iid) tx_data = self._storage._server.iterator_next(self._iid)
if tx_data is None: if tx_data is None:
# The iterator is exhausted, and the server has already # The iterator is exhausted, and the server has already
......
...@@ -99,3 +99,53 @@ class IterationTests: ...@@ -99,3 +99,53 @@ class IterationTests:
self.assertEquals(txn_info1.tid, txn_info2.tid) self.assertEquals(txn_info1.tid, txn_info2.tid)
self.assertRaises(StopIteration, iter1.next) self.assertRaises(StopIteration, iter1.next)
self.assertRaises(StopIteration, iter2.next) self.assertRaises(StopIteration, iter2.next)
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, transaction
>>> db = ZEO.DB(addr)
>>> conn = db.open()
>>> for i in range(10):
... conn.root().i = i
... transaction.commit()
Create an iterator:
>>> it = conn._storage.iterator()
>>> tid1 = it.next().tid
Restart the storage:
>>> stop_server(adminaddr)
>>> wait_disconnected(conn._storage)
>>> _ = start_server('<filestorage>\npath fs\n</filestorage>', addr=addr)
>>> wait_connected(conn._storage)
Now, we'll create a second iterator:
>>> it2 = conn._storage.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()
"""
...@@ -1103,10 +1103,6 @@ slow_test_classes = [ ...@@ -1103,10 +1103,6 @@ slow_test_classes = [
quick_test_classes = [FileStorageRecoveryTests, ConfigurationTests] quick_test_classes = [FileStorageRecoveryTests, ConfigurationTests]
def setUp(test):
ZODB.tests.util.setUp(test)
test.globs['get_port'] = lambda : get_port(test)
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
...@@ -1115,12 +1111,14 @@ def test_suite(): ...@@ -1115,12 +1111,14 @@ def test_suite():
zeo = unittest.TestSuite() zeo = unittest.TestSuite()
zeo.addTest(unittest.makeSuite(ZODB.tests.util.AAAA_Test_Runner_Hack)) zeo.addTest(unittest.makeSuite(ZODB.tests.util.AAAA_Test_Runner_Hack))
zeo.addTest(doctest.DocTestSuite( zeo.addTest(doctest.DocTestSuite(
setUp=setUp, tearDown=zope.testing.setupstack.tearDown)) setUp=forker.setUp, tearDown=zope.testing.setupstack.tearDown))
zeo.addTest(doctest.DocTestSuite(ZEO.tests.IterationTests,
setUp=forker.setUp, tearDown=zope.testing.setupstack.tearDown))
zeo.addTest(doctest.DocFileSuite('registerDB.test')) zeo.addTest(doctest.DocFileSuite('registerDB.test'))
zeo.addTest( zeo.addTest(
doctest.DocFileSuite( doctest.DocFileSuite(
'zeo-fan-out.test', 'zeo-fan-out.test', 'zdoptions.test',
setUp=setUp, tearDown=zope.testing.setupstack.tearDown, setUp=forker.setUp, tearDown=zope.testing.setupstack.tearDown,
), ),
) )
for klass in quick_test_classes: for klass in quick_test_classes:
......
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