Skip to content

PYTHON-4008 Support exhaust cursors on mongos 7.1+ - #2985

Open
carsontung666 wants to merge 1 commit into
mongodb:mainfrom
carsontung666:PYTHON-4008-mongos-exhaust
Open

PYTHON-4008 Support exhaust cursors on mongos 7.1+#2985
carsontung666 wants to merge 1 commit into
mongodb:mainfrom
carsontung666:PYTHON-4008-mongos-exhaust

Conversation

@carsontung666

Copy link
Copy Markdown

PYTHON-4008

Changes in this PR

PyMongo refuses an exhaust cursor whenever the client is connected to a mongos:

if self._collection.database.client.is_mongos:
    raise InvalidOperation("Exhaust cursors are not supported by mongos")

That check dates to 2.6, when exhaust was an OP_QUERY flag and mongos rejected it outright. PYTHON-1636 carried it across to OP_MSG in 4.0 unchanged. mongos has served exhaust getMore since 7.1, SERVER-57297.

  • MONGOS_EXHAUST_WIRE_VERSION = 22 in pymongo/common.py. libmongoc uses the same value.
  • _check_exhaust_supported(conn) in pymongo/message.py, called from _Query.use_command and _GetMore.use_command. Both already gate exhaust on conn.max_wire_version, and both run before anything reaches the wire.
  • Cursor._supports_exhaust and Cursor.add_option keep only their limit check.

The check reads the connection, not a freshly selected server. A Sharded topology can hold mongoses of different versions during a rolling upgrade.

Load-balanced deployments are left ungated. The connection reports SERVER_TYPE.LoadBalancer there, so the old check never fired either, and _should_pin_cursor pins every LB cursor anyway. An older mongos costs nothing extra, and refusing would break clients that work today.

Two timing changes, both in the changelog. Against a pre-7.1 mongos the InvalidOperation now comes on first iteration instead of from find(), because the check needs a connection. Async limit with EXHAUST now raises at find() instead of on first iteration, matching sync, because that check no longer needs I/O.

find() documents the limits. The saving is on the router-to-client leg only. The connection is monopolized for the cursor's lifetime. Nothing steers selection toward a 7.1+ mongos, so mid-upgrade the same call may succeed or raise depending on routing.

A pre-7.1 mongos does not hang a client that asks for exhaust. exhaustAllowed is an optional OP_MSG flag, so it is ignored and the cursor falls back to ordinary getMores. The refusal is because it would hold a connection for no benefit.

Motivation: on a workload streaming thousands of documents through mongos, 40-45% of wall time is the client blocked between getMore round trips. Exhaust makes those reads about 23% faster on a single shard, and about 19% on two shards, where the merge leg does not use exhaust.

Test Plan

  • Added unit tests in test/test_message.py for the wire version boundary and both use_command call sites. Mutation-tested: removing the version comparison, removing the is_mongos condition, or deleting either call site each fails the suite.
  • The exhaust tests gated with require_no_mongos purely because of this restriction now run under require_exhaust_cursors. They pass against a two-shard cluster fronted by a 9.0.0-alpha0 mongos, where they previously skipped, and against a standalone of the same build.
  • An exhaust cursor raises InvalidOperation on main and streams the full result set through the same mongos on this branch.
  • tools/synchro.py reports no drift. ruff, format and mypy pass.

Not run: test_encryption.py, for lack of an encryption environment, and the Evergreen matrix. That includes the load_balancer suite, which these gate changes newly activate. The value 22 is not pinned by a test. Doing that needs a pre-7.1 mongos.

Checklist

Checklist for Author

  • Did you update the changelog (if necessary)?
  • Is there test coverage?
  • Is any followup work tracked in a JIRA ticket? If so, add link(s). DRIVERS-3231 tracks the cross-driver spec follow-up.

Checklist for Reviewer

  • Does the title of the PR reference a JIRA Ticket?
  • Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?)
  • Is all relevant documentation (README or docstring) updated?

AI assistance disclosure

I used Claude Opus 5 to help write and review this change. I reviewed and understand every changed line and can explain and maintain the contribution.

@carsontung666
carsontung666 requested a review from a team as a code owner August 13, 2026 16:12
@blink1073

Copy link
Copy Markdown
Member

Thanks @carsontung666! I will review the PR tomorrow or Monday and get back to you 😄.

Comment thread pymongo/synchronous/collection.py
Comment thread pymongo/message.py Outdated
Comment thread pymongo/message.py Outdated
Comment thread test/test_message.py
Comment thread pymongo/asynchronous/cursor.py
Gate the refusal on wire version instead of on server type. mongos has served
exhaust getMore since 7.1 (SERVER-57297); the categorical check dates to 2.6
(4d42258, 2013), when exhaust was an OP_QUERY flag. The check is made against
the connection in use, in _Query.use_command and _GetMore.use_command, since a
sharded topology can hold mongoses of different versions.
@carsontung666
carsontung666 force-pushed the PYTHON-4008-mongos-exhaust branch from 3a65b63 to 213fc64 Compare August 14, 2026 13:29
@blink1073

Copy link
Copy Markdown
Member

We're getting close! One thing I released - test_cursors.py is not actually run against our load balancer tests. We use the load_balancer pytest marker, which only runs test_load_balancer.py.

  • supports_exhaust_cursors() needs if self.load_balancer: return True. Right now it reports False for any LB deployment, because client_context.is_mongos is True under LB, so 7.0 skips even though _check_exhaust_supported would allow it through.
  • Add an exhaust test to test_load_balancer.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants