-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Major - P3
-
None
-
Affects Version/s: None
-
Component/s: None
-
None
-
Python Drivers
-
None
-
None
-
None
-
None
-
None
-
None
Summary: No CommandFailedEvent (and no "Command failed" log message) when an async operation is cancelled
Description
When an asyncio task is cancelled while a command waits for its reply — task.cancel(), asyncio.timeout() / asyncio.wait_for(), a failing TaskGroup sibling, or an ASGI server cancelling a disconnected client's request — PyMongo publishes the CommandStartedEvent but never a correlating CommandSucceededEvent or CommandFailedEvent. The structured "Command started" log message likewise gets no "Command failed" message.
The Command Logging and Monitoring spec, section "Guarantees":
The driver MUST guarantee that every CommandStartedEvent has either a correlating CommandSucceededEvent or CommandFailedEvent, and that every "command started" log message has either a correlating "command succeeded" log message or "command failed" log message.
Cause: _run_command in pymongo/asynchronous/command_runner.py publishes the failure (telemetry.failed(...)) under except Exception as exc:. Since Python 3.8 asyncio.CancelledError derives from BaseException, so that handler never runs for a cancellation. The layer below does handle it — AsyncConnection.command() / receive_message() catch BaseException ("Catch socket.error, KeyboardInterrupt, CancelledError, etc. and close ourselves"), and the CMAP events ConnectionChecked and ConnectionClosed are published — so only command monitoring and command logging lose the outcome. The same except Exception is in pymongo/synchronous/command_runner.py (there it skips e.g. KeyboardInterrupt), in 4.13.0's pymongo/asynchronous/network.py, and on master as of 2026-10-26.
In asyncio code a cancellation is routine: every client that disconnects from an ASGI app cancels its request's queries. A CommandListener that tracks commands in flight (latency, stall detection, tracing spans) therefore keeps entries for commands that are long gone and reports them as still running. We hit this in production: a stall detector built on CommandListener reported a find as waiting for 62 s while the server answered a ping in 2 ms — the find had been cancelled when its HTTP client went away.
Environment: PyMongo 4.19.0 (C extensions), CPython 3.13.5, Linux. MongoDB 9.0.2 (local) and Atlas 9.0.4.
Suggested fix: in _run_command (async and sync), publish the failure for any BaseExceptionand re-raise, as the connection code already does. Precedent for the guarantee: PYTHON-976 (fixed in 3.1, "Publish CommandFailedEvent for arbitrary failures").
Workaround for others: a cancelled command's connection is checked in (and closed) in the same task, so a ConnectionPoolListener.connection_checked_in can end the in-flight commands of asyncio.current_task() — a task waits for one command at a time.