diff --git a/Makefile.PL b/Makefile.PL index 9acaa058..159a2822 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -43,7 +43,7 @@ my %WriteMakefileArgs = ( "Moo" => 2, "Moo::Role" => 0, "Net::DNS" => 0, - "Safe::Isa" => 0, + "Safe::Isa" => '1.000007', "Scalar::Util" => 0, "Socket" => 0, "Sub::Quote" => 0, diff --git a/devel/config/replicaset-multi-4.0-w_arbiter.yml b/devel/config/replicaset-multi-4.0-w_arbiter.yml new file mode 100644 index 00000000..38d7094a --- /dev/null +++ b/devel/config/replicaset-multi-4.0-w_arbiter.yml @@ -0,0 +1,13 @@ +--- +type: replica +setName: foo +default_args: -v --noprealloc --smallfiles --bind_ip 0.0.0.0 --nssize 6 --quiet +default_version: 4.0 +mongod: + - name: host1 + - name: host2 + - name: host3 + rs_config: + arbiterOnly: true + +# vim: ts=4 sts=4 sw=4 et: diff --git a/devel/config/replicaset-multi-4.0.yml b/devel/config/replicaset-multi-4.0.yml new file mode 100644 index 00000000..aec05d85 --- /dev/null +++ b/devel/config/replicaset-multi-4.0.yml @@ -0,0 +1,11 @@ +--- +type: replica +setName: foo +default_args: -v --noprealloc --smallfiles --bind_ip 0.0.0.0 --nssize 6 --quiet +default_version: 4.0 +mongod: + - name: host1 + - name: host2 + - name: host3 + +# vim: ts=4 sts=4 sw=4 et: diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index c25ef3c7..fcca3051 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -24,15 +24,19 @@ our $VERSION = 'v1.999.1'; use MongoDB::Error; use Moo; +use MongoDB::_Constants; use MongoDB::_Types qw( Document BSONTimestamp + TransactionState + Boolish ); use Types::Standard qw( Maybe HashRef InstanceOf ); +use MongoDB::_TransactionOptions; use namespace::clean -except => 'meta'; =attr client @@ -66,13 +70,17 @@ has cluster_time => ( Options provided for this particular session. Available options include: -=for :list +=for :list * C - If true, will enable causalConsistency for this session. For more information, see L. Note that causalConsistency does not apply for unacknowledged writes. Defaults to true. - +* C - Options to use by default for transactions + created with this session. If when creating a transaction, none or only some of + the transaction options are defined, these options will be used as a fallback. + Defaults to inheriting from the parent client. See L for + available options. =cut @@ -83,10 +91,16 @@ has options => ( # Shallow copy to prevent action at a distance. # Upgrade to use Storable::dclone if a more complex option is required coerce => sub { - $_[0] = { - causalConsistency => 1, - %{ $_[0] } - }; + # Will cause the isa requirement to fire + return unless defined( $_[0] ) && ref( $_[0] ) eq 'HASH'; + $_[0] = { + causalConsistency => defined $_[0]->{causalConsistency} + ? $_[0]->{causalConsistency} + : 1, + defaultTransactionOptions => { + %{ $_[0]->{defaultTransactionOptions} || {} } + }, + }; }, ); @@ -98,6 +112,37 @@ has _server_session => ( clearer => '__clear_server_session', ); +has _current_transaction_options => ( + is => 'rwp', + isa => InstanceOf[ 'MongoDB::_TransactionOptions' ], + handles => { + _get_transaction_write_concern => 'write_concern', + _get_transaction_read_concern => 'read_concern', + _get_transaction_read_preference => 'read_preference', + }, +); + +has _transaction_state => ( + is => 'rwp', + isa => TransactionState, + default => 'none', +); + +# Flag used to say we are still in a transaction +has _active_transaction => ( + is => 'rwp', + isa => Boolish, + default => 0, +); + +# Flag used to say whether any operations have been performed on the +# transaction +has _has_transaction_operations => ( + is => 'rwp', + isa => Boolish, + default => 0, +); + =attr operation_time The last operation time. This is updated when an operation is performed during @@ -215,6 +260,196 @@ sub advance_operation_time { return; } +# Returns 1 if the session is in one of the specified transaction states. +# Returns a false value if not in any of the states defined as an argument. +sub _in_transaction_state { + my ( $self, @states ) = @_; + return 1 if scalar ( grep { $_ eq $self->_transaction_state } @states ); + return; +} + +=method start_transaction + +Start a transaction in this session. Takes a hashref of options which can contain the following options: + +=for :list +* C - The read concern to use for the first command in this + transaction. If not defined here or in the C in + L, will inherit from the parent client. +* C - The write concern to use for committing or aborting this + transaction. As per C, if not defined here then the value defined + in C will be used, or the parent client if not + defined. +* C - The read preference to use for all read operations in + this transaction. If not defined, then will inherit from + C or from the parent client. This value will + override all other read preferences set in any subsequent commands inside this + transaction. + +=cut + +sub start_transaction { + my ( $self, $opts ) = @_; + + MongoDB::UsageError->throw("Transaction already in progress") + if $self->_in_transaction_state( TXN_STARTING, TXN_IN_PROGRESS ); + + MongoDB::ConfigurationError->throw("Transactions are unsupported on this deployment") + unless $self->client->_topology->_supports_transactions; + + $opts ||= {}; + my $trans_opts = MongoDB::_TransactionOptions->new( + client => $self->client, + options => $opts, + default_options => $self->options->{defaultTransactionOptions}, + ); + + $self->_set__current_transaction_options( $trans_opts ); + + $self->_set__transaction_state( TXN_STARTING ); + + $self->_increment_transaction_id; + + $self->_set__active_transaction( 1 ); + $self->_set__has_transaction_operations( 0 ); + + return; +} + +sub _increment_transaction_id { + my $self = shift; + return if $self->_active_transaction; + + $self->_server_session->transaction_id->binc(); +} + +=method commit_transaction + +Commit the current transaction. This will use the writeConcern set on this transaction. + +=cut + +sub commit_transaction { + my $self = shift; + + MongoDB::UsageError->throw("No transaction started") + if $self->_in_transaction_state( TXN_NONE ); + + # Error message tweaked to use our function names + MongoDB::UsageError->throw("Cannot call commit_transaction after calling abort_transaction") + if $self->_in_transaction_state( TXN_ABORTED ); + + # Commit can be called multiple times - even if the transaction completes + # correctly. Setting this here makes sure we dont increment transaction id + # until after another command has been called using this session + $self->_set__active_transaction( 1 ); + + eval { + $self->_send_end_transaction_command( TXN_COMMITTED, [ commitTransaction => 1 ] ); + }; + if ( my $err = $@ ) { + # catch and re-throw after retryable errors + # TODO maybe need better checking logic that theres actually an error code in output? + my $err_code_name; + my $err_code; + if ( $err->can('result') ) { + if ( $err->result->can('output') ) { + $err_code_name = $err->result->output->{codeName}; + $err_code = $err->result->output->{code}; + $err_code_name ||= $err->result->output->{writeConcernError} + ? $err->result->output->{writeConcernError}->{codeName} + : ''; # Empty string just in case + $err_code ||= $err->result->output->{writeConcernError} + ? $err->result->output->{writeConcernError}->{code} + : 0; # just in case + } + } + # If its a write concern error, retrying a commit would still error + unless ( + ( defined( $err_code_name ) && grep { $_ eq $err_code_name } qw/ + CannotSatisfyWriteConcern + UnsatisfiableWriteConcern + UnknownReplWriteConcern + NoSuchTransaction + / ) + # Spec tests include code numbers only with no codeName + || ( defined ( $err_code ) && grep { $_ == $err_code } + 100, # UnsatisfiableWriteConcern/CannotSatisfyWriteConcern + 79, # UnknownReplWriteConcern + 251, # NoSuchTransaction + ) + ) { + push @{ $err->error_labels }, 'UnknownTransactionCommitResult'; + } + die $err; + } + + return; +} + +=method abort_transaction + +Abort the current transaction. This will use the writeConcern set on this transaction. + +=cut + +sub abort_transaction { + my $self = shift; + + MongoDB::UsageError->throw("No transaction started") + if $self->_in_transaction_state( TXN_NONE ); + + # Error message tweaked to use our function names + MongoDB::UsageError->throw("Cannot call abort_transaction after calling commit_transaction") + if $self->_in_transaction_state( TXN_COMMITTED ); + + # Error message tweaked to use our function names + MongoDB::UsageError->throw("Cannot call abort_transaction twice") + if $self->_in_transaction_state( TXN_ABORTED ); + + eval { + $self->_send_end_transaction_command( TXN_ABORTED, [ abortTransaction => 1 ] ); + }; + # Ignore all errors thrown by abortTransaction + + return; +} + +sub _send_end_transaction_command { + my ( $self, $end_state, $command ) = @_; + + $self->_set__transaction_state( $end_state ); + + # Only need to send commit command if the transaction actually sent anything + if ( $self->_has_transaction_operations ) { + my $op = MongoDB::Op::_Command->_new( + db_name => 'admin', + query => $command, + query_flags => {}, + bson_codec => $self->client->bson_codec, + session => $self, + monitoring_callback => $self->client->monitoring_callback, + ); + + my $result = $self->client->send_retryable_write_op( $op, 'force' ); + # TODO This may be redundant after 918 is merged? + $result->assert_no_write_concern_error; + } + + # If the commit/abort succeeded, we are no longer in an active transaction + $self->_set__active_transaction( 0 ); +} + +# For applying connection errors etc +sub _maybe_apply_error_labels { + my ( $self, $err ) = @_; + + if ( $self->_in_transaction_state( TXN_STARTING, TXN_IN_PROGRESS ) ) { + push @{ $err->error_labels }, 'TransientTransactionError'; + } + return; +} + =method end_session $session->end_session; @@ -227,6 +462,10 @@ recycling. Has no effect after calling for the first time. sub end_session { my ( $self ) = @_; + if ( $self->_in_transaction_state ( TXN_IN_PROGRESS ) ) { + # Ignore all errors + eval { $self->abort_transaction }; + } if ( defined $self->_server_session ) { $self->client->_server_session_pool->retire_server_session( $self->_server_session ); $self->__clear_server_session; diff --git a/lib/MongoDB/CommandResult.pm b/lib/MongoDB/CommandResult.pm index 2ee2536d..f86a6545 100644 --- a/lib/MongoDB/CommandResult.pm +++ b/lib/MongoDB/CommandResult.pm @@ -115,6 +115,18 @@ sub last_wtimeout { || exists $self->output->{writeConcernError} ); } +=method last_error_labels + +Returns an array of error labels from the command, or an empty array if there +are none + +=cut + +sub last_error_labels { + my ( $self ) = @_; + return $self->output->{errorLabels} || []; +} + =method assert Throws an exception if the command failed. diff --git a/lib/MongoDB/Error.pm b/lib/MongoDB/Error.pm index 4e76cd8d..026bd2a8 100644 --- a/lib/MongoDB/Error.pm +++ b/lib/MongoDB/Error.pm @@ -30,8 +30,13 @@ use Carp; use MongoDB::_Types qw( ErrorStr ); +use Types::Standard qw( + ArrayRef + Str +); use Scalar::Util (); use Sub::Quote (); +use Safe::Isa; use Exporter 5.57 qw/import/; use namespace::clean -except => ['import']; @@ -40,6 +45,8 @@ my $ERROR_CODES; BEGIN { $ERROR_CODES = { BAD_VALUE => 2, + HOST_UNREACHABLE => 6, + HOST_NOT_FOUND => 7, UNKNOWN_ERROR => 8, USER_NOT_FOUND => 11, NAMESPACE_NOT_FOUND => 26, @@ -48,9 +55,15 @@ BEGIN { EXCEEDED_TIME_LIMIT => 50, COMMAND_NOT_FOUND => 59, WRITE_CONCERN_ERROR => 64, + NETWORK_TIMEOUT => 89, + SHUTDOWN_IN_PROGRESS => 91, + PRIMARY_STEPPED_DOWN => 189, + SOCKET_EXCEPTION => 9001, NOT_MASTER => 10107, DUPLICATE_KEY => 11000, DUPLICATE_KEY_UPDATE => 11001, # legacy before 2.6 + INTERRUPTED_AT_SHUTDOWN => 11600, + INTERRUPTED_DUE_TO_REPL_STATE_CHANGE => 11602, DUPLICATE_KEY_CAPPED => 12582, # legacy before 2.6 UNRECOGNIZED_COMMAND => 13390, # mongos error before 2.4 NOT_MASTER_NO_SLAVE_OK => 13435, @@ -94,6 +107,19 @@ has 'previous_exception' => ( >), ); +has error_labels => ( + is => 'ro', + isa => ArrayRef[Str], + default => sub { [] }, +); + +sub has_error_label { + my ( $self, $expected ) = @_; + + return unless defined $self->error_labels; + return grep { $_ eq $expected } @{ $self->error_labels }; +} + sub throw { my ($inv) = shift; @@ -113,6 +139,63 @@ sub throw { # an error occurs. sub _is_resumable { 1 } +# internal flag for if this error type specifically can be retried regardless +# of other state. See _is_retryable which contains the full retryable error +# logic. +sub __is_retryable_error { 0 } + +sub _check_is_retryable_code { + my $code = $_[-1]; + + my @retryable_codes = ( + MongoDB::Error::HOST_NOT_FOUND(), + MongoDB::Error::HOST_UNREACHABLE(), + MongoDB::Error::NETWORK_TIMEOUT(), + MongoDB::Error::SHUTDOWN_IN_PROGRESS(), + MongoDB::Error::PRIMARY_STEPPED_DOWN(), + MongoDB::Error::SOCKET_EXCEPTION(), + MongoDB::Error::NOT_MASTER(), + MongoDB::Error::INTERRUPTED_AT_SHUTDOWN(), + MongoDB::Error::INTERRUPTED_DUE_TO_REPL_STATE_CHANGE(), + MongoDB::Error::NOT_MASTER_NO_SLAVE_OK(), + MongoDB::Error::NOT_MASTER_OR_SECONDARY(), + ); + + return 1 if grep { $code == $_ } @retryable_codes; + return 0; +} + +sub _check_is_retryable_message { + my $message = $_[-1]; + + return 0 unless defined $message; + return 1 if $message =~ /(not master|node is recovering)/i; + return 0; +} + +# indicates if this error can be retried under retryable writes +sub _is_retryable { + my $self = shift; + + if ( $self->$_can( 'result' ) ) { + return 1 if _check_is_retryable_code( $self->result->last_code ); + } + + if ( $self->$_can( 'code' ) ) { + return 1 if _check_is_retryable_code( $self->code ); + } + + return 1 if _check_is_retryable_message( $self->message ); + + if ( $self->$_isa( 'MongoDB::WriteConcernError' ) ) { + return 1 if _check_is_retryable_code( $self->result->output->{writeConcernError}{code} ); + return 1 if _check_is_retryable_message( $self->result->output->{writeConcernError}{message} ); + } + + # Defaults to 0 unless its a network exception + return $self->__is_retryable_error; +} + #--------------------------------------------------------------------------# # Subclasses with attributes included inline below #--------------------------------------------------------------------------# @@ -200,6 +283,7 @@ use namespace::clean; extends 'MongoDB::Error'; sub _is_resumable { 1 } +sub __is_retryable_error { 1 } package MongoDB::HandshakeError; use Moo; @@ -217,6 +301,8 @@ use Moo; use namespace::clean; extends 'MongoDB::Error'; +sub __is_retryable_error { 1 } + package MongoDB::ExecutionTimeout; use Moo; use namespace::clean; @@ -536,6 +622,8 @@ will throw this — only ones originating directly from the MongoDB::* library files. Some type and usage errors will originate from the L library if the objects are used incorrectly. +Also used to indicate usage errors for transaction commands. + =head1 ERROR CODES The following error code constants are automatically exported by this module. @@ -565,6 +653,20 @@ B: * The database uses multiple write concern error codes. The driver maps them all to WRITE_CONCERN_ERROR for consistency and convenience. +=head1 ERROR LABELS + +From MongoDB 4.0 onwards, errors may contain an error labels field. This field +is populated for extra information from either the server or the driver, +depending on the error. + +Known error labels include (but are not limited to): + +=for :list +* C - added when network errors are encountered + inside a transaction. +* C - added when a transaction commit may not + have been able to satisfy the provided write concern. + =cut # vim: ts=4 sts=4 sw=4 et: diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index 0476b02f..2d815609 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1075,7 +1075,8 @@ has _write_concern => ( sub _build__write_concern { my ($self) = @_; return MongoDB::WriteConcern->new( - ( $self->w ? ( w => $self->w ) : () ), + # Must check for defined as w can be 0, and defaults to undef + ( defined $self->w ? ( w => $self->w ) : () ), ( $self->wtimeout ? ( wtimeout => $self->wtimeout ) : () ), ( $self->j ? ( j => $self->j ) : () ), ); @@ -1526,10 +1527,24 @@ sub send_admin_command { return $self->send_read_op( $op ); } +# Reset session state if we're outside an active transaction, otherwise set +# that this transaction actually has operations +sub _maybe_update_session_state { + my ( $self, $op ) = @_; + if ( defined $op->session && ! $op->session->_active_transaction ) { + $op->session->_set__transaction_state( TXN_NONE ); + } elsif ( defined $op->session ) { + $op->session->_set__has_transaction_operations( 1 ); + } +} + # op dispatcher written in highly optimized style sub send_direct_op { my ( $self, $op, $address ) = @_; my ( $link, $result ); + + $self->_maybe_update_session_state( $op ); + ( $link = $self->{_topology}->get_specific_link($address) ), ( eval { ($result) = $op->execute($link); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; @@ -1551,17 +1566,12 @@ sub send_direct_op { sub send_write_op { my ( $self, $op ) = @_; my ( $link, $result ); + + $self->_maybe_update_session_state( $op ); + ( $link = $self->{_topology}->get_writable_link ), ( - eval { ($result) = $op->execute($link, $self->{_topology}->type); 1 } or do { + eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; - if ( $err->$_isa("MongoDB::ConnectionError") ) { - $self->{_topology}->mark_server_unknown( $link->server, $err ); - } - elsif ( $err->$_isa("MongoDB::NotMasterError") ) { - $self->{_topology}->mark_server_unknown( $link->server, $err ); - $self->{_topology}->mark_stale; - } - # regardless of cleanup, rethrow the error WITH_ASSERTS ? ( confess $err ) : ( die $err ); } ), @@ -1577,15 +1587,25 @@ BEGIN { } sub send_retryable_write_op { - my ( $self, $op ) = @_; - - return $self->send_write_op( $op ) unless $self->retry_writes; + my ( $self, $op, $force ) = @_; my $result; my $link = $self->{_topology}->get_writable_link; - # If server doesnt support retryable writes, pretend its not enabled - unless ( $link->supports_retryWrites ) { + $self->_maybe_update_session_state( $op ); + + # Need to force to do a retryable write on a Transaction Commit or Abort. $force is an override for retry_writes, but theres no point trying that if the link doesnt support it anyway. + # This triggers on the following: + # * $force is not set to 'force' + # (specifically for retrying writes in ending transaction operations) + # * retry writes is not enabled or the link doesnt support retryWrites + # * if an active transaction is starting or in progress + unless ( $link->supports_retryWrites + && ( $self->retry_writes || ( defined $force && $force eq 'force' ) ) + && ( defined $op->session + && ! $op->session->_in_transaction_state( TXN_STARTING, TXN_IN_PROGRESS ) + ) + ) { eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; WITH_ASSERTS ? ( confess $err ) : ( die $err ); @@ -1595,14 +1615,23 @@ sub send_retryable_write_op { # If we get this far and there is no session, then somethings gone really # wrong, so probably not worth worrying about. - # - # increment transaction id before write, but otherwise is the same for both attempts - $op->session->_server_session->_increment_transaction_id; + + # increment transaction id before write, but otherwise is the same for both + # attempts. If not in a transaction, is a no-op + $op->session->_increment_transaction_id; $op->retryable_write( 1 ); # attempt the op the first time eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; + + # If the error is not retryable, then drop out + unless ( $err->$_call_if_can('_is_retryable') ) { + WITH_ASSERTS ? ( confess $err ) : ( die $err ); + } + + # Must check if error is retryable before getting the link, in case we + # get a 'no writable servers' error my $retry_link = $self->{_topology}->get_writable_link; # Rare chance that the new link is not retryable @@ -1652,6 +1681,17 @@ sub _try_write_op_for_link { sub send_read_op { my ( $self, $op ) = @_; my ( $link, $type, $result ); + + # Get transaction read preference if in a transaction. + if ( defined $op->session && $op->session->_active_transaction ) { + # Transactions may only read from primary in MongoDB 4.0, so get and + # check the read preference from the transaction settings as per + # transaction spec - see MongoDB::_TransactionOptions + $op->read_preference( $op->session->_get_transaction_read_preference ); + } + + $self->_maybe_update_session_state( $op ); + ( $link = $self->{_topology}->get_readable_link( $op->read_preference ) ), ( $type = $self->{_topology}->type ), ( eval { ($result) = $op->execute( $link, $type ); 1 } or do { diff --git a/lib/MongoDB/Op/_Command.pm b/lib/MongoDB/Op/_Command.pm index eed6e74b..05780c7a 100644 --- a/lib/MongoDB/Op/_Command.pm +++ b/lib/MongoDB/Op/_Command.pm @@ -52,7 +52,8 @@ has query_flags => ( ); has read_preference => ( - is => 'ro', + # Needs to be rw for transactions + is => 'rw', isa => Maybe [ReadPreference], ); @@ -111,6 +112,7 @@ sub execute { ( $result = MongoDB::_Protocol::parse_reply( $link->read, $request_id ) ); }; if ( my $err = $@ ) { + $self->_update_session_connection_error( $err ); $self->publish_command_exception($err) if $self->monitoring_callback; die $err; } @@ -123,13 +125,14 @@ sub execute { address => $link->address, ); - # Must happen even on an error (ie. the command fails) - $self->_update_operation_time( $res ); + $self->_update_session_pre_assert( $res ); $res->assert; $self->_update_session_and_cluster_time($res); + $self->_assert_session_errors($res); + return $res; } diff --git a/lib/MongoDB/Role/_DatabaseErrorThrower.pm b/lib/MongoDB/Role/_DatabaseErrorThrower.pm index a31be93e..5f423d83 100644 --- a/lib/MongoDB/Role/_DatabaseErrorThrower.pm +++ b/lib/MongoDB/Role/_DatabaseErrorThrower.pm @@ -27,7 +27,7 @@ use MongoDB::Error; use namespace::clean; -requires qw/last_errmsg last_code last_wtimeout/; +requires qw/last_errmsg last_code last_wtimeout last_error_labels/; my $ANY_DUP_KEY = [ DUPLICATE_KEY, DUPLICATE_KEY_UPDATE, DUPLICATE_KEY_CAPPED ]; my $ANY_NOT_MASTER = [ NOT_MASTER, NOT_MASTER_NO_SLAVE_OK, NOT_MASTER_OR_SECONDARY ]; @@ -40,6 +40,7 @@ sub _throw_database_error { my $err = $self->last_errmsg; my $code = $self->last_code; + my $error_labels = $self->last_error_labels; if ( grep { $code == $_ } @$ANY_NOT_MASTER || $err =~ /^(?:not master|node is recovering)/ ) { $error_class = "MongoDB::NotMasterError"; @@ -57,6 +58,7 @@ sub _throw_database_error { $error_class->throw( result => $self, code => $code || UNKNOWN_ERROR, + error_labels => $error_labels, ( length($err) ? ( message => $err ) : () ), ); diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index 8e76a2cb..284420f9 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -23,7 +23,9 @@ our $VERSION = 'v1.999.1'; use Moo::Role; use MongoDB::_Types -types, 'to_IxHash'; +use MongoDB::_Constants; use Safe::Isa; +use boolean; use namespace::clean; requires qw/ session retryable_write /; @@ -43,17 +45,49 @@ sub _apply_session_and_cluster_time { $$query_ref = to_IxHash( $$query_ref ); ($$query_ref)->Push( 'lsid' => $self->session->session_id ); - if ( $self->retryable_write ) { + if ( $self->retryable_write || ! $self->session->_in_transaction_state( TXN_NONE ) ) { ($$query_ref)->Push( 'txnNumber' => $self->session->_server_session->transaction_id ); } + if ( ! $self->session->_in_transaction_state( TXN_NONE ) ) { + ($$query_ref)->Push( 'autocommit' => false ); + } + + if ( $self->session->_in_transaction_state( TXN_STARTING ) ) { + ($$query_ref)->Push( 'startTransaction' => true ); + ($$query_ref)->Push( @{ $self->session->_get_transaction_read_concern->as_args( $self->session ) } ); + } elsif ( ! $self->session->_in_transaction_state( TXN_NONE ) ) { + # read concern only valid outside a transaction or when starting + ($$query_ref)->Delete( 'readConcern' ); + } + + # write concern not allowed in transactions except when ending. We can + # safely delete it here as you can only pass writeConcern through by + # arguments to client of collection. + if ( $self->session->_in_transaction_state( TXN_STARTING, TXN_IN_PROGRESS ) ) { + ($$query_ref)->Delete( 'writeConcern' ); + } + + if ( $self->session->_in_transaction_state( TXN_ABORTED, TXN_COMMITTED ) + && ! ($$query_ref)->EXISTS('writeConcern') + ) { + ($$query_ref)->Push( @{ $self->session->_get_transaction_write_concern->as_args() } ); + } + + # MUST be the last thing to touch the transaction state before sending, + # so the various starting specific query modifications can be applied + # The spec states that this should happen after the command even on error, + # so happening before the command is sent is still valid + if ( $self->session->_in_transaction_state( TXN_STARTING ) ) { + $self->session->_set__transaction_state( TXN_IN_PROGRESS ); + } + $self->session->_server_session->update_last_use; my $cluster_time = $self->session->get_latest_cluster_time; if ( defined $cluster_time && $link->supports_clusterTime ) { # Gossip the clusterTime - $$query_ref = to_IxHash($$query_ref); ($$query_ref)->Push( '$clusterTime' => $cluster_time ); } @@ -81,7 +115,7 @@ sub _update_session_and_cluster_time { return; } -sub _update_operation_time { +sub _update_session_pre_assert { my ( $self, $response ) = @_; return unless defined $self->session; @@ -92,6 +126,27 @@ sub _update_operation_time { return; } +# Certain errors have to happen as soon as possible, such as write concern +# errors in a retryable write. This has to be seperate to the other functions +# due to not all result objects having the base response inside, so cannot be +# used to parse operationTime or $clusterTime +sub _assert_session_errors { + my ( $self, $response ) = @_; + + if ( $self->retryable_write ) { + $response->assert_no_write_concern_error; + } + + return; +} + +sub _update_session_connection_error { + my ( $self, $err ) = @_; + + return unless defined $self->session; + return $self->session->_maybe_apply_error_labels( $err ); +} + sub __extract_from { my ( $self, $response, $key ) = @_; diff --git a/lib/MongoDB/Role/_SingleBatchDocWrite.pm b/lib/MongoDB/Role/_SingleBatchDocWrite.pm index 70430f22..76e4d58d 100644 --- a/lib/MongoDB/Role/_SingleBatchDocWrite.pm +++ b/lib/MongoDB/Role/_SingleBatchDocWrite.pm @@ -191,6 +191,7 @@ sub _send_write_command { ( $result = MongoDB::_Protocol::parse_reply( $link->read, $request_id ) ); }; if ( my $err = $@ ) { + $self->_update_session_connection_error( $err ); $self->publish_command_exception($err) if $self->monitoring_callback; die $err; } @@ -200,11 +201,12 @@ sub _send_write_command { my $res = $self->bson_codec->decode_one( $result->{docs} ); - $self->_update_operation_time( $res ); + $self->_update_session_pre_assert( $res ); $self->_update_session_and_cluster_time($res); # Error checking depends on write concern + # TODO does this logic get affected by transactions???? Probably? if ( $self->write_concern->is_acknowledged ) { # errors in the command itself get handled as normal CommandResult @@ -222,12 +224,14 @@ sub _send_write_command { # otherwise, construct the desired result object, calling back # on class-specific parser to generate additional attributes - return $result_class->_new( + my $built_result = $result_class->_new( write_errors => ( $res->{writeErrors} ? $res->{writeErrors} : [] ), write_concern_errors => ( $res->{writeConcernError} ? [ $res->{writeConcernError} ] : [] ), $self->_parse_cmd($res), ); + $self->_assert_session_errors( $built_result ); + return $built_result; } else { return MongoDB::UnacknowledgedResult->_new( diff --git a/lib/MongoDB/Role/_WriteResult.pm b/lib/MongoDB/Role/_WriteResult.pm index 70739f65..7d6c8418 100644 --- a/lib/MongoDB/Role/_WriteResult.pm +++ b/lib/MongoDB/Role/_WriteResult.pm @@ -122,4 +122,15 @@ sub last_wtimeout { return !!( $self->count_write_concern_errors && !$self->count_write_errors ); } +sub last_error_labels { + my ( $self ) = @_; + if ( $self->count_write_errors ) { + return $self->write_errors->[-1]{errorLabels} || []; + } + elsif ( $self->count_write_concern_errors ) { + return $self->write_errors->[-1]{errorLabels} || []; + } + return []; +} + 1; diff --git a/lib/MongoDB/_Constants.pm b/lib/MongoDB/_Constants.pm index e875f600..15a39f61 100644 --- a/lib/MongoDB/_Constants.pm +++ b/lib/MongoDB/_Constants.pm @@ -49,6 +49,12 @@ BEGIN { P_INT32 => $] lt '5.010' ? 'l' : 'l<', SMALLEST_MAX_STALENESS_SEC => 90, WITH_ASSERTS => $ENV{PERL_MONGO_WITH_ASSERTS}, + # Transaction state tracking + TXN_NONE => 'none', + TXN_STARTING => 'starting', + TXN_IN_PROGRESS => 'in_progress', + TXN_COMMITTED => 'committed', + TXN_ABORTED => 'aborted', }; } diff --git a/lib/MongoDB/_ServerSession.pm b/lib/MongoDB/_ServerSession.pm index 15e00e33..45a753f8 100644 --- a/lib/MongoDB/_ServerSession.pm +++ b/lib/MongoDB/_ServerSession.pm @@ -123,11 +123,6 @@ sub _is_expiring { return; } -sub _increment_transaction_id { - my $self = shift; - $self->transaction_id->binc(); -} - 1; __END__ diff --git a/lib/MongoDB/_Topology.pm b/lib/MongoDB/_Topology.pm index 52413a6e..ab4a3ebd 100644 --- a/lib/MongoDB/_Topology.pm +++ b/lib/MongoDB/_Topology.pm @@ -657,6 +657,14 @@ sub _supports_retry_writes { return; } +sub _supports_transactions { + my ( $self ) = @_; + + return unless $self->_supports_sessions; + return if $self->wire_version_ceil < 7; + return if $self->type eq 'Sharded'; + return 1; +} sub _check_staleness_compatibility { my ($self, $read_pref) = @_; diff --git a/lib/MongoDB/_TransactionOptions.pm b/lib/MongoDB/_TransactionOptions.pm new file mode 100644 index 00000000..b423036a --- /dev/null +++ b/lib/MongoDB/_TransactionOptions.pm @@ -0,0 +1,134 @@ +# 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. + +use strict; +use warnings; +package MongoDB::_TransactionOptions; + +# MongoDB options for transactions + +use version; +our $VERSION = 'v1.999.0'; + +use MongoDB::Error; + +use Moo; +use MongoDB::ReadConcern; +use MongoDB::WriteConcern; +use MongoDB::ReadPreference; +use MongoDB::_Types qw( + MongoDBClient + WriteConcern + ReadConcern + ReadPreference +); +use Types::Standard qw( + HashRef + Any +); +use namespace::clean -except => 'meta'; + +# Options provided during start transaction +has options => ( + is => 'ro', + required => 1, + isa => HashRef, +); + +# Options provided during start session +has default_options => ( + is => 'ro', + required => 1, + isa => HashRef, +); + +# needed for defaults +has client => ( + is => 'ro', + required => 1, + isa => MongoDBClient, +); + +has write_concern => ( + # must error on start_transaction, so is built immediately + is => 'ro', + isa => WriteConcern, + init_arg => undef, + builder => '_build_write_concern', +); + +sub _build_write_concern { + my $self = shift; + + my $options = $self->options->{writeConcern}; + $options ||= $self->default_options->{writeConcern}; + + my $write_concern; + $write_concern = MongoDB::WriteConcern->new( $options ) if defined $options; + $write_concern ||= $self->client->write_concern; + + unless ( $write_concern->is_acknowledged ) { + MongoDB::ConfigurationError->throw( + 'transactions do not support unacknowledged write concerns' ); + } + + return $write_concern; +} + +has read_concern => ( + is => 'lazy', + isa => ReadConcern, + init_arg => undef, + builder => '_build_read_concern', +); + +# Read concern errors are returned by the database, so no need to check for +# errors +sub _build_read_concern { + my $self = shift; + + my $options = $self->options->{readConcern}; + $options ||= $self->default_options->{readConcern}; + + return MongoDB::ReadConcern->new( $options ) if defined $options; + return $self->client->read_concern; +} + +has read_preference => ( + is => 'lazy', + isa => ReadPreference, + init_arg => undef, + builder => '_build_read_preference', +); + +# Read preferences must be primary at present, so check after building it +sub _build_read_preference { + my $self = shift; + + my $options = $self->options->{readPreference}; + $options ||= $self->default_options->{readPreference}; + + my $read_pref; + $read_pref = MongoDB::ReadPreference->new( $options ) if defined $options; + $read_pref ||= $self->client->read_preference; + + if ( $read_pref->mode ne 'primary' ) { + MongoDB::ConfigurationError->throw( + "read preference in a transaction must be primary" ); + } + + return $read_pref; +} + +1; diff --git a/lib/MongoDB/_Types.pm b/lib/MongoDB/_Types.pm index c6947193..d4b50478 100644 --- a/lib/MongoDB/_Types.pm +++ b/lib/MongoDB/_Types.pm @@ -49,6 +49,7 @@ use Type::Library IxHash MaxStalenessNum MaybeHashRef + MongoDBClient MongoDBCollection MongoDBDatabase BSONTimestamp @@ -67,6 +68,7 @@ use Type::Library SingleKeyHash Stringish TopologyType + TransactionState WriteConcern ); @@ -89,6 +91,7 @@ use Types::Standard qw( use Scalar::Util qw/reftype/; use boolean 0.25; +use MongoDB::_Constants; require Tie::IxHash; #--------------------------------------------------------------------------# @@ -151,6 +154,8 @@ class_type IxHash, { class => 'Tie::IxHash' }; declare MaybeHashRef, as Maybe[ HashRef ]; +class_type MongoDBClient, { class => 'MongoDB::MongoClient' }; + class_type MongoDBCollection, { class => 'MongoDB::Collection' }; class_type MongoDBDatabase, { class => 'MongoDB::Database' }; @@ -195,6 +200,9 @@ declare SingleKeyHash, as HashRef, where { 1 == scalar keys %$_ }; enum TopologyType, [qw/Single ReplicaSetNoPrimary ReplicaSetWithPrimary Sharded Direct Unknown/]; +enum TransactionState, + [ TXN_NONE, TXN_STARTING, TXN_IN_PROGRESS, TXN_COMMITTED, TXN_ABORTED ]; + class_type WriteConcern, { class => 'MongoDB::WriteConcern' }; # after SingleKeyHash, PairArrayRef and IxHash diff --git a/t/data/retryable-writes/README.rst b/t/data/retryable-writes/README.rst index b9f1008c..606ba93b 100644 --- a/t/data/retryable-writes/README.rst +++ b/t/data/retryable-writes/README.rst @@ -15,10 +15,11 @@ that drivers can use to prove their conformance to the Retryable Writes spec. Several prose tests, which are not easily expressed in YAML, are also presented in this file. Those tests will need to be manually implemented by each driver. -Tests will require a MongoClient with ``retryWrites`` enabled. Integration tests -will require a running MongoDB cluster with server versions 3.6.0 or later. The -``{setFeatureCompatibilityVersion: 3.6}`` admin command will also need to have -been executed to enable support for retryable writes on the cluster. +Tests will require a MongoClient created with options defined in the tests. +Integration tests will require a running MongoDB cluster with server versions +3.6.0 or later. The ``{setFeatureCompatibilityVersion: 3.6}`` admin command +will also need to have been executed to enable support for retryable writes on +the cluster. Server Fail Point ================= @@ -137,15 +138,16 @@ Each YAML file has the following keys: - ``description``: The name of the test. - - ``failPoint``: Document describing options for configuring the - ``onPrimaryTransactionalWrite`` fail point on the primary server. This - document should be merged with the - ``{ configureFailPoint: "onPrimaryTransactionalWrite" }`` command document. + - ``clientOptions``: Parameters to pass to MongoClient(). + + - ``failPoint``: The ``configureFailPoint`` command document to run to + configure a fail point on the primary server. Drivers must ensure that + ``configureFailPoint`` is the first field in the command. - ``operation``: Document describing the operation to be executed. The operation should be executed through a collection object derived from a - client that has been created with the ``retryWrites=true`` option. - This will have some or all of the following fields: + client that has been created with ``clientOptions``. The operation will have + some or all of the following fields: - ``name``: The name of the operation as defined in the CRUD specification. diff --git a/t/data/retryable-writes/bulkWrite-serverErrors.json b/t/data/retryable-writes/bulkWrite-serverErrors.json new file mode 100644 index 00000000..cd81fc61 --- /dev/null +++ b/t/data/retryable-writes/bulkWrite-serverErrors.json @@ -0,0 +1,182 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "BulkWrite succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "deletedCount": 1, + "insertedIds": { + "1": 3 + }, + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0, + "upsertedIds": {} + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 23 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "BulkWrite succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "deletedCount": 1, + "insertedIds": { + "1": 3 + }, + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0, + "upsertedIds": {} + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 23 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/bulkWrite-serverErrors.yml b/t/data/retryable-writes/bulkWrite-serverErrors.yml new file mode 100644 index 00000000..de9cff5e --- /dev/null +++ b/t/data/retryable-writes/bulkWrite-serverErrors.yml @@ -0,0 +1,90 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "BulkWrite succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + errorCode: 189 + operation: + name: "bulkWrite" + arguments: + requests: + - + name: "deleteOne" + arguments: + filter: { _id: 1 } + - + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + - + name: "updateOne" + arguments: + filter: { _id: 2 } + update: { $inc: { x : 1 }} + options: { ordered: true } + outcome: + result: + deletedCount: 1 + insertedIds: { 1: 3 } + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + upsertedIds: { } + collection: + data: + - { _id: 2, x: 23 } + - { _id: 3, x: 33 } + - + description: "BulkWrite succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "bulkWrite" + arguments: + requests: + - + name: "deleteOne" + arguments: + filter: { _id: 1 } + - + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + - + name: "updateOne" + arguments: + filter: { _id: 2 } + update: { $inc: { x : 1 }} + options: { ordered: true } + outcome: + result: + deletedCount: 1 + insertedIds: { 1: 3 } + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + upsertedIds: { } + collection: + data: + - { _id: 2, x: 23 } + - { _id: 3, x: 33 } diff --git a/t/data/retryable-writes/bulkWrite.json b/t/data/retryable-writes/bulkWrite.json index 7b88ffb3..6029aece 100644 --- a/t/data/retryable-writes/bulkWrite.json +++ b/t/data/retryable-writes/bulkWrite.json @@ -9,7 +9,11 @@ "tests": [ { "description": "First command is retried", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -77,7 +81,11 @@ }, { "description": "All commands are retried", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 7 } @@ -206,7 +214,11 @@ }, { "description": "Both commands are retried after their first statement fails", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 } @@ -283,7 +295,11 @@ }, { "description": "Second command is retried after its second statement fails", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "skip": 2 } @@ -360,7 +376,11 @@ }, { "description": "BulkWrite with unordered execution", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -425,7 +445,11 @@ }, { "description": "First insertOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, @@ -495,7 +519,11 @@ }, { "description": "Second updateOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "skip": 1 }, @@ -571,7 +599,11 @@ }, { "description": "Third updateOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "skip": 2 }, @@ -629,7 +661,140 @@ "result": { "deletedCount": 0, "insertedIds": { - "0": 2 + "1": 2 + }, + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0, + "upsertedIds": {} + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Single-document write following deleteMany is retried", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", + "mode": { + "times": 1 + }, + "data": { + "failBeforeCommitExceptionCode": 1 + } + }, + "operation": { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteMany", + "arguments": { + "filter": { + "x": 11 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "deletedCount": 1, + "insertedIds": { + "1": 2 + }, + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 0, + "upsertedIds": {} + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Single-document write following updateMany is retried", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", + "mode": { + "times": 1 + }, + "data": { + "failBeforeCommitExceptionCode": 1 + } + }, + "operation": { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateMany", + "arguments": { + "filter": { + "x": 11 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "deletedCount": 0, + "insertedIds": { + "1": 2 }, "matchedCount": 1, "modifiedCount": 1, diff --git a/t/data/retryable-writes/bulkWrite.yml b/t/data/retryable-writes/bulkWrite.yml index 167f8943..408aba7e 100644 --- a/t/data/retryable-writes/bulkWrite.yml +++ b/t/data/retryable-writes/bulkWrite.yml @@ -6,7 +6,10 @@ minServerVersion: '3.6' tests: - description: "First command is retried" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "bulkWrite" @@ -42,7 +45,10 @@ tests: # that each write command consists of a single statement, which will # fail on the first attempt and succeed on the second, retry attempt. description: "All commands are retried" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 7 } operation: name: "bulkWrite" @@ -97,7 +103,10 @@ tests: - { _id: 5, x: 55 } - description: "Both commands are retried after their first statement fails" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } operation: name: "bulkWrite" @@ -132,7 +141,10 @@ tests: - { _id: 2, x: 23 } - description: "Second command is retried after its second statement fails" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { skip: 2 } operation: name: "bulkWrite" @@ -167,7 +179,10 @@ tests: - { _id: 2, x: 23 } - description: "BulkWrite with unordered execution" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "bulkWrite" @@ -197,7 +212,10 @@ tests: - { _id: 3, x: 33 } - description: "First insertOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -232,7 +250,10 @@ tests: - { _id: 1, x: 11 } - description: "Second updateOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { skip: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -268,7 +289,10 @@ tests: - { _id: 2, x: 22 } - description: "Third updateOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { skip: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -294,7 +318,81 @@ tests: error: true result: deletedCount: 0 - insertedIds: { 0: 2 } + insertedIds: { 1: 2 } + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + upsertedIds: { } + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } + - + # The onPrimaryTransactionalWrite fail point only triggers for write + # operations that include a transaction ID. Therefore, it will not + # affect the initial deleteMany and will trigger once (and only once) + # for the first insertOne attempt. + description: "Single-document write following deleteMany is retried" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: onPrimaryTransactionalWrite + mode: { times: 1 } + data: { failBeforeCommitExceptionCode: 1 } + operation: + name: "bulkWrite" + arguments: + requests: + - + name: "deleteMany" + arguments: + filter: { x: 11 } + - + name: "insertOne" + arguments: + document: { _id: 2, x: 22 } + options: { ordered: true } + outcome: + result: + deletedCount: 1 + insertedIds: { 1: 2 } + matchedCount: 0 + modifiedCount: 0 + upsertedCount: 0 + upsertedIds: { } + collection: + data: + - { _id: 2, x: 22 } + - + # The onPrimaryTransactionalWrite fail point only triggers for write + # operations that include a transaction ID. Therefore, it will not + # affect the initial updateMany and will trigger once (and only once) + # for the first insertOne attempt. + description: "Single-document write following updateMany is retried" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: onPrimaryTransactionalWrite + mode: { times: 1 } + data: { failBeforeCommitExceptionCode: 1 } + operation: + name: "bulkWrite" + arguments: + requests: + - + name: "updateMany" + arguments: + filter: { x: 11 } + update: { $inc: { x : 1 }} + - + name: "insertOne" + arguments: + document: { _id: 2, x: 22 } + options: { ordered: true } + outcome: + result: + deletedCount: 0 + insertedIds: { 1: 2 } matchedCount: 1 modifiedCount: 1 upsertedCount: 0 diff --git a/t/data/retryable-writes/deleteOne-serverErrors.json b/t/data/retryable-writes/deleteOne-serverErrors.json new file mode 100644 index 00000000..606fc66f --- /dev/null +++ b/t/data/retryable-writes/deleteOne-serverErrors.json @@ -0,0 +1,96 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "DeleteOne succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "delete" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + } + } + }, + "outcome": { + "result": { + "deletedCount": 1 + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "DeleteOne succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "delete" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + } + } + }, + "outcome": { + "result": { + "deletedCount": 1 + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/deleteOne-serverErrors.yml b/t/data/retryable-writes/deleteOne-serverErrors.yml new file mode 100644 index 00000000..006f1513 --- /dev/null +++ b/t/data/retryable-writes/deleteOne-serverErrors.yml @@ -0,0 +1,50 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "DeleteOne succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["delete"] + errorCode: 189 + operation: + name: "deleteOne" + arguments: + filter: { _id: 1 } + outcome: + result: + deletedCount: 1 + collection: + data: + - { _id: 2, x: 22 } + - + description: "DeleteOne succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["delete"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "deleteOne" + arguments: + filter: { _id: 1 } + outcome: + result: + deletedCount: 1 + collection: + data: + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/deleteOne.json b/t/data/retryable-writes/deleteOne.json index a552b893..8e42e9c4 100644 --- a/t/data/retryable-writes/deleteOne.json +++ b/t/data/retryable-writes/deleteOne.json @@ -13,7 +13,11 @@ "tests": [ { "description": "DeleteOne is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -42,7 +46,11 @@ }, { "description": "DeleteOne is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -74,7 +82,11 @@ }, { "description": "DeleteOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/deleteOne.yml b/t/data/retryable-writes/deleteOne.yml index 20d61966..d82bfc05 100644 --- a/t/data/retryable-writes/deleteOne.yml +++ b/t/data/retryable-writes/deleteOne.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "DeleteOne is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "deleteOne" @@ -21,7 +24,10 @@ tests: - { _id: 2, x: 22 } - description: "DeleteOne is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -36,7 +42,10 @@ tests: - { _id: 2, x: 22 } - description: "DeleteOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/findOneAndDelete-serverErrors.json b/t/data/retryable-writes/findOneAndDelete-serverErrors.json new file mode 100644 index 00000000..306b1423 --- /dev/null +++ b/t/data/retryable-writes/findOneAndDelete-serverErrors.json @@ -0,0 +1,108 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "FindOneAndDelete succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "findOneAndDelete", + "arguments": { + "filter": { + "x": { + "$gte": 11 + } + }, + "sort": { + "x": 1 + } + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndDelete succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "findOneAndDelete", + "arguments": { + "filter": { + "x": { + "$gte": 11 + } + }, + "sort": { + "x": 1 + } + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/findOneAndDelete-serverErrors.yml b/t/data/retryable-writes/findOneAndDelete-serverErrors.yml new file mode 100644 index 00000000..75071a36 --- /dev/null +++ b/t/data/retryable-writes/findOneAndDelete-serverErrors.yml @@ -0,0 +1,50 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "FindOneAndDelete succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + errorCode: 189 + operation: + name: "findOneAndDelete" + arguments: + filter: { x: { $gte: 11 }} + sort: { x: 1 } + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 2, x: 22 } + - + description: "FindOneAndDelete succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "findOneAndDelete" + arguments: + filter: { x: { $gte: 11 }} + sort: { x: 1 } + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/findOneAndDelete.json b/t/data/retryable-writes/findOneAndDelete.json index d8f6c8fa..5963c432 100644 --- a/t/data/retryable-writes/findOneAndDelete.json +++ b/t/data/retryable-writes/findOneAndDelete.json @@ -13,7 +13,11 @@ "tests": [ { "description": "FindOneAndDelete is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -48,7 +52,11 @@ }, { "description": "FindOneAndDelete is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -86,7 +94,11 @@ }, { "description": "FindOneAndDelete is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/findOneAndDelete.yml b/t/data/retryable-writes/findOneAndDelete.yml index d945bc4d..4f5d71ca 100644 --- a/t/data/retryable-writes/findOneAndDelete.yml +++ b/t/data/retryable-writes/findOneAndDelete.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "FindOneAndDelete is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "findOneAndDelete" @@ -21,7 +24,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndDelete is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -36,7 +42,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndDelete is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/findOneAndReplace-serverErrors.json b/t/data/retryable-writes/findOneAndReplace-serverErrors.json new file mode 100644 index 00000000..ab209b8d --- /dev/null +++ b/t/data/retryable-writes/findOneAndReplace-serverErrors.json @@ -0,0 +1,116 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "FindOneAndReplace succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "_id": 1, + "x": 111 + }, + "returnDocument": "Before" + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 111 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndReplace succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "_id": 1, + "x": 111 + }, + "returnDocument": "Before" + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 111 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/findOneAndReplace-serverErrors.yml b/t/data/retryable-writes/findOneAndReplace-serverErrors.yml new file mode 100644 index 00000000..8b6a2012 --- /dev/null +++ b/t/data/retryable-writes/findOneAndReplace-serverErrors.yml @@ -0,0 +1,54 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "FindOneAndReplace succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + errorCode: 189 + operation: + name: "findOneAndReplace" + arguments: + filter: { _id: 1 } + replacement: { _id: 1, x: 111 } + returnDocument: "Before" + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 1, x: 111 } + - { _id: 2, x: 22 } + - + description: "FindOneAndReplace succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "findOneAndReplace" + arguments: + filter: { _id: 1 } + replacement: { _id: 1, x: 111 } + returnDocument: "Before" + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 1, x: 111 } + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/findOneAndReplace.json b/t/data/retryable-writes/findOneAndReplace.json index 22c03d74..20b9e0bc 100644 --- a/t/data/retryable-writes/findOneAndReplace.json +++ b/t/data/retryable-writes/findOneAndReplace.json @@ -13,7 +13,11 @@ "tests": [ { "description": "FindOneAndReplace is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -52,7 +56,11 @@ }, { "description": "FindOneAndReplace is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -94,7 +102,11 @@ }, { "description": "FindOneAndReplace is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/findOneAndReplace.yml b/t/data/retryable-writes/findOneAndReplace.yml index f2dfc572..6e26d7cf 100644 --- a/t/data/retryable-writes/findOneAndReplace.yml +++ b/t/data/retryable-writes/findOneAndReplace.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "FindOneAndReplace is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "findOneAndReplace" @@ -23,7 +26,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndReplace is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -40,7 +46,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndReplace is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/findOneAndUpdate-serverErrors.json b/t/data/retryable-writes/findOneAndUpdate-serverErrors.json new file mode 100644 index 00000000..92f09ce9 --- /dev/null +++ b/t/data/retryable-writes/findOneAndUpdate-serverErrors.json @@ -0,0 +1,118 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "FindOneAndUpdate succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndUpdate succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/findOneAndUpdate-serverErrors.yml b/t/data/retryable-writes/findOneAndUpdate-serverErrors.yml new file mode 100644 index 00000000..7eddcdc0 --- /dev/null +++ b/t/data/retryable-writes/findOneAndUpdate-serverErrors.yml @@ -0,0 +1,54 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "FindOneAndUpdate succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + errorCode: 189 + operation: + name: "findOneAndUpdate" + arguments: + filter: { _id: 1 } + update: { $inc: { x : 1 }} + returnDocument: "Before" + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } + - + description: "FindOneAndUpdate succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "findOneAndUpdate" + arguments: + filter: { _id: 1 } + update: { $inc: { x : 1 }} + returnDocument: "Before" + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/findOneAndUpdate.json b/t/data/retryable-writes/findOneAndUpdate.json index 11a76ab1..92d4f54b 100644 --- a/t/data/retryable-writes/findOneAndUpdate.json +++ b/t/data/retryable-writes/findOneAndUpdate.json @@ -13,7 +13,11 @@ "tests": [ { "description": "FindOneAndUpdate is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -53,7 +57,11 @@ }, { "description": "FindOneAndUpdate is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -96,7 +104,11 @@ }, { "description": "FindOneAndUpdate is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/findOneAndUpdate.yml b/t/data/retryable-writes/findOneAndUpdate.yml index c9fc7672..a82bd034 100644 --- a/t/data/retryable-writes/findOneAndUpdate.yml +++ b/t/data/retryable-writes/findOneAndUpdate.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "FindOneAndUpdate is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "findOneAndUpdate" @@ -23,7 +26,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndUpdate is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -40,7 +46,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndUpdate is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/insertMany-serverErrors.json b/t/data/retryable-writes/insertMany-serverErrors.json new file mode 100644 index 00000000..17ba9b14 --- /dev/null +++ b/t/data/retryable-writes/insertMany-serverErrors.json @@ -0,0 +1,134 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "InsertMany succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "insertMany", + "arguments": { + "documents": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "insertedIds": { + "0": 2, + "1": 3 + } + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertMany succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertMany", + "arguments": { + "documents": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "insertedIds": { + "0": 2, + "1": 3 + } + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/insertMany-serverErrors.yml b/t/data/retryable-writes/insertMany-serverErrors.yml new file mode 100644 index 00000000..aafaee81 --- /dev/null +++ b/t/data/retryable-writes/insertMany-serverErrors.yml @@ -0,0 +1,59 @@ +data: + - { _id: 1, x: 11 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "InsertMany succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 189 + operation: + name: "insertMany" + arguments: + documents: + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + options: { ordered: true } + outcome: + result: + insertedIds: { 0: 2, 1: 3 } + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertMany succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "insertMany" + arguments: + documents: + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + options: { ordered: true } + outcome: + result: + insertedIds: { 0: 2, 1: 3 } + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } diff --git a/t/data/retryable-writes/insertMany.json b/t/data/retryable-writes/insertMany.json index 2d71cb91..74dd4a7a 100644 --- a/t/data/retryable-writes/insertMany.json +++ b/t/data/retryable-writes/insertMany.json @@ -9,7 +9,11 @@ "tests": [ { "description": "InsertMany succeeds after one network error", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -59,7 +63,11 @@ }, { "description": "InsertMany with unordered execution", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -109,7 +117,11 @@ }, { "description": "InsertMany fails after multiple network errors", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": "alwaysOn", "data": { "failBeforeCommitExceptionCode": 1 diff --git a/t/data/retryable-writes/insertMany.yml b/t/data/retryable-writes/insertMany.yml index f3ff5d2f..7559e340 100644 --- a/t/data/retryable-writes/insertMany.yml +++ b/t/data/retryable-writes/insertMany.yml @@ -6,7 +6,10 @@ minServerVersion: '3.6' tests: - description: "InsertMany succeeds after one network error" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "insertMany" @@ -25,7 +28,10 @@ tests: - { _id: 3, x: 33 } - description: "InsertMany with unordered execution" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "insertMany" @@ -44,6 +50,8 @@ tests: - { _id: 3, x: 33 } - description: "InsertMany fails after multiple network errors" + clientOptions: + retryWrites: true failPoint: # Normally, a mongod will insert the documents as a batch with a # single commit. If this fails, mongod may try to insert each @@ -51,6 +59,7 @@ tests: # single insert command may trigger the failpoint twice on each # driver attempt. This test permanently enables the fail point to # ensure the retry attempt always fails. + configureFailPoint: onPrimaryTransactionalWrite mode: "alwaysOn" data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/insertOne-serverErrors.json b/t/data/retryable-writes/insertOne-serverErrors.json new file mode 100644 index 00000000..9ef84370 --- /dev/null +++ b/t/data/retryable-writes/insertOne-serverErrors.json @@ -0,0 +1,1000 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "InsertOne succeeds after connection failure", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "closeConnection": true + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after NotMaster", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 10107, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after NotMasterOrSecondary", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 13436, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after NotMasterNoSlaveOk", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 13435, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after InterruptedDueToReplStateChange", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 11602, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after InterruptedAtShutdown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 11600, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 189, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 91, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after HostNotFound", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 7, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after HostUnreachable", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 6, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after SocketException", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 9001, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after NetworkTimeout", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 89, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne fails after Interrupted", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 11601, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "error": true, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after WriteConcernError InterruptedAtShutdown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 11600, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after WriteConcernError InterruptedDueToReplStateChange", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 11602, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after WriteConcernError PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 189, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne fails after multiple retryable writeConcernErrors", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "error": true, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne fails after WriteConcernError Interrupted", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 11601, + "errmsg": "operation was interrupted" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "error": true, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne fails after WriteConcernError WriteConcernFailed", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 64, + "codeName": "WriteConcernFailed", + "errmsg": "waiting for replication timed out", + "errInfo": { + "wtimeout": true + } + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "error": true, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/insertOne-serverErrors.yml b/t/data/retryable-writes/insertOne-serverErrors.yml new file mode 100644 index 00000000..69173b9c --- /dev/null +++ b/t/data/retryable-writes/insertOne-serverErrors.yml @@ -0,0 +1,471 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "InsertOne succeeds after connection failure" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + closeConnection: true + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after NotMaster" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 10107 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after NotMasterOrSecondary" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 13436 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after NotMasterNoSlaveOk" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 13435 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after InterruptedDueToReplStateChange" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 11602 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after InterruptedAtShutdown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 11600 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 189 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 91 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after HostNotFound" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 7 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after HostUnreachable" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 6 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after SocketException" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 9001 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after NetworkTimeout" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 89 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne fails after Interrupted" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 11601 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + error: true + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - + description: "InsertOne succeeds after WriteConcernError InterruptedAtShutdown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 11600 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after WriteConcernError InterruptedDueToReplStateChange" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 11602 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after WriteConcernError PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 189 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne fails after multiple retryable writeConcernErrors" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["insert"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + error: true + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } # The write was still applied. + - + description: "InsertOne fails after WriteConcernError Interrupted" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 11601 + errmsg: operation was interrupted + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + error: true + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } # The write was still applied. + - + description: "InsertOne fails after WriteConcernError WriteConcernFailed" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 64 + codeName: WriteConcernFailed + errmsg: waiting for replication timed out + errInfo: {wtimeout: True} + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + error: true + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } # The write was still applied. diff --git a/t/data/retryable-writes/insertOne.json b/t/data/retryable-writes/insertOne.json index 58ad6cc6..123d51ed 100644 --- a/t/data/retryable-writes/insertOne.json +++ b/t/data/retryable-writes/insertOne.json @@ -13,7 +13,11 @@ "tests": [ { "description": "InsertOne is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -51,7 +55,11 @@ }, { "description": "InsertOne is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -92,7 +100,11 @@ }, { "description": "InsertOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/insertOne.yml b/t/data/retryable-writes/insertOne.yml index 5a303ddc..0e535ddb 100644 --- a/t/data/retryable-writes/insertOne.yml +++ b/t/data/retryable-writes/insertOne.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "InsertOne is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "insertOne" @@ -23,7 +26,10 @@ tests: - { _id: 3, x: 33 } - description: "InsertOne is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -40,7 +46,10 @@ tests: - { _id: 3, x: 33 } - description: "InsertOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/replaceOne-serverErrors.json b/t/data/retryable-writes/replaceOne-serverErrors.json new file mode 100644 index 00000000..b9c449a8 --- /dev/null +++ b/t/data/retryable-writes/replaceOne-serverErrors.json @@ -0,0 +1,116 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "ReplaceOne succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "_id": 1, + "x": 111 + } + } + }, + "outcome": { + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 111 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "ReplaceOne succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "_id": 1, + "x": 111 + } + } + }, + "outcome": { + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 111 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/replaceOne-serverErrors.yml b/t/data/retryable-writes/replaceOne-serverErrors.yml new file mode 100644 index 00000000..bd2bb0f3 --- /dev/null +++ b/t/data/retryable-writes/replaceOne-serverErrors.yml @@ -0,0 +1,58 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "ReplaceOne succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + errorCode: 189 + operation: + name: "replaceOne" + arguments: + filter: { _id: 1 } + replacement: { _id: 1, x: 111 } + outcome: + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + collection: + data: + - { _id: 1, x: 111 } + - { _id: 2, x: 22 } + - + description: "ReplaceOne succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "replaceOne" + arguments: + filter: { _id: 1 } + replacement: { _id: 1, x: 111 } + outcome: + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + collection: + data: + - { _id: 1, x: 111 } + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/replaceOne.json b/t/data/retryable-writes/replaceOne.json index f31966c3..6e9107b7 100644 --- a/t/data/retryable-writes/replaceOne.json +++ b/t/data/retryable-writes/replaceOne.json @@ -13,7 +13,11 @@ "tests": [ { "description": "ReplaceOne is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -52,7 +56,11 @@ }, { "description": "ReplaceOne is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -94,7 +102,11 @@ }, { "description": "ReplaceOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/replaceOne.yml b/t/data/retryable-writes/replaceOne.yml index 08fbda9a..1eed95ed 100644 --- a/t/data/retryable-writes/replaceOne.yml +++ b/t/data/retryable-writes/replaceOne.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "ReplaceOne is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "replaceOne" @@ -25,7 +28,10 @@ tests: - { _id: 2, x: 22 } - description: "ReplaceOne is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -44,7 +50,10 @@ tests: - { _id: 2, x: 22 } - description: "ReplaceOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/updateOne-serverErrors.json b/t/data/retryable-writes/updateOne-serverErrors.json new file mode 100644 index 00000000..84ebf877 --- /dev/null +++ b/t/data/retryable-writes/updateOne-serverErrors.json @@ -0,0 +1,118 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "UpdateOne succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + "outcome": { + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "UpdateOne succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + "outcome": { + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/updateOne-serverErrors.yml b/t/data/retryable-writes/updateOne-serverErrors.yml new file mode 100644 index 00000000..1d468861 --- /dev/null +++ b/t/data/retryable-writes/updateOne-serverErrors.yml @@ -0,0 +1,58 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "UpdateOne succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + errorCode: 189 + operation: + name: "updateOne" + arguments: + filter: { _id: 1 } + update: { $inc: { x : 1 }} + outcome: + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } + - + description: "UpdateOne succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "updateOne" + arguments: + filter: { _id: 1 } + update: { $inc: { x : 1 }} + outcome: + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/updateOne.json b/t/data/retryable-writes/updateOne.json index 96aa2bde..c342c562 100644 --- a/t/data/retryable-writes/updateOne.json +++ b/t/data/retryable-writes/updateOne.json @@ -13,7 +13,11 @@ "tests": [ { "description": "UpdateOne is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -53,7 +57,11 @@ }, { "description": "UpdateOne is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -96,7 +104,11 @@ }, { "description": "UpdateOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, @@ -135,7 +147,11 @@ }, { "description": "UpdateOne with upsert is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -182,7 +198,11 @@ }, { "description": "UpdateOne with upsert is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -232,7 +252,11 @@ }, { "description": "UpdateOne with upsert is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/updateOne.yml b/t/data/retryable-writes/updateOne.yml index 810e49f2..900d4e7f 100644 --- a/t/data/retryable-writes/updateOne.yml +++ b/t/data/retryable-writes/updateOne.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "UpdateOne is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "updateOne" @@ -25,7 +28,10 @@ tests: - { _id: 2, x: 22 } - description: "UpdateOne is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -44,7 +50,10 @@ tests: - { _id: 2, x: 22 } - description: "UpdateOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -60,7 +69,10 @@ tests: - { _id: 2, x: 22 } - description: "UpdateOne with upsert is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "updateOne" @@ -81,7 +93,10 @@ tests: - { _id: 3, x: 34 } - description: "UpdateOne with upsert is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -103,7 +118,10 @@ tests: - { _id: 3, x: 34 } - description: "UpdateOne with upsert is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/transactions/README.rst b/t/data/transactions/README.rst new file mode 100644 index 00000000..838fb588 --- /dev/null +++ b/t/data/transactions/README.rst @@ -0,0 +1,270 @@ +================== +Transactions Tests +================== + +.. contents:: + +---- + +Introduction +============ + +The YAML and JSON files in this directory are platform-independent tests that +drivers can use to prove their conformance to the Transactions Spec. They are +designed with the intention of sharing some test-runner code with the CRUD Spec +tests and the Command Monitoring Spec tests. + +Several prose tests, which are not easily expressed in YAML, are also presented +in this file. Those tests will need to be manually implemented by each driver. + +Server Fail Point +================= + +Some tests depend on a server fail point, expressed in the ``failPoint`` field. +For example the ``failCommand`` fail point allows the client to force the +server to return an error. Keep in mind that the fail point only triggers for +commands listed in the "failCommands" field. See `SERVER-35004`_ and +`SERVER-35083`_ for more information. + +.. _SERVER-35004: https://jira.mongodb.org/browse/SERVER-35004 +.. _SERVER-35083: https://jira.mongodb.org/browse/SERVER-35083 + +The ``failCommand`` fail point may be configured like so:: + + db.adminCommand({ + configureFailPoint: "failCommand", + mode: , + data: { + failCommands: ["commandName", "commandName2"], + closeConnection: , + errorCode: , + writeConcernError: + } + }); + +``mode`` is a generic fail point option and may be assigned a string or document +value. The string values ``"alwaysOn"`` and ``"off"`` may be used to enable or +disable the fail point, respectively. A document may be used to specify either +``times`` or ``skip``, which are mutually exclusive: + +- ``{ times: }`` may be used to limit the number of times the fail + point may trigger before transitioning to ``"off"``. +- ``{ skip: }`` may be used to defer the first trigger of a fail + point, after which it will transition to ``"alwaysOn"``. + +The ``data`` option is a document that may be used to specify options that +control the fail point's behavior. ``failCommand`` supports the following +``data`` options, which may be combined if desired: + +- ``failCommands``: Required, the list of command names to fail. +- ``closeConnection``: Boolean option, which defaults to ``false``. If + ``true``, the connection on which the command is executed will be closed + and the client will see a network error. +- ``errorCode``: Integer option, which is unset by default. If set, the + specified command error code will be returned as a command error. +- ``writeConcernError``: A document, which is unset by default. If set, the + server will return this document in the "writeConcernError" field. This + failure response only applies to commands that support write concern and + happens *after* the command finishes (regardless of success or failure). + +Test Format +=========== + +Each YAML file has the following keys: + +- ``database_name`` and ``collection_name``: The database and collection to use + for testing. + +- ``data``: The data that should exist in the collection under test before each + test run. + +- ``tests``: An array of tests that are to be run independently of each other. + Each test will have some or all of the following fields: + + - ``description``: The name of the test. + + - ``clientOptions``: Optional, parameters to pass to MongoClient(). + + - ``failPoint``: Optional, a server failpoint to enable expressed as the + configureFailPoint command to run on the admin database. + + - ``sessionOptions``: Optional, parameters to pass to + MongoClient.startSession(). + + - ``operations``: Array of documents, each describing an operation to be + executed. Each document has the following fields: + + - ``name``: The name of the operation on ``object``. + + - ``object``: The name of the object to perform the operation on. Can be + "database", "collection", "session0", or "session1". + + - ``collectionOptions``: Optional, parameters to pass to the Collection() + used for this operation. + + - ``command_name``: Present only when ``name`` is "runCommand". The name + of the command to run. Required for languages that are unable preserve + the order keys in the "command" argument when parsing JSON/YAML. + + - ``arguments``: Optional, the names and values of arguments. + + - ``result``: The return value from the operation, if any. This field may + be a single document or an array of documents in the case of a + multi-document read. If the operation is expected to return an error, the + ``result`` is a single document that has one or more of the following + fields: + + - ``errorContains``: A substring of the expected error message. + + - ``errorCodeName``: The expected "codeName" field in the server + error response. + + - ``errorLabelsContain``: A list of error label strings that the + error is expected to have. + + - ``errorLabelsOmit``: A list of error label strings that the + error is expected not to have. + + - ``expectations``: Optional list of command-started events. + + - ``outcome``: Document describing the return value and/or expected state of + the collection after the operation is executed. Contains the following + fields: + + - ``collection``: + + - ``data``: The data that should exist in the collection after the + operations have run. + +Use as integration tests +======================== + +Run a MongoDB replica set with a primary, a secondary, and an arbiter, +**server version 4.0.0-rc4 or later**. (Including a secondary ensures that +server selection in a transaction works properly. Including an arbiter helps +ensure that no new bugs have been introduced related to arbiters.) + +Load each YAML (or JSON) file using a Canonical Extended JSON parser. + +Then for each element in ``tests``: + +#. Create a MongoClient and call + ``client.admin.runCommand({killAllSessions: []})`` to clean up any open + transactions from previous test failures. The command will fail with message + "operation was interrupted", because it kills its own implicit session. Catch + the exception and continue. +#. Create a collection object from the MongoClient, using the ``database_name`` + and ``collection_name`` fields of the YAML file. +#. Drop the test collection, using writeConcern "majority". +#. Execute the "create" command to recreate the collection, using writeConcern + "majority". (Creating the collection inside a transaction is prohibited, so + create it explicitly.) +#. If the YAML file contains a ``data`` array, insert the documents in ``data`` + into the test collection, using writeConcern "majority". +#. If ``failPoint`` is specified, its value is a configureFailPoint command. + Run the command on the admin database to enable the fail point. +#. Create a **new** MongoClient ``client``, with Command Monitoring listeners + enabled. (Using a new MongoClient for each test ensures a fresh session pool + that hasn't executed any transactions previously, so the tests can assert + actual txnNumbers, starting from 1.) Pass this test's ``clientOptions`` if + present. +#. Call ``client.startSession`` twice to create ClientSession objects + ``session0`` and ``session1``, using the test's "sessionOptions" if they + are present. Save their lsids so they are available after calling + ``endSession``, see `Logical Session Id`. +#. For each element in ``operations``: + + - Enter a "try" block or your programming language's closest equivalent. + - Create a Database object from the MongoClient, using the ``database_name`` + field at the top level of the test file. + - Create a Collection object from the Database, using the + ``collection_name`` field at the top level of the test file. + If ``collectionOptions`` is present create the Collection object with the + provided options. Otherwise create the object with the default options. + - Execute the named method on the provided ``object``, passing the + arguments listed. Pass ``session0`` or ``session1`` to the method, + depending on which session's name is in the arguments list. + If ``arguments`` contains no "session", pass no explicit session to the + method. + - If the driver throws an exception / returns an error while executing this + series of operations, store the error message and server error code. + - If the result document has an "errorContains" field, verify that the + method threw an exception or returned an error, and that the value of the + "errorContains" field matches the error string. "errorContains" is a + substring (case-insensitive) of the actual error message. + + If the result document has an "errorCodeName" field, verify that the + method threw a command failed exception or returned an error, and that + the value of the "errorCodeName" field matches the "codeName" in the + server error response. + + If the result document has an "errorLabelsContain" field, verify that the + method threw an exception or returned an error. Verify that all of the + error labels in "errorLabelsContain" are present in the error or exception + using the ``hasErrorLabel`` method. + + If the result document has an "errorLabelsOmit" field, verify that the + method threw an exception or returned an error. Verify that none of the + error labels in "errorLabelsOmit" are present in the error or exception + using the ``hasErrorLabel`` method. + - If the operation returns a raw command response, eg from ``runCommand``, + then compare only the fields present in the expected result document. + Otherwise, compare the method's return value to ``result`` using the same + logic as the CRUD Spec Tests runner. + +#. Call ``session0.endSession()`` and ``session1.endSession``. +#. If the test includes a list of command-started events in ``expectations``, + compare them to the actual command-started events using the + same logic as the Command Monitoring Spec Tests runner, plus the rules in + the Command-Started Events instructions below. +#. If ``failPoint`` is specified, disable the fail point to avoid spurious + failures in subsequent tests. The fail point may be disabled like so:: + + db.adminCommand({ + configureFailPoint: , + mode: "off" + }); + +#. For each element in ``outcome``: + + - If ``name`` is "collection", verify that the test collection contains + exactly the documents in the ``data`` array. Ensure this find uses + Primary read preference even when the MongoClient is configured with + another read preference. + +Command-Started Events +`````````````````````` + +The event listener used for these tests MUST ignore the security commands +listed in the Command Monitoring Spec. + +Logical Session Id +~~~~~~~~~~~~~~~~~~ + +Each command-started event in ``expectations`` includes an ``lsid`` with the +value "session0" or "session1". Tests MUST assert that the command's actual +``lsid`` matches the id of the correct ClientSession named ``session0`` or +``session1``. + +Null Values +~~~~~~~~~~~ + +Some command-started events in ``expectations`` include ``null`` values for +fields such as ``txnNumber``, ``autocommit``, and ``writeConcern``. +Tests MUST assert that the actual command **omits** any field that has a +``null`` value in the expected command. + +Cursor Id +^^^^^^^^^ + +A ``getMore`` value of ``"42"`` in a command-started event is a fake cursorId +that MUST be ignored. (In the Command Monitoring Spec tests, fake cursorIds are +correlated with real ones, but that is not necessary for Transactions Spec +tests.) + +afterClusterTime +^^^^^^^^^^^^^^^^ + +A ``readConcern.afterClusterTime`` value of ``42`` in a command-started event +is a fake cluster time. Drivers MUST assert that the actual command includes an +afterClusterTime. diff --git a/t/data/transactions/abort.json b/t/data/transactions/abort.json new file mode 100644 index 00000000..084429b7 --- /dev/null +++ b/t/data/transactions/abort.json @@ -0,0 +1,607 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "implicit abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "two aborts", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "cannot call abortTransaction twice" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort without start", + "operations": [ + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "no transaction started" + } + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort directly after no-op commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "Cannot call abortTransaction after calling commitTransaction" + } + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort directly after commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "Cannot call abortTransaction after calling commitTransaction" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "abort ignores TransactionAborted", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorCodeName": "DuplicateKey", + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorCodeName": "NoSuchTransaction", + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort does not apply writeConcern", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": 10 + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [] + } + } + } + ] +} diff --git a/t/data/transactions/abort.yml b/t/data/transactions/abort.yml new file mode 100644 index 00000000..f1bbca76 --- /dev/null +++ b/t/data/transactions/abort.yml @@ -0,0 +1,403 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: abort + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: implicit abort + + operations: + # Start a transaction but don't commit - the driver calls abortTransaction + # from ClientSession.endSession(). + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: two aborts + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + - name: abortTransaction + object: session0 + result: + errorContains: cannot call abortTransaction twice + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abort without start + + operations: + - name: abortTransaction + object: session0 + result: + errorContains: no transaction started + + expectations: [] + + outcome: + collection: + data: [] + + - description: abort directly after no-op commit + + operations: + - name: startTransaction + object: session0 + - name: commitTransaction + object: session0 + - name: abortTransaction # Error calling abort after no-op commit. + object: session0 + result: + errorContains: Cannot call abortTransaction after calling commitTransaction + + expectations: [] + + outcome: + collection: + data: [] + + - description: abort directly after commit + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + - name: abortTransaction # Error calling abort after commit. + object: session0 + result: + errorContains: Cannot call abortTransaction after calling commitTransaction + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: abort ignores TransactionAborted + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + # Abort the server transaction with a duplicate key error. + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + errorCodeName: DuplicateKey + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + # Make sure the server aborted the transaction. + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + errorCodeName: NoSuchTransaction + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + # abortTransaction must ignore the TransactionAborted and succeed. + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + 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: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + 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: + command_name: abortTransaction + database_name: admin + + + outcome: + collection: + data: [] + + - description: abort does not apply writeConcern + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: 10 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + # No write concern error. + + outcome: + collection: + data: [] diff --git a/t/data/transactions/bulk.json b/t/data/transactions/bulk.json new file mode 100644 index 00000000..c5a572df --- /dev/null +++ b/t/data/transactions/bulk.json @@ -0,0 +1,524 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "bulk", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "deletedCount": 1 + } + }, + { + "name": "bulkWrite", + "object": "collection", + "arguments": { + "session": "session0", + "requests": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + } + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "x": 1 + } + } + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$set": { + "x": 2 + } + }, + "upsert": true + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 4 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 5 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 6 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 7 + } + } + }, + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "y": 1 + } + } + }, + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 2 + }, + "replacement": { + "y": 2 + } + } + }, + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 3 + } + } + }, + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 4 + } + } + }, + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gte": 2 + } + }, + "update": { + "$set": { + "z": 1 + } + } + } + }, + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gte": 6 + } + } + } + } + ] + }, + "result": { + "deletedCount": 4, + "insertedIds": { + "0": 1, + "3": 3, + "4": 4, + "5": 5, + "6": 6, + "7": 7 + }, + "matchedCount": 7, + "modifiedCount": 7, + "upsertedCount": 1, + "upsertedIds": { + "2": 2 + } + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 1 + }, + "limit": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$set": { + "x": 1 + } + }, + "multi": false, + "upsert": false + }, + { + "q": { + "_id": 2 + }, + "u": { + "$set": { + "x": 2 + } + }, + "multi": false, + "upsert": true + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 3 + }, + { + "_id": 4 + }, + { + "_id": 5 + }, + { + "_id": 6 + }, + { + "_id": 7 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "y": 1 + }, + "multi": false, + "upsert": false + }, + { + "q": { + "_id": 2 + }, + "u": { + "y": 2 + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 3 + }, + "limit": 1 + }, + { + "q": { + "_id": 4 + }, + "limit": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gte": 2 + } + }, + "u": { + "$set": { + "z": 1 + } + }, + "multi": true, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": { + "$gte": 6 + } + }, + "limit": 0 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "y": 1 + }, + { + "_id": 2, + "y": 2, + "z": 1 + }, + { + "_id": 5, + "z": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/bulk.yml b/t/data/transactions/bulk.yml new file mode 100644 index 00000000..f940f71f --- /dev/null +++ b/t/data/transactions/bulk.yml @@ -0,0 +1,267 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: bulk + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: deleteOne + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + deletedCount: 1 + - name: bulkWrite + object: collection + arguments: + session: session0 + requests: + - name: insertOne + arguments: + document: {_id: 1} + - name: updateOne + arguments: + filter: {_id: 1} + update: {$set: {x: 1}} + - name: updateOne + arguments: + filter: {_id: 2} + update: {$set: {x: 2}} + upsert: true # Produces upsertedIds: {2: 2} in the result. + - name: insertOne + arguments: + document: {_id: 3} + - name: insertOne + arguments: + document: {_id: 4} + - name: insertOne + arguments: + document: {_id: 5} + - name: insertOne + arguments: + document: {_id: 6} + - name: insertOne + arguments: + document: {_id: 7} + # Keep replaces segregated from updates, so that drivers that aren't able to coalesce + # adjacent updates and replaces into a single update command will still pass this test + - name: replaceOne + arguments: + filter: {_id: 1} + replacement: {y: 1} + - name: replaceOne + arguments: + filter: {_id: 2} + replacement: {y: 2} + - name: deleteOne + arguments: + filter: {_id: 3} + - name: deleteOne + arguments: + filter: {_id: 4} + - name: updateMany + arguments: + filter: {_id: {$gte: 2}} + update: {$set: {z: 1}} + # Keep deleteMany segregated from deleteOne, so that drivers that aren't able to coalesce + # adjacent mixed deletes into a single delete command will still pass this test + - name: deleteMany + arguments: + filter: {_id: {$gte: 6}} + result: + deletedCount: 4 + insertedIds: {0: 1, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7} + matchedCount: 7 + modifiedCount: 7 + upsertedCount: 1 + upsertedIds: {2: 2} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 1} + limit: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + # Commands in the bulkWrite. + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {$set: {x: 1}} + multi: false + upsert: false + - q: {_id: 2} + u: {$set: {x: 2}} + multi: false + upsert: true + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 3 + - _id: 4 + - _id: 5 + - _id: 6 + - _id: 7 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {y: 1} + multi: false + upsert: false + - q: {_id: 2} + u: {y: 2} + multi: false + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 3} + limit: 1 + - q: {_id: 4} + limit: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: {$gte: 2}} + u: {$set: {z: 1}} + multi: true + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: {$gte: 6}} + limit: 0 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1, y: 1} + - {_id: 2, y: 2, z: 1} + - {_id: 5, z: 1} diff --git a/t/data/transactions/causal-consistency.json b/t/data/transactions/causal-consistency.json new file mode 100644 index 00000000..567350f3 --- /dev/null +++ b/t/data/transactions/causal-consistency.json @@ -0,0 +1,294 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1, + "count": 0 + } + ], + "tests": [ + { + "description": "causal consistency", + "operations": [ + { + "name": "updateOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "count": 1 + } + }, + "upsert": false + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "updateOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "count": 1 + } + }, + "upsert": false + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "count": 1 + } + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "readConcern": null, + "txnNumber": null, + "startTransaction": null, + "autocommit": null, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "count": 1 + } + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "count": 2 + } + ] + } + } + }, + { + "description": "causal consistency disabled", + "sessionOptions": { + "session0": { + "causalConsistency": false + } + }, + "operations": [ + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "updateOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "count": 1 + } + }, + "upsert": false + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": null, + "autocommit": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "count": 1 + } + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "count": 1 + }, + { + "_id": 2 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/causal-consistency.yml b/t/data/transactions/causal-consistency.yml new file mode 100644 index 00000000..bfb85725 --- /dev/null +++ b/t/data/transactions/causal-consistency.yml @@ -0,0 +1,173 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + count: 0 + +tests: + - description: causal consistency + + operations: + # Update a document without a transaction. + - &updateOne + name: updateOne + object: collection + arguments: + session: session0 + filter: {_id: 1} + update: + $inc: {count: 1} + upsert: false + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + # Updating the same document inside a transaction. + # Casual consistency ensures that the transaction snapshot is causally + # after the first updateOne. + - name: startTransaction + object: session0 + - *updateOne + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {$inc: {count: 1}} + multi: false + upsert: false + ordered: true + lsid: session0 + readConcern: + txnNumber: + startTransaction: + autocommit: + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {$inc: {count: 1}} + multi: false + upsert: false + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + count: 2 + + - description: causal consistency disabled + + sessionOptions: + session0: + causalConsistency: false + + operations: + # Insert a document without a transaction. + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: startTransaction + object: session0 + - name: updateOne + object: collection + arguments: + session: session0 + filter: {_id: 1} + update: + $inc: {count: 1} + upsert: false + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + lsid: session0 + txnNumber: + autocommit: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {$inc: {count: 1}} + multi: false + upsert: false + ordered: true + # No afterClusterTime + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + count: 1 + - _id: 2 diff --git a/t/data/transactions/commit.json b/t/data/transactions/commit.json new file mode 100644 index 00000000..173fae6c --- /dev/null +++ b/t/data/transactions/commit.json @@ -0,0 +1,899 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + } + }, + { + "description": "rerun commit after empty transaction", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "multiple commits in a row", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "write concern error on commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": 10 + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commit without start", + "operations": [ + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorContains": "no transaction started" + } + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "commit after no-op abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorContains": "Cannot call commitTransaction after calling abortTransaction" + } + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "commit after abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorContains": "Cannot call commitTransaction after calling abortTransaction" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ] + }, + { + "description": "multiple commits after empty transaction", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "reset session state commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorContains": "no transaction started" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": null, + "startTransaction": null, + "autocommit": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + } + }, + { + "description": "reset session state abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "no transaction started" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": null, + "startTransaction": null, + "autocommit": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 2 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/commit.yml b/t/data/transactions/commit.yml new file mode 100644 index 00000000..32f73099 --- /dev/null +++ b/t/data/transactions/commit.yml @@ -0,0 +1,593 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] +tests: + - description: commit + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Again, to verify that txnNumber is incremented. + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + + - description: rerun commit after empty transaction + + operations: + - name: startTransaction + object: session0 + - name: commitTransaction + object: session0 + # Rerun the commit (which does not increment the txnNumber). + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: multiple commits in a row + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + - name: commitTransaction + object: session0 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: write concern error on commit + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: 10 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + # { + # 'ok': 1.0, + # 'writeConcernError': { + # 'code': 100, + # 'codeName': 'UnsatisfiableWriteConcern', + # 'errmsg': 'Not enough data-bearing nodes' + # } + # } + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + + outcome: + collection: + data: + - _id: 1 + + - description: commit without start + + operations: + - name: commitTransaction + object: session0 + result: + errorContains: no transaction started + + expectations: [] + + outcome: + collection: + data: [] + + - description: commit after no-op abort + + operations: + - name: startTransaction + object: session0 + - name: abortTransaction + object: session0 + - name: commitTransaction + object: session0 + result: + errorContains: Cannot call commitTransaction after calling abortTransaction + + expectations: [] + + outcome: + collection: + data: [] + + - description: commit after abort + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + - name: commitTransaction + object: session0 + result: + errorContains: Cannot call commitTransaction after calling abortTransaction + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + - description: multiple commits after empty transaction + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + # Increments txnNumber. + - name: startTransaction + object: session0 + # These commits aren't sent to server, transaction is empty. + - name: commitTransaction + object: session0 + - name: commitTransaction + object: session0 + # Verify that previous, empty transaction incremented txnNumber. + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + # txnNumber 2 was skipped. + txnNumber: + $numberLong: "3" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: reset session state commit + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Running any operation after an ended transaction resets the session + # state to "no transaction". + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + # Calling commit again should error instead of re-running the commit. + - name: commitTransaction + object: session0 + result: + errorContains: no transaction started + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + lsid: session0 + txnNumber: + startTransaction: + autocommit: + command_name: insert + database_name: *database_name + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + + - description: reset session state abort + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + # Running any operation after an ended transaction resets the session + # state to "no transaction". + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + # Calling abort should error with "no transaction started" instead of + # "cannot call abortTransaction twice". + - name: abortTransaction + object: session0 + result: + errorContains: no transaction started + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + lsid: session0 + txnNumber: + startTransaction: + autocommit: + command_name: insert + database_name: *database_name + + outcome: + collection: + data: + - _id: 2 diff --git a/t/data/transactions/delete.json b/t/data/transactions/delete.json new file mode 100644 index 00000000..80ff1be9 --- /dev/null +++ b/t/data/transactions/delete.json @@ -0,0 +1,313 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + }, + { + "_id": 5 + } + ], + "tests": [ + { + "description": "delete", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "deletedCount": 1 + } + }, + { + "name": "deleteMany", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$lte": 3 + } + } + }, + "result": { + "deletedCount": 2 + } + }, + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + } + }, + "result": { + "deletedCount": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 1 + }, + "limit": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": { + "$lte": 3 + } + }, + "limit": 0 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 4 + }, + "limit": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 5 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for delete", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "deleteOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "deletedCount": 1 + } + }, + { + "name": "deleteMany", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$lte": 3 + } + } + }, + "result": { + "deletedCount": 2 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 1 + }, + "limit": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": { + "$lte": 3 + } + }, + "limit": 0 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/delete.yml b/t/data/transactions/delete.yml new file mode 100644 index 00000000..86b5a3a9 --- /dev/null +++ b/t/data/transactions/delete.yml @@ -0,0 +1,184 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + - _id: 5 + +tests: + - description: delete + + operations: + - name: startTransaction + object: session0 + - name: deleteOne + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + deletedCount: 1 + - name: deleteMany + object: collection + arguments: + session: session0 + filter: + _id: {$lte: 3} + result: + deletedCount: 2 + - name: deleteOne + object: collection + arguments: + session: session0 + filter: + _id: 4 + result: + deletedCount: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 1} + limit: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: {$lte: 3}} + limit: 0 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 4} + limit: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 5 + + - description: collection writeConcern ignored for delete + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: deleteOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: + _id: 1 + result: + deletedCount: 1 + - name: deleteMany + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: + _id: {$lte: 3} + result: + deletedCount: 2 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 1} + limit: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: {$lte: 3}} + limit: 0 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin diff --git a/t/data/transactions/error-labels.json b/t/data/transactions/error-labels.json new file mode 100644 index 00000000..edb65e74 --- /dev/null +++ b/t/data/transactions/error-labels.json @@ -0,0 +1,1539 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "DuplicateKey errors do not contain transient label", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "session": "session0", + "documents": [ + { + "_id": 1 + }, + { + "_id": 1 + } + ] + }, + "result": { + "errorCodeName": "DuplicateKey", + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + }, + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "NotMaster errors contain transient label", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 10107 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "WriteConflict errors contain transient label", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 112 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "NoSuchTransaction errors contain transient label", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 251 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "NoSuchTransaction errors on commit contain transient label", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 251 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "add transient label to connection errors", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 4 + }, + "data": { + "failCommands": [ + "insert", + "find", + "aggregate", + "distinct" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0" + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "session": "session0" + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "distinct", + "object": "collection", + "arguments": { + "fieldName": "_id", + "session": "session0" + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "aggregate", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "distinct": "test", + "key": "_id", + "lsid": "session0", + "readConcern": null, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "distinct", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "add unknown commit label to connection errors", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "add unknown commit label to retryable commit errors", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 11602 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "add unknown commit label to writeConcernError ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "add unknown commit label to writeConcernError WriteConcernFailed", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 64, + "errmsg": "multiple errors reported" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "add unknown commit label to writeConcernError WriteConcernFailed with wtimeout", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 64, + "codeName": "WriteConcernFailed", + "errmsg": "waiting for replication timed out", + "errInfo": { + "wtimeout": true + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "omit unknown commit label to writeConcernError UnsatisfiableWriteConcern", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 100, + "errmsg": "Not enough data-bearing nodes" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "omit unknown commit label to writeConcernError UnknownReplWriteConcern", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 79, + "errmsg": "No write concern mode named 'blah' found in replica set configuration" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/error-labels.yml b/t/data/transactions/error-labels.yml new file mode 100644 index 00000000..d40e93ca --- /dev/null +++ b/t/data/transactions/error-labels.yml @@ -0,0 +1,948 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: DuplicateKey errors do not contain transient label + + operations: + - name: startTransaction + object: session0 + - name: insertMany + object: collection + arguments: + session: session0 + documents: + - _id: 1 + - _id: 1 + result: + errorCodeName: DuplicateKey + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: NotMaster errors contain transient label + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 10107 # NotMaster + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + # Note, the server will return the errorLabel in this case. + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: WriteConflict errors contain transient label + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 112 # WriteConflict + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + # Note, the server will return the errorLabel in this case. + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: NoSuchTransaction errors contain transient label + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 251 # NoSuchTransaction + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + # Note, the server will return the errorLabel in this case. + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: NoSuchTransaction errors on commit contain transient label + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 251 # NoSuchTransaction + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + # Note, the server will return the errorLabel in this case. + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: add transient label to connection errors + + failPoint: + configureFailPoint: failCommand + mode: { times: 4 } + data: + failCommands: ["insert", "find", "aggregate", "distinct"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: &transient_label_only + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: find + object: collection + arguments: + session: session0 + result: *transient_label_only + - name: aggregate + object: collection + arguments: + pipeline: + - $project: + _id: 1 + session: session0 + result: *transient_label_only + - name: distinct + object: collection + arguments: + fieldName: _id + session: session0 + result: *transient_label_only + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + find: *collection_name + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: find + database_name: *database_name + - command_started_event: + command: + aggregate: *collection_name + pipeline: + - $project: + _id: 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: aggregate + database_name: *database_name + - command_started_event: + command: + distinct: *collection_name + key: _id + lsid: session0 + readConcern: + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: distinct + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: add unknown commit label to connection errors + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["commitTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: add unknown commit label to retryable commit errors + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["commitTransaction"] + errorCode: 11602 # InterruptedDueToReplStateChange + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: add unknown commit label to writeConcernError ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: add unknown commit label to writeConcernError WriteConcernFailed + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 64 # WriteConcernFailed without wtimeout + errmsg: multiple errors reported + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: add unknown commit label to writeConcernError WriteConcernFailed with wtimeout + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 64 + codeName: WriteConcernFailed + errmsg: waiting for replication timed out + errInfo: {wtimeout: True} + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: omit unknown commit label to writeConcernError UnsatisfiableWriteConcern + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 100 # UnsatisfiableWriteConcern/CannotSatisfyWriteConcern + errmsg: Not enough data-bearing nodes + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: omit unknown commit label to writeConcernError UnknownReplWriteConcern + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 79 # UnknownReplWriteConcern + errmsg: No write concern mode named 'blah' found in replica set configuration + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 diff --git a/t/data/transactions/errors.json b/t/data/transactions/errors.json new file mode 100644 index 00000000..159cc3ce --- /dev/null +++ b/t/data/transactions/errors.json @@ -0,0 +1,208 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "start insert start", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "startTransaction", + "object": "session0", + "result": { + "errorContains": "transaction already in progress" + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ] + }, + { + "description": "start twice", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0", + "result": { + "errorContains": "transaction already in progress" + } + } + ] + }, + { + "description": "commit and start twice", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0", + "result": { + "errorContains": "transaction already in progress" + } + } + ] + }, + { + "description": "write conflict commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "startTransaction", + "object": "session1" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session1", + "document": { + "_id": 1 + } + }, + "result": { + "errorCodeName": "WriteConflict", + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session1", + "result": { + "errorCodeName": "NoSuchTransaction", + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + } + ] + }, + { + "description": "write conflict abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "startTransaction", + "object": "session1" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session1", + "document": { + "_id": 1 + } + }, + "result": { + "errorCodeName": "WriteConflict", + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session1" + } + ] + } + ] +} diff --git a/t/data/transactions/errors.yml b/t/data/transactions/errors.yml new file mode 100644 index 00000000..57d0dc97 --- /dev/null +++ b/t/data/transactions/errors.yml @@ -0,0 +1,125 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] +tests: + - description: start insert start + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: startTransaction + object: session0 + result: + # Client-side error. + errorContains: transaction already in progress + # Just to clean up. + - name: commitTransaction + object: session0 + + - description: start twice + + operations: + - name: startTransaction + object: session0 + - name: startTransaction + object: session0 + result: + # Client-side error. + errorContains: transaction already in progress + + - description: commit and start twice + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + - name: startTransaction + object: session0 + result: + # Client-side error. + errorContains: transaction already in progress + + - description: write conflict commit + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: startTransaction + object: session1 + - name: insertOne + object: collection + arguments: + session: session1 + document: + _id: 1 + result: + errorCodeName: WriteConflict + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: commitTransaction + object: session0 + - name: commitTransaction + object: session1 + result: + errorCodeName: NoSuchTransaction + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + + - description: write conflict abort + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: startTransaction + object: session1 + - name: insertOne + object: collection + arguments: + session: session1 + document: + _id: 1 + result: + errorCodeName: WriteConflict + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: commitTransaction + object: session0 + # Driver ignores "NoSuchTransaction" error. + - name: abortTransaction + object: session1 diff --git a/t/data/transactions/findOneAndDelete.json b/t/data/transactions/findOneAndDelete.json new file mode 100644 index 00000000..f02facf2 --- /dev/null +++ b/t/data/transactions/findOneAndDelete.json @@ -0,0 +1,207 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "tests": [ + { + "description": "findOneAndDelete", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndDelete", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + } + }, + "result": { + "_id": 3 + } + }, + { + "name": "findOneAndDelete", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + } + }, + "result": null + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "remove": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 4 + }, + "remove": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for findOneAndDelete", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "findOneAndDelete", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + } + }, + "result": { + "_id": 3 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "remove": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/findOneAndDelete.yml b/t/data/transactions/findOneAndDelete.yml new file mode 100644 index 00000000..7ade9cbf --- /dev/null +++ b/t/data/transactions/findOneAndDelete.yml @@ -0,0 +1,126 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + +tests: + - description: findOneAndDelete + + operations: + - name: startTransaction + object: session0 + - name: findOneAndDelete + object: collection + arguments: + session: session0 + filter: {_id: 3} + result: {_id: 3} + - name: findOneAndDelete + object: collection + arguments: + session: session0 + filter: {_id: 4} + result: + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + remove: True + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 4} + remove: True + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1} + - {_id: 2} + + - description: collection writeConcern ignored for findOneAndDelete + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: findOneAndDelete + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {_id: 3} + result: {_id: 3} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + remove: True + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin diff --git a/t/data/transactions/findOneAndReplace.json b/t/data/transactions/findOneAndReplace.json new file mode 100644 index 00000000..9c77ebf4 --- /dev/null +++ b/t/data/transactions/findOneAndReplace.json @@ -0,0 +1,241 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "tests": [ + { + "description": "findOneAndReplace", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndReplace", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "replacement": { + "x": 1 + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3 + } + }, + { + "name": "findOneAndReplace", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + }, + "replacement": { + "x": 1 + }, + "upsert": true, + "returnDocument": "After" + }, + "result": { + "_id": 4, + "x": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "x": 1 + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 4 + }, + "update": { + "x": 1 + }, + "new": true, + "upsert": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3, + "x": 1 + }, + { + "_id": 4, + "x": 1 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for findOneAndReplace", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "findOneAndReplace", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "replacement": { + "x": 1 + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "x": 1 + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/findOneAndReplace.yml b/t/data/transactions/findOneAndReplace.yml new file mode 100644 index 00000000..58db2a75 --- /dev/null +++ b/t/data/transactions/findOneAndReplace.yml @@ -0,0 +1,140 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + +tests: + - description: findOneAndReplace + + operations: + - name: startTransaction + object: session0 + - name: findOneAndReplace + object: collection + arguments: + session: session0 + filter: {_id: 3} + replacement: {x: 1} + returnDocument: Before + result: {_id: 3} + - name: findOneAndReplace + object: collection + arguments: + session: session0 + filter: {_id: 4} + replacement: {x: 1} + upsert: true + returnDocument: After + result: {_id: 4, x: 1} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {x: 1} + new: false + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 4} + update: {x: 1} + new: true + upsert: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1} + - {_id: 2} + - {_id: 3, x: 1} + - {_id: 4, x: 1} + + - description: collection writeConcern ignored for findOneAndReplace + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: findOneAndReplace + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {_id: 3} + replacement: {x: 1} + returnDocument: Before + result: {_id: 3} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {x: 1} + new: false + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + diff --git a/t/data/transactions/findOneAndUpdate.json b/t/data/transactions/findOneAndUpdate.json new file mode 100644 index 00000000..c56e631e --- /dev/null +++ b/t/data/transactions/findOneAndUpdate.json @@ -0,0 +1,399 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "tests": [ + { + "description": "findOneAndUpdate", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3 + } + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "upsert": true, + "returnDocument": "After" + }, + "result": { + "_id": 4, + "x": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3, + "x": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3, + "x": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 4 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": true, + "upsert": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3, + "x": 2 + }, + { + "_id": 4, + "x": 1 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for findOneAndUpdate", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/findOneAndUpdate.yml b/t/data/transactions/findOneAndUpdate.yml new file mode 100644 index 00000000..2616bb16 --- /dev/null +++ b/t/data/transactions/findOneAndUpdate.yml @@ -0,0 +1,228 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + +tests: + - description: findOneAndUpdate + + operations: + - name: startTransaction + object: session0 + - name: findOneAndUpdate + object: collection + arguments: + session: session0 + filter: {_id: 3} + update: + $inc: {x: 1} + returnDocument: Before + result: {_id: 3} + - name: findOneAndUpdate + object: collection + arguments: + session: session0 + filter: {_id: 4} + update: + $inc: {x: 1} + upsert: true + returnDocument: After + result: {_id: 4, x: 1} + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + # Test a second time to ensure txnNumber is incremented. + - name: findOneAndUpdate + object: collection + arguments: + session: session0 + filter: {_id: 3} + update: + $inc: {x: 1} + returnDocument: Before + result: {_id: 3, x: 1} + - name: commitTransaction + object: session0 + # Test a third time to ensure abort works. + - name: startTransaction + object: session0 + - name: findOneAndUpdate + object: collection + arguments: + session: session0 + filter: {_id: 3} + update: + $inc: {x: 1} + returnDocument: Before + result: {_id: 3, x: 2} + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {$inc: {x: 1}} + new: false + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 4} + update: {$inc: {x: 1}} + new: true + upsert: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {$inc: {x: 1}} + new: false + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + afterClusterTime: 42 + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {$inc: {x: 1}} + new: false + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: true + autocommit: false + readConcern: + afterClusterTime: 42 + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1} + - {_id: 2} + - {_id: 3, x: 2} + - {_id: 4, x: 1} + + - description: collection writeConcern ignored for findOneAndUpdate + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: findOneAndUpdate + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {_id: 3} + update: + $inc: {x: 1} + returnDocument: Before + result: {_id: 3} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: + $inc: {x: 1} + new: false + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + diff --git a/t/data/transactions/insert.json b/t/data/transactions/insert.json new file mode 100644 index 00000000..4dc9b85f --- /dev/null +++ b/t/data/transactions/insert.json @@ -0,0 +1,441 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "insert", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 2, + "1": 3 + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "result": { + "insertedId": 4 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 5 + } + }, + "result": { + "insertedId": 5 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "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": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 4 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 5 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + }, + { + "_id": 5 + } + ] + } + } + }, + { + "description": "collection writeConcern without transaction", + "operations": [ + { + "name": "insertOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": null, + "startTransaction": null, + "autocommit": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for insert", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "insertMany", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "documents": [ + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 2, + "1": 3 + } + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "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": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/insert.yml b/t/data/transactions/insert.yml new file mode 100644 index 00000000..55f0a18a --- /dev/null +++ b/t/data/transactions/insert.yml @@ -0,0 +1,264 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: insert + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: insertMany + object: collection + arguments: + documents: + - _id: 2 + - _id: 3 + session: session0 + result: + insertedIds: {0: 2, 1: 3} + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 4 + result: + insertedId: 4 + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 5 + result: + insertedId: 5 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + 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: 2 + - _id: 3 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 4 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 5 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + - _id: 5 + + # This test proves that the driver parses the collectionOptions writeConcern. + - description: collection writeConcern without transaction + operations: + - name: insertOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + startTransaction: + autocommit: + writeConcern: + w: majority + command_name: insert + database_name: *database_name + + outcome: + collection: + data: + - _id: 1 + + - description: collection writeConcern ignored for insert + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: insertMany + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + documents: + - _id: 2 + - _id: 3 + session: session0 + result: + insertedIds: {0: 2, 1: 3} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + 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: 2 + - _id: 3 + ordered: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + - _id: 3 diff --git a/t/data/transactions/isolation.json b/t/data/transactions/isolation.json new file mode 100644 index 00000000..f5b5ea0c --- /dev/null +++ b/t/data/transactions/isolation.json @@ -0,0 +1,211 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "one transaction", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "two transactions", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session1" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "commitTransaction", + "object": "session1" + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/isolation.yml b/t/data/transactions/isolation.yml new file mode 100644 index 00000000..79e031f2 --- /dev/null +++ b/t/data/transactions/isolation.yml @@ -0,0 +1,125 @@ +# Test snapshot isolation. +# This test doesn't check contents of command-started events. +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: one transaction + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + - {_id: 1} + - name: find + object: collection + arguments: + session: session1 + filter: + _id: 1 + result: [] + - name: find + object: collection + arguments: + filter: + _id: 1 + result: [] + - name: commitTransaction + object: session0 + - name: find + object: collection + arguments: + session: session1 + filter: + _id: 1 + result: + - {_id: 1} + - name: find + object: collection + arguments: + filter: + _id: 1 + result: + - {_id: 1} + + outcome: + collection: + data: + - _id: 1 + + - description: two transactions + + operations: + - name: startTransaction + object: session0 + - name: startTransaction + object: session1 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + - {_id: 1} + - name: find + object: collection + arguments: + session: session1 + filter: + _id: 1 + result: [] + - name: find + object: collection + arguments: + filter: + _id: 1 + result: [] + - name: commitTransaction + object: session0 + # Snapshot isolation in session1, not read-committed. + - name: find + object: collection + arguments: + session: session1 + filter: + _id: 1 + result: [] + - name: find + object: collection + arguments: + filter: + _id: 1 + result: + - {_id: 1} + - name: commitTransaction + object: session1 + + outcome: + collection: + data: + - {_id: 1} diff --git a/t/data/transactions/read-pref.json b/t/data/transactions/read-pref.json new file mode 100644 index 00000000..03a7eca1 --- /dev/null +++ b/t/data/transactions/read-pref.json @@ -0,0 +1,706 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "default readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": [ + { + "count": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "primary readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Primary" + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": [ + { + "count": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "secondary readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Secondary" + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "primaryPreferred readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "PrimaryPreferred" + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "nearest readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Nearest" + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "secondary write only", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Secondary" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/read-pref.yml b/t/data/transactions/read-pref.yml new file mode 100644 index 00000000..de1b41f6 --- /dev/null +++ b/t/data/transactions/read-pref.yml @@ -0,0 +1,340 @@ +# This test doesn't check contents of command-started events. +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: default readPreference + + operations: + - name: startTransaction + object: session0 + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + # The driver overrides the collection's read pref with the + # transaction's so count runs with Primary and succeeds. + readPreference: + mode: Secondary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + - count: 1 + - name: find + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + session: session0 + batchSize: 3 + result: *insertedDocs + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: *insertedDocs + - name: commitTransaction + object: session0 + + outcome: + collection: + data: *insertedDocs + + - description: primary readPreference + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Primary + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + - count: 1 + - name: find + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + session: session0 + batchSize: 3 + result: *insertedDocs + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: *insertedDocs + - name: commitTransaction + object: session0 + + outcome: + collection: + data: *insertedDocs + + - description: secondary readPreference + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Secondary + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + errorContains: read preference in a transaction must be primary + - name: find + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + batchSize: 3 + result: + errorContains: read preference in a transaction must be primary + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: + errorContains: read preference in a transaction must be primary + - name: abortTransaction + object: session0 + + outcome: + collection: + data: [] + + - description: primaryPreferred readPreference + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: PrimaryPreferred + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + errorContains: read preference in a transaction must be primary + - name: find + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + batchSize: 3 + result: + errorContains: read preference in a transaction must be primary + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: + errorContains: read preference in a transaction must be primary + - name: abortTransaction + object: session0 + + outcome: + collection: + data: [] + + - description: nearest readPreference + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Nearest + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + errorContains: read preference in a transaction must be primary + - name: find + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + batchSize: 3 + result: + errorContains: read preference in a transaction must be primary + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: + errorContains: read preference in a transaction must be primary + - name: abortTransaction + object: session0 + + outcome: + collection: + data: [] + + - description: secondary write only + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Secondary + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + outcome: + collection: + data: + - _id: 1 diff --git a/t/data/transactions/reads.json b/t/data/transactions/reads.json new file mode 100644 index 00000000..0b7cf836 --- /dev/null +++ b/t/data/transactions/reads.json @@ -0,0 +1,615 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "tests": [ + { + "description": "collection readConcern without transaction", + "operations": [ + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readConcern": { + "level": "majority" + } + }, + "arguments": { + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "find": "test", + "readConcern": { + "level": "majority" + }, + "lsid": "session0", + "txnNumber": null, + "startTransaction": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "count", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "count", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "errorContains": "Cannot run 'count' in a multi-document transaction", + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "count": "test", + "query": { + "_id": 1 + }, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "find", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "find": "test", + "batchSize": 3, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "collection": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "collection": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "aggregate", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "cursor": { + "batchSize": 3 + }, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false + }, + "command_name": "aggregate", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "collection": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "cursor": { + "batchSize": 3 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "aggregate", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "collection": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "distinct", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "distinct", + "object": "collection", + "arguments": { + "session": "session0", + "fieldName": "_id" + }, + "result": [ + 1, + 2, + 3, + 4 + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "distinct": "test", + "key": "_id", + "lsid": "session0", + "readConcern": null, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "distinct", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "readConcern": null, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/reads.yml b/t/data/transactions/reads.yml new file mode 100644 index 00000000..4ebc4dfa --- /dev/null +++ b/t/data/transactions/reads.yml @@ -0,0 +1,298 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: &data + - {_id: 1} + - {_id: 2} + - {_id: 3} + - {_id: 4} + +tests: + - description: collection readConcern without transaction + + operations: + - name: find + object: collection + collectionOptions: + readConcern: + level: majority + arguments: + session: session0 + result: *data + + expectations: + - command_started_event: + command: + find: *collection_name + readConcern: + level: majority + lsid: session0 + txnNumber: + startTransaction: + autocommit: + command_name: find + database_name: *database_name + + outcome: &outcome + collection: + data: + *data + + - description: count + + operations: + - &startTransaction + name: startTransaction + object: session0 + - name: count + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + errorContains: "Cannot run 'count' in a multi-document transaction" + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + count: *collection_name + query: + _id: 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: count + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: find + + operations: + - *startTransaction + - &find + name: find + object: collection + arguments: + session: session0 + batchSize: 3 + result: *data + - *find + - &commitTransaction + name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + find: *collection_name + batchSize: 3 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + command_name: find + database_name: *database_name + - command_started_event: + command: + getMore: + # 42 is a fake placeholder value for the cursorId. + $numberLong: '42' + collection: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: getMore + database_name: *database_name + - command_started_event: + command: + find: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: find + database_name: *database_name + - command_started_event: + command: + getMore: + $numberLong: '42' + collection: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: getMore + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: *outcome + + - description: aggregate + + operations: + - *startTransaction + - &aggregate + name: aggregate + object: collection + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: *data + - *aggregate + - *commitTransaction + + expectations: + - command_started_event: + command: + aggregate: *collection_name + pipeline: + - $project: + _id: 1 + cursor: + batchSize: 3 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + command_name: aggregate + database_name: *database_name + - command_started_event: + command: + getMore: + # 42 is a fake placeholder value for the cursorId. + $numberLong: '42' + collection: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: getMore + database_name: *database_name + - command_started_event: + command: + aggregate: *collection_name + pipeline: + - $project: + _id: 1 + cursor: + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: aggregate + database_name: *database_name + - command_started_event: + command: + getMore: + $numberLong: '42' + collection: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: getMore + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: *outcome + + - description: distinct + + operations: + - *startTransaction + - name: distinct + object: collection + arguments: + session: session0 + fieldName: _id + result: [1, 2, 3, 4] + - *commitTransaction + + expectations: + - command_started_event: + command: + distinct: *collection_name + key: _id + lsid: session0 + readConcern: + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: distinct + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + readConcern: + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: *outcome diff --git a/t/data/transactions/retryable-abort.json b/t/data/transactions/retryable-abort.json new file mode 100644 index 00000000..d2ab05dc --- /dev/null +++ b/t/data/transactions/retryable-abort.json @@ -0,0 +1,1958 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "abortTransaction only performs a single retry", + "clientOptions": { + "retryWrites": false + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction does not retry after Interrupted", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 11601, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction does not retry after WriteConcernError Interrupted", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 11601, + "errmsg": "operation was interrupted" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after connection error", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after NotMaster", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 10107, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after NotMasterOrSecondary", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 13436, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after NotMasterNoSlaveOk", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 13435, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after InterruptedDueToReplStateChange", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 11602, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after InterruptedAtShutdown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 11600, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after PrimarySteppedDown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 189, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 91, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after HostNotFound", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 7, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after HostUnreachable", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 6, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after SocketException", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 9001, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after NetworkTimeout", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 89, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after WriteConcernError InterruptedAtShutdown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 11600, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 11602, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after WriteConcernError PrimarySteppedDown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 189, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after WriteConcernError ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + } + ] +} diff --git a/t/data/transactions/retryable-abort.yml b/t/data/transactions/retryable-abort.yml new file mode 100644 index 00000000..3dd53209 --- /dev/null +++ b/t/data/transactions/retryable-abort.yml @@ -0,0 +1,1292 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: abortTransaction only performs a single retry + + clientOptions: + retryWrites: false + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["abortTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + # Call to abort returns no error even when the retry attempt fails. + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction does not retry after Interrupted + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 11601 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction does not retry after WriteConcernError Interrupted + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 11601 + errmsg: operation was interrupted + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after connection error + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after NotMaster + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 10107 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after NotMasterOrSecondary + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 13436 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after NotMasterNoSlaveOk + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 13435 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after InterruptedDueToReplStateChange + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 11602 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after InterruptedAtShutdown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 11600 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after PrimarySteppedDown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 189 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 91 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after HostNotFound + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 7 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after HostUnreachable + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 6 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after SocketException + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 9001 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after NetworkTimeout + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 89 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after WriteConcernError InterruptedAtShutdown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 11600 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 11602 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after WriteConcernError PrimarySteppedDown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 189 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after WriteConcernError ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] diff --git a/t/data/transactions/retryable-commit.json b/t/data/transactions/retryable-commit.json new file mode 100644 index 00000000..a4a6850c --- /dev/null +++ b/t/data/transactions/retryable-commit.json @@ -0,0 +1,2069 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "commitTransaction fails after two errors", + "clientOptions": { + "retryWrites": false + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction fails after Interrupted", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 11601, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorCodeName": "Interrupted", + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "commitTransaction fails after WriteConcernError Interrupted", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 11601, + "errmsg": "operation was interrupted" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after connection error", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after NotMaster", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 10107, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after NotMasterOrSecondary", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 13436, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after NotMasterNoSlaveOk", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 13435, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after InterruptedDueToReplStateChange", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 11602, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after InterruptedAtShutdown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 11600, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after PrimarySteppedDown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 189, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 91, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after HostNotFound", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 7, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after HostUnreachable", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 6, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after SocketException", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 9001, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after NetworkTimeout", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 89, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after WriteConcernError InterruptedAtShutdown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 11600, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 11602, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after WriteConcernError PrimarySteppedDown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 189, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after WriteConcernError ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/retryable-commit.yml b/t/data/transactions/retryable-commit.yml new file mode 100644 index 00000000..d0354ddb --- /dev/null +++ b/t/data/transactions/retryable-commit.yml @@ -0,0 +1,1332 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: commitTransaction fails after two errors + + clientOptions: + retryWrites: false + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["commitTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + # First call to commit fails after a single retry attempt. + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + # Second call to commit succeeds because the failpoint was disabled. + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction fails after Interrupted + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 11601 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorCodeName: Interrupted + errorLabelsOmit: ["TransientTransactionError"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: commitTransaction fails after WriteConcernError Interrupted + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 11601 + errmsg: operation was interrupted + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsOmit: ["TransientTransactionError"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after connection error + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after NotMaster + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 10107 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after NotMasterOrSecondary + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 13436 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after NotMasterNoSlaveOk + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 13435 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after InterruptedDueToReplStateChange + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 11602 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after InterruptedAtShutdown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 11600 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after PrimarySteppedDown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 189 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 91 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after HostNotFound + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 7 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after HostUnreachable + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 6 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after SocketException + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 9001 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after NetworkTimeout + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 89 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after WriteConcernError InterruptedAtShutdown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 11600 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 11602 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after WriteConcernError PrimarySteppedDown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 189 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after WriteConcernError ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 diff --git a/t/data/transactions/retryable-writes.json b/t/data/transactions/retryable-writes.json new file mode 100644 index 00000000..e568de5c --- /dev/null +++ b/t/data/transactions/retryable-writes.json @@ -0,0 +1,329 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "increment txnNumber", + "clientOptions": { + "retryWrites": true + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 3 + } + }, + "result": { + "insertedId": 3 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 4 + }, + { + "_id": 5 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 4, + "1": 5 + } + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 3 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 4 + }, + { + "_id": 5 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "4" + }, + "startTransaction": null, + "autocommit": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 4 + }, + { + "_id": 5 + } + ] + } + } + }, + { + "description": "writes are not retried", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + } + ] +} diff --git a/t/data/transactions/retryable-writes.yml b/t/data/transactions/retryable-writes.yml new file mode 100644 index 00000000..e60d04c3 --- /dev/null +++ b/t/data/transactions/retryable-writes.yml @@ -0,0 +1,208 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: increment txnNumber + + clientOptions: + retryWrites: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Retryable write should include the next txnNumber + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + # Next transaction should include the next txnNumber + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 3 + result: + insertedId: 3 + - name: abortTransaction + object: session0 + # Retryable write should include the next txnNumber + - name: insertMany + object: collection + arguments: + documents: + - _id: 4 + - _id: 5 + session: session0 + result: + insertedIds: {0: 4, 1: 5} + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 3 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 4 + - _id: 5 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "4" + startTransaction: + autocommit: + writeConcern: + command_name: insert + database_name: *database_name + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + - _id: 4 + - _id: 5 + + - description: writes are not retried + + clientOptions: + retryWrites: true + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + errorLabelsContain: ["TransientTransactionError"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] diff --git a/t/data/transactions/run-command.json b/t/data/transactions/run-command.json new file mode 100644 index 00000000..a69a5445 --- /dev/null +++ b/t/data/transactions/run-command.json @@ -0,0 +1,292 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "run command with default read preference", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "runCommand", + "object": "database", + "command_name": "insert", + "arguments": { + "session": "session0", + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ] + } + }, + "result": { + "n": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + }, + { + "description": "run command with secondary read preference in client option and primary read preference in transaction options", + "clientOptions": { + "readPreference": "secondary" + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Primary" + } + } + } + }, + { + "name": "runCommand", + "object": "database", + "command_name": "insert", + "arguments": { + "session": "session0", + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ] + } + }, + "result": { + "n": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + }, + { + "description": "run command with explicit primary read preference", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "runCommand", + "object": "database", + "command_name": "insert", + "arguments": { + "session": "session0", + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ] + }, + "readPreference": { + "mode": "Primary" + } + }, + "result": { + "n": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + }, + { + "description": "run command fails with explicit secondary read preference", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "runCommand", + "object": "database", + "command_name": "find", + "arguments": { + "session": "session0", + "command": { + "find": "test" + }, + "readPreference": { + "mode": "Secondary" + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + } + ] + }, + { + "description": "run command fails with secondary read preference from transaction options", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "secondary" + } + } + } + }, + { + "name": "runCommand", + "object": "database", + "command_name": "find", + "arguments": { + "session": "session0", + "command": { + "find": "test" + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + } + ] + } + ] +} diff --git a/t/data/transactions/run-command.yml b/t/data/transactions/run-command.yml new file mode 100644 index 00000000..16a633a7 --- /dev/null +++ b/t/data/transactions/run-command.yml @@ -0,0 +1,189 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: run command with default read preference + + operations: + - name: startTransaction + object: session0 + - name: runCommand + object: database + command_name: insert + arguments: + session: session0 + command: + insert: *collection_name + documents: + - _id : 1 + result: + n: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id : 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + + - description: run command with secondary read preference in client option and primary read preference in transaction options + + clientOptions: + readPreference: secondary + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Primary + - name: runCommand + object: database + command_name: insert + arguments: + session: session0 + command: + insert: *collection_name + documents: + - _id : 1 + result: + n: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id : 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + + - description: run command with explicit primary read preference + + operations: + - name: startTransaction + object: session0 + - name: runCommand + object: database + command_name: insert + arguments: + session: session0 + command: + insert: *collection_name + documents: + - _id : 1 + readPreference: + mode: Primary + result: + n: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id : 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + + - description: run command fails with explicit secondary read preference + + operations: + - name: startTransaction + object: session0 + - name: runCommand + object: database + command_name: find + arguments: + session: session0 + command: + find: *collection_name + readPreference: + mode: Secondary + result: + errorContains: read preference in a transaction must be primary + + - description: run command fails with secondary read preference from transaction options + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Secondary + - name: runCommand + object: database + command_name: find + arguments: + session: session0 + command: + find: *collection_name + result: + errorContains: read preference in a transaction must be primary + diff --git a/t/data/transactions/transaction-options.json b/t/data/transactions/transaction-options.json new file mode 100644 index 00000000..c962f9f2 --- /dev/null +++ b/t/data/transactions/transaction-options.json @@ -0,0 +1,1534 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "no transaction options set", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "transaction options inherited from client", + "clientOptions": { + "w": 1, + "readConcernLevel": "local" + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "local" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "local", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "transaction options inherited from defaultTransactionOptions", + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "snapshot" + }, + "writeConcern": { + "w": 1 + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "startTransaction options override defaults", + "clientOptions": { + "readConcernLevel": "local", + "w": 1 + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "majority" + }, + "writeConcern": { + "w": 1 + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readConcern": { + "level": "snapshot" + }, + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readConcern": { + "level": "snapshot" + }, + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "defaultTransactionOptions override client options", + "clientOptions": { + "readConcernLevel": "local", + "w": 1 + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "snapshot" + }, + "writeConcern": { + "w": "majority" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "readConcern local in defaultTransactionOptions", + "clientOptions": { + "w": 1 + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "local" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "local" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "local", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "readConcern local in startTransaction options", + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "majority" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readConcern": { + "level": "snapshot" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readConcern": { + "level": "snapshot" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "client writeConcern ignored for bulk", + "clientOptions": { + "w": "majority" + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": 1 + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection", + "arguments": { + "requests": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + } + } + } + ], + "session": "session0" + }, + "result": { + "deletedCount": 0, + "insertedIds": { + "0": 1 + }, + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 0, + "upsertedIds": {} + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": 1 + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "readPreference inherited from client", + "clientOptions": { + "readPreference": "secondary" + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "readPreference inherited from defaultTransactionOptions", + "clientOptions": { + "readPreference": "primary" + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readPreference": { + "mode": "Secondary" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "startTransaction overrides readPreference", + "clientOptions": { + "readPreference": "primary" + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readPreference": { + "mode": "Primary" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Secondary" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/transaction-options.yml b/t/data/transactions/transaction-options.yml new file mode 100644 index 00000000..5e183a35 --- /dev/null +++ b/t/data/transactions/transaction-options.yml @@ -0,0 +1,877 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: no transaction options set + + operations: &commitAbortOperations + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Now test abort. + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: &outcome + collection: + data: + - _id: 1 + + - description: transaction options inherited from client + + clientOptions: + w: 1 + readConcernLevel: local + + operations: *commitAbortOperations + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: local + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: local + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: transaction options inherited from defaultTransactionOptions + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: snapshot + writeConcern: + w: 1 + + operations: *commitAbortOperations + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: startTransaction options override defaults + + clientOptions: + readConcernLevel: local + w: 1 + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: majority + writeConcern: + w: 1 + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readConcern: + level: snapshot + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + arguments: + options: + readConcern: + level: snapshot + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: defaultTransactionOptions override client options + + clientOptions: + readConcernLevel: local + w: 1 + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: snapshot + writeConcern: + w: majority + + operations: *commitAbortOperations + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: readConcern local in defaultTransactionOptions + + clientOptions: + w: 1 + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: local + + operations: *commitAbortOperations + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: local + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: local + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: readConcern local in startTransaction options + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: majority # Overridden. + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readConcern: + level: snapshot + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Now test abort. + - name: startTransaction + object: session0 + arguments: + options: + readConcern: + level: snapshot + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: client writeConcern ignored for bulk + + clientOptions: + w: majority + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: 1 + - name: bulkWrite + object: collection + arguments: + requests: + - name: insertOne + arguments: + document: {_id: 1} + session: session0 + result: + deletedCount: 0 + insertedIds: {0: 1} + matchedCount: 0 + modifiedCount: 0 + upsertedCount: 0 + upsertedIds: {} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + # No writeConcern. + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: 1 + command_name: commitTransaction + database_name: admin + + outcome: *outcome + + - description: readPreference inherited from client + + clientOptions: + readPreference: secondary + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + errorContains: read preference in a transaction must be primary + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: readPreference inherited from defaultTransactionOptions + + clientOptions: + readPreference: primary + + sessionOptions: + session0: + defaultTransactionOptions: + readPreference: + mode: Secondary + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + errorContains: read preference in a transaction must be primary + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: startTransaction overrides readPreference + + clientOptions: + readPreference: primary + + sessionOptions: + session0: + defaultTransactionOptions: + readPreference: + mode: Primary + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Secondary + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + errorContains: read preference in a transaction must be primary + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 diff --git a/t/data/transactions/update.json b/t/data/transactions/update.json new file mode 100644 index 00000000..26590c56 --- /dev/null +++ b/t/data/transactions/update.json @@ -0,0 +1,436 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "tests": [ + { + "description": "update", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "updateOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "upsert": true + }, + "result": { + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 1, + "upsertedId": 4 + } + }, + { + "name": "replaceOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "x": 1 + }, + "replacement": { + "y": 1 + } + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "updateMany", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 3 + } + }, + "update": { + "$set": { + "z": 1 + } + } + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": false, + "upsert": true + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "x": 1 + }, + "u": { + "y": 1 + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gte": 3 + } + }, + "u": { + "$set": { + "z": 1 + } + }, + "multi": true, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3, + "z": 1 + }, + { + "_id": 4, + "y": 1, + "z": 1 + } + ] + } + } + }, + { + "description": "collections writeConcern ignored for update", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "updateOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "upsert": true + }, + "result": { + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 1, + "upsertedId": 4 + } + }, + { + "name": "replaceOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "x": 1 + }, + "replacement": { + "y": 1 + } + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "updateMany", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 3 + } + }, + "update": { + "$set": { + "z": 1 + } + } + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": false, + "upsert": true + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "x": 1 + }, + "u": { + "y": 1 + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gte": 3 + } + }, + "u": { + "$set": { + "z": 1 + } + }, + "multi": true, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/update.yml b/t/data/transactions/update.yml new file mode 100644 index 00000000..b325b440 --- /dev/null +++ b/t/data/transactions/update.yml @@ -0,0 +1,246 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + +tests: + - description: update + + operations: + - name: startTransaction + object: session0 + - name: updateOne + object: collection + arguments: + session: session0 + filter: {_id: 4} + update: + $inc: {x: 1} + upsert: true + result: + matchedCount: 0 + modifiedCount: 0 + upsertedCount: 1 + upsertedId: 4 + - name: replaceOne + object: collection + arguments: + session: session0 + filter: {x: 1} + replacement: {y: 1} + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + - name: updateMany + object: collection + arguments: + session: session0 + filter: + _id: {$gte: 3} + update: + $set: {z: 1} + result: + matchedCount: 2 + modifiedCount: 2 + upsertedCount: 0 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 4} + u: {$inc: {x: 1}} + multi: false + upsert: true + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {x: 1} + u: {y: 1} + multi: false + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: {$gte: 3}} + u: {$set: {z: 1}} + multi: true + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1} + - {_id: 2} + - {_id: 3, z: 1} + - {_id: 4, y: 1, z: 1} + + - description: collections writeConcern ignored for update + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: updateOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {_id: 4} + update: + $inc: {x: 1} + upsert: true + result: + matchedCount: 0 + modifiedCount: 0 + upsertedCount: 1 + upsertedId: 4 + - name: replaceOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {x: 1} + replacement: {y: 1} + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + - name: updateMany + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: + _id: {$gte: 3} + update: + $set: {z: 1} + result: + matchedCount: 2 + modifiedCount: 2 + upsertedCount: 0 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 4} + u: {$inc: {x: 1}} + multi: false + upsert: true + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {x: 1} + u: {y: 1} + multi: false + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: {$gte: 3}} + u: {$set: {z: 1}} + multi: true + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin diff --git a/t/data/transactions/write-concern.json b/t/data/transactions/write-concern.json new file mode 100644 index 00000000..dfc75f76 --- /dev/null +++ b/t/data/transactions/write-concern.json @@ -0,0 +1,355 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "commit with majority", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commit with default", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "abort with majority", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort with default", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "start with unacknowledged write concern", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": 0 + } + } + }, + "result": { + "errorContains": "transactions do not support unacknowledged write concern" + } + } + ] + }, + { + "description": "start with implicit unacknowledged write concern", + "clientOptions": { + "w": 0 + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "result": { + "errorContains": "transactions do not support unacknowledged write concern" + } + } + ] + } + ] +} diff --git a/t/data/transactions/write-concern.yml b/t/data/transactions/write-concern.yml new file mode 100644 index 00000000..adadbd8e --- /dev/null +++ b/t/data/transactions/write-concern.yml @@ -0,0 +1,236 @@ +# Assumes the default for transactions is the same as for all ops, tests +# setting the writeConcern to "majority". +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: commit with majority + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commit with default + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + + - _id: 1 + + - description: abort with majority + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abort with default + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + 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: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: start with unacknowledged write concern + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: 0 + result: + # Client-side error. + errorContains: transactions do not support unacknowledged write concern + + - description: start with implicit unacknowledged write concern + + clientOptions: + w: 0 + + operations: + - name: startTransaction + object: session0 + result: + # Client-side error. + errorContains: transactions do not support unacknowledged write concern diff --git a/t/lib/MongoDBTest.pm b/t/lib/MongoDBTest.pm index f9cd0871..7e9d03c2 100644 --- a/t/lib/MongoDBTest.pm +++ b/t/lib/MongoDBTest.pm @@ -36,6 +36,9 @@ our @EXPORT_OK = qw( skip_unless_mongod skip_unless_failpoints_available skip_unless_sessions + skip_unless_transactions + to_snake_case + remap_hashref_to_snake_case uri_escape get_unique_collection get_features @@ -209,6 +212,13 @@ sub skip_unless_sessions { unless $conn->_topology->_supports_sessions; } +sub skip_unless_transactions { + my $conn = build_client; + + plan skip_all => "Transaction support not available" + unless $conn->_topology->_supports_transactions; +} + sub server_version { my $conn = shift; @@ -271,6 +281,22 @@ sub uri_escape { return $str; } +sub to_snake_case { + my $t = shift; + $t =~ s{([A-Z])}{_\L$1}g; + return $t; +} + +sub remap_hashref_to_snake_case { + my $hash = shift; + return { + map { + my $k = to_snake_case( $_ ); + $k => $hash->{ $_ } + } keys %$hash + } +} + sub uuid_to_string { my $uuid = shift; return join "-", unpack( "H8H4H4H4H12", $uuid ); diff --git a/t/lib/MongoDBTest/Callback.pm b/t/lib/MongoDBTest/Callback.pm new file mode 100644 index 00000000..e87980d0 --- /dev/null +++ b/t/lib/MongoDBTest/Callback.pm @@ -0,0 +1,22 @@ +package MongoDBTest::Callback; + +use Moo; +use Storable qw/ dclone /; + +has events => ( + is => 'lazy', + default => sub { [] }, + clearer => 1, +); + +sub callback { + my $self = shift; + return sub { push @{ $self->events }, dclone $_[0] }; +} + +sub count { + my $self = shift; + return scalar( @{ $self->events } ); +} + +1; diff --git a/t/retryable-writes-spec.t b/t/retryable-writes-spec.t index 44d87c62..43235de4 100644 --- a/t/retryable-writes-spec.t +++ b/t/retryable-writes-spec.t @@ -17,7 +17,8 @@ use warnings; use JSON::MaybeXS; use Path::Tiny 0.054; # basename with suffix use Test::More 0.88; -use Test::Fatal; +use Test::Deep ':v1'; +use Safe::Isa; use lib "t/lib"; @@ -32,6 +33,8 @@ use MongoDBTest qw/ skip_unless_mongod skip_unless_sessions skip_unless_failpoints_available + to_snake_case + remap_hashref_to_snake_case get_features /; @@ -46,7 +49,7 @@ my $server_type = server_type($conn); sub run_test { my ( $coll, $test ) = @_; - enable_failpoint( $test->{failPoint} ) if exists $test->{failPoint}; + enable_failpoint( $test->{failPoint} ); my $op = $test->{operation}; my $method = $op->{name}; @@ -63,33 +66,13 @@ sub run_test { if ( !exists $test->{outcome}{error} && exists $test->{outcome}->{result} ) { - #Dwarn $ret; - #Dwarn $test->{outcome}; - for my $res_key ( keys %{ $test->{outcome}->{result} } ) { - next if $res_key eq 'upsertedCount' && ! $ret->can('upserted_count'); # Driver does not parse this value on all things? - # next if $res_key eq 'upsertedId' && ! defined $ret->upserted_id; # upserted id is always present - my $res = $test->{outcome}->{result}->{$res_key}; - - if ( $res_key eq 'insertedIds' ) { - my $ret_parsed = {}; - for my $item ( @{ $ret->inserted } ) { - $ret_parsed->{$item->{index}} = $item->{_id}; - } - is_deeply $ret_parsed, $test->{outcome}->{result}->{insertedIds}, 'insertedIds correct in result'; - next; - } - if ( $res_key eq 'upsertedIds' ) { - my $ret_parsed = {}; - for my $item ( @{ $ret->upserted } ) { - $ret_parsed->{$item->{index}} = $item->{_id}; - } - is_deeply $ret_parsed, $test->{outcome}->{result}->{upsertedIds}, 'upsertedIds correct in result'; - next; - } - my $ret_key = $res_key; - $ret_key =~ s{([A-Z])}{_\L$1}g; + my $expected = remap_hashref_to_snake_case( $test->{outcome}->{result} ); + # not all commands return an upserted count + delete $expected->{upserted_count} unless $ret->$_can('upserted_count'); - is $ret->{$ret_key}, $res, "$res_key correct in result"; + for my $key ( keys %$expected ) { + my $got = ref $ret eq 'HASH' ? $ret->{$key} : $ret->$key; + cmp_deeply $got, $expected->{$key}, "$key result as expected"; } } @@ -97,7 +80,7 @@ sub run_test { my $coll_expected = $test->{outcome}->{collection}->{data}; is_deeply \@coll_outcome, $coll_expected, 'Collection has correct outcome'; - disable_failpoint() if exists $test->{failPoint}; + disable_failpoint( $test->{failPoint} ); } sub do_delete_one { @@ -152,6 +135,15 @@ sub do_find_one_and_delete { return $coll->find_one_and_delete( $filter, $options ); } +my %bulk_remap = ( + insert_one => [qw( document )], + update_one => [qw( filter update )], + update_many => [qw( filter update )], + replace_one => [qw( filter replacement )], + delete_one => [qw( filter )], + delete_many => [qw( filter )], +); + sub do_bulk_write { my ( $self, $coll, $args ) = @_; my $options = { @@ -164,24 +156,13 @@ sub do_bulk_write { my @arguments; for my $request ( @{ $args->{requests} } ) { - if ( $request->{name} eq 'insertOne' ) { - push @arguments, { insert_one => [ $request->{arguments}->{document} ] }; - } elsif ( $request->{name} eq 'updateOne' ) { - push @arguments, { update_one => [ - $request->{arguments}->{filter}, - $request->{arguments}->{update}, - ( defined $request->{arguments}->{upsert} - ? ( { upsert => $request->{arguments}->{upsert} ? 1 : 0 } ) - : () ) - ] }; - } elsif ( $request->{name} eq 'deleteOne' ) { - push @arguments, { delete_one => [ $request->{arguments}->{filter} ] }; - } elsif ( $request->{name} eq 'replaceOne' ) { - push @arguments, { replace_one => [ - $request->{arguments}->{filter}, - $request->{arguments}->{replacement} - ] }; - } + my $req_name = to_snake_case( $request->{name} ); + my @req_fields = @{ $bulk_remap{ $req_name } }; + my @arg = map { + delete $request->{arguments}->{ $_ } + } @req_fields; + push @arg, $request->{arguments} if keys %{ $request->{arguments} }; + push @arguments, { $req_name => \@arg }; } return $coll->bulk_write( \@arguments, $options ); } @@ -227,9 +208,14 @@ while ( my $path = $iterator->() ) { } for my $test ( @{ $plan->{tests} } ) { - my $coll = get_unique_collection( $testdb, 'retry_write' ); + my $client_options = $test->{clientOptions}; + $client_options = remap_hashref_to_snake_case( $client_options ); + my $test_conn = build_client( %$client_options ); + my $test_db = get_test_db( $test_conn ); + my $coll = get_unique_collection( $test_db, 'retry_write' ); my $ret = $coll->insert_many( $plan->{data} ); my $description = $test->{description}; + subtest $description => sub { run_test( $coll, $test ); } @@ -238,17 +224,22 @@ while ( my $path = $iterator->() ) { } sub enable_failpoint { - my $doc = shift; + my $failpoint = shift; + return unless defined $failpoint; $conn->send_admin_command([ - configureFailPoint => 'onPrimaryTransactionalWrite', - %$doc, + configureFailPoint => $failpoint->{configureFailPoint}, + mode => $failpoint->{mode}, + defined $failpoint->{data} + ? ( data => $failpoint->{data} ) + : (), ]); } sub disable_failpoint { - my $doc = shift; + my $failpoint = shift; + return unless defined $failpoint; $conn->send_admin_command([ - configureFailPoint => 'onPrimaryTransactionalWrite', + configureFailPoint => $failpoint->{configureFailPoint}, mode => 'off', ]); } diff --git a/t/testrules.yml b/t/testrules.yml index 1864a79c..0df6bb62 100644 --- a/t/testrules.yml +++ b/t/testrules.yml @@ -7,4 +7,5 @@ seq: - seq: t/examples/changestream.t - seq: t/retryable-writes-spec.t - seq: t/retryable-writes-split-batch.t + - seq: t/transactions-spec.t - par: ** diff --git a/t/transactions-spec.t b/t/transactions-spec.t new file mode 100644 index 00000000..fe6cdb7b --- /dev/null +++ b/t/transactions-spec.t @@ -0,0 +1,655 @@ +# +# Copyright 2015 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. +# + +use strict; +use warnings; +use JSON::MaybeXS qw( is_bool decode_json ); +use Path::Tiny 0.054; # basename with suffix +use Test::More 0.96; +use Test::Deep; +use Math::BigInt; +use Storable qw( dclone ); + +use utf8; + +use MongoDB; +use MongoDB::_Types qw/ + to_IxHash +/; +use MongoDB::Error; + +use lib "t/lib"; + +use if $ENV{MONGOVERBOSE}, qw/Log::Any::Adapter Stderr/; + +use MongoDBTest qw/ + build_client + get_test_db + server_version + server_type + clear_testdbs + get_unique_collection + skip_unless_mongod + skip_unless_failpoints_available + skip_unless_transactions +/; + +skip_unless_mongod(); +skip_unless_failpoints_available(); +skip_unless_transactions(); + +my @events; + +sub clear_events { @events = () } +sub event_count { scalar @events } +# Must use dclone, as was causing action at a distance for binc on txn number +sub event_cb { push @events, dclone $_[0] } + +my $conn = build_client(); +my $server_version = server_version($conn); +my $server_type = server_type($conn); + +plan skip_all => "Requires MongoDB 4.0" + if $server_version < v4.0.0; + +plan skip_all => "deployment does not support transactions" + unless $conn->_topology->_supports_transactions; + +# defines which argument hash fields become positional arguments +my %method_args = ( + insert_one => [qw( document )], + insert_many => [qw( documents )], + delete_one => [qw( filter )], + delete_many => [qw( filter )], + replace_one => [qw( filter replacement )], + update_one => [qw( filter update )], + update_many => [qw( filter update )], + find => [qw( filter )], + count => [qw( filter )], + bulk_write => [qw( requests )], + find_one_and_update => [qw( filter update )], + find_one_and_replace => [qw( filter replacement )], + find_one_and_delete => [qw( filter )], + run_command => [qw( command readPreference )], + aggregate => [qw( pipeline )], + distinct => [qw( fieldName filter )], +); + +my $dir = path("t/data/transactions"); +my $iterator = $dir->iterator; +while ( my $path = $iterator->() ) { + next unless $path =~ /\.json$/; + my $plan = eval { decode_json( $path->slurp_utf8 ) }; + if ($@) { + die "Error decoding $path: $@"; + } + my $test_db_name = $plan->{database_name}; + my $test_coll_name = $plan->{collection_name}; + + subtest $path => sub { + + for my $test ( @{ $plan->{tests} } ) { + my $description = $test->{description}; + local $TODO = 'does a run_command read_preference count as a user configurable read_preference?' if $path =~ /run-command/ && $description =~ /explicit secondary read preference/; + subtest $description => sub { + my $client = build_client(); + + # Kills its own session as well + eval { $client->send_admin_command([ killAllSessions => [] ]) }; + my $test_db = $client->get_database( $test_db_name ); + + # We crank wtimeout up to 10 seconds to help reduce + # replication timeouts in testing + my $test_coll = $test_db->get_collection( + $test_coll_name, + { write_concern => { w => 'majority', wtimeout => 10000 } } + ); + $test_coll->drop; + + # Drop first to make sure its clear for the next test. + # MongoDB::Collection doesnt have a ->create option so done as + # a seperate step. + $test_db->run_command([ create => $test_coll_name ]); + + if ( scalar @{ $plan->{data} } > 0 ) { + $test_coll->insert_many( $plan->{data} ); + } + + set_failpoint( $client, $test->{failPoint} ); + run_test( $test_db_name, $test_coll_name, $test ); + clear_failpoint( $client, $test->{failPoint} ); + + if ( defined $test->{outcome}{collection}{data} ) { + my @outcome = $test_coll->find()->all; + cmp_deeply( \@outcome, $test->{outcome}{collection}{data}, 'outcome as expected' ) + } + }; + } + }; +} + +sub set_failpoint { + my ( $client, $failpoint ) = @_; + + return unless defined $failpoint; + my $ret = $client->send_admin_command([ + configureFailPoint => $failpoint->{configureFailPoint}, + mode => $failpoint->{mode}, + defined $failpoint->{data} + ? ( data => $failpoint->{data} ) + : (), + ]); +} + +sub clear_failpoint { + my ( $client, $failpoint ) = @_; + + return unless defined $failpoint; + my $ret = $client->send_admin_command([ + configureFailPoint => $failpoint->{configureFailPoint}, + mode => 'off', + ]); +} + +sub to_snake_case { + my $t = shift; + $t =~ s{([A-Z])}{_\L$1}g; + return $t; +} + +sub remap_hash_to_snake_case { + my $hash = shift; + return { + map { + my $k = to_snake_case( $_ ); + $k => $hash->{ $_ } + } keys %$hash + } +} + +# Global so can get values when checking sessions +my %sessions; + +sub run_test { + my ( $test_db_name, $test_coll_name, $test ) = @_; + + my $client_options = $test->{clientOptions} // {}; + $client_options = remap_hash_to_snake_case( $client_options ); + + # TODO Why is read_preference a read only mutator????.... + if ( exists $client_options->{read_preference} ) { + $client_options->{read_pref_mode} = delete $client_options->{read_preference}; + } + + my $client = build_client( monitoring_callback => \&event_cb, %$client_options ); + + my $session_options = $test->{sessionOptions} // {}; + + %sessions = ( + session0 => $client->start_session( $session_options->{session0} ), + session1 => $client->start_session( $session_options->{session1} ), + ); + $sessions{session0_lsid} = $sessions{session0}->session_id; + $sessions{session1_lsid} = $sessions{session1}->session_id; + + clear_events(); + for my $operation ( @{ $test->{operations} } ) { + + my $collection_options = $operation->{collectionOptions} // {}; + $collection_options = remap_hash_to_snake_case( $collection_options ); + + my $op_result = $operation->{result}; + + eval { + $sessions{ database } = $client->get_database( $test_db_name ); + $sessions{ collection } = $sessions{ database }->get_collection( $test_coll_name, $collection_options ); + my $cmd = to_snake_case( $operation->{name} ); + + # TODO count is checked specifically for errors during a transaction so warning here is not useful - we cannot change to count_documents, which is actually allowed in transactions. + local $ENV{PERL_MONGO_NO_DEP_WARNINGS} = 1 if $cmd eq 'count'; + + if ( $cmd =~ /_transaction$/ ) { + my $op_args = $operation->{arguments} // {}; + $sessions{ $operation->{object} }->$cmd( $op_args->{options} ); + } else { + my @args = _adjust_arguments( $cmd, $operation->{arguments} ); + $args[-1]->{session} = $sessions{ $args[-1]->{session} } + if exists $args[-1]->{session}; + $args[-1]->{returnDocument} = lc $args[-1]->{returnDocument} + if exists $args[-1]->{returnDocument}; + + if ( $cmd eq 'find' ) { + # not every find command actually has a filter + @args = ( undef, $args[0] ) + if scalar( @args ) == 1; + } + if ( $cmd eq 'run_command' ) { + $args[0] = to_IxHash( $args[0] ); + # move command to the beginning of the hash + my $cmd_arg = $args[0]->DELETE( $operation->{command_name} ); + $args[0]->Unshift( $operation->{command_name}, $cmd_arg ); + # May not have had a readPreference set + @args = ( $args[0], undef, $args[1] ) + if scalar( @args ) == 2; + } + if ( $cmd eq 'distinct' ) { + @args = ( $args[0], undef, $args[1] ) + if scalar( @args ) == 2; + } + my $ret = $sessions{ $operation->{object} }->$cmd( @args ); + + # special case 'find' so commands are actually emitted + my $result = $ret; + $result = [ $ret->all ] + if ( grep { $cmd eq $_ } qw/ find aggregate distinct / ); + + check_result_outcome( $result, $op_result ); + } + }; + my $err = $@; + check_error( $err, $op_result ); + } + + $sessions{session0}->end_session; + $sessions{session1}->end_session; + + if ( defined $test->{expectations} ) { + check_event_expectations( _adjust_types( $test->{expectations} ) ); + } + %sessions = (); +} + +sub check_error { + my ( $err, $exp ) = @_; + + my $expecting_error = 0; + if ( ref( $exp ) eq 'HASH' ) { + $expecting_error = grep {/^error/} keys %{ $exp }; + } + if ( $err ) { + unless ( $expecting_error ) { + my $diag_msg = 'Not expecting error, got "' . $err->message . '"'; + # abortTransactions are errors????? + if ( defined $events[-2] && $events[-2]->{commandName} eq 'abortTransaction' ) { + diag $diag_msg; + } else { + fail $diag_msg; + } + return; + } + + my $err_contains = $exp->{errorContains}; + my $err_code_name = $exp->{errorCodeName}; + my $err_labels_contains = $exp->{errorLabelsContain}; + my $err_labels_omit = $exp->{errorLabelsOmit}; + if ( defined $err_contains ) { + $err_contains =~ s/abortTransaction/abort_transaction/; + $err_contains =~ s/commitTransaction/commit_transaction/; + like $err->message, qr/$err_contains/i, 'error contains ' . $err_contains; + } + if ( defined $err_code_name ) { + is $err->result->output->{codeName}, + $err_code_name, + 'error has name ' . $err_code_name; + } + if ( defined $err_labels_omit ) { + for my $err_label ( @{ $err_labels_omit } ) { + ok ! $err->has_error_label( $err_label ), 'error doesnt have label ' . $err_label; + } + } + if ( defined $err_labels_omit ) { + for my $err_label ( @{ $err_labels_contains } ) { + ok $err->has_error_label( $err_label ), 'error has label ' . $err_label; + } + } + } elsif ( $expecting_error ) { + fail 'Expecting error, but no error found'; + } +} + +sub check_result_outcome { + my ( $got, $exp ) = @_; + + if ( ref( $exp ) eq 'ARRAY' ) { + check_array_result_outcome( $got, $exp ); + } else { + check_hash_result_outcome( $got, $exp ); + } +} + +sub check_array_result_outcome { + my ( $got, $exp ) = @_; + + cmp_deeply $got, $exp, 'result as expected'; +} + +sub check_hash_result_outcome { + my ( $got, $exp ) = @_; + + for my $key ( keys %$exp ) { + my $obj_key = to_snake_case( $key ); + next if ( $key eq 'upsertedCount' && !$got->can('upserted_count') ); + # Some results are just raw results + if ( ref $got eq 'HASH' ) { + cmp_deeply $got->{ $obj_key }, $exp->{ $key }, "$key result correct"; + } else { + cmp_deeply $got->$obj_key, $exp->{ $key }, "$key result correct"; + } + } +} + +# Following subs modified from monitoring_spec.t +# + + +# prepare collection method arguments +# adjusts data structures and extracts leading positional arguments +sub _adjust_arguments { + my ($method, $args) = @_; + + $args = _adjust_types($args); + my @fields = @{ $method_args{$method} }; + my @field_values = map { + my $val = delete $args->{$_}; + # bulk write is special cased to reuse argument extraction + ($method eq 'bulk_write' and $_ eq 'requests') + ? _adjust_bulk_write_requests($val) + : $val; + } @fields; + + return( + (grep { defined } @field_values), + scalar(keys %$args) ? $args : (), + ); +} + +# prepare bulk write requests for use as argument to ->bulk_write +sub _adjust_bulk_write_requests { + my ($requests) = @_; + + return [map { + # Different data structure in bulk writes compared to command_monitoring + my $name = to_snake_case( $_->{name} ); + +{ $name => [_adjust_arguments($name, $_->{arguments})] }; + } @$requests]; +} + +# some type transformations +# turns { '$numberLong' => $n } into 0+$n +sub _adjust_types { + my ($value) = @_; + if (ref $value eq 'HASH') { + if (scalar(keys %$value) == 1) { + my ($name, $value) = %$value; + if ($name eq '$numberLong') { + return 0+$value; + } + } + return +{map { + my $key = $_; + ($key, _adjust_types($value->{$key})); + } keys %$value}; + } + elsif (ref $value eq 'ARRAY') { + return [map { _adjust_types($_) } @$value]; + } + else { + return $value; + } +} + +# common overrides for event data expectations +sub prepare_data_spec { + my ($spec) = @_; + if ( ! defined $spec ) { + return $spec; + } + elsif (not ref $spec) { + if ($spec eq 'test') { + return any(qw( test test_collection )); + } + if ($spec eq 'test-unacknowledged-bulk-write') { + return code(\&_verify_is_nonempty_str); + } + if ($spec eq 'command-monitoring-tests.test') { + return code(\&_verify_is_nonempty_str); + } + return $spec; + } + elsif (is_bool $spec) { + my $specced = $spec ? 1 : 0; + return code(sub { + my $value = shift; + return(0, 'expected a true boolean value') + if $specced and not $value; + return(0, 'expected a false boolean value') + if $value and not $specced; + return 1; + }); + } + elsif (ref $spec eq 'ARRAY') { + return [map { + prepare_data_spec($_) + } @$spec]; + } + elsif (ref $spec eq 'HASH') { + return +{map { + ($_, prepare_data_spec($spec->{$_})) + } keys %$spec}; + } + else { + return $spec; + } +} + +sub check_event_expectations { + my ( $expected ) = @_; + # We only care about command_started events + # also ignoring ismaster commands caused by re-negotiation after network error + my @got = grep { $_->{type} eq 'command_started' && $_->{commandName} ne 'ismaster' } @events; + + for my $exp ( @$expected ) { + my ($exp_type, $exp_spec) = %$exp; + # We only have command_started_event checks + subtest $exp_type => sub { + ok(scalar(@got), 'event available') + or return; + my $event = shift @got; + is($event->{type}.'_event', $exp_type, "is a $exp_type") + or return; + my $event_tester = "check_$exp_type"; + main->can($event_tester)->($exp_spec, $event); + }; + } + + is scalar(@got), 0, 'no outstanding events'; +} + +sub check_event { + my ($exp, $event) = @_; + for my $key (sort keys %$exp) { + my $check = "check_${key}_field"; + main->can($check)->($exp->{$key}, $event); + } +} + +# +# per-event type test handlers +# + +sub check_command_started_event { + my ($exp, $event) = @_; + check_event($exp, $event); +} + +# +# verificationi subs for use with Test::Deep::code +# + +sub _verify_is_positive_num { + my $value = shift; + return(0, "error code is not defined") + unless defined $value; + return(0, "error code is not positive") + unless $value > 1; + return 1; +} + +sub _verify_is_nonempty_str { + my $value = shift; + return(0, "error message is not defined") + unless defined $value; + return(0, "error message is empty") + unless length $value; + return 1; +} + +# +# event field test handlers +# + +# $event.database_name +sub check_database_name_field { + my ($exp_name, $event) = @_; + ok defined($event->{databaseName}), "database_name defined"; + ok length($event->{databaseName}), "database_name non-empty"; +} + +# $event.command_name +sub check_command_name_field { + my ($exp_name, $event) = @_; + is $event->{commandName}, $exp_name, "command name"; +} + +# $event.reply +sub check_reply_field { + my ($exp_reply, $event) = @_; + my $event_reply = $event->{reply}; + + # special case for $event.reply.cursor.id + if (exists $exp_reply->{cursor}) { + if (exists $exp_reply->{cursor}{id}) { + $exp_reply->{cursor}{id} = code(\&_verify_is_positive_num) + if $exp_reply->{cursor}{id} eq '42'; + } + } + + # special case for $event.reply.writeErrors + if (exists $exp_reply->{writeErrors}) { + for my $i ( 0 .. $#{ $exp_reply->{writeErrors} } ) { + my $error = $exp_reply->{writeErrors}[$i]; + if (exists $error->{code} and $error->{code} eq 42) { + $error->{code} = code(\&_verify_is_positive_num); + } + if (exists $error->{errmsg} and $error->{errmsg} eq '') { + $error->{errmsg} = code(\&_verify_is_nonempty_str); + } + $exp_reply->{writeErrors}[$i] = superhashof( $error ); + } + } + + # special case for $event.command.cursorsUnknown on killCursors + if ($event->{commandName} eq 'killCursors' + and defined $exp_reply->{cursorsUnknown} + ) { + for my $index (0 .. $#{ $exp_reply->{cursorsUnknown} }) { + $exp_reply->{cursorsUnknown}[$index] + = code(\&_verify_is_positive_num) + if $exp_reply->{cursorsUnknown}[$index] eq 42; + } + } + + for my $exp_key (sort keys %$exp_reply) { + cmp_deeply + $event_reply->{$exp_key}, + prepare_data_spec($exp_reply->{$exp_key}), + "reply field $exp_key" or diag explain $event_reply->{$exp_key}; + } +} + +# $event.command +sub check_command_field { + my ($exp_command, $event) = @_; + my $event_command = $event->{command}; + + # ordered defaults to true + delete $exp_command->{ordered}; + + # special case for $event.command.getMore + if (exists $exp_command->{getMore}) { + $exp_command->{getMore} = code(\&_verify_is_positive_num) + if $exp_command->{getMore} eq '42'; + } + + # special case for $event.command.writeConcern.wtimeout + if (defined $exp_command->{writeConcern}) { + $exp_command->{writeConcern}{wtimeout} = ignore(); + } + + # special case for $event.command.cursors on killCursors + if ($event->{commandName} eq 'killCursors' + and defined $exp_command->{cursors} + ) { + for my $index (0 .. $#{ $exp_command->{cursors} }) { + $exp_command->{cursors}[$index] + = code(\&_verify_is_positive_num) + if $exp_command->{cursors}[$index] eq 42; + } + } + + if ( defined $exp_command->{lsid} ) { + # Stuff correct session id in + $exp_command->{lsid} = $sessions{ $exp_command->{lsid} . '_lsid' }; + } + + if ( defined $exp_command->{readConcern} ) { + $exp_command->{readConcern}{afterClusterTime} = Isa('BSON::Timestamp') + if ( defined $exp_command->{readConcern}{afterClusterTime} && $exp_command->{readConcern}{afterClusterTime} eq '42' ); + } + + if ( defined $exp_command->{txnNumber} ) { + $exp_command->{txnNumber} = Math::BigInt->new($exp_command->{txnNumber}); + } + + for my $exp_key (sort keys %$exp_command) { + my $event_value = $event_command->{$exp_key}; + my $exp_value = prepare_data_spec($exp_command->{$exp_key}); + my $label = "command field '$exp_key'"; + + if ( + (grep { $exp_key eq $_ } qw( comment maxTimeMS )) + or + ($event->{commandName} eq 'getMore' and $exp_key eq 'batchSize') + ) { + TODO: { + local $TODO = + "Command field '$exp_key' requires other fixes"; + cmp_deeply $event_value, $exp_value, $label; + } + } + elsif ( !defined $exp_value ) + { + ok ! exists $event_command->{$exp_key}, $label . ' does not exist'; + } + else { + cmp_deeply $event_value, $exp_value, $label; + } + } +} + +clear_testdbs; + +done_testing;