Skip to content

Commit e5e3a2a

Browse files
author
Aleksandr Usenko
committed
Backport YDB #41435
1 parent 8b01a90 commit e5e3a2a

7 files changed

Lines changed: 682 additions & 7 deletions

File tree

contrib/ydb/core/cms/console/console__replace_yaml_config.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ class TConfigsManager::TTxReplaceDatabaseYamlConfig
307307
DoInternalAudit(txc, ctx);
308308

309309
db.Table<Schema::DatabaseYamlConfigs>().Key(TargetDatabase, Version + 1)
310-
.Update<Schema::DatabaseYamlConfigs::Config>(Config);
310+
.Update<Schema::DatabaseYamlConfigs::Config>(UpdatedDatabaseConfig);
311311

312312
/* Later we shift this boundary to support rollback and history */
313313
db.Table<Schema::DatabaseYamlConfigs>().Key(TargetDatabase, Version)

contrib/ydb/core/cms/console/console_configs_manager.cpp

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,9 @@ void TConfigsManager::ReplaceMainConfigMetadata(const TString &config, bool forc
7474

7575
void TConfigsManager::ValidateMainConfig(TUpdateConfigOpContext& opCtx) {
7676
try {
77+
// Re-applying an unchanged body with the same version is silently accepted
78+
// (idempotent fast path)
7779
if (opCtx.UpdatedConfig != MainYamlConfig || YamlDropped) {
78-
auto tree = NFyaml::TDocument::Parse(opCtx.UpdatedConfig);
79-
auto resolved = NYamlConfig::ResolveAll(tree);
80-
8180
if (ClusterName != opCtx.Cluster) {
8281
ythrow yexception() << "ClusterName mismatch"
8382
<< " expected " << ClusterName
@@ -90,6 +89,9 @@ void TConfigsManager::ValidateMainConfig(TUpdateConfigOpContext& opCtx) {
9089
<< " but got " << opCtx.Version;
9190
}
9291

92+
auto tree = NFyaml::TDocument::Parse(opCtx.UpdatedConfig);
93+
auto resolved = NYamlConfig::ResolveAll(tree);
94+
9395
TSimpleSharedPtr<NYamlConfig::TBasicUnknownFieldsCollector> unknownFieldsCollector = new NYamlConfig::TBasicUnknownFieldsCollector;
9496

9597
std::vector<TString> errors;
@@ -135,7 +137,12 @@ void TConfigsManager::ReplaceDatabaseConfigMetadata(const TString &config, bool
135137
if (!force) {
136138
opCtx.Version = metadata.Version.value_or(0);
137139
} else {
138-
opCtx.Version = YamlVersion;
140+
ui32 currentVersion = 0;
141+
if (auto it = DatabaseYamlConfigs.find(opCtx.TargetDatabase); it != DatabaseYamlConfigs.end())
142+
{
143+
currentVersion = it->second.Version;
144+
}
145+
opCtx.Version = currentVersion;
139146
}
140147

141148
opCtx.UpdatedConfig = NYamlConfig::ReplaceMetadata(config, NYamlConfig::TDatabaseMetadata{
@@ -150,10 +157,22 @@ void TConfigsManager::ReplaceDatabaseConfigMetadata(const TString &config, bool
150157
void TConfigsManager::ValidateDatabaseConfig(TUpdateDatabaseConfigOpContext& opCtx) {
151158
try {
152159
TString currentConfig;
160+
ui32 currentVersion = 0;
161+
153162
if (auto it = DatabaseYamlConfigs.find(opCtx.TargetDatabase); it != DatabaseYamlConfigs.end()) {
154163
currentConfig = it->second.Config;
164+
currentVersion = it->second.Version;
155165
}
166+
167+
// Re-applying an unchanged body with the same version is silently accepted
168+
// (idempotent fast path)
156169
if (opCtx.UpdatedConfig != currentConfig) {
170+
if (opCtx.Version != currentVersion) {
171+
ythrow yexception() << "Version mismatch"
172+
<< " expected " << currentVersion
173+
<< " but got " << opCtx.Version;
174+
}
175+
157176
auto databaseTree = NFyaml::TDocument::Parse(opCtx.UpdatedConfig);
158177
auto databaseConfig = NYamlConfig::ParseConfig(databaseTree);
159178

contrib/ydb/core/cms/console/console_configs_provider.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -745,8 +745,7 @@ bool TConfigsProvider::CheckSubscription(TInMemorySubscription::TPtr subscriptio
745745
subscription->VolatileYamlConfigHashes = VolatileYamlConfigHashes;
746746

747747
if (auto it = DatabaseYamlConfigs.find(subscription->Tenant); it != DatabaseYamlConfigs.end()) {
748-
// FIXME: handle version change correctly, instead of always sending on first update
749-
if (!subscription->DatabaseYamlConfigVersion || *subscription->DatabaseYamlConfigVersion != it->second.Version || !subscription->FirstUpdateSent) {
748+
if (!subscription->DatabaseYamlConfigVersion || *subscription->DatabaseYamlConfigVersion != it->second.Version) {
750749
subscription->DatabaseYamlConfigVersion = it->second.Version;
751750
request->Record.SetDatabaseYamlConfig(it->second.Config);
752751
} else {

contrib/ydb/core/cms/console/console_ut_configs.cpp

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,72 @@ const TString DATABASE_2_YAML_CONFIG_2_UPDATED = R"(
874874
some_removed_feature_flag_example: true
875875
)";
876876

877+
const TString VERSIONED_MAIN_CONFIG_V0_STALE = R"(
878+
---
879+
metadata:
880+
kind: MainConfig
881+
cluster: ""
882+
version: 0
883+
config:
884+
log_config:
885+
cluster_name: clusterA
886+
)";
887+
888+
const TString VERSIONED_MAIN_CONFIG_V1 = R"(
889+
---
890+
metadata:
891+
kind: MainConfig
892+
cluster: ""
893+
version: 1
894+
config:
895+
log_config:
896+
cluster_name: clusterA
897+
)";
898+
899+
const TString VERSIONED_MAIN_CONFIG_V2 = R"(
900+
---
901+
metadata:
902+
kind: MainConfig
903+
cluster: ""
904+
version: 2
905+
config:
906+
log_config:
907+
cluster_name: clusterB
908+
)";
909+
910+
const TString VERSIONED_DATABASE_CONFIG_V0_STALE = R"(
911+
---
912+
metadata:
913+
kind: DatabaseConfig
914+
database: "/dc-1/users/tenant-1"
915+
version: 0
916+
config:
917+
feature_flags:
918+
some_removed_feature_flag_example: true
919+
)";
920+
921+
const TString VERSIONED_DATABASE_CONFIG_V1 = R"(
922+
---
923+
metadata:
924+
kind: DatabaseConfig
925+
database: "/dc-1/users/tenant-1"
926+
version: 1
927+
config:
928+
feature_flags:
929+
some_removed_feature_flag_example: true
930+
)";
931+
932+
const TString VERSIONED_DATABASE_CONFIG_V2 = R"(
933+
---
934+
metadata:
935+
kind: DatabaseConfig
936+
database: "/dc-1/users/tenant-1"
937+
version: 2
938+
config:
939+
feature_flags:
940+
some_removed_feature_flag_example: false
941+
)";
942+
877943

878944
void InitializeTestConfigItems()
879945
{
@@ -1116,7 +1182,101 @@ TVector<ui64> CheckConfigureLogAffected(TTenantTestRuntime &runtime,
11161182
return {reply->Record.GetAddedItemIds().begin(), reply->Record.GetAddedItemIds().end()};
11171183
}
11181184

1185+
void DoFetchMainConfigFromConsole(TTenantTestRuntime &runtime, TString &yamlConfig)
1186+
{
1187+
auto *event = new TEvConsole::TEvGetAllConfigsRequest;
1188+
runtime.SendToConsole(event);
1189+
1190+
TAutoPtr<IEventHandle> handle;
1191+
auto response = runtime.GrabEdgeEventRethrow<TEvConsole::TEvGetAllConfigsResponse>(handle);
1192+
1193+
UNIT_ASSERT_C(response->Record.GetResponse().config_size() == 1,
1194+
"expected exactly one config in response");
1195+
UNIT_ASSERT_C(response->Record.GetResponse().identity_size() == 1,
1196+
"expected exactly one identity in response");
1197+
UNIT_ASSERT_C(response->Record.GetResponse().identity(0).type_case() == Ydb::DynamicConfig::ConfigIdentity::kCluster,
1198+
"expected kCluster identity in response");
1199+
1200+
yamlConfig = response->Record.GetResponse().config(0);
1201+
}
1202+
1203+
void DoFetchDatabaseConfigFromConsole(TTenantTestRuntime &runtime, const TString &databasePath, TString &yamlConfig)
1204+
{
1205+
auto *event = new TEvConsole::TEvGetAllConfigsRequest;
1206+
event->Record.SetIngressDatabase(databasePath);
1207+
runtime.SendToConsole(event);
1208+
1209+
TAutoPtr<IEventHandle> handle;
1210+
auto response = runtime.GrabEdgeEventRethrow<TEvConsole::TEvGetAllConfigsResponse>(handle);
1211+
1212+
UNIT_ASSERT_C(response->Record.GetResponse().config_size() == 1,
1213+
"expected at least one config in response for database: " << databasePath);
1214+
UNIT_ASSERT_C(response->Record.GetResponse().identity_size() == 1,
1215+
"expected exactly one identity in response");
1216+
UNIT_ASSERT_C(response->Record.GetResponse().identity(0).type_case() == Ydb::DynamicConfig::ConfigIdentity::kDatabase,
1217+
"expected kDatabase identity in response");
1218+
UNIT_ASSERT_C(response->Record.GetResponse().identity(0).has_database(),
1219+
"kDatabase identity in response with no database set");
1220+
UNIT_ASSERT_EQUAL_C(response->Record.GetResponse().identity(0).database(), databasePath,
1221+
"database in response not match requested database");
1222+
1223+
yamlConfig = response->Record.GetResponse().config(0);
1224+
}
1225+
1226+
void DoReplaceYamlConfig(TTenantTestRuntime &runtime,
1227+
const TString &yaml,
1228+
Ydb::StatusIds::StatusCode expectedCode,
1229+
const TString &errorSubstring = {},
1230+
bool allowAbsentDatabase = false)
1231+
{
1232+
auto *event = new TEvConsole::TEvReplaceYamlConfigRequest;
1233+
event->Record.MutableRequest()->set_config(yaml);
1234+
if (allowAbsentDatabase) {
1235+
event->Record.MutableRequest()->set_allow_absent_database(true);
1236+
}
1237+
runtime.SendToConsole(event);
1238+
1239+
TAutoPtr<IEventHandle> handle;
1240+
auto [success, error] = runtime.GrabEdgeEvents<
1241+
TEvConsole::TEvReplaceYamlConfigResponse,
1242+
TEvConsole::TEvGenericError>(handle);
1243+
1244+
if (expectedCode == Ydb::StatusIds::SUCCESS) {
1245+
UNIT_ASSERT_C(success != nullptr,
1246+
"expected success but got error: " <<
1247+
(error ? error->Record.ShortDebugString() : TString("no event")));
1248+
} else {
1249+
UNIT_ASSERT_C(error != nullptr,
1250+
"expected error " << static_cast<int>(expectedCode) << " but got success");
1251+
UNIT_ASSERT_VALUES_EQUAL(error->Record.GetYdbStatus(), expectedCode);
1252+
if (!errorSubstring.empty()) {
1253+
TString allMessages;
1254+
for (const auto& issue : error->Record.GetIssues()) {
1255+
allMessages += issue.message();
1256+
allMessages += ";";
1257+
}
1258+
UNIT_ASSERT_STRING_CONTAINS(allMessages, errorSubstring);
1259+
}
1260+
}
1261+
}
1262+
1263+
void DoEnsureMainConfigReplacedWith(TTenantTestRuntime &runtime, TString replaceConfig)
1264+
{
1265+
TString consoleConfig;
1266+
TString expectedConfig = NYamlConfig::UpgradeMainConfigVersion(replaceConfig);
1267+
1268+
DoFetchMainConfigFromConsole(runtime, consoleConfig);
1269+
UNIT_ASSERT_VALUES_EQUAL_C(consoleConfig, expectedConfig, "CONSOLE config version not match replace config version");
1270+
}
1271+
1272+
void DoEnsureDatabaseConfigReplacedWith(TTenantTestRuntime &runtime, const TString& database, TString replaceConfig)
1273+
{
1274+
TString consoleConfig;
1275+
TString expectedConfig = NYamlConfig::UpgradeDatabaseConfigVersion(replaceConfig);
11191276

1277+
DoFetchDatabaseConfigFromConsole(runtime, database, consoleConfig);
1278+
UNIT_ASSERT_VALUES_EQUAL_C(consoleConfig, expectedConfig, "CONSOLE config version not match replace config version");
1279+
}
11201280

11211281
} // anonymous namespace
11221282

@@ -4520,6 +4680,93 @@ Y_UNIT_TEST_SUITE(TConsoleInMemoryConfigSubscriptionTests) {
45204680
UNIT_ASSERT(notification->Get()->Record.GetVolatileConfigs()[0].GetNotChanged());
45214681
UNIT_ASSERT(notification->Get()->Record.GetVolatileConfigs()[1].GetNotChanged());
45224682
}
4683+
4684+
Y_UNIT_TEST(TestReplaceMainYamlConfigVersionCheck) {
4685+
TTenantTestRuntime runtime(DefaultConsoleTestConfig());
4686+
4687+
// Fresh runtime: YamlVersion = 0. Per documentation, the client must send
4688+
// the *current* stored version. A version ahead of stored is rejected.
4689+
DoReplaceYamlConfig(runtime, VERSIONED_MAIN_CONFIG_V1,
4690+
Ydb::StatusIds::BAD_REQUEST, "Version mismatch");
4691+
4692+
// version: 0 matches stored YamlVersion = 0 — accepted; YamlVersion becomes 1.
4693+
DoReplaceYamlConfig(runtime, VERSIONED_MAIN_CONFIG_V0_STALE,
4694+
Ydb::StatusIds::SUCCESS);
4695+
DoEnsureMainConfigReplacedWith(runtime, VERSIONED_MAIN_CONFIG_V0_STALE);
4696+
4697+
// version: 2 with stored = 1 — rejected.
4698+
DoReplaceYamlConfig(runtime, VERSIONED_MAIN_CONFIG_V2,
4699+
Ydb::StatusIds::BAD_REQUEST, "Version mismatch");
4700+
4701+
// Re-applying the same body with the stale version: 0 is silently
4702+
// accepted (documented idempotent behaviour: wire == stored - 1 with
4703+
// identical content). YamlVersion stays at 1.
4704+
DoReplaceYamlConfig(runtime, VERSIONED_MAIN_CONFIG_V0_STALE,
4705+
Ydb::StatusIds::SUCCESS);
4706+
DoEnsureMainConfigReplacedWith(runtime, VERSIONED_MAIN_CONFIG_V0_STALE);
4707+
4708+
// version: 1 matches stored YamlVersion = 1 — accepted; YamlVersion becomes 2.
4709+
DoReplaceYamlConfig(runtime, VERSIONED_MAIN_CONFIG_V1,
4710+
Ydb::StatusIds::SUCCESS);
4711+
DoEnsureMainConfigReplacedWith(runtime, VERSIONED_MAIN_CONFIG_V1);
4712+
4713+
// version: 2 with a differing body now matches stored — accepted; YamlVersion = 3.
4714+
DoReplaceYamlConfig(runtime, VERSIONED_MAIN_CONFIG_V2,
4715+
Ydb::StatusIds::SUCCESS);
4716+
DoEnsureMainConfigReplacedWith(runtime, VERSIONED_MAIN_CONFIG_V2);
4717+
}
4718+
4719+
Y_UNIT_TEST(TestReplaceDatabaseYamlConfigVersionCheck) {
4720+
NKikimrConfig::TAppConfig appcfg;
4721+
appcfg.MutableFeatureFlags()->SetDatabaseYamlConfigAllowed(true);
4722+
TTenantTestRuntime runtime(MultipleTenantsConsoleTestConfig(), appcfg);
4723+
4724+
const TString DATABASE = "/dc-1/users/tenant-1";
4725+
4726+
// ValidateDatabaseConfig appends the database config into the main config
4727+
// and re-validates the result, which requires a non-empty MainYamlConfig.
4728+
// Seed a minimal main config first (fresh YamlVersion = 0, so wire = 0).
4729+
DoReplaceYamlConfig(runtime, VERSIONED_MAIN_CONFIG_V0_STALE,
4730+
Ydb::StatusIds::SUCCESS);
4731+
4732+
// Fresh runtime: no per-database config stored, so currentVersion = 0.
4733+
// Per documentation, the wire version must equal the stored version.
4734+
// Sending version: 1 must be rejected.
4735+
DoReplaceYamlConfig(runtime, VERSIONED_DATABASE_CONFIG_V1,
4736+
Ydb::StatusIds::BAD_REQUEST, "Version mismatch",
4737+
/* allowAbsentDatabase = */ true);
4738+
4739+
// version: 0 matches stored currentVersion = 0 — accepted; stored version becomes 1.
4740+
DoReplaceYamlConfig(runtime, VERSIONED_DATABASE_CONFIG_V0_STALE,
4741+
Ydb::StatusIds::SUCCESS, {},
4742+
/* allowAbsentDatabase = */ true);
4743+
DoEnsureDatabaseConfigReplacedWith(runtime, DATABASE, VERSIONED_DATABASE_CONFIG_V0_STALE);
4744+
4745+
// version: 2 with stored = 1 — rejected.
4746+
DoReplaceYamlConfig(runtime, VERSIONED_DATABASE_CONFIG_V2,
4747+
Ydb::StatusIds::BAD_REQUEST, "Version mismatch",
4748+
/* allowAbsentDatabase = */ true);
4749+
4750+
// Re-applying the same body with the stale version: 0 is silently
4751+
// accepted (documented idempotent behaviour: wire == stored - 1 with
4752+
// identical content). Stored version stays at 1.
4753+
DoReplaceYamlConfig(runtime, VERSIONED_DATABASE_CONFIG_V0_STALE,
4754+
Ydb::StatusIds::SUCCESS, {},
4755+
/* allowAbsentDatabase = */ true);
4756+
DoEnsureDatabaseConfigReplacedWith(runtime, DATABASE, VERSIONED_DATABASE_CONFIG_V0_STALE);
4757+
4758+
// version: 1 matches stored = 1 — accepted; stored version becomes 2.
4759+
DoReplaceYamlConfig(runtime, VERSIONED_DATABASE_CONFIG_V1,
4760+
Ydb::StatusIds::SUCCESS, {},
4761+
/* allowAbsentDatabase = */ true);
4762+
DoEnsureDatabaseConfigReplacedWith(runtime, DATABASE, VERSIONED_DATABASE_CONFIG_V1);
4763+
4764+
// version: 2 with a differing body now matches stored = 2 — accepted; version = 3.
4765+
DoReplaceYamlConfig(runtime, VERSIONED_DATABASE_CONFIG_V2,
4766+
Ydb::StatusIds::SUCCESS, {},
4767+
/* allowAbsentDatabase = */ true);
4768+
DoEnsureDatabaseConfigReplacedWith(runtime, DATABASE, VERSIONED_DATABASE_CONFIG_V2);
4769+
}
45234770
}
45244771

45254772
Y_UNIT_TEST_SUITE(TConsoleConfigHelpersTests) {

contrib/ydb/library/yaml_config/public/yaml_config.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,13 @@ TString UpgradeStorageConfigVersion(const TString& config) {
895895
return ReplaceMetadata(config, metadata);
896896
}
897897

898+
TString UpgradeDatabaseConfigVersion(const TString& config) {
899+
auto metadata = GetDatabaseMetadata(config);
900+
Y_ENSURE(metadata.Version);
901+
*metadata.Version = *metadata.Version + 1;
902+
return ReplaceMetadata(config, metadata);
903+
}
904+
898905
} // namespace NKikimr::NYamlConfig
899906

900907
template <>

contrib/ydb/library/yaml_config/public/yaml_config.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ TString UpgradeMainConfigVersion(const TString& config);
255255
*/
256256
TString UpgradeStorageConfigVersion(const TString& config);
257257

258+
/**
259+
* Takes valid DatabaseConfig and increases version exactly by one
260+
*/
261+
TString UpgradeDatabaseConfigVersion(const TString& config);
262+
258263
/**
259264
* Replaces metadata in database config
260265
*/

0 commit comments

Comments
 (0)