diff options
author | Yoonsoo Kim <yoonsoo.kim@mongodb.com> | 2021-07-20 11:07:58 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-07-20 11:47:23 +0000 |
commit | 14a176dd6a3359266ac0bb79725b7cb13b7c214d (patch) | |
tree | b43751dd9508f34f15c5fc54a15d9dfd2ddc4f03 /src | |
parent | d587a23122d5e64a828eb182bbe3f311541d89a2 (diff) | |
download | mongo-14a176dd6a3359266ac0bb79725b7cb13b7c214d.tar.gz |
Revert "SERVER-58566 Clean up remaining OP_QUERY code" due to a wrong author info
This reverts commit 3b293a78b0e24c5497464c3979daddfa0792bb55.
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/client/dbclient_cursor.cpp | 255 | ||||
-rw-r--r-- | src/mongo/client/dbclient_cursor.h | 2 | ||||
-rw-r--r-- | src/mongo/client/query.cpp | 3 | ||||
-rw-r--r-- | src/mongo/client/query.h | 1 | ||||
-rw-r--r-- | src/mongo/client/query_spec.h | 124 | ||||
-rw-r--r-- | src/mongo/db/query/query_request_helper.cpp | 35 | ||||
-rw-r--r-- | src/mongo/db/query/query_request_helper.h | 10 | ||||
-rw-r--r-- | src/mongo/db/query/query_request_test.cpp | 38 | ||||
-rw-r--r-- | src/mongo/s/commands/cluster_explain_cmd.cpp | 9 | ||||
-rw-r--r-- | src/mongo/shell/explain_query.js | 119 | ||||
-rw-r--r-- | src/mongo/shell/mongo.js | 5 | ||||
-rw-r--r-- | src/mongo/transport/service_state_machine.cpp | 49 |
12 files changed, 520 insertions, 130 deletions
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(); } |