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)):