diff options
40 files changed, 653 insertions, 144 deletions
diff --git a/buildscripts/resmokeconfig/suites/multi_shard_local_read_write_multi_stmt_txn_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/multi_shard_local_read_write_multi_stmt_txn_jscore_passthrough.yml index aa9e84dbc1b..e830c47cd85 100644 --- a/buildscripts/resmokeconfig/suites/multi_shard_local_read_write_multi_stmt_txn_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/multi_shard_local_read_write_multi_stmt_txn_jscore_passthrough.yml @@ -134,7 +134,7 @@ selector: ## Some aggregation stages aren't allowed in a transaction. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/collation.js - jstests/core/explain_shell_helpers.js @@ -178,6 +178,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed in a transaction. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_jscore_passthrough.yml index a2d14686ceb..fead8bf2e92 100644 --- a/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_jscore_passthrough.yml @@ -136,7 +136,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -200,6 +200,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_kill_primary_jscore_passthrough.yml index b61aed35737..5b68cfbb2b7 100644 --- a/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_kill_primary_jscore_passthrough.yml @@ -131,7 +131,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -188,6 +188,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_stepdown_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_stepdown_primary_jscore_passthrough.yml index 4fe4a9b5343..f3cc72c5a3c 100644 --- a/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_stepdown_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_stepdown_primary_jscore_passthrough.yml @@ -132,7 +132,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -189,6 +189,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/multi_stmt_txn_jscore_passthrough_with_migration.yml b/buildscripts/resmokeconfig/suites/multi_stmt_txn_jscore_passthrough_with_migration.yml index 1cb8898a421..108a850e49d 100644 --- a/buildscripts/resmokeconfig/suites/multi_stmt_txn_jscore_passthrough_with_migration.yml +++ b/buildscripts/resmokeconfig/suites/multi_stmt_txn_jscore_passthrough_with_migration.yml @@ -144,7 +144,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -208,6 +208,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_kill_primary_jscore_passthrough.yml index 4d7ac98666e..2784b7ee3e6 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_kill_primary_jscore_passthrough.yml @@ -47,6 +47,7 @@ selector: - jstests/core/insert2.js # Creates new mongo connection. - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/startup_log.js # Checks pid, which is different on each server. + - jstests/core/validate_cmd_ns.js # Calls _exec() directly, not retryable. # Creates new mongo connection but won't retry connecting. - jstests/core/shell_connection_strings.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml index f6e153321c3..b941a428ed3 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml @@ -78,7 +78,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -148,6 +148,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml index 658df1cca30..2d8f1367138 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml @@ -73,7 +73,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -136,6 +136,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml index 13227f2f413..b75d012a07f 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml @@ -72,7 +72,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -135,6 +135,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml index a0a30f7a18b..c4f70cafc08 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml @@ -70,7 +70,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -133,6 +133,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_passthrough.yml index a12f9273ee6..e6318e6bd0a 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_passthrough.yml @@ -19,6 +19,7 @@ selector: # These test run commands using legacy queries, which are not supported on sessions. - jstests/core/comment_field.js - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # Unacknowledged writes prohibited in an explicit session. - jstests/core/batch_write_command_w0.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_stepdown_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_stepdown_passthrough.yml index f0187167f88..b2f526f9170 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_stepdown_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_stepdown_passthrough.yml @@ -34,6 +34,7 @@ selector: # These test run commands using legacy queries, which are not supported on sessions. - jstests/core/comment_field.js - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js @@ -61,6 +62,7 @@ selector: - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/top.js # Tests read commands (including getMore) against the secondary - jstests/core/drop3.js # getMore is not causally consistent if collection is dropped + - jstests/core/validate_cmd_ns.js # Calls _exec() directly, not retryable. - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/explain_large_bounds.js # Stepdown can timeout waiting for global lock. diff --git a/buildscripts/resmokeconfig/suites/replica_sets_reconfig_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_reconfig_kill_primary_jscore_passthrough.yml index 7462c89afb2..92db4edf639 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_reconfig_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_reconfig_kill_primary_jscore_passthrough.yml @@ -35,6 +35,7 @@ selector: # These test run commands using legacy queries, which are not supported on sessions. - jstests/core/comment_field.js - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js @@ -62,6 +63,7 @@ selector: - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/top.js # Tests read commands (including getMore) against the secondary - jstests/core/drop3.js # getMore is not causally consistent if collection is dropped + - jstests/core/validate_cmd_ns.js # Calls _exec() directly, not retryable. - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/explain_large_bounds.js # Stepdown can timeout waiting for global lock. diff --git a/buildscripts/resmokeconfig/suites/replica_sets_terminate_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_terminate_primary_jscore_passthrough.yml index 22e53534b16..0decf8530d8 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_terminate_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_terminate_primary_jscore_passthrough.yml @@ -47,6 +47,7 @@ selector: - jstests/core/insert2.js # Creates new mongo connection. - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/startup_log.js # Checks pid, which is different on each server. + - jstests/core/validate_cmd_ns.js # Calls _exec() directly, not retryable. # Creates new mongo connection but won't retry connecting. - jstests/core/shell_connection_strings.js diff --git a/buildscripts/resmokeconfig/suites/retryable_writes_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/retryable_writes_jscore_passthrough.yml index 6a16af8357c..101b4295dfa 100644 --- a/buildscripts/resmokeconfig/suites/retryable_writes_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/retryable_writes_jscore_passthrough.yml @@ -19,6 +19,7 @@ selector: # These test run commands using legacy queries, which are not supported on sessions. - jstests/core/comment_field.js - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # TODO SERVER-31242: findAndModify no-op retry should respect the fields option. - jstests/core/crud_api.js diff --git a/buildscripts/resmokeconfig/suites/retryable_writes_jscore_stepdown_passthrough.yml b/buildscripts/resmokeconfig/suites/retryable_writes_jscore_stepdown_passthrough.yml index fd1a3c2963a..f81f81d153f 100644 --- a/buildscripts/resmokeconfig/suites/retryable_writes_jscore_stepdown_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/retryable_writes_jscore_stepdown_passthrough.yml @@ -51,6 +51,7 @@ selector: - jstests/core/insert2.js # Creates new mongo connection. - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/startup_log.js # Checks pid, which is different on each server. + - jstests/core/validate_cmd_ns.js # Calls _exec() directly, not retryable. exclude_with_any_tags: - assumes_standalone_mongod diff --git a/buildscripts/resmokeconfig/suites/session_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/session_jscore_passthrough.yml index 9a44de18f12..2bb5fdfb37d 100644 --- a/buildscripts/resmokeconfig/suites/session_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/session_jscore_passthrough.yml @@ -10,6 +10,7 @@ selector: # These test run commands using legacy queries, which are not supported on sessions. - jstests/core/comment_field.js - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # Unacknowledged writes prohibited in an explicit session. - jstests/core/crud_api.js diff --git a/buildscripts/resmokeconfig/suites/sharded_multi_stmt_txn_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/sharded_multi_stmt_txn_jscore_passthrough.yml index a631b980d9f..f38fc8934f6 100644 --- a/buildscripts/resmokeconfig/suites/sharded_multi_stmt_txn_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/sharded_multi_stmt_txn_jscore_passthrough.yml @@ -104,7 +104,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -172,6 +172,7 @@ selector: # SERVER-34868 Cannot run a legacy query on a session. - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js diff --git a/buildscripts/resmokeconfig/suites/tenant_migration_causally_consistent_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/tenant_migration_causally_consistent_jscore_passthrough.yml index f5aaf2f72b5..cc0c454e223 100644 --- a/buildscripts/resmokeconfig/suites/tenant_migration_causally_consistent_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/tenant_migration_causally_consistent_jscore_passthrough.yml @@ -144,6 +144,7 @@ selector: # These test run commands using legacy queries, which are not supported on sessions. - jstests/core/comment_field.js - jstests/core/exhaust.js + - jstests/core/validate_cmd_ns.js # Unacknowledged writes prohibited in an explicit session. - jstests/core/crud_api.js diff --git a/buildscripts/resmokeconfig/suites/tenant_migration_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/tenant_migration_kill_primary_jscore_passthrough.yml index 3b00933e879..afa3606c245 100644 --- a/buildscripts/resmokeconfig/suites/tenant_migration_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/tenant_migration_kill_primary_jscore_passthrough.yml @@ -144,6 +144,7 @@ selector: - jstests/core/insert2.js # Creates new mongo connection. - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/startup_log.js # Checks pid, which is different on each server. + - jstests/core/validate_cmd_ns.js # Calls _exec() directly, not retryable. # # Denylists specific to this suite diff --git a/buildscripts/resmokeconfig/suites/tenant_migration_multi_stmt_txn_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/tenant_migration_multi_stmt_txn_jscore_passthrough.yml index 5c813fd7a53..d0555fc4e6d 100644 --- a/buildscripts/resmokeconfig/suites/tenant_migration_multi_stmt_txn_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/tenant_migration_multi_stmt_txn_jscore_passthrough.yml @@ -193,7 +193,7 @@ selector: ## Some aggregation stages don't support snapshot readconcern. ## - # explain (requires read concern local) + # $explain (requires read concern local) - jstests/core/agg_hint.js - jstests/core/and.js - jstests/core/collation.js @@ -253,6 +253,9 @@ selector: ## Misc. reasons. ## + # SERVER-34868 Cannot run a legacy query on a session. + - jstests/core/validate_cmd_ns.js + # SERVER-34772 Tailable Cursors are not allowed with snapshot readconcern. - jstests/core/awaitdata_getmore_cmd.js - jstests/core/getmore_cmd_maxtimems.js diff --git a/buildscripts/resmokeconfig/suites/tenant_migration_stepdown_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/tenant_migration_stepdown_jscore_passthrough.yml index be354b863c4..8be100ca0f6 100644 --- a/buildscripts/resmokeconfig/suites/tenant_migration_stepdown_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/tenant_migration_stepdown_jscore_passthrough.yml @@ -144,6 +144,7 @@ selector: - jstests/core/insert2.js # Creates new mongo connection. - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/startup_log.js # Checks pid, which is different on each server. + - jstests/core/validate_cmd_ns.js # Calls _exec() directly, not retryable. # # Denylists specific to this suite diff --git a/buildscripts/resmokeconfig/suites/tenant_migration_terminate_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/tenant_migration_terminate_primary_jscore_passthrough.yml index dc26c216799..87679d6a701 100644 --- a/buildscripts/resmokeconfig/suites/tenant_migration_terminate_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/tenant_migration_terminate_primary_jscore_passthrough.yml @@ -144,6 +144,7 @@ selector: - jstests/core/insert2.js # Creates new mongo connection. - jstests/core/list_collections_filter.js # Temporary collections are dropped on failover. - jstests/core/startup_log.js # Checks pid, which is different on each server. + - jstests/core/validate_cmd_ns.js # Calls _exec() directly, not retryable. # # Denylists specific to this suite diff --git a/jstests/core/exists7.js b/jstests/core/exists7.js index 285559e82f8..6ca55a42286 100644 --- a/jstests/core/exists7.js +++ b/jstests/core/exists7.js @@ -1,6 +1,6 @@ // @tags: [requires_non_retryable_writes] -// Test that non boolean value types are allowed with $exists spec. SERVER-2322 +// Test that non boolean value types are allowed with $explain spec. SERVER-2322 t = db.jstests_exists7; t.drop(); diff --git a/jstests/core/validate_cmd_ns.js b/jstests/core/validate_cmd_ns.js new file mode 100644 index 00000000000..f5f706335ad --- /dev/null +++ b/jstests/core/validate_cmd_ns.js @@ -0,0 +1,24 @@ +/** + * Tests that query against the $cmd namespace will error out when the request has + * a number to return value other than 1 or -1. Note that users cannot have + * collections named $cmd since $ is an illegal character. + */ + +// Note: _exec gives you the raw response from the server. +var res = db.$cmd.find({whatsmyuri: 1})._exec().next(); +assert.commandFailed(res); +assert(res.errmsg.indexOf('Bad numberToReturn') > -1); + +res = db.$cmd.find({whatsmyuri: 1}).limit(0)._exec().next(); +assert.commandFailed(res); +assert(res.errmsg.indexOf('Bad numberToReturn') > -1); + +res = db.$cmd.find({whatsmyuri: 1}).limit(-2)._exec().next(); +assert.commandFailed(res); +assert(res.errmsg.indexOf('Bad numberToReturn') > -1); + +res = db.$cmd.find({whatsmyuri: 1}).limit(1).next(); +assert.commandWorked(res); + +res = db.$cmd.find({whatsmyuri: 1}).limit(-1).next(); +assert.commandWorked(res); diff --git a/jstests/noPassthrough/exhaust_option_disallowed_in_session.js b/jstests/noPassthrough/exhaust_option_disallowed_in_session.js new file mode 100644 index 00000000000..1ba1014dc2f --- /dev/null +++ b/jstests/noPassthrough/exhaust_option_disallowed_in_session.js @@ -0,0 +1,32 @@ +/** + * Make sure the 'exhaust' query option is not able to be used in a session. + */ +(function() { +"use strict"; + +let conn = MongoRunner.runMongod(); + +const dbName = 'test'; +const collName = 'coll'; + +const session = conn.startSession(); +const sessionColl = session.getDatabase(dbName).getCollection(collName); +const testColl = conn.getDB(dbName).getCollection(collName); + +testColl.drop(); + +// Create a collection to query. +assert.commandWorked(testColl.insert({_id: 1})); + +// Exhaust outside of session should work. +let docs = testColl.find().addOption(DBQuery.Option.exhaust).toArray(); +assert.docEq([{_id: 1}], docs); + +// Exhaust in session should fail. +assert.throws(() => { + sessionColl.find().addOption(DBQuery.Option.exhaust).toArray(); +}); + +session.endSession(); +MongoRunner.stopMongod(conn); +}()); diff --git a/jstests/noPassthrough/max_time_ms.js b/jstests/noPassthrough/max_time_ms.js index a780b21cbbb..050b85ac648 100644 --- a/jstests/noPassthrough/max_time_ms.js +++ b/jstests/noPassthrough/max_time_ms.js @@ -320,9 +320,20 @@ function executeTest(db, isMongos) { ErrorCodes.BadValue); assert.commandFailedWithCode(db.runCommand({ping: 1, maxTimeMS: {}}), ErrorCodes.BadValue); + // Verify that the maxTimeMS command argument can be sent with $query-wrapped commands. + cursor = t.getDB().$cmd.find({ping: 1, maxTimeMS: 0}).limit(-1); + cursor._ensureSpecial(); + assert.eq(1, cursor.next().ok); + // Verify that the server rejects invalid command argument $maxTimeMS. - assert.commandFailed(t.getDB().runCommand({ping: 1, $maxTimeMS: 0}), - [ErrorCodes.InvalidOptions, 40415]); + cursor = t.getDB().$cmd.find({ping: 1, $maxTimeMS: 0}).limit(-1); + cursor._ensureSpecial(); + assert.commandFailed(cursor.next()); + + // Verify that the $maxTimeMS query option can't be sent with $query-wrapped commands. + cursor = t.getDB().$cmd.find({ping: 1}).limit(-1).maxTimeMS(0); + cursor._ensureSpecial(); + assert.commandFailed(cursor.next()); })(); // diff --git a/jstests/sharding/query_sharded.js b/jstests/sharding/query_sharded.js new file mode 100644 index 00000000000..a043f257d6c --- /dev/null +++ b/jstests/sharding/query_sharded.js @@ -0,0 +1,25 @@ +// +// Tests mongos-only query behavior +// + +var st = new ShardingTest({shards: 1, mongos: 1, verbose: 0}); + +var mongos = st.s0; +var coll = mongos.getCollection("foo.bar"); + +// +// +// Ensure we can't trick mongos by inserting exhaust option on a command through mongos +coll.remove({}); +assert.commandWorked(coll.insert({a: 'b'})); +var cmdColl = mongos.getCollection(coll.getDB().toString() + ".$cmd"); +var cmdQuery = cmdColl.find({ping: 1}).limit(1); +assert.commandWorked(cmdQuery.next()); +cmdQuery = cmdColl.find({ping: 1}).limit(1).addOption(DBQuery.Option.exhaust); +assert.throws(function() { + assert.commandWorked(cmdQuery.next()); +}); + +jsTest.log("DONE!"); + +st.stop(); diff --git a/src/mongo/client/dbclient_cursor.cpp b/src/mongo/client/dbclient_cursor.cpp index 96a509da122..db9cc55d6f3 100644 --- a/src/mongo/client/dbclient_cursor.cpp +++ b/src/mongo/client/dbclient_cursor.cpp @@ -103,72 +103,106 @@ Message DBClientCursor::_assembleInit() { } // If we haven't gotten a cursorId yet, we need to issue a new query or command. - // The caller supplies a 'query' object which may have $-prefixed directives in the format - // expected for a legacy OP_QUERY. Therefore, we use the legacy parsing code supplied by - // query_request_helper. When actually issuing the request to the remote node, we will - // assemble a find command. - auto findCommand = - query_request_helper::fromLegacyQuery(_nsOrUuid, - query, - fieldsToReturn ? *fieldsToReturn : BSONObj(), - nToSkip, - nextBatchSize(), - opts); - // If there was a problem building the query request, report that. - uassertStatusOK(findCommand.getStatus()); - - if (query.getBoolField("$readOnce")) { - // Legacy queries don't handle readOnce. - findCommand.getValue()->setReadOnce(true); - } - if (query.getBoolField(FindCommandRequest::kRequestResumeTokenFieldName)) { - // Legacy queries don't handle requestResumeToken. - findCommand.getValue()->setRequestResumeToken(true); - } - if (query.hasField(FindCommandRequest::kResumeAfterFieldName)) { - // Legacy queries don't handle resumeAfter. - findCommand.getValue()->setResumeAfter( - query.getObjectField(FindCommandRequest::kResumeAfterFieldName)); - } - if (auto replTerm = query[FindCommandRequest::kTermFieldName]) { - // Legacy queries don't handle term. - findCommand.getValue()->setTerm(replTerm.numberLong()); - } - // Legacy queries don't handle readConcern. - // We prioritize the readConcern parsed from the query object over '_readConcernObj'. - if (auto readConcern = query[repl::ReadConcernArgs::kReadConcernFieldName]) { - findCommand.getValue()->setReadConcern(readConcern.Obj()); - } else if (_readConcernObj) { - findCommand.getValue()->setReadConcern(_readConcernObj); - } - BSONObj cmd = findCommand.getValue()->toBSON(BSONObj()); - if (auto readPref = query["$readPreference"]) { - // FindCommandRequest doesn't handle $readPreference. - cmd = BSONObjBuilder(std::move(cmd)).append(readPref).obj(); + if (_isCommand) { + // HACK: + // Unfortunately, this code is used by the shell to run commands, + // so we need to allow the shell to send invalid options so that we can + // test that the server rejects them. Thus, to allow generating commands with + // invalid options, we validate them here, and fall back to generating an OP_QUERY + // through makeDeprecatedQueryMessage() if the options are invalid. + bool hasValidNToReturnForCommand = (nToReturn == 1 || nToReturn == -1); + bool hasValidFlagsForCommand = !(opts & mongo::QueryOption_Exhaust); + bool hasInvalidMaxTimeMs = query.hasField("$maxTimeMS"); + + if (hasValidNToReturnForCommand && hasValidFlagsForCommand && !hasInvalidMaxTimeMs) { + return assembleCommandRequest(_client, ns.db(), opts, query); + } + } else if (_useFindCommand) { + // The caller supplies a 'query' object which may have $-prefixed directives in the format + // expected for a legacy OP_QUERY. Therefore, we use the legacy parsing code supplied by + // query_request_helper. When actually issuing the request to the remote node, we will + // assemble a find command. + bool explain = false; + auto findCommand = + query_request_helper::fromLegacyQuery(_nsOrUuid, + query, + fieldsToReturn ? *fieldsToReturn : BSONObj(), + nToSkip, + nextBatchSize(), + opts, + &explain); + if (findCommand.isOK() && !explain) { + if (query.getBoolField("$readOnce")) { + // Legacy queries don't handle readOnce. + findCommand.getValue()->setReadOnce(true); + } + if (query.getBoolField(FindCommandRequest::kRequestResumeTokenFieldName)) { + // Legacy queries don't handle requestResumeToken. + findCommand.getValue()->setRequestResumeToken(true); + } + if (query.hasField(FindCommandRequest::kResumeAfterFieldName)) { + // Legacy queries don't handle resumeAfter. + findCommand.getValue()->setResumeAfter( + query.getObjectField(FindCommandRequest::kResumeAfterFieldName)); + } + if (auto replTerm = query[FindCommandRequest::kTermFieldName]) { + // Legacy queries don't handle term. + findCommand.getValue()->setTerm(replTerm.numberLong()); + } + // Legacy queries don't handle readConcern. + // We prioritize the readConcern parsed from the query object over '_readConcernObj'. + if (auto readConcern = query[repl::ReadConcernArgs::kReadConcernFieldName]) { + findCommand.getValue()->setReadConcern(readConcern.Obj()); + } else if (_readConcernObj) { + findCommand.getValue()->setReadConcern(_readConcernObj); + } + BSONObj cmd = findCommand.getValue()->toBSON(BSONObj()); + + if (auto readPref = query["$readPreference"]) { + // FindCommandRequest doesn't handle $readPreference. + cmd = BSONObjBuilder(std::move(cmd)).append(readPref).obj(); + } + return assembleCommandRequest(_client, ns.db(), opts, std::move(cmd)); + } + // else use legacy OP_QUERY request. + // Legacy OP_QUERY request does not support UUIDs. + if (_nsOrUuid.uuid()) { + // If there was a problem building the query request, report that. + uassertStatusOK(findCommand.getStatus()); + // Otherwise it must have been explain. + uasserted(50937, "Query by UUID is not supported for explain queries."); + } } - return assembleCommandRequest(_client, ns.db(), opts, std::move(cmd)); + _useFindCommand = false; // Make sure we handle the reply correctly. + return makeDeprecatedQueryMessage( + ns.ns(), query, nextBatchSize(), nToSkip, fieldsToReturn, opts); } Message DBClientCursor::_assembleGetMore() { invariant(cursorId); - std::int64_t batchSize = nextBatchSize(); - auto getMoreRequest = GetMoreCommandRequest(cursorId, ns.coll().toString()); - getMoreRequest.setBatchSize(boost::make_optional(batchSize != 0, batchSize)); - getMoreRequest.setMaxTimeMS(boost::make_optional( - tailableAwaitData(), - static_cast<std::int64_t>(durationCount<Milliseconds>(_awaitDataTimeout)))); - if (_term) { - getMoreRequest.setTerm(static_cast<std::int64_t>(*_term)); - } - getMoreRequest.setLastKnownCommittedOpTime(_lastKnownCommittedOpTime); - auto msg = assembleCommandRequest(_client, ns.db(), opts, getMoreRequest.toBSON({})); + if (_useFindCommand) { + std::int64_t batchSize = nextBatchSize(); + auto getMoreRequest = GetMoreCommandRequest(cursorId, ns.coll().toString()); + getMoreRequest.setBatchSize(boost::make_optional(batchSize != 0, batchSize)); + getMoreRequest.setMaxTimeMS(boost::make_optional( + tailableAwaitData(), + static_cast<std::int64_t>(durationCount<Milliseconds>(_awaitDataTimeout)))); + if (_term) { + getMoreRequest.setTerm(static_cast<std::int64_t>(*_term)); + } + getMoreRequest.setLastKnownCommittedOpTime(_lastKnownCommittedOpTime); + auto msg = assembleCommandRequest(_client, ns.db(), opts, getMoreRequest.toBSON({})); - // Set the exhaust flag if needed. - if (opts & QueryOption_Exhaust && msg.operation() == dbMsg) { - OpMsg::setFlag(&msg, OpMsg::kExhaustSupported); + // Set the exhaust flag if needed. + if (opts & QueryOption_Exhaust && msg.operation() == dbMsg) { + OpMsg::setFlag(&msg, OpMsg::kExhaustSupported); + } + return msg; + } else { + // Assemble a legacy getMore request. + return makeDeprecatedGetMoreMessage(ns.ns(), cursorId, nextBatchSize(), opts); } - return msg; } bool DBClientCursor::init() { @@ -305,22 +339,87 @@ void DBClientCursor::dataReceived(const Message& reply, bool& retry, string& hos batch.objs.clear(); batch.pos = 0; - const auto replyObj = commandDataReceived(reply); - cursorId = 0; // Don't try to kill cursor if we get back an error. - auto cr = uassertStatusOK(CursorResponse::parseFromBSON(replyObj)); - cursorId = cr.getCursorId(); - uassert(50935, - "Received a getMore response with a cursor id of 0 and the moreToCome flag set.", - !(_connectionHasPendingReplies && cursorId == 0)); - - ns = cr.getNSS(); // find command can change the ns to use for getMores. - // Store the resume token, if we got one. - _postBatchResumeToken = cr.getPostBatchResumeToken(); - batch.objs = cr.releaseBatch(); - - if (replyObj.hasField(LogicalTime::kOperationTimeFieldName)) { - _operationTime = LogicalTime::fromOperationTime(replyObj).asTimestamp(); + // If this is a reply to our initial command request. + if (_isCommand && cursorId == 0) { + batch.objs.push_back(commandDataReceived(reply)); + return; + } + + if (_useFindCommand) { + const auto replyObj = commandDataReceived(reply); + cursorId = 0; // Don't try to kill cursor if we get back an error. + auto cr = uassertStatusOK(CursorResponse::parseFromBSON(replyObj)); + cursorId = cr.getCursorId(); + uassert(50935, + "Received a getMore response with a cursor id of 0 and the moreToCome flag set.", + !(_connectionHasPendingReplies && cursorId == 0)); + + ns = cr.getNSS(); // Unlike OP_REPLY, find command can change the ns to use for getMores. + // Store the resume token, if we got one. + _postBatchResumeToken = cr.getPostBatchResumeToken(); + batch.objs = cr.releaseBatch(); + + if (replyObj.hasField(LogicalTime::kOperationTimeFieldName)) { + _operationTime = LogicalTime::fromOperationTime(replyObj).asTimestamp(); + } + return; + } + + QueryResult::View qr = reply.singleData().view2ptr(); + resultFlags = qr.getResultFlags(); + + if (resultFlags & ResultFlag_ErrSet) { + wasError = true; + } + + if (resultFlags & ResultFlag_CursorNotFound) { + // cursor id no longer valid at the server. + invariant(qr.getCursorId() == 0); + + // 0 indicates no longer valid (dead). + cursorId = 0; + + uasserted(ErrorCodes::CursorNotFound, + str::stream() << "cursor id " << cursorId << " didn't exist on server."); + } + + if (cursorId == 0 || !(opts & QueryOption_CursorTailable)) { + // only set initially: we don't want to kill it on end of data + // if it's a tailable cursor + cursorId = qr.getCursorId(); } + + if (opts & QueryOption_Exhaust) { + // With exhaust mode, each reply after the first claims to be a reply to the previous one + // rather than the initial request. + _connectionHasPendingReplies = (cursorId != 0); + _lastRequestId = reply.header().getId(); + } + + batch.objs.reserve(qr.getNReturned()); + + BufReader data(qr.data(), qr.dataLen()); + while (static_cast<int>(batch.objs.size()) < qr.getNReturned()) { + if (serverGlobalParams.objcheck) { + batch.objs.push_back(data.read<Validated<BSONObj>>()); + } else { + batch.objs.push_back(data.read<BSONObj>()); + } + batch.objs.back().shareOwnershipWith(reply.sharedBuffer()); + } + uassert(ErrorCodes::InvalidBSON, + "Got invalid reply from external server while reading from cursor", + data.atEof()); + + _client->checkResponse(batch.objs, false, &retry, &host); // watches for "not primary" + + tassert(5262101, + "Deprecated ShardConfigStale flag encountered in query result", + !(resultFlags & ResultFlag_ShardConfigStaleDeprecated)); + + /* this assert would fire the way we currently work: + verify( nReturned || cursorId == 0 ); + */ } /** If true, safe to call next(). Requests more from server if necessary. */ @@ -484,6 +583,7 @@ DBClientCursor::DBClientCursor(DBClientBase* client, _originalHost(_client->getServerAddress()), _nsOrUuid(nsOrUuid), ns(nsOrUuid.nss() ? *nsOrUuid.nss() : NamespaceString(nsOrUuid.dbname())), + _isCommand(ns.isCommand()), query(query), nToReturn(nToReturn), haveLimit(nToReturn > 0 && !(queryOptions & QueryOption_CursorTailable)), @@ -539,7 +639,14 @@ DBClientCursor::~DBClientCursor() { void DBClientCursor::kill() { DESTRUCTOR_GUARD({ if (cursorId && _ownCursor && !globalInShutdownDeprecated()) { - auto killCursor = [&](auto&& conn) { conn->killCursor(ns, cursorId); }; + auto killCursor = [&](auto&& conn) { + if (_useFindCommand) { + conn->killCursor(ns, cursorId); + } else { + auto toSend = makeDeprecatedKillCursorsMessage(cursorId); + conn->say(toSend); + } + }; // We only need to kill the cursor if there aren't pending replies. Pending replies // indicates that this is an exhaust cursor, so the connection must be closed and the diff --git a/src/mongo/client/dbclient_cursor.h b/src/mongo/client/dbclient_cursor.h index c3555a698e6..9fd0139254c 100644 --- a/src/mongo/client/dbclient_cursor.h +++ b/src/mongo/client/dbclient_cursor.h @@ -299,6 +299,7 @@ private: // After a successful 'find' command, 'ns' is updated to contain the namespace returned by that // command. NamespaceString ns; + const bool _isCommand; BSONObj query; int nToReturn; bool haveLimit; @@ -313,6 +314,7 @@ private: std::string _scopedHost; std::string _lazyHost; bool wasError; + bool _useFindCommand = true; bool _connectionHasPendingReplies = false; int _lastRequestId = 0; Milliseconds _awaitDataTimeout = Milliseconds{0}; diff --git a/src/mongo/client/query.cpp b/src/mongo/client/query.cpp index 9aa28e92711..6f8ae2e3615 100644 --- a/src/mongo/client/query.cpp +++ b/src/mongo/client/query.cpp @@ -146,6 +146,9 @@ BSONObj Query::getHint() const { return BSONObj(); return obj.getObjectField("$hint"); } +bool Query::isExplain() const { + return isComplex() && obj.getBoolField("$explain"); +} string Query::toString() const { return obj.toString(); diff --git a/src/mongo/client/query.h b/src/mongo/client/query.h index 6ef2b2531c0..f295873d98e 100644 --- a/src/mongo/client/query.h +++ b/src/mongo/client/query.h @@ -131,6 +131,7 @@ public: BSONObj getFilter() const; BSONObj getSort() const; BSONObj getHint() const; + bool isExplain() const; /** * @return true if the query object contains a read preference specification object. diff --git a/src/mongo/client/query_spec.h b/src/mongo/client/query_spec.h new file mode 100644 index 00000000000..534a841842f --- /dev/null +++ b/src/mongo/client/query_spec.h @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/bson/bsonobj.h" +#include "mongo/client/query.h" +#include "mongo/util/str.h" + +namespace mongo { +/** + * Represents a full query description, including all options required for the query to be passed on + * to other hosts + */ +class QuerySpec { + std::string _ns; + int _ntoskip; + int _ntoreturn; + int _options; + BSONObj _query; + BSONObj _fields; + Query _queryObj; + +public: + QuerySpec(const std::string& ns, + const BSONObj& query, + const BSONObj& fields, + int ntoskip, + int ntoreturn, + int options) + : _ns(ns), + _ntoskip(ntoskip), + _ntoreturn(ntoreturn), + _options(options), + _query(query.getOwned()), + _fields(fields.getOwned()), + _queryObj(_query) {} + + QuerySpec() {} + + bool isEmpty() const { + return _ns.size() == 0; + } + + bool isExplain() const { + return _queryObj.isExplain(); + } + BSONObj filter() const { + return _queryObj.getFilter(); + } + + BSONObj hint() const { + return _queryObj.getHint(); + } + BSONObj sort() const { + return _queryObj.getSort(); + } + BSONObj query() const { + return _query; + } + BSONObj fields() const { + return _fields; + } + BSONObj* fieldsData() { + return &_fields; + } + + // don't love this, but needed downstrem + const BSONObj* fieldsPtr() const { + return &_fields; + } + + std::string ns() const { + return _ns; + } + int ntoskip() const { + return _ntoskip; + } + int ntoreturn() const { + return _ntoreturn; + } + int options() const { + return _options; + } + + void setFields(BSONObj& o) { + _fields = o.getOwned(); + } + + std::string toString() const { + return str::stream() << "QSpec " + << BSON("ns" << _ns << "n2skip" << _ntoskip << "n2return" << _ntoreturn + << "options" << _options << "query" << _query << "fields" + << _fields); + } +}; + +} // namespace mongo diff --git a/src/mongo/db/query/query_request_helper.cpp b/src/mongo/db/query/query_request_helper.cpp index 5f5ce186be8..b466781d5a3 100644 --- a/src/mongo/db/query/query_request_helper.cpp +++ b/src/mongo/db/query/query_request_helper.cpp @@ -92,7 +92,7 @@ void addMetaProjection(FindCommandRequest* findCommand) { } } -Status initFullQuery(const BSONObj& top, FindCommandRequest* findCommand) { +Status initFullQuery(const BSONObj& top, FindCommandRequest* findCommand, bool* explain) { BSONObjIterator i(top); while (i.more()) { @@ -140,8 +140,8 @@ Status initFullQuery(const BSONObj& top, FindCommandRequest* findCommand) { } else if (name.startsWith("$")) { name = name.substr(1); // chop first char if (name == "explain") { - return Status(ErrorCodes::Error(5856600), - "the $explain OP_QUERY flag is no longer supported"); + // Won't throw. + *explain = e.trueValue(); } else if (name == "min") { if (!e.isABSONObj()) { return Status(ErrorCodes::BadValue, "$min must be a BSONObj"); @@ -191,7 +191,8 @@ Status initFindCommandRequest(int ntoskip, const BSONObj& queryObj, const BSONObj& proj, bool fromQueryMessage, - FindCommandRequest* findCommand) { + FindCommandRequest* findCommand, + bool* explain) { if (!proj.isEmpty()) { findCommand->setProjection(proj.getOwned()); } @@ -227,7 +228,7 @@ Status initFindCommandRequest(int ntoskip, } if (queryField.isABSONObj()) { findCommand->setFilter(queryField.embeddedObject().getOwned()); - Status status = initFullQuery(queryObj, findCommand); + Status status = initFullQuery(queryObj, findCommand, explain); if (!status.isOK()) { return status; } @@ -417,16 +418,36 @@ void validateCursorResponse(const BSONObj& outputAsBson) { // Old QueryRequest parsing code: SOON TO BE DEPRECATED. // +StatusWith<std::unique_ptr<FindCommandRequest>> fromLegacyQueryMessage(const QueryMessage& qm, + bool* explain) { + auto findCommand = std::make_unique<FindCommandRequest>(NamespaceString(qm.ns)); + + Status status = initFindCommandRequest(qm.ntoskip, + qm.ntoreturn, + qm.queryOptions, + qm.query, + qm.fields, + true, + findCommand.get(), + explain); + if (!status.isOK()) { + return status; + } + + return std::move(findCommand); +} + StatusWith<std::unique_ptr<FindCommandRequest>> fromLegacyQuery(NamespaceStringOrUUID nssOrUuid, const BSONObj& queryObj, const BSONObj& proj, int ntoskip, int ntoreturn, - int queryOptions) { + int queryOptions, + bool* explain) { auto findCommand = std::make_unique<FindCommandRequest>(std::move(nssOrUuid)); Status status = initFindCommandRequest( - ntoskip, ntoreturn, queryOptions, queryObj, proj, true, findCommand.get()); + ntoskip, ntoreturn, queryOptions, queryObj, proj, true, findCommand.get(), explain); if (!status.isOK()) { return status; } diff --git a/src/mongo/db/query/query_request_helper.h b/src/mongo/db/query/query_request_helper.h index 14409c62e39..2617c9231d4 100644 --- a/src/mongo/db/query/query_request_helper.h +++ b/src/mongo/db/query/query_request_helper.h @@ -148,6 +148,13 @@ void validateCursorResponse(const BSONObj& outputAsBson); // /** + * Parse the provided QueryMessage and return a heap constructed FindCommandRequest, which + * represents it or an error. + */ +StatusWith<std::unique_ptr<FindCommandRequest>> fromLegacyQueryMessage(const QueryMessage& qm, + bool* explain); + +/** * Parse the provided legacy query object and parameters to construct a FindCommandRequest. */ StatusWith<std::unique_ptr<FindCommandRequest>> fromLegacyQuery(NamespaceStringOrUUID nsOrUuid, @@ -155,7 +162,8 @@ StatusWith<std::unique_ptr<FindCommandRequest>> fromLegacyQuery(NamespaceStringO const BSONObj& proj, int ntoskip, int ntoreturn, - int queryOptions); + int queryOptions, + bool* explain); } // namespace query_request_helper } // namespace mongo diff --git a/src/mongo/db/query/query_request_test.cpp b/src/mongo/db/query/query_request_test.cpp index 2e92a48f4e9..8518c0e4661 100644 --- a/src/mongo/db/query/query_request_test.cpp +++ b/src/mongo/db/query/query_request_test.cpp @@ -1562,14 +1562,17 @@ TEST(QueryRequestTest, ParseFromLegacyQuery) { query: {query: 1}, orderby: {sort: 1}, $hint: {hint: 1}, + $explain: false, $min: {x: 'min'}, $max: {x: 'max'} })"); + bool explain = false; unique_ptr<FindCommandRequest> findCommand(assertGet(query_request_helper::fromLegacyQuery( - nss, queryObj, BSON("proj" << 1), kSkip, kNToReturn, QueryOption_Exhaust))); + nss, queryObj, BSON("proj" << 1), kSkip, kNToReturn, QueryOption_Exhaust, &explain))); ASSERT_EQ(*findCommand->getNamespaceOrUUID().nss(), nss); + ASSERT_EQ(explain, false); ASSERT_BSONOBJ_EQ(findCommand->getFilter(), fromjson("{query: 1}")); ASSERT_BSONOBJ_EQ(findCommand->getProjection(), fromjson("{proj: 1}")); ASSERT_BSONOBJ_EQ(findCommand->getSort(), fromjson("{sort: 1}")); @@ -1594,8 +1597,9 @@ TEST(QueryRequestTest, ParseFromLegacyQueryOplogReplayFlagAllowed) { // Test that parsing succeeds even if the oplog replay bit is set in the OP_QUERY message. This // flag may be set by old clients. auto options = QueryOption_OplogReplay_DEPRECATED; + bool explain = false; unique_ptr<FindCommandRequest> findCommand(assertGet(query_request_helper::fromLegacyQuery( - nss, queryObj, projectionObj, nToSkip, nToReturn, options))); + nss, queryObj, projectionObj, nToSkip, nToReturn, options, &explain))); // Verify that if we reserialize the find command, the 'oplogReplay' field // does not appear. @@ -1615,8 +1619,9 @@ TEST(QueryRequestTest, ParseFromLegacyQueryUnwrapped) { foo: 1 })"); const NamespaceString nss("test.testns"); + bool explain = false; unique_ptr<FindCommandRequest> findCommand(assertGet(query_request_helper::fromLegacyQuery( - nss, queryObj, BSONObj(), 0, 0, QueryOption_Exhaust))); + nss, queryObj, BSONObj(), 0, 0, QueryOption_Exhaust, &explain))); ASSERT_EQ(*findCommand->getNamespaceOrUUID().nss(), nss); ASSERT_BSONOBJ_EQ(findCommand->getFilter(), fromjson("{foo: 1}")); @@ -1642,24 +1647,15 @@ TEST(QueryRequestTest, ParseFromLegacyQueryTooNegativeNToReturn) { })"); const NamespaceString nss("test.testns"); - ASSERT_NOT_OK( - query_request_helper::fromLegacyQuery( - nss, queryObj, BSONObj(), 0, std::numeric_limits<int>::min(), QueryOption_Exhaust) - .getStatus()); -} - -TEST(QueryRequestTest, ParseFromLegacyQueryExplainError) { - BSONObj queryObj = fromjson(R"({ - query: {query: 1}, - $explain: false - })"); - - const NamespaceString nss("test.testns"); - ASSERT_EQUALS( - query_request_helper::fromLegacyQuery(nss, queryObj, BSONObj(), 0, -1, QueryOption_Exhaust) - .getStatus() - .code(), - static_cast<ErrorCodes::Error>(5856600)); + bool explain = false; + ASSERT_NOT_OK(query_request_helper::fromLegacyQuery(nss, + queryObj, + BSONObj(), + 0, + std::numeric_limits<int>::min(), + QueryOption_Exhaust, + &explain) + .getStatus()); } class QueryRequestTest : public ServiceContextTest {}; diff --git a/src/mongo/s/commands/cluster_explain_cmd.cpp b/src/mongo/s/commands/cluster_explain_cmd.cpp index 9ee65bdbc49..287ec9902d7 100644 --- a/src/mongo/s/commands/cluster_explain_cmd.cpp +++ b/src/mongo/s/commands/cluster_explain_cmd.cpp @@ -40,6 +40,15 @@ namespace { /** * Implements the explain command on mongos. + * + * "Old-style" explains (i.e. queries which have the $explain flag set), do not run + * through this path. Such explains will be supported for backwards compatibility, + * and must succeed in multiversion clusters. + * + * "New-style" explains use the explain command. When the explain command is routed + * through mongos, it is forwarded to all relevant shards. If *any* shard does not + * support a new-style explain, then the entire explain will fail (i.e. new-style + * explains cannot be used in multiversion clusters). */ class ClusterExplainCmd final : public Command { diff --git a/src/mongo/shell/explain_query.js b/src/mongo/shell/explain_query.js index 453751d88de..b4935fd07d4 100644 --- a/src/mongo/shell/explain_query.js +++ b/src/mongo/shell/explain_query.js @@ -9,6 +9,37 @@ var DBExplainQuery = (function() { // /** + * In 2.6 and before, .explain(), .explain(false), or .explain(<falsy value>) instructed the + * shell to reduce the explain verbosity by removing certain fields from the output. This + * is implemented here for backwards compatibility. + */ + function removeVerboseFields(obj) { + if (typeof (obj) !== "object") { + return; + } + + delete obj.allPlans; + delete obj.oldPlan; + delete obj.stats; + + if (typeof (obj.length) === "number") { + for (var i = 0; i < obj.length; i++) { + removeVerboseFields(obj[i]); + } + } + + if (obj.shards) { + for (var key in obj.shards) { + removeVerboseFields(obj.shards[key]); + } + } + + if (obj.clauses) { + removeVerboseFields(obj.clauses); + } + } + + /** * Many of the methods of an explain query just pass through to the underlying * non-explained DBQuery. Use this to generate a function which calls function 'name' on * 'destObj' and then returns this. @@ -20,6 +51,28 @@ var DBExplainQuery = (function() { }; } + /** + * Where possible, the explain query will be sent to the server as an explain command. + * However, if one of the nodes we are talking to (either a standalone or a shard in + * a sharded cluster) is of a version that doesn't have the explain command, we will + * use this function to fall back on the $explain query option. + */ + function explainWithLegacyQueryOption(explainQuery) { + // The wire protocol version indicates that the server does not have the explain + // command. Add $explain to the query and send it to the server. + var clone = explainQuery._query.clone(); + clone._addSpecial("$explain", true); + var result = clone.next(); + + // Remove some fields from the explain if verbosity is + // just "queryPlanner". + if ("queryPlanner" === explainQuery._verbosity) { + removeVerboseFields(result); + } + + return Explainable.throwOrReturn(result); + } + function constructor(query, verbosity) { // // Private vars. @@ -83,35 +136,47 @@ var DBExplainQuery = (function() { // Explain always gets pretty printed. this._query._prettyShell = true; - // Convert this explain query into an explain command, and send the command to - // the server. - var innerCmd; - if (this._isCount) { - // True means to always apply the skip and limit values. - innerCmd = this._query._convertToCountCmd(this._applySkipLimit); + if (this._mongo.hasExplainCommand()) { + // The wire protocol version indicates that the server has the explain command. + // Convert this explain query into an explain command, and send the command to + // the server. + var innerCmd; + if (this._isCount) { + // True means to always apply the skip and limit values. + innerCmd = this._query._convertToCountCmd(this._applySkipLimit); + } else { + var canAttachReadPref = false; + innerCmd = this._query._convertToCommand(canAttachReadPref); + } + + var explainCmd = {explain: innerCmd}; + explainCmd["verbosity"] = this._verbosity; + // If "maxTimeMS" is set on innerCmd, it needs to be propagated to the top-level + // of explainCmd so that it has the intended effect. + if (innerCmd.hasOwnProperty("maxTimeMS")) { + explainCmd.maxTimeMS = innerCmd.maxTimeMS; + } + + var explainDb = this._query._db; + + if ("$readPreference" in this._query._query) { + var prefObj = this._query._query.$readPreference; + explainCmd = explainDb._attachReadPreferenceToCommand(explainCmd, prefObj); + } + + var explainResult = + explainDb.runReadCommand(explainCmd, null, this._query._options); + + if (!explainResult.ok && explainResult.code === ErrorCodes.CommandNotFound) { + // One of the shards doesn't have the explain command available. Retry using + // the legacy $explain format, which should be supported by all shards. + return explainWithLegacyQueryOption(this); + } + + return Explainable.throwOrReturn(explainResult); } else { - var canAttachReadPref = false; - innerCmd = this._query._convertToCommand(canAttachReadPref); + return explainWithLegacyQueryOption(this); } - - var explainCmd = {explain: innerCmd}; - explainCmd["verbosity"] = this._verbosity; - // If "maxTimeMS" is set on innerCmd, it needs to be propagated to the top-level - // of explainCmd so that it has the intended effect. - if (innerCmd.hasOwnProperty("maxTimeMS")) { - explainCmd.maxTimeMS = innerCmd.maxTimeMS; - } - - var explainDb = this._query._db; - - if ("$readPreference" in this._query._query) { - var prefObj = this._query._query.$readPreference; - explainCmd = explainDb._attachReadPreferenceToCommand(explainCmd, prefObj); - } - - var explainResult = explainDb.runReadCommand(explainCmd, null, this._query._options); - - return Explainable.throwOrReturn(explainResult); }; this.next = function() { diff --git a/src/mongo/shell/mongo.js b/src/mongo/shell/mongo.js index 7146fd8670a..cf05337eead 100644 --- a/src/mongo/shell/mongo.js +++ b/src/mongo/shell/mongo.js @@ -418,6 +418,11 @@ connect = function(url, user, pass, apiParameters) { return db; }; +Mongo.prototype.hasExplainCommand = function() { + var hasExplain = (this.getMinWireVersion() <= 3 && 3 <= this.getMaxWireVersion()); + return hasExplain; +}; + // // Write Concern can be set at the connection level, and is used for all write operations unless // overridden at the collection level. diff --git a/src/mongo/transport/service_state_machine.cpp b/src/mongo/transport/service_state_machine.cpp index bd96331bd50..f1deac76b13 100644 --- a/src/mongo/transport/service_state_machine.cpp +++ b/src/mongo/transport/service_state_machine.cpp @@ -68,12 +68,61 @@ namespace { MONGO_FAIL_POINT_DEFINE(doNotSetMoreToCome); MONGO_FAIL_POINT_DEFINE(beforeCompressingExhaustResponse); /** + * Creates and returns a legacy exhaust message, if exhaust is allowed. The returned message is to + * be used as the subsequent 'synthetic' exhaust request. Returns an empty message if exhaust is not + * allowed. Any messages that do not have an opcode of OP_MSG are considered legacy. + */ +Message makeLegacyExhaustMessage(Message* m, const DbResponse& dbresponse) { + // OP_QUERY responses are always of type OP_REPLY. + invariant(dbresponse.response.operation() == opReply); + + if (!dbresponse.shouldRunAgainForExhaust) { + return Message(); + } + + // Legacy find operations via the OP_QUERY/OP_GET_MORE network protocol never provide the next + // invocation for exhaust. + invariant(!dbresponse.nextInvocation); + + DbMessage dbmsg(*m); + invariant(dbmsg.messageShouldHaveNs()); + const char* ns = dbmsg.getns(); + + MsgData::View header = dbresponse.response.header(); + QueryResult::View qr = header.view2ptr(); + long long cursorid = qr.getCursorId(); + + if (cursorid == 0) { + return Message(); + } + + // Generate a message that will act as the subsequent 'synthetic' exhaust request. + BufBuilder b(512); + b.appendNum(static_cast<int>(0)); // size set later in setLen() + b.appendNum(header.getId()); // message id + b.appendNum(header.getResponseToMsgId()); // in response to + b.appendNum(static_cast<int>(dbGetMore)); // opCode is OP_GET_MORE + b.appendNum(static_cast<int>(0)); // Must be ZERO (reserved) + b.appendStr(StringData(ns)); // Namespace + b.appendNum(static_cast<int>(0)); // ntoreturn + b.appendNum(cursorid); // cursor id from the OP_REPLY + + MsgData::View(b.buf()).setLen(b.len()); + + return Message(b.release()); +} + +/** * Given a request and its already generated response, checks for exhaust flags. If exhaust is * allowed, produces the subsequent request message, and modifies the response message to indicate * it is part of an exhaust stream. Returns the subsequent request message, which is known as a * 'synthetic' exhaust request. Returns an empty message if exhaust is not allowed. */ Message makeExhaustMessage(Message requestMsg, DbResponse* dbresponse) { + if (requestMsg.operation() == dbQuery) { + return makeLegacyExhaustMessage(&requestMsg, *dbresponse); + } + if (!OpMsgRequest::isFlagSet(requestMsg, OpMsg::kExhaustSupported)) { return Message(); } |