summaryrefslogtreecommitdiff
path: root/src/mongo/db/commands
diff options
context:
space:
mode:
authorBenety Goh <benety@mongodb.com>2014-01-09 15:05:55 -0500
committerBenety Goh <benety@mongodb.com>2014-01-15 16:35:44 -0500
commit9b94912fe8847977c4d23a12604cb900e6f426b7 (patch)
tree688de4d301133aa1a409529926487349584c7204 /src/mongo/db/commands
parent26a8aaf0e660c247c2667e0f182bc4964096735b (diff)
downloadmongo-9b94912fe8847977c4d23a12604cb900e6f426b7.tar.gz
SERVER-8871 admin hints
Diffstat (limited to 'src/mongo/db/commands')
-rw-r--r--src/mongo/db/commands/hint_commands.cpp407
-rw-r--r--src/mongo/db/commands/hint_commands.h167
-rw-r--r--src/mongo/db/commands/hint_commands_test.cpp297
-rw-r--r--src/mongo/db/commands/plan_cache_commands.cpp29
-rw-r--r--src/mongo/db/commands/plan_cache_commands.h6
-rw-r--r--src/mongo/db/commands/plan_cache_commands_test.cpp71
6 files changed, 911 insertions, 66 deletions
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;