SERVER-109991 Load config files to individual extensions (#41094)

GitOrigin-RevId: 570033339afe3f03f0e978488743b355b0289cce
This commit is contained in:
Daniel Segel 2025-09-10 18:37:25 -04:00 committed by MongoDB Bot
parent cb44035fc4
commit 2f9e0a26ac
12 changed files with 300 additions and 10 deletions

View File

@ -445,6 +445,9 @@ extensions_with_config(
# TODO SERVER-109108: Remove this entry when the bar extension is no longer needed.
"//src/mongo/db/extension/test_examples:bar_mongo_extension",
"//src/mongo/db/extension/test_examples:foo_mongo_extension",
# TODO SERVER-110326: Add these tests when possible.
# "//src/mongo/db/extension/test_examples:test_options_mongo_extension",
# "//src/mongo/db/extension/test_examples:parse_options_mongo_extension",
# Any extension that is just loaded in a no-passthrough test MUST NOT have the
# "_mongo_extension" suffix.

View File

@ -69,6 +69,8 @@ mongo_cc_unit_test(
"//src/mongo/db/extension/test_examples:duplicate_version_bad_extension",
"//src/mongo/db/extension/test_examples:no_compatible_version_bad_extension",
"//src/mongo/db/extension/test_examples:loadHighestCompatibleVersion_mongo_extension",
"//src/mongo/db/extension/test_examples:test_options_mongo_extension",
"//src/mongo/db/extension/test_examples:parse_options_mongo_extension",
],
tags = ["mongo_unittest_seventh_group"],
target_compatible_with = select({

View File

@ -47,4 +47,9 @@ void registerStageDescriptor(const ::MongoExtensionAggregationStageDescriptor* d
return sdk::enterCXX([&]() { return registerStageDescriptor(stageDesc); });
}
::MongoExtensionByteView HostPortal::_extGetOptions(
const ::MongoExtensionHostPortal* portal) noexcept {
return sdk::stringViewAsByteView(static_cast<const HostPortal*>(portal)->_extensionOpts);
}
} // namespace mongo::extension::host

View File

@ -30,20 +30,31 @@
#include "mongo/db/extension/public/api.h"
#include <string>
namespace mongo::extension::host {
void registerStageDescriptor(const ::MongoExtensionAggregationStageDescriptor* descriptor);
class HostPortal final : public ::MongoExtensionHostPortal {
public:
HostPortal(::MongoExtensionAPIVersion apiVersion, int maxWireVersion)
: ::MongoExtensionHostPortal(&VTABLE, apiVersion, maxWireVersion) {}
HostPortal(::MongoExtensionAPIVersion apiVersion,
int maxWireVersion,
std::string extensionOptions)
: ::MongoExtensionHostPortal{&VTABLE, apiVersion, maxWireVersion},
_extensionOpts(std::move(extensionOptions)) {}
private:
static ::MongoExtensionStatus* _extRegisterStageDescriptor(
const MongoExtensionAggregationStageDescriptor* stageDesc) noexcept;
static constexpr ::MongoExtensionHostPortalVTable VTABLE{&_extRegisterStageDescriptor};
static ::MongoExtensionByteView _extGetOptions(
const ::MongoExtensionHostPortal* portal) noexcept;
static constexpr ::MongoExtensionHostPortalVTable VTABLE{&_extRegisterStageDescriptor,
&_extGetOptions};
const std::string _extensionOpts;
};
} // namespace mongo::extension::host

View File

@ -183,7 +183,7 @@ ExtensionConfig ExtensionLoader::loadExtensionConfig(const std::string& extensio
LOGV2(11042903,
"Successfully loaded config file",
"sharedLibraryPath"_attr = config.sharedLibraryPath,
// TODO SERVER-109991: Remove 'extensionOptions' from log.
// TODO SERVER-110474: Remove or modify 'extensionOptions' logging.
"extensionOptions"_attr = YAML::Dump(config.extOptions));
return config;
@ -220,8 +220,7 @@ void ExtensionLoader::load(const ExtensionConfig& config) {
.getIncomingInternalClient()
.maxWireVersion);
// TODO SERVER-109991: Pass 'config.extOptions' to HostPortal.
HostPortal portal{extHandle.getVersion(), maxWireVersion};
HostPortal portal{extHandle.getVersion(), maxWireVersion, YAML::Dump(config.extOptions)};
extHandle.initialize(portal);
}
} // namespace mongo::extension::host

View File

@ -50,10 +50,8 @@ static std::filesystem::path getExtensionPath(const std::string& extensionName)
}
static ExtensionConfig makeEmptyExtensionConfig(const std::string& extensionName) {
ExtensionConfig config;
config.sharedLibraryPath = getExtensionPath(extensionName).string();
config.extOptions = YAML::Node(YAML::NodeType::Map);
return config;
return ExtensionConfig{.sharedLibraryPath = getExtensionPath(extensionName).string(),
.extOptions = YAML::Node(YAML::NodeType::Map)};
}
class LoadExtensionsTest : public unittest::Test {
@ -273,4 +271,54 @@ TEST(LoadExtensionTest, LoadHighestCompatibleVersionSucceeds) {
pipeline = {BSON("$extensionV4" << BSONObj())};
ASSERT_THROWS_CODE(Pipeline::parse(pipeline, expCtx), AssertionException, 16436);
}
TEST_F(LoadExtensionsTest, LoadExtensionBothOptionsSucceed) {
{
const auto extOptions = YAML::Load("optionA: true\n");
const ExtensionConfig config = {
.sharedLibraryPath = getExtensionPath("libtest_options_mongo_extension.so").string(),
.extOptions = extOptions};
ASSERT_DOES_NOT_THROW(ExtensionLoader::load(config));
auto expCtx = make_intrusive<ExpressionContextForTest>();
std::vector<BSONObj> pipeline = {BSON("$optionA" << BSONObj())};
auto parsedPipeline = Pipeline::parse(pipeline, expCtx);
ASSERT_TRUE(parsedPipeline != nullptr);
ASSERT_EQUALS(parsedPipeline->getSources().size(), 1U);
auto stage =
dynamic_cast<DocumentSourceExtension*>(parsedPipeline->getSources().front().get());
ASSERT_TRUE(stage != nullptr);
ASSERT_EQUALS(std::string(stage->getSourceName()), "$optionA");
// Assert that $optionB is unavailable.
pipeline = {BSON("$optionB" << BSONObj())};
ASSERT_THROWS_CODE(Pipeline::parse(pipeline, expCtx), AssertionException, 16436);
}
}
TEST_F(LoadExtensionsTest, LoadExtensionParseWithExtensionOptions) {
{
const auto extOptions = YAML::Load("checkMax: true\nmax: 10");
const ExtensionConfig config = {
.sharedLibraryPath = getExtensionPath("libparse_options_mongo_extension.so").string(),
.extOptions = extOptions};
ASSERT_DOES_NOT_THROW(ExtensionLoader::load(config));
auto expCtx = make_intrusive<ExpressionContextForTest>();
std::vector<BSONObj> pipeline = {BSON("$checkNum" << BSON("num" << 9))};
auto parsedPipeline = Pipeline::parse(pipeline, expCtx);
ASSERT_TRUE(parsedPipeline != nullptr);
ASSERT_EQUALS(parsedPipeline->getSources().size(), 1U);
auto stage =
dynamic_cast<DocumentSourceExtension*>(parsedPipeline->getSources().front().get());
ASSERT_TRUE(stage != nullptr);
ASSERT_EQUALS(std::string(stage->getSourceName()), "$checkNum");
// Assert that parsing fails when the provided num is greater than max 10.
pipeline = {BSON("$checkNum" << BSON("num" << 11))};
ASSERT_THROWS_CODE(Pipeline::parse(pipeline, expCtx), AssertionException, 10999106);
}
}
} // namespace mongo::extension::host

View File

@ -238,6 +238,9 @@ typedef struct MongoExtensionHostPortal {
typedef struct MongoExtensionHostPortalVTable {
MongoExtensionStatus* (*registerStageDescriptor)(
const MongoExtensionAggregationStageDescriptor* descriptor);
// Returns a MongoExtensionByteView containing the raw extension options associated with this
// extension.
MongoExtensionByteView (*getExtensionOptions)(const MongoExtensionHostPortal* portal);
} MongoExtensionHostPortalVTable;
/**

View File

@ -24,5 +24,6 @@ mongo_cc_library(
deps = [
"//src/mongo:base",
"//src/mongo/db/extension/public:api",
"//src/third_party/yaml-cpp:yaml",
],
)

View File

@ -32,6 +32,8 @@
#include "mongo/db/extension/sdk/aggregation_stage.h"
#include "mongo/db/extension/sdk/extension_status.h"
#include <yaml-cpp/yaml.h>
namespace mongo::extension::sdk {
/**
@ -62,11 +64,19 @@ public:
return get()->hostMongoDBMaxWireVersion;
}
YAML::Node getExtensionOptions() const {
assertValid();
return YAML::Load(std::string(byteViewAsStringView(vtable().getExtensionOptions(get()))));
}
private:
void _assertVTableConstraints(const VTable_t& vtable) const override {
tassert(10926401,
"Extension 'registerStageDescriptor' is null",
vtable.registerStageDescriptor != nullptr);
tassert(10999108,
"Extension 'getExtensionOptions' is null",
vtable.getExtensionOptions != nullptr);
};
};

View File

@ -33,6 +33,18 @@ mongo_cc_extension_shared_library(
srcs = ["vector_search.cpp"],
)
# Extensions under test_examples/extension_options/
[
mongo_cc_extension_shared_library(
name = extension_name + "_mongo_extension",
srcs = ["extension_options/" + extension_name + ".cpp"],
)
for extension_name in [
"test_options",
"parse_options",
]
]
# Extensions under test_examples/loading/
[
mongo_cc_extension_shared_library(

View File

@ -0,0 +1,92 @@
/**
* 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 "mongo/bson/bsonobj.h"
#include "mongo/db/extension/sdk/aggregation_stage.h"
#include "mongo/db/extension/sdk/extension_factory.h"
namespace sdk = mongo::extension::sdk;
struct ExtensionOptions {
inline static bool checkMax = false;
inline static double max = -1;
};
/**
* $checkNum is a no-op stage.
*
* The stage definition must include a "num" field, like {$checkNum: {num: <double>}}, or it will
* fail to parse. If 'checkMax' is true and the supplied num is greater than 'max', it will fail to
* parse.
*/
class CheckNumLogicalStage : public sdk::LogicalAggregationStage {};
class CheckNumStageDescriptor : public sdk::AggregationStageDescriptor {
public:
static inline const std::string kStageName = "$checkNum";
CheckNumStageDescriptor()
: sdk::AggregationStageDescriptor(kStageName, MongoExtensionAggregationStageType::kNoOp) {}
std::unique_ptr<sdk::LogicalAggregationStage> parse(mongo::BSONObj stageBson) const override {
uassert(10999104,
"Failed to parse " + kStageName + ", expected an object for $checkNum",
stageBson.hasField(kStageName) && stageBson.getField(kStageName).isABSONObj());
const auto obj = stageBson.getField(kStageName).Obj();
uassert(10999105,
"Failed to parse " + kStageName + ", expected {" + kStageName +
": {num: <double>}}",
obj.hasField("num") && obj.getField("num").isNumber());
if (ExtensionOptions::checkMax) {
uassert(10999106,
"Failed to parse " + kStageName + ", provided num is higher than max " +
std::to_string(ExtensionOptions::max),
obj.getField("num").numberDouble() <= ExtensionOptions::max);
}
return std::make_unique<CheckNumLogicalStage>();
}
};
class MyExtension : public sdk::Extension {
public:
void initialize(const sdk::HostPortalHandle& portal) override {
YAML::Node node = portal.getExtensionOptions();
uassert(10999107, "Extension options must include 'checkMax'", node["checkMax"]);
ExtensionOptions::checkMax = node["checkMax"].as<bool>();
if (ExtensionOptions::checkMax) {
uassert(10999103, "Extension options must include 'max'", node["max"]);
ExtensionOptions::max = node["max"].as<double>();
}
_registerStage<CheckNumStageDescriptor>(portal);
}
};
REGISTER_EXTENSION(MyExtension)
DEFINE_GET_EXTENSION()

View File

@ -0,0 +1,104 @@
/**
* 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 "mongo/bson/bsonobj.h"
#include "mongo/db/extension/sdk/aggregation_stage.h"
#include "mongo/db/extension/sdk/extension_factory.h"
namespace sdk = mongo::extension::sdk;
struct ExtensionOptions {
inline static bool optionA = false;
};
/**
* $optionA is a no-op stage.
*
* The stage definition must be empty, like {$optionA: {}}, or it will fail to parse.
*/
class OptionALogicalStage : public sdk::LogicalAggregationStage {};
class OptionAStageDescriptor : public sdk::AggregationStageDescriptor {
public:
static inline const std::string kStageName = "$optionA";
OptionAStageDescriptor()
: sdk::AggregationStageDescriptor(kStageName, MongoExtensionAggregationStageType::kNoOp) {}
std::unique_ptr<sdk::LogicalAggregationStage> parse(mongo::BSONObj stageBson) const override {
uassert(10999101,
"Failed to parse " + kStageName + ", expected object",
stageBson.hasField(kStageName) && stageBson.getField(kStageName).isABSONObj() &&
stageBson.getField(kStageName).Obj().isEmpty());
return std::make_unique<OptionALogicalStage>();
}
};
/**
* $optionB is a no-op stage.
*
* The stage definition must be empty, like {$optionB: {}}, or it will fail to parse.
*/
class OptionBLogicalStage : public sdk::LogicalAggregationStage {};
class OptionBStageDescriptor : public sdk::AggregationStageDescriptor {
public:
static inline const std::string kStageName = "$optionB";
OptionBStageDescriptor()
: sdk::AggregationStageDescriptor(kStageName, MongoExtensionAggregationStageType::kNoOp) {}
std::unique_ptr<sdk::LogicalAggregationStage> parse(mongo::BSONObj stageBson) const override {
uassert(10999102,
"Failed to parse " + kStageName + ", expected object",
stageBson.hasField(kStageName) && stageBson.getField(kStageName).isABSONObj() &&
stageBson.getField(kStageName).Obj().isEmpty());
return std::make_unique<OptionBLogicalStage>();
}
};
class MyExtension : public sdk::Extension {
public:
void initialize(const sdk::HostPortalHandle& portal) override {
YAML::Node node = portal.getExtensionOptions();
uassert(10999100, "Extension options must include 'optionA'", node["optionA"]);
ExtensionOptions::optionA = node["optionA"].as<bool>();
if (ExtensionOptions::optionA) {
_registerStage<OptionAStageDescriptor>(portal);
} else {
_registerStage<OptionBStageDescriptor>(portal);
}
}
};
REGISTER_EXTENSION(MyExtension)
DEFINE_GET_EXTENSION()