prototype transaction tests

This commit is contained in:
A. Jesse Jiryu Davis 2018-03-08 15:14:49 -05:00
parent 04de1697cb
commit 9df87b6401
18 changed files with 3717 additions and 34 deletions

View File

@ -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))

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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:

358
test/test_transactions.py Normal file
View File

@ -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()

View File

@ -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": []
}
}
}
]
}

View File

@ -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
}
]
}
}
}
]
}

View File

@ -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
}
]
}
}
}
]
}

View File

@ -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"
}
}
]
}
]
}

View File

@ -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
}
]
}
}
}
]
}

View File

@ -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": []
}
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}
]
}
}
}
]
}

View File

@ -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
}
]
}
}
}
]
}