SERVER-100957 Provide different QuerySettingsService implementations for router and shard (#35027)

Co-authored-by: Catalin Sumanaru <catalin.sumanaru@mongodb.com>
GitOrigin-RevId: 8bd8a3e7a3756108a2893b3457353a53778aa77a
This commit is contained in:
Denis Grebennicov 2025-04-25 15:41:43 +02:00 committed by MongoDB Bot
parent 3de1221ad8
commit 9f8a5dc5bd
13 changed files with 1144 additions and 838 deletions

View File

@ -213,17 +213,19 @@ void validateAndSimplifyQuerySettings(
const boost::optional<const RepresentativeQueryInfo&>& representativeQueryInfo,
const boost::optional<QueryInstance>& previousRepresentativeQuery,
QuerySettings& querySettings) {
auto& service = QuerySettingsService::get(opCtx);
// In case the representative query was not provided but the previous representative query is
// available, assert that query settings will be set on a valid query.
if (!representativeQueryInfo && previousRepresentativeQuery) {
validateQueryCompatibleWithAnyQuerySettings(
service.validateQueryCompatibleWithAnyQuerySettings(
createRepresentativeInfo(opCtx, *previousRepresentativeQuery, tenantId));
}
if (representativeQueryInfo) {
validateQueryCompatibleWithQuerySettings(*representativeQueryInfo, querySettings);
service.validateQueryCompatibleWithQuerySettings(*representativeQueryInfo, querySettings);
}
simplifyQuerySettings(querySettings);
validateQuerySettings(querySettings);
service.simplifyQuerySettings(querySettings);
service.validateQuerySettings(querySettings);
}
class SetQuerySettingsCommand final : public TypedCommand<SetQuerySettingsCommand> {
@ -312,7 +314,8 @@ public:
// Assert that query settings will be set on a valid query.
if (representativeQuery) {
validateQueryCompatibleWithAnyQuerySettings(*representativeQueryInfo);
QuerySettingsService::get(opCtx).validateQueryCompatibleWithAnyQuerySettings(
*representativeQueryInfo);
}
SetQuerySettingsCommandReply reply;

View File

@ -56,6 +56,7 @@ idl_generator(
mongo_cc_library(
name = "query_settings_service",
srcs = [
"query_settings_cluster_parameter_impl.cpp",
"query_settings_hash.cpp",
"query_settings_manager.cpp",
"query_settings_manager.h",
@ -90,6 +91,7 @@ mongo_cc_unit_test(
srcs = [
"index_hints_serialization_test.cpp",
"query_framework_serialization_test.cpp",
"query_settings_cluster_parameter_impl_test.cpp",
"query_settings_hash_test.cpp",
"query_settings_manager_test.cpp",
"query_settings_service_test.cpp",

View File

@ -0,0 +1,147 @@
/**
* Copyright (C) 2025-present MongoDB, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the Server Side Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*/
#include <algorithm>
#include <boost/move/utility_core.hpp>
#include <boost/optional.hpp>
#include "mongo/base/error_codes.h"
#include "mongo/base/status.h"
#include "mongo/base/string_data.h"
#include "mongo/bson/bsonelement.h"
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/bson/bsontypes.h"
#include "mongo/db/client.h"
#include "mongo/db/commands/server_status.h"
#include "mongo/db/logical_time.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/query/query_settings/query_settings_cluster_parameter_gen.h"
#include "mongo/db/query/query_settings/query_settings_service.h"
#include "mongo/db/service_context.h"
#include "mongo/idl/idl_parser.h"
namespace mongo::query_settings {
class QuerySettingsServerStatusSection final : public ServerStatusSection {
public:
using ServerStatusSection::ServerStatusSection;
bool includeByDefault() const override {
// Only include if Query Settings are enabled.
// We need to use isEnabledUseLatestFCVWhenUninitialized() instead of isEnabled() because
// this could run during startup while the FCV is still uninitialized.
return feature_flags::gFeatureFlagQuerySettings.isEnabledUseLatestFCVWhenUninitialized(
serverGlobalParams.featureCompatibility.acquireFCVSnapshot());
}
BSONObj generateSection(OperationContext* opCtx,
const BSONElement& configElement) const override {
stdx::lock_guard<stdx::mutex> lk(_mutex);
return BSON("count" << _count << "size" << _size << "rejectCount"
<< _numSettingsWithReject);
}
void record(int count, int size, int numSettingsWithReject) {
stdx::lock_guard<stdx::mutex> lk(_mutex);
_count = count;
_size = size;
_numSettingsWithReject = numSettingsWithReject;
}
private:
int _count = 0;
int _size = 0;
int _numSettingsWithReject = 0;
mutable stdx::mutex _mutex;
};
auto& querySettingsServerStatusSection =
*ServerStatusSectionBuilder<QuerySettingsServerStatusSection>("querySettings");
void QuerySettingsClusterParameter::append(OperationContext* opCtx,
BSONObjBuilder* bob,
StringData name,
const boost::optional<TenantId>& tenantId) {
auto& querySettingsService = QuerySettingsService::get(getGlobalServiceContext());
auto config = querySettingsService.getAllQueryShapeConfigurations(tenantId);
bob->append(QuerySettingsClusterParameterValue::k_idFieldName,
querySettingsService.getQuerySettingsClusterParameterName());
BSONArrayBuilder arrayBuilder(
bob->subarrayStart(QuerySettingsClusterParameterValue::kSettingsArrayFieldName));
for (auto&& item : config.queryShapeConfigurations) {
arrayBuilder.append(item.toBSON());
}
arrayBuilder.done();
bob->append(QuerySettingsClusterParameterValue::kClusterParameterTimeFieldName,
config.clusterParameterTime.asTimestamp());
}
Status QuerySettingsClusterParameter::set(const BSONElement& newValueElement,
const boost::optional<TenantId>& tenantId) {
auto& querySettingsService = QuerySettingsService::get(getGlobalServiceContext());
auto newSettings = QuerySettingsClusterParameterValue::parse(
IDLParserContext("querySettingsParameterValue",
boost::none /* vts */,
tenantId,
SerializationContext::stateDefault()),
newValueElement.Obj());
auto& settingsArray = newSettings.getSettingsArray();
// TODO SERVER-97546 Remove PQS index hint sanitization.
querySettingsService.sanitizeQuerySettingsHints(settingsArray);
size_t rejectCount = 0;
for (const auto& config : settingsArray) {
if (config.getSettings().getReject()) {
++rejectCount;
}
}
querySettingsServerStatusSection.record(
/* count */ static_cast<int>(settingsArray.size()),
/* size */ static_cast<int>(newValueElement.valuesize()),
/* numSettingsWithReject */ static_cast<int>(rejectCount));
querySettingsService.setAllQueryShapeConfigurations(
{std::move(settingsArray), newSettings.getClusterParameterTime()}, tenantId);
return Status::OK();
}
Status QuerySettingsClusterParameter::reset(const boost::optional<TenantId>& tenantId) {
auto& querySettingsService = QuerySettingsService::get(getGlobalServiceContext());
querySettingsService.removeAllQueryShapeConfigurations(tenantId);
return Status::OK();
}
LogicalTime QuerySettingsClusterParameter::getClusterParameterTime(
const boost::optional<TenantId>& tenantId) const {
auto& querySettingsService = QuerySettingsService::get(getGlobalServiceContext());
return querySettingsService.getClusterParameterTime(tenantId);
}
}; // namespace mongo::query_settings

View File

@ -0,0 +1,343 @@
/**
* Copyright (C) 2025-present MongoDB, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the Server Side Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*/
#include <boost/move/utility_core.hpp>
#include <boost/optional.hpp>
#include <boost/optional/optional.hpp>
#include "mongo/base/error_codes.h"
#include "mongo/base/string_data.h"
#include "mongo/bson/bsonelement.h"
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/bson/bsontypes.h"
#include "mongo/bson/timestamp.h"
#include "mongo/db/client.h"
#include "mongo/db/logical_time.h"
#include "mongo/db/matcher/expression_parser.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/query/query_knobs_gen.h"
#include "mongo/db/query/query_settings/query_settings_cluster_parameter_gen.h"
#include "mongo/db/query/query_settings/query_settings_gen.h"
#include "mongo/db/query/query_settings/query_settings_service.h"
#include "mongo/db/server_parameter.h"
#include "mongo/db/service_context.h"
#include "mongo/db/service_context_test_fixture.h"
#include "mongo/db/tenant_id.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/serialization_context.h"
namespace mongo::query_settings {
static bool operator==(const QueryShapeConfiguration& lhs, const QueryShapeConfiguration& rhs) {
return SimpleBSONObjComparator::kInstance.compare(lhs.toBSON(), rhs.toBSON()) == 0;
}
static bool operator==(const QueryShapeConfigurationsWithTimestamp& lhs,
const QueryShapeConfigurationsWithTimestamp& rhs) {
return lhs.clusterParameterTime == rhs.clusterParameterTime &&
lhs.queryShapeConfigurations == rhs.queryShapeConfigurations;
}
namespace {
static auto const kSerializationContext =
SerializationContext{SerializationContext::Source::Command,
SerializationContext::CallerType::Request,
SerializationContext::Prefix::ExcludePrefix};
auto makeDbName(StringData dbName) {
return DatabaseNameUtil::deserialize(
boost::none /*tenantId=*/, dbName, SerializationContext::stateDefault());
}
NamespaceSpec makeNsSpec(StringData collName) {
NamespaceSpec ns;
ns.setDb(makeDbName("testDbA"));
ns.setColl(collName);
return ns;
}
QuerySettings makeQuerySettings(const IndexHintSpecs& indexHints, bool setFramework = false) {
QuerySettings settings;
if (!indexHints.empty()) {
settings.setIndexHints(indexHints);
}
if (setFramework) {
settings.setQueryFramework(mongo::QueryFrameworkControlEnum::kTrySbeEngine);
}
return settings;
}
class QuerySettingsClusterParameterTest : public ServiceContextTest {
public:
void setUp() final {
_opCtx = cc().makeOperationContext();
}
OperationContext* opCtx() {
return _opCtx.get();
}
QuerySettingsService& service() {
return QuerySettingsService::get(opCtx());
}
QueryShapeConfiguration makeQueryShapeConfiguration(
const BSONObj& cmdBSON,
const QuerySettings& querySettings,
boost::optional<TenantId> tenantId = boost::none) {
auto queryShapeHash = createRepresentativeInfo(opCtx(), cmdBSON, tenantId).queryShapeHash;
QueryShapeConfiguration config(queryShapeHash, querySettings);
config.setRepresentativeQuery(cmdBSON);
return config;
}
BSONObj makeQuerySettingsClusterParameter(const QueryShapeConfigurationsWithTimestamp& config) {
BSONArrayBuilder bob;
for (const auto& c : config.queryShapeConfigurations) {
bob.append(c.toBSON());
}
return BSON("_id" << QuerySettingsService::getQuerySettingsClusterParameterName()
<< QuerySettingsClusterParameterValue::kSettingsArrayFieldName
<< bob.arr()
<< QuerySettingsClusterParameterValue::kClusterParameterTimeFieldName
<< config.clusterParameterTime.asTimestamp());
}
void assertTransformInvalidQuerySettings(
const std::list<std::pair<const IndexHintSpecs, const IndexHintSpecs>>&
listOfIndexHintSpecs) {
auto sp = std::make_unique<QuerySettingsClusterParameter>(
QuerySettingsService::getQuerySettingsClusterParameterName(),
ServerParameterType::kClusterWide);
QueryShapeConfigurationsWithTimestamp configsWithTs;
std::vector<QueryShapeConfiguration> expectedQueryShapeConfigurations;
size_t collIndex = 0;
// Create the 'querySettingsClusterParamValue' as BSON.
for (const auto& [initialIndexHintSpecs, expectedIndexHintSpecs] : listOfIndexHintSpecs) {
QueryInstance representativeQuery =
BSON("find" << "coll_" + std::to_string(collIndex) << "$db"
<< "foo");
configsWithTs.queryShapeConfigurations.emplace_back(makeQueryShapeConfiguration(
representativeQuery, makeQuerySettings(initialIndexHintSpecs)));
if (!expectedIndexHintSpecs.empty()) {
expectedQueryShapeConfigurations.emplace_back(makeQueryShapeConfiguration(
representativeQuery, makeQuerySettings(expectedIndexHintSpecs)));
}
collIndex++;
}
// Set the new 'clusterParamValue' for "querySettings" cluster parameter.
const auto clusterParamValue = makeQuerySettingsClusterParameter(configsWithTs);
ASSERT_OK(
sp->set(BSON("" << clusterParamValue).firstElement(), boost::none /* tenantId */));
// Assert that parsing after transforming invalid settings (if any) works.
ASSERT_EQ(expectedQueryShapeConfigurations,
service()
.getAllQueryShapeConfigurations(/* tenantId */ boost::none)
.queryShapeConfigurations);
}
private:
ServiceContext::UniqueOperationContext _opCtx;
};
/**
* Tests that set/reset methods over the QuerySettings cluster parameter correctly populate the data
* in QuerySettingsService, as well as that QuerySettings data is correctly populated when
* serializing cluster parameter.
*/
TEST_F(QuerySettingsClusterParameterTest, QuerySettingsClusterParameterSetReset) {
boost::optional<TenantId> tenantId;
auto sp = std::make_unique<QuerySettingsClusterParameter>(
QuerySettingsService::getQuerySettingsClusterParameterName(),
ServerParameterType::kClusterWide);
// Ensure no clusterParameterTime is specified for "querySettings" cluster parameter, if no
// settings are specified.
ASSERT_EQ(sp->getClusterParameterTime(tenantId), LogicalTime());
ASSERT_EQ(service().getClusterParameterTime(tenantId), LogicalTime());
{
BSONObjBuilder bob;
sp->append(
opCtx(), &bob, QuerySettingsService::getQuerySettingsClusterParameterName(), tenantId);
ASSERT_BSONOBJ_EQ(
bob.done(), makeQuerySettingsClusterParameter(QueryShapeConfigurationsWithTimestamp()));
}
// Set query shape configuration.
QuerySettings settings;
settings.setQueryFramework(QueryFrameworkControlEnum::kTrySbeEngine);
QueryInstance findCmdBSON = BSON("find" << "exampleColl"
<< "$db"
<< "foo");
auto config = makeQueryShapeConfiguration(findCmdBSON, settings);
LogicalTime clusterParameterTime(Timestamp(1, 2));
QueryShapeConfigurationsWithTimestamp configsWithTs{{config}, clusterParameterTime};
// Ensure that after parameter is set, the query shape configurations are present in the
// QuerySettingsService.
const auto clusterParamValue = makeQuerySettingsClusterParameter(configsWithTs);
ASSERT_OK(sp->set(BSON("" << clusterParamValue).firstElement(), tenantId));
ASSERT_EQ(service().getAllQueryShapeConfigurations(tenantId), configsWithTs);
ASSERT_EQ(sp->getClusterParameterTime(tenantId), clusterParameterTime);
ASSERT_EQ(service().getClusterParameterTime(tenantId), clusterParameterTime);
// Ensure the serialized parameter value contains 'settingsArray' with 'config' as value as well
// parameter id and clusterParameterTime.
{
BSONObjBuilder bob;
sp->append(
opCtx(), &bob, QuerySettingsService::getQuerySettingsClusterParameterName(), tenantId);
ASSERT_BSONOBJ_EQ(bob.done(), clusterParamValue);
}
// Ensure that after parameter is reset, no query shape configurations are present in the
// QuerySettingsService and clusterParameterTime is reset.
ASSERT_OK(sp->reset(tenantId));
ASSERT(service().getAllQueryShapeConfigurations(tenantId).queryShapeConfigurations.empty());
ASSERT_EQ(sp->getClusterParameterTime(tenantId), LogicalTime());
ASSERT_EQ(service().getClusterParameterTime(tenantId), LogicalTime());
{
BSONObjBuilder bob;
sp->append(
opCtx(), &bob, QuerySettingsService::getQuerySettingsClusterParameterName(), tenantId);
ASSERT_BSONOBJ_EQ(
bob.done(), makeQuerySettingsClusterParameter(QueryShapeConfigurationsWithTimestamp()));
}
}
/**
* Tests that query settings with invalid key-pattern index hints leads to no query settings after
* sanitization.
*/
TEST_F(QuerySettingsClusterParameterTest,
QuerySettingsClusterParameterSetInvalidQSWithSanitization) {
boost::optional<TenantId> tenantId;
auto sp = std::make_unique<QuerySettingsClusterParameter>(
QuerySettingsService::getQuerySettingsClusterParameterName(),
ServerParameterType::kClusterWide);
IndexHintSpecs initialIndexHintSpec{
IndexHintSpec(makeNsSpec("testCollC"_sd),
{IndexHint(BSON("a" << 1 << "$natural" << 1)), IndexHint(BSONObj{})}),
IndexHintSpec(makeNsSpec("testCollD"_sd),
{IndexHint(BSON("b" << "some-string"
<< "a" << 1))})};
QuerySettings querySettings;
querySettings.setIndexHints(initialIndexHintSpec);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 9646001);
// Create the 'querySettingsClusterParamValue' as BSON.
const auto findCmdBSON = BSON("find" << "bar"
<< "$db"
<< "foo");
const auto config = makeQueryShapeConfiguration(findCmdBSON, querySettings);
// Assert that parsing after transforming invalid settings (if any) works.
const auto clusterParamValue =
makeQuerySettingsClusterParameter({{config}, LogicalTime(Timestamp(3, 4))});
ASSERT_OK(sp->set(BSON("" << clusterParamValue).firstElement(), tenantId));
ASSERT(service().getAllQueryShapeConfigurations(tenantId).queryShapeConfigurations.empty());
}
/**
* Sets the value of QuerySettingsClusterParameter and asserts the resulting index hint spec is
* sanitized.
*/
TEST_F(QuerySettingsClusterParameterTest, SetValidClusterParameterAndAssertResultIsSanitized) {
IndexHintSpecs indexHintSpec{IndexHintSpec(
makeNsSpec("testCollA"), {IndexHint(BSON("a" << 1)), IndexHint(BSON("b" << -1.0))})};
assertTransformInvalidQuerySettings({{indexHintSpec, indexHintSpec}});
}
/**
* Same as above test, but assert that the validation would fail with invalid key-pattern indexes.
*/
TEST_F(QuerySettingsClusterParameterTest, SetClusterParameterAndAssertResultIsSanitized) {
IndexHintSpecs initialIndexHintSpec{
IndexHintSpec(makeNsSpec("testCollA"_sd),
{IndexHint(BSON("a" << 1.0)), IndexHint(BSON("b" << "-1.0"))}),
IndexHintSpec(makeNsSpec("testCollB"_sd),
{IndexHint(BSON("a" << 2)), IndexHint(BSONObj{})})};
IndexHintSpecs expectedIndexHintSpec{
IndexHintSpec(makeNsSpec("testCollA"_sd), {IndexHint(BSON("a" << 1))}),
IndexHintSpec(makeNsSpec("testCollB"_sd), {IndexHint(BSON("a" << 2))})};
ASSERT_THROWS_CODE(service().validateQuerySettings(makeQuerySettings(initialIndexHintSpec)),
DBException,
9646001);
assertTransformInvalidQuerySettings({{initialIndexHintSpec, expectedIndexHintSpec}});
}
/**
* Same as above test, but with multiple QuerySettings set.
*/
TEST_F(QuerySettingsClusterParameterTest, SetClusterParameterAndAssertResultIsSanitizedMultipleQS) {
// First pair of index hints.
IndexHintSpecs initialIndexHintSpec1{
IndexHintSpec(makeNsSpec("testCollA"_sd),
{IndexHint(BSON("a" << 2)), IndexHint(BSONObj{})}),
IndexHintSpec(makeNsSpec("testCollB"_sd),
{IndexHint(BSON("a" << 1.0)),
IndexHint(BSON("b" << 3)),
IndexHint("a_1"),
IndexHint(NaturalOrderHint(NaturalOrderHint::Direction::kForward))})};
IndexHintSpecs expectedIndexHintSpec1{
IndexHintSpec(makeNsSpec("testCollA"_sd), {IndexHint(BSON("a" << 2.0))}),
IndexHintSpec(makeNsSpec("testCollB"_sd),
{IndexHint(BSON("a" << 1.0)),
IndexHint(BSON("b" << 3)),
IndexHint("a_1"),
IndexHint(NaturalOrderHint(NaturalOrderHint::Direction::kForward))})};
// Second pair of index hints.
IndexHintSpecs initialIndexHintSpec2{
IndexHintSpec(makeNsSpec("testCollC"_sd),
{IndexHint(BSON("a" << 1 << "$natural" << 1)), IndexHint(BSONObj{})}),
IndexHintSpec(makeNsSpec("testCollD"_sd),
{IndexHint(BSON("b" << "some-string"
<< "a" << 1)),
IndexHint(BSONObj{})})};
ASSERT_THROWS_CODE(service().validateQuerySettings(makeQuerySettings(initialIndexHintSpec1)),
DBException,
9646000);
ASSERT_THROWS_CODE(service().validateQuerySettings(makeQuerySettings(initialIndexHintSpec2)),
DBException,
9646001);
assertTransformInvalidQuerySettings(
{{initialIndexHintSpec1, expectedIndexHintSpec1}, {initialIndexHintSpec2, {}}});
}
} // namespace
} // namespace mongo::query_settings

View File

@ -264,11 +264,11 @@ public:
benchmark::Counter::OneK::kIs1024);
// Update the qurey shape configurations present in the system.
query_settings::setAllQueryShapeConfigurations(
opCtx.get(),
QueryShapeConfigurationsWithTimestamp{std::move(queryShapeConfigs),
LogicalTime(Timestamp(1))},
tenantId);
QuerySettingsService::get(opCtx.get())
.setAllQueryShapeConfigurations(
QueryShapeConfigurationsWithTimestamp{std::move(queryShapeConfigs),
LogicalTime(Timestamp(1))},
tenantId);
benchmark::ClobberMemory();
}
@ -311,11 +311,12 @@ public:
auto&& queryShapeConfigurations =
queryShapeConfigurationsWithTimestamp.queryShapeConfigurations;
queryShapeConfigurations.push_back(hitQueryShapeConfiguration);
query_settings::setAllQueryShapeConfigurations(
opCtx.get(),
QueryShapeConfigurationsWithTimestamp{std::move(queryShapeConfigurations),
LogicalTime(Timestamp(2))},
tid);
QuerySettingsService::get(opCtx.get())
.setAllQueryShapeConfigurations(
QueryShapeConfigurationsWithTimestamp{std::move(queryShapeConfigurations),
LogicalTime(Timestamp(2))},
tid);
}
return parsedFindRequest;

View File

@ -32,67 +32,15 @@
#include <algorithm>
#include <boost/move/utility_core.hpp>
#include <boost/optional.hpp>
#include <boost/optional/optional.hpp>
#include "mongo/base/error_codes.h"
#include "mongo/base/status.h"
#include "mongo/base/string_data.h"
#include "mongo/bson/bsonelement.h"
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/bson/bsontypes.h"
#include "mongo/db/client.h"
#include "mongo/db/commands/server_status.h"
#include "mongo/db/logical_time.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/query/query_settings/query_settings_cluster_parameter_gen.h"
#include "mongo/db/query/query_settings/query_settings_gen.h"
#include "mongo/db/service_context.h"
#include "mongo/idl/idl_parser.h"
#include "mongo/util/decorable.h"
#include "mongo/db/query/query_settings/query_settings_service.h"
namespace mongo::query_settings {
namespace {
const auto getQuerySettingsManager =
ServiceContext::declareDecoration<std::unique_ptr<QuerySettingsManager>>();
class QuerySettingsServerStatusSection final : public ServerStatusSection {
public:
using ServerStatusSection::ServerStatusSection;
bool includeByDefault() const override {
// Only include if Query Settings are enabled.
// We need to use isEnabledUseLatestFCVWhenUninitialized instead of isEnabled because
// this could run during startup while the FCV is still uninitialized.
return feature_flags::gFeatureFlagQuerySettings.isEnabledUseLatestFCVWhenUninitialized(
serverGlobalParams.featureCompatibility.acquireFCVSnapshot());
}
BSONObj generateSection(OperationContext* opCtx,
const BSONElement& configElement) const override {
stdx::lock_guard<stdx::mutex> lk(_mutex);
return BSON("count" << _count << "size" << _size << "rejectCount"
<< _numSettingsWithReject);
}
void record(int count, int size, int numSettingsWithReject) {
stdx::lock_guard<stdx::mutex> lk(_mutex);
_count = count;
_size = size;
_numSettingsWithReject = numSettingsWithReject;
}
private:
int _count = 0;
int _size = 0;
int _numSettingsWithReject = 0;
mutable stdx::mutex _mutex;
};
auto& querySettingsServerStatusSection =
*ServerStatusSectionBuilder<QuerySettingsServerStatusSection>("querySettings");
auto computeTenantConfiguration(std::vector<QueryShapeConfiguration>&& settingsArray) {
QueryShapeConfigurationsMap queryShapeConfigurationMap;
queryShapeConfigurationMap.reserve(settingsArray.size());
@ -105,22 +53,6 @@ auto computeTenantConfiguration(std::vector<QueryShapeConfiguration>&& settingsA
}
} // namespace
QuerySettingsManager& QuerySettingsManager::get(ServiceContext* service) {
return *getQuerySettingsManager(service);
}
QuerySettingsManager& QuerySettingsManager::get(OperationContext* opCtx) {
return *getQuerySettingsManager(opCtx->getServiceContext());
}
void QuerySettingsManager::create(
ServiceContext* service,
std::function<void(OperationContext*)> clusterParameterRefreshFn,
std::function<void(std::vector<QueryShapeConfiguration>&)> sanitizeQuerySettingsHintsFn) {
getQuerySettingsManager(service) = std::make_unique<QuerySettingsManager>(
service, clusterParameterRefreshFn, sanitizeQuerySettingsHintsFn);
}
boost::optional<QuerySettings> QuerySettingsManager::getQuerySettingsForQueryShapeHash(
const query_shape::QueryShapeHash& queryShapeHash,
const boost::optional<TenantId>& tenantId) const {
@ -145,13 +77,12 @@ boost::optional<QuerySettings> QuerySettingsManager::getQuerySettingsForQuerySha
return queryShapeConfigurationsIt->second.first;
}
void QuerySettingsManager::setQueryShapeConfigurations(
std::vector<QueryShapeConfiguration>&& settingsArray,
LogicalTime parameterClusterTime,
const boost::optional<TenantId>& tenantId) {
void QuerySettingsManager::setAllQueryShapeConfigurations(
QueryShapeConfigurationsWithTimestamp&& config, const boost::optional<TenantId>& tenantId) {
// Build new query shape configurations.
VersionedQueryShapeConfigurations newQueryShapeConfigurations{
computeTenantConfiguration(std::move(settingsArray)), parameterClusterTime};
computeTenantConfiguration(std::move(config.queryShapeConfigurations)),
config.clusterParameterTime};
// Install the query shape configurations.
{
@ -171,6 +102,27 @@ void QuerySettingsManager::setQueryShapeConfigurations(
}
}
void QuerySettingsManager::removeAllQueryShapeConfigurations(
const boost::optional<TenantId>& tenantId) {
// Previous query shape configurations for destruction outside the critical section.
VersionedQueryShapeConfigurations previousQueryShapeConfigurations;
{
auto writeLock = _mutex.writeLock();
const auto versionedQueryShapeConfigurationsIt =
_tenantIdToVersionedQueryShapeConfigurationsMap.find(tenantId);
if (_tenantIdToVersionedQueryShapeConfigurationsMap.end() !=
versionedQueryShapeConfigurationsIt) {
// Swap the configurations to minimize the time the lock is held in exclusive mode by
// deferring the destruction of the previous version of the query shape configurations
// to the time when the lock is not held.
std::swap(versionedQueryShapeConfigurationsIt->second,
previousQueryShapeConfigurations);
_tenantIdToVersionedQueryShapeConfigurationsMap.erase(
versionedQueryShapeConfigurationsIt);
}
}
}
QueryShapeConfigurationsWithTimestamp QuerySettingsManager::getAllQueryShapeConfigurations(
const boost::optional<TenantId>& tenantId) const {
auto readLock = _mutex.readLock();
@ -200,32 +152,6 @@ std::vector<QueryShapeConfiguration> QuerySettingsManager::getAllQueryShapeConfi
return configurations;
}
void QuerySettingsManager::refreshQueryShapeConfigurations(OperationContext* opCtx) {
if (_clusterParameterRefreshFn)
_clusterParameterRefreshFn(opCtx);
}
void QuerySettingsManager::removeAllQueryShapeConfigurations(
const boost::optional<TenantId>& tenantId) {
// Previous query shape configurations for destruction outside the critical section.
VersionedQueryShapeConfigurations previousQueryShapeConfigurations;
{
auto writeLock = _mutex.writeLock();
const auto versionedQueryShapeConfigurationsIt =
_tenantIdToVersionedQueryShapeConfigurationsMap.find(tenantId);
if (_tenantIdToVersionedQueryShapeConfigurationsMap.end() !=
versionedQueryShapeConfigurationsIt) {
// Swap the configurations to minimize the time the lock is held in exclusive mode by
// deferring the destruction of the previous version of the query shape configurations
// to the time when the lock is not held.
std::swap(versionedQueryShapeConfigurationsIt->second,
previousQueryShapeConfigurations);
_tenantIdToVersionedQueryShapeConfigurationsMap.erase(
versionedQueryShapeConfigurationsIt);
}
}
}
LogicalTime QuerySettingsManager::getClusterParameterTime(
const boost::optional<TenantId>& tenantId) const {
auto readLock = _mutex.readLock();
@ -242,76 +168,4 @@ LogicalTime QuerySettingsManager::getClusterParameterTime_inlock(
}
return versionedQueryShapeConfigurationsIt->second.clusterParameterTime;
}
void QuerySettingsManager::appendQuerySettingsClusterParameterValue(
BSONObjBuilder* bob, const boost::optional<TenantId>& tenantId) {
auto readLock = _mutex.readLock();
bob->append("_id"_sd, QuerySettingsManager::kQuerySettingsClusterParameterName);
BSONArrayBuilder arrayBuilder(
bob->subarrayStart(QuerySettingsClusterParameterValue::kSettingsArrayFieldName));
for (auto&& item : getAllQueryShapeConfigurations_inlock(tenantId)) {
arrayBuilder.append(item.toBSON());
}
arrayBuilder.done();
bob->append(QuerySettingsClusterParameterValue::kClusterParameterTimeFieldName,
getClusterParameterTime_inlock(tenantId).asTimestamp());
}
void QuerySettingsClusterParameter::append(OperationContext* opCtx,
BSONObjBuilder* bob,
StringData name,
const boost::optional<TenantId>& tenantId) {
auto& querySettingsManager = QuerySettingsManager::get(getGlobalServiceContext());
querySettingsManager.appendQuerySettingsClusterParameterValue(bob, tenantId);
}
Status QuerySettingsClusterParameter::set(const BSONElement& newValueElement,
const boost::optional<TenantId>& tenantId) {
auto& querySettingsManager = QuerySettingsManager::get(getGlobalServiceContext());
auto newSettings = QuerySettingsClusterParameterValue::parse(
IDLParserContext("querySettingsParameterValue",
boost::none /* vts */,
tenantId,
SerializationContext::stateDefault()),
newValueElement.Obj());
auto& settingsArray = newSettings.getSettingsArray();
// TODO SERVER-97546 Remove PQS index hint sanitization.
querySettingsManager.sanitizeQuerySettingsHints(settingsArray);
size_t rejectCount = 0;
for (const auto& config : settingsArray) {
if (config.getSettings().getReject()) {
++rejectCount;
}
}
querySettingsServerStatusSection.record(
/* count */ static_cast<int>(settingsArray.size()),
/* size */ static_cast<int>(newValueElement.valuesize()),
/* numSettingsWithReject */ static_cast<int>(rejectCount));
querySettingsManager.setQueryShapeConfigurations(
std::move(settingsArray), newSettings.getClusterParameterTime(), tenantId);
return Status::OK();
}
Status QuerySettingsClusterParameter::reset(const boost::optional<TenantId>& tenantId) {
auto& querySettingsManager = QuerySettingsManager::get(getGlobalServiceContext());
querySettingsManager.removeAllQueryShapeConfigurations(tenantId);
return Status::OK();
}
LogicalTime QuerySettingsClusterParameter::getClusterParameterTime(
const boost::optional<TenantId>& tenantId) const {
auto& querySettingsManager = QuerySettingsManager::get(getGlobalServiceContext());
return querySettingsManager.getClusterParameterTime(tenantId);
}
// TODO SERVER-97546 Remove PQS index hint sanitization.
void QuerySettingsManager::sanitizeQuerySettingsHints(
std::vector<QueryShapeConfiguration>& queryShapeConfigs) {
if (_sanitizeQuerySettingsHintsFn) {
_sanitizeQuerySettingsHintsFn(queryShapeConfigs);
}
}
}; // namespace mongo::query_settings

View File

@ -94,38 +94,20 @@ struct VersionedQueryShapeConfigurations {
};
/**
* Class responsible for managing in-memory storage and fetching of query settings. The in-memory
* storage is eventually consistent with the query settings on other cluster nodes and is updated
* based on OpObserver call performed when executing setClusterParameter command.
*
* Query settings in-memory storage is maintained separately for each tenant. In dedicated
* environments the 'tenantId' argument passed to the methods must be boost::none.
*
* Query settings should only be retrieved through this class.
* Class responsible for managing in-memory storage and fetching of query settings. Query settings
* in-memory storage is maintained separately for each tenant. In dedicated environments the
* 'tenantId' argument passed to the methods must be boost::none.
*/
class QuerySettingsManager {
public:
static constexpr auto kQuerySettingsClusterParameterName = "querySettings"_sd;
QuerySettingsManager(
ServiceContext* service,
std::function<void(OperationContext*)> clusterParameterRefreshFn,
std::function<void(std::vector<QueryShapeConfiguration>&)> sanitizeQuerySettingsHintsFn)
: _clusterParameterRefreshFn(clusterParameterRefreshFn),
_sanitizeQuerySettingsHintsFn(sanitizeQuerySettingsHintsFn) {}
QuerySettingsManager() = default;
~QuerySettingsManager() = default;
QuerySettingsManager(QuerySettingsManager&&) = delete;
QuerySettingsManager(const QuerySettingsManager&) = delete;
QuerySettingsManager& operator=(QuerySettingsManager&&) = delete;
QuerySettingsManager& operator=(const QuerySettingsManager&) = delete;
static void create(
ServiceContext* service,
std::function<void(OperationContext*)> clusterParameterRefreshFn,
std::function<void(std::vector<QueryShapeConfiguration>&)> sanitizeQuerySettingsHintsFn);
static QuerySettingsManager& get(ServiceContext* service);
static QuerySettingsManager& get(OperationContext* opCtx);
/**
* Returns QuerySettings associated with a query which query shape hash is 'queryShapeHash' for
* the given tenant.
@ -145,15 +127,8 @@ public:
* Sets the QueryShapeConfigurations by replacing an existing VersionedQueryShapeConfigurations
* with the newly built one.
*/
void setQueryShapeConfigurations(std::vector<QueryShapeConfiguration>&& settings,
LogicalTime parameterClusterTime,
const boost::optional<TenantId>& tenantId);
/**
* Updates the QueryShapeConfiguration cache by calling the 'clusterParameterRefreshFn'. This
* can be a no-op if no 'clusterParameterRefreshFn' was provided in the constructor.
*/
void refreshQueryShapeConfigurations(OperationContext* opCtx);
void setAllQueryShapeConfigurations(QueryShapeConfigurationsWithTimestamp&& config,
const boost::optional<TenantId>& tenantId);
/**
* Removes all query settings documents for the given tenant.
@ -166,16 +141,6 @@ public:
*/
LogicalTime getClusterParameterTime(const boost::optional<TenantId>& tenantId) const;
/**
* Appends the QuerySettingsClusterParameterValue maintained as
* VersionedQueryShapeConfigurations for the given tenant.
*/
void appendQuerySettingsClusterParameterValue(BSONObjBuilder* bob,
const boost::optional<TenantId>& tenantId);
// TODO SERVER-97546 Remove PQS index hint sanitization.
void sanitizeQuerySettingsHints(std::vector<QueryShapeConfiguration>& queryShapeConfigs);
private:
std::vector<QueryShapeConfiguration> getAllQueryShapeConfigurations_inlock(
const boost::optional<TenantId>& tenantId) const;
@ -185,9 +150,6 @@ private:
mutable WriteRarelyRWMutex _mutex;
absl::flat_hash_map<boost::optional<TenantId>, VersionedQueryShapeConfigurations>
_tenantIdToVersionedQueryShapeConfigurationsMap;
std::function<void(OperationContext*)> _clusterParameterRefreshFn;
// TODO SERVER-97546 Remove PQS index hint sanitization.
std::function<void(std::vector<QueryShapeConfiguration>&)> _sanitizeQuerySettingsHintsFn;
};
}; // namespace query_settings
} // namespace mongo

View File

@ -27,31 +27,24 @@
* it in the license file.
*/
#include <algorithm>
#include <iterator>
#include <vector>
#include <boost/move/utility_core.hpp>
#include <boost/none.hpp>
#include <boost/optional/optional.hpp>
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <vector>
#include "mongo/base/string_data.h"
#include "mongo/bson/bsonmisc.h"
#include "mongo/bson/bsonobj.h"
#include "mongo/bson/oid.h"
#include "mongo/bson/simple_bsonobj_comparator.h"
#include "mongo/bson/timestamp.h"
#include "mongo/db/client.h"
#include "mongo/db/logical_time.h"
#include "mongo/db/pipeline/expression_context.h"
#include "mongo/db/query/index_hint.h"
#include "mongo/db/query/query_knobs_gen.h"
#include "mongo/db/query/query_settings/query_settings_cluster_parameter_gen.h"
#include "mongo/db/query/query_settings/query_settings_gen.h"
#include "mongo/db/query/query_settings/query_settings_manager.h"
#include "mongo/db/query/query_settings/query_settings_service.h"
#include "mongo/db/query/query_shape/query_shape.h"
#include "mongo/db/server_parameter.h"
#include "mongo/db/service_context_test_fixture.h"
#include "mongo/db/tenant_id.h"
@ -62,67 +55,19 @@
namespace mongo::query_settings {
namespace {
QueryShapeConfiguration makeQueryShapeConfiguration(const QuerySettings& settings,
QueryInstance query,
OperationContext* opCtx,
boost::optional<TenantId> tenantId) {
auto queryShapeHash = createRepresentativeInfo(opCtx, query, tenantId).queryShapeHash;
QueryShapeConfiguration result(queryShapeHash, settings);
result.setRepresentativeQuery(query);
return result;
bool operator==(const QuerySettings& lhs, const QuerySettings& rhs) {
return SimpleBSONObjComparator::kInstance.compare(lhs.toBSON(), rhs.toBSON()) == 0;
}
// QueryShapeConfiguration is not comparable, therefore comparing the corresponding
// BSONObj encoding.
void assertQueryShapeConfigurationsEquals(
const std::vector<QueryShapeConfiguration>& expectedQueryShapeConfigurations,
const std::vector<QueryShapeConfiguration>& actualQueryShapeConfigurations) {
std::vector<BSONObj> lhs, rhs;
std::transform(expectedQueryShapeConfigurations.begin(),
expectedQueryShapeConfigurations.end(),
std::back_inserter(lhs),
[](auto x) { return x.toBSON(); });
std::transform(actualQueryShapeConfigurations.begin(),
actualQueryShapeConfigurations.end(),
std::back_inserter(rhs),
[](auto x) { return x.toBSON(); });
std::sort(lhs.begin(), lhs.end(), SimpleBSONObjComparator::kInstance.makeLessThan());
std::sort(rhs.begin(), rhs.end(), SimpleBSONObjComparator::kInstance.makeLessThan());
ASSERT(std::equal(
lhs.begin(), lhs.end(), rhs.begin(), SimpleBSONObjComparator::kInstance.makeEqualTo()));
}
BSONObj makeSettingsClusterParameter(const BSONArray& settings) {
LogicalTime clusterParameterTime(Timestamp(113, 59));
return BSON("_id" << QuerySettingsManager::kQuerySettingsClusterParameterName
<< QuerySettingsClusterParameterValue::kSettingsArrayFieldName << settings
<< QuerySettingsClusterParameterValue::kClusterParameterTimeFieldName
<< clusterParameterTime.asTimestamp());
}
QuerySettings makeQuerySettings(const IndexHintSpecs& indexHints, bool setFramework = true) {
QuerySettings makeQuerySettings(const IndexHintSpecs& indexHints) {
QuerySettings settings;
if (!indexHints.empty()) {
settings.setIndexHints(indexHints);
}
if (setFramework) {
settings.setQueryFramework(mongo::QueryFrameworkControlEnum::kTrySbeEngine);
}
settings.setQueryFramework(mongo::QueryFrameworkControlEnum::kTrySbeEngine);
return settings;
}
auto makeDbName(StringData dbName) {
return DatabaseNameUtil::deserialize(
boost::none /*tenantId=*/, dbName, SerializationContext::stateDefault());
}
NamespaceSpec makeNsSpec(StringData collName) {
NamespaceSpec ns;
ns.setDb(makeDbName("testDbA"));
ns.setColl(collName);
return ns;
}
} // namespace
static auto const kSerializationContext =
@ -135,8 +80,7 @@ public:
static constexpr StringData kCollName = "exampleCol"_sd;
static constexpr StringData kDbName = "foo"_sd;
static std::vector<QueryShapeConfiguration> getExampleQueryShapeConfigurations(
OperationContext* opCtx, TenantId tenantId) {
std::vector<QueryShapeConfiguration> getExampleQueryShapeConfigurations(TenantId tenantId) {
NamespaceSpec ns;
ns.setDb(DatabaseNameUtil::deserialize(tenantId, kDbName, kSerializationContext));
ns.setColl(kCollName);
@ -146,28 +90,29 @@ public:
BSON("find" << kCollName << "$db" << kDbName << "filter" << BSON("a" << 2));
QueryInstance queryB =
BSON("find" << kCollName << "$db" << kDbName << "filter" << BSON("a" << BSONNULL));
return {makeQueryShapeConfiguration(settings, queryA, opCtx, tenantId),
makeQueryShapeConfiguration(settings, queryB, opCtx, tenantId)};
return {makeQueryShapeConfiguration(settings, queryA, tenantId),
makeQueryShapeConfiguration(settings, queryB, tenantId)};
}
QueryShapeConfiguration makeQueryShapeConfiguration(const QuerySettings& settings,
QueryInstance query,
boost::optional<TenantId> tenantId) {
auto queryShapeHash = createRepresentativeInfo(opCtx(), query, tenantId).queryShapeHash;
QueryShapeConfiguration result(queryShapeHash, settings);
result.setRepresentativeQuery(query);
return result;
}
void setUp() final {
QuerySettingsManager::create(
getServiceContext(), {}, query_settings::sanitizeQuerySettingsHints);
_opCtx = cc().makeOperationContext();
_expCtx = ExpressionContext::makeBlankExpressionContext(opCtx(), {NamespaceString()});
}
OperationContext* opCtx() {
return _opCtx.get();
}
boost::intrusive_ptr<ExpressionContext> expCtx() {
return _expCtx;
}
QuerySettingsManager& manager() {
return QuerySettingsManager::get(opCtx());
return _manager;
}
static NamespaceString nss(boost::optional<TenantId> tenantId) {
@ -180,331 +125,32 @@ public:
tenantId, kDbName, kCollName, kSerializationContext);
}
void assertTransformInvalidQuerySettings(
const std::list<std::pair<const IndexHintSpecs, const IndexHintSpecs>>&
listOfIndexHintSpecs) {
auto sp = std::make_unique<QuerySettingsClusterParameter>(
"querySettings", ServerParameterType::kClusterWide);
BSONArrayBuilder bob;
std::vector<QueryShapeConfiguration> expectedQueryShapeConfigurations;
size_t collIndex = 0;
// Create the 'querySettingsClusterParamValue' as BSON.
for (const auto& [initialIndexHintSpecs, expectedIndexHintSpecs] : listOfIndexHintSpecs) {
QueryInstance representativeQuery =
BSON("find" << "coll_" + std::to_string(collIndex) << "$db"
<< "foo");
bob.append(makeQueryShapeConfiguration(makeQuerySettings(initialIndexHintSpecs),
representativeQuery,
opCtx(),
/* tenantId */ boost::none)
.toBSON());
expectedQueryShapeConfigurations.emplace_back(
makeQueryShapeConfiguration(makeQuerySettings(expectedIndexHintSpecs),
representativeQuery,
opCtx(),
/* tenantId */ boost::none));
collIndex++;
}
// Set the cluster param value.
const auto clusterParamValues = BSON_ARRAY(makeSettingsClusterParameter(bob.arr()));
// Assert that parsing after transforming invalid settings (if any) works.
ASSERT_OK(sp->set(clusterParamValues.firstElement(), boost::none));
assertQueryShapeConfigurationsEquals(
expectedQueryShapeConfigurations,
manager()
.getAllQueryShapeConfigurations(/* tenantId */ boost::none)
.queryShapeConfigurations);
}
void assertSanitizeInvalidIndexHints(const IndexHintSpecs& initialSpec,
const IndexHintSpecs& expectedSpec) {
QueryInstance query = BSON("find" << "exampleColl"
<< "$db"
<< "foo");
auto initSettings = makeQueryShapeConfiguration(makeQuerySettings(initialSpec),
query,
opCtx(),
/* tenantId */ boost::none);
std::vector<QueryShapeConfiguration> queryShapeConfigs{initSettings};
const auto expectedSettings = makeQuerySettings(expectedSpec);
ASSERT_DOES_NOT_THROW(sanitizeQuerySettingsHints(queryShapeConfigs));
ASSERT_BSONOBJ_EQ(queryShapeConfigs[0].getSettings().toBSON(), expectedSettings.toBSON());
}
private:
ServiceContext::UniqueOperationContext _opCtx;
boost::intrusive_ptr<ExpressionContext> _expCtx;
QuerySettingsManager _manager;
};
TEST_F(QuerySettingsManagerTest, QuerySettingsClusterParameterSerialization) {
// Set query shape configuration.
QuerySettings settings;
settings.setQueryFramework(QueryFrameworkControlEnum::kTrySbeEngine);
QueryInstance query = BSON("find" << "exampleColl"
<< "$db"
<< "foo");
auto config = makeQueryShapeConfiguration(settings, query, opCtx(), /* tenantId */ boost::none);
LogicalTime clusterParameterTime(Timestamp(113, 59));
manager().setQueryShapeConfigurations(
{config}, clusterParameterTime, /* tenantId */ boost::none);
// Ensure the serialized parameter value contains 'settingsArray' with 'config' as value as well
// parameter id and clusterParameterTime.
BSONObjBuilder bob;
manager().appendQuerySettingsClusterParameterValue(&bob, /* tenantId */ boost::none);
ASSERT_BSONOBJ_EQ(
bob.done(),
BSON("_id" << QuerySettingsManager::kQuerySettingsClusterParameterName
<< QuerySettingsClusterParameterValue::kSettingsArrayFieldName
<< BSON_ARRAY(config.toBSON())
<< QuerySettingsClusterParameterValue::kClusterParameterTimeFieldName
<< clusterParameterTime.asTimestamp()));
}
TEST_F(QuerySettingsManagerTest, QuerySettingsSetAndReset) {
RAIIServerParameterControllerForTest multitenanyController("multitenancySupport", true);
LogicalTime firstWriteTime(Timestamp(1, 0)), secondWriteTime(Timestamp(2, 0));
TenantId tenantId(OID::fromTerm(1)), otherTenantId(OID::fromTerm(2));
auto firstConfig = getExampleQueryShapeConfigurations(opCtx(), tenantId)[0];
auto secondConfig = getExampleQueryShapeConfigurations(opCtx(), otherTenantId)[1];
// Ensure that the maintained in-memory query shape configurations equal to the
// configurations specified in the parameter for both tenants.
manager().setQueryShapeConfigurations({firstConfig}, firstWriteTime, tenantId);
manager().setQueryShapeConfigurations({firstConfig}, firstWriteTime, otherTenantId);
assertQueryShapeConfigurationsEquals(
{firstConfig}, manager().getAllQueryShapeConfigurations(tenantId).queryShapeConfigurations);
assertQueryShapeConfigurationsEquals(
{firstConfig},
manager().getAllQueryShapeConfigurations(otherTenantId).queryShapeConfigurations);
ASSERT_EQ(manager().getClusterParameterTime(tenantId), firstWriteTime);
ASSERT_EQ(manager().getClusterParameterTime(otherTenantId), firstWriteTime);
// Update query settings for tenant with 'tenantId'. Ensure its query shape configurations and
// parameter cluster time are updated accordingly.
manager().setQueryShapeConfigurations({secondConfig}, secondWriteTime, tenantId);
assertQueryShapeConfigurationsEquals(
{secondConfig},
manager().getAllQueryShapeConfigurations(tenantId).queryShapeConfigurations);
assertQueryShapeConfigurationsEquals(
{firstConfig},
manager().getAllQueryShapeConfigurations(otherTenantId).queryShapeConfigurations);
ASSERT_EQ(manager().getClusterParameterTime(tenantId), secondWriteTime);
ASSERT_EQ(manager().getClusterParameterTime(otherTenantId), firstWriteTime);
// Reset the parameter value and ensure that the in-memory storage is cleared for tenant with
// 'tenantId'.
manager().removeAllQueryShapeConfigurations(tenantId);
assertQueryShapeConfigurationsEquals(
{}, manager().getAllQueryShapeConfigurations(tenantId).queryShapeConfigurations);
// Attempt to remove QueryShapeConfigurations for a tenant that does not have any.
manager().removeAllQueryShapeConfigurations(tenantId);
assertQueryShapeConfigurationsEquals(
{}, manager().getAllQueryShapeConfigurations(tenantId).queryShapeConfigurations);
// Verify that QueryShapeConfigurations for tenant with 'otherTenantId' were not be affected.
assertQueryShapeConfigurationsEquals(
{firstConfig},
manager().getAllQueryShapeConfigurations(otherTenantId).queryShapeConfigurations);
ASSERT_EQ(manager().getClusterParameterTime(tenantId), LogicalTime::kUninitialized);
ASSERT_EQ(manager().getClusterParameterTime(otherTenantId), firstWriteTime);
}
TEST_F(QuerySettingsManagerTest, QuerySettingsLookup) {
using Result = boost::optional<QuerySettings>;
RAIIServerParameterControllerForTest multitenanyController("multitenancySupport", true);
TenantId tenantId1(OID::fromTerm(1));
TenantId tenantId2(OID::fromTerm(2));
// Helper function for ensuring that two
// 'QuerySettingsManager::getQuerySettingsForQueryShapeHash()' results are identical.
auto assertResultsEq = [](Result r0, Result r1) {
// Ensure that either both results are there, or both are missing.
ASSERT_EQ(r0.has_value(), r1.has_value());
// Early exit if any of them are missing.
if (!r0.has_value() || !r1.has_value()) {
return;
}
ASSERT_BSONOBJ_EQ(r0->toBSON(), r1->toBSON());
};
TenantId tenantId(OID::fromTerm(1));
auto configs = getExampleQueryShapeConfigurations(opCtx(), tenantId);
manager().setQueryShapeConfigurations(
std::vector<QueryShapeConfiguration>(configs), LogicalTime(), tenantId);
auto configs = getExampleQueryShapeConfigurations(tenantId1);
manager().setAllQueryShapeConfigurations({{configs}, LogicalTime()}, tenantId1);
// Ensure QuerySettingsManager returns boost::none when QuerySettings are not found.
assertResultsEq(
manager().getQuerySettingsForQueryShapeHash(query_shape::QueryShapeHash(), tenantId),
boost::none);
ASSERT_EQ(manager().getQuerySettingsForQueryShapeHash(query_shape::QueryShapeHash(), tenantId1),
boost::none);
// Ensure QuerySettingsManager returns a valid QuerySettings on lookup.
assertResultsEq(
manager().getQuerySettingsForQueryShapeHash(configs[1].getQueryShapeHash(), tenantId),
ASSERT_EQ(
manager().getQuerySettingsForQueryShapeHash(configs[1].getQueryShapeHash(), tenantId1),
configs[1].getSettings());
// Ensure QuerySettingsManager does not return a valid QuerySettings of 'tenantId1', when
// performing lookup as 'tenantId2'.
ASSERT_EQ(
manager().getQuerySettingsForQueryShapeHash(configs[1].getQueryShapeHash(), tenantId2),
boost::none);
}
/**
* Tests that valid index hint specs are the same before and after index hint sanitization.
*/
TEST_F(QuerySettingsManagerTest, ValidIndexHintsAreTheSameBeforeAndAfterSanitization) {
IndexHintSpecs indexHintSpec{IndexHintSpec(
makeNsSpec("testCollA"_sd), {IndexHint(BSON("a" << 1)), IndexHint(BSON("b" << -1.0))})};
assertSanitizeInvalidIndexHints(indexHintSpec, indexHintSpec);
}
/**
* Tests that invalid key-pattern are removed after sanitization.
*/
TEST_F(QuerySettingsManagerTest, InvalidKeyPatternIndexesAreRemovedAfterSanitization) {
IndexHintSpecs indexHintSpec{IndexHintSpec(makeNsSpec("testCollA"_sd),
{IndexHint(BSON("a" << 1 << "c"
<< "invalid")),
IndexHint(BSON("b" << -1.0))})};
IndexHintSpecs expectedHintSpec{
IndexHintSpec(makeNsSpec("testCollA"_sd), {IndexHint(BSON("b" << -1.0))})};
assertSanitizeInvalidIndexHints(indexHintSpec, expectedHintSpec);
}
/**
* Same as the above test but with more complex examples.
*/
TEST_F(QuerySettingsManagerTest, InvalidKeyPatternIndexesAreRemovedAfterSanitizationComplex) {
IndexHintSpecs indexHintSpec{IndexHintSpec(makeNsSpec("testCollA"_sd),
{
IndexHint(BSON("a" << 1 << "c"
<< "invalid")),
IndexHint(BSON("b" << -1.0)),
IndexHint(BSON("c" << -2.0 << "b" << 4)),
IndexHint("index_name"),
IndexHint(BSON("$natural" << 1)),
IndexHint(BSON("$natural" << -1 << "a" << 2)),
IndexHint(BSON("a" << -1 << "$natural" << 1)),
})};
IndexHintSpecs expectedHintSpec{IndexHintSpec(makeNsSpec("testCollA"_sd),
{IndexHint(BSON("b" << -1.0)),
IndexHint(BSON("c" << -2.0 << "b" << 4)),
IndexHint("index_name")})};
assertSanitizeInvalidIndexHints(indexHintSpec, expectedHintSpec);
}
/**
* Tests that invalid key-pattern are removed after sanitization and the resulted index hints are
* empty.
*/
TEST_F(QuerySettingsManagerTest, InvalidKeyPatternIndexesAreRemovedAfterSanitizationEmptyHints) {
IndexHintSpecs indexHintSpec{IndexHintSpec(makeNsSpec("testCollA"_sd),
{
IndexHint(BSON("a" << 1 << "c"
<< "invalid")),
})};
IndexHintSpecs expectedHintSpec;
assertSanitizeInvalidIndexHints(indexHintSpec, expectedHintSpec);
}
/**
* Sets the value of QuerySettingsClusterParameter and asserts the resulting index hint spec is
* sanitized.
*/
TEST_F(QuerySettingsManagerTest, SetValidClusterParameterAndAssertResultIsSanitized) {
IndexHintSpecs indexHintSpec{IndexHintSpec(
makeNsSpec("testCollA"), {IndexHint(BSON("a" << 1)), IndexHint(BSON("b" << -1.0))})};
assertTransformInvalidQuerySettings({{indexHintSpec, indexHintSpec}});
}
/**
* Same as above test, but assert that the validation would fail with invalid key-pattern indexes.
*/
TEST_F(QuerySettingsManagerTest, SetClusterParameterAndAssertResultIsSanitized) {
IndexHintSpecs initialIndexHintSpec{
IndexHintSpec(makeNsSpec("testCollA"_sd),
{IndexHint(BSON("a" << 1.0)), IndexHint(BSON("b" << "-1.0"))}),
IndexHintSpec(makeNsSpec("testCollB"_sd),
{IndexHint(BSON("a" << 2)), IndexHint(BSONObj{})})};
IndexHintSpecs expectedIndexHintSpec{
IndexHintSpec(makeNsSpec("testCollA"_sd), {IndexHint(BSON("a" << 1))}),
IndexHintSpec(makeNsSpec("testCollB"_sd), {IndexHint(BSON("a" << 2))})};
ASSERT_THROWS_CODE(
validateQuerySettings(makeQuerySettings(initialIndexHintSpec)), DBException, 9646001);
assertTransformInvalidQuerySettings({{initialIndexHintSpec, expectedIndexHintSpec}});
}
/**
* Same as above test, but with multiple QuerySettings set.
*/
TEST_F(QuerySettingsManagerTest, SetClusterParameterAndAssertResultIsSanitizedMultipleQS) {
// First pair of index hints.
IndexHintSpecs initialIndexHintSpec1{
IndexHintSpec(makeNsSpec("testCollA"_sd),
{IndexHint(BSON("a" << 2)), IndexHint(BSONObj{})}),
IndexHintSpec(makeNsSpec("testCollB"_sd),
{IndexHint(BSON("a" << 1.0)),
IndexHint(BSON("b" << 3)),
IndexHint("a_1"),
IndexHint(NaturalOrderHint(NaturalOrderHint::Direction::kForward))})};
IndexHintSpecs expectedIndexHintSpec1{
IndexHintSpec(makeNsSpec("testCollA"_sd), {IndexHint(BSON("a" << 2.0))}),
IndexHintSpec(makeNsSpec("testCollB"_sd),
{IndexHint(BSON("a" << 1.0)),
IndexHint(BSON("b" << 3)),
IndexHint("a_1"),
IndexHint(NaturalOrderHint(NaturalOrderHint::Direction::kForward))})};
// Second pair of index hints.
IndexHintSpecs initialIndexHintSpec2{
IndexHintSpec(makeNsSpec("testCollC"_sd),
{IndexHint(BSON("a" << 1 << "$natural" << 1)), IndexHint(BSONObj{})}),
IndexHintSpec(makeNsSpec("testCollD"_sd),
{IndexHint(BSON("b" << "some-string"
<< "a" << 1)),
IndexHint(BSONObj{})})};
ASSERT_THROWS_CODE(
validateQuerySettings(makeQuerySettings(initialIndexHintSpec1)), DBException, 9646000);
ASSERT_THROWS_CODE(
validateQuerySettings(makeQuerySettings(initialIndexHintSpec2)), DBException, 9646001);
assertTransformInvalidQuerySettings(
{{initialIndexHintSpec1, expectedIndexHintSpec1}, {initialIndexHintSpec2, {}}});
}
/**
* Tests that query settings with invalid key-pattern index hints leads to no query settings after
* sanitization.
*/
TEST_F(QuerySettingsManagerTest, SetClusterParameterInvalidQSSanitization) {
auto sp = std::make_unique<QuerySettingsClusterParameter>("querySettings",
ServerParameterType::kClusterWide);
IndexHintSpecs initialIndexHintSpec{
IndexHintSpec(makeNsSpec("testCollC"_sd),
{IndexHint(BSON("a" << 1 << "$natural" << 1)), IndexHint(BSONObj{})}),
IndexHintSpec(makeNsSpec("testCollD"_sd),
{IndexHint(BSON("b" << "some-string"
<< "a" << 1))})};
const auto querySettings = makeQuerySettings(initialIndexHintSpec, false);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 9646001);
// Create the 'querySettingsClusterParamValue' as BSON.
const auto config = makeQueryShapeConfiguration(querySettings,
BSON("find" << "bar"
<< "$db"
<< "foo"),
opCtx(),
/* tenantId */ boost::none);
// Assert that parsing after transforming invalid settings (if any) works.
const auto clusterParamValues =
BSON_ARRAY(makeSettingsClusterParameter(BSON_ARRAY(config.toBSON())));
ASSERT_OK(sp->set(clusterParamValues.firstElement(), boost::none));
auto res = manager().getQuerySettingsForQueryShapeHash(config.getQueryShapeHash(),
/* tenantId */ boost::none);
ASSERT(!res.has_value());
ASSERT(manager()
.getAllQueryShapeConfigurations(/* tenantId */ boost::none)
.queryShapeConfigurations.empty());
}
} // namespace mongo::query_settings

View File

@ -75,6 +75,11 @@ const stdx::unordered_set<StringData, StringMapHasher> rejectionIncompatibleStag
"$operationMetrics"_sd,
};
const auto getQuerySettingsService =
ServiceContext::declareDecoration<std::unique_ptr<QuerySettingsService>>();
static constexpr auto kQuerySettingsClusterParameterName = "querySettings"_sd;
MONGO_FAIL_POINT_DEFINE(allowAllSetQuerySettings);
/**
@ -380,6 +385,149 @@ void sanitizeKeyPatternIndexHints(QueryShapeConfiguration& queryShapeItem) {
}
} // namespace
class QuerySettingsRouterService : public QuerySettingsService {
public:
QuerySettingsRouterService() : QuerySettingsService() {}
QueryShapeConfigurationsWithTimestamp getAllQueryShapeConfigurations(
const boost::optional<TenantId>& tenantId) const final {
return _manager.getAllQueryShapeConfigurations(tenantId);
}
void setAllQueryShapeConfigurations(QueryShapeConfigurationsWithTimestamp&& config,
const boost::optional<TenantId>& tenantId) final {
_manager.setAllQueryShapeConfigurations(std::move(config), tenantId);
}
void removeAllQueryShapeConfigurations(const boost::optional<TenantId>& tenantId) final {
_manager.removeAllQueryShapeConfigurations(tenantId);
}
LogicalTime getClusterParameterTime(const boost::optional<TenantId>& tenantId) const final {
return _manager.getClusterParameterTime(tenantId);
}
QuerySettings lookupQuerySettingsWithRejectionCheck(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const query_shape::DeferredQueryShape& deferredShape,
const NamespaceString& nss,
const boost::optional<QuerySettings>& querySettingsFromOriginalCommand =
boost::none) const override {
QuerySettings settings = [&]() {
try {
// No query settings lookup for IDHACK queries.
if (expCtx->isIdHackQuery()) {
return QuerySettings();
}
// No query settings for queries with encryption information.
if (expCtx->isFleQuery()) {
return QuerySettings();
}
// No query settings lookup on internal dbs or system collections in user dbs.
if (nss.isOnInternalDb() || nss.isSystem()) {
return QuerySettings();
}
// Force shape computation and early exit with empty settings if shape computation
// has failed.
const auto& shapePtr = deferredShape();
if (!shapePtr.isOK()) {
return QuerySettings();
}
// Compute the QueryShapeHash and store it in CurOp.
auto* opCtx = expCtx->getOperationContext();
QueryShapeHash hash = shapePtr.getValue()->sha256Hash(opCtx, kSerializationContext);
setQueryShapeHash(opCtx, hash);
// Return the found query settings or an empty one.
return _manager.getQuerySettingsForQueryShapeHash(hash, nss.tenantId())
.get_value_or({});
} catch (const DBException& ex) {
LOGV2_WARNING_OPTIONS(10153400,
{logv2::LogComponent::kQuery},
"Failed to perform query settings lookup",
"error"_attr = ex.toString());
return QuerySettings();
}
}();
// Fail the current command, if 'reject: true' flag is present.
failIfRejectedBySettings(expCtx, settings);
return settings;
}
void refreshQueryShapeConfigurations(OperationContext* opCtx) override {
// QuerySettingsManager modifies a cluster-wide parameter and thus a refresh of
// the parameter after that modification should observe results of preceeding
// writes.
const bool kEnsureReadYourWritesConsistency = true;
auto refreshStatus = ClusterServerParameterRefresher::get(opCtx)->refreshParameters(
opCtx, kEnsureReadYourWritesConsistency);
if (!refreshStatus.isOK()) {
LOGV2_WARNING(8472500,
"Error occurred when fetching the latest version of query settings",
"error_code"_attr = refreshStatus.code(),
"reason"_attr = refreshStatus.reason());
}
}
private:
QuerySettingsManager _manager;
};
class QuerySettingsShardService : public QuerySettingsRouterService {
public:
QuerySettingsShardService() : QuerySettingsRouterService() {}
QuerySettings lookupQuerySettingsWithRejectionCheck(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const query_shape::DeferredQueryShape& queryShape,
const NamespaceString& nss,
const boost::optional<QuerySettings>& querySettingsFromOriginalCommand =
boost::none) const override {
// No query settings lookup for IDHACK queries.
if (expCtx->isIdHackQuery()) {
return QuerySettings();
}
auto* opCtx = expCtx->getOperationContext();
if (requestComesFromRouterOrSentDirectlyToShard(opCtx->getClient()) ||
querySettingsFromOriginalCommand.has_value()) {
return querySettingsFromOriginalCommand.get_value_or(QuerySettings());
}
// The underlying shard does not belong to a sharded cluster, therefore proceed by
// performing the lookup as a router.
return QuerySettingsRouterService::lookupQuerySettingsWithRejectionCheck(
expCtx, queryShape, nss);
}
void refreshQueryShapeConfigurations(OperationContext* opCtx) override {
/* no-op */
}
};
QuerySettingsService& QuerySettingsService::get(ServiceContext* service) {
return *getQuerySettingsService(service);
}
QuerySettingsService& QuerySettingsService::get(OperationContext* opCtx) {
return *getQuerySettingsService(opCtx->getServiceContext());
}
std::string QuerySettingsService::getQuerySettingsClusterParameterName() {
return kQuerySettingsClusterParameterName.toString();
}
const stdx::unordered_set<StringData, StringMapHasher>&
QuerySettingsService::getRejectionIncompatibleStages() {
return rejectionIncompatibleStages;
};
RepresentativeQueryInfo createRepresentativeInfo(OperationContext* opCtx,
const BSONObj& cmd,
const boost::optional<TenantId>& tenantId) {
@ -397,81 +545,25 @@ RepresentativeQueryInfo createRepresentativeInfo(OperationContext* opCtx,
}
void initializeForRouter(ServiceContext* serviceContext) {
QuerySettingsManager::create(
serviceContext,
[](OperationContext* opCtx) {
// QuerySettingsManager modifies a cluster-wide parameter and thus a refresh of the
// parameter after that modification should observe results of preceeding writes.
const bool kEnsureReadYourWritesConsistency = true;
auto refreshStatus = ClusterServerParameterRefresher::get(opCtx)->refreshParameters(
opCtx, kEnsureReadYourWritesConsistency);
if (!refreshStatus.isOK()) {
LOGV2_WARNING(8472500,
"Error occurred when fetching the latest version of query settings",
"error_code"_attr = refreshStatus.code(),
"reason"_attr = refreshStatus.reason());
}
},
sanitizeQuerySettingsHints);
getQuerySettingsService(serviceContext) = std::make_unique<QuerySettingsRouterService>();
}
void initializeForShard(ServiceContext* serviceContext) {
query_settings::QuerySettingsManager::create(serviceContext, {}, sanitizeQuerySettingsHints);
getQuerySettingsService(serviceContext) = std::make_unique<QuerySettingsShardService>();
}
void initializeForTest(ServiceContext* serviceContext) {
query_settings::QuerySettingsManager::create(serviceContext, {}, {});
initializeForShard(serviceContext);
}
QuerySettings lookupQuerySettingsWithRejectionCheckOnRouter(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const query_shape::DeferredQueryShape& deferredShape,
const NamespaceString& nss) {
QuerySettings settings = [&]() {
try {
// No query settings lookup for IDHACK queries.
if (expCtx->isIdHackQuery()) {
return QuerySettings();
}
// No query settings for queries with encryption information.
if (expCtx->isFleQuery()) {
return QuerySettings();
}
// No query settings lookup on internal dbs or system collections in user dbs.
if (nss.isOnInternalDb() || nss.isSystem()) {
return QuerySettings();
}
// Force shape computation and early exit with empty settings if shape computation has
// failed.
const auto& shapePtr = deferredShape();
if (!shapePtr.isOK()) {
return QuerySettings();
}
// Compute the QueryShapeHash and store it in CurOp.
auto* opCtx = expCtx->getOperationContext();
QueryShapeHash hash = shapePtr.getValue()->sha256Hash(opCtx, kSerializationContext);
setQueryShapeHash(opCtx, hash);
// Return the found query settings or an empty one.
auto& manager = QuerySettingsManager::get(opCtx);
return manager.getQuerySettingsForQueryShapeHash(hash, nss.tenantId()).get_value_or({});
} catch (const DBException& ex) {
LOGV2_WARNING_OPTIONS(10153400,
{logv2::LogComponent::kQuery},
"Failed to perform query settings lookup",
"error"_attr = ex.toString());
return QuerySettings();
}
}();
// Fail the current command, if 'reject: true' flag is present.
failIfRejectedBySettings(expCtx, settings);
return settings;
auto* service =
getQuerySettingsService(expCtx->getOperationContext()->getServiceContext()).get();
dassert(dynamic_cast<QuerySettingsRouterService*>(service));
return service->lookupQuerySettingsWithRejectionCheck(expCtx, deferredShape, nss);
}
QuerySettings lookupQuerySettingsWithRejectionCheckOnShard(
@ -479,49 +571,30 @@ QuerySettings lookupQuerySettingsWithRejectionCheckOnShard(
const query_shape::DeferredQueryShape& deferredShape,
const NamespaceString& nss,
const boost::optional<QuerySettings>& querySettingsFromOriginalCommand) {
// No query settings lookup for IDHACK queries.
if (expCtx->isIdHackQuery()) {
return QuerySettings();
}
auto* opCtx = expCtx->getOperationContext();
if (requestComesFromRouterOrSentDirectlyToShard(opCtx->getClient()) ||
querySettingsFromOriginalCommand.has_value()) {
return querySettingsFromOriginalCommand.get_value_or(QuerySettings());
}
// The underlying shard does not belong to a sharded cluster, therefore proceed by performing
// the lookup as a router.
return lookupQuerySettingsWithRejectionCheckOnRouter(expCtx, deferredShape, nss);
auto* service =
getQuerySettingsService(expCtx->getOperationContext()->getServiceContext()).get();
dassert(dynamic_cast<QuerySettingsShardService*>(service));
return service->lookupQuerySettingsWithRejectionCheck(
expCtx, deferredShape, nss, querySettingsFromOriginalCommand);
}
QueryShapeConfigurationsWithTimestamp getAllQueryShapeConfigurations(
OperationContext* opCtx, const boost::optional<TenantId>& tenantId) {
return QuerySettingsManager::get(opCtx).getAllQueryShapeConfigurations(tenantId);
}
void setAllQueryShapeConfigurations(OperationContext* opCtx,
QueryShapeConfigurationsWithTimestamp&& config,
const boost::optional<TenantId>& tenantId) {
QuerySettingsManager::get(opCtx).setQueryShapeConfigurations(
std::move(config.queryShapeConfigurations), config.clusterParameterTime, tenantId);
return QuerySettingsService::get(opCtx).getAllQueryShapeConfigurations(tenantId);
}
void refreshQueryShapeConfigurations(OperationContext* opCtx) {
QuerySettingsManager::get(opCtx).refreshQueryShapeConfigurations(opCtx);
QuerySettingsService::get(opCtx).refreshQueryShapeConfigurations(opCtx);
}
std::string getQuerySettingsClusterParameterName() {
return QuerySettingsManager::kQuerySettingsClusterParameterName.toString();
return kQuerySettingsClusterParameterName.toString();
}
const stdx::unordered_set<StringData, StringMapHasher>& getRejectionIncompatibleStages() {
return rejectionIncompatibleStages;
};
bool canPipelineBeRejected(const std::vector<BSONObj>& pipeline) {
return pipeline.empty() ||
!getRejectionIncompatibleStages().contains(pipeline.at(0).firstElementFieldName());
!QuerySettingsService::getRejectionIncompatibleStages().contains(
pipeline.at(0).firstElementFieldName());
}
bool allowQuerySettingsFromClient(Client* client) {
@ -543,7 +616,7 @@ bool isDefault(const QuerySettings& settings) {
return !(settings.getQueryFramework() || settings.getIndexHints() || settings.getReject());
}
void validateQuerySettings(const QuerySettings& querySettings) {
void QuerySettingsService::validateQuerySettings(const QuerySettings& querySettings) const {
// Validates that the settings field for query settings is not empty.
uassert(7746604,
"the resulting settings cannot be empty or contain only default values",
@ -552,8 +625,8 @@ void validateQuerySettings(const QuerySettings& querySettings) {
validateQuerySettingsIndexHints(querySettings.getIndexHints());
}
void validateQueryCompatibleWithAnyQuerySettings(
const RepresentativeQueryInfo& representativeQueryInfo) {
void QuerySettingsService::validateQueryCompatibleWithAnyQuerySettings(
const RepresentativeQueryInfo& representativeQueryInfo) const {
if (MONGO_unlikely(allowAllSetQuerySettings.shouldFail())) {
return;
}
@ -573,8 +646,8 @@ void validateQueryCompatibleWithAnyQuerySettings(
!representativeQueryInfo.isIdHackQuery);
}
void validateQueryCompatibleWithQuerySettings(
const RepresentativeQueryInfo& representativeQueryInfo, const QuerySettings& settings) {
void QuerySettingsService::validateQueryCompatibleWithQuerySettings(
const RepresentativeQueryInfo& representativeQueryInfo, const QuerySettings& settings) const {
if (MONGO_unlikely(allowAllSetQuerySettings.shouldFail())) {
return;
}
@ -584,7 +657,7 @@ void validateQueryCompatibleWithQuerySettings(
!(settings.getReject() && representativeQueryInfo.systemStage.has_value()));
}
void simplifyQuerySettings(QuerySettings& settings) {
void QuerySettingsService::simplifyQuerySettings(QuerySettings& settings) const {
// If reject is present, but is false, set to an empty optional.
if (settings.getReject().has_value() && !settings.getReject()) {
settings.setReject({});
@ -610,8 +683,9 @@ void simplifyQuerySettings(QuerySettings& settings) {
}
// TODO SERVER-97546 Remove PQS index hint sanitization.
void sanitizeQuerySettingsHints(std::vector<QueryShapeConfiguration>& queryShapeConfigs) {
std::erase_if(queryShapeConfigs, [](QueryShapeConfiguration& queryShapeItem) {
void QuerySettingsService::sanitizeQuerySettingsHints(
std::vector<QueryShapeConfiguration>& queryShapeConfigs) const {
std::erase_if(queryShapeConfigs, [&](QueryShapeConfiguration& queryShapeItem) {
auto& settings = queryShapeItem.getSettings();
sanitizeKeyPatternIndexHints(queryShapeItem);
simplifyQuerySettings(settings);
@ -626,5 +700,4 @@ void sanitizeQuerySettingsHints(std::vector<QueryShapeConfiguration>& queryShape
return false;
});
}
} // namespace mongo::query_settings

View File

@ -70,6 +70,126 @@ RepresentativeQueryInfo createRepresentativeInfo(OperationContext* opCtx,
const QueryInstance& queryInstance,
const boost::optional<TenantId>& tenantId);
class QuerySettingsService {
public:
/**
* Gets the instance of the class using the service context.
*/
static QuerySettingsService& get(ServiceContext* serviceContext);
/**
* Gets the instance of the class using the operation context.
*/
static QuerySettingsService& get(OperationContext* opCtx);
/**
* Returns the name of the cluster parameter that stores QuerySettings for all QueryShapes.
*/
static std::string getQuerySettingsClusterParameterName();
/**
* Returns a set of system and administrative aggregation pipeline stages that, if used as the
* initial stage, prevent the query from being rejected via query settings.
*
* Query settings module is responsible for maintaining the information about what aggregation
* stages can be rejected.
*/
static const stdx::unordered_set<StringData, StringMapHasher>& getRejectionIncompatibleStages();
virtual ~QuerySettingsService() = default;
/**
* Returns the appropriate QuerySettings:
*
* - On router and shard in replica set deployment performs QuerySettings lookup for a specified
* 'queryShape'. If no settings are found or if the 'queryShape' is ineligible (e.g., IDHACK
* queries), returns empty QuerySettings. If the QuerySettings include 'reject: true' and is not
* run in explain, a uassert is thrown with the QueryRejectedBySettings error code, rejecting
* the query. Additionally, records the QueryShapeHash within the CurOp
*
* - On shard in sharded cluster returns 'querySettingsFromOriginalCommand'. This corresponds to
* the QuerySettings looked up on the router and passed to shards as part of the command.
* Rejection check is not performed here, as queries with 'reject: true' in their QuerySettings
* would already have been rejected by the router.
*/
virtual QuerySettings lookupQuerySettingsWithRejectionCheck(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const query_shape::DeferredQueryShape& queryShape,
const NamespaceString& nss,
const boost::optional<QuerySettings>& querySettingsFromOriginalCommand =
boost::none) const = 0;
/**
* Returns all the query shape configurations and the timestamp of the last modification.
*/
virtual QueryShapeConfigurationsWithTimestamp getAllQueryShapeConfigurations(
const boost::optional<TenantId>& tenantId) const = 0;
/**
* Sets all the query shape configurations with the given timestamp.
*/
virtual void setAllQueryShapeConfigurations(
QueryShapeConfigurationsWithTimestamp&& queryShapeConfigurations,
const boost::optional<TenantId>& tenantId) = 0;
/**
* Removes all query shape configurations.
*/
virtual void removeAllQueryShapeConfigurations(const boost::optional<TenantId>& tenantId) = 0;
/**
* Returns the LogicalTime of the 'querySettings' cluster parameter.
*/
virtual LogicalTime getClusterParameterTime(
const boost::optional<TenantId>& tenantId) const = 0;
/**
* Refreshes the local copy of all QueryShapeConfiguration by fetching the latest version from
* the configsvr. Is a no-op when called on shard.
*/
virtual void refreshQueryShapeConfigurations(OperationContext* opCtx) = 0;
/**
* Validates that 'querySettings' do not have:
* - empty settings or settings with default values
* - index hints specified without namespace information
* - index hints specified for the same namespace more than once
*
* Throws a uassert if compatibility checks fail, indicating that 'querySettings' cannot be set.
*/
void validateQuerySettings(const QuerySettings& querySettings) const;
/**
* Validates that QuerySettings can be applied to the query represented by 'queryInfo'.
* Throws a uassert if compatibility checks fail, indicating that 'querySettings' cannot be set.
*/
void validateQueryCompatibleWithAnyQuerySettings(
const RepresentativeQueryInfo& queryInfo) const;
/**
* Validates that 'querySettings' can be applied to the query represented by 'queryInfo'.
* Throws a uassert if compatibility checks fail, indicating that 'querySettings' cannot be set.
*/
void validateQueryCompatibleWithQuerySettings(const RepresentativeQueryInfo& queryInfo,
const QuerySettings& querySettings) const;
/**
* Simplifies 'querySettings' in-place by:
* - resetting the 'reject' field to boost::none if it contains a false value
* - removing index hints that specify empty 'allowedIndexes', potentially resetting
* 'indexHints' to boost::none if all 'allowedIndexes' are empty.
*/
void simplifyQuerySettings(QuerySettings& querySettings) const;
/**
* Sanitizes the 'queryShapeConfigurations' removing those hints that contain invalid index key
* pattern. In case the underlying query settings object contains only the default settings, the
* corresponding QueryShapeConfiguration is removed.
*/
void sanitizeQuerySettingsHints(
std::vector<QueryShapeConfiguration>& queryShapeConfigurations) const;
};
void initializeForRouter(ServiceContext* serviceContext);
void initializeForShard(ServiceContext* serviceContext);
void initializeForTest(ServiceContext* serviceContext);
@ -112,14 +232,6 @@ QuerySettings lookupQuerySettingsWithRejectionCheckOnShard(
QueryShapeConfigurationsWithTimestamp getAllQueryShapeConfigurations(
OperationContext* opCtx, const boost::optional<TenantId>& tenantId);
/**
* Sets all the query shape configurations with the given timestamp.
*/
void setAllQueryShapeConfigurations(
OperationContext* opCtx,
QueryShapeConfigurationsWithTimestamp&& queryShapeConfigurations,
const boost::optional<TenantId>& tenantId);
/**
* Refreshes the local copy of all QueryShapeConfiguration by fetching the latest version from the
* configsvr. Is a no-op when called on shard.
@ -131,15 +243,6 @@ void refreshQueryShapeConfigurations(OperationContext* opCtx);
*/
std::string getQuerySettingsClusterParameterName();
/**
* Returns a set of system and administrative aggregation pipeline stages that, if used as the
* initial stage, prevent the query from being rejected via query settings.
*
* Query settings module is responsible for maintaining the information about what aggregation
* stages can be rejected.
*/
const stdx::unordered_set<StringData, StringMapHasher>& getRejectionIncompatibleStages();
/**
* Returns true if the aggregation pipeline 'pipeline' does not start with rejection incompatible
* stage, and therefore can be rejected.
@ -156,42 +259,4 @@ bool allowQuerySettingsFromClient(Client* client);
* Returns true if given QuerySettings instance contains only default values.
*/
bool isDefault(const QuerySettings& querySettings);
/**
* Validates that 'querySettings' do not have:
* - empty settings or settings with default values
* - index hints specified without namespace information
* - index hints specified for the same namespace more than once
*
* Throws a uassert if compatibility checks fail, indicating that 'querySettings' cannot be set.
*/
void validateQuerySettings(const QuerySettings& querySettings);
/**
* Validates that QuerySettings can be applied to the query represented by 'queryInfo'.
* Throws a uassert if compatibility checks fail, indicating that 'querySettings' cannot be set.
*/
void validateQueryCompatibleWithAnyQuerySettings(const RepresentativeQueryInfo& queryInfo);
/**
* Validates that 'querySettings' can be applied to the query represented by 'queryInfo'.
* Throws a uassert if compatibility checks fail, indicating that 'querySettings' cannot be set.
*/
void validateQueryCompatibleWithQuerySettings(const RepresentativeQueryInfo& queryInfo,
const QuerySettings& querySettings);
/**
* Simplifies 'querySettings' in-place by:
* - resetting the 'reject' field to boost::none if it contains a false value
* - removing index hints that specify empty 'allowedIndexes', potentially resetting 'indexHints'
* to boost::none if all 'allowedIndexes' are empty.
*/
void simplifyQuerySettings(QuerySettings& querySettings);
/**
* Sanitizes the 'queryShapeConfigurations' removing those hints that contain invalid index key
* pattern. In case the underlying query settings object contains only the default settings, the
* corresponding QueryShapeConfiguration is removed.
*/
void sanitizeQuerySettingsHints(std::vector<QueryShapeConfiguration>& queryShapeConfigurations);
} // namespace mongo::query_settings

View File

@ -42,6 +42,7 @@
#include "mongo/db/pipeline/expression_context.h"
#include "mongo/db/query/query_knobs_gen.h"
#include "mongo/db/query/query_request_helper.h"
#include "mongo/db/query/query_settings/query_settings_cluster_parameter_gen.h"
#include "mongo/db/query/query_settings/query_settings_gen.h"
#include "mongo/db/query/query_settings/query_settings_service.h"
#include "mongo/db/query/query_shape/agg_cmd_shape.h"
@ -55,12 +56,65 @@
#include "mongo/util/serialization_context.h"
namespace mongo::query_settings {
static bool operator==(const QueryShapeConfiguration& lhs, const QueryShapeConfiguration& rhs) {
return SimpleBSONObjComparator::kInstance.compare(lhs.toBSON(), rhs.toBSON()) == 0;
}
static bool operator==(const QueryShapeConfigurationsWithTimestamp& lhs,
const QueryShapeConfigurationsWithTimestamp& rhs) {
return lhs.clusterParameterTime == rhs.clusterParameterTime &&
lhs.queryShapeConfigurations == rhs.queryShapeConfigurations;
}
namespace {
static auto const kSerializationContext =
SerializationContext{SerializationContext::Source::Command,
SerializationContext::CallerType::Request,
SerializationContext::Prefix::ExcludePrefix};
BSONObj makeSettingsClusterParameter(const QueryShapeConfigurationsWithTimestamp& config) {
BSONArrayBuilder settings;
for (const auto& queryShapeConfig : config.queryShapeConfigurations) {
settings.append(queryShapeConfig.toBSON());
}
return BSON("_id" << QuerySettingsService::getQuerySettingsClusterParameterName()
<< QuerySettingsClusterParameterValue::kSettingsArrayFieldName
<< settings.arr()
<< QuerySettingsClusterParameterValue::kClusterParameterTimeFieldName
<< config.clusterParameterTime.asTimestamp());
}
BSONObj makeSettingsClusterParameter(
const BSONArray& settings, LogicalTime clusterParameterTime = LogicalTime(Timestamp(113, 59))) {
return BSON("_id" << QuerySettingsService::getQuerySettingsClusterParameterName()
<< QuerySettingsClusterParameterValue::kSettingsArrayFieldName << settings
<< QuerySettingsClusterParameterValue::kClusterParameterTimeFieldName
<< clusterParameterTime.asTimestamp());
}
QuerySettings makeQuerySettings(const IndexHintSpecs& indexHints, bool setFramework = true) {
QuerySettings settings;
if (!indexHints.empty()) {
settings.setIndexHints(indexHints);
}
if (setFramework) {
settings.setQueryFramework(mongo::QueryFrameworkControlEnum::kTrySbeEngine);
}
return settings;
}
auto makeDbName(StringData dbName) {
return DatabaseNameUtil::deserialize(
boost::none /*tenantId=*/, dbName, SerializationContext::stateDefault());
}
NamespaceSpec makeNsSpec(StringData collName) {
NamespaceSpec ns;
ns.setDb(makeDbName("testDbA"));
ns.setColl(collName);
return ns;
}
class QuerySettingsScope {
public:
QuerySettingsScope(OperationContext* opCtx,
@ -71,16 +125,14 @@ public:
LogicalTime newTime = _previousQueryShapeConfigurationsWithTimestamp.clusterParameterTime;
newTime.addTicks(1);
setAllQueryShapeConfigurations(
_opCtx,
QuerySettingsService::get(_opCtx).setAllQueryShapeConfigurations(
QueryShapeConfigurationsWithTimestamp{queryShapeConfigurations, newTime},
boost::none /* tenantId */);
}
~QuerySettingsScope() {
setAllQueryShapeConfigurations(_opCtx,
std::move(_previousQueryShapeConfigurationsWithTimestamp),
boost::none /* tenantId */);
QuerySettingsService::get(_opCtx).setAllQueryShapeConfigurations(
std::move(_previousQueryShapeConfigurationsWithTimestamp), boost::none /* tenantId */);
}
private:
@ -127,17 +179,14 @@ private:
boost::optional<ExplainOptions::Verbosity> _previousExplain;
};
class QuerySettingsLoookupTest : public ServiceContextTest {
class QuerySettingsServiceTest : public ServiceContextTest {
public:
static constexpr StringData kCollName = "exampleColl"_sd;
static constexpr StringData kDbName = "foo"_sd;
void setUp() final {
// Initialize the query settings.
initializeForTest(getServiceContext());
_opCtx = cc().makeOperationContext();
_expCtx = ExpressionContext::makeBlankExpressionContext(opCtx(), {NamespaceString()});
}
OperationContext* opCtx() {
@ -145,34 +194,66 @@ public:
}
boost::intrusive_ptr<ExpressionContext> expCtx() {
if (!_expCtx) {
_expCtx = ExpressionContext::makeBlankExpressionContext(opCtx(), {NamespaceString()});
}
return _expCtx;
}
QuerySettingsService& service() {
return QuerySettingsService::get(opCtx());
}
static NamespaceString nss() {
return NamespaceStringUtil::deserialize(
boost::none, kDbName, kCollName, kSerializationContext);
}
QueryShapeConfiguration makeQueryShapeConfiguration(const BSONObj& cmdBSON,
const QuerySettings& querySettings) {
auto queryShapeHash =
createRepresentativeInfo(opCtx(), cmdBSON, boost::none /* tenantId */).queryShapeHash;
QueryShapeConfiguration makeQueryShapeConfiguration(
const BSONObj& cmdBSON,
const QuerySettings& querySettings,
boost::optional<TenantId> tenantId = boost::none) {
auto queryShapeHash = createRepresentativeInfo(opCtx(), cmdBSON, tenantId).queryShapeHash;
QueryShapeConfiguration config(queryShapeHash, querySettings);
config.setRepresentativeQuery(cmdBSON);
return config;
}
std::vector<QueryShapeConfiguration> getExampleQueryShapeConfigurations(TenantId tenantId) {
NamespaceSpec ns;
ns.setDb(DatabaseNameUtil::deserialize(tenantId, kDbName, kSerializationContext));
ns.setColl(kCollName);
const QuerySettings settings = makeQuerySettings({IndexHintSpec(ns, {IndexHint("a_1")})});
QueryInstance queryA =
BSON("find" << kCollName << "$db" << kDbName << "filter" << BSON("a" << 2));
QueryInstance queryB =
BSON("find" << kCollName << "$db" << kDbName << "filter" << BSON("a" << BSONNULL));
return {makeQueryShapeConfiguration(queryA, settings, tenantId),
makeQueryShapeConfiguration(queryB, settings, tenantId)};
}
void assertQuerySettingsLookup(const BSONObj& cmdBSON,
const query_shape::DeferredQueryShape& deferredShape,
const NamespaceString& nss) {
assertQuerySettingsLookupWithoutRejectionCheck(cmdBSON, deferredShape, nss);
assertQuerySettingsLookupWithRejectionCheck(cmdBSON, deferredShape, nss);
{
initializeForRouter(getServiceContext());
assertQuerySettingsLookupWithoutRejectionCheckForRouter(cmdBSON, deferredShape, nss);
assertQuerySettingsLookupWithRejectionCheckForRouter(cmdBSON, deferredShape, nss);
}
{
initializeForShard(getServiceContext());
assertQuerySettingsLookupWithoutRejectionCheckForShard(cmdBSON, deferredShape, nss);
assertQuerySettingsLookupWithRejectionCheckForShard(cmdBSON, deferredShape, nss);
}
}
/**
* Ensures that QuerySettings lookup returns the correct settings for the corresponding query.
*/
void assertQuerySettingsLookupWithoutRejectionCheck(
void assertQuerySettingsLookupWithoutRejectionCheckForRouter(
const BSONObj& cmdBSON,
const query_shape::DeferredQueryShape& deferredShape,
const NamespaceString& nss) {
@ -186,10 +267,7 @@ public:
useSbeEngineSettings.setQueryFramework(QueryFrameworkControlEnum::kTrySbeEngine);
// Ensure empty settings are returned if no settings are present in the system.
ASSERT_EQ(lookupQuerySettingsWithRejectionCheckOnRouter(expCtx(), deferredShape, nss),
QuerySettings());
ASSERT_EQ(lookupQuerySettingsWithRejectionCheckOnShard(
expCtx(), deferredShape, nss, QuerySettings()),
ASSERT_EQ(service().lookupQuerySettingsWithRejectionCheck(expCtx(), deferredShape, nss),
QuerySettings());
// Set { queryFramework: 'classic' } settings to 'cmdForSettingsBSON'.
@ -198,12 +276,39 @@ public:
// Ensure that 'forceClassicEngineSettings' are returned during the lookup, after query
// settings have been populated.
ASSERT_EQ(lookupQuerySettingsWithRejectionCheckOnRouter(expCtx(), deferredShape, nss),
ASSERT_EQ(service().lookupQuerySettingsWithRejectionCheck(expCtx(), deferredShape, nss),
forceClassicEngineSettings);
}
/**
* Ensures that QuerySettings lookup returns the correct settings for the corresponding query.
*/
void assertQuerySettingsLookupWithoutRejectionCheckForShard(
const BSONObj& cmdBSON,
const query_shape::DeferredQueryShape& deferredShape,
const NamespaceString& nss) {
const bool isExplain = cmdBSON.firstElementFieldNameStringData() == "explain";
BSONObj cmdForSettingsBSON = isExplain ? cmdBSON.firstElement().Obj() : cmdBSON;
QuerySettings forceClassicEngineSettings;
forceClassicEngineSettings.setQueryFramework(
QueryFrameworkControlEnum::kForceClassicEngine);
QuerySettings useSbeEngineSettings;
useSbeEngineSettings.setQueryFramework(QueryFrameworkControlEnum::kTrySbeEngine);
// Ensure empty settings are returned if no settings are present in the system.
ASSERT_EQ(service().lookupQuerySettingsWithRejectionCheck(
expCtx(), deferredShape, nss, QuerySettings()),
QuerySettings());
// Set { queryFramework: 'classic' } settings to 'cmdForSettingsBSON'.
QuerySettingsScope forceClassicEngineQuerySettingsScope(
opCtx(), {makeQueryShapeConfiguration(cmdForSettingsBSON, forceClassicEngineSettings)});
// Ensure that in case of a replica set case, a regular query settings lookup is performed.
ASSERT_EQ(
lookupQuerySettingsWithRejectionCheckOnShard(
service().lookupQuerySettingsWithRejectionCheck(
expCtx(), deferredShape, nss, boost::none /* querySettingsFromOriginalCommand */),
forceClassicEngineSettings);
@ -213,13 +318,13 @@ public:
// Ensure that settings passed to the method are being returned as opposed to performing
// the QuerySettings lookup.
ASSERT_EQ(lookupQuerySettingsWithRejectionCheckOnShard(
ASSERT_EQ(service().lookupQuerySettingsWithRejectionCheck(
expCtx(), deferredShape, nss, useSbeEngineSettings),
useSbeEngineSettings);
// Ensure that empty settings are returned if original command did not have any settings
// specified as opposed to performing QuerySettings lookup.
ASSERT_EQ(lookupQuerySettingsWithRejectionCheckOnShard(
ASSERT_EQ(service().lookupQuerySettingsWithRejectionCheck(
expCtx(),
deferredShape,
nss,
@ -232,7 +337,7 @@ public:
* Ensures that QuerySettings lookup rejects the queries if the associated settings contain
* 'reject: true'.
*/
void assertQuerySettingsLookupWithRejectionCheck(
void assertQuerySettingsLookupWithRejectionCheckForRouter(
const BSONObj& cmdBSON,
const query_shape::DeferredQueryShape& deferredShape,
const NamespaceString& nss) {
@ -250,13 +355,37 @@ public:
if (isExplain) {
ASSERT_DOES_NOT_THROW(
lookupQuerySettingsWithRejectionCheckOnRouter(expCtx(), deferredShape, nss));
ASSERT_DOES_NOT_THROW(lookupQuerySettingsWithRejectionCheckOnShard(
expCtx(), deferredShape, nss, boost::none /* querySettingsFromOriginalCommand */));
} else {
ASSERT_THROWS_CODE(
lookupQuerySettingsWithRejectionCheckOnRouter(expCtx(), deferredShape, nss),
DBException,
ErrorCodes::QueryRejectedBySettings);
}
}
/**
* Ensures that QuerySettings lookup rejects the queries if the associated settings contain
* 'reject: true'.
*/
void assertQuerySettingsLookupWithRejectionCheckForShard(
const BSONObj& cmdBSON,
const query_shape::DeferredQueryShape& deferredShape,
const NamespaceString& nss) {
const bool isExplain = cmdBSON.firstElementFieldNameStringData() == "explain";
BSONObj cmdForSettingsBSON = isExplain ? cmdBSON.firstElement().Obj() : cmdBSON;
// Set { reject: true } settings to 'cmdForSettingsBSON'.
QuerySettings querySettingsWithReject;
querySettingsWithReject.setReject(true);
QuerySettingsScope rejectQuerySettingsScope(
opCtx(), {makeQueryShapeConfiguration(cmdForSettingsBSON, querySettingsWithReject)});
// Ensure query is not rejected if an explain query is run, otherwise is rejected by
// throwing an exception with QueryRejectedBySettings error code.
if (isExplain) {
ASSERT_DOES_NOT_THROW(lookupQuerySettingsWithRejectionCheckOnShard(
expCtx(), deferredShape, nss, boost::none /* querySettingsFromOriginalCommand */));
} else {
ASSERT_THROWS_CODE(lookupQuerySettingsWithRejectionCheckOnShard(
expCtx(),
deferredShape,
@ -276,12 +405,25 @@ public:
}
}
void assertSanitizeInvalidIndexHints(const IndexHintSpecs& initialSpec,
const IndexHintSpecs& expectedSpec) {
QueryInstance query = BSON("find" << "exampleColl"
<< "$db"
<< "foo");
auto initSettings = makeQueryShapeConfiguration(query, makeQuerySettings(initialSpec));
std::vector<QueryShapeConfiguration> queryShapeConfigs{initSettings};
const auto expectedSettings = makeQuerySettings(expectedSpec);
ASSERT_DOES_NOT_THROW(service().sanitizeQuerySettingsHints(queryShapeConfigs));
ASSERT_BSONOBJ_EQ(queryShapeConfigs[0].getSettings().toBSON(), expectedSettings.toBSON());
}
private:
ServiceContext::UniqueOperationContext _opCtx;
boost::intrusive_ptr<ExpressionContext> _expCtx;
};
TEST_F(QuerySettingsLoookupTest, QuerySettingsLookupForFind) {
TEST_F(QuerySettingsServiceTest, QuerySettingsLookupForFind) {
auto findCmdStr = "{find: 'exampleColl', '$db': 'foo'}"_sd;
auto findCmdBSON = fromjson(findCmdStr);
auto findCmd = query_request_helper::makeFromFindCommandForTests(findCmdBSON, nss());
@ -298,7 +440,7 @@ TEST_F(QuerySettingsLoookupTest, QuerySettingsLookupForFind) {
}
}
TEST_F(QuerySettingsLoookupTest, QuerySettingsLookupForAgg) {
TEST_F(QuerySettingsServiceTest, QuerySettingsLookupForAgg) {
auto aggCmdStr =
"{aggregate: 'exampleColl', pipeline: [{$match: {_id: 0}}], cursor: {}, '$db': 'foo'}"_sd;
auto aggCmdBSON = fromjson(aggCmdStr);
@ -317,7 +459,7 @@ TEST_F(QuerySettingsLoookupTest, QuerySettingsLookupForAgg) {
}
}
TEST_F(QuerySettingsLoookupTest, QuerySettingsLookupForDistinct) {
TEST_F(QuerySettingsServiceTest, QuerySettingsLookupForDistinct) {
auto distinctCmdBSON = fromjson("{distinct: 'exampleColl', key: 'x', $db: 'foo'}");
auto distinctCmd = std::make_unique<DistinctCommandRequest>(
DistinctCommandRequest::parse(IDLParserContext("distinctCommandRequest",
@ -342,4 +484,62 @@ TEST_F(QuerySettingsLoookupTest, QuerySettingsLookupForDistinct) {
}
}
/**
* Tests that valid index hint specs are the same before and after index hint sanitization.
*/
TEST_F(QuerySettingsServiceTest, ValidIndexHintsAreTheSameBeforeAndAfterSanitization) {
IndexHintSpecs indexHintSpec{IndexHintSpec(
makeNsSpec("testCollA"_sd), {IndexHint(BSON("a" << 1)), IndexHint(BSON("b" << -1.0))})};
assertSanitizeInvalidIndexHints(indexHintSpec, indexHintSpec);
}
/**
* Tests that invalid key-pattern are removed after sanitization.
*/
TEST_F(QuerySettingsServiceTest, InvalidKeyPatternIndexesAreRemovedAfterSanitization) {
IndexHintSpecs indexHintSpec{IndexHintSpec(makeNsSpec("testCollA"_sd),
{IndexHint(BSON("a" << 1 << "c"
<< "invalid")),
IndexHint(BSON("b" << -1.0))})};
IndexHintSpecs expectedHintSpec{
IndexHintSpec(makeNsSpec("testCollA"_sd), {IndexHint(BSON("b" << -1.0))})};
assertSanitizeInvalidIndexHints(indexHintSpec, expectedHintSpec);
}
/**
* Same as the above test but with more complex examples.
*/
TEST_F(QuerySettingsServiceTest, InvalidKeyPatternIndexesAreRemovedAfterSanitizationComplex) {
IndexHintSpecs indexHintSpec{IndexHintSpec(makeNsSpec("testCollA"_sd),
{
IndexHint(BSON("a" << 1 << "c"
<< "invalid")),
IndexHint(BSON("b" << -1.0)),
IndexHint(BSON("c" << -2.0 << "b" << 4)),
IndexHint("index_name"),
IndexHint(BSON("$natural" << 1)),
IndexHint(BSON("$natural" << -1 << "a" << 2)),
IndexHint(BSON("a" << -1 << "$natural" << 1)),
})};
IndexHintSpecs expectedHintSpec{IndexHintSpec(makeNsSpec("testCollA"_sd),
{IndexHint(BSON("b" << -1.0)),
IndexHint(BSON("c" << -2.0 << "b" << 4)),
IndexHint("index_name")})};
assertSanitizeInvalidIndexHints(indexHintSpec, expectedHintSpec);
}
/**
* Tests that invalid key-pattern are removed after sanitization and the resulted index hints are
* empty.
*/
TEST_F(QuerySettingsServiceTest, InvalidKeyPatternIndexesAreRemovedAfterSanitizationEmptyHints) {
IndexHintSpecs indexHintSpec{IndexHintSpec(makeNsSpec("testCollA"_sd),
{
IndexHint(BSON("a" << 1 << "c"
<< "invalid")),
})};
IndexHintSpecs expectedHintSpec;
assertSanitizeInvalidIndexHints(indexHintSpec, expectedHintSpec);
}
} // namespace
} // namespace mongo::query_settings

View File

@ -46,6 +46,11 @@ protected:
ShardingState::create(getServiceContext());
expCtx = make_intrusive<ExpressionContextForTest>();
gAggregateOperationResourceConsumptionMetrics = true;
query_settings::initializeForTest(getServiceContext());
}
QuerySettingsService& service() {
return QuerySettingsService::get(getServiceContext());
}
boost::intrusive_ptr<ExpressionContext> expCtx;
@ -59,7 +64,8 @@ void assertInvalidQueryWithAnyQuerySettings(OperationContext* opCtx,
size_t errorCode) {
auto representativeQueryInfo =
createRepresentativeInfo(opCtx, representativeQuery, boost::none);
ASSERT_THROWS_CODE(validateQueryCompatibleWithAnyQuerySettings(representativeQueryInfo),
ASSERT_THROWS_CODE(QuerySettingsService::get(opCtx).validateQueryCompatibleWithAnyQuerySettings(
representativeQueryInfo),
DBException,
errorCode);
}
@ -70,10 +76,10 @@ void assertInvalidQueryAndQuerySettingsCombination(OperationContext* opCtx,
size_t errorCode) {
auto representativeQueryInfo =
createRepresentativeInfo(opCtx, representativeQuery, boost::none);
ASSERT_THROWS_CODE(
validateQueryCompatibleWithQuerySettings(representativeQueryInfo, querySettings),
DBException,
errorCode);
ASSERT_THROWS_CODE(QuerySettingsService::get(opCtx).validateQueryCompatibleWithQuerySettings(
representativeQueryInfo, querySettings),
DBException,
errorCode);
}
NamespaceSpec makeNamespace(StringData dbName, StringData collName) {
@ -137,7 +143,7 @@ TEST_F(QuerySettingsValidationTestFixture,
"$operationMetrics"_sd,
};
for (auto&& stage : getRejectionIncompatibleStages()) {
for (auto&& stage : QuerySettingsService::getRejectionIncompatibleStages()) {
// Avoid testing these stages, as they require more complex setup.
if (stage == "$listLocalSessions" || stage == "$listSessions" ||
stage == "$listSampledQueries") {
@ -186,21 +192,21 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndicesCannotReferToSame
auto indexSpecA = IndexHintSpec(ns, {IndexHint("sku")});
auto indexSpecB = IndexHintSpec(ns, {IndexHint("uks")});
querySettings.setIndexHints({{indexSpecA, indexSpecB}});
simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 7746608);
service().simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 7746608);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsCannotBeEmpty) {
QuerySettings querySettings;
simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 7746604);
service().simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 7746604);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsCannotHaveDefaultValues) {
QuerySettings querySettings;
querySettings.setReject(false);
simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 7746604);
service().simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 7746604);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithNoDbSpecified) {
@ -208,8 +214,8 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithNoDbSpecif
NamespaceSpec ns;
ns.setColl("collName"_sd);
querySettings.setIndexHints({{IndexHintSpec(ns, {IndexHint("a")})}});
simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 8727500);
service().simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 8727500);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithNoCollSpecified) {
@ -218,17 +224,17 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithNoCollSpec
ns.setDb(DatabaseNameUtil::deserialize(
boost::none /* tenantId */, "dbName"_sd, SerializationContext::stateDefault()));
querySettings.setIndexHints({{IndexHintSpec(ns, {IndexHint("a")})}});
simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 8727501);
service().simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 8727501);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithEmptyAllowedIndexes) {
QuerySettings querySettings;
auto ns = makeNamespace("testDB", "testColl");
querySettings.setIndexHints({{IndexHintSpec(ns, {})}});
simplifyQuerySettings(querySettings);
service().simplifyQuerySettings(querySettings);
ASSERT_EQUALS(querySettings.getIndexHints(), boost::none);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 7746604);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 7746604);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithAllEmptyAllowedIndexes) {
@ -238,9 +244,9 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithAllEmptyAl
IndexHintSpec(makeNamespace("testDB", "testColl2"), {}),
IndexHintSpec(makeNamespace("testDB", "testColl3"), {}),
}});
simplifyQuerySettings(querySettings);
service().simplifyQuerySettings(querySettings);
ASSERT_EQUALS(querySettings.getIndexHints(), boost::none);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 7746604);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 7746604);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithSomeEmptyAllowedIndexes) {
@ -251,7 +257,7 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithSomeEmptyA
}});
const auto expectedIndexHintSpec =
IndexHintSpec(makeNamespace("testDB", "testColl2"), {IndexHint("a")});
simplifyQuerySettings(querySettings);
service().simplifyQuerySettings(querySettings);
const auto simplifiedIndexHints = querySettings.getIndexHints();
ASSERT_NE(simplifiedIndexHints, boost::none);
@ -259,7 +265,7 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithSomeEmptyA
ASSERT_EQ(indexHintsList.size(), 1);
const auto& actualIndexHintSpec = indexHintsList[0];
ASSERT_BSONOBJ_EQ(expectedIndexHintSpec.toBSON(), actualIndexHintSpec.toBSON());
ASSERT_DOES_NOT_THROW(validateQuerySettings(querySettings));
ASSERT_DOES_NOT_THROW(service().validateQuerySettings(querySettings));
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithEmptyKeyPattern) {
@ -267,8 +273,8 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithEmptyKeyPa
querySettings.setIndexHints({{
IndexHintSpec(makeNamespace("testDB", "testColl"), {IndexHint(BSONObj{})}),
}});
simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 9646000);
service().simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 9646000);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithInvalidKeyPattern) {
@ -278,8 +284,8 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithInvalidKey
{IndexHint(BSON("a" << 1 << "b"
<< "some-string"))}),
}});
simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 9646001);
service().simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 9646001);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithInvalidNaturalHint) {
@ -288,8 +294,8 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithInvalidNat
IndexHintSpec(makeNamespace("testDB", "testColl"),
{IndexHint(BSON("$natural" << 1 << "b" << 2))}),
}});
simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 9646001);
service().simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 9646001);
}
TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithInvalidNaturalHintInverse) {
@ -298,8 +304,8 @@ TEST_F(QuerySettingsValidationTestFixture, QuerySettingsIndexHintsWithInvalidNat
IndexHintSpec(makeNamespace("testDB", "testColl"),
{IndexHint(BSON("b" << 2 << "$natural" << 1))}),
}});
simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(validateQuerySettings(querySettings), DBException, 9646001);
service().simplifyQuerySettings(querySettings);
ASSERT_THROWS_CODE(service().validateQuerySettings(querySettings), DBException, 9646001);
}
} // namespace

View File

@ -54,6 +54,7 @@
#include "mongo/db/index/index_descriptor.h"
#include "mongo/db/index_builds/multi_index_block.h"
#include "mongo/db/query/client_cursor/cursor_manager.h"
#include "mongo/db/query/query_settings/query_settings_service.h"
#include "mongo/db/repl/member_state.h"
#include "mongo/db/repl/repl_settings.h"
#include "mongo/db/repl/replication_coordinator.h"
@ -277,6 +278,9 @@ int dbtestsMain(int argc, char** argv) {
AuthorizationManager::get(service->getService())->setAuthEnabled(false);
ScriptEngine::setup(ExecutionEnvironment::Server);
query_settings::initializeForTest(service);
return mongo::dbtests::runDbTests(argc, argv);
}