Skip to content

Commit f9c029e

Browse files
authored
PYTHON-5856 Move cursor execution logic into cursor class (#2894)
1 parent 7ba7481 commit f9c029e

15 files changed

Lines changed: 248 additions & 316 deletions

pymongo/asynchronous/command_cursor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ async def _send_message(self, operation: _GetMore) -> None:
173173
client = self._collection.database.client
174174
try:
175175
response = await client._run_operation(
176-
operation, self._unpack_response, address=self._address
176+
operation, self._run_with_conn, address=self._address
177177
)
178178
except OperationFailure as exc:
179179
if exc.code in _CURSOR_CLOSED_ERRORS:

pymongo/asynchronous/command_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
batches. Pre-encrypted, so decryption is skipped. Callers: ``bulk.py``,
2525
``client_bulk.py``.
2626
- :func:`run_cursor_command` — cursor ``find``/``getMore`` operations with
27-
exhaust-cursor handling. Caller: ``server.py``.
27+
exhaust-cursor handling. Caller: ``cursor_base.py``.
2828
2929
:func:`_run_command` owns the entire shared skeleton: command logging, APM
3030
event publishing, ``send``/``receive``, ``$clusterTime`` gossip,

pymongo/asynchronous/cursor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,7 @@ async def _send_message(self, operation: Union[_Query, _GetMore]) -> None:
980980

981981
try:
982982
response = await client._run_operation(
983-
operation, self._unpack_response, address=self._address
983+
operation, self._run_with_conn, address=self._address
984984
)
985985
except OperationFailure as exc:
986986
if exc.code in _CURSOR_CLOSED_ERRORS or self._exhaust:

pymongo/asynchronous/cursor_base.py

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,40 @@
1717
from __future__ import annotations
1818

1919
from abc import abstractmethod
20-
from typing import TYPE_CHECKING, Any, Optional
20+
from collections.abc import Mapping, Sequence
21+
from typing import TYPE_CHECKING, Any, Optional, Union
2122

2223
from pymongo import _csot
23-
from pymongo.cursor_shared import _AgnosticCursorBase
24+
from pymongo.asynchronous.command_runner import run_cursor_command
25+
from pymongo.asynchronous.helpers import _handle_reauth
26+
from pymongo.cursor_shared import _CURSOR_DOC_FIELDS, _AgnosticCursorBase, _split_message
2427
from pymongo.lock import _async_create_lock
25-
from pymongo.typings import _DocumentType
28+
from pymongo.message import _GetMore, _OpMsg, _Query
29+
from pymongo.response import PinnedResponse, Response
30+
from pymongo.typings import _DocumentOut, _DocumentType
2631

2732
if TYPE_CHECKING:
2833
from pymongo.asynchronous.client_session import AsyncClientSession
2934
from pymongo.asynchronous.pool import AsyncConnection
35+
from pymongo.read_preferences import _ServerMode
3036

3137
_IS_SYNC = False
3238

3339

40+
async def _operation_to_command(
41+
operation: Union[_Query, _GetMore],
42+
conn: AsyncConnection,
43+
apply_timeout: bool,
44+
) -> tuple[dict[str, Any], str]:
45+
cmd, db = operation.as_command(conn, apply_timeout)
46+
if operation.client._encrypter and not operation.client._encrypter._bypass_auto_encryption:
47+
cmd = await operation.client._encrypter.encrypt( # type: ignore[misc, assignment]
48+
operation.db, cmd, operation.codec_options
49+
)
50+
operation.update_command(cmd)
51+
return cmd, db
52+
53+
3454
class _ConnectionManager:
3555
"""Used with exhaust cursors to ensure the connection is returned."""
3656

@@ -66,6 +86,87 @@ def session(self) -> Optional[AsyncClientSession]:
6686
async def _next_batch(self, result: list, total: Optional[int] = None) -> bool: # type: ignore[type-arg]
6787
...
6888

89+
@abstractmethod
90+
def _unpack_response(
91+
self,
92+
response: _OpMsg,
93+
cursor_id: Optional[int],
94+
codec_options: Any,
95+
user_fields: Optional[Mapping[str, Any]] = None,
96+
legacy_response: bool = False,
97+
) -> Sequence[_DocumentOut]: ...
98+
99+
@_handle_reauth
100+
async def _run_with_conn(
101+
self,
102+
conn: AsyncConnection,
103+
operation: Union[_Query, _GetMore],
104+
read_preference: _ServerMode,
105+
) -> Response:
106+
"""Execute a cursor operation on the given connection and return a Response.
107+
108+
:param conn: An AsyncConnection instance.
109+
:param operation: A _Query or _GetMore object.
110+
:param read_preference: The read preference to use.
111+
"""
112+
client = self._collection.database.client
113+
use_cmd = operation.use_command(conn)
114+
more_to_come = bool(operation.conn_mgr and operation.conn_mgr.more_to_come)
115+
cmd, dbn = await _operation_to_command(operation, conn, use_cmd)
116+
if more_to_come:
117+
request_id, data, max_doc_size = 0, b"", 0
118+
else:
119+
message = operation.get_message(read_preference, conn, use_cmd)
120+
request_id, data, max_doc_size = _split_message(message)
121+
user_fields = _CURSOR_DOC_FIELDS if use_cmd else None
122+
docs, reply, duration = await run_cursor_command(
123+
conn,
124+
cmd,
125+
dbn,
126+
request_id,
127+
data,
128+
client=client,
129+
session=operation.session, # type: ignore[arg-type]
130+
listeners=client._event_listeners,
131+
codec_options=operation.codec_options,
132+
user_fields=user_fields,
133+
command_name=operation.name,
134+
pool_opts=conn.opts,
135+
max_doc_size=max_doc_size,
136+
more_to_come=more_to_come,
137+
unpack_res=self._unpack_response,
138+
cursor_id=operation.cursor_id,
139+
)
140+
assert reply is not None
141+
if client._should_pin_cursor(operation.session) or operation.exhaust: # type: ignore[arg-type]
142+
conn.pin_cursor()
143+
if isinstance(reply, _OpMsg):
144+
# In OP_MSG, the server keeps sending only if the more_to_come flag is set.
145+
more_to_come = reply.more_to_come
146+
else:
147+
# In OP_REPLY, the server keeps sending until cursor_id is 0.
148+
more_to_come = bool(operation.exhaust and reply.cursor_id)
149+
if operation.conn_mgr:
150+
operation.conn_mgr.update_exhaust(more_to_come)
151+
return PinnedResponse(
152+
data=reply,
153+
address=conn.address,
154+
conn=conn,
155+
duration=duration,
156+
request_id=request_id,
157+
from_command=use_cmd,
158+
docs=docs, # type: ignore[arg-type]
159+
more_to_come=more_to_come,
160+
)
161+
return Response(
162+
data=reply,
163+
address=conn.address,
164+
duration=duration,
165+
request_id=request_id,
166+
from_command=use_cmd,
167+
docs=docs, # type: ignore[arg-type]
168+
)
169+
69170
async def _die_lock(self) -> None:
70171
"""Closes this cursor."""
71172
try:

pymongo/asynchronous/mongo_client.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,13 +1911,14 @@ async def _conn_for_reads(
19111911
async def _run_operation(
19121912
self,
19131913
operation: Union[_Query, _GetMore],
1914-
unpack_res: Callable, # type: ignore[type-arg]
1914+
run_with_conn: Callable, # type: ignore[type-arg]
19151915
address: Optional[_Address] = None,
19161916
) -> Response:
19171917
"""Run a _Query/_GetMore operation and return a Response.
19181918
19191919
:param operation: a _Query or _GetMore object.
1920-
:param unpack_res: A callable that decodes the wire protocol response.
1920+
:param run_with_conn: A callable ``(conn, operation, read_preference) -> Awaitable[Response]``
1921+
that executes the operation on a given connection.
19211922
:param address: Optional address when sending a message
19221923
to a specific server, used for getMore.
19231924
"""
@@ -1932,30 +1933,18 @@ async def _run_operation(
19321933
async with operation.conn_mgr._lock:
19331934
async with _MongoClientErrorHandler(self, server, operation.session) as err_handler: # type: ignore[arg-type]
19341935
err_handler.contribute_socket(operation.conn_mgr.conn)
1935-
return await server.run_operation(
1936-
operation.conn_mgr.conn,
1937-
operation,
1938-
operation.read_preference,
1939-
self._event_listeners,
1940-
unpack_res,
1941-
self,
1936+
return await run_with_conn(
1937+
operation.conn_mgr.conn, operation, operation.read_preference
19421938
)
19431939

19441940
async def _cmd(
19451941
_session: Optional[AsyncClientSession],
1946-
server: Server,
1942+
_server: Server,
19471943
conn: AsyncConnection,
19481944
read_preference: _ServerMode,
19491945
) -> Response:
19501946
operation.reset() # Reset op in case of retry.
1951-
return await server.run_operation(
1952-
conn,
1953-
operation,
1954-
read_preference,
1955-
self._event_listeners,
1956-
unpack_res,
1957-
self,
1958-
)
1947+
return await run_with_conn(conn, operation, read_preference)
19591948

19601949
return await self._retryable_read(
19611950
_cmd,

pymongo/asynchronous/server.py

Lines changed: 1 addition & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,28 @@
2121
from typing import (
2222
TYPE_CHECKING,
2323
Any,
24-
Callable,
2524
Optional,
26-
Union,
2725
)
2826

29-
from pymongo.asynchronous.command_runner import run_cursor_command
30-
from pymongo.asynchronous.helpers import _handle_reauth
3127
from pymongo.logger import (
3228
_SDAM_LOGGER,
3329
_debug_log,
3430
_SDAMStatusMessage,
3531
)
36-
from pymongo.message import _GetMore, _OpMsg, _Query
37-
from pymongo.response import PinnedResponse, Response
3832

3933
if TYPE_CHECKING:
4034
from queue import Queue
4135
from weakref import ReferenceType
4236

4337
from bson.objectid import ObjectId
44-
from pymongo.asynchronous.mongo_client import AsyncMongoClient, _MongoClientErrorHandler
38+
from pymongo.asynchronous.mongo_client import _MongoClientErrorHandler
4539
from pymongo.asynchronous.monitor import Monitor
4640
from pymongo.asynchronous.pool import AsyncConnection, Pool
4741
from pymongo.monitoring import _EventListeners
48-
from pymongo.read_preferences import _ServerMode
4942
from pymongo.server_description import ServerDescription
50-
from pymongo.typings import _DocumentOut
5143

5244
_IS_SYNC = False
5345

54-
_CURSOR_DOC_FIELDS = {"cursor": {"firstBatch": 1, "nextBatch": 1}}
55-
5646

5747
class Server:
5848
def __init__(
@@ -117,112 +107,6 @@ def request_check(self) -> None:
117107
"""Check the server's state soon."""
118108
self._monitor.request_check()
119109

120-
async def operation_to_command(
121-
self, operation: Union[_Query, _GetMore], conn: AsyncConnection, apply_timeout: bool = False
122-
) -> tuple[dict[str, Any], str]:
123-
cmd, db = operation.as_command(conn, apply_timeout)
124-
# Support auto encryption
125-
if operation.client._encrypter and not operation.client._encrypter._bypass_auto_encryption:
126-
cmd = await operation.client._encrypter.encrypt( # type: ignore[misc, assignment]
127-
operation.db, cmd, operation.codec_options
128-
)
129-
operation.update_command(cmd)
130-
131-
return cmd, db
132-
133-
@_handle_reauth
134-
async def run_operation(
135-
self,
136-
conn: AsyncConnection,
137-
operation: Union[_Query, _GetMore],
138-
read_preference: _ServerMode,
139-
listeners: Optional[_EventListeners],
140-
unpack_res: Callable[..., list[_DocumentOut]],
141-
client: AsyncMongoClient[Any],
142-
) -> Response:
143-
"""Run a _Query or _GetMore operation and return a Response object.
144-
145-
This method is used only to run _Query/_GetMore operations from
146-
cursors.
147-
Can raise ConnectionFailure, OperationFailure, etc.
148-
149-
:param conn: An AsyncConnection instance.
150-
:param operation: A _Query or _GetMore object.
151-
:param read_preference: The read preference to use.
152-
:param listeners: Instance of _EventListeners or None.
153-
:param unpack_res: A callable that decodes the wire protocol response.
154-
:param client: An AsyncMongoClient instance.
155-
"""
156-
assert listeners is not None
157-
158-
use_cmd = operation.use_command(conn)
159-
more_to_come = bool(operation.conn_mgr and operation.conn_mgr.more_to_come)
160-
cmd, dbn = await self.operation_to_command(operation, conn, use_cmd)
161-
if more_to_come:
162-
request_id = 0
163-
data = b""
164-
max_doc_size = 0
165-
else:
166-
message = operation.get_message(read_preference, conn, use_cmd)
167-
request_id, data, max_doc_size = self._split_message(message)
168-
169-
user_fields = _CURSOR_DOC_FIELDS if use_cmd else None
170-
171-
docs, reply, duration = await run_cursor_command(
172-
conn,
173-
cmd,
174-
dbn,
175-
request_id,
176-
data,
177-
client=client,
178-
session=operation.session, # type: ignore[arg-type]
179-
listeners=listeners,
180-
codec_options=operation.codec_options,
181-
user_fields=user_fields,
182-
command_name=operation.name,
183-
pool_opts=conn.opts,
184-
max_doc_size=max_doc_size,
185-
more_to_come=more_to_come,
186-
unpack_res=unpack_res,
187-
cursor_id=operation.cursor_id,
188-
)
189-
assert reply is not None
190-
191-
response: Response
192-
193-
if client._should_pin_cursor(operation.session) or operation.exhaust: # type: ignore[arg-type]
194-
conn.pin_cursor()
195-
if isinstance(reply, _OpMsg):
196-
# In OP_MSG, the server keeps sending only if the
197-
# more_to_come flag is set.
198-
more_to_come = reply.more_to_come
199-
else:
200-
# In OP_REPLY, the server keeps sending until cursor_id is 0.
201-
more_to_come = bool(operation.exhaust and reply.cursor_id)
202-
if operation.conn_mgr:
203-
operation.conn_mgr.update_exhaust(more_to_come)
204-
response = PinnedResponse(
205-
data=reply,
206-
address=self._description.address,
207-
conn=conn,
208-
duration=duration,
209-
request_id=request_id,
210-
from_command=use_cmd,
211-
docs=docs, # type: ignore[arg-type]
212-
more_to_come=more_to_come,
213-
)
214-
else:
215-
response = Response(
216-
data=reply,
217-
address=self._description.address,
218-
duration=duration,
219-
request_id=request_id,
220-
from_command=use_cmd,
221-
docs=docs, # type: ignore[arg-type]
222-
)
223-
224-
return response
225-
226110
async def checkout(
227111
self, handler: Optional[_MongoClientErrorHandler] = None
228112
) -> AbstractAsyncContextManager[AsyncConnection]:
@@ -241,19 +125,5 @@ def description(self, server_description: ServerDescription) -> None:
241125
def pool(self) -> Pool:
242126
return self._pool
243127

244-
def _split_message(
245-
self, message: Union[tuple[int, Any], tuple[int, Any, int]]
246-
) -> tuple[int, Any, int]:
247-
"""Return request_id, data, max_doc_size.
248-
249-
:param message: (request_id, data, max_doc_size) or (request_id, data)
250-
"""
251-
if len(message) == 3:
252-
return message # type: ignore[return-value]
253-
else:
254-
# get_more and kill_cursors messages don't include BSON documents.
255-
request_id, data = message # type: ignore[misc]
256-
return request_id, data, 0
257-
258128
def __repr__(self) -> str:
259129
return f"<{self.__class__.__name__} {self._description!r}>"

0 commit comments

Comments
 (0)