From 9df87b6401fec19b86baa15dbff09b50673ae2da Mon Sep 17 00:00:00 2001 From: "A. Jesse Jiryu Davis" Date: Thu, 8 Mar 2018 15:14:49 -0500 Subject: [PATCH] prototype transaction tests --- pymongo/bulk.py | 8 +- pymongo/client_session.py | 160 +++- pymongo/command_cursor.py | 2 +- pymongo/cursor.py | 2 +- pymongo/message.py | 10 +- pymongo/mongo_client.py | 7 +- pymongo/network.py | 3 +- pymongo/pool.py | 4 +- test/test_transactions.py | 358 ++++++++ test/transactions/abort.json | 181 ++++ test/transactions/auto-start.json | 578 ++++++++++++ test/transactions/commit.json | 136 +++ test/transactions/errors.json | 47 + test/transactions/isolation.json | 392 +++++++++ test/transactions/read-pref.json | 180 ++++ test/transactions/snapshot-reads.json | 1164 +++++++++++++++++++++++++ test/transactions/statement-ids.json | 218 +++++ test/transactions/write-concern.json | 301 +++++++ 18 files changed, 3717 insertions(+), 34 deletions(-) create mode 100644 test/test_transactions.py create mode 100644 test/transactions/abort.json create mode 100644 test/transactions/auto-start.json create mode 100644 test/transactions/commit.json create mode 100644 test/transactions/errors.json create mode 100644 test/transactions/isolation.json create mode 100644 test/transactions/read-pref.json create mode 100644 test/transactions/snapshot-reads.json create mode 100644 test/transactions/statement-ids.json create mode 100644 test/transactions/write-concern.json diff --git a/pymongo/bulk.py b/pymongo/bulk.py index 8efc5c5fb..ee7dd1377 100644 --- a/pymongo/bulk.py +++ b/pymongo/bulk.py @@ -260,15 +260,13 @@ class _Bulk(object): cmd['writeConcern'] = write_concern.document if self.bypass_doc_val and sock_info.max_wire_version >= 4: cmd['bypassDocumentValidation'] = True - if session: - cmd['lsid'] = session._use_lsid() bwc = _BulkWriteContext(db_name, cmd, sock_info, op_id, listeners, session) results = [] while run.idx_offset < len(run.ops): - if session and retryable: - cmd['txnNumber'] = session._transaction_id() + if session: + session._apply_to(cmd, retryable) sock_info.send_cluster_time(cmd, session, client) check_keys = run.op_type == _INSERT ops = islice(run.ops, run.idx_offset, None) @@ -278,6 +276,8 @@ class _Bulk(object): self.collection.codec_options, bwc) if not to_send: raise InvalidOperation("cannot do an empty bulk write") + if session: + session._advance_statement_id(len(to_send)) result = bwc.write_command(request_id, msg, to_send) client._receive_cluster_time(result, session) results.append((run.idx_offset, result)) diff --git a/pymongo/client_session.py b/pymongo/client_session.py index 695634ca6..a84d55403 100644 --- a/pymongo/client_session.py +++ b/pymongo/client_session.py @@ -61,15 +61,51 @@ class SessionOptions(object): :Parameters: - `causal_consistency` (optional): If True (the default), read operations are causally ordered within the session. + - `auto_start_transaction` (optional): If True, any operation using + the session begins a transaction if none is in progress. """ - def __init__(self, causal_consistency=True): + # TODO: accept a TransactionOptions. + def __init__(self, + causal_consistency=True, + auto_start_transaction=False): self._causal_consistency = causal_consistency + self._auto_start_transaction = auto_start_transaction @property def causal_consistency(self): """Whether causal consistency is configured.""" return self._causal_consistency + @property + def auto_start_transaction(self): + """Whether the session is configured to always start a transaction.""" + return self._auto_start_transaction + + +class TransactionOptions(object): + """Options for :meth:`ClientSession.start_transaction`. + + :Parameters: + - `read_concern`: The :class:`~read_concern.ReadConcern` to use for this + transaction. + - `write_concern`: The :class:`~write_concern.WriteConcern` to use for + this transaction. + """ + def __init__(self, read_concern=None, write_concern=None): + # TODO: validate arguments. + self._read_concern = read_concern + self._write_concern = write_concern + + @property + def read_concern(self): + """This transaction's :class:`~read_concern.ReadConcern`.""" + return self._read_concern + + @property + def write_concern(self): + """This transaction's :class:`~write_concern.WriteConcern`.""" + return self._write_concern + class ClientSession(object): """A session for ordering sequential operations.""" @@ -81,27 +117,41 @@ class ClientSession(object): self._authset = authset self._cluster_time = None self._operation_time = None + self._transaction_options = None # Current transaction's options. + if self.options.auto_start_transaction: + # TODO: Get transaction options from self.options. + self._transaction_options = TransactionOptions() + self._server_session.start_transaction() def end_session(self): - """Finish this session. + """Finish this session. If a transaction has started, abort it. It is an error to use the session or any derived :class:`~pymongo.database.Database`, :class:`~pymongo.collection.Collection`, or :class:`~pymongo.cursor.Cursor` after the session has ended. """ - self._end_session(True) + self._end_session(lock=True, abort_txn=True) - def _end_session(self, lock): + def _end_session(self, lock, abort_txn): if self._server_session is not None: - self._client._return_server_session(self._server_session, lock) - self._server_session = None + try: + if self.in_transaction: + if abort_txn: + self.abort_txn() + else: + self.commit_transaction() + finally: + self._client._return_server_session(self._server_session, lock) + self._server_session = None def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - self.end_session() + # Abort when exiting with an exception, otherwise commit. + # TODO: test and document this. + self._end_session(lock=True, abort_txn=exc_val is not None) @property def client(self): @@ -137,6 +187,45 @@ class ClientSession(object): """ return self._operation_time + def start_transaction(self, **kwargs): + """Start a multi-statement transaction. + + Takes the same arguments as :class:`TransactionOptions`. + + Do not use this method if the session is configured to automatically + start a transaction. + """ + self._transaction_options = TransactionOptions(**kwargs) + self._server_session.start_transaction() + + def commit_transaction(self): + """Commit a multi-statement transaction.""" + self._finish_transaction("commitTransaction") + + def abort_txn(self): + """Abort a multi-statement transaction.""" + assert False, "Not implemented" # Await server. + self._finish_transaction("abortTransaction") + + def _finish_transaction(self, command_name): + if (self.options.auto_start_transaction + and self._server_session.statement_id == 0): + # Not really started. + return + + try: + # TODO: retryable. And it's weird to pass parse_write_concern_error + # from outside database.py. + self._client.admin.command( + command_name, + txnNumber=self._server_session.transaction_id, + session=self, + write_concern=self._transaction_options.write_concern, + parse_write_concern_error=True) + finally: + self._server_session.reset_transaction() + self._transaction_options = None + def _advance_cluster_time(self, cluster_time): """Internal cluster time helper.""" if self._cluster_time is None: @@ -186,19 +275,28 @@ class ClientSession(object): """True if this session is finished.""" return self._server_session is None - def _use_lsid(self): + @property + def in_transaction(self): + """True if this session has an active multi-statement transaction.""" + return (self._server_session is not None + and self._server_session.in_transaction) + + def _apply_to(self, command, is_retryable): # Internal function. if self._server_session is None: raise InvalidOperation("Cannot use ended session") - return self._server_session.use_lsid() + if self.options.auto_start_transaction and not self.in_transaction: + self.start_transaction() - def _transaction_id(self): + self._server_session.apply_to(command, is_retryable) + + def _advance_statement_id(self, n): # Internal function. if self._server_session is None: raise InvalidOperation("Cannot use ended session") - return self._server_session.transaction_id() + self._server_session.advance_statement_id(n) def _retry_transaction_id(self): # Internal function. @@ -213,7 +311,9 @@ class _ServerSession(object): # Ensure id is type 4, regardless of CodecOptions.uuid_representation. self.session_id = {'id': Binary(uuid.uuid4().bytes, 4)} self.last_use = monotonic.time() + self.in_transaction = False self._transaction_id = 0 + self.statement_id = 0 def timed_out(self, session_timeout_minutes): idle_seconds = monotonic.time() - self.last_use @@ -221,15 +321,43 @@ class _ServerSession(object): # Timed out if we have less than a minute to live. return idle_seconds > (session_timeout_minutes - 1) * 60 - def use_lsid(self): - self.last_use = monotonic.time() - return self.session_id + def apply_to(self, command, is_retryable): + command['lsid'] = self.session_id + if is_retryable: + self._transaction_id += 1 + command['txnNumber'] = self.transaction_id + elif self.in_transaction: + command['txnNumber'] = self.transaction_id + # TODO: Allow stmtId for find/getMore, SERVER-33213. + name = next(iter(command)) + if name not in ('find', 'getMore'): + command['stmtId'] = self.statement_id + if self.statement_id == 0: + command['readConcern'] = {'level': 'snapshot'} + command['autocommit'] = False + self.statement_id += 1 + + self.last_use = monotonic.time() + + def advance_statement_id(self, n): + # Every command advances the statement id by 1 already. + self.statement_id += (n - 1) + + @property def transaction_id(self): - """Monotonically increasing positive 64-bit integer.""" - self._transaction_id += 1 + """Positive 64-bit integer.""" return Int64(self._transaction_id) + def start_transaction(self): + self._transaction_id += 1 + self.statement_id = 0 + self.in_transaction = True + + def reset_transaction(self): + self.in_transaction = False + self.statement_id = 0 + def retry_transaction_id(self): self._transaction_id -= 1 diff --git a/pymongo/command_cursor.py b/pymongo/command_cursor.py index 9d88a9543..b2df88aa0 100644 --- a/pymongo/command_cursor.py +++ b/pymongo/command_cursor.py @@ -87,7 +87,7 @@ class CommandCursor(object): def __end_session(self, synchronous): if self.__session and not self.__explicit_session: - self.__session._end_session(lock=synchronous) + self.__session._end_session(lock=synchronous, abort_txn=False) self.__session = None def close(self): diff --git a/pymongo/cursor.py b/pymongo/cursor.py index 393972dae..bb6fe25ff 100644 --- a/pymongo/cursor.py +++ b/pymongo/cursor.py @@ -316,7 +316,7 @@ class Cursor(object): if self.__exhaust and self.__exhaust_mgr: self.__exhaust_mgr.close() if self.__session and not self.__explicit_session: - self.__session._end_session(lock=synchronous) + self.__session._end_session(lock=synchronous, abort_txn=False) self.__session = None def close(self): diff --git a/pymongo/message.py b/pymongo/message.py index 1aaa3258f..c8937692a 100644 --- a/pymongo/message.py +++ b/pymongo/message.py @@ -279,10 +279,11 @@ class _Query(object): cmd = SON([('explain', cmd)]) session = self.session if session: - cmd['lsid'] = session._use_lsid() + session._apply_to(cmd, False) # Explain does not support readConcern. if (not explain and session.options.causal_consistency - and session.operation_time is not None): + and session.operation_time is not None + and not session.in_transaction): cmd.setdefault( 'readConcern', {})[ 'afterClusterTime'] = session.operation_time @@ -353,7 +354,7 @@ class _GetMore(object): self.max_await_time_ms) if self.session: - cmd['lsid'] = self.session._use_lsid() + self.session._apply_to(cmd, False) sock_info.send_cluster_time(cmd, self.session, self.client) return cmd, self.db @@ -653,9 +654,6 @@ class _BulkWriteContext(object): def write_command(self, request_id, msg, docs): """A proxy for SocketInfo.write_command that handles event publishing. """ - if self.session: - # Update last_use time. - self.session._use_lsid() if self.publish: duration = datetime.datetime.now() - self.start_time self._start(request_id, docs) diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index 940a406fb..aec402723 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -1344,7 +1344,9 @@ class MongoClient(common.BaseObject): except Exception: helpers._handle_exception() - def start_session(self, causal_consistency=True): + def start_session(self, + causal_consistency=True, + auto_start_transaction=False): """Start a logical session. This method takes the same parameters as @@ -1374,7 +1376,8 @@ class MongoClient(common.BaseObject): # Raises ConfigurationError if sessions are not supported. server_session = self._get_server_session() opts = client_session.SessionOptions( - causal_consistency=causal_consistency) + causal_consistency=causal_consistency, + auto_start_transaction=auto_start_transaction) return client_session.ClientSession( self, server_session, opts, authset) diff --git a/pymongo/network.py b/pymongo/network.py index c0de3308a..d198c83ea 100644 --- a/pymongo/network.py +++ b/pymongo/network.py @@ -88,7 +88,8 @@ def command(sock, dbname, spec, slave_ok, is_mongos, if read_concern.level: spec['readConcern'] = read_concern.document if (session and session.options.causal_consistency - and session.operation_time is not None): + and session.operation_time is not None + and not session.in_transaction): spec.setdefault( 'readConcern', {})['afterClusterTime'] = session.operation_time if collation is not None: diff --git a/pymongo/pool.py b/pymongo/pool.py index 5a3b3021a..90d99840a 100644 --- a/pymongo/pool.py +++ b/pymongo/pool.py @@ -502,9 +502,7 @@ class SocketInfo(object): # Ensure command name remains in first place. spec = SON(spec) if session: - spec['lsid'] = session._use_lsid() - if retryable_write: - spec['txnNumber'] = session._transaction_id() + session._apply_to(spec, retryable_write) self.send_cluster_time(spec, session, client) listeners = self.listeners if publish_events else None try: diff --git a/test/test_transactions.py b/test/test_transactions.py new file mode 100644 index 000000000..4613e19d5 --- /dev/null +++ b/test/test_transactions.py @@ -0,0 +1,358 @@ +# Copyright 2018-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. + +"""Execute Transactions Spec tests.""" + +import collections +import os +import re +import sys + +from bson import json_util, py3compat +from pymongo.errors import OperationFailure +from pymongo.read_concern import ReadConcern +from pymongo.read_preferences import (make_read_preference, + read_pref_mode_from_name) + +sys.path[0:0] = [""] + +from bson.py3compat import iteritems +from pymongo import operations, WriteConcern +from pymongo.command_cursor import CommandCursor +from pymongo.cursor import Cursor +from pymongo.results import _WriteResult, BulkWriteResult + +from test import unittest, client_context, IntegrationTest +from test.utils import EventListener, rs_client + +# Location of JSON test specifications. +_TEST_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'transactions') + +_TXN_TESTS_DEBUG = os.environ.get('TRANSACTION_TESTS_DEBUG') + + +# TODO: factor the following functions with test_crud.py. +def camel_to_snake(camel): + # Regex to convert CamelCase to snake_case. + snake = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake).lower() + + +def camel_to_upper_camel(camel): + return camel[0].upper() + camel[1:] + + +def camel_to_snake_args(arguments): + for arg_name in list(arguments): + c2s = camel_to_snake(arg_name) + arguments[c2s] = arguments.pop(arg_name) + return arguments + + +class TestTransactions(IntegrationTest): + def transaction_test_debug(self, msg): + if _TXN_TESTS_DEBUG: + print(msg) + + # TODO: factor the following function with test_crud.py. + def check_result(self, expected_result, result): + if isinstance(result, Cursor) or isinstance(result, CommandCursor): + self.assertEqual(list(result), expected_result) + + elif isinstance(result, _WriteResult): + for res in expected_result: + prop = camel_to_snake(res) + # SPEC-869: Only BulkWriteResult has upserted_count. + if (prop == "upserted_count" and + not isinstance(result, BulkWriteResult)): + if result.upserted_id is not None: + upserted_count = 1 + else: + upserted_count = 0 + self.assertEqual(upserted_count, expected_result[res]) + elif prop == "inserted_ids": + # BulkWriteResult does not have inserted_ids. + if isinstance(result, BulkWriteResult): + self.assertEqual(len(expected_result[res]), result.inserted_count) + else: + # InsertManyResult may be compared to [id1] from the + # crud spec or {"0": id1} from the retryable write spec. + ids = expected_result[res] + if isinstance(ids, dict): + ids = [ids[str(i)] for i in range(len(ids))] + self.assertEqual(ids, result.inserted_ids) + elif prop == "upserted_ids": + # Convert indexes from strings to integers. + ids = expected_result[res] + expected_ids = {} + for str_index in ids: + expected_ids[int(str_index)] = ids[str_index] + self.assertEqual(expected_ids != result.upserted_ids) + self.assertEqual(getattr(result, prop), expected_result[res]) + return True + else: + self.assertEqual(result, expected_result) + + def run_operation(self, sessions, collection, operation): + session = None + name = camel_to_snake(operation['name']) + self.transaction_test_debug(name) + session_name = operation['arguments'].pop('session', None) + if session_name: + session = sessions[session_name] + + # Convert arguments to snake_case and handle special cases. + arguments = operation['arguments'] + options = arguments.pop("options", {}) + for option_name in options: + arguments[camel_to_snake(option_name)] = options[option_name] + + pref = arguments.pop('readPreference', None) + if pref: + mode = read_pref_mode_from_name(pref['mode']) + collection = collection.with_options( + read_preference=make_read_preference(mode, None)) + + if name.endswith('_transaction'): + cmd = getattr(session, name) + else: + cmd = getattr(collection, name) + arguments['session'] = session + + if operation == "bulk_write": + # Parse each request into a bulk write model. + requests = [] + for request in arguments["requests"]: + bulk_model = camel_to_upper_camel(request["name"]) + bulk_class = getattr(operations, bulk_model) + bulk_arguments = camel_to_snake_args(request["arguments"]) + requests.append(bulk_class(**bulk_arguments)) + arguments["requests"] = requests + else: + for arg_name in list(arguments): + c2s = camel_to_snake(arg_name) + # PyMongo accepts sort as list of tuples. Asserting len=1 + # because ordering dicts from JSON in 2.6 is unwieldy. + if arg_name == "sort": + sort_dict = arguments[arg_name] + assert len(sort_dict) == 1, 'test can only have 1 sort key' + arguments[arg_name] = list(iteritems(sort_dict)) + # Named "key" instead not fieldName. + if arg_name == "fieldName": + arguments["key"] = arguments.pop(arg_name) + # Aggregate uses "batchSize", while find uses batch_size. + elif arg_name == "batchSize" and operation == "aggregate": + continue + # Requires boolean returnDocument. + elif arg_name == "returnDocument": + arguments[c2s] = arguments[arg_name] == "After" + elif arg_name == "readConcern": + arguments[c2s] = ReadConcern(**arguments.pop(arg_name)) + elif arg_name == "writeConcern": + arguments[c2s] = WriteConcern(**arguments.pop(arg_name)) + else: + arguments[c2s] = arguments.pop(arg_name) + + result = cmd(**arguments) + + if operation == "aggregate": + if arguments["pipeline"] and "$out" in arguments["pipeline"][-1]: + out = collection.database[arguments["pipeline"][-1]["$out"]] + return out.find() + + return result + + # TODO: factor with test_command_monitoring.py + def check_events(self, test, listener, sessions): + res = listener.results + if not len(test['expectations']): + return + + self.assertEqual(len(res['started']), len(test['expectations'])) + for i, expectation in enumerate(test['expectations']): + event_type = next(iter(expectation)) + event = res['started'][i] + + # The tests substitute 42 for any number other than 0. + if (event.command_name == 'getMore' + and event.command['getMore']): + event.command['getMore'] = 42 + elif event.command_name == 'killCursors': + event.command['cursors'] = [42] + + # Replace lsid with a name like "session0" to match test. + if 'lsid' in event.command: + for name, session in sessions.items(): + if event.command['lsid'] == session.session_id: + event.command['lsid'] = name + break + + # TODO: Allow stmtId for find/getMore, SERVER-33213. + if event.command_name in ('find', 'getMore'): + expectation[event_type]['command'].pop('stmtId', None) + + for attr, expected in expectation[event_type].items(): + actual = getattr(event, attr) + if isinstance(expected, dict): + for key, val in expected.items(): + if val is None: + if key in actual: + self.fail("Unexpected key [%s] in %r" % ( + key, actual)) + elif key not in actual: + self.fail("Expected key [%s] in %r" % ( + key, actual)) + else: + self.assertEqual(val, actual[key], + "Key [%s] in %s" % (key, actual)) + else: + self.assertEqual(actual, expected) + + +def expect_error(expected_result): + if isinstance(expected_result, dict): + return expected_result['errorContains'] + + return False + + +def end_sessions(sessions): + for s in sessions.values(): + try: + s.commit_transaction() + except Exception: + # Ignore errors from committing without an open transaction. + pass + + for s in sessions.values(): + s.end_session() + + +def create_test(scenario_def, test): + def run_scenario(self): + listener = EventListener() + # New client to avoid interference from pooled sessions. + client = rs_client(event_listeners=[listener]) + write_concern_db = client.get_database( + 'transaction-tests', write_concern=WriteConcern(w='majority')) + + write_concern_db.test.drop() + write_concern_db.create_collection('test') + if scenario_def['data']: + # Load data. + write_concern_db.test.insert_many(scenario_def['data']) + + # Create session0 and session1. + sessions = {} + for i in range(2): + session_name = 'session%d' % i + sessions[session_name] = client.start_session( + **camel_to_snake_args(test['transactionOptions'][session_name])) + + self.addCleanup(end_sessions, sessions) + + listener.results.clear() + collection = client['transaction-tests'].test + + if _TXN_TESTS_DEBUG: + self.transaction_test_debug("") + + for op in test['operations']: + expected_result = op.get('result') + if expect_error(expected_result): + with self.assertRaises(OperationFailure) as context: + self.run_operation(sessions, collection, op.copy()) + + self.assertIn(expected_result['errorContains'], + str(context.exception)) + + else: + result = self.run_operation(sessions, collection, op.copy()) + self.check_result(expected_result, result) + + self.check_events(test, listener, sessions) + + # Assert final state is expected. + expected_c = test['outcome'].get('collection') + if expected_c is not None: + self.assertEqual(list(collection.find()), expected_c['data']) + + return run_scenario + + +class ScenarioDict(dict): + """Dict that returns {} for any unknown key, recursively.""" + def __init__(self, data): + def convert(v): + if isinstance(v, collections.Mapping): + return ScenarioDict(v) + if isinstance(v, py3compat.string_type): + return v + if isinstance(v, collections.Sequence): + return [convert(item) for item in v] + return v + + dict.__init__(self, [(k, convert(v)) for k, v in data.items()]) + + def __getitem__(self, item): + try: + return dict.__getitem__(self, item) + except KeyError: + # Unlike a defaultdict, don't set the key, just return a dict. + return ScenarioDict({}) + + +def create_tests(): + for dirpath, _, filenames in os.walk(_TEST_PATH): + dirname = os.path.split(dirpath)[-1] + + for filename in filenames: + test_type, ext = os.path.splitext(filename) + if ext != '.json': + continue + + with open(os.path.join(dirpath, filename)) as scenario_stream: + scenario_def = ScenarioDict( + json_util.loads(scenario_stream.read())) + + # Construct test from scenario. + for test in scenario_def['tests']: + test_name = 'test_%s_%s_%s' % ( + dirname, + test_type.replace("-", "_"), + str(test['description'].replace(" ", "_"))) + + new_test = create_test(scenario_def, test) + new_test = client_context.require_version_min(3, 7)(new_test) + new_test = client_context.require_replica_set(new_test) + new_test = client_context._require( + not test.get('skipReason'), + test.get('skipReason'), + new_test) + + if 'secondary' in test_name: + new_test = client_context._require( + client_context.has_secondaries, + 'No secondaries', + new_test) + + new_test.__name__ = test_name + setattr(TestTransactions, new_test.__name__, new_test) + + +create_tests() + +if __name__ == "__main__": + unittest.main() diff --git a/test/transactions/abort.json b/test/transactions/abort.json new file mode 100644 index 000000000..eb8192191 --- /dev/null +++ b/test/transactions/abort.json @@ -0,0 +1,181 @@ +{ + "data": [], + "tests": [ + { + "description": "abort", + "skipReason": "Server must implement abortTransaction", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 2, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": 2, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "implicit abort", + "skipReason": "Server must implement abortTransaction", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + } + ] +} diff --git a/test/transactions/auto-start.json b/test/transactions/auto-start.json new file mode 100644 index 000000000..c8042cb49 --- /dev/null +++ b/test/transactions/auto-start.json @@ -0,0 +1,578 @@ +{ + "data": [], + "tests": [ + { + "description": "commit", + "transactionOptions": { + "session0": { + "autoStartTransaction": true + } + }, + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 2 + }, + "session": "session0" + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3 + }, + "session": "session0" + }, + "result": { + "insertedId": 3 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 2, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 3 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 2, + "stmtId": 1, + "autocommit": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 2, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ] + } + } + }, + { + "description": "explicit start", + "skipReason": "Server must implement abortTransaction", + "transactionOptions": { + "session0": { + "autoStartTransaction": true + } + }, + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 2 + }, + "session": "session0" + }, + "result": { + "errorContains": "Cannot start" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 2, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort", + "skipReason": "Server must implement abortTransaction", + "transactionOptions": { + "session0": { + "autoStartTransaction": true + } + }, + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 2 + }, + "session": "session0" + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 2, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 2, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 2 + } + ] + } + } + }, + { + "description": "commit empty transaction", + "transactionOptions": { + "session0": { + "autoStartTransaction": true + } + }, + "operations": [ + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "isolation", + "transactionOptions": { + "session0": { + "autoStartTransaction": true + } + }, + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": [] + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": [ + { + "_id": 1 + } + ] + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session1", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session1", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/test/transactions/commit.json b/test/transactions/commit.json new file mode 100644 index 000000000..e27994151 --- /dev/null +++ b/test/transactions/commit.json @@ -0,0 +1,136 @@ +{ + "data": [], + "tests": [ + { + "description": "commit", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 2 + }, + "session": "session0" + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 2, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 2, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + } + } + ] +} diff --git a/test/transactions/errors.json b/test/transactions/errors.json new file mode 100644 index 000000000..09183f12d --- /dev/null +++ b/test/transactions/errors.json @@ -0,0 +1,47 @@ +{ + "data": [], + "tests": [ + { + "description": "start twice", + "skipReason": "Server hangs afterward", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "errorContains": "Cannot start" + } + } + ] + } + ] +} diff --git a/test/transactions/isolation.json b/test/transactions/isolation.json new file mode 100644 index 000000000..d420f3e15 --- /dev/null +++ b/test/transactions/isolation.json @@ -0,0 +1,392 @@ +{ + "data": [], + "tests": [ + { + "description": "one transaction", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session0" + }, + "result": 1 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session0" + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id", + "session": "session0" + }, + "result": [ + 1 + ] + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": 0 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": [] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id", + "session": "session1" + }, + "result": [] + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": 0 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id" + }, + "result": [] + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": 1 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id", + "session": "session1" + }, + "result": [ + 1 + ] + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": 1 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id" + }, + "result": [ + 1 + ] + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "two transactions", + "skipReason": "TODO: Crashes server.", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "startTransaction", + "arguments": { + "session": "session1" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session0" + }, + "result": 1 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session0" + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id", + "session": "session0" + }, + "result": [ + 1 + ] + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": 0 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": [] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id", + "session": "session1" + }, + "result": [] + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": 0 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id" + }, + "result": [] + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": 0 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + }, + "session": "session1" + }, + "result": [] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id", + "session": "session1" + }, + "result": [] + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": 1 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "distinct", + "arguments": { + "fieldName": "_id" + }, + "result": [ + 1 + ] + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session1" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/test/transactions/read-pref.json b/test/transactions/read-pref.json new file mode 100644 index 000000000..4d7c37234 --- /dev/null +++ b/test/transactions/read-pref.json @@ -0,0 +1,180 @@ +{ + "data": [], + "tests": [ + { + "description": "primary", + "skipReason": "Implement read preference rules", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "count", + "arguments": { + "filter": { + "_id": 1 + }, + "readPreference": { + "mode": "primary" + }, + "session": "session0" + }, + "result": 1 + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "write and read", + "skipReason": "Implement read preference rules", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": { + "errorContains": "cannot use read preference \"secondary\" in a transaction that began with a write" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "conflict", + "skipReason": "Implement read preference rules", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "primaryPreferred" + }, + "session": "session0" + }, + "result": 0 + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "primary" + }, + "session": "session0" + }, + "result": { + "errorContains": "read preference \"primary\" does not match the transaction's original read preference \"primaryPreferred\"" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "read and write", + "skipReason": "Implement read preference rules", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": 0 + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "errorContains": "cannot write in a transaction that began with read preference \"secondary\"" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + } + ] +} diff --git a/test/transactions/snapshot-reads.json b/test/transactions/snapshot-reads.json new file mode 100644 index 000000000..d3d37f01a --- /dev/null +++ b/test/transactions/snapshot-reads.json @@ -0,0 +1,1164 @@ +{ + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "tests": [ + { + "description": "primary", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": { + "$gte": 1 + } + }, + "sort": { + "_id": 1 + }, + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": 1, + "stmtId": 1, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 2, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": 2, + "stmtId": 0, + "autocommit": false + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": 2, + "stmtId": 1, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": 2, + "stmtId": 2, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 2, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "primary abort", + "skipReason": "Server must implement abortTransaction", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "abortTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": 1, + "stmtId": 1, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 2, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "secondary", + "skipReason": "Implement transaction pinning", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": 1, + "stmtId": 1, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 2, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": 2, + "stmtId": 0, + "autocommit": false + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": 2, + "stmtId": 1, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": 2, + "stmtId": 2, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 2, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "secondary abort", + "skipReason": "Server must implement abortTransaction", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "abortTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "count", + "arguments": { + "readPreference": { + "mode": "secondary" + }, + "session": "session0" + }, + "result": 4 + }, + { + "name": "find", + "arguments": { + "sort": { + "_id": 1 + }, + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": 1, + "stmtId": 1, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 2, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "count": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "lsid": "session0", + "txnNumber": null, + "stmtId": null, + "autocommit": null + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + } + ] +} diff --git a/test/transactions/statement-ids.json b/test/transactions/statement-ids.json new file mode 100644 index 000000000..6d01dddba --- /dev/null +++ b/test/transactions/statement-ids.json @@ -0,0 +1,218 @@ +{ + "data": [], + "tests": [ + { + "description": "insertMany", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "insertMany", + "arguments": { + "documents": [ + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": [ + 2, + 3 + ] + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 4 + }, + "session": "session0" + }, + "result": { + "insertedId": 4 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 5 + }, + "session": "session0" + }, + "result": { + "insertedId": 5 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 1, + "stmtId": 1, + "autocommit": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 4 + } + ], + "lsid": "session0", + "txnNumber": 1, + "stmtId": 3, + "autocommit": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 5 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": 2, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 2, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + }, + { + "_id": 5 + } + ] + } + } + } + ] +} diff --git a/test/transactions/write-concern.json b/test/transactions/write-concern.json new file mode 100644 index 000000000..9e8239eae --- /dev/null +++ b/test/transactions/write-concern.json @@ -0,0 +1,301 @@ +{ + "data": [], + "tests": [ + { + "description": "commit with majority", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0", + "writeConcern": { + "w": "majority" + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commit with default", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "abort with majority", + "skipReason": "Server must implement abortTransaction", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0", + "writeConcern": { + "w": "majority" + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autoabort": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "abort with default", + "skipReason": "Server must implement abortTransaction", + "operations": [ + { + "name": "startTransaction", + "arguments": { + "session": "session0" + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session0" + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "arguments": { + "session": "session0" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "lsid": "session0", + "txnNumber": 1, + "stmtId": 0, + "autoabort": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": 1, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +}