Skip to content

Commit 5dba74c

Browse files
committed
PYTHON-952 - killCursors monitoring
1 parent 8ed682b commit 5dba74c

File tree

5 files changed

+83
-10
lines changed

5 files changed

+83
-10
lines changed

pymongo/command_cursor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from bson.py3compat import integer_types
2222
from pymongo import helpers, monitoring
2323
from pymongo.errors import AutoReconnect, NotMasterError, OperationFailure
24-
from pymongo.message import _GetMore
24+
from pymongo.message import _CursorAddress, _GetMore
2525

2626

2727
class CommandCursor(object):
@@ -52,8 +52,8 @@ def __die(self):
5252
"""Closes this cursor.
5353
"""
5454
if self.__id and not self.__killed:
55-
self.__collection.database.client.close_cursor(self.__id,
56-
self.__address)
55+
self.__collection.database.client.close_cursor(
56+
self.__id, _CursorAddress(self.__address, self.__ns))
5757
self.__killed = True
5858

5959
def close(self):

pymongo/cursor.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
InvalidOperation,
3333
NotMasterError,
3434
OperationFailure)
35-
from pymongo.message import _GetMore, _Query
35+
from pymongo.message import _CursorAddress, _GetMore, _Query
3636
from pymongo.read_preferences import ReadPreference
3737

3838
_QUERY_OPTIONS = {
@@ -269,8 +269,10 @@ def __die(self):
269269
# to stop the server from sending more data.
270270
self.__exhaust_mgr.sock.close()
271271
else:
272-
self.__collection.database.client.close_cursor(self.__id,
273-
self.__address)
272+
self.__collection.database.client.close_cursor(
273+
self.__id,
274+
_CursorAddress(
275+
self.__address, self.__collection.full_name))
274276
if self.__exhaust and self.__exhaust_mgr:
275277
self.__exhaust_mgr.close()
276278
self.__killed = True

pymongo/message.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,20 @@ def get_message(self, dummy0, dummy1):
219219
return get_more(self.ns, self.ntoreturn, self.cursor_id)
220220

221221

222+
class _CursorAddress(tuple):
223+
"""The server address (host, port) of a cursor, with namespace property."""
224+
225+
def __new__(cls, address, namespace):
226+
self = tuple.__new__(cls, address)
227+
self.__namespace = namespace
228+
return self
229+
230+
@property
231+
def namespace(self):
232+
"""The namespace this cursor."""
233+
return self.__namespace
234+
235+
222236
def __last_error(namespace, args):
223237
"""Data to send to do a lastError.
224238
"""

pymongo/mongo_client.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@
4040

4141
from bson.py3compat import (integer_types,
4242
string_type)
43+
from bson.son import SON
4344
from pymongo import (common,
4445
database,
4546
message,
47+
monitoring,
4648
periodic_executor,
4749
uri_parser)
4850
from pymongo.client_options import ClientOptions
@@ -910,6 +912,7 @@ def _process_kill_cursors_queue(self):
910912

911913
# Don't re-open topology if it's closed and there's no pending cursors.
912914
if address_to_cursor_ids:
915+
publish = monitoring.enabled()
913916
topology = self._get_topology()
914917
for address, cursor_ids in address_to_cursor_ids.items():
915918
try:
@@ -920,8 +923,28 @@ def _process_kill_cursors_queue(self):
920923
server = topology.select_server(
921924
writable_server_selector)
922925

923-
server.send_message(message.kill_cursors(cursor_ids),
924-
self.__all_credentials)
926+
if publish:
927+
start = datetime.datetime.now()
928+
data = message.kill_cursors(cursor_ids)
929+
if publish:
930+
duration = datetime.datetime.now() - start
931+
try:
932+
dbname, collname = address.namespace.split(".", 1)
933+
except AttributeError:
934+
dbname = collname = 'OP_KILL_CURSORS'
935+
command = SON([('killCursors', collname),
936+
('cursors', cursor_ids)])
937+
monitoring.publish_command_start(
938+
command, dbname, data[0], address)
939+
start = datetime.datetime.now()
940+
server.send_message(data, self.__all_credentials)
941+
if publish:
942+
duration = (datetime.datetime.now() - start) + duration
943+
# OP_KILL_CURSORS returns no reply, fake one.
944+
reply = {'cursorsUnknown': cursor_ids, 'ok': 1}
945+
monitoring.publish_command_success(
946+
duration, reply, 'killCursors', data[0], address)
947+
925948
except ConnectionFailure as exc:
926949
warnings.warn("couldn't close cursor on %s: %s"
927950
% (address, exc))

test/test_monitoring.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
# limitations under the License.
1414

1515
import sys
16+
import time
1617

1718
sys.path[0:0] = [""]
1819

1920
from bson.son import SON
20-
from pymongo import CursorType, MongoClient, monitoring
21+
from pymongo import CursorType, monitoring
2122
from pymongo.command_cursor import CommandCursor
2223
from pymongo.errors import NotMasterError, OperationFailure
23-
from test import unittest, IntegrationTest, client_context
24+
from test import unittest, IntegrationTest, client_context, client_knobs
2425
from test.utils import single_client
2526

2627

@@ -421,6 +422,39 @@ def test_exhaust(self):
421422
'ok': 1}
422423
self.assertEqual(expected_result, succeeded.reply)
423424

425+
def test_kill_cursors(self):
426+
with client_knobs(kill_cursor_frequency=0.01):
427+
self.client.pymongo_test.test.drop()
428+
self.client.pymongo_test.test.insert_many([{} for _ in range(10)])
429+
cursor = self.client.pymongo_test.test.find().batch_size(5)
430+
next(cursor)
431+
cursor_id = cursor.cursor_id
432+
self.listener.results = {}
433+
cursor.close()
434+
time.sleep(2)
435+
results = self.listener.results
436+
started = results.get('started')
437+
succeeded = results.get('succeeded')
438+
self.assertIsNone(results.get('failed'))
439+
self.assertTrue(
440+
isinstance(started, monitoring.CommandStartedEvent))
441+
# There could be more than one cursor_id here depending on
442+
# when the thread last ran.
443+
self.assertIn(cursor_id, started.command['cursors'])
444+
self.assertEqual('killCursors', started.command_name)
445+
self.assertEqual(cursor.address, started.connection_id)
446+
self.assertEqual('pymongo_test', started.database_name)
447+
self.assertTrue(isinstance(started.request_id, int))
448+
self.assertTrue(
449+
isinstance(succeeded, monitoring.CommandSucceededEvent))
450+
self.assertTrue(isinstance(succeeded.duration_micros, int))
451+
self.assertEqual('killCursors', succeeded.command_name)
452+
self.assertTrue(isinstance(succeeded.request_id, int))
453+
self.assertEqual(cursor.address, succeeded.connection_id)
454+
# There could be more than one cursor_id here depending on
455+
# when the thread last ran.
456+
self.assertIn(cursor_id, succeeded.reply['cursorsUnknown'])
457+
424458

425459
if __name__ == "__main__":
426460
unittest.main()

0 commit comments

Comments
 (0)