diff options
author | Benety Goh <benety@mongodb.com> | 2014-01-09 15:05:55 -0500 |
---|---|---|
committer | Benety Goh <benety@mongodb.com> | 2014-01-15 16:35:44 -0500 |
commit | 9b94912fe8847977c4d23a12604cb900e6f426b7 (patch) | |
tree | 688de4d301133aa1a409529926487349584c7204 /src | |
parent | 26a8aaf0e660c247c2667e0f182bc4964096735b (diff) | |
download | mongo-9b94912fe8847977c4d23a12604cb900e6f426b7.tar.gz |
SERVER-8871 admin hints
Diffstat (limited to 'src')
27 files changed, 1691 insertions, 97 deletions
diff --git a/src/mongo/SConscript b/src/mongo/SConscript index 295859cd5a3..6b5d99e5fa6 100644 --- a/src/mongo/SConscript +++ b/src/mongo/SConscript @@ -632,6 +632,7 @@ serverOnlyFiles = [ "db/curop.cpp", "db/commands/distinct.cpp", "db/commands/find_and_modify.cpp", "db/commands/group.cpp", + "db/commands/hint_commands.cpp", "db/commands/index_stats.cpp", "db/commands/mr.cpp", "db/commands/oplog_note.cpp", @@ -694,6 +695,7 @@ mongosLibraryFiles = [ "s/commands_admin.cpp", "s/commands_public.cpp", "s/commands/auth_schema_upgrade_s.cpp", + "s/commands/cluster_hint_cmd.cpp", "s/commands/cluster_merge_chunks_cmd.cpp", "s/commands/cluster_plan_cache_cmd.cpp", "s/commands/cluster_write_cmd.cpp", @@ -797,6 +799,20 @@ env.Library("geoquery", [ "db/geo/geoquery.cpp", ], env.CppUnitTest("hash_test", [ "db/geo/hash_test.cpp" ], LIBDEPS = ["geometry" ]) env.CppUnitTest("geoparser_test", [ "db/geo/geoparser_test.cpp" ], LIBDEPS = ["geoparser"]) + +env.CppUnitTest( + target="hint_commands_test", + source=[ + "db/commands/hint_commands_test.cpp", + ], + LIBDEPS=[ + "$BUILD_DIR/mongo/serveronly", + "$BUILD_DIR/mongo/coreserver", + "$BUILD_DIR/mongo/coredb", + ], + NO_CRUTCH = True, +) + env.CppUnitTest( target="plan_cache_commands_test", source=[ diff --git a/src/mongo/db/auth/action_types.txt b/src/mongo/db/auth/action_types.txt index ea7cc057d12..e8ee88b25f3 100644 --- a/src/mongo/db/auth/action_types.txt +++ b/src/mongo/db/auth/action_types.txt @@ -71,6 +71,7 @@ "logRotate", "moveChunk", "netstat", +"planCacheHint", # view/update admin hints "planCacheRead", # view contents of plan cache "planCacheWrite", # clear cache, drop cache entry, pin/unpin/shun plans "reIndex", diff --git a/src/mongo/db/auth/role_graph_builtin_roles.cpp b/src/mongo/db/auth/role_graph_builtin_roles.cpp index de6c5f8d44f..9eec7fd9077 100644 --- a/src/mongo/db/auth/role_graph_builtin_roles.cpp +++ b/src/mongo/db/auth/role_graph_builtin_roles.cpp @@ -159,6 +159,7 @@ namespace { << ActionType::createIndex << ActionType::indexStats << ActionType::enableProfiler + << ActionType::planCacheHint << ActionType::planCacheRead << ActionType::planCacheWrite << ActionType::reIndex diff --git a/src/mongo/db/catalog/collection_info_cache.cpp b/src/mongo/db/catalog/collection_info_cache.cpp index 49eca9cee2c..7f84c034bfc 100644 --- a/src/mongo/db/catalog/collection_info_cache.cpp +++ b/src/mongo/db/catalog/collection_info_cache.cpp @@ -45,13 +45,16 @@ namespace mongo { CollectionInfoCache::CollectionInfoCache( Collection* collection ) : _collection( collection ), _keysComputed( false ), - _planCache(new PlanCache()) { } + _planCache(new PlanCache()), + _querySettings(new QuerySettings()) { } void CollectionInfoCache::reset() { Lock::assertWriteLocked( _collection->ns().ns() ); clearQueryCache(); _keysComputed = false; _planCache->clear(); + // query settings is not affected by info cache reset. + // admin hints should persist throughout life of collection } void CollectionInfoCache::computeIndexKeys() { @@ -89,4 +92,8 @@ namespace mongo { return _planCache.get(); } + QuerySettings* CollectionInfoCache::getQuerySettings() const { + return _querySettings.get(); + } + } diff --git a/src/mongo/db/catalog/collection_info_cache.h b/src/mongo/db/catalog/collection_info_cache.h index 55d20781fcd..9e695c1cbb9 100644 --- a/src/mongo/db/catalog/collection_info_cache.h +++ b/src/mongo/db/catalog/collection_info_cache.h @@ -33,6 +33,7 @@ #include <boost/scoped_ptr.hpp> #include "mongo/db/index_set.h" +#include "mongo/db/query/query_settings.h" #include "mongo/db/query/plan_cache.h" namespace mongo { @@ -62,6 +63,11 @@ namespace mongo { */ PlanCache* getPlanCache() const; + /** + * Get the QuerySettings for this collection. + */ + QuerySettings* getQuerySettings() const; + // ------------------- /* get set of index keys for this namespace. handy to quickly check if a given @@ -96,6 +102,10 @@ namespace mongo { // A cache for query plans. boost::scoped_ptr<PlanCache> _planCache; + // Query settings. + // Includes admin hints. + boost::scoped_ptr<QuerySettings> _querySettings; + void computeIndexKeys(); }; diff --git a/src/mongo/db/commands/hint_commands.cpp b/src/mongo/db/commands/hint_commands.cpp new file mode 100644 index 00000000000..3f0fdbcdb33 --- /dev/null +++ b/src/mongo/db/commands/hint_commands.cpp @@ -0,0 +1,407 @@ +/** +* Copyright (C) 2014 MongoDB Inc. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License, version 3, +* as published by the Free Software Foundation. +* +* 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 +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +* +* 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 GNU Affero General 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. +*/ + +#include <string> +#include <sstream> + +#include "mongo/base/init.h" +#include "mongo/base/status.h" +#include "mongo/db/client.h" +#include "mongo/db/catalog/database.h" +#include "mongo/db/jsobj.h" +#include "mongo/db/auth/authorization_session.h" +#include "mongo/db/commands/hint_commands.h" +#include "mongo/db/commands/plan_cache_commands.h" +#include "mongo/db/catalog/collection.h" + +namespace { + + using std::string; + using std::vector; + using namespace mongo; + + /** + * Releases memory for container of pointers. + * XXX: move elsewhere when something similar is available in util libraries. + */ + template <typename T> + class ContainerPointersDeleter { + public: + ContainerPointersDeleter(T* container) : _container(container) { + invariant(_container); + } + + ~ContainerPointersDeleter() { + for (typename T::const_iterator i = _container->begin(); + i != _container->end(); ++i) { + invariant(*i); + delete *i; + } + _container->clear(); + } + private: + MONGO_DISALLOW_COPYING(ContainerPointersDeleter); + T* _container; + }; + + /** + * Utility function to extract error code and message from status + * and append to BSON results. + */ + void addStatus(const Status& status, BSONObjBuilder& builder) { + builder.append("ok", status.isOK() ? 1.0 : 0.0); + if (!status.isOK()) { + builder.append("code", status.code()); + } + if (!status.reason().empty()) { + builder.append("errmsg", status.reason()); + } + } + + /** + * Retrieves a collection's query settings from the database. + */ + Status getQuerySettings(Database* db, const string& ns, QuerySettings** querySettingsOut) { + invariant(db); + + Collection* collection = db->getCollection(ns); + if (NULL == collection) { + return Status(ErrorCodes::BadValue, "no such collection"); + } + + CollectionInfoCache* infoCache = collection->infoCache(); + invariant(infoCache); + + QuerySettings* querySettings = infoCache->getQuerySettings(); + invariant(querySettings); + + *querySettingsOut = querySettings; + + return Status::OK(); + } + + /** + * Retrieves a collection's plan cache from the database. + */ + Status getPlanCache(Database* db, const string& ns, PlanCache** planCacheOut) { + invariant(db); + + Collection* collection = db->getCollection(ns); + if (NULL == collection) { + return Status(ErrorCodes::BadValue, "no such collection"); + } + + CollectionInfoCache* infoCache = collection->infoCache(); + invariant(infoCache); + + PlanCache* planCache = infoCache->getPlanCache(); + invariant(planCache); + + *planCacheOut = planCache; + return Status::OK(); + } + + // + // Command instances. + // Registers commands with the command system and make commands + // available to the client. + // + + MONGO_INITIALIZER_WITH_PREREQUISITES(SetupHintCommands, MONGO_NO_PREREQUISITES)( + InitializerContext* context) { + + new ListHints(); + new ClearHints(); + new SetHint(); + + return Status::OK(); + } + +} // namespace + +namespace mongo { + + using std::string; + using std::stringstream; + using std::vector; + using boost::scoped_ptr; + + HintCommand::HintCommand(const string& name, const string& helpText) + : Command(name), + helpText(helpText) { } + + bool HintCommand::run(const string& dbname, BSONObj& cmdObj, int options, + string& errmsg, BSONObjBuilder& result, bool fromRepl) { + string ns = parseNs(dbname, cmdObj); + + Status status = runHintCommand(ns, cmdObj, &result); + + if (!status.isOK()) { + addStatus(status, result); + return false; + } + + return true; + } + + Command::LockType HintCommand::locktype() const { + return NONE; + } + + bool HintCommand::slaveOk() const { + return false; + } + + void HintCommand::help(stringstream& ss) const { + ss << helpText; + } + + Status HintCommand::checkAuthForCommand(ClientBasic* client, const std::string& dbname, + const BSONObj& cmdObj) { + AuthorizationSession* authzSession = client->getAuthorizationSession(); + ResourcePattern pattern = parseResourcePattern(dbname, cmdObj); + + if (authzSession->isAuthorizedForActionsOnResource(pattern, ActionType::planCacheHint)) { + return Status::OK(); + } + + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } + + ListHints::ListHints() : HintCommand("planCacheListHints", + "Displays admin hints for all query shapes in a collection.") { } + + Status ListHints::runHintCommand(const string& ns, BSONObj& cmdObj, BSONObjBuilder* bob) { + // This is a read lock. The query settings is owned by the collection. + Client::ReadContext readCtx(ns); + Client::Context& ctx = readCtx.ctx(); + QuerySettings* querySettings; + Status status = getQuerySettings(ctx.db(), ns, &querySettings); + if (!status.isOK()) { + return status; + } + return list(*querySettings, bob); + } + + // static + Status ListHints::list(const QuerySettings& querySettings, BSONObjBuilder* bob) { + invariant(bob); + + // Format of BSON result: + // + // { + // hints: [ + // { + // query: <query>, + // sort: <sort>, + // projection: <projection>, + // indexes: [<index1>, <index2>, <index3>, ...] + // } + // } + BSONArrayBuilder hintsBuilder(bob->subarrayStart("hints")); + vector<AllowedIndexEntry*> entries = querySettings.getAllAllowedIndices(); + // Frees resources in entries on destruction. + ContainerPointersDeleter<vector<AllowedIndexEntry*> > deleter(&entries); + for (vector<AllowedIndexEntry*>::const_iterator i = entries.begin(); + i != entries.end(); ++i) { + AllowedIndexEntry* entry = *i; + invariant(entry); + + BSONObjBuilder hintBob(hintsBuilder.subobjStart()); + hintBob.append("query", entry->query); + hintBob.append("sort", entry->sort); + hintBob.append("projection", entry->projection); + BSONArrayBuilder indexesBuilder(hintBob.subarrayStart("indexes")); + for (vector<BSONObj>::const_iterator j = entry->indexKeyPatterns.begin(); + j != entry->indexKeyPatterns.end(); ++j) { + const BSONObj& index = *j; + indexesBuilder.append(index); + } + indexesBuilder.doneFast(); + } + hintsBuilder.doneFast(); + return Status::OK(); + } + + ClearHints::ClearHints() : HintCommand("planCacheClearHints", + "Clears all admin hints for a single query shape or, " + "if the query shape is omitted, for the entire collection.") { } + + Status ClearHints::runHintCommand(const string& ns, BSONObj& cmdObj, BSONObjBuilder* bob) { + // This is a read lock. The query settings is owned by the collection. + Client::ReadContext readCtx(ns); + Client::Context& ctx = readCtx.ctx(); + QuerySettings* querySettings; + Status status = getQuerySettings(ctx.db(), ns, &querySettings); + if (!status.isOK()) { + return status; + } + PlanCache* planCache; + status = getPlanCache(ctx.db(), ns, &planCache); + if (!status.isOK()) { + return status; + } + return clear(querySettings, planCache, ns, cmdObj); + } + + // static + Status ClearHints::clear(QuerySettings* querySettings, PlanCache* planCache, + const std::string& ns, const BSONObj& cmdObj) { + invariant(querySettings); + + // According to the specification, the planCacheClearHints command runs in two modes: + // - clear all hints; or + // - clear hints for single query shape when a query shape is described in the + // command arguments. + if (cmdObj.hasField("query")) { + CanonicalQuery* cqRaw; + Status status = PlanCacheCommand::canonicalize(ns, cmdObj, &cqRaw); + if (!status.isOK()) { + return status; + } + + scoped_ptr<CanonicalQuery> cq(cqRaw); + querySettings->removeAllowedIndices(*cq); + + // Remove entry from plan cache + planCache->remove(*cq); + return Status::OK(); + } + + // If query is not provided, make sure sort and projection are not in arguments. + // We do not want to clear the entire cache inadvertently when the user + // forgot to provide a value for "query". + if (cmdObj.hasField("sort") || cmdObj.hasField("projection")) { + return Status(ErrorCodes::BadValue, "sort or projection provided without query"); + } + + // Get entries from query settings. We need to remove corresponding entries from the plan + // cache shortly. + vector<AllowedIndexEntry*> entries = querySettings->getAllAllowedIndices(); + // Frees resources in entries on destruction. + ContainerPointersDeleter<vector<AllowedIndexEntry*> > deleter(&entries); + + // OK to proceed with clearing entire cache. + querySettings->clearAllowedIndices(); + + // Remove corresponding entries from plan cache. + // Admin hints affect the planning process directly. If there were + // plans generated as a result of applying admin hints, these need to be + // invalidated. This allows the planner to re-populate the plan cache with + // non-admin hinted solutions next time the query is run. + // Resolve plan cache key from (query, sort, projection) in query settings entry. + // Concurrency note: There's no harm in removing plan cache entries one at at time. + // Only way that PlanCache::remove() can fail is when the query shape has been removed from + // the cache by some other means (re-index, collection info reset, ...). This is OK since + // that's the intended effect of calling the remove() function with the key from the hint entry. + for (vector<AllowedIndexEntry*>::const_iterator i = entries.begin(); + i != entries.end(); ++i) { + AllowedIndexEntry* entry = *i; + invariant(entry); + + // Create canonical query. + CanonicalQuery* cqRaw; + Status result = CanonicalQuery::canonicalize(ns, entry->query, entry->sort, + entry->projection, &cqRaw); + invariant(result.isOK()); + scoped_ptr<CanonicalQuery> cq(cqRaw); + + // Remove plan cache entry. + planCache->remove(*cq); + } + + return Status::OK(); + } + + SetHint::SetHint() : HintCommand("planCacheSetHint", + "Sets admin hints for a query shape. Overrides existing hints.") { } + + Status SetHint::runHintCommand(const string& ns, BSONObj& cmdObj, BSONObjBuilder* bob) { + // This is a read lock. The query settings is owned by the collection. + Client::ReadContext readCtx(ns); + Client::Context& ctx = readCtx.ctx(); + QuerySettings* querySettings; + Status status = getQuerySettings(ctx.db(), ns, &querySettings); + if (!status.isOK()) { + return status; + } + PlanCache* planCache; + status = getPlanCache(ctx.db(), ns, &planCache); + if (!status.isOK()) { + return status; + } + return set(querySettings, planCache, ns, cmdObj); + } + + // static + Status SetHint::set(QuerySettings* querySettings, PlanCache* planCache, + const string& ns, const BSONObj& cmdObj) { + // indexes - required + BSONElement indexesElt = cmdObj.getField("indexes"); + if (indexesElt.eoo()) { + return Status(ErrorCodes::BadValue, "required field indexes missing"); + } + if (indexesElt.type() != mongo::Array) { + return Status(ErrorCodes::BadValue, "required field indexes must be an array"); + } + vector<BSONElement> indexesEltArray = indexesElt.Array(); + if (indexesEltArray.empty()) { + return Status(ErrorCodes::BadValue, + "required field indexes must contain at least one index"); + } + vector<BSONObj> indexes; + for (vector<BSONElement>::const_iterator i = indexesEltArray.begin(); + i != indexesEltArray.end(); ++i) { + const BSONElement& elt = *i; + if (!elt.isABSONObj()) { + return Status(ErrorCodes::BadValue, "each item in indexes must be an object"); + } + BSONObj obj = elt.Obj(); + if (obj.isEmpty()) { + return Status(ErrorCodes::BadValue, "index specification cannot be empty"); + } + indexes.push_back(obj.copy()); + } + + CanonicalQuery* cqRaw; + Status status = PlanCacheCommand::canonicalize(ns, cmdObj, &cqRaw); + if (!status.isOK()) { + return status; + } + scoped_ptr<CanonicalQuery> cq(cqRaw); + + // Add allowed indices to query settings, overriding any previous entries. + querySettings->setAllowedIndices(*cq, indexes); + + // Remove entry from plan cache. + planCache->remove(*cq); + + return Status::OK(); + } + +} // namespace mongo diff --git a/src/mongo/db/commands/hint_commands.h b/src/mongo/db/commands/hint_commands.h new file mode 100644 index 00000000000..c0729560551 --- /dev/null +++ b/src/mongo/db/commands/hint_commands.h @@ -0,0 +1,167 @@ +/** + * Copyright (C) 2014 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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/db/commands.h" +#include "mongo/db/query/query_settings.h" +#include "mongo/db/query/plan_cache.h" + +namespace mongo { + + /** + * DB commands for admin hints. + * Admin hint commands work on a different data structure in the collection + * info cache from the plan cache. + * The user still thinks of admin hint commands as part of the plan cache functionality + * so the command name prefix is still "planCache". + * + * These are in a header to facilitate unit testing. See hint_commands_test.cpp. + */ + + /** + * HintCommand + * Defines common attributes for all admin hint related commands + * such as slaveOk and locktype. + */ + class HintCommand : public Command { + public: + HintCommand(const std::string& name, const std::string& helpText); + + /** + * Entry point from command subsystem. + * Implementation provides standardization of error handling + * such as adding error code and message to BSON result. + * + * Do not override in derived classes. + * Override runPlanCacheCommands instead to + * implement plan cache command functionality. + */ + + bool run(const std::string& dbname, BSONObj& cmdObj, int options, + std::string& errmsg, BSONObjBuilder& result, bool fromRepl); + + /** + * It's fine to return NONE here because plan cache commands + * create explicit read context to access collection info cache. + * Refer to dbcommands.cpp on how locktype() is handled. + */ + virtual LockType locktype() const; + + virtual bool slaveOk() const; + + virtual void help(std::stringstream& ss) const; + + /** + * One action type defined for hint commands: + * - adminHintReadWrite + */ + virtual Status checkAuthForCommand(ClientBasic* client, const std::string& dbname, + const BSONObj& cmdObj); + + /** + * Subset of command arguments used by hint commands + * Override to provide command functionality. + * Should contain just enough logic to invoke run*Command() function + * in admin_hint.h + */ + virtual Status runHintCommand(const std::string& ns, BSONObj& cmdObj, + BSONObjBuilder* bob) = 0; + + private: + std::string helpText; + }; + + /** + * ListHints + * + * { planCacheListHints: <collection> } + * + */ + class ListHints : public HintCommand { + public: + ListHints(); + + virtual Status runHintCommand(const std::string& ns, BSONObj& cmdObj, BSONObjBuilder* bob); + + /** + * Looks up admin hints from collection's query settings. + * Inserts hints into BSON builder. + */ + static Status list(const QuerySettings& querySettings, BSONObjBuilder* bob); + }; + + /** + * ClearHints + * + * { planCacheClearHints: <collection>, query: <query>, sort: <sort>, projection: <projection> } + * + */ + class ClearHints : public HintCommand { + public: + ClearHints(); + + virtual Status runHintCommand(const std::string& ns, BSONObj& cmdObj, BSONObjBuilder* bob); + + /** + * If query shape is provided, clears hints for a query. + * Otherwise, clears collection's query settings. + * Namespace argument ns is ignored if we are clearing the entire cache. + * Removes corresponding entries from plan cache. + */ + static Status clear(QuerySettings* querySettings, PlanCache* planCache, const std::string& ns, + const BSONObj& cmdObj); + }; + + /** + * SetHint + * + * { + * planCacheSetHint: <collection>, + * query: <query>, + * sort: <sort>, + * projection: <projection>, + * indexes: [ <index1>, <index2>, <index3>, ... ] + * } + * + */ + class SetHint : public HintCommand { + public: + SetHint(); + + virtual Status runHintCommand(const std::string& ns, BSONObj& cmdObj, BSONObjBuilder* bob); + + /** + * Sets admin hints for a query shape. + * Removes entry for query shape from plan cache. + */ + static Status set(QuerySettings* querySettings, PlanCache* planCache, const std::string& ns, + const BSONObj& cmdObj); + }; + +} // namespace mongo diff --git a/src/mongo/db/commands/hint_commands_test.cpp b/src/mongo/db/commands/hint_commands_test.cpp new file mode 100644 index 00000000000..33d911c25dc --- /dev/null +++ b/src/mongo/db/commands/hint_commands_test.cpp @@ -0,0 +1,297 @@ +/** + * Copyright (C) 2014 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +/** + * This file contains tests for mongo/db/commands/hint_commands.h + */ + +#include "mongo/db/commands/hint_commands.h" + +#include "mongo/db/json.h" +#include "mongo/db/query/plan_ranker.h" +#include "mongo/db/query/query_solution.h" +#include "mongo/unittest/unittest.h" + +using namespace mongo; + +namespace { + + using std::string; + using std::vector; + + static const char* ns = "somebogusns"; + + /** + * Utility function to get list of hints from the query settings. + */ + vector<BSONObj> getHints(const QuerySettings& querySettings) { + BSONObjBuilder bob; + ASSERT_OK(ListHints::list(querySettings, &bob)); + BSONObj resultObj = bob.obj(); + BSONElement hintsElt = resultObj.getField("hints"); + ASSERT_EQUALS(hintsElt.type(), mongo::Array); + vector<BSONElement> hintsEltArray = hintsElt.Array(); + vector<BSONObj> hints; + for (vector<BSONElement>::const_iterator i = hintsEltArray.begin(); + i != hintsEltArray.end(); ++i) { + const BSONElement& elt = *i; + + ASSERT_TRUE(elt.isABSONObj()); + BSONObj obj = elt.Obj(); + + // Check required fields. + // query + BSONElement queryElt = obj.getField("query"); + ASSERT_TRUE(queryElt.isABSONObj()); + + // sort + BSONElement sortElt = obj.getField("sort"); + ASSERT_TRUE(sortElt.isABSONObj()); + + // projection + BSONElement projectionElt = obj.getField("projection"); + ASSERT_TRUE(projectionElt.isABSONObj()); + + // indexes + BSONElement indexesElt = obj.getField("indexes"); + ASSERT_EQUALS(indexesElt.type(), mongo::Array); + + // All fields OK. Append to vector. + hints.push_back(obj.copy()); + } + + return hints; + } + + /** + * Injects an entry into plan cache for query shape. + */ + void addQueryShapeToPlanCache(PlanCache* planCache, const char* queryStr, const char* sortStr, + const char* projectionStr) { + BSONObj queryObj = fromjson(queryStr); + BSONObj sortObj = fromjson(sortStr); + BSONObj projectionObj = fromjson(projectionStr); + + // Create canonical query. + CanonicalQuery* cqRaw; + ASSERT_OK(CanonicalQuery::canonicalize(ns, queryObj, sortObj, projectionObj, &cqRaw)); + scoped_ptr<CanonicalQuery> cq(cqRaw); + + QuerySolution qs; + qs.cacheData.reset(new SolutionCacheData()); + qs.cacheData->tree.reset(new PlanCacheIndexTree()); + std::vector<QuerySolution*> solns; + solns.push_back(&qs); + ASSERT_OK(planCache->add(*cq, solns, new PlanRankingDecision())); + } + + /** + * Checks if plan cache contains query shape. + */ + bool planCacheContains(const PlanCache& planCache, const char* queryStr, const char* sortStr, + const char* projectionStr) { + BSONObj queryObj = fromjson(queryStr); + BSONObj sortObj = fromjson(sortStr); + BSONObj projectionObj = fromjson(projectionStr); + + // Create canonical query. + CanonicalQuery* cqRaw; + ASSERT_OK(CanonicalQuery::canonicalize(ns, queryObj, sortObj, projectionObj, &cqRaw)); + scoped_ptr<CanonicalQuery> cq(cqRaw); + + // Retrieve cached solutions from plan cache. + vector<CachedSolution*> solutions = planCache.getAllSolutions(); + + // Search keys. + bool found = false; + for (vector<CachedSolution*>::const_iterator i = solutions.begin(); i != solutions.end(); i++) { + CachedSolution* cs = *i; + const PlanCacheKey& currentKey = cs->key; + if (currentKey == cq->getPlanCacheKey()) { + found = true; + } + // Release resources for cached solution after extracting key. + delete cs; + } + return found; + } + + /** + * Tests for ListHints + */ + + TEST(HintCommandsTest, ListHintsEmpty) { + QuerySettings empty; + vector<BSONObj> hints = getHints(empty); + ASSERT_TRUE(hints.empty()); + } + + /** + * Tests for ClearHints + */ + + TEST(HintCommandsTest, ClearHintsInvalidParameter) { + QuerySettings empty; + PlanCache planCache; + // If present, query has to be an object. + ASSERT_NOT_OK(ClearHints::clear(&empty, &planCache, ns, fromjson("{query: 1234}"))); + // If present, sort must be an object. + ASSERT_NOT_OK(ClearHints::clear(&empty, &planCache, ns, + fromjson("{query: {a: 1}, sort: 1234}"))); + // If present, projection must be an object. + ASSERT_NOT_OK(ClearHints::clear(&empty, &planCache, ns, + fromjson("{query: {a: 1}, projection: 1234}"))); + // Query must pass canonicalization. + ASSERT_NOT_OK(ClearHints::clear(&empty, &planCache, ns, + fromjson("{query: {a: {$no_such_op: 1}}}"))); + // Sort present without query is an error. + ASSERT_NOT_OK(ClearHints::clear(&empty, &planCache, ns, fromjson("{sort: {a: 1}}"))); + // Projection present without query is an error. + ASSERT_NOT_OK(ClearHints::clear(&empty, &planCache, ns, + fromjson("{projection: {_id: 0, a: 1}}"))); + } + + TEST(HintCommandsTest, ClearNonexistentHint) { + QuerySettings querySettings; + PlanCache planCache; + ASSERT_OK(SetHint::set(&querySettings, &planCache, ns, + fromjson("{query: {a: 1}, indexes: [{a: 1}]}"))); + vector<BSONObj> hints = getHints(querySettings); + ASSERT_EQUALS(hints.size(), 1U); + + // Clear nonexistent hint. + // Command should succeed and cache should remain unchanged. + ASSERT_OK(ClearHints::clear(&querySettings, &planCache, ns, fromjson("{query: {b: 1}}"))); + hints = getHints(querySettings); + ASSERT_EQUALS(hints.size(), 1U); + } + + /** + * Tests for SetHint + */ + + TEST(HintCommandsTest, SetHintInvalidParameter) { + QuerySettings empty; + PlanCache planCache; + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, fromjson("{}"))); + // Missing required query field. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, fromjson("{indexes: [{a: 1}]}"))); + // Missing required indexes field. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, fromjson("{query: {a: 1}}"))); + // Query has to be an object. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, + fromjson("{query: 1234, indexes: [{a: 1}, {b: 1}]}"))); + // Indexes field has to be an array. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, + fromjson("{query: {a: 1}, indexes: 1234}"))); + // Array indexes field cannot empty. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, + fromjson("{query: {a: 1}, indexes: []}"))); + // Elements in indexes have to be objects. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, + fromjson("{query: {a: 1}, indexes: [{a: 1}, 99]}"))); + // Objects in indexes cannot be empty. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, + fromjson("{query: {a: 1}, indexes: [{a: 1}, {}]}"))); + // If present, sort must be an object. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, + fromjson("{query: {a: 1}, sort: 1234, indexes: [{a: 1}, {b: 1}]}"))); + // If present, projection must be an object. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, + fromjson("{query: {a: 1}, projection: 1234, indexes: [{a: 1}, {b: 1}]}"))); + // Query must pass canonicalization. + ASSERT_NOT_OK(SetHint::set(&empty, &planCache, ns, + fromjson("{query: {a: {$no_such_op: 1}}, indexes: [{a: 1}, {b: 1}]}"))); + } + + TEST(HintCommandsTest, SetAndClearHints) { + QuerySettings querySettings; + PlanCache planCache; + + // Inject query shape into plan cache. + addQueryShapeToPlanCache(&planCache, "{a: 1, b: 1}", "{a: -1}", "{_id: 0, a: 1}"); + ASSERT_TRUE(planCacheContains(planCache, "{a: 1, b: 1}", "{a: -1}", "{_id: 0, a: 1}")); + + ASSERT_OK(SetHint::set(&querySettings, &planCache, ns, + fromjson("{query: {a: 1, b: 1}, sort: {a: -1}, projection: {_id: 0, a: 1}, " + "indexes: [{a: 1}]}"))); + vector<BSONObj> hints = getHints(querySettings); + ASSERT_EQUALS(hints.size(), 1U); + + // Query shape should not exist in plan cache after hint is updated. + ASSERT_FALSE(planCacheContains(planCache, "{a: 1, b: 1}", "{a: -1}", "{_id: 0, a: 1}")); + + // Value of entries in hints should match criteria in most recent query settings update. + ASSERT_EQUALS(hints[0].getObjectField("query"), fromjson("{a: 1, b: 1}")); + ASSERT_EQUALS(hints[0].getObjectField("sort"), fromjson("{a: -1}")); + ASSERT_EQUALS(hints[0].getObjectField("projection"), fromjson("{_id: 0, a: 1}")); + + // Replacing the hint for the same query shape ({a: 1, b: 1} and {b: 2, a: 3} + // share same shape) should not change the query settings size. + ASSERT_OK(SetHint::set(&querySettings, &planCache, ns, + fromjson("{query: {b: 2, a: 3}, sort: {a: -1}, projection: {_id: 0, a: 1}, " + "indexes: [{a: 1, b: 1}]}"))); + hints = getHints(querySettings); + ASSERT_EQUALS(hints.size(), 1U); + + // Add hint for different query shape. + ASSERT_OK(SetHint::set(&querySettings, &planCache, ns, + fromjson("{query: {b: 1}, indexes: [{b: 1}]}"))); + hints = getHints(querySettings); + ASSERT_EQUALS(hints.size(), 2U); + + // Add hint for 3rd query shape. This is to prepare for ClearHint tests. + ASSERT_OK(SetHint::set(&querySettings, &planCache, ns, + fromjson("{query: {a: 1}, indexes: [{a: 1}]}"))); + hints = getHints(querySettings); + ASSERT_EQUALS(hints.size(), 3U); + + // Add 2 entries to plan cache and check plan cache after clearing one/all hints. + addQueryShapeToPlanCache(&planCache, "{a: 1}", "{}", "{}"); + addQueryShapeToPlanCache(&planCache, "{b: 1}", "{}", "{}"); + + // Clear single hint. + ASSERT_OK(ClearHints::clear(&querySettings, &planCache, ns, + fromjson("{query: {a: 1}}"))); + hints = getHints(querySettings); + ASSERT_EQUALS(hints.size(), 2U); + + // Query shape should not exist in plan cache after cleaing 1 hint. + ASSERT_FALSE(planCacheContains(planCache, "{a: 1}", "{}", "{}")); + ASSERT_TRUE(planCacheContains(planCache, "{b: 1}", "{}", "{}")); + + // Clear all hints + ASSERT_OK(ClearHints::clear(&querySettings, &planCache, ns, fromjson("{}"))); + hints = getHints(querySettings); + ASSERT_TRUE(hints.empty()); + + // {b: 1} should be gone from plan cache after flushing query settings. + ASSERT_FALSE(planCacheContains(planCache, "{b: 1}", "{}", "{}")); + } + +} // namespace diff --git a/src/mongo/db/commands/plan_cache_commands.cpp b/src/mongo/db/commands/plan_cache_commands.cpp index 31d75254b53..2e6f6c9ea06 100644 --- a/src/mongo/db/commands/plan_cache_commands.cpp +++ b/src/mongo/db/commands/plan_cache_commands.cpp @@ -136,7 +136,7 @@ namespace mongo { } void PlanCacheCommand::help(stringstream& ss) const { - ss << helpText << endl; + ss << helpText; } Status PlanCacheCommand::checkAuthForCommand(ClientBasic* client, const std::string& dbname, @@ -152,7 +152,8 @@ namespace mongo { } // static - Status PlanCacheCommand::makeCacheKey(const string& ns, const BSONObj& cmdObj, PlanCacheKey* keyOut) { + Status PlanCacheCommand::canonicalize(const string& ns, const BSONObj& cmdObj, + CanonicalQuery** canonicalQueryOut) { // query - required BSONElement queryElt = cmdObj.getField("query"); if (queryElt.eoo()) { @@ -192,12 +193,8 @@ namespace mongo { if (!result.isOK()) { return result; } - scoped_ptr<CanonicalQuery> cq(cqRaw); - - // Generate key - PlanCacheKey key = cq->getPlanCacheKey(); - *keyOut = key; + *canonicalQueryOut = cqRaw; return Status::OK(); } @@ -288,13 +285,14 @@ namespace mongo { // static Status PlanCacheDrop::drop(PlanCache* planCache, const string& ns, const BSONObj& cmdObj) { - PlanCacheKey key; - Status status = makeCacheKey(ns, cmdObj, &key); + CanonicalQuery* cqRaw; + Status status = canonicalize(ns, cmdObj, &cqRaw); if (!status.isOK()) { return status; } - Status result = planCache->remove(key); + scoped_ptr<CanonicalQuery> cq(cqRaw); + Status result = planCache->remove(*cq); if (!result.isOK()) { return result; } @@ -320,14 +318,15 @@ namespace mongo { // static Status PlanCacheListPlans::list(const PlanCache& planCache, const std::string& ns, const BSONObj& cmdObj, BSONObjBuilder* bob) { - PlanCacheKey key; - Status status = makeCacheKey(ns, cmdObj, &key); + CanonicalQuery* cqRaw; + Status status = canonicalize(ns, cmdObj, &cqRaw); if (!status.isOK()) { return status; } + scoped_ptr<CanonicalQuery> cq(cqRaw); CachedSolution* crRaw; - Status result = planCache.get(key, &crRaw); + Status result = planCache.get(*cq, &crRaw); if (!result.isOK()) { return result; } @@ -347,10 +346,12 @@ namespace mongo { detailsBob.append("solution", scd->toString()); detailsBob.doneFast(); - // XXX: Fill in rest of fields with bogus data. // XXX: Fix these field values once we have fleshed out cache entries. + // reason should contain initial plan stats and score from ranking process. + // feedback should contain execution stats from running the query to completion. planBob.append("reason", BSONObj()); planBob.append("feedback", BSONObj()); + planBob.append("hint", scd->adminHintApplied); } plansBuilder.doneFast(); diff --git a/src/mongo/db/commands/plan_cache_commands.h b/src/mongo/db/commands/plan_cache_commands.h index a72a19de4bc..8cf4d38f9ca 100644 --- a/src/mongo/db/commands/plan_cache_commands.h +++ b/src/mongo/db/commands/plan_cache_commands.h @@ -89,10 +89,10 @@ namespace mongo { BSONObjBuilder* bob) = 0; /** - * Parses query shape from command object and returns plan cache key. + * Validatess query shape from command object and returns canonical query. */ - static Status makeCacheKey(const std::string& ns, const BSONObj& cmdObj, - PlanCacheKey* keyOut); + static Status canonicalize(const std::string& ns, const BSONObj& cmdObj, + CanonicalQuery** canonicalQueryOut); private: std::string helpText; diff --git a/src/mongo/db/commands/plan_cache_commands_test.cpp b/src/mongo/db/commands/plan_cache_commands_test.cpp index 16c94a55665..0b9c591e00e 100644 --- a/src/mongo/db/commands/plan_cache_commands_test.cpp +++ b/src/mongo/db/commands/plan_cache_commands_test.cpp @@ -135,9 +135,6 @@ namespace { ASSERT_OK(CanonicalQuery::canonicalize(ns, fromjson("{a: 1}"), &cqRaw)); auto_ptr<CanonicalQuery> cq(cqRaw); - // Retrieve the cache key - PlanCacheKey queryKey = cq->getPlanCacheKey(); - // Plan cache with one entry PlanCache planCache; QuerySolution qs; @@ -157,55 +154,41 @@ namespace { * Mostly validation on the input parameters */ - /** - * Utility function to generate a cache key from command object string. - */ - PlanCacheKey generateKey(const char* cmdStr) { - BSONObj cmdObj = fromjson(cmdStr); - PlanCacheKey key; - Status status = PlanCacheCommand::makeCacheKey(ns, cmdObj, &key); - if (!status.isOK()) { - mongoutils::str::stream ss; - ss << "failed to generate cache key. cmdObj: " << cmdStr; - FAIL(ss); - } - if (key.empty()) { - mongoutils::str::stream ss; - ss << "zero-length cache key generated. cmdObj: " << cmdStr; - FAIL(ss); - } - return key; - } - - TEST(PlanCacheCommandsTest, planCacheGenerateKey) { + TEST(PlanCacheCommandsTest, Canonicalize) { // Invalid parameters - PlanCacheKey ignored; + CanonicalQuery* cqRaw; // Missing query field - ASSERT_NOT_OK(PlanCacheCommand::makeCacheKey(ns, fromjson("{}"), &ignored)); + ASSERT_NOT_OK(PlanCacheCommand::canonicalize(ns, fromjson("{}"), &cqRaw)); // Query needs to be an object - ASSERT_NOT_OK(PlanCacheCommand::makeCacheKey(ns, fromjson("{query: 1}"), &ignored)); + ASSERT_NOT_OK(PlanCacheCommand::canonicalize(ns, fromjson("{query: 1}"), &cqRaw)); // Sort needs to be an object - ASSERT_NOT_OK(PlanCacheCommand::makeCacheKey(ns, fromjson("{query: {}, sort: 1}"), - &ignored)); + ASSERT_NOT_OK(PlanCacheCommand::canonicalize(ns, fromjson("{query: {}, sort: 1}"), + &cqRaw)); // Bad query (invalid sort order) - ASSERT_NOT_OK(PlanCacheCommand::makeCacheKey(ns, fromjson("{query: {}, sort: {a: 0}}"), - &ignored)); + ASSERT_NOT_OK(PlanCacheCommand::canonicalize(ns, fromjson("{query: {}, sort: {a: 0}}"), + &cqRaw)); // Valid parameters - PlanCacheKey queryKey = generateKey("{query: {a: 1, b: 1}}"); + ASSERT_OK(PlanCacheCommand::canonicalize(ns, fromjson("{query: {a: 1, b: 1}}"), &cqRaw)); + scoped_ptr<CanonicalQuery> query(cqRaw); + // Equivalent query should generate same key. - PlanCacheKey equivQueryKey = generateKey("{query: {b: 1, a: 1}}"); - ASSERT_EQUALS(queryKey, equivQueryKey); + ASSERT_OK(PlanCacheCommand::canonicalize(ns, fromjson("{query: {b: 1, a: 1}}"), &cqRaw)); + scoped_ptr<CanonicalQuery> equivQuery(cqRaw); + ASSERT_EQUALS(query->getPlanCacheKey(), equivQuery->getPlanCacheKey()); // Sort query should generate different key from unsorted query. - PlanCacheKey sortQueryKey = generateKey("{query: {a: 1, b: 1}, sort: {a: 1}}"); - ASSERT_NOT_EQUALS(queryKey, sortQueryKey); + ASSERT_OK(PlanCacheCommand::canonicalize(ns, + fromjson("{query: {a: 1, b: 1}, sort: {a: 1}}"), &cqRaw)); + scoped_ptr<CanonicalQuery> sortQuery(cqRaw); + ASSERT_NOT_EQUALS(query->getPlanCacheKey(), sortQuery->getPlanCacheKey()); // Projected query should generate different key from unprojected query. - PlanCacheKey projectionQueryKey = - generateKey("{query: {a: 1, b: 1}, projection: {_id: 0, a: 1}}"); - ASSERT_NOT_EQUALS(queryKey, projectionQueryKey); + ASSERT_OK(PlanCacheCommand::canonicalize(ns, + fromjson("{query: {a: 1, b: 1}, projection: {_id: 0, a: 1}}"), &cqRaw)); + scoped_ptr<CanonicalQuery> projectionQuery(cqRaw); + ASSERT_NOT_EQUALS(query->getPlanCacheKey(), projectionQuery->getPlanCacheKey()); } /** @@ -234,10 +217,6 @@ namespace { ASSERT_OK(CanonicalQuery::canonicalize(ns, fromjson("{b: 1}"), &cqRaw)); auto_ptr<CanonicalQuery> cqB(cqRaw); - // Generate 2 cache keys. - PlanCacheKey keyA = cqA->getPlanCacheKey(); - PlanCacheKey keyB = cqB->getPlanCacheKey(); - // Create plan cache with 2 entries. PlanCache planCache; QuerySolution qs; @@ -347,9 +326,6 @@ namespace { ASSERT_OK(CanonicalQuery::canonicalize(ns, fromjson("{a: 1}"), &cqRaw)); auto_ptr<CanonicalQuery> cq(cqRaw); - // Retrieve the cache key - PlanCacheKey queryKey = cq->getPlanCacheKey(); - // Plan cache with one entry PlanCache planCache; QuerySolution qs; @@ -369,9 +345,6 @@ namespace { ASSERT_OK(CanonicalQuery::canonicalize(ns, fromjson("{a: 1}"), &cqRaw)); auto_ptr<CanonicalQuery> cq(cqRaw); - // Retrieve the cache key - PlanCacheKey queryKey = cq->getPlanCacheKey(); - // Plan cache with one entry PlanCache planCache; QuerySolution qs; diff --git a/src/mongo/db/query/SConscript b/src/mongo/db/query/SConscript index f010afcdf15..1c70fb4676b 100644 --- a/src/mongo/db/query/SConscript +++ b/src/mongo/db/query/SConscript @@ -6,6 +6,7 @@ env.Library( target='query_planner', source=[ "canonical_query.cpp", + "query_settings.cpp", "index_tag.cpp", "parsed_projection.cpp", "plan_cache.cpp", @@ -47,6 +48,21 @@ env.Library( ], ) +env.CppUnitTest( + target="get_runner_test", + source=[ + "get_runner_test.cpp" + ], + LIBDEPS=[ + "query", + "$BUILD_DIR/mongo/serveronly", + "$BUILD_DIR/mongo/coreserver", + "$BUILD_DIR/mongo/coredb", + "$BUILD_DIR/mongo/mocklib", + ], + NO_CRUTCH = True, +) + env.Library( target="index_bounds", source=[ diff --git a/src/mongo/db/query/canonical_query.cpp b/src/mongo/db/query/canonical_query.cpp index 4e3f7233503..0a06f47915a 100644 --- a/src/mongo/db/query/canonical_query.cpp +++ b/src/mongo/db/query/canonical_query.cpp @@ -261,7 +261,7 @@ namespace mongo { return Status::OK(); } - PlanCacheKey CanonicalQuery::getPlanCacheKey() const { + const PlanCacheKey& CanonicalQuery::getPlanCacheKey() const { return _cacheKey; } diff --git a/src/mongo/db/query/canonical_query.h b/src/mongo/db/query/canonical_query.h index 4bd9400339f..292978b6582 100644 --- a/src/mongo/db/query/canonical_query.h +++ b/src/mongo/db/query/canonical_query.h @@ -87,7 +87,7 @@ namespace mongo { /** * Get the cache key for this canonical query. */ - PlanCacheKey getPlanCacheKey() const; + const PlanCacheKey& getPlanCacheKey() const; // Debugging string toString() const; diff --git a/src/mongo/db/query/canonical_query_test.cpp b/src/mongo/db/query/canonical_query_test.cpp index 359a46ff35a..79146bb12b1 100644 --- a/src/mongo/db/query/canonical_query_test.cpp +++ b/src/mongo/db/query/canonical_query_test.cpp @@ -337,7 +337,7 @@ namespace { const char* projStr, const char *expectedStr) { auto_ptr<CanonicalQuery> cq(canonicalize(queryStr, sortStr, projStr)); - PlanCacheKey key = cq->getPlanCacheKey(); + const PlanCacheKey& key = cq->getPlanCacheKey(); PlanCacheKey expectedKey(expectedStr); if (key == expectedKey) { return; diff --git a/src/mongo/db/query/get_runner.cpp b/src/mongo/db/query/get_runner.cpp index fe70d5eac22..fd03213cbf0 100644 --- a/src/mongo/db/query/get_runner.cpp +++ b/src/mongo/db/query/get_runner.cpp @@ -32,6 +32,7 @@ #include "mongo/db/query/cached_plan_runner.h" #include "mongo/db/query/canonical_query.h" #include "mongo/db/query/eof_runner.h" +#include "mongo/db/query/query_settings.h" #include "mongo/db/query/idhack_runner.h" #include "mongo/db/query/internal_plans.h" #include "mongo/db/query/multi_plan_runner.h" @@ -91,6 +92,33 @@ namespace mongo { && !query.getParsed().hasOption(QueryOption_CursorTailable); } + // static + void filterAllowedIndexEntries(const AllowedIndices& allowedIndices, + std::vector<IndexEntry>* indexEntries) { + invariant(indexEntries); + + // Filter index entries + // Check BSON objects in AllowedIndices::_indexKeyPatterns against IndexEntry::keyPattern. + // Removes IndexEntrys that do not match _indexKeyPatterns. + std::vector<IndexEntry> temp; + for (std::vector<IndexEntry>::const_iterator i = indexEntries->begin(); + i != indexEntries->end(); ++i) { + const IndexEntry& indexEntry = *i; + for (std::vector<BSONObj>::const_iterator j = allowedIndices.indexKeyPatterns.begin(); + j != allowedIndices.indexKeyPatterns.end(); ++j) { + const BSONObj& index = *j; + // Copy index entry to temp vector if found in query settings. + if (0 == indexEntry.keyPattern.woCompare(index)) { + temp.push_back(indexEntry); + break; + } + } + } + + // Update results. + temp.swap(*indexEntries); + } + /** * For a given query, get a runner. The runner could be a SingleSolutionRunner, a * CachedQueryRunner, or a MultiPlanRunner, depending on the cache/query solver/etc. @@ -144,6 +172,18 @@ namespace mongo { } } + // If query supports admin hint, filter params.indices by indexes in query settings. + QuerySettings* querySettings = collection->infoCache()->getQuerySettings(); + AllowedIndices* allowedIndicesRaw; + + // Filter index catalog if admin hint is specified for query. + // Also, signal to planner that application hint should be ignored. + if (querySettings->getAllowedIndices(*canonicalQuery, &allowedIndicesRaw)) { + boost::scoped_ptr<AllowedIndices> allowedIndices(allowedIndicesRaw); + filterAllowedIndexEntries(*allowedIndices, &plannerParams.indices); + plannerParams.adminHintApplied = true; + } + // Tailable: If the query requests tailable the collection must be capped. if (canonicalQuery->getParsed().hasOption(QueryOption_CursorTailable)) { if (!collection->isCapped()) { @@ -210,7 +250,8 @@ namespace mongo { WorkingSet* ws; PlanStage* root; verify(StageBuilder::build(*qs, &root, &ws)); - CachedPlanRunner* cpr = new CachedPlanRunner(canonicalQuery.release(), qs, root, ws); + CachedPlanRunner* cpr = new CachedPlanRunner(canonicalQuery.release(), qs, + root, ws); if (NULL != backupQs) { WorkingSet* backupWs; @@ -263,6 +304,9 @@ namespace mongo { for (size_t i = 0; i < solutions.size(); ++i) { WorkingSet* ws; PlanStage* root; + if (solutions[i]->cacheData.get()) { + solutions[i]->cacheData->adminHintApplied = plannerParams.adminHintApplied; + } verify(StageBuilder::build(*solutions[i], &root, &ws)); // Takes ownership of all arguments. mpr->addPlan(solutions[i], root, ws); diff --git a/src/mongo/db/query/get_runner.h b/src/mongo/db/query/get_runner.h index 87ca08b2c0e..acb25cb08bd 100644 --- a/src/mongo/db/query/get_runner.h +++ b/src/mongo/db/query/get_runner.h @@ -27,6 +27,7 @@ */ #include "mongo/db/query/canonical_query.h" +#include "mongo/db/query/query_settings.h" #include "mongo/db/query/runner.h" namespace mongo { @@ -34,6 +35,16 @@ namespace mongo { class Collection; /** + * Filter indexes retrieved from index catalog by + * allowed indices in query settings. + * Used by getRunner(). + * This function is public to facilitate testing. + */ + void filterAllowedIndexEntries(const AllowedIndices& allowedIndices, + std::vector<IndexEntry>* indexEntries); + + + /** * Get a runner for a query. Takes ownership of rawCanonicalQuery. * * If the query is valid and a runner could be created, returns Status::OK() diff --git a/src/mongo/db/query/get_runner_test.cpp b/src/mongo/db/query/get_runner_test.cpp new file mode 100644 index 00000000000..19cebfd7252 --- /dev/null +++ b/src/mongo/db/query/get_runner_test.cpp @@ -0,0 +1,144 @@ +/** + * Copyright (C) 2014 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +/** + * This file contains tests for mongo/db/query/get_runner.h + */ + +#include "mongo/db/query/get_runner.h" + +#include "mongo/db/json.h" +#include "mongo/db/query/query_settings.h" +#include "mongo/unittest/unittest.h" +#include "mongo/util/mongoutils/str.h" + +using namespace mongo; + +namespace { + + using std::auto_ptr; + + static const char* ns = "somebogusns"; + + /** + * Utility functions to create a CanonicalQuery + */ + CanonicalQuery* canonicalize(const char* queryStr, const char* sortStr, + const char* projStr) { + BSONObj queryObj = fromjson(queryStr); + BSONObj sortObj = fromjson(sortStr); + BSONObj projObj = fromjson(projStr); + CanonicalQuery* cq; + Status result = CanonicalQuery::canonicalize(ns, queryObj, sortObj, + projObj, + &cq); + ASSERT_OK(result); + return cq; + } + + // + // get_runner tests + // + + // + // filterAllowedIndexEntries + // + + /** + * Test function to check filterAllowedIndexEntries + */ + void testAllowedIndices(const char* hintKeyPatterns[], + const char* indexCatalogKeyPatterns[], + const char* expectedFilteredKeyPatterns[]) { + QuerySettings querySettings; + AllowedIndices *allowedIndicesRaw; + + // getAllowedIndices should return false when query shape is not yet in query settings. + auto_ptr<CanonicalQuery> cq(canonicalize("{a: 1}", "{}", "{}")); + ASSERT_FALSE(querySettings.getAllowedIndices(*cq, &allowedIndicesRaw)); + + // Add entry to query settings. + const PlanCacheKey& key = cq->getPlanCacheKey(); + std::vector<BSONObj> indexKeyPatterns; + for (int i=0; hintKeyPatterns[i] != NULL; ++i) { + indexKeyPatterns.push_back(fromjson(hintKeyPatterns[i])); + } + querySettings.setAllowedIndices(*cq, indexKeyPatterns); + + // Index entry vector should contain 1 entry after filtering. + ASSERT_TRUE(querySettings.getAllowedIndices(*cq, &allowedIndicesRaw)); + ASSERT_FALSE(key.empty()); + ASSERT(NULL != allowedIndicesRaw); + auto_ptr<AllowedIndices> allowedIndices(allowedIndicesRaw); + + // Indexes from index catalog. + std::vector<IndexEntry> indexEntries; + for (int i=0; indexCatalogKeyPatterns[i] != NULL; ++i) { + indexEntries.push_back(IndexEntry(fromjson(indexCatalogKeyPatterns[i]))); + } + + // Apply filter in allowed indices. + filterAllowedIndexEntries(*allowedIndices, &indexEntries); + size_t numExpected = 0; + while (expectedFilteredKeyPatterns[numExpected] != NULL) { + ASSERT_LESS_THAN(numExpected, indexEntries.size()); + ASSERT_EQUALS(indexEntries[numExpected].keyPattern, + fromjson(expectedFilteredKeyPatterns[numExpected])); + numExpected++; + } + ASSERT_EQUALS(indexEntries.size(), numExpected); + } + + // Use of admin hint to select compound index over single key index. + TEST(GetRunnerTest, GetAllowedIndices) { + const char* hintKeyPatterns[] = {"{a: 1, b: 1}", NULL}; + const char* indexCatalogKeyPatterns[] = {"{a: 1}", "{a: 1, b: 1}", "{a: 1, c: 1}", NULL}; + const char* expectedFilteredKeyPatterns[] = {"{a: 1, b: 1}", NULL}; + testAllowedIndices(hintKeyPatterns, indexCatalogKeyPatterns, expectedFilteredKeyPatterns); + } + + // Settings admin hints which refer to non-existent indexes + // will effectively disregard the index catalog and + // result in the planner generating a collection scan. + TEST(GetRunnerTest, GetAllowedIndicesNonExistentIndexKeyPatterns) { + const char* hintKeyPatterns[] = {"{nosuchfield: 1}", NULL}; + const char* indexCatalogKeyPatterns[] = {"{a: 1}", "{a: 1, b: 1}", "{a: 1, c: 1}", NULL}; + const char* expectedFilteredKeyPatterns[] = {NULL}; + testAllowedIndices(hintKeyPatterns, indexCatalogKeyPatterns, expectedFilteredKeyPatterns); + } + + // This test case shows how to force query execution to use + // an index that orders items in descending order. + TEST(GetRunnerTest, GetAllowedIndicesDescendingOrder) { + const char* hintKeyPatterns[] = {"{a: -1}", NULL}; + const char* indexCatalogKeyPatterns[] = {"{a: 1}", "{a: -1}", NULL}; + const char* expectedFilteredKeyPatterns[] = {"{a: -1}", NULL}; + testAllowedIndices(hintKeyPatterns, indexCatalogKeyPatterns, expectedFilteredKeyPatterns); + } + +} // namespace diff --git a/src/mongo/db/query/multi_plan_runner.cpp b/src/mongo/db/query/multi_plan_runner.cpp index 28f6dc326a4..d2905d8f647 100644 --- a/src/mongo/db/query/multi_plan_runner.cpp +++ b/src/mongo/db/query/multi_plan_runner.cpp @@ -276,7 +276,7 @@ namespace mongo { Collection* collection = db->getCollection(_query->ns()); verify(NULL != collection); PlanCache* cache = collection->infoCache()->getPlanCache(); - cache->remove(_query->getPlanCacheKey()); + cache->remove(*_query); _bestPlan.reset(_backupPlan); _backupPlan = NULL; diff --git a/src/mongo/db/query/plan_cache.cpp b/src/mongo/db/query/plan_cache.cpp index f66c69274e7..e994447f05e 100644 --- a/src/mongo/db/query/plan_cache.cpp +++ b/src/mongo/db/query/plan_cache.cpp @@ -34,6 +34,7 @@ #include "mongo/db/query/plan_ranker.h" #include "mongo/db/query/query_solution.h" #include "mongo/db/query/qlog.h" +#include "mongo/util/mongoutils/str.h" namespace mongo { @@ -203,6 +204,7 @@ namespace mongo { } other->solnType = this->solnType; other->wholeIXSolnDir = this->wholeIXSolnDir; + other->adminHintApplied = this->adminHintApplied; return other; } @@ -244,7 +246,6 @@ namespace mongo { return Status(ErrorCodes::BadValue, "no solutions provided"); } - PlanCacheKey key = query.getPlanCacheKey(); PlanCacheEntry* entry = new PlanCacheEntry(solns, why); const LiteParsedQuery& pq = query.getParsed(); entry->query = pq.getFilter().copy(); @@ -265,6 +266,7 @@ namespace mongo { boost::lock_guard<boost::mutex> cacheLock(_cacheMutex); // Replace existing entry. typedef unordered_map<PlanCacheKey, PlanCacheEntry*>::const_iterator ConstIterator; + const PlanCacheKey& key = query.getPlanCacheKey(); ConstIterator i = _cache.find(key); if (i != _cache.end()) { PlanCacheEntry* previousEntry = i->second; @@ -276,11 +278,7 @@ namespace mongo { } Status PlanCache::get(const CanonicalQuery& query, CachedSolution** crOut) const{ - PlanCacheKey key = query.getPlanCacheKey(); - return get(key, crOut); - } - - Status PlanCache::get(const PlanCacheKey& key, CachedSolution** crOut) const { + const PlanCacheKey& key = query.getPlanCacheKey(); verify(crOut); boost::lock_guard<boost::mutex> cacheLock(_cacheMutex); @@ -297,14 +295,15 @@ namespace mongo { return Status::OK(); } - Status PlanCache::feedback(const PlanCacheKey& ck, PlanCacheEntryFeedback* feedback) { + Status PlanCache::feedback(const CanonicalQuery& cq, PlanCacheEntryFeedback* feedback) { boost::lock_guard<boost::mutex> cacheLock(_cacheMutex); return Status(ErrorCodes::BadValue, "not implemented yet"); } - Status PlanCache::remove(const PlanCacheKey& ck) { + Status PlanCache::remove(const CanonicalQuery& canonicalQuery) { boost::lock_guard<boost::mutex> cacheLock(_cacheMutex); typedef unordered_map<PlanCacheKey, PlanCacheEntry*>::const_iterator ConstIterator; + const PlanCacheKey& ck = canonicalQuery.getPlanCacheKey(); ConstIterator i = _cache.find(ck); if (i == _cache.end()) { return Status(ErrorCodes::BadValue, "no such key in cache"); diff --git a/src/mongo/db/query/plan_cache.h b/src/mongo/db/query/plan_cache.h index 94c12316ea3..0d9f11e373f 100644 --- a/src/mongo/db/query/plan_cache.h +++ b/src/mongo/db/query/plan_cache.h @@ -128,7 +128,8 @@ namespace mongo { SolutionCacheData() : tree(NULL), solnType(USE_INDEX_TAGS_SOLN), - wholeIXSolnDir(1) { + wholeIXSolnDir(1), + adminHintApplied(false) { } // Make a deep copy. @@ -161,6 +162,9 @@ namespace mongo { // a proxy for a collection scan. Used only // for WHOLE_IXSCAN_SOLN. int wholeIXSolnDir; + + // True if admin hints were applied. + bool adminHintApplied; }; class PlanCacheEntry; @@ -268,6 +272,10 @@ namespace mongo { /** * Generates a key for a normalized (for caching) canonical query * from the match expression and sort order. + * This is an expensive operation because it clones and sorts + * the expression tree in order to generate a string from + * the normalized expression tree. The string generation is also + * potentially expensive. */ static PlanCacheKey getPlanCacheKey(const CanonicalQuery& query); @@ -302,35 +310,25 @@ namespace mongo { Status get(const CanonicalQuery& query, CachedSolution** crOut) const; /** - * Look up the cached data access for the provided key. - * - * If there is no entry in the cache for the 'query', returns an error Status. - * - * If there is an entry in the cache, populates 'crOut' and returns Status::OK(). Caller - * owns '*crOut'. - */ - Status get(const PlanCacheKey& key, CachedSolution** crOut) const; - - /** * When the CachedPlanRunner runs a plan out of the cache, we want to record data about the * plan's performance. The CachedPlanRunner calls feedback(...) at the end of query * execution in order to do this. * * Cache takes ownership of 'feedback'. * - * If the entry corresponding to 'ck' isn't in the cache anymore, the feedback is ignored + * If the entry corresponding to 'cq' isn't in the cache anymore, the feedback is ignored * and an error Status is returned. * - * If the entry corresponding to 'ck' still exists, 'feedback' is added to the run + * If the entry corresponding to 'cq' still exists, 'feedback' is added to the run * statistics about the plan. Status::OK() is returned. */ - Status feedback(const PlanCacheKey& ck, PlanCacheEntryFeedback* feedback); + Status feedback(const CanonicalQuery& cq, PlanCacheEntryFeedback* feedback); /** * Remove the entry corresponding to 'ck' from the cache. Returns Status::OK() if the plan * was present and removed and an error status otherwise. */ - Status remove(const PlanCacheKey& ck); + Status remove(const CanonicalQuery& canonicalQuery); /** * Remove *all* entries. diff --git a/src/mongo/db/query/query_planner.cpp b/src/mongo/db/query/query_planner.cpp index bd8956cbf26..8629cd63157 100644 --- a/src/mongo/db/query/query_planner.cpp +++ b/src/mongo/db/query/query_planner.cpp @@ -439,7 +439,13 @@ namespace mongo { vector<IndexEntry> relevantIndices; // Hints require us to only consider the hinted index. - BSONObj hintIndex = query.getParsed().getHint(); + // If admin hints in the query settings were used to override + // the allowed indices for planning, we should not use the hinted index + // requested in the query. + BSONObj hintIndex; + if (!params.adminHintApplied) { + hintIndex = query.getParsed().getHint(); + } // Snapshot is a form of a hint. If snapshot is set, try to use _id index to make a real // plan. If that fails, just scan the _id index. diff --git a/src/mongo/db/query/query_planner_params.h b/src/mongo/db/query/query_planner_params.h index 6e2972604ef..8fb00b110f8 100644 --- a/src/mongo/db/query/query_planner_params.h +++ b/src/mongo/db/query/query_planner_params.h @@ -36,7 +36,7 @@ namespace mongo { struct QueryPlannerParams { - QueryPlannerParams() : options(DEFAULT) { } + QueryPlannerParams() : options(DEFAULT), adminHintApplied(false) { } enum Options { // You probably want to set this. @@ -76,6 +76,9 @@ namespace mongo { // stage. If we know the shard key, we can perform covering analysis instead of always // forcing a fetch. BSONObj shardKey; + + // Were admin hints applied to indices? + bool adminHintApplied; }; } // namespace mongo diff --git a/src/mongo/db/query/query_settings.cpp b/src/mongo/db/query/query_settings.cpp new file mode 100644 index 00000000000..35ab6de5694 --- /dev/null +++ b/src/mongo/db/query/query_settings.cpp @@ -0,0 +1,166 @@ +/** + * Copyright (C) 2014 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#include "mongo/db/query/query_settings.h" +#include "mongo/db/query/canonical_query.h" +#include "mongo/db/query/plan_cache.h" + +namespace mongo { + + using std::vector; + + // + // HintOverride + // + + AllowedIndices::AllowedIndices(const std::vector<BSONObj>& indexKeyPatterns) { + for (std::vector<BSONObj>::const_iterator i = indexKeyPatterns.begin(); + i != indexKeyPatterns.end(); ++i) { + const BSONObj& indexKeyPattern = *i; + this->indexKeyPatterns.push_back(indexKeyPattern.copy()); + } + } + + AllowedIndices::~AllowedIndices() { } + + // + // AllowedIndexEntry + // + + AllowedIndexEntry::AllowedIndexEntry(const BSONObj& query, const BSONObj& sort, + const BSONObj& projection, + const std::vector<BSONObj>& indexKeyPatterns) + : query(query.copy()), + sort(sort.copy()), + projection(projection.copy()) { + for (std::vector<BSONObj>::const_iterator i = indexKeyPatterns.begin(); + i != indexKeyPatterns.end(); ++i) { + const BSONObj& indexKeyPattern = *i; + this->indexKeyPatterns.push_back(indexKeyPattern.copy()); + } + } + + AllowedIndexEntry::~AllowedIndexEntry() { } + + AllowedIndexEntry* AllowedIndexEntry::clone() const { + AllowedIndexEntry* entry = new AllowedIndexEntry(query, sort, projection, indexKeyPatterns); + return entry; + } + + // + // QuerySettings + // + + QuerySettings::QuerySettings() { } + + QuerySettings::~QuerySettings() { + _clear(); + } + + bool QuerySettings::getAllowedIndices(const CanonicalQuery& query, + AllowedIndices** allowedIndicesOut) const { + invariant(allowedIndicesOut); + + const PlanCacheKey& key = query.getPlanCacheKey(); + + boost::lock_guard<boost::mutex> cacheLock(_mutex); + AllowedIndexEntryMap::const_iterator cacheIter = _allowedIndexEntryMap.find(key); + + // Nothing to do if key does not exist in query settings. + if (cacheIter == _allowedIndexEntryMap.end()) { + *allowedIndicesOut = NULL; + return false; + } + + AllowedIndexEntry* entry = cacheIter->second; + + // Create a AllowedIndices from entry. + *allowedIndicesOut = new AllowedIndices(entry->indexKeyPatterns); + + return true; + } + + std::vector<AllowedIndexEntry*> QuerySettings::getAllAllowedIndices() const { + boost::lock_guard<boost::mutex> cacheLock(_mutex); + vector<AllowedIndexEntry*> entries; + for (AllowedIndexEntryMap::const_iterator i = _allowedIndexEntryMap.begin(); i != _allowedIndexEntryMap.end(); ++i) { + AllowedIndexEntry* entry = i->second; + entries.push_back(entry->clone()); + } + return entries; + } + + void QuerySettings::setAllowedIndices(const CanonicalQuery& canonicalQuery, + const std::vector<BSONObj>& indexes) { + const LiteParsedQuery& lpq = canonicalQuery.getParsed(); + const BSONObj& query = lpq.getFilter(); + const BSONObj& sort = lpq.getSort(); + const BSONObj& projection = lpq.getProj(); + AllowedIndexEntry* entry = new AllowedIndexEntry(query, sort, projection, indexes); + + const PlanCacheKey& key = canonicalQuery.getPlanCacheKey(); + boost::lock_guard<boost::mutex> cacheLock(_mutex); + AllowedIndexEntryMap::iterator i = _allowedIndexEntryMap.find(key); + // Replace existing entry. + if (i != _allowedIndexEntryMap.end()) { + AllowedIndexEntry* entry = i->second; + delete entry; + } + _allowedIndexEntryMap[key] = entry; + } + + void QuerySettings::removeAllowedIndices(const CanonicalQuery& canonicalQuery) { + const PlanCacheKey& key = canonicalQuery.getPlanCacheKey(); + boost::lock_guard<boost::mutex> cacheLock(_mutex); + AllowedIndexEntryMap::iterator i = _allowedIndexEntryMap.find(key); + + // Nothing to do if key does not exist in query settings. + if (i == _allowedIndexEntryMap.end()) { + return; + } + + // Free up resources and delete entry. + AllowedIndexEntry* entry = i->second; + _allowedIndexEntryMap.erase(i); + delete entry; + } + + void QuerySettings::clearAllowedIndices() { + boost::lock_guard<boost::mutex> cacheLock(_mutex); + _clear(); + } + + void QuerySettings::_clear() { + for (AllowedIndexEntryMap::const_iterator i = _allowedIndexEntryMap.begin(); i != _allowedIndexEntryMap.end(); ++i) { + AllowedIndexEntry* entry = i->second; + delete entry; + } + _allowedIndexEntryMap.clear(); + } + +} // namespace mongo diff --git a/src/mongo/db/query/query_settings.h b/src/mongo/db/query/query_settings.h new file mode 100644 index 00000000000..a25c8309947 --- /dev/null +++ b/src/mongo/db/query/query_settings.h @@ -0,0 +1,145 @@ +/** + * Copyright (C) 2014 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <string> +#include <vector> +#include <boost/thread/mutex.hpp> +#include "mongo/base/disallow_copying.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/db/query/canonical_query.h" +#include "mongo/db/query/index_entry.h" +#include "mongo/platform/unordered_map.h" + +namespace mongo { + + /** + * Holds allowed indices. + */ + class AllowedIndices { + private: + MONGO_DISALLOW_COPYING(AllowedIndices); + public: + AllowedIndices(const std::vector<BSONObj>& indexKeyPatterns); + ~AllowedIndices(); + + // These are the index key patterns that + // we will use to override the indexes retrieved from + // the index catalog. + std::vector<BSONObj> indexKeyPatterns; + }; + + /** + * Value type for query settings. + * Holds: + * query shape (query, sort, projection) + * vector of index specs + */ + class AllowedIndexEntry { + private: + MONGO_DISALLOW_COPYING(AllowedIndexEntry); + public: + AllowedIndexEntry(const BSONObj& query, const BSONObj& sort, + const BSONObj& projection, + const std::vector<BSONObj>& indexKeyPatterns); + ~AllowedIndexEntry(); + AllowedIndexEntry* clone() const; + + // _query, _sort and _projection collectively + // represent the query shape that we are storing hint overrides for. + BSONObj query; + BSONObj sort; + BSONObj projection; + + // These are the index key patterns that + // we will use to override the indexes retrieved from + // the index catalog. + std::vector<BSONObj> indexKeyPatterns; + }; + + /** + * Holds the admin hints in a collection. + */ + class QuerySettings { + private: + MONGO_DISALLOW_COPYING(QuerySettings); + public: + QuerySettings(); + + ~QuerySettings(); + + /** + * Returns true and fills out allowedIndicesOut if a hint is set in the query settings + * for the query. + * Returns false and sets allowedIndicesOut to NULL otherwise. + * Caller owns AllowedIndices. + */ + bool getAllowedIndices(const CanonicalQuery& query, + AllowedIndices** allowedIndicesOut) const; + + /** + * Returns copies all overrides for the collection.. + * Caller owns overrides in vector. + */ + std::vector<AllowedIndexEntry*> getAllAllowedIndices() const; + + /** + * Adds or replaces entry in query settings. + * If existing entry is found for the same key, + * frees resources for existing entry before replacing. + */ + void setAllowedIndices(const CanonicalQuery& canonicalQuery, + const std::vector<BSONObj>& indexes); + + /** + * Removes single entry from query settings. No effect if query shape is not found. + */ + void removeAllowedIndices(const CanonicalQuery& canonicalQuery); + + /** + * Clears all allowed indices from query settings. + */ + void clearAllowedIndices(); + + private: + /** + * Clears entries without acquiring mutex. + */ + void _clear(); + + typedef unordered_map<PlanCacheKey, AllowedIndexEntry*> AllowedIndexEntryMap; + AllowedIndexEntryMap _allowedIndexEntryMap; + + /** + * Protects data in query settings. + */ + mutable boost::mutex _mutex; + }; + +} // namespace mongo diff --git a/src/mongo/s/commands/cluster_hint_cmd.cpp b/src/mongo/s/commands/cluster_hint_cmd.cpp new file mode 100644 index 00000000000..f4ddf4ada36 --- /dev/null +++ b/src/mongo/s/commands/cluster_hint_cmd.cpp @@ -0,0 +1,178 @@ +/** + * Copyright (C) 2014 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#include "mongo/base/init.h" +#include "mongo/base/error_codes.h" +#include "mongo/db/auth/authorization_session.h" +#include "mongo/db/client_basic.h" +#include "mongo/db/commands.h" +#include "mongo/s/client_info.h" +#include "mongo/s/config.h" +#include "mongo/s/grid.h" +#include "mongo/s/stale_exception.h" +#include "mongo/s/strategy.h" + +namespace mongo { + + using std::string; + using std::vector; + + /** + * Base class for mongos hint commands. + * Cluster hint commands don't do much more than + * forwarding the commands to all shards and combining the results. + */ + class ClusterHintCmd : public Command { + MONGO_DISALLOW_COPYING(ClusterHintCmd); + public: + + virtual ~ClusterHintCmd() { + } + + bool logTheOp() { + return false; + } + + bool slaveOk() const { + return false; + } + + LockType locktype() const { + return Command::NONE; + } + + void help(stringstream& ss) const { + ss << _helpText; + } + + Status checkAuthForCommand( ClientBasic* client, + const std::string& dbname, + const BSONObj& cmdObj ) { + AuthorizationSession* authzSession = client->getAuthorizationSession(); + ResourcePattern pattern = parseResourcePattern(dbname, cmdObj); + + if (authzSession->isAuthorizedForActionsOnResource(pattern, ActionType::planCacheHint)) { + return Status::OK(); + } + + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } + + // Cluster plan cache command entry point. + bool run( const std::string& dbname, + BSONObj& cmdObj, + int options, + std::string& errmsg, + BSONObjBuilder& result, + bool fromRepl ); + + public: + + /** + * Instantiates a command that can be invoked by "name", which will be described by + * "helpText", and will require privilege "actionType" to run. + */ + ClusterHintCmd( const std::string& name, const std::string& helpText) : + Command( name ), _helpText( helpText ) { + } + + private: + + std::string _helpText; + }; + + // + // Cluster hint command implementation(s) below + // + + bool ClusterHintCmd::run( const std::string& dbName, + BSONObj& cmdObj, + int options, + std::string& errMsg, + BSONObjBuilder& result, + bool ) { + const std::string fullns = parseNs(dbName, cmdObj); + NamespaceString nss(fullns); + + // Dispatch command to all the shards. + // Targeted shard commands are generally data-dependent but hint + // commands are tied to query shape (data has no effect on query shape). + vector<Strategy::CommandResult> results; + STRATEGY->commandOp(dbName, cmdObj, options, nss.ns(), BSONObj(), &results); + + // Set value of first shard result's "ok" field. + bool clusterCmdResult = true; + + for (vector<Strategy::CommandResult>::const_iterator i = results.begin(); + i != results.end(); ++i) { + const Strategy::CommandResult& cmdResult = *i; + + // XXX: In absence of sensible aggregation strategy, + // promote first shard's result to top level. + if (i == results.begin()) { + result.appendElements(cmdResult.result); + clusterCmdResult = cmdResult.result["ok"].trueValue(); + } + + // Append shard result as a sub object. + // Name the field after the shard. + string shardName = cmdResult.shardTarget.getName(); + result.append(shardName, cmdResult.result); + } + + return clusterCmdResult; + } + + // + // Register hint commands at startup + // + + namespace { + + MONGO_INITIALIZER(RegisterHintCommands)(InitializerContext* context) { + // Leaked intentionally: a Command registers itself when constructed. + + new ClusterHintCmd( + "planCacheListHints", + "Displays admin hints for all query shapes in a collection." ); + + new ClusterHintCmd( + "planCacheClearHints", + "Clears all admin hints for a single query shape or, " + "if the query shape is omitted, for the entire collection." ); + + new ClusterHintCmd( + "planCacheSetHint", + "Sets admin hints for a query shape. Overrides existing hints." ); + + return Status::OK(); + } + + } // namespace + +} // namespace mongo diff --git a/src/mongo/s/commands/cluster_plan_cache_cmd.cpp b/src/mongo/s/commands/cluster_plan_cache_cmd.cpp index fe44da91b6d..be041dd6cc8 100644 --- a/src/mongo/s/commands/cluster_plan_cache_cmd.cpp +++ b/src/mongo/s/commands/cluster_plan_cache_cmd.cpp @@ -66,6 +66,10 @@ namespace mongo { return Command::NONE; } + void help(stringstream& ss) const { + ss << _helpText; + } + Status checkAuthForCommand( ClientBasic* client, const std::string& dbname, const BSONObj& cmdObj ) { |