Skip to content

Commit e3c809b

Browse files
committed
Gracefully kill cursor on capped rollover PYTHON-637
This change does a few things: - Raises a new exception for CursorNotFound, inheriting from OperationFailure so we don't break existing code. - Catches the exception in cursor.Cursor and command_cursor.CommandCursor, setting __killed to True. - If the cursor is not tailable, re-raises the exception. This makes it easier to deal with capped collection rollover when iterating a tailable cursor.
1 parent 940d73f commit e3c809b

File tree

6 files changed

+63
-30
lines changed

6 files changed

+63
-30
lines changed

doc/faq.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ For `Twisted <http://twistedmatrix.com/>`_, see `TxMongo
9898
<http://github.com/fiorix/mongo-async-python-driver>`_. Compared to PyMongo,
9999
TxMongo is less stable, lacks features, and is less actively maintained.
100100

101-
What does *OperationFailure* cursor id not valid at server mean?
102-
----------------------------------------------------------------
101+
What does *CursorNotFound* cursor id not valid at server mean?
102+
--------------------------------------------------------------
103103
Cursors in MongoDB can timeout on the server if they've been open for
104104
a long time without any operations being performed on them. This can
105-
lead to an :class:`~pymongo.errors.OperationFailure` exception being
105+
lead to an :class:`~pymongo.errors.CursorNotFound` exception being
106106
raised when attempting to iterate the cursor.
107107

108108
How do I change the timeout value for cursors?

pymongo/command_cursor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from collections import deque
1818

1919
from pymongo import helpers, message
20-
from pymongo.errors import AutoReconnect
20+
from pymongo.errors import AutoReconnect, CursorNotFound
2121

2222

2323
class CommandCursor(object):
@@ -107,6 +107,9 @@ def __send_message(self, msg):
107107
response = helpers._unpack_response(response,
108108
self.__id,
109109
*self.__decode_opts)
110+
except CursorNotFound:
111+
self.__killed = True
112+
raise
110113
except AutoReconnect:
111114
# Don't send kill cursors to another server after a "not master"
112115
# error. It's completely pointless.

pymongo/cursor.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
from bson.son import SON
2222
from pymongo import helpers, message, read_preferences
2323
from pymongo.read_preferences import ReadPreference, secondary_ok_commands
24-
from pymongo.errors import (InvalidOperation,
25-
AutoReconnect)
24+
from pymongo.errors import (AutoReconnect,
25+
CursorNotFound,
26+
InvalidOperation)
2627

2728
_QUERY_OPTIONS = {
2829
"tailable_cursor": 2,
@@ -896,6 +897,15 @@ def __send_message(self, message):
896897
self.__tz_aware,
897898
self.__uuid_subtype,
898899
self.__compile_re)
900+
except CursorNotFound:
901+
self.__killed = True
902+
# If this is a tailable cursor the error is likely
903+
# due to capped collection roll over. Setting
904+
# self.__killed to True ensures Cursor.alive will be
905+
# False. No need to re-raise.
906+
if self.__query_flags & _QUERY_OPTIONS["tailable_cursor"]:
907+
return
908+
raise
899909
except AutoReconnect:
900910
# Don't send kill cursors to another server after a "not master"
901911
# error. It's completely pointless.

pymongo/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ def details(self):
8888
return self.__details
8989

9090

91+
class CursorNotFound(OperationFailure):
92+
"""Raised while iterating query results if the cursor is
93+
invalidated on the server.
94+
95+
.. versionadded:: 2.7
96+
"""
97+
98+
9199
class ExecutionTimeout(OperationFailure):
92100
"""Raised when a database operation times out, exceeding the $maxTimeMS
93101
set in the query or command option.

pymongo/helpers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from bson.binary import OLD_UUID_SUBTYPE
2424
from bson.son import SON
2525
from pymongo.errors import (AutoReconnect,
26+
CursorNotFound,
2627
DuplicateKeyError,
2728
OperationFailure,
2829
ExecutionTimeout,
@@ -92,8 +93,8 @@ def _unpack_response(response, cursor_id=None, as_class=dict,
9293
# Shouldn't get this response if we aren't doing a getMore
9394
assert cursor_id is not None
9495

95-
raise OperationFailure("cursor id '%s' not valid at server" %
96-
cursor_id)
96+
raise CursorNotFound("cursor id '%s' not valid at server" %
97+
cursor_id)
9798
elif response_flag & 2:
9899
error_object = bson.BSON(response[20:]).decode()
99100
if error_object["$err"].startswith("not master"):

test/test_cursor.py

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -921,33 +921,44 @@ def test_get_more(self):
921921
def test_tailable(self):
922922
db = self.db
923923
db.drop_collection("test")
924-
db.create_collection("test", capped=True, size=1000)
924+
db.create_collection("test", capped=True, size=1000, max=3)
925925

926-
cursor = db.test.find(tailable=True)
926+
try:
927+
cursor = db.test.find(tailable=True)
927928

928-
db.test.insert({"x": 1})
929-
count = 0
930-
for doc in cursor:
931-
count += 1
932-
self.assertEqual(1, doc["x"])
933-
self.assertEqual(1, count)
929+
db.test.insert({"x": 1})
930+
count = 0
931+
for doc in cursor:
932+
count += 1
933+
self.assertEqual(1, doc["x"])
934+
self.assertEqual(1, count)
934935

935-
db.test.insert({"x": 2})
936-
count = 0
937-
for doc in cursor:
938-
count += 1
939-
self.assertEqual(2, doc["x"])
940-
self.assertEqual(1, count)
936+
db.test.insert({"x": 2})
937+
count = 0
938+
for doc in cursor:
939+
count += 1
940+
self.assertEqual(2, doc["x"])
941+
self.assertEqual(1, count)
941942

942-
db.test.insert({"x": 3})
943-
count = 0
944-
for doc in cursor:
945-
count += 1
946-
self.assertEqual(3, doc["x"])
947-
self.assertEqual(1, count)
943+
db.test.insert({"x": 3})
944+
count = 0
945+
for doc in cursor:
946+
count += 1
947+
self.assertEqual(3, doc["x"])
948+
self.assertEqual(1, count)
948949

949-
self.assertEqual(3, db.test.count())
950-
db.drop_collection("test")
950+
# Capped rollover - the collection can never
951+
# have more than 3 documents. Just make sure
952+
# this doesn't raise...
953+
db.test.insert(({"x": i} for i in xrange(4, 7)))
954+
self.assertEqual(0, len(list(cursor)))
955+
956+
# and that the cursor doesn't think it's still alive.
957+
self.assertFalse(cursor.alive)
958+
959+
self.assertEqual(3, db.test.count())
960+
finally:
961+
db.drop_collection("test")
951962

952963
def test_distinct(self):
953964
if not version.at_least(self.db.connection, (1, 1, 3, 1)):

0 commit comments

Comments
 (0)