Skip to content

Commit 994cf80

Browse files
committed
PYTHON-1545 Resume more getMore errors for ChangeStreams
1 parent 96291c8 commit 994cf80

File tree

2 files changed

+43
-20
lines changed

2 files changed

+43
-20
lines changed

pymongo/change_stream.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,20 @@
2121
from pymongo import common
2222
from pymongo.collation import validate_collation_or_none
2323
from pymongo.command_cursor import CommandCursor
24-
from pymongo.errors import (ConnectionFailure, CursorNotFound,
25-
InvalidOperation, PyMongoError)
24+
from pymongo.errors import (ConnectionFailure,
25+
InvalidOperation,
26+
OperationFailure,
27+
PyMongoError)
28+
29+
30+
# The change streams spec considers the following server errors from the
31+
# getMore command non-resumable. All other getMore errors are resumable.
32+
_NON_RESUMABLE_GETMORE_ERRORS = frozenset([
33+
11601, # Interrupted
34+
136, # CappedPositionLost
35+
237, # CursorKilled
36+
None, # No error code was returned.
37+
])
2638

2739

2840
class ChangeStream(object):
@@ -144,6 +156,14 @@ def _create_cursor(self):
144156
explicit_session=self._session is not None
145157
)
146158

159+
def _resume(self):
160+
"""Reestablish this change stream after a resumable error."""
161+
try:
162+
self._cursor.close()
163+
except PyMongoError:
164+
pass
165+
self._cursor = self._create_cursor()
166+
147167
def close(self):
148168
"""Close this ChangeStream."""
149169
self._cursor.close()
@@ -162,12 +182,13 @@ def next(self):
162182
while True:
163183
try:
164184
change = self._cursor.next()
165-
except (ConnectionFailure, CursorNotFound):
166-
try:
167-
self._cursor.close()
168-
except PyMongoError:
169-
pass
170-
self._cursor = self._create_cursor()
185+
except ConnectionFailure:
186+
self._resume()
187+
continue
188+
except OperationFailure as exc:
189+
if exc.code in _NON_RESUMABLE_GETMORE_ERRORS:
190+
raise
191+
self._resume()
171192
continue
172193
try:
173194
resume_token = change['_id']

test/test_change_stream.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument
3838

3939
from pymongo import monitoring
40+
from pymongo.change_stream import _NON_RESUMABLE_GETMORE_ERRORS
4041
from pymongo.command_cursor import CommandCursor
4142
from pymongo.errors import (InvalidOperation, OperationFailure,
4243
ServerSelectionTimeoutError)
@@ -317,23 +318,24 @@ def test_resume_on_error(self):
317318
self.client._close_cursor_now(cursor.cursor_id, address)
318319
self.insert_and_check(change_stream, {'_id': 2})
319320

320-
def test_does_not_resume_on_server_error(self):
321-
"""ChangeStream will not attempt to resume on a server error."""
322-
def mock_next(self, *args, **kwargs):
323-
self._CommandCursor__killed = True
324-
raise OperationFailure('Mock server error')
325-
326-
original_next = CommandCursor.next
327-
CommandCursor.next = mock_next
328-
try:
321+
def test_does_not_resume_fatal_errors(self):
322+
"""ChangeStream will not attempt to resume fatal server errors."""
323+
for code in _NON_RESUMABLE_GETMORE_ERRORS:
329324
with self.coll.watch() as change_stream:
325+
self.coll.insert_one({})
326+
327+
def mock_next(*args, **kwargs):
328+
change_stream._cursor.close()
329+
raise OperationFailure('Mock server error', code=code)
330+
331+
original_next = change_stream._cursor.next
332+
change_stream._cursor.next = mock_next
333+
330334
with self.assertRaises(OperationFailure):
331335
next(change_stream)
332-
CommandCursor.next = original_next
336+
change_stream._cursor.next = original_next
333337
with self.assertRaises(StopIteration):
334338
next(change_stream)
335-
finally:
336-
CommandCursor.next = original_next
337339

338340
def test_initial_empty_batch(self):
339341
"""Ensure that a cursor returned from an aggregate command with a

0 commit comments

Comments
 (0)