diff --git a/doc/faq.rst b/doc/faq.rst index 6c313d5bb..0aa200a50 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -98,11 +98,11 @@ For `Twisted `_, see `TxMongo `_. Compared to PyMongo, TxMongo is less stable, lacks features, and is less actively maintained. -What does *OperationFailure* cursor id not valid at server mean? ----------------------------------------------------------------- +What does *CursorNotFound* cursor id not valid at server mean? +-------------------------------------------------------------- Cursors in MongoDB can timeout on the server if they've been open for a long time without any operations being performed on them. This can -lead to an :class:`~pymongo.errors.OperationFailure` exception being +lead to an :class:`~pymongo.errors.CursorNotFound` exception being raised when attempting to iterate the cursor. How do I change the timeout value for cursors? diff --git a/pymongo/command_cursor.py b/pymongo/command_cursor.py index 04b9ffc10..b4481b4a7 100644 --- a/pymongo/command_cursor.py +++ b/pymongo/command_cursor.py @@ -17,7 +17,7 @@ from collections import deque from pymongo import helpers, message -from pymongo.errors import AutoReconnect +from pymongo.errors import AutoReconnect, CursorNotFound class CommandCursor(object): @@ -107,6 +107,9 @@ class CommandCursor(object): response = helpers._unpack_response(response, self.__id, *self.__decode_opts) + except CursorNotFound: + self.__killed = True + raise except AutoReconnect: # Don't send kill cursors to another server after a "not master" # error. It's completely pointless. diff --git a/pymongo/cursor.py b/pymongo/cursor.py index 5551d09c9..714ce7d6a 100644 --- a/pymongo/cursor.py +++ b/pymongo/cursor.py @@ -21,8 +21,9 @@ from bson.code import Code from bson.son import SON from pymongo import helpers, message, read_preferences from pymongo.read_preferences import ReadPreference, secondary_ok_commands -from pymongo.errors import (InvalidOperation, - AutoReconnect) +from pymongo.errors import (AutoReconnect, + CursorNotFound, + InvalidOperation) _QUERY_OPTIONS = { "tailable_cursor": 2, @@ -896,6 +897,15 @@ class Cursor(object): self.__tz_aware, self.__uuid_subtype, self.__compile_re) + except CursorNotFound: + self.__killed = True + # If this is a tailable cursor the error is likely + # due to capped collection roll over. Setting + # self.__killed to True ensures Cursor.alive will be + # False. No need to re-raise. + if self.__query_flags & _QUERY_OPTIONS["tailable_cursor"]: + return + raise except AutoReconnect: # Don't send kill cursors to another server after a "not master" # error. It's completely pointless. diff --git a/pymongo/errors.py b/pymongo/errors.py index 24e75d566..b4541a555 100644 --- a/pymongo/errors.py +++ b/pymongo/errors.py @@ -88,6 +88,14 @@ class OperationFailure(PyMongoError): return self.__details +class CursorNotFound(OperationFailure): + """Raised while iterating query results if the cursor is + invalidated on the server. + + .. versionadded:: 2.7 + """ + + class ExecutionTimeout(OperationFailure): """Raised when a database operation times out, exceeding the $maxTimeMS set in the query or command option. diff --git a/pymongo/helpers.py b/pymongo/helpers.py index 87569590e..0ecd1f479 100644 --- a/pymongo/helpers.py +++ b/pymongo/helpers.py @@ -23,6 +23,7 @@ import pymongo from bson.binary import OLD_UUID_SUBTYPE from bson.son import SON from pymongo.errors import (AutoReconnect, + CursorNotFound, DuplicateKeyError, OperationFailure, ExecutionTimeout, @@ -92,8 +93,8 @@ def _unpack_response(response, cursor_id=None, as_class=dict, # Shouldn't get this response if we aren't doing a getMore assert cursor_id is not None - raise OperationFailure("cursor id '%s' not valid at server" % - cursor_id) + raise CursorNotFound("cursor id '%s' not valid at server" % + cursor_id) elif response_flag & 2: error_object = bson.BSON(response[20:]).decode() if error_object["$err"].startswith("not master"): diff --git a/test/test_cursor.py b/test/test_cursor.py index 8151681bf..39c91b4bc 100644 --- a/test/test_cursor.py +++ b/test/test_cursor.py @@ -921,33 +921,44 @@ class TestCursor(unittest.TestCase): def test_tailable(self): db = self.db db.drop_collection("test") - db.create_collection("test", capped=True, size=1000) + db.create_collection("test", capped=True, size=1000, max=3) - cursor = db.test.find(tailable=True) + try: + cursor = db.test.find(tailable=True) - db.test.insert({"x": 1}) - count = 0 - for doc in cursor: - count += 1 - self.assertEqual(1, doc["x"]) - self.assertEqual(1, count) + db.test.insert({"x": 1}) + count = 0 + for doc in cursor: + count += 1 + self.assertEqual(1, doc["x"]) + self.assertEqual(1, count) - db.test.insert({"x": 2}) - count = 0 - for doc in cursor: - count += 1 - self.assertEqual(2, doc["x"]) - self.assertEqual(1, count) + db.test.insert({"x": 2}) + count = 0 + for doc in cursor: + count += 1 + self.assertEqual(2, doc["x"]) + self.assertEqual(1, count) - db.test.insert({"x": 3}) - count = 0 - for doc in cursor: - count += 1 - self.assertEqual(3, doc["x"]) - self.assertEqual(1, count) + db.test.insert({"x": 3}) + count = 0 + for doc in cursor: + count += 1 + self.assertEqual(3, doc["x"]) + self.assertEqual(1, count) - self.assertEqual(3, db.test.count()) - db.drop_collection("test") + # Capped rollover - the collection can never + # have more than 3 documents. Just make sure + # this doesn't raise... + db.test.insert(({"x": i} for i in xrange(4, 7))) + self.assertEqual(0, len(list(cursor))) + + # and that the cursor doesn't think it's still alive. + self.assertFalse(cursor.alive) + + self.assertEqual(3, db.test.count()) + finally: + db.drop_collection("test") def test_distinct(self): if not version.at_least(self.db.connection, (1, 1, 3, 1)):