Skip to content

Commit caf1f50

Browse files
veselink1BenHuddleston
authored andcommitted
MB-61655: Add compat_version parameter
This parameter will hold the bucket configuration compatiblity version. It will be initialised and updated by ns_server, to the cluster compat version. Defining it as a "normal" parameter in configuration.json, has the benefit that the parameter is automatically reported in STAT(config), which ns_server monitor for configuration differences between it's expectation and actual values set in the bucket. Change-Id: I339bd5f820358bb5825dd2a2e9e4e0bc2eb95f07 Reviewed-on: https://review.couchbase.org/c/kv_engine/+/234660 Tested-by: Build Bot <[email protected]> Reviewed-by: Faizan Alam <[email protected]>
1 parent cd1bcc4 commit caf1f50

File tree

10 files changed

+255
-15
lines changed

10 files changed

+255
-15
lines changed

engines/ep/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ ADD_LIBRARY(ep STATIC
251251
src/collections/vbucket_manifest_handles.cc
252252
src/collections/vbucket_manifest_scope_entry.cc
253253
src/configuration.cc
254+
src/configuration_types.cc
254255
src/conflict_resolution.cc
255256
src/conn_store.cc
256257
src/connhandler.cc

engines/ep/configuration.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@
6262
*/
6363
{
6464
"params": {
65+
"compat_version": {
66+
"descr": "Compatibility version applied to the bucket, in the format \"major.minor\"",
67+
"default": "",
68+
"dynamic": true,
69+
"type": "std::string"
70+
},
6571
"allow_sanitize_value_in_deletion" : {
6672
"default" : "true",
6773
"descr": "Let EPE delete/prepare/del_with_meta prune any invalid body in the payload instead of failing",

engines/ep/src/configuration.cc

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ void ValueChangedValidator::validateString(std::string_view key, const char*) {
126126
Configuration::Configuration(bool isServerless, bool isDevAssertEnabled)
127127
: isServerless(isServerless), isDevAssertEnabled(isDevAssertEnabled) {
128128
initialize();
129+
initializeCompatVersion();
129130
initialized = true;
130131
}
131132

@@ -339,6 +340,7 @@ Configuration::Configuration(const Configuration& other)
339340
: isServerless(other.isServerless),
340341
isDevAssertEnabled(other.isDevAssertEnabled) {
341342
initialize();
343+
initializeCompatVersion();
342344
initialized = true;
343345
for (const auto& [key, value] : other.attributes) {
344346
(void)attributes[key]->setValue(value->getValue());
@@ -706,6 +708,57 @@ template void Configuration::addValueChangedFunc(std::string_view,
706708
template void Configuration::addValueChangedFunc(
707709
std::string_view, std::function<void(std::string_view)>);
708710

711+
class FeatureVersionValidator : public ValueChangedValidator {
712+
public:
713+
void validateString(std::string_view key, const char* value) override {
714+
if (value[0] == '\0') {
715+
// Allow the default value to be set.
716+
return;
717+
}
718+
cb::config::FeatureVersion::parse(value);
719+
}
720+
};
721+
722+
void Configuration::initializeCompatVersion() {
723+
static constexpr auto compatVersionKey = "compat_version";
724+
auto itr = attributes.find(compatVersionKey);
725+
if (itr == attributes.end()) {
726+
throw std::invalid_argument(fmt::format(
727+
"Configuration: No such config key '{}'", compatVersionKey));
728+
}
729+
730+
if (itr->second->validator) {
731+
throw std::logic_error(
732+
fmt::format("Configuration: Validator already set for key '{}'",
733+
compatVersionKey));
734+
}
735+
// We need a special validator for the compat version, since it's not a
736+
// supported type by the configuration.
737+
itr->second->validator = std::make_unique<FeatureVersionValidator>();
738+
739+
// Use a listener to update the compat version atomic when it changes.
740+
// We will read this atomic in the getter.
741+
std::function<void(std::string_view)> callback =
742+
[this](const std::string_view str) {
743+
auto version = str.empty()
744+
? cb::config::FeatureVersion::max()
745+
: cb::config::FeatureVersion::parse(str);
746+
processCompatVersionChange(version);
747+
};
748+
itr->second->addChangeListener(
749+
std::make_unique<ValueChangedCallback<std::string_view>>(
750+
std::move(callback)));
751+
}
752+
753+
void Configuration::processCompatVersionChange(
754+
cb::config::FeatureVersion version) {
755+
compatVersion.store(version, std::memory_order_release);
756+
}
757+
758+
cb::config::FeatureVersion Configuration::getEffectiveCompatVersion() const {
759+
return compatVersion.load(std::memory_order_acquire);
760+
}
761+
709762
// Explicit instantiations for addParameter for supported types.
710763
#define INSTANTIATE_TEMPLATES(T) \
711764
template void Configuration::addParameter(std::string_view, \

engines/ep/src/configuration.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,13 @@ class Configuration : public ConfigurationIface {
292292
ParameterValidationMap validateParameters(
293293
const ParameterMap& parameters) const override;
294294

295+
/**
296+
* Get the compatibility version for the configuration.
297+
* If the compatibility version is not set, the maximum feature version is
298+
* returned.
299+
*/
300+
cb::config::FeatureVersion getEffectiveCompatVersion() const;
301+
295302
const bool isServerless;
296303

297304
const bool isDevAssertEnabled = false;
@@ -306,6 +313,12 @@ class Configuration : public ConfigurationIface {
306313
*/
307314
Configuration(const Configuration& other);
308315

316+
/// The compatibility version for the configuration.
317+
void initializeCompatVersion();
318+
319+
/// Process a change to the compatibility version.
320+
void processCompatVersionChange(cb::config::FeatureVersion version);
321+
309322
std::pair<ParameterValidationMap, bool> setParametersInternal(
310323
const ParameterMap& parameters);
311324

@@ -403,6 +416,12 @@ class Configuration : public ConfigurationIface {
403416
*/
404417
cb::RelaxedAtomic<bool> initialized{false};
405418

419+
/**
420+
* The compatibility version for the configuration.
421+
*/
422+
std::atomic<cb::config::FeatureVersion> compatVersion{
423+
cb::config::FeatureVersion::max()};
424+
406425
void initialize();
407426

408427
/**
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2025-Present Couchbase, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License included
5+
* in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
6+
* in that file, in accordance with the Business Source License, use of this
7+
* software will be governed by the Apache License, Version 2.0, included in
8+
* the file licenses/APL2.txt.
9+
*/
10+
11+
#include "configuration_types.h"
12+
13+
#include <fmt/format.h>
14+
#include <charconv>
15+
#include <optional>
16+
17+
namespace cb::config {
18+
19+
FeatureVersion::FeatureVersion(uint8_t major, uint8_t minor)
20+
: major_version(major), minor_version(minor) {
21+
}
22+
23+
static std::optional<uint8_t> parseUint8(std::string_view num) {
24+
uint8_t number = 0;
25+
auto [ptr, ec] =
26+
std::from_chars(num.data(), num.data() + num.size(), number);
27+
if (ec != std::errc()) {
28+
return std::nullopt;
29+
}
30+
return number;
31+
}
32+
33+
static std::pair<uint8_t, uint8_t> parseVersionComponents(
34+
std::string_view version) {
35+
std::optional<uint8_t> major;
36+
std::optional<uint8_t> minor;
37+
const auto sep = version.find('.');
38+
if (sep != std::string_view::npos) {
39+
major = parseUint8(version.substr(0, sep));
40+
minor = parseUint8(version.substr(sep + 1));
41+
}
42+
if (!major || !minor || sep + 2 != version.size()) {
43+
throw std::invalid_argument("Invalid FeatureVersion: " +
44+
std::string(version));
45+
}
46+
return {*major, *minor};
47+
}
48+
49+
FeatureVersion FeatureVersion::max() {
50+
return {std::numeric_limits<uint8_t>::max(),
51+
std::numeric_limits<uint8_t>::max()};
52+
}
53+
54+
FeatureVersion FeatureVersion::parse(std::string_view version) {
55+
auto [major, minor] = parseVersionComponents(version);
56+
return {major, minor};
57+
}
58+
59+
std::strong_ordering FeatureVersion::operator<=>(
60+
const FeatureVersion&) const noexcept = default;
61+
62+
bool FeatureVersion::operator==(const FeatureVersion& other) const noexcept =
63+
default;
64+
65+
std::string format_as(const FeatureVersion& version) {
66+
if (version == FeatureVersion::max()) {
67+
return "<max>";
68+
}
69+
return fmt::format("{}.{}", version.major_version, version.minor_version);
70+
}
71+
72+
} // namespace cb::config

engines/ep/src/configuration_types.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#pragma once
1212

13+
#include <atomic>
1314
#include <string>
1415
#include <string_view>
1516
#include <type_traits>
@@ -40,4 +41,44 @@ std::string to_string(T val) {
4041
return std::string(cb::config::format_as(val));
4142
}
4243

44+
/**
45+
* A feature version is a major and minor version.
46+
*
47+
* Supports comparison and ordering.
48+
*/
49+
struct FeatureVersion {
50+
/**
51+
* The maximum feature version.
52+
*/
53+
static FeatureVersion max();
54+
/**
55+
* Parses a feature version from a string in the format "major.minor".
56+
*
57+
* @param version the input to parse
58+
* @return the parsed feature version
59+
* @throws std::invalid_argument
60+
*/
61+
static FeatureVersion parse(std::string_view version);
62+
63+
/// Construct a feature version.
64+
FeatureVersion(uint8_t major, uint8_t minor);
65+
66+
FeatureVersion(const FeatureVersion&) = default;
67+
FeatureVersion& operator=(const FeatureVersion&) = default;
68+
69+
/// Compare two feature versions.
70+
std::strong_ordering operator<=>(const FeatureVersion&) const noexcept;
71+
bool operator==(const FeatureVersion&) const noexcept;
72+
73+
/// The major version
74+
uint8_t major_version{};
75+
/// The minor version
76+
uint8_t minor_version{};
77+
};
78+
79+
static_assert(std::atomic<FeatureVersion>::is_always_lock_free,
80+
"cb::config::FeatureVersion atomic is not lock-free");
81+
82+
std::string format_as(const FeatureVersion& version);
83+
4384
} // namespace cb::config

engines/ep/src/ep_parameters.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,18 @@ static const std::unordered_set<std::string_view> vbucketParamSet{
182182
"dcp_hlc_invalid_strategy",
183183
};
184184

185+
/// Parameters which are exclusive to the config category.
186+
static const std::unordered_set<std::string_view> extraConfigParamSet{
187+
"compat_version",
188+
};
189+
185190
static const std::unordered_set<std::string_view> allParamSet = []() {
186191
std::unordered_set<std::string_view> ret;
187192
ret.insert(checkpointParamSet.begin(), checkpointParamSet.end());
188193
ret.insert(flushParamSet.begin(), flushParamSet.end());
189194
ret.insert(dcpParamSet.begin(), dcpParamSet.end());
190195
ret.insert(vbucketParamSet.begin(), vbucketParamSet.end());
196+
ret.insert(extraConfigParamSet.begin(), extraConfigParamSet.end());
191197
return ret;
192198
}();
193199

engines/ep/tests/ep_testsuite.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6279,6 +6279,7 @@ static enum test_result test_mb19687_fixed(EngineIface* h) {
62796279
"ep_bucket_type",
62806280
"ep_bucket_quota_change_task_poll_interval",
62816281
"ep_cache_size",
6282+
"ep_compat_version",
62826283
"ep_chk_expel_enabled",
62836284
"ep_checkpoint_destruction_tasks",
62846285
"ep_checkpoint_memory_ratio",
@@ -6542,6 +6543,7 @@ static enum test_result test_mb19687_fixed(EngineIface* h) {
65426543
"ep_bucket_type",
65436544
"ep_bucket_quota_change_task_poll_interval",
65446545
"ep_cache_size",
6546+
"ep_compat_version",
65456547
"ep_chk_expel_enabled",
65466548
"ep_chk_persistence_remains",
65476549
"ep_checkpoint_destruction_tasks",

engines/ep/tests/module_tests/configuration_test.cc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,3 +752,41 @@ TEST(ConfigurationTest, ValidateNonDynamic) {
752752
EXPECT_EQ(validation["param"]["value"], 1);
753753
EXPECT_EQ(validation["param"]["requiresRestart"], true);
754754
}
755+
756+
TEST(ConfigurationTest, CompatVersionComparison) {
757+
using cb::config::FeatureVersion;
758+
EXPECT_EQ(FeatureVersion(8, 0), FeatureVersion(8, 0));
759+
EXPECT_LT(FeatureVersion(8, 0), FeatureVersion(8, 1));
760+
EXPECT_GE(FeatureVersion(8, 0), FeatureVersion(7, 6));
761+
}
762+
763+
TEST(ConfigurationTest, CompatVersionDefault) {
764+
using cb::config::FeatureVersion;
765+
ConfigurationShim configuration;
766+
auto version = configuration.getEffectiveCompatVersion();
767+
EXPECT_EQ(version, FeatureVersion::max());
768+
}
769+
770+
TEST(ConfigurationTest, CompatVersionSet) {
771+
using cb::config::FeatureVersion;
772+
ConfigurationShim configuration;
773+
configuration.setParameter("compat_version", "8.0");
774+
auto version = configuration.getEffectiveCompatVersion();
775+
EXPECT_EQ(version, FeatureVersion(8, 0));
776+
777+
configuration.setParameter("compat_version", "");
778+
auto versionMax = configuration.getEffectiveCompatVersion();
779+
EXPECT_EQ(versionMax, FeatureVersion::max());
780+
}
781+
782+
TEST(ConfigurationTest, CompatVersionSetInvalid) {
783+
using cb::config::FeatureVersion;
784+
ConfigurationShim configuration;
785+
EXPECT_THROW(configuration.setParameter("compat_version", "asd"),
786+
std::invalid_argument);
787+
EXPECT_THROW(configuration.setParameter("compat_version", "8"),
788+
std::invalid_argument);
789+
EXPECT_THROW(configuration.setParameter("compat_version", "8.0.0"),
790+
std::invalid_argument);
791+
EXPECT_EQ(configuration.getEffectiveCompatVersion(), FeatureVersion::max());
792+
}

engines/ep/tests/module_tests/evp_engine_test.cc

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,28 @@ TEST_P(EPEngineParamTest, DynamicConfigValuesModifiable) {
332332
handled.emplace_back("setVBucketParam");
333333
}
334334

335+
bool canSetViaConfigurationParameter =
336+
engine->setConfigurationParameter(
337+
std::string(key), value, msg) ==
338+
cb::engine_errc::success;
339+
if (!canSetViaConfigurationParameter) {
340+
// Any parameter settable via the above param methods
341+
// should be settable via the setConfigurationParameter method
342+
// too.
343+
ADD_FAILURE()
344+
<< "Dynamic config key \"" << key
345+
<< "\" should be settable via "
346+
"setConfigurationParameter() - actually settable "
347+
"via: ["
348+
<< boost::algorithm::join(handled, ", ") << "]";
349+
}
350+
335351
if (deprecated.contains(key)) {
336352
EXPECT_EQ(0, handled.size())
337353
<< "Dynamic config key \"" << key
338354
<< "\" can no longer be set - actually settable via: ["
339355
<< boost::algorithm::join(handled, ", ") << "]";
340-
} else if (handled.empty()) {
356+
} else if (handled.empty() && !canSetViaConfigurationParameter) {
341357
ADD_FAILURE() << "Dynamic config key \"" << key
342358
<< "\" cannot be set via any of the set...Param "
343359
"methods.";
@@ -348,20 +364,6 @@ TEST_P(EPEngineParamTest, DynamicConfigValuesModifiable) {
348364
"set...Param() method - actually settable via: ["
349365
<< boost::algorithm::join(handled, ", ") << "]";
350366
}
351-
352-
if (engine->setConfigurationParameter(
353-
std::string(key), value, msg) !=
354-
cb::engine_errc::success) {
355-
// Any parameter settable via the above param methods
356-
// should be settable via the setConfigurationParameter method
357-
// too.
358-
ADD_FAILURE()
359-
<< "Dynamic config key \"" << key
360-
<< "\" should be settable via "
361-
"setConfigurationParameter() - actually settable "
362-
"via: ["
363-
<< boost::algorithm::join(handled, ", ") << "]";
364-
}
365367
}
366368
});
367369
}

0 commit comments

Comments
 (0)