From 409468ef7db34ff16a9a38d987304b7d81fb824f Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 4 Jun 2026 11:50:50 -0400 Subject: [PATCH 1/2] PYTHON-5809 - Send afterClusterTime on writes in causally-consistent sessions --- .evergreen/resync-specs.sh | 3 + pymongo/asynchronous/client_session.py | 19 +- pymongo/synchronous/client_session.py | 19 +- test/asynchronous/test_causal_consistency.py | 33 + test/asynchronous/test_session.py | 47 +- test/asynchronous/unified_format.py | 6 + .../causal-consistency-clientBulkWrite.json | 151 ++ .../causal-consistency-write-commands.json | 1396 +++++++++++++++++ test/test_causal_consistency.py | 33 + test/test_session.py | 39 +- .../unified/callback-aborts.json | 8 +- .../unified/callback-commits.json | 8 +- test/transactions/unified/commit.json | 8 +- .../unified/retryable-writes.json | 8 +- test/unified_format.py | 6 + tools/synchro.py | 1 + 16 files changed, 1689 insertions(+), 96 deletions(-) create mode 100644 test/asynchronous/test_causal_consistency.py create mode 100644 test/causal_consistency/causal-consistency-clientBulkWrite.json create mode 100644 test/causal_consistency/causal-consistency-write-commands.json create mode 100644 test/test_causal_consistency.py diff --git a/.evergreen/resync-specs.sh b/.evergreen/resync-specs.sh index 4bb9c86304..5fa5b3e56d 100755 --- a/.evergreen/resync-specs.sh +++ b/.evergreen/resync-specs.sh @@ -117,6 +117,9 @@ do apm|APM|command-monitoring|command_monitoring) cpjson command-logging-and-monitoring/tests/monitoring command_monitoring ;; + causal|causal-consistency|causal_consistency) + cpjson causal-consistency/tests/ causal_consistency + ;; command-logging|command_logging) cpjson command-logging-and-monitoring/tests/logging command_logging ;; diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index 9fbd10c8e9..038b20604d 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -1111,7 +1111,24 @@ def _apply_to( return self._check_ended() self._materialize(conn.logical_session_timeout_minutes) - if self.options.snapshot: + # Add afterClusterTime on writes in casually-consistent sessions + if self.options.snapshot or ( + self.options.causal_consistency + and not self.in_transaction + and operation + in [ + "insert", + "update", + "findAndModify", + "delete", + "bulkWrite", + "create", + "createIndexes", + "drop", + "dropDatabase", + "dropIndexes", + ] + ): self._update_read_concern(command, conn) self._server_session.last_use = time.monotonic() diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 7563850843..962135b539 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -1107,7 +1107,24 @@ def _apply_to( return self._check_ended() self._materialize(conn.logical_session_timeout_minutes) - if self.options.snapshot: + # Add afterClusterTime on writes in casually-consistent sessions + if self.options.snapshot or ( + self.options.causal_consistency + and not self.in_transaction + and operation + in [ + "insert", + "update", + "findAndModify", + "delete", + "bulkWrite", + "create", + "createIndexes", + "drop", + "dropDatabase", + "dropIndexes", + ] + ): self._update_read_concern(command, conn) self._server_session.last_use = time.monotonic() diff --git a/test/asynchronous/test_causal_consistency.py b/test/asynchronous/test_causal_consistency.py new file mode 100644 index 0000000000..e802b5244e --- /dev/null +++ b/test/asynchronous/test_causal_consistency.py @@ -0,0 +1,33 @@ +# Copyright 2021-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test the causal consistency unified spec tests.""" +from __future__ import annotations + +import os +import pathlib +import sys + +sys.path[0:0] = [""] + +from test import unittest +from test.asynchronous.unified_format import generate_test_classes, get_test_path + +_IS_SYNC = False + +# Generate unified tests. +globals().update(generate_test_classes(get_test_path("causal_consistency"), module=__name__)) + +if __name__ == "__main__": + unittest.main() diff --git a/test/asynchronous/test_session.py b/test/asynchronous/test_session.py index 13ce578671..c5e51fa2f2 100644 --- a/test/asynchronous/test_session.py +++ b/test/asynchronous/test_session.py @@ -1138,52 +1138,7 @@ async def _test_no_read_concern(self, op): self.assertIsNone(rc) @async_client_context.require_no_standalone - async def test_writes_do_not_include_read_concern(self): - await self._test_no_read_concern( - lambda coll, session: coll.bulk_write([InsertOne[dict]({})], session=session) - ) - await self._test_no_read_concern(lambda coll, session: coll.insert_one({}, session=session)) - await self._test_no_read_concern( - lambda coll, session: coll.insert_many([{}], session=session) - ) - await self._test_no_read_concern( - lambda coll, session: coll.replace_one({"_id": 1}, {"x": 1}, session=session) - ) - await self._test_no_read_concern( - lambda coll, session: coll.update_one({}, {"$set": {"X": 1}}, session=session) - ) - await self._test_no_read_concern( - lambda coll, session: coll.update_many({}, {"$set": {"x": 1}}, session=session) - ) - await self._test_no_read_concern(lambda coll, session: coll.delete_one({}, session=session)) - await self._test_no_read_concern( - lambda coll, session: coll.delete_many({}, session=session) - ) - await self._test_no_read_concern( - lambda coll, session: coll.find_one_and_replace({"x": 1}, {"y": 1}, session=session) - ) - await self._test_no_read_concern( - lambda coll, session: coll.find_one_and_update( - {"y": 1}, {"$set": {"x": 1}}, session=session - ) - ) - await self._test_no_read_concern( - lambda coll, session: coll.find_one_and_delete({"x": 1}, session=session) - ) - await self._test_no_read_concern( - lambda coll, session: coll.create_index("foo", session=session) - ) - await self._test_no_read_concern( - lambda coll, session: coll.create_indexes( - [IndexModel([("bar", ASCENDING)])], session=session - ) - ) - await self._test_no_read_concern( - lambda coll, session: coll.drop_index("foo_1", session=session) - ) - await self._test_no_read_concern(lambda coll, session: coll.drop_indexes(session=session)) - - # Not a write, but explain also doesn't support readConcern. + async def test_explain_does_not_include_read_concern(self): await self._test_no_read_concern( lambda coll, session: coll.find({}, session=session).explain() ) diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 1fb93e7b86..79ec7150a0 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -761,6 +761,12 @@ async def _databaseOperation_createChangeStream(self, target, *args, **kwargs): async def _collectionOperation_createChangeStream(self, target, *args, **kwargs): return await self.__entityOperation_createChangeStream(target, *args, **kwargs) + async def _clientOperation_dropDatabase(self, target, **kwargs): + self.__raise_if_unsupported("dropDatabase", target, AsyncMongoClient) + return await target.drop_database( + name_or_database=kwargs.pop("database"), session=kwargs.pop("session", None) + ) + async def _databaseOperation_runCommand(self, target, **kwargs): self.__raise_if_unsupported("runCommand", target, AsyncDatabase) # Ensure the first key is the command name. diff --git a/test/causal_consistency/causal-consistency-clientBulkWrite.json b/test/causal_consistency/causal-consistency-clientBulkWrite.json new file mode 100644 index 0000000000..c2a04422dd --- /dev/null +++ b/test/causal_consistency/causal-consistency-clientBulkWrite.json @@ -0,0 +1,151 @@ +{ + "description": "causal consistency bulkWrite include afterClusterTime", + "schemaVersion": "1.3", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "uriOptions": { + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "causal-consistency-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + }, + { + "session": { + "id": "session0", + "client": "client0", + "sessionOptions": { + "causalConsistency": true + } + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "causal-consistency-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "clientBulkWrite includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "clientBulkWrite", + "object": "client0", + "arguments": { + "session": "session0", + "models": [ + { + "insertOne": { + "namespace": "causal-consistency-tests.test", + "document": { + "_id": 4 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "command": { + "bulkWrite": 1, + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + } + ] +} diff --git a/test/causal_consistency/causal-consistency-write-commands.json b/test/causal_consistency/causal-consistency-write-commands.json new file mode 100644 index 0000000000..a8140c2b3f --- /dev/null +++ b/test/causal_consistency/causal-consistency-write-commands.json @@ -0,0 +1,1396 @@ +{ + "description": "causal consistency write commands include afterClusterTime", + "schemaVersion": "1.3", + "runOnRequirements": [ + { + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "uriOptions": { + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "causal-consistency-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + }, + { + "session": { + "id": "session0", + "client": "client0", + "sessionOptions": { + "causalConsistency": true + } + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "causal-consistency-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "insertOne includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 4 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "insert", + "command": { + "insert": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "insertMany includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "insertMany", + "object": "collection0", + "arguments": { + "session": "session0", + "documents": [ + { + "_id": 4 + }, + { + "_id": 5 + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "insert", + "command": { + "insert": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "updateOne includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "x": 100 + } + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "update", + "command": { + "update": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "updateMany includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "updateMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gt": 0 + } + }, + "update": { + "$set": { + "updated": true + } + } + }, + "expectResult": { + "matchedCount": 3, + "modifiedCount": 3, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "update", + "command": { + "update": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "replaceOne includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "x": 100 + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "update", + "command": { + "update": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "deleteOne includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "deletedCount": 1 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "delete", + "command": { + "delete": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "deleteMany includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "deleteMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gt": 0 + } + } + }, + "expectResult": { + "deletedCount": 3 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "delete", + "command": { + "delete": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndUpdate includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "x": 100 + } + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "findAndModify", + "command": { + "findAndModify": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndDelete includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "findOneAndDelete", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "findAndModify", + "command": { + "findAndModify": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndReplace includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "findOneAndReplace", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "x": 100 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "findAndModify", + "command": { + "findAndModify": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "bulkWrite includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "insertOne": { + "document": { + "_id": 4 + } + } + }, + { + "updateOne": { + "filter": { + "_id": 2 + }, + "update": { + "$set": { + "x": 100 + } + } + } + }, + { + "deleteOne": { + "filter": { + "_id": 3 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "insert", + "command": { + "insert": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "update", + "command": { + "update": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "delete", + "command": { + "delete": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "create includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "dropCollection", + "object": "database0", + "arguments": { + "session": "session0", + "collection": "causal-consistency-createCollection-test" + } + }, + { + "name": "createCollection", + "object": "database0", + "arguments": { + "session": "session0", + "collection": "causal-consistency-createCollection-test" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "drop", + "command": { + "drop": "causal-consistency-createCollection-test", + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "create", + "command": { + "create": "causal-consistency-createCollection-test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "createIndexes includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "createIndex", + "object": "collection0", + "arguments": { + "session": "session0", + "keys": { + "x": 1 + }, + "name": "x_1" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "createIndexes", + "command": { + "createIndexes": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "drop includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "dropCollection", + "object": "database0", + "arguments": { + "session": "session0", + "collection": "test" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "drop", + "command": { + "drop": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "dropDatabase includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "dropDatabase", + "object": "client0", + "arguments": { + "session": "session0", + "database": "causal-consistency-dropDatabase-test" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "dropDatabase", + "command": { + "dropDatabase": 1, + "$db": "causal-consistency-dropDatabase-test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "dropIndexes includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "dropIndexes", + "object": "collection0", + "arguments": { + "session": "session0" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "dropIndexes", + "command": { + "dropIndexes": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + }, + "level": { + "$$exists": false + } + } + } + } + } + ] + } + ] + }, + { + "description": "first write command in a causally consistent session does not include afterClusterTime", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 4 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "insert", + "command": { + "insert": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "$$exists": false + } + } + } + } + ] + } + ] + } + ] +} diff --git a/test/test_causal_consistency.py b/test/test_causal_consistency.py new file mode 100644 index 0000000000..40264e0f17 --- /dev/null +++ b/test/test_causal_consistency.py @@ -0,0 +1,33 @@ +# Copyright 2021-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test the causal consistency unified spec tests.""" +from __future__ import annotations + +import os +import pathlib +import sys + +sys.path[0:0] = [""] + +from test import unittest +from test.unified_format import generate_test_classes, get_test_path + +_IS_SYNC = True + +# Generate unified tests. +globals().update(generate_test_classes(get_test_path("causal_consistency"), module=__name__)) + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_session.py b/test/test_session.py index cf071df49a..684c21260d 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -1134,44 +1134,7 @@ def _test_no_read_concern(self, op): self.assertIsNone(rc) @client_context.require_no_standalone - def test_writes_do_not_include_read_concern(self): - self._test_no_read_concern( - lambda coll, session: coll.bulk_write([InsertOne[dict]({})], session=session) - ) - self._test_no_read_concern(lambda coll, session: coll.insert_one({}, session=session)) - self._test_no_read_concern(lambda coll, session: coll.insert_many([{}], session=session)) - self._test_no_read_concern( - lambda coll, session: coll.replace_one({"_id": 1}, {"x": 1}, session=session) - ) - self._test_no_read_concern( - lambda coll, session: coll.update_one({}, {"$set": {"X": 1}}, session=session) - ) - self._test_no_read_concern( - lambda coll, session: coll.update_many({}, {"$set": {"x": 1}}, session=session) - ) - self._test_no_read_concern(lambda coll, session: coll.delete_one({}, session=session)) - self._test_no_read_concern(lambda coll, session: coll.delete_many({}, session=session)) - self._test_no_read_concern( - lambda coll, session: coll.find_one_and_replace({"x": 1}, {"y": 1}, session=session) - ) - self._test_no_read_concern( - lambda coll, session: coll.find_one_and_update( - {"y": 1}, {"$set": {"x": 1}}, session=session - ) - ) - self._test_no_read_concern( - lambda coll, session: coll.find_one_and_delete({"x": 1}, session=session) - ) - self._test_no_read_concern(lambda coll, session: coll.create_index("foo", session=session)) - self._test_no_read_concern( - lambda coll, session: coll.create_indexes( - [IndexModel([("bar", ASCENDING)])], session=session - ) - ) - self._test_no_read_concern(lambda coll, session: coll.drop_index("foo_1", session=session)) - self._test_no_read_concern(lambda coll, session: coll.drop_indexes(session=session)) - - # Not a write, but explain also doesn't support readConcern. + def test_explain_does_not_include_read_concern(self): self._test_no_read_concern(lambda coll, session: coll.find({}, session=session).explain()) @client_context.require_no_standalone diff --git a/test/transactions-convenient-api/unified/callback-aborts.json b/test/transactions-convenient-api/unified/callback-aborts.json index 206428715c..dc8f7fb5a4 100644 --- a/test/transactions-convenient-api/unified/callback-aborts.json +++ b/test/transactions-convenient-api/unified/callback-aborts.json @@ -308,10 +308,12 @@ "lsid": { "$$sessionLsid": "session0" }, - "autocommit": { - "$$exists": false - }, "readConcern": { + "afterClusterTime": { + "$$exists": true + } + }, + "autocommit": { "$$exists": false }, "startTransaction": { diff --git a/test/transactions-convenient-api/unified/callback-commits.json b/test/transactions-convenient-api/unified/callback-commits.json index 06f791e9ae..edda386418 100644 --- a/test/transactions-convenient-api/unified/callback-commits.json +++ b/test/transactions-convenient-api/unified/callback-commits.json @@ -381,10 +381,12 @@ "lsid": { "$$sessionLsid": "session0" }, - "autocommit": { - "$$exists": false - }, "readConcern": { + "afterClusterTime": { + "$$exists": true + } + }, + "autocommit": { "$$exists": false }, "startTransaction": { diff --git a/test/transactions/unified/commit.json b/test/transactions/unified/commit.json index ab778d8df2..f033906940 100644 --- a/test/transactions/unified/commit.json +++ b/test/transactions/unified/commit.json @@ -1040,7 +1040,9 @@ ], "ordered": true, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "lsid": { "$$sessionLsid": "session1" @@ -1196,7 +1198,9 @@ ], "ordered": true, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "lsid": { "$$sessionLsid": "session1" diff --git a/test/transactions/unified/retryable-writes.json b/test/transactions/unified/retryable-writes.json index c196e68622..21ef22b8ab 100644 --- a/test/transactions/unified/retryable-writes.json +++ b/test/transactions/unified/retryable-writes.json @@ -217,7 +217,9 @@ ], "ordered": true, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "lsid": { "$$sessionLsid": "session0" @@ -306,7 +308,9 @@ ], "ordered": true, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "lsid": { "$$sessionLsid": "session0" diff --git a/test/unified_format.py b/test/unified_format.py index 5516a7adf1..85858765e1 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -758,6 +758,12 @@ def _databaseOperation_createChangeStream(self, target, *args, **kwargs): def _collectionOperation_createChangeStream(self, target, *args, **kwargs): return self.__entityOperation_createChangeStream(target, *args, **kwargs) + def _clientOperation_dropDatabase(self, target, **kwargs): + self.__raise_if_unsupported("dropDatabase", target, MongoClient) + return target.drop_database( + name_or_database=kwargs.pop("database"), session=kwargs.pop("session", None) + ) + def _databaseOperation_runCommand(self, target, **kwargs): self.__raise_if_unsupported("runCommand", target, Database) # Ensure the first key is the command name. diff --git a/tools/synchro.py b/tools/synchro.py index 39250ab14a..f816b18cb9 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -211,6 +211,7 @@ def async_only_test(f: str) -> bool: "test_auth_oidc.py", "test_auth_spec.py", "test_bulk.py", + "test_causal_consistency.py", "test_change_stream.py", "test_client.py", "test_client_backpressure.py", From 8c081441e5b39ce771a75f3dda2acf7c686d89a1 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 4 Jun 2026 14:50:34 -0400 Subject: [PATCH 2/2] Cleanup --- .evergreen/spec-patch/PYTHON-5809.patch | 97 ------------------------- pymongo/asynchronous/client_session.py | 17 +---- pymongo/operations.py | 16 ++++ pymongo/synchronous/client_session.py | 17 +---- 4 files changed, 22 insertions(+), 125 deletions(-) delete mode 100644 .evergreen/spec-patch/PYTHON-5809.patch diff --git a/.evergreen/spec-patch/PYTHON-5809.patch b/.evergreen/spec-patch/PYTHON-5809.patch deleted file mode 100644 index 22b2289782..0000000000 --- a/.evergreen/spec-patch/PYTHON-5809.patch +++ /dev/null @@ -1,97 +0,0 @@ -PYMONGO DOES NOT IMPLEMENT afterClusterTime IN readConcern FOR TRANSACTIONS -- WE NEED THIS PATCH -diff --git a/test/transactions-convenient-api/unified/callback-aborts.json b/test/transactions-convenient-api/unified/callback-aborts.json -index dc8f7fb5..20642871 100644 ---- a/test/transactions-convenient-api/unified/callback-aborts.json -+++ b/test/transactions-convenient-api/unified/callback-aborts.json -@@ -308,14 +308,12 @@ - "lsid": { - "$$sessionLsid": "session0" - }, -- "readConcern": { -- "afterClusterTime": { -- "$$exists": true -- } -- }, - "autocommit": { - "$$exists": false - }, -+ "readConcern": { -+ "$$exists": false -+ }, - "startTransaction": { - "$$exists": false - }, -diff --git a/test/transactions-convenient-api/unified/callback-commits.json b/test/transactions-convenient-api/unified/callback-commits.json -index edda3864..06f791e9 100644 ---- a/test/transactions-convenient-api/unified/callback-commits.json -+++ b/test/transactions-convenient-api/unified/callback-commits.json -@@ -381,14 +381,12 @@ - "lsid": { - "$$sessionLsid": "session0" - }, -- "readConcern": { -- "afterClusterTime": { -- "$$exists": true -- } -- }, - "autocommit": { - "$$exists": false - }, -+ "readConcern": { -+ "$$exists": false -+ }, - "startTransaction": { - "$$exists": false - }, -diff --git a/test/transactions/unified/commit.json b/test/transactions/unified/commit.json -index f0339069..ab778d8d 100644 ---- a/test/transactions/unified/commit.json -+++ b/test/transactions/unified/commit.json -@@ -1040,9 +1040,7 @@ - ], - "ordered": true, - "readConcern": { -- "afterClusterTime": { -- "$$exists": true -- } -+ "$$exists": false - }, - "lsid": { - "$$sessionLsid": "session1" -@@ -1198,9 +1196,7 @@ - ], - "ordered": true, - "readConcern": { -- "afterClusterTime": { -- "$$exists": true -- } -+ "$$exists": false - }, - "lsid": { - "$$sessionLsid": "session1" -diff --git a/test/transactions/unified/retryable-writes.json b/test/transactions/unified/retryable-writes.json -index 21ef22b8..c196e686 100644 ---- a/test/transactions/unified/retryable-writes.json -+++ b/test/transactions/unified/retryable-writes.json -@@ -217,9 +217,7 @@ - ], - "ordered": true, - "readConcern": { -- "afterClusterTime": { -- "$$exists": true -- } -+ "$$exists": false - }, - "lsid": { - "$$sessionLsid": "session0" -@@ -308,9 +306,7 @@ - ], - "ordered": true, - "readConcern": { -- "afterClusterTime": { -- "$$exists": true -- } -+ "$$exists": false - }, - "lsid": { - "$$sessionLsid": "session0" diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index 038b20604d..f9d778f648 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -172,6 +172,7 @@ WTimeoutError, ) from pymongo.helpers_shared import _RETRYABLE_ERROR_CODES +from pymongo.operations import _WRITES_WITH_CLUSTER_TIME from pymongo.read_concern import ReadConcern from pymongo.read_preferences import ReadPreference, _ServerMode from pymongo.server_type import SERVER_TYPE @@ -1111,23 +1112,11 @@ def _apply_to( return self._check_ended() self._materialize(conn.logical_session_timeout_minutes) - # Add afterClusterTime on writes in casually-consistent sessions + # Add afterClusterTime on snapshot reads or writes in causally-consistent sessions if self.options.snapshot or ( self.options.causal_consistency and not self.in_transaction - and operation - in [ - "insert", - "update", - "findAndModify", - "delete", - "bulkWrite", - "create", - "createIndexes", - "drop", - "dropDatabase", - "dropIndexes", - ] + and operation in _WRITES_WITH_CLUSTER_TIME ): self._update_read_concern(command, conn) diff --git a/pymongo/operations.py b/pymongo/operations.py index 73fb8b5f36..f546335b6e 100644 --- a/pymongo/operations.py +++ b/pymongo/operations.py @@ -82,6 +82,22 @@ class _Op(str, enum.Enum): TEST = "testOperation" +_WRITES_WITH_CLUSTER_TIME = frozenset( + { + _Op.INSERT.value, + _Op.UPDATE.value, + _Op.FIND_AND_MODIFY.value, + _Op.DELETE.value, + _Op.BULK_WRITE.value, + _Op.CREATE.value, + _Op.CREATE_INDEXES.value, + _Op.DROP.value, + _Op.DROP_DATABASE.value, + _Op.DROP_INDEXES.value, + } +) + + class InsertOne(Generic[_DocumentType]): """Represents an insert_one operation.""" diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 962135b539..240372f0cf 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -169,6 +169,7 @@ WTimeoutError, ) from pymongo.helpers_shared import _RETRYABLE_ERROR_CODES +from pymongo.operations import _WRITES_WITH_CLUSTER_TIME from pymongo.read_concern import ReadConcern from pymongo.read_preferences import ReadPreference, _ServerMode from pymongo.server_type import SERVER_TYPE @@ -1107,23 +1108,11 @@ def _apply_to( return self._check_ended() self._materialize(conn.logical_session_timeout_minutes) - # Add afterClusterTime on writes in casually-consistent sessions + # Add afterClusterTime on snapshot reads or writes in causally-consistent sessions if self.options.snapshot or ( self.options.causal_consistency and not self.in_transaction - and operation - in [ - "insert", - "update", - "findAndModify", - "delete", - "bulkWrite", - "create", - "createIndexes", - "drop", - "dropDatabase", - "dropIndexes", - ] + and operation in _WRITES_WITH_CLUSTER_TIME ): self._update_read_concern(command, conn)