import itertools
import sys
# Require Python 3.7+ for ordered dictionaries so that the order of the
# generated tests remain the same.
# Usage:
# python3.7 mongos-pin-auto-tests.py > mongos-pin-auto.yml
if sys.version_info[:2] < (3, 7):
print('ERROR: This script requires Python >= 3.7, not:')
print(sys.version)
print('Usage: python3.7 mongos-pin-auto-tests.py > mongos-pin-auto.yml')
exit(1)
HEADER = '''# Autogenerated tests that transient errors in a transaction unpin the session.
# See mongos-pin-auto-tests.py
runOn:
-
minServerVersion: "4.1.8"
topology: ["sharded"]
database_name: &database_name "transaction-tests"
collection_name: &collection_name "test"
data: &data
- {_id: 1}
- {_id: 2}
tests:
- description: remain pinned after non-transient Interrupted error on insertOne
useMultipleMongoses: true
operations:
- &startTransaction
name: startTransaction
object: session0
- &initialCommand
name: insertOne
object: collection
arguments:
session: session0
document: {_id: 3}
result:
insertedId: 3
- name: targetedFailPoint
object: testRunner
arguments:
session: session0
failPoint:
configureFailPoint: failCommand
mode: {times: 1}
data:
failCommands: ["insert"]
errorCode: 11601
- name: insertOne
object: collection
arguments:
session: session0
document:
_id: 4
result:
errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"]
errorCodeName: Interrupted
- &assertSessionPinned
name: assertSessionPinned
object: testRunner
arguments:
session: session0
- &commitTransaction
name: commitTransaction
object: session0
expectations:
- command_started_event:
command:
insert: *collection_name
documents:
- _id: 3
ordered: true
readConcern:
lsid: session0
txnNumber:
$numberLong: "1"
startTransaction: true
autocommit: false
writeConcern:
command_name: insert
database_name: *database_name
- command_started_event:
command:
insert: *collection_name
documents:
- _id: 4
ordered: true
readConcern:
lsid: session0
txnNumber:
$numberLong: "1"
startTransaction:
autocommit: false
writeConcern:
command_name: insert
database_name: *database_name
- command_started_event:
command:
commitTransaction: 1
lsid: session0
txnNumber:
$numberLong: "1"
startTransaction:
autocommit: false
writeConcern:
recoveryToken: 42
command_name: commitTransaction
database_name: admin
outcome: &outcome
collection:
data:
- {_id: 1}
- {_id: 2}
- {_id: 3}
- description: unpin after transient error within a transaction
useMultipleMongoses: true
operations:
- &startTransaction
name: startTransaction
object: session0
- &initialCommand
name: insertOne
object: collection
arguments:
session: session0
document:
_id: 3
result:
insertedId: 3
- name: targetedFailPoint
object: testRunner
arguments:
session: session0
failPoint:
configureFailPoint: failCommand
mode: { times: 1 }
data:
failCommands: ["insert"]
closeConnection: true
- name: insertOne
object: collection
arguments:
session: session0
document:
_id: 4
result:
errorLabelsContain: ["TransientTransactionError"]
errorLabelsOmit: ["UnknownTransactionCommitResult"]
# Session unpins from the first mongos after the insert error and
# abortTransaction succeeds immediately on any mongos.
- &assertSessionUnpinned
name: assertSessionUnpinned
object: testRunner
arguments:
session: session0
- &abortTransaction
name: abortTransaction
object: session0
expectations:
- command_started_event:
command:
insert: *collection_name
documents:
- _id: 3
ordered: true
readConcern:
lsid: session0
txnNumber:
$numberLong: "1"
startTransaction: true
autocommit: false
writeConcern:
command_name: insert
database_name: *database_name
- command_started_event:
command:
insert: *collection_name
documents:
- _id: 4
ordered: true
readConcern:
lsid: session0
txnNumber:
$numberLong: "1"
startTransaction:
autocommit: false
writeConcern:
command_name: insert
database_name: *database_name
- command_started_event:
command:
abortTransaction: 1
lsid: session0
txnNumber:
$numberLong: "1"
startTransaction:
autocommit: false
writeConcern:
recoveryToken: 42
command_name: abortTransaction
database_name: admin
outcome: &outcome
collection:
data: *data
# The rest of the tests in this file test every operation type against
# multiple types of transient errors (connection and error code).'''
TEMPLATE = '''
- description: {test_name} {error_name} error on {op_name} {command_name}
useMultipleMongoses: true
operations:
- *startTransaction
- *initialCommand
- name: targetedFailPoint
object: testRunner
arguments:
session: session0
failPoint:
configureFailPoint: failCommand
mode: {{times: 1}}
data:
failCommands: ["{command_name}"]
{error_data}
- name: {op_name}
object: {object_name}
arguments:
session: session0
{op_args}
result:
{error_labels}: ["TransientTransactionError"]
- *{assertion}
- *abortTransaction
outcome: *outcome
'''
# Maps from op_name to (command_name, object_name, op_args)
OPS = {
# Write ops:
'insertOne': ('insert', 'collection', r'document: {_id: 4}'),
'insertMany': ('insert', 'collection', r'documents: [{_id: 4}, {_id: 5}]'),
'updateOne': ('update', 'collection', r'''filter: {_id: 1}
update: {$inc: {x: 1}}'''),
'replaceOne': ('update', 'collection', r'''filter: {_id: 1}
replacement: {y: 1}'''),
'updateMany': ('update', 'collection', r'''filter: {_id: {$gte: 1}}
update: {$set: {z: 1}}'''),
'deleteOne': ('delete', 'collection', r'filter: {_id: 1}'),
'deleteMany': ('delete', 'collection', r'filter: {_id: {$gte: 1}}'),
'findOneAndDelete': ('findAndModify', 'collection', r'filter: {_id: 1}'),
'findOneAndUpdate': ('findAndModify', 'collection', r'''filter: {_id: 1}
update: {$inc: {x: 1}}
returnDocument: Before'''),
'findOneAndReplace': ('findAndModify', 'collection', r'''filter: {_id: 1}
replacement: {y: 1}
returnDocument: Before'''),
# Bulk write insert/update/delete:
'bulkWrite insert': ('insert', 'collection', r'''requests:
- name: insertOne
arguments:
document: {_id: 1}'''),
'bulkWrite update': ('update', 'collection', r'''requests:
- name: updateOne
arguments:
filter: {_id: 1}
update: {$set: {x: 1}}'''),
'bulkWrite delete': ('delete', 'collection', r'''requests:
- name: deleteOne
arguments:
filter: {_id: 1}'''),
# Read ops:
'find': ('find', 'collection', r'filter: {_id: 1}'),
'countDocuments': ('aggregate', 'collection', r'filter: {}'),
'aggregate': ('aggregate', 'collection', r'pipeline: []'),
'distinct': ('distinct', 'collection', r'fieldName: _id'),
# runCommand:
'runCommand': (
'insert',
r'''database
command_name: insert''', # runCommand requires command_name.
r'''command:
insert: *collection_name
documents:
- _id : 1'''),
}
# Maps from error_name to error_data.
NON_TRANSIENT_ERRORS = {
'Interrupted': 'errorCode: 11601',
}
# Maps from error_name to error_data.
TRANSIENT_ERRORS = {
'connection': 'closeConnection: true',
'ShutdownInProgress': 'errorCode: 91',
}
def create_pin_test(op_name, error_name):
test_name = 'remain pinned after non-transient'
assertion = 'assertSessionPinned'
error_labels = 'errorLabelsOmit'
command_name, object_name, op_args = OPS[op_name]
error_data = NON_TRANSIENT_ERRORS[error_name]
if op_name.startswith('bulkWrite'):
op_name = 'bulkWrite'
return TEMPLATE.format(**locals())
def create_unpin_test(op_name, error_name):
test_name = 'unpin after transient'
assertion = 'assertSessionUnpinned'
error_labels = 'errorLabelsContain'
command_name, object_name, op_args = OPS[op_name]
error_data = TRANSIENT_ERRORS[error_name]
if op_name.startswith('bulkWrite'):
op_name = 'bulkWrite'
return TEMPLATE.format(**locals())
tests = []
for op_name, error_name in itertools.product(OPS, NON_TRANSIENT_ERRORS):
tests.append(create_pin_test(op_name, error_name))
for op_name, error_name in itertools.product(OPS, TRANSIENT_ERRORS):
tests.append(create_unpin_test(op_name, error_name))
print(HEADER)
print(''.join(tests))