diff --git a/lib/MongoDB/IndexView.pm b/lib/MongoDB/IndexView.pm index e72fe079..a9b70172 100644 --- a/lib/MongoDB/IndexView.pm +++ b/lib/MongoDB/IndexView.pm @@ -173,6 +173,12 @@ direction/type. See L for important information about index specifications and options. +The following additional options are recognized: + +=for :list +* C — maximum time in milliseconds before the operation will + time out. + =cut my $create_one_args; @@ -183,8 +189,15 @@ sub create_one { MongoDB::UsageError->throw("Argument to create_one must be an ordered document") unless is_OrderedDoc($keys); - my ($name) = - $self->create_many( { keys => $keys, ( $opts ? ( options => $opts ) : () ) } ); + my $global_opts = {}; + if (exists $opts->{maxTimeMS}) { + $global_opts->{maxTimeMS} = delete $opts->{maxTimeMS}; + } + + my ($name) = $self->create_many( + { keys => $keys, ( $opts ? ( options => $opts ) : () ) }, + $global_opts, + ); return $name; } @@ -195,10 +208,20 @@ sub create_one { { keys => [ z => 1 ], options => { unique => 1 } } ); + @names = $indexes->create_many( + { keys => [ x => 1, y => 1 ] }, + { keys => [ z => 1 ], options => { unique => 1 } } + \%global_options, + ); + This method takes a list of index models (given as hash references) and returns a list of index names created. It will throw an exception on error. +If the last value is a hash reference without a C entry, it will +be assumed to be a set of global options. See below for a list of +accepted global options. + Each index module is described by the following fields: =for :list @@ -242,6 +265,13 @@ Some of the more commonly used options include: * C — a name (string) for the index; one will be generated if this is omitted. +Global options specified as the last value can contain the following +keys: + +=for :list +* C — maximum time in milliseconds before the operation will + time out. + =cut my $create_many_args; @@ -249,6 +279,11 @@ my $create_many_args; sub create_many { my ( $self, @models ) = @_; + my $opts; + if (@models and ref $models[-1] eq 'HASH' and not exists $models[-1]{keys}) { + $opts = pop @models; + } + MongoDB::UsageError->throw("Argument to create_many must be a list of index models") unless is_IndexModelList(\@models); @@ -260,6 +295,10 @@ sub create_many { bson_codec => $self->_bson_codec, indexes => $indexes, write_concern => $self->_write_concern, + (defined($opts->{maxTimeMS}) + ? (max_time_ms => $opts->{maxTimeMS}) + : () + ), ); # succeed or die; we don't care about response document @@ -271,6 +310,7 @@ sub create_many { =method drop_one $output = $indexes->drop_one( $name ); + $output = $indexes->drop_one( $name, \%options ); This method takes the name of an index and drops it. It returns the output of the dropIndexes command (a hash reference) on success or throws a @@ -278,12 +318,18 @@ exception if the command errors. However, if the index does not exist, the command output will have the C field as a false value, but no exception will e thrown. +Valid options are: + +=for :list +* C — maximum time in milliseconds before the operation will + time out. + =cut my $drop_one_args; sub drop_one { - my ( $self, $name ) = @_; + my ( $self, $name, $opts ) = @_; MongoDB::UsageError->throw("Argument to drop_one must be a string") unless is_Str($name); @@ -299,6 +345,10 @@ sub drop_one { bson_codec => $self->_bson_codec, write_concern => $self->_write_concern, index_name => $name, + (defined($opts->{maxTimeMS}) + ? (max_time_ms => $opts->{maxTimeMS}) + : () + ), ); $self->_client->send_write_op($op)->output; @@ -307,17 +357,24 @@ sub drop_one { =method drop_all $output = $indexes->drop_all; + $output = $indexes->drop_all(\%options); This method drops all indexes (except the one on the C<_id> field). It returns the output of the dropIndexes command (a hash reference) on success or throws a exception if the command fails. +Valid options are: + +=for :list +* C — maximum time in milliseconds before the operation will + time out. + =cut my $drop_all_args; sub drop_all { - my ($self) = @_; + my ($self, $opts) = @_; my $op = MongoDB::Op::_DropIndexes->_new( db_name => $self->_db_name, @@ -326,6 +383,10 @@ sub drop_all { bson_codec => $self->_bson_codec, write_concern => $self->_write_concern, index_name => '*', + (defined($opts->{maxTimeMS}) + ? (max_time_ms => $opts->{maxTimeMS}) + : () + ), ); $self->_client->send_write_op($op)->output; diff --git a/lib/MongoDB/Op/_CreateIndexes.pm b/lib/MongoDB/Op/_CreateIndexes.pm index 51c1051f..d832e598 100644 --- a/lib/MongoDB/Op/_CreateIndexes.pm +++ b/lib/MongoDB/Op/_CreateIndexes.pm @@ -32,6 +32,7 @@ use Types::Standard qw( ArrayRef Bool HashRef + Num ); use namespace::clean; @@ -42,6 +43,11 @@ has indexes => ( isa => ArrayRef [HashRef], ); +has max_time_ms => ( + is => 'ro', + isa => Num, +); + with $_ for qw( MongoDB::Role::_PrivateConstructor MongoDB::Role::_CollectionOp @@ -76,7 +82,11 @@ sub _command_create_indexes { query => [ createIndexes => $self->coll_name, indexes => $self->indexes, - ( $link->accepts_wire_version(5) ? ( @{ $self->write_concern->as_args } ) : () ) + ( $link->accepts_wire_version(5) ? ( @{ $self->write_concern->as_args } ) : () ), + (defined($self->max_time_ms) + ? (maxTimeMS => $self->max_time_ms) + : () + ), ], query_flags => {}, bson_codec => $self->bson_codec, diff --git a/lib/MongoDB/Op/_DropIndexes.pm b/lib/MongoDB/Op/_DropIndexes.pm index 04ad2d04..c7d6e576 100644 --- a/lib/MongoDB/Op/_DropIndexes.pm +++ b/lib/MongoDB/Op/_DropIndexes.pm @@ -30,6 +30,7 @@ use MongoDB::Op::_Command; use Safe::Isa; use Types::Standard qw( Str + Num ); use namespace::clean; @@ -40,6 +41,11 @@ has index_name => ( isa => Str, ); +has max_time_ms => ( + is => 'ro', + isa => Num, +); + with $_ for qw( MongoDB::Role::_PrivateConstructor MongoDB::Role::_CollectionOp @@ -55,6 +61,10 @@ sub execute { dropIndexes => $self->coll_name, index => $self->index_name, ( $link->accepts_wire_version(5) ? ( @{ $self->write_concern->as_args } ) : () ), + (defined($self->max_time_ms) + ? (maxTimeMS => $self->max_time_ms) + : () + ), ], query_flags => {}, bson_codec => $self->bson_codec, diff --git a/t/indexview.t b/t/indexview.t index 90a4d76f..df61ff93 100644 --- a/t/indexview.t +++ b/t/indexview.t @@ -35,6 +35,7 @@ my $testdb = get_test_db($conn); my $server_version = server_version($conn); my $server_type = server_type($conn); my $coll = $testdb->get_collection('test_collection'); +my $admin = $conn->get_database("admin"); my $supports_collation = $server_version >= 3.3.9; my $valid_collation = { locale => "en_US", strength => 2 }; diff --git a/t/max_time_ms.t b/t/max_time_ms.t index f387e5da..8ca77957 100644 --- a/t/max_time_ms.t +++ b/t/max_time_ms.t @@ -64,6 +64,8 @@ $bulk->insert_one( { _id => $_ } ) for 1 .. 20; my $err = exception { $bulk->execute }; is( $err, undef, "inserted 20 documents for testing" ); +my $iv = $coll->indexes; + subtest "expected behaviors" => sub { is( exception { $coll->find->max_time_ms()->next }, undef, "find->max_time_ms()" ); @@ -429,4 +431,254 @@ subtest "force maxTimeMS failures" => sub { ); }; +subtest "create_many w/ maxTimeMS" => sub { + plan skip_all => "maxTimeMS not available before 3.6" + unless $server_version >= v3.6.0; + + plan skip_all => "enableTestCommands is off" + unless $param && $param->{enableTestCommands}; + + plan skip_all => "fail points not supported via mongos" + if $server_type eq 'Mongos'; + + $coll->drop; + + is( + exception { + $admin->run_command([ + configureFailPoint => 'maxTimeAlwaysTimeOut', + mode => 'alwaysOn', + ]); + }, + undef, + 'max time failpoint on', + ); + + like( + exception { + $iv->create_many( + { keys => [ x => 1 ] }, { keys => [ y => -1 ] }, + { maxTimeMS => 10 }, + ); + }, + qr/exceeded time limit/, + 'timeout for index creation', + ); + + is( + exception { + $iv->create_many( + { keys => [ x => 1 ] }, { keys => [ y => -1 ] }, + ); + }, + undef, + 'no timeout without max time', + ); + + is( + exception { + $admin->run_command([ + configureFailPoint => 'maxTimeAlwaysTimeOut', + mode => 'off', + ]); + }, + undef, + 'max time failpoint off', + ); + + is( + exception { + $iv->create_many( + { keys => [ x => 1 ] }, { keys => [ y => -1 ] }, + { maxTimeMS => 5000 }, + ); + }, + undef, + 'no timeout for index creation', + ); +}; + +subtest "create_one w/ maxTimeMS" => sub { + plan skip_all => "maxTimeMS not available before 3.6" + unless $server_version >= v3.6.0; + + plan skip_all => "enableTestCommands is off" + unless $param && $param->{enableTestCommands}; + + plan skip_all => "fail points not supported via mongos" + if $server_type eq 'Mongos'; + + $coll->drop; + + is( + exception { + $admin->run_command([ + configureFailPoint => 'maxTimeAlwaysTimeOut', + mode => 'alwaysOn', + ]); + }, + undef, + 'max time failpoint on', + ); + + is( + exception { + $iv->create_one([ x => 1 ]); + }, + undef, + 'no timeout without max time', + ); + + like( + exception { + $iv->create_one([ x => 1 ], { maxTimeMS => 10 }); + }, + qr/exceeded time limit/, + 'timeout for index creation', + ); + + is( + exception { + $admin->run_command([ + configureFailPoint => 'maxTimeAlwaysTimeOut', + mode => 'off', + ]); + }, + undef, + 'max time failpoint off', + ); + + is( + exception { + $iv->create_one([ x => 1 ], { maxTimeMS => 5000 }); + }, + undef, + 'no timeout for index creation', + ); +}; + +subtest "drop_one w/ maxTimeMS" => sub { + plan skip_all => "maxTimeMS not available before 3.6" + unless $server_version >= v3.6.0; + + plan skip_all => "enableTestCommands is off" + unless $param && $param->{enableTestCommands}; + + plan skip_all => "fail points not supported via mongos" + if $server_type eq 'Mongos'; + + $coll->drop; + + is( + exception { + $admin->run_command([ + configureFailPoint => 'maxTimeAlwaysTimeOut', + mode => 'alwaysOn', + ]); + }, + undef, + 'max time failpoint on', + ); + + is( + exception { + my $name = $iv->create_one([ x => 1 ]); + $iv->drop_one($name); + }, + undef, + 'no timeout without max time', + ); + + like( + exception { + my $name = $iv->create_one([ x => 1 ]); + $iv->drop_one($name, { maxTimeMS => 10 }); + }, + qr/exceeded time limit/, + 'timeout for index drop', + ); + + is( + exception { + $admin->run_command([ + configureFailPoint => 'maxTimeAlwaysTimeOut', + mode => 'off', + ]); + }, + undef, + 'max time failpoint off', + ); + + is( + exception { + my $name = $iv->create_one([ x => 1 ]); + $iv->drop_one($name, { maxTimeMS => 5000 }); + }, + undef, + 'no timeout for index drop', + ); +}; + +subtest "drop_all w/ maxTimeMS" => sub { + plan skip_all => "maxTimeMS not available before 3.6" + unless $server_version >= v3.6.0; + + plan skip_all => "enableTestCommands is off" + unless $param && $param->{enableTestCommands}; + + plan skip_all => "fail points not supported via mongos" + if $server_type eq 'Mongos'; + + $coll->drop; + + is( + exception { + $admin->run_command([ + configureFailPoint => 'maxTimeAlwaysTimeOut', + mode => 'alwaysOn', + ]); + }, + undef, + 'max time failpoint on', + ); + + is( + exception { + $iv->create_many( map { { keys => $_ } }[ x => 1 ], [ y => 1 ], [ z => 1 ] ); + $iv->drop_all(); + }, + undef, + 'no timeout without max time', + ); + + like( + exception { + $iv->create_many( map { { keys => $_ } }[ x => 1 ], [ y => 1 ], [ z => 1 ] ); + $iv->drop_all({ maxTimeMS => 10 }); + }, + qr/exceeded time limit/, + 'timeout for index drop', + ); + + is( + exception { + $admin->run_command([ + configureFailPoint => 'maxTimeAlwaysTimeOut', + mode => 'off', + ]); + }, + undef, + 'max time failpoint off', + ); + + is( + exception { + $iv->create_many( map { { keys => $_ } }[ x => 1 ], [ y => 1 ], [ z => 1 ] ); + $iv->drop_all({ maxTimeMS => 5000 }); + }, + undef, + 'no timeout for index drop', + ); +}; + done_testing;