Merge pull request #7549 from sfc-gh-ajbeamon/feature-tenant-groups
Add support for tenant groups
This commit is contained in:
commit
dec6dbfbfb
|
@ -601,7 +601,7 @@ def tenants(logger):
|
|||
output = run_fdbcli_command('createtenant tenant')
|
||||
assert output == 'The tenant `tenant\' has been created'
|
||||
|
||||
output = run_fdbcli_command('createtenant tenant2')
|
||||
output = run_fdbcli_command('createtenant tenant2 tenant_group=tenant_group2')
|
||||
assert output == 'The tenant `tenant2\' has been created'
|
||||
|
||||
output = run_fdbcli_command('listtenants')
|
||||
|
@ -636,6 +636,58 @@ def tenants(logger):
|
|||
assert('printable' in json_output['tenant']['prefix'])
|
||||
assert(json_output['tenant']['tenant_state'] == 'ready')
|
||||
|
||||
output = run_fdbcli_command('gettenant tenant2')
|
||||
lines = output.split('\n')
|
||||
assert len(lines) == 4
|
||||
assert lines[0].strip().startswith('id: ')
|
||||
assert lines[1].strip().startswith('prefix: ')
|
||||
assert lines[2].strip() == 'tenant state: ready'
|
||||
assert lines[3].strip() == 'tenant group: tenant_group2'
|
||||
|
||||
output = run_fdbcli_command('gettenant tenant2 JSON')
|
||||
json_output = json.loads(output, strict=False)
|
||||
assert(len(json_output) == 2)
|
||||
assert('tenant' in json_output)
|
||||
assert(json_output['type'] == 'success')
|
||||
assert(len(json_output['tenant']) == 4)
|
||||
assert('id' in json_output['tenant'])
|
||||
assert('prefix' in json_output['tenant'])
|
||||
assert(json_output['tenant']['tenant_state'] == 'ready')
|
||||
assert('tenant_group' in json_output['tenant'])
|
||||
assert(len(json_output['tenant']['tenant_group']) == 2)
|
||||
assert('base64' in json_output['tenant']['tenant_group'])
|
||||
assert(json_output['tenant']['tenant_group']['printable'] == 'tenant_group2')
|
||||
|
||||
output = run_fdbcli_command('configuretenant tenant tenant_group=tenant_group1')
|
||||
assert output == 'The configuration for tenant `tenant\' has been updated'
|
||||
|
||||
output = run_fdbcli_command('gettenant tenant')
|
||||
lines = output.split('\n')
|
||||
assert len(lines) == 4
|
||||
assert lines[3].strip() == 'tenant group: tenant_group1'
|
||||
|
||||
output = run_fdbcli_command('configuretenant tenant unset tenant_group')
|
||||
assert output == 'The configuration for tenant `tenant\' has been updated'
|
||||
|
||||
output = run_fdbcli_command('gettenant tenant')
|
||||
lines = output.split('\n')
|
||||
assert len(lines) == 3
|
||||
|
||||
output = run_fdbcli_command_and_get_error('configuretenant tenant tenant_group=tenant_group1 tenant_group=tenant_group2')
|
||||
assert output == 'ERROR: configuration parameter `tenant_group\' specified more than once.'
|
||||
|
||||
output = run_fdbcli_command_and_get_error('configuretenant tenant unset')
|
||||
assert output == 'ERROR: `unset\' specified without a configuration parameter.'
|
||||
|
||||
output = run_fdbcli_command_and_get_error('configuretenant tenant unset tenant_group=tenant_group1')
|
||||
assert output == 'ERROR: unrecognized configuration parameter `tenant_group=tenant_group1\'.'
|
||||
|
||||
output = run_fdbcli_command_and_get_error('configuretenant tenant tenant_group')
|
||||
assert output == 'ERROR: invalid configuration string `tenant_group\'. String must specify a value using `=\'.'
|
||||
|
||||
output = run_fdbcli_command_and_get_error('configuretenant tenant3 tenant_group=tenant_group1')
|
||||
assert output == 'ERROR: Tenant does not exist (2131)'
|
||||
|
||||
output = run_fdbcli_command('usetenant')
|
||||
assert output == 'Using the default tenant'
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ For details, see http://sourceforge.net/projects/libb64
|
|||
#define BASE64_DECODE_H
|
||||
|
||||
#include <iostream>
|
||||
#include "libb64/encode.h"
|
||||
|
||||
namespace base64 {
|
||||
extern "C" {
|
||||
|
|
|
@ -35,20 +35,104 @@
|
|||
|
||||
namespace fdb_cli {
|
||||
|
||||
const KeyRangeRef tenantSpecialKeyRange(LiteralStringRef("\xff\xff/management/tenant/map/"),
|
||||
LiteralStringRef("\xff\xff/management/tenant/map0"));
|
||||
const KeyRangeRef tenantMapSpecialKeyRange720("\xff\xff/management/tenant/map/"_sr,
|
||||
"\xff\xff/management/tenant/map0"_sr);
|
||||
const KeyRangeRef tenantConfigSpecialKeyRange("\xff\xff/management/tenant/configure/"_sr,
|
||||
"\xff\xff/management/tenant/configure0"_sr);
|
||||
|
||||
const KeyRangeRef tenantMapSpecialKeyRange710("\xff\xff/management/tenant_map/"_sr,
|
||||
"\xff\xff/management/tenant_map0"_sr);
|
||||
|
||||
KeyRangeRef const& tenantMapSpecialKeyRange(int apiVersion) {
|
||||
if (apiVersion >= 720) {
|
||||
return tenantMapSpecialKeyRange720;
|
||||
} else {
|
||||
return tenantMapSpecialKeyRange710;
|
||||
}
|
||||
}
|
||||
|
||||
Optional<std::map<Standalone<StringRef>, Optional<Value>>>
|
||||
parseTenantConfiguration(std::vector<StringRef> const& tokens, int startIndex, bool allowUnset) {
|
||||
std::map<Standalone<StringRef>, Optional<Value>> configParams;
|
||||
for (int tokenNum = startIndex; tokenNum < tokens.size(); ++tokenNum) {
|
||||
Optional<Value> value;
|
||||
|
||||
StringRef token = tokens[tokenNum];
|
||||
StringRef param;
|
||||
if (allowUnset && token == "unset"_sr) {
|
||||
if (++tokenNum == tokens.size()) {
|
||||
fmt::print(stderr, "ERROR: `unset' specified without a configuration parameter.\n");
|
||||
return {};
|
||||
}
|
||||
param = tokens[tokenNum];
|
||||
} else {
|
||||
bool foundEquals;
|
||||
param = token.eat("=", &foundEquals);
|
||||
if (!foundEquals) {
|
||||
fmt::print(stderr,
|
||||
"ERROR: invalid configuration string `{}'. String must specify a value using `='.\n",
|
||||
param.toString().c_str());
|
||||
return {};
|
||||
}
|
||||
value = token;
|
||||
}
|
||||
|
||||
if (configParams.count(param)) {
|
||||
fmt::print(
|
||||
stderr, "ERROR: configuration parameter `{}' specified more than once.\n", param.toString().c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
if (tokencmp(param, "tenant_group")) {
|
||||
configParams[param] = value;
|
||||
} else {
|
||||
fmt::print(stderr, "ERROR: unrecognized configuration parameter `{}'.\n", param.toString().c_str());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
return configParams;
|
||||
}
|
||||
|
||||
Key makeConfigKey(TenantNameRef tenantName, StringRef configName) {
|
||||
return tenantConfigSpecialKeyRange.begin.withSuffix(Tuple().append(tenantName).append(configName).pack());
|
||||
}
|
||||
|
||||
void applyConfiguration(Reference<ITransaction> tr,
|
||||
TenantNameRef tenantName,
|
||||
std::map<Standalone<StringRef>, Optional<Value>> configuration) {
|
||||
for (auto [configName, value] : configuration) {
|
||||
if (value.present()) {
|
||||
tr->set(makeConfigKey(tenantName, configName), value.get());
|
||||
} else {
|
||||
tr->clear(makeConfigKey(tenantName, configName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createtenant command
|
||||
ACTOR Future<bool> createTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens) {
|
||||
if (tokens.size() != 2) {
|
||||
ACTOR Future<bool> createTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens, int apiVersion) {
|
||||
if (tokens.size() < 2 || tokens.size() > 3) {
|
||||
printUsage(tokens[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
state Key tenantNameKey = fdb_cli::tenantSpecialKeyRange.begin.withSuffix(tokens[1]);
|
||||
state Key tenantNameKey = tenantMapSpecialKeyRange(apiVersion).begin.withSuffix(tokens[1]);
|
||||
state Reference<ITransaction> tr = db->createTransaction();
|
||||
state bool doneExistenceCheck = false;
|
||||
|
||||
state Optional<std::map<Standalone<StringRef>, Optional<Value>>> configuration =
|
||||
parseTenantConfiguration(tokens, 2, false);
|
||||
|
||||
if (!configuration.present()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (apiVersion < 720 && !configuration.get().empty()) {
|
||||
fmt::print(stderr, "ERROR: tenants do not accept configuration options before API version 720.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
loop {
|
||||
tr->setOption(FDBTransactionOptions::SPECIAL_KEY_SPACE_ENABLE_WRITES);
|
||||
try {
|
||||
|
@ -63,12 +147,13 @@ ACTOR Future<bool> createTenantCommandActor(Reference<IDatabase> db, std::vector
|
|||
}
|
||||
|
||||
tr->set(tenantNameKey, ValueRef());
|
||||
applyConfiguration(tr, tokens[1], configuration.get());
|
||||
wait(safeThreadFutureToFuture(tr->commit()));
|
||||
break;
|
||||
} catch (Error& e) {
|
||||
state Error err(e);
|
||||
if (e.code() == error_code_special_keys_api_failure) {
|
||||
std::string errorMsgStr = wait(fdb_cli::getSpecialKeysFailureErrorMessage(tr));
|
||||
std::string errorMsgStr = wait(getSpecialKeysFailureErrorMessage(tr));
|
||||
fmt::print(stderr, "ERROR: {}\n", errorMsgStr.c_str());
|
||||
return false;
|
||||
}
|
||||
|
@ -81,18 +166,18 @@ ACTOR Future<bool> createTenantCommandActor(Reference<IDatabase> db, std::vector
|
|||
}
|
||||
|
||||
CommandFactory createTenantFactory("createtenant",
|
||||
CommandHelp("createtenant <TENANT_NAME>",
|
||||
CommandHelp("createtenant <TENANT_NAME> [tenant_group=<TENANT_GROUP>]",
|
||||
"creates a new tenant in the cluster",
|
||||
"Creates a new tenant in the cluster with the specified name."));
|
||||
|
||||
// deletetenant command
|
||||
ACTOR Future<bool> deleteTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens) {
|
||||
ACTOR Future<bool> deleteTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens, int apiVersion) {
|
||||
if (tokens.size() != 2) {
|
||||
printUsage(tokens[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
state Key tenantNameKey = fdb_cli::tenantSpecialKeyRange.begin.withSuffix(tokens[1]);
|
||||
state Key tenantNameKey = tenantMapSpecialKeyRange(apiVersion).begin.withSuffix(tokens[1]);
|
||||
state Reference<ITransaction> tr = db->createTransaction();
|
||||
state bool doneExistenceCheck = false;
|
||||
|
||||
|
@ -115,7 +200,7 @@ ACTOR Future<bool> deleteTenantCommandActor(Reference<IDatabase> db, std::vector
|
|||
} catch (Error& e) {
|
||||
state Error err(e);
|
||||
if (e.code() == error_code_special_keys_api_failure) {
|
||||
std::string errorMsgStr = wait(fdb_cli::getSpecialKeysFailureErrorMessage(tr));
|
||||
std::string errorMsgStr = wait(getSpecialKeysFailureErrorMessage(tr));
|
||||
fmt::print(stderr, "ERROR: {}\n", errorMsgStr.c_str());
|
||||
return false;
|
||||
}
|
||||
|
@ -135,7 +220,7 @@ CommandFactory deleteTenantFactory(
|
|||
"Deletes a tenant from the cluster. Deletion will be allowed only if the specified tenant contains no data."));
|
||||
|
||||
// listtenants command
|
||||
ACTOR Future<bool> listTenantsCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens) {
|
||||
ACTOR Future<bool> listTenantsCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens, int apiVersion) {
|
||||
if (tokens.size() > 4) {
|
||||
printUsage(tokens[0]);
|
||||
return false;
|
||||
|
@ -157,14 +242,14 @@ ACTOR Future<bool> listTenantsCommandActor(Reference<IDatabase> db, std::vector<
|
|||
}
|
||||
if (tokens.size() == 4) {
|
||||
int n = 0;
|
||||
if (sscanf(tokens[3].toString().c_str(), "%d%n", &limit, &n) != 1 || n != tokens[3].size()) {
|
||||
fmt::print(stderr, "ERROR: invalid limit {}\n", tokens[3].toString().c_str());
|
||||
if (sscanf(tokens[3].toString().c_str(), "%d%n", &limit, &n) != 1 || n != tokens[3].size() || limit <= 0) {
|
||||
fmt::print(stderr, "ERROR: invalid limit `{}'\n", tokens[3].toString().c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
state Key beginTenantKey = fdb_cli::tenantSpecialKeyRange.begin.withSuffix(beginTenant);
|
||||
state Key endTenantKey = fdb_cli::tenantSpecialKeyRange.begin.withSuffix(endTenant);
|
||||
state Key beginTenantKey = tenantMapSpecialKeyRange(apiVersion).begin.withSuffix(beginTenant);
|
||||
state Key endTenantKey = tenantMapSpecialKeyRange(apiVersion).begin.withSuffix(endTenant);
|
||||
state Reference<ITransaction> tr = db->createTransaction();
|
||||
|
||||
loop {
|
||||
|
@ -186,14 +271,14 @@ ACTOR Future<bool> listTenantsCommandActor(Reference<IDatabase> db, std::vector<
|
|||
for (auto tenant : tenants) {
|
||||
fmt::print(" {}. {}\n",
|
||||
++index,
|
||||
printable(tenant.key.removePrefix(fdb_cli::tenantSpecialKeyRange.begin)).c_str());
|
||||
printable(tenant.key.removePrefix(tenantMapSpecialKeyRange(apiVersion).begin)).c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Error& e) {
|
||||
state Error err(e);
|
||||
if (e.code() == error_code_special_keys_api_failure) {
|
||||
std::string errorMsgStr = wait(fdb_cli::getSpecialKeysFailureErrorMessage(tr));
|
||||
std::string errorMsgStr = wait(getSpecialKeysFailureErrorMessage(tr));
|
||||
fmt::print(stderr, "ERROR: {}\n", errorMsgStr.c_str());
|
||||
return false;
|
||||
}
|
||||
|
@ -217,7 +302,7 @@ ACTOR Future<bool> getTenantCommandActor(Reference<IDatabase> db, std::vector<St
|
|||
}
|
||||
|
||||
state bool useJson = tokens.size() == 3;
|
||||
state Key tenantNameKey = fdb_cli::tenantSpecialKeyRange.begin.withSuffix(tokens[1]);
|
||||
state Key tenantNameKey = tenantMapSpecialKeyRange(apiVersion).begin.withSuffix(tokens[1]);
|
||||
state Reference<ITransaction> tr = db->createTransaction();
|
||||
|
||||
loop {
|
||||
|
@ -245,6 +330,7 @@ ACTOR Future<bool> getTenantCommandActor(Reference<IDatabase> db, std::vector<St
|
|||
int64_t id;
|
||||
std::string prefix;
|
||||
std::string tenantState;
|
||||
std::string tenantGroup;
|
||||
|
||||
doc.get("id", id);
|
||||
|
||||
|
@ -255,10 +341,14 @@ ACTOR Future<bool> getTenantCommandActor(Reference<IDatabase> db, std::vector<St
|
|||
}
|
||||
|
||||
doc.get("tenant_state", tenantState);
|
||||
bool hasTenantGroup = doc.tryGet("tenant_group.printable", tenantGroup);
|
||||
|
||||
fmt::print(" id: {}\n", id);
|
||||
fmt::print(" prefix: {}\n", printable(prefix).c_str());
|
||||
fmt::print(" tenant state: {}\n", printable(tenantState).c_str());
|
||||
if (hasTenantGroup) {
|
||||
fmt::print(" tenant group: {}\n", tenantGroup.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -299,6 +389,50 @@ CommandFactory getTenantFactory(
|
|||
"prints the metadata for a tenant",
|
||||
"Prints the metadata for a tenant. If JSON is specified, then the output will be in JSON format."));
|
||||
|
||||
// configuretenant command
|
||||
ACTOR Future<bool> configureTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens) {
|
||||
if (tokens.size() < 3) {
|
||||
printUsage(tokens[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
state Optional<std::map<Standalone<StringRef>, Optional<Value>>> configuration =
|
||||
parseTenantConfiguration(tokens, 2, true);
|
||||
|
||||
if (!configuration.present()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state Reference<ITransaction> tr = db->createTransaction();
|
||||
|
||||
loop {
|
||||
tr->setOption(FDBTransactionOptions::SPECIAL_KEY_SPACE_ENABLE_WRITES);
|
||||
try {
|
||||
applyConfiguration(tr, tokens[1], configuration.get());
|
||||
wait(safeThreadFutureToFuture(tr->commit()));
|
||||
break;
|
||||
} catch (Error& e) {
|
||||
state Error err(e);
|
||||
if (e.code() == error_code_special_keys_api_failure) {
|
||||
std::string errorMsgStr = wait(getSpecialKeysFailureErrorMessage(tr));
|
||||
fmt::print(stderr, "ERROR: {}\n", errorMsgStr.c_str());
|
||||
return false;
|
||||
}
|
||||
wait(safeThreadFutureToFuture(tr->onError(err)));
|
||||
}
|
||||
}
|
||||
|
||||
fmt::print("The configuration for tenant `{}' has been updated\n", printable(tokens[1]).c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
CommandFactory configureTenantFactory(
|
||||
"configuretenant",
|
||||
CommandHelp("configuretenant <TENANT_NAME> <[unset] tenant_group[=<GROUP_NAME>]> ...",
|
||||
"updates the configuration for a tenant",
|
||||
"Updates the configuration for a tenant. Use `tenant_group=<GROUP_NAME>' to change the tenant group "
|
||||
"that a tenant is assigned to or `unset tenant_group' to remove a tenant from its tenant group."));
|
||||
|
||||
// renametenant command
|
||||
ACTOR Future<bool> renameTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens) {
|
||||
if (tokens.size() != 3) {
|
||||
|
@ -316,6 +450,6 @@ CommandFactory renameTenantFactory(
|
|||
"renametenant",
|
||||
CommandHelp(
|
||||
"renametenant <OLD_NAME> <NEW_NAME>",
|
||||
"renames a tenant in the cluster.",
|
||||
"renames a tenant in the cluster",
|
||||
"Renames a tenant in the cluster. The old name must exist and the new name must not exist in the cluster."));
|
||||
} // namespace fdb_cli
|
||||
|
|
|
@ -1909,14 +1909,14 @@ ACTOR Future<int> cli(CLIOptions opt, LineNoise* plinenoise) {
|
|||
}
|
||||
|
||||
if (tokencmp(tokens[0], "createtenant")) {
|
||||
bool _result = wait(makeInterruptable(createTenantCommandActor(db, tokens)));
|
||||
bool _result = wait(makeInterruptable(createTenantCommandActor(db, tokens, opt.apiVersion)));
|
||||
if (!_result)
|
||||
is_error = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tokencmp(tokens[0], "deletetenant")) {
|
||||
bool _result = wait(makeInterruptable(deleteTenantCommandActor(db, tokens)));
|
||||
bool _result = wait(makeInterruptable(deleteTenantCommandActor(db, tokens, opt.apiVersion)));
|
||||
if (!_result)
|
||||
is_error = true;
|
||||
else if (tenantName.present() && tokens[1] == tenantName.get()) {
|
||||
|
@ -1928,7 +1928,7 @@ ACTOR Future<int> cli(CLIOptions opt, LineNoise* plinenoise) {
|
|||
}
|
||||
|
||||
if (tokencmp(tokens[0], "listtenants")) {
|
||||
bool _result = wait(makeInterruptable(listTenantsCommandActor(db, tokens)));
|
||||
bool _result = wait(makeInterruptable(listTenantsCommandActor(db, tokens, opt.apiVersion)));
|
||||
if (!_result)
|
||||
is_error = true;
|
||||
continue;
|
||||
|
@ -1941,7 +1941,26 @@ ACTOR Future<int> cli(CLIOptions opt, LineNoise* plinenoise) {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (tokencmp(tokens[0], "configuretenant")) {
|
||||
if (opt.apiVersion < 720) {
|
||||
fmt::print(stderr, "ERROR: tenants cannot be configured before API version 720.\n");
|
||||
is_error = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool _result = wait(makeInterruptable(configureTenantCommandActor(db, tokens)));
|
||||
if (!_result)
|
||||
is_error = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tokencmp(tokens[0], "renametenant")) {
|
||||
if (opt.apiVersion < 720) {
|
||||
fmt::print(stderr, "ERROR: tenants cannot be renamed before API version 720.\n");
|
||||
is_error = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool _result = wait(makeInterruptable(renameTenantCommandActor(db, tokens)));
|
||||
if (!_result)
|
||||
is_error = true;
|
||||
|
|
|
@ -157,6 +157,8 @@ ACTOR Future<bool> configureCommandActor(Reference<IDatabase> db,
|
|||
std::vector<StringRef> tokens,
|
||||
LineNoise* linenoise,
|
||||
Future<Void> warn);
|
||||
// configuretenant command
|
||||
ACTOR Future<bool> configureTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens);
|
||||
// consistency command
|
||||
ACTOR Future<bool> consistencyCheckCommandActor(Reference<ITransaction> tr,
|
||||
std::vector<StringRef> tokens,
|
||||
|
@ -164,11 +166,11 @@ ACTOR Future<bool> consistencyCheckCommandActor(Reference<ITransaction> tr,
|
|||
// coordinators command
|
||||
ACTOR Future<bool> coordinatorsCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens);
|
||||
// createtenant command
|
||||
ACTOR Future<bool> createTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens);
|
||||
ACTOR Future<bool> createTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens, int apiVersion);
|
||||
// datadistribution command
|
||||
ACTOR Future<bool> dataDistributionCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens);
|
||||
// deletetenant command
|
||||
ACTOR Future<bool> deleteTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens);
|
||||
ACTOR Future<bool> deleteTenantCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens, int apiVersion);
|
||||
// exclude command
|
||||
ACTOR Future<bool> excludeCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens, Future<Void> warn);
|
||||
// expensive_data_check command
|
||||
|
@ -194,7 +196,7 @@ ACTOR Future<bool> killCommandActor(Reference<IDatabase> db,
|
|||
std::vector<StringRef> tokens,
|
||||
std::map<Key, std::pair<Value, ClientLeaderRegInterface>>* address_interface);
|
||||
// listtenants command
|
||||
ACTOR Future<bool> listTenantsCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens);
|
||||
ACTOR Future<bool> listTenantsCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens, int apiVersion);
|
||||
// lock/unlock command
|
||||
ACTOR Future<bool> lockCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens);
|
||||
ACTOR Future<bool> unlockDatabaseActor(Reference<IDatabase> db, UID uid);
|
||||
|
|
|
@ -1627,11 +1627,6 @@ BlobWorkerInterface decodeBlobWorkerListValue(ValueRef const& value) {
|
|||
return interf;
|
||||
}
|
||||
|
||||
const KeyRangeRef tenantMapKeys("\xff/tenant/map/"_sr, "\xff/tenant/map0"_sr);
|
||||
const KeyRef tenantMapPrefix = tenantMapKeys.begin;
|
||||
const KeyRef tenantMapPrivatePrefix = "\xff\xff/tenant/map/"_sr;
|
||||
const KeyRef tenantLastIdKey = "\xff/tenant/lastId"_sr;
|
||||
|
||||
const KeyRangeRef storageQuotaKeys(LiteralStringRef("\xff/storageQuota/"), LiteralStringRef("\xff/storageQuota0"));
|
||||
const KeyRef storageQuotaPrefix = storageQuotaKeys.begin;
|
||||
|
||||
|
|
|
@ -74,6 +74,11 @@ TenantMapEntry::TenantMapEntry() {}
|
|||
TenantMapEntry::TenantMapEntry(int64_t id, TenantState tenantState) : tenantState(tenantState) {
|
||||
setId(id);
|
||||
}
|
||||
TenantMapEntry::TenantMapEntry(int64_t id, TenantState tenantState, Optional<TenantGroupName> tenantGroup)
|
||||
: tenantState(tenantState), tenantGroup(tenantGroup) {
|
||||
setId(id);
|
||||
}
|
||||
|
||||
void TenantMapEntry::setId(int64_t id) {
|
||||
ASSERT(id >= 0);
|
||||
this->id = id;
|
||||
|
@ -100,9 +105,33 @@ std::string TenantMapEntry::toJson(int apiVersion) const {
|
|||
|
||||
tenantEntry["tenant_state"] = TenantMapEntry::tenantStateToString(tenantState);
|
||||
|
||||
if (tenantGroup.present()) {
|
||||
json_spirit::mObject tenantGroupObject;
|
||||
std::string encodedTenantGroup = base64::encoder::from_string(tenantGroup.get().toString());
|
||||
// Remove trailing newline
|
||||
encodedTenantGroup.resize(encodedTenantGroup.size() - 1);
|
||||
|
||||
tenantGroupObject["base64"] = encodedTenantGroup;
|
||||
tenantGroupObject["printable"] = printable(tenantGroup.get());
|
||||
tenantEntry["tenant_group"] = tenantGroupObject;
|
||||
}
|
||||
|
||||
return json_spirit::write_string(json_spirit::mValue(tenantEntry));
|
||||
}
|
||||
|
||||
bool TenantMapEntry::matchesConfiguration(TenantMapEntry const& other) const {
|
||||
return tenantGroup == other.tenantGroup;
|
||||
}
|
||||
|
||||
void TenantMapEntry::configure(Standalone<StringRef> parameter, Optional<Value> value) {
|
||||
if (parameter == "tenant_group"_sr) {
|
||||
tenantGroup = value;
|
||||
} else {
|
||||
TraceEvent(SevWarnAlways, "UnknownTenantConfigurationParameter").detail("Parameter", parameter);
|
||||
throw invalid_tenant_configuration();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("/fdbclient/TenantMapEntry/Serialization") {
|
||||
TenantMapEntry entry1(1, TenantState::READY);
|
||||
ASSERT(entry1.prefix == "\x00\x00\x00\x00\x00\x00\x00\x01"_sr);
|
||||
|
|
|
@ -31,3 +31,13 @@ const KeyRangeRef TenantRangeImpl<false>::submoduleRange = KeyRangeRef(""_sr, "\
|
|||
|
||||
template <>
|
||||
const KeyRangeRef TenantRangeImpl<false>::mapSubRange = KeyRangeRef("tenant_map/"_sr, "tenant_map0"_sr);
|
||||
|
||||
template <>
|
||||
bool TenantRangeImpl<true>::subRangeIntersects(KeyRangeRef subRange, KeyRangeRef range) {
|
||||
return subRange.intersects(range);
|
||||
}
|
||||
|
||||
template <>
|
||||
bool TenantRangeImpl<false>::subRangeIntersects(KeyRangeRef subRange, KeyRangeRef range) {
|
||||
return subRange == mapSubRange;
|
||||
}
|
|
@ -29,6 +29,8 @@
|
|||
|
||||
typedef StringRef TenantNameRef;
|
||||
typedef Standalone<TenantNameRef> TenantName;
|
||||
typedef StringRef TenantGroupNameRef;
|
||||
typedef Standalone<TenantGroupNameRef> TenantGroupName;
|
||||
|
||||
enum class TenantState { REGISTERING, READY, REMOVING, UPDATING_CONFIGURATION, ERROR };
|
||||
|
||||
|
@ -44,28 +46,29 @@ struct TenantMapEntry {
|
|||
int64_t id = -1;
|
||||
Key prefix;
|
||||
TenantState tenantState = TenantState::READY;
|
||||
Optional<TenantGroupName> tenantGroup;
|
||||
|
||||
constexpr static int PREFIX_SIZE = sizeof(id);
|
||||
|
||||
public:
|
||||
TenantMapEntry();
|
||||
TenantMapEntry(int64_t id, TenantState tenantState);
|
||||
TenantMapEntry(int64_t id, TenantState tenantState, Optional<TenantGroupName> tenantGroup);
|
||||
|
||||
void setId(int64_t id);
|
||||
std::string toJson(int apiVersion) const;
|
||||
|
||||
Value encode() const { return ObjectWriter::toValue(*this, IncludeVersion(ProtocolVersion::withTenants())); }
|
||||
bool matchesConfiguration(TenantMapEntry const& other) const;
|
||||
void configure(Standalone<StringRef> parameter, Optional<Value> value);
|
||||
|
||||
Value encode() const { return ObjectWriter::toValue(*this, IncludeVersion()); }
|
||||
static TenantMapEntry decode(ValueRef const& value) {
|
||||
TenantMapEntry entry;
|
||||
ObjectReader reader(value.begin(), IncludeVersion());
|
||||
reader.deserialize(entry);
|
||||
return entry;
|
||||
return ObjectReader::fromStringRef<TenantMapEntry>(value, IncludeVersion());
|
||||
}
|
||||
|
||||
template <class Ar>
|
||||
void serialize(Ar& ar) {
|
||||
serializer(ar, id, tenantState);
|
||||
serializer(ar, id, tenantState, tenantGroup);
|
||||
if constexpr (Ar::isDeserializing) {
|
||||
if (id >= 0) {
|
||||
prefix = idToPrefix(id);
|
||||
|
@ -75,15 +78,35 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
struct TenantGroupEntry {
|
||||
constexpr static FileIdentifier file_identifier = 10764222;
|
||||
|
||||
TenantGroupEntry() = default;
|
||||
|
||||
Value encode() { return ObjectWriter::toValue(*this, IncludeVersion()); }
|
||||
static TenantGroupEntry decode(ValueRef const& value) {
|
||||
return ObjectReader::fromStringRef<TenantGroupEntry>(value, IncludeVersion());
|
||||
}
|
||||
|
||||
template <class Ar>
|
||||
void serialize(Ar& ar) {
|
||||
serializer(ar);
|
||||
}
|
||||
};
|
||||
|
||||
struct TenantMetadataSpecification {
|
||||
static KeyRef subspace;
|
||||
|
||||
KeyBackedObjectMap<TenantName, TenantMapEntry, decltype(IncludeVersion()), NullCodec> tenantMap;
|
||||
KeyBackedProperty<int64_t> lastTenantId;
|
||||
KeyBackedSet<Tuple> tenantGroupTenantIndex;
|
||||
KeyBackedObjectMap<TenantGroupName, TenantGroupEntry, decltype(IncludeVersion()), NullCodec> tenantGroupMap;
|
||||
|
||||
TenantMetadataSpecification(KeyRef subspace)
|
||||
: tenantMap(subspace.withSuffix("tenant/map/"_sr), IncludeVersion(ProtocolVersion::withTenants())),
|
||||
lastTenantId(subspace.withSuffix("tenant/lastId"_sr)) {}
|
||||
: tenantMap(subspace.withSuffix("tenant/map/"_sr), IncludeVersion()),
|
||||
lastTenantId(subspace.withSuffix("tenant/lastId"_sr)),
|
||||
tenantGroupTenantIndex(subspace.withSuffix("tenant/tenantGroup/tenantIndex/"_sr)),
|
||||
tenantGroupMap(subspace.withSuffix("tenant/tenantGroup/map/"_sr), IncludeVersion()) {}
|
||||
};
|
||||
|
||||
struct TenantMetadata {
|
||||
|
@ -93,6 +116,8 @@ private:
|
|||
public:
|
||||
static inline auto& tenantMap = instance.tenantMap;
|
||||
static inline auto& lastTenantId = instance.lastTenantId;
|
||||
static inline auto& tenantGroupTenantIndex = instance.tenantGroupTenantIndex;
|
||||
static inline auto& tenantGroupMap = instance.tenantGroupMap;
|
||||
|
||||
static inline Key tenantMapPrivatePrefix = "\xff"_sr.withSuffix(tenantMap.subspace.begin);
|
||||
};
|
||||
|
|
|
@ -102,11 +102,18 @@ Future<std::pair<Optional<TenantMapEntry>, bool>> createTenantTransaction(Transa
|
|||
if (name.startsWith("\xff"_sr)) {
|
||||
throw invalid_tenant_name();
|
||||
}
|
||||
if (tenantEntry.tenantGroup.present() && tenantEntry.tenantGroup.get().startsWith("\xff"_sr)) {
|
||||
throw invalid_tenant_group_name();
|
||||
}
|
||||
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
|
||||
state Future<Optional<TenantMapEntry>> existingEntryFuture = tryGetTenantTransaction(tr, name);
|
||||
wait(checkTenantMode(tr));
|
||||
state Future<Optional<TenantGroupEntry>> existingTenantGroupEntryFuture;
|
||||
if (tenantEntry.tenantGroup.present()) {
|
||||
existingTenantGroupEntryFuture = TenantMetadata::tenantGroupMap.get(tr, tenantEntry.tenantGroup.get());
|
||||
}
|
||||
|
||||
Optional<TenantMapEntry> existingEntry = wait(existingEntryFuture);
|
||||
if (existingEntry.present()) {
|
||||
|
@ -123,6 +130,15 @@ Future<std::pair<Optional<TenantMapEntry>, bool>> createTenantTransaction(Transa
|
|||
|
||||
tenantEntry.tenantState = TenantState::READY;
|
||||
TenantMetadata::tenantMap.set(tr, name, tenantEntry);
|
||||
if (tenantEntry.tenantGroup.present()) {
|
||||
TenantMetadata::tenantGroupTenantIndex.insert(tr, Tuple::makeTuple(tenantEntry.tenantGroup.get(), name));
|
||||
|
||||
// Create the tenant group associated with this tenant if it doesn't already exist
|
||||
Optional<TenantGroupEntry> existingTenantGroup = wait(existingTenantGroupEntryFuture);
|
||||
if (!existingTenantGroup.present()) {
|
||||
TenantMetadata::tenantGroupMap.set(tr, tenantEntry.tenantGroup.get(), TenantGroupEntry());
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_pair(tenantEntry, true);
|
||||
}
|
||||
|
@ -182,6 +198,7 @@ Future<Optional<TenantMapEntry>> createTenant(Reference<DB> db,
|
|||
.detail("Tenant", name)
|
||||
.detail("TenantId", newTenant.first.get().id)
|
||||
.detail("Prefix", newTenant.first.get().prefix)
|
||||
.detail("TenantGroup", tenantEntry.tenantGroup)
|
||||
.detail("Version", tr->getCommittedVersion());
|
||||
}
|
||||
|
||||
|
@ -216,6 +233,19 @@ Future<Void> deleteTenantTransaction(Transaction tr,
|
|||
}
|
||||
|
||||
TenantMetadata::tenantMap.erase(tr, name);
|
||||
if (tenantEntry.get().tenantGroup.present()) {
|
||||
TenantMetadata::tenantGroupTenantIndex.erase(tr,
|
||||
Tuple::makeTuple(tenantEntry.get().tenantGroup.get(), name));
|
||||
KeyBackedSet<Tuple>::RangeResultType tenantsInGroup = wait(TenantMetadata::tenantGroupTenantIndex.getRange(
|
||||
tr,
|
||||
Tuple::makeTuple(tenantEntry.get().tenantGroup.get()),
|
||||
Tuple::makeTuple(keyAfter(tenantEntry.get().tenantGroup.get())),
|
||||
2));
|
||||
if (tenantsInGroup.results.empty() ||
|
||||
(tenantsInGroup.results.size() == 1 && tenantsInGroup.results[0].getString(1) == name)) {
|
||||
TenantMetadata::tenantGroupMap.erase(tr, tenantEntry.get().tenantGroup.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Void();
|
||||
|
@ -256,6 +286,56 @@ Future<Void> deleteTenant(Reference<DB> db, TenantName name, Optional<int64_t> t
|
|||
}
|
||||
}
|
||||
|
||||
// This should only be called from a transaction that has already confirmed that the tenant entry
|
||||
// is present. The tenantEntry should start with the existing entry and modify only those fields that need
|
||||
// to be changed. This must only be called on a non-management cluster.
|
||||
ACTOR template <class Transaction>
|
||||
Future<Void> configureTenantTransaction(Transaction tr,
|
||||
TenantNameRef tenantName,
|
||||
TenantMapEntry originalEntry,
|
||||
TenantMapEntry updatedTenantEntry) {
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
TenantMetadata::tenantMap.set(tr, tenantName, updatedTenantEntry);
|
||||
|
||||
// If the tenant group was changed, we need to update the tenant group metadata structures
|
||||
if (originalEntry.tenantGroup != updatedTenantEntry.tenantGroup) {
|
||||
if (updatedTenantEntry.tenantGroup.present() && updatedTenantEntry.tenantGroup.get().startsWith("\xff"_sr)) {
|
||||
throw invalid_tenant_group_name();
|
||||
}
|
||||
if (originalEntry.tenantGroup.present()) {
|
||||
// Remove this tenant from the original tenant group index
|
||||
TenantMetadata::tenantGroupTenantIndex.erase(tr,
|
||||
Tuple::makeTuple(originalEntry.tenantGroup.get(), tenantName));
|
||||
|
||||
// Check if the original tenant group is now empty. If so, remove the tenant group.
|
||||
KeyBackedSet<Tuple>::RangeResultType tenants = wait(TenantMetadata::tenantGroupTenantIndex.getRange(
|
||||
tr,
|
||||
Tuple::makeTuple(originalEntry.tenantGroup.get()),
|
||||
Tuple::makeTuple(keyAfter(originalEntry.tenantGroup.get())),
|
||||
2));
|
||||
|
||||
if (tenants.results.empty() ||
|
||||
(tenants.results.size() == 1 && tenants.results[0].getString(1) == tenantName)) {
|
||||
TenantMetadata::tenantGroupMap.erase(tr, originalEntry.tenantGroup.get());
|
||||
}
|
||||
}
|
||||
if (updatedTenantEntry.tenantGroup.present()) {
|
||||
// If this is creating a new tenant group, add it to the tenant group map
|
||||
Optional<TenantGroupEntry> entry =
|
||||
wait(TenantMetadata::tenantGroupMap.get(tr, updatedTenantEntry.tenantGroup.get()));
|
||||
if (!entry.present()) {
|
||||
TenantMetadata::tenantGroupMap.set(tr, updatedTenantEntry.tenantGroup.get(), TenantGroupEntry());
|
||||
}
|
||||
|
||||
// Insert this tenant in the tenant group index
|
||||
TenantMetadata::tenantGroupTenantIndex.insert(
|
||||
tr, Tuple::makeTuple(updatedTenantEntry.tenantGroup.get(), tenantName));
|
||||
}
|
||||
}
|
||||
|
||||
return Void();
|
||||
}
|
||||
|
||||
ACTOR template <class Transaction>
|
||||
Future<std::vector<std::pair<TenantName, TenantMapEntry>>> listTenantsTransaction(Transaction tr,
|
||||
TenantNameRef begin,
|
||||
|
@ -339,6 +419,14 @@ Future<Void> renameTenant(Reference<DB> db, TenantName oldName, TenantName newNa
|
|||
TenantMetadata::tenantMap.erase(tr, oldName);
|
||||
TenantMetadata::tenantMap.set(tr, newName, oldEntry.get());
|
||||
|
||||
// Update the tenant group index to reflect the new tenant name
|
||||
if (oldEntry.get().tenantGroup.present()) {
|
||||
TenantMetadata::tenantGroupTenantIndex.erase(
|
||||
tr, Tuple::makeTuple(oldEntry.get().tenantGroup.get(), oldName));
|
||||
TenantMetadata::tenantGroupTenantIndex.insert(
|
||||
tr, Tuple::makeTuple(oldEntry.get().tenantGroup.get(), newName));
|
||||
}
|
||||
|
||||
wait(safeThreadFutureToFuture(tr->commit()));
|
||||
TraceEvent("RenameTenantSuccess").detail("OldName", oldName).detail("NewName", newName);
|
||||
return Void();
|
||||
|
|
|
@ -31,14 +31,16 @@
|
|||
#include "fdbclient/DatabaseContext.h"
|
||||
#include "fdbclient/SpecialKeySpace.actor.h"
|
||||
#include "fdbclient/TenantManagement.actor.h"
|
||||
#include "libb64/encode.h"
|
||||
#include "fdbclient/Tuple.h"
|
||||
#include "flow/Arena.h"
|
||||
#include "flow/UnitTest.h"
|
||||
#include "flow/actorcompiler.h" // This must be the last #include.
|
||||
|
||||
template <bool HasSubRanges = true>
|
||||
template <bool HasSubRanges>
|
||||
class TenantRangeImpl : public SpecialKeyRangeRWImpl {
|
||||
private:
|
||||
static bool subRangeIntersects(KeyRangeRef subRange, KeyRangeRef range);
|
||||
|
||||
static KeyRangeRef removePrefix(KeyRangeRef range, KeyRef prefix, KeyRef defaultEnd) {
|
||||
KeyRef begin = range.begin.removePrefix(prefix);
|
||||
KeyRef end;
|
||||
|
@ -53,15 +55,14 @@ private:
|
|||
|
||||
static KeyRef withTenantMapPrefix(KeyRef key, Arena& ar) {
|
||||
int keySize = SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin.size() +
|
||||
TenantRangeImpl::submoduleRange.begin.size() + TenantRangeImpl::mapSubRange.begin.size() +
|
||||
key.size();
|
||||
submoduleRange.begin.size() + mapSubRange.begin.size() + key.size();
|
||||
|
||||
KeyRef prefixedKey = makeString(keySize, ar);
|
||||
uint8_t* mutableKey = mutateString(prefixedKey);
|
||||
|
||||
mutableKey = SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin.copyTo(mutableKey);
|
||||
mutableKey = TenantRangeImpl::submoduleRange.begin.copyTo(mutableKey);
|
||||
mutableKey = TenantRangeImpl::mapSubRange.begin.copyTo(mutableKey);
|
||||
mutableKey = submoduleRange.begin.copyTo(mutableKey);
|
||||
mutableKey = mapSubRange.begin.copyTo(mutableKey);
|
||||
|
||||
key.copyTo(mutableKey);
|
||||
return prefixedKey;
|
||||
|
@ -84,20 +85,21 @@ private:
|
|||
return Void();
|
||||
}
|
||||
|
||||
ACTOR static Future<RangeResult> getTenantRange(ReadYourWritesTransaction* ryw,
|
||||
KeyRangeRef kr,
|
||||
GetRangeLimits limitsHint) {
|
||||
ACTOR template <bool B>
|
||||
static Future<RangeResult> getTenantRange(ReadYourWritesTransaction* ryw,
|
||||
KeyRangeRef kr,
|
||||
GetRangeLimits limitsHint) {
|
||||
state RangeResult results;
|
||||
|
||||
kr = kr.removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin)
|
||||
.removePrefix(TenantRangeImpl::submoduleRange.begin);
|
||||
.removePrefix(TenantRangeImpl<B>::submoduleRange.begin);
|
||||
|
||||
if (kr.intersects(TenantRangeImpl::mapSubRange)) {
|
||||
if (kr.intersects(TenantRangeImpl<B>::mapSubRange)) {
|
||||
GetRangeLimits limits = limitsHint;
|
||||
limits.decrement(results);
|
||||
wait(getTenantList(
|
||||
ryw,
|
||||
removePrefix(kr & TenantRangeImpl::mapSubRange, TenantRangeImpl::mapSubRange.begin, "\xff"_sr),
|
||||
removePrefix(kr & TenantRangeImpl<B>::mapSubRange, TenantRangeImpl<B>::mapSubRange.begin, "\xff"_sr),
|
||||
&results,
|
||||
limits));
|
||||
}
|
||||
|
@ -105,15 +107,39 @@ private:
|
|||
return results;
|
||||
}
|
||||
|
||||
ACTOR static Future<Void> createTenants(ReadYourWritesTransaction* ryw, std::vector<TenantNameRef> tenants) {
|
||||
ACTOR static Future<Void> createTenant(
|
||||
ReadYourWritesTransaction* ryw,
|
||||
TenantNameRef tenantName,
|
||||
std::vector<std::pair<Standalone<StringRef>, Optional<Value>>> configMutations,
|
||||
int64_t tenantId,
|
||||
std::map<TenantGroupName, int>* tenantGroupNetTenantDelta) {
|
||||
state TenantMapEntry tenantEntry;
|
||||
tenantEntry.setId(tenantId);
|
||||
|
||||
for (auto const& [name, value] : configMutations) {
|
||||
tenantEntry.configure(name, value);
|
||||
}
|
||||
|
||||
if (tenantEntry.tenantGroup.present()) {
|
||||
(*tenantGroupNetTenantDelta)[tenantEntry.tenantGroup.get()]++;
|
||||
}
|
||||
|
||||
std::pair<Optional<TenantMapEntry>, bool> entry =
|
||||
wait(TenantAPI::createTenantTransaction(&ryw->getTransaction(), tenantName, tenantEntry));
|
||||
|
||||
return Void();
|
||||
}
|
||||
|
||||
ACTOR static Future<Void> createTenants(
|
||||
ReadYourWritesTransaction* ryw,
|
||||
std::map<TenantName, std::vector<std::pair<Standalone<StringRef>, Optional<Value>>>> tenants,
|
||||
std::map<TenantGroupName, int>* tenantGroupNetTenantDelta) {
|
||||
int64_t _nextId = wait(TenantAPI::getNextTenantId(&ryw->getTransaction()));
|
||||
int64_t nextId = _nextId;
|
||||
|
||||
std::vector<Future<Void>> createFutures;
|
||||
for (auto tenant : tenants) {
|
||||
state TenantMapEntry tenantEntry(nextId++, TenantState::READY);
|
||||
createFutures.push_back(
|
||||
success(TenantAPI::createTenantTransaction(&ryw->getTransaction(), tenant, tenantEntry)));
|
||||
for (auto const& [tenant, config] : tenants) {
|
||||
createFutures.push_back(createTenant(ryw, tenant, config, nextId++, tenantGroupNetTenantDelta));
|
||||
}
|
||||
|
||||
TenantMetadata::lastTenantId.set(&ryw->getTransaction(), nextId - 1);
|
||||
|
@ -121,9 +147,49 @@ private:
|
|||
return Void();
|
||||
}
|
||||
|
||||
ACTOR static Future<Void> changeTenantConfig(
|
||||
ReadYourWritesTransaction* ryw,
|
||||
TenantName tenantName,
|
||||
std::vector<std::pair<Standalone<StringRef>, Optional<Value>>> configEntries,
|
||||
std::map<TenantGroupName, int>* tenantGroupNetTenantDelta) {
|
||||
TenantMapEntry originalEntry = wait(TenantAPI::getTenantTransaction(&ryw->getTransaction(), tenantName));
|
||||
TenantMapEntry updatedEntry = originalEntry;
|
||||
for (auto const& [name, value] : configEntries) {
|
||||
updatedEntry.configure(name, value);
|
||||
}
|
||||
|
||||
if (originalEntry.tenantGroup != updatedEntry.tenantGroup) {
|
||||
if (originalEntry.tenantGroup.present()) {
|
||||
(*tenantGroupNetTenantDelta)[originalEntry.tenantGroup.get()]--;
|
||||
}
|
||||
if (updatedEntry.tenantGroup.present()) {
|
||||
(*tenantGroupNetTenantDelta)[updatedEntry.tenantGroup.get()]++;
|
||||
}
|
||||
}
|
||||
|
||||
wait(TenantAPI::configureTenantTransaction(&ryw->getTransaction(), tenantName, originalEntry, updatedEntry));
|
||||
return Void();
|
||||
}
|
||||
|
||||
ACTOR static Future<Void> deleteSingleTenant(ReadYourWritesTransaction* ryw,
|
||||
TenantName tenantName,
|
||||
std::map<TenantGroupName, int>* tenantGroupNetTenantDelta) {
|
||||
state Optional<TenantMapEntry> tenantEntry =
|
||||
wait(TenantAPI::tryGetTenantTransaction(&ryw->getTransaction(), tenantName));
|
||||
if (tenantEntry.present()) {
|
||||
wait(TenantAPI::deleteTenantTransaction(&ryw->getTransaction(), tenantName));
|
||||
if (tenantEntry.get().tenantGroup.present()) {
|
||||
(*tenantGroupNetTenantDelta)[tenantEntry.get().tenantGroup.get()]--;
|
||||
}
|
||||
}
|
||||
|
||||
return Void();
|
||||
}
|
||||
|
||||
ACTOR static Future<Void> deleteTenantRange(ReadYourWritesTransaction* ryw,
|
||||
TenantName beginTenant,
|
||||
TenantName endTenant) {
|
||||
TenantName endTenant,
|
||||
std::map<TenantGroupName, int>* tenantGroupNetTenantDelta) {
|
||||
state std::vector<std::pair<TenantName, TenantMapEntry>> tenants = wait(
|
||||
TenantAPI::listTenantsTransaction(&ryw->getTransaction(), beginTenant, endTenant, CLIENT_KNOBS->TOO_MANY));
|
||||
|
||||
|
@ -139,69 +205,154 @@ private:
|
|||
std::vector<Future<Void>> deleteFutures;
|
||||
for (auto tenant : tenants) {
|
||||
deleteFutures.push_back(TenantAPI::deleteTenantTransaction(&ryw->getTransaction(), tenant.first));
|
||||
if (tenant.second.tenantGroup.present()) {
|
||||
(*tenantGroupNetTenantDelta)[tenant.second.tenantGroup.get()]--;
|
||||
}
|
||||
}
|
||||
|
||||
wait(waitForAll(deleteFutures));
|
||||
return Void();
|
||||
}
|
||||
|
||||
// Check if the number of tenants in the tenant group is equal to the net reduction in the number of tenants.
|
||||
// If it is, then we can delete the tenant group.
|
||||
ACTOR static Future<Void> checkAndRemoveTenantGroup(ReadYourWritesTransaction* ryw,
|
||||
TenantGroupName tenantGroup,
|
||||
int tenantDelta) {
|
||||
ASSERT(tenantDelta < 0);
|
||||
state int removedTenants = -tenantDelta;
|
||||
KeyBackedSet<Tuple>::RangeResultType tenantsInGroup =
|
||||
wait(TenantMetadata::tenantGroupTenantIndex.getRange(&ryw->getTransaction(),
|
||||
Tuple::makeTuple(tenantGroup),
|
||||
Tuple::makeTuple(keyAfter(tenantGroup)),
|
||||
removedTenants + 1));
|
||||
|
||||
ASSERT(tenantsInGroup.results.size() >= removedTenants);
|
||||
if (tenantsInGroup.results.size() == removedTenants) {
|
||||
TenantMetadata::tenantGroupMap.erase(&ryw->getTransaction(), tenantGroup);
|
||||
}
|
||||
|
||||
return Void();
|
||||
}
|
||||
|
||||
public:
|
||||
// These ranges vary based on the template parameter
|
||||
const static KeyRangeRef submoduleRange;
|
||||
const static KeyRangeRef mapSubRange;
|
||||
|
||||
// These sub-ranges should only be used if HasSubRanges=true
|
||||
const inline static KeyRangeRef configureSubRange = KeyRangeRef("configure/"_sr, "configure0"_sr);
|
||||
|
||||
explicit TenantRangeImpl(KeyRangeRef kr) : SpecialKeyRangeRWImpl(kr) {}
|
||||
|
||||
Future<RangeResult> getRange(ReadYourWritesTransaction* ryw,
|
||||
KeyRangeRef kr,
|
||||
GetRangeLimits limitsHint) const override {
|
||||
return getTenantRange(ryw, kr, limitsHint);
|
||||
return getTenantRange<HasSubRanges>(ryw, kr, limitsHint);
|
||||
}
|
||||
|
||||
Future<Optional<std::string>> commit(ReadYourWritesTransaction* ryw) override {
|
||||
auto ranges = ryw->getSpecialKeySpaceWriteMap().containedRanges(range);
|
||||
std::vector<Future<Void>> tenantManagementFutures;
|
||||
ACTOR static Future<Optional<std::string>> commitImpl(TenantRangeImpl* self, ReadYourWritesTransaction* ryw) {
|
||||
state std::vector<Future<Void>> tenantManagementFutures;
|
||||
|
||||
std::vector<std::pair<KeyRangeRef, Optional<Value>>> mapMutations;
|
||||
// This map is an ugly workaround to the fact that we cannot use RYW in these transactions.
|
||||
// It tracks the net change to the number of tenants in a tenant group, and at the end we can compare
|
||||
// that with how many tenants the tenant group started with. If we removed all of the tenants, then we
|
||||
// delete the tenant group.
|
||||
//
|
||||
// SOMEDAY: enable RYW support in special keys and remove this complexity.
|
||||
state std::map<TenantGroupName, int> tenantGroupNetTenantDelta;
|
||||
|
||||
state KeyRangeMap<std::pair<bool, Optional<Value>>>::Ranges ranges =
|
||||
ryw->getSpecialKeySpaceWriteMap().containedRanges(self->range);
|
||||
|
||||
state std::vector<std::pair<KeyRangeRef, Optional<Value>>> mapMutations;
|
||||
state std::map<TenantName, std::vector<std::pair<Standalone<StringRef>, Optional<Value>>>> configMutations;
|
||||
|
||||
tenantManagementFutures.push_back(TenantAPI::checkTenantMode(&ryw->getTransaction()));
|
||||
|
||||
for (auto range : ranges) {
|
||||
if (!range.value().first) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KeyRangeRef adjustedRange =
|
||||
state KeyRangeRef adjustedRange =
|
||||
range.range()
|
||||
.removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin)
|
||||
.removePrefix(submoduleRange.begin);
|
||||
|
||||
if (mapSubRange.intersects(adjustedRange)) {
|
||||
if (subRangeIntersects(mapSubRange, adjustedRange)) {
|
||||
adjustedRange = mapSubRange & adjustedRange;
|
||||
adjustedRange = removePrefix(adjustedRange, mapSubRange.begin, "\xff"_sr);
|
||||
mapMutations.push_back(std::make_pair(adjustedRange, range.value().second));
|
||||
} else if (subRangeIntersects(configureSubRange, adjustedRange) && adjustedRange.singleKeyRange()) {
|
||||
StringRef configTupleStr = adjustedRange.begin.removePrefix(configureSubRange.begin);
|
||||
try {
|
||||
Tuple tuple = Tuple::unpack(configTupleStr);
|
||||
if (tuple.size() != 2) {
|
||||
throw invalid_tuple_index();
|
||||
}
|
||||
configMutations[tuple.getString(0)].push_back(
|
||||
std::make_pair(tuple.getString(1), range.value().second));
|
||||
} catch (Error& e) {
|
||||
TraceEvent(SevWarn, "InvalidTenantConfigurationKey").error(e).detail("Key", adjustedRange.begin);
|
||||
ryw->setSpecialKeySpaceErrorMsg(ManagementAPIError::toJsonString(
|
||||
false, "configure tenant", "invalid tenant configuration key"));
|
||||
throw special_keys_api_failure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<TenantNameRef> tenantsToCreate;
|
||||
std::map<TenantName, std::vector<std::pair<Standalone<StringRef>, Optional<Value>>>> tenantsToCreate;
|
||||
for (auto mapMutation : mapMutations) {
|
||||
TenantNameRef tenantName = mapMutation.first.begin;
|
||||
if (mapMutation.second.present()) {
|
||||
tenantsToCreate.push_back(tenantName);
|
||||
std::vector<std::pair<Standalone<StringRef>, Optional<Value>>> createMutations;
|
||||
auto itr = configMutations.find(tenantName);
|
||||
if (itr != configMutations.end()) {
|
||||
createMutations = itr->second;
|
||||
configMutations.erase(itr);
|
||||
}
|
||||
tenantsToCreate[tenantName] = createMutations;
|
||||
} else {
|
||||
// For a single key clear, just issue the delete
|
||||
if (mapMutation.first.singleKeyRange()) {
|
||||
tenantManagementFutures.push_back(
|
||||
TenantAPI::deleteTenantTransaction(&ryw->getTransaction(), tenantName));
|
||||
tenantManagementFutures.push_back(deleteSingleTenant(ryw, tenantName, &tenantGroupNetTenantDelta));
|
||||
|
||||
// Configuration changes made to a deleted tenant are discarded
|
||||
configMutations.erase(tenantName);
|
||||
} else {
|
||||
tenantManagementFutures.push_back(deleteTenantRange(ryw, tenantName, mapMutation.first.end));
|
||||
tenantManagementFutures.push_back(
|
||||
deleteTenantRange(ryw, tenantName, mapMutation.first.end, &tenantGroupNetTenantDelta));
|
||||
|
||||
// Configuration changes made to a deleted tenant are discarded
|
||||
configMutations.erase(configMutations.lower_bound(tenantName),
|
||||
configMutations.lower_bound(mapMutation.first.end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!tenantsToCreate.empty()) {
|
||||
tenantManagementFutures.push_back(createTenants(ryw, tenantsToCreate));
|
||||
tenantManagementFutures.push_back(createTenants(ryw, tenantsToCreate, &tenantGroupNetTenantDelta));
|
||||
}
|
||||
for (auto configMutation : configMutations) {
|
||||
tenantManagementFutures.push_back(
|
||||
changeTenantConfig(ryw, configMutation.first, configMutation.second, &tenantGroupNetTenantDelta));
|
||||
}
|
||||
|
||||
return tag(waitForAll(tenantManagementFutures), Optional<std::string>());
|
||||
wait(waitForAll(tenantManagementFutures));
|
||||
|
||||
state std::vector<Future<Void>> tenantGroupUpdateFutures;
|
||||
for (auto [tenantGroup, count] : tenantGroupNetTenantDelta) {
|
||||
if (count < 0) {
|
||||
tenantGroupUpdateFutures.push_back(checkAndRemoveTenantGroup(ryw, tenantGroup, count));
|
||||
}
|
||||
}
|
||||
|
||||
wait(waitForAll(tenantGroupUpdateFutures));
|
||||
return Optional<std::string>();
|
||||
}
|
||||
|
||||
Future<Optional<std::string>> commit(ReadYourWritesTransaction* ryw) override { return commitImpl(this, ryw); }
|
||||
};
|
||||
|
||||
#include "flow/unactorcompiler.h"
|
||||
|
|
|
@ -1635,8 +1635,12 @@ ACTOR Future<Void> runTests(Reference<AsyncVar<Optional<struct ClusterController
|
|||
if (useDB) {
|
||||
std::vector<Future<Void>> tenantFutures;
|
||||
for (auto tenant : tenantsToCreate) {
|
||||
TraceEvent("CreatingTenant").detail("Tenant", tenant);
|
||||
tenantFutures.push_back(success(TenantAPI::createTenant(cx.getReference(), tenant)));
|
||||
TenantMapEntry entry;
|
||||
if (deterministicRandom()->coinflip()) {
|
||||
entry.tenantGroup = "TestTenantGroup"_sr;
|
||||
}
|
||||
TraceEvent("CreatingTenant").detail("Tenant", tenant).detail("TenantGroup", entry.tenantGroup);
|
||||
tenantFutures.push_back(success(TenantAPI::createTenant(cx.getReference(), tenant, entry)));
|
||||
}
|
||||
|
||||
wait(waitForAll(tenantFutures));
|
||||
|
|
|
@ -131,6 +131,7 @@ struct FuzzApiCorrectnessWorkload : TestWorkload {
|
|||
std::vector<Reference<ITenant>> tenants;
|
||||
std::set<TenantName> createdTenants;
|
||||
int numTenants;
|
||||
int numTenantGroups;
|
||||
|
||||
// Map from tenant number to key prefix
|
||||
std::map<int, std::string> keyPrefixes;
|
||||
|
@ -154,6 +155,9 @@ struct FuzzApiCorrectnessWorkload : TestWorkload {
|
|||
int maxTenants = getOption(options, "numTenants"_sr, 4);
|
||||
numTenants = deterministicRandom()->randomInt(0, maxTenants + 1);
|
||||
|
||||
int maxTenantGroups = getOption(options, "numTenantGroups"_sr, numTenants);
|
||||
numTenantGroups = deterministicRandom()->randomInt(0, maxTenantGroups + 1);
|
||||
|
||||
// See https://github.com/apple/foundationdb/issues/2424
|
||||
if (BUGGIFY) {
|
||||
enableBuggify(true, BuggifyType::Client);
|
||||
|
@ -206,6 +210,14 @@ struct FuzzApiCorrectnessWorkload : TestWorkload {
|
|||
std::string description() const override { return "FuzzApiCorrectness"; }
|
||||
|
||||
static TenantName getTenant(int num) { return TenantNameRef(format("tenant_%d", num)); }
|
||||
Optional<TenantGroupName> getTenantGroup(int num) {
|
||||
int groupNum = num % (numTenantGroups + 1);
|
||||
if (groupNum == numTenantGroups - 1) {
|
||||
return Optional<TenantGroupName>();
|
||||
} else {
|
||||
return TenantGroupNameRef(format("tenantgroup_%d", groupNum));
|
||||
}
|
||||
}
|
||||
bool canUseTenant(Optional<TenantName> tenant) { return !tenant.present() || createdTenants.count(tenant.get()); }
|
||||
|
||||
Future<Void> setup(Database const& cx) override {
|
||||
|
@ -226,7 +238,9 @@ struct FuzzApiCorrectnessWorkload : TestWorkload {
|
|||
|
||||
// The last tenant will not be created
|
||||
if (i < self->numTenants) {
|
||||
tenantFutures.push_back(::success(TenantAPI::createTenant(cx.getReference(), tenantName)));
|
||||
TenantMapEntry entry;
|
||||
entry.tenantGroup = self->getTenantGroup(i);
|
||||
tenantFutures.push_back(::success(TenantAPI::createTenant(cx.getReference(), tenantName, entry)));
|
||||
self->createdTenants.insert(tenantName);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -228,11 +228,13 @@ ERROR( tenant_name_required, 2130, "Tenant name must be specified to access data
|
|||
ERROR( tenant_not_found, 2131, "Tenant does not exist" )
|
||||
ERROR( tenant_already_exists, 2132, "A tenant with the given name already exists" )
|
||||
ERROR( tenant_not_empty, 2133, "Cannot delete a non-empty tenant" )
|
||||
ERROR( invalid_tenant_name, 2134, "Tenant name cannot begin with \\xff");
|
||||
ERROR( tenant_prefix_allocator_conflict, 2135, "The database already has keys stored at the prefix allocated for the tenant");
|
||||
ERROR( tenants_disabled, 2136, "Tenants have been disabled in the cluster");
|
||||
ERROR( unknown_tenant, 2137, "Tenant is not available from this server")
|
||||
ERROR( illegal_tenant_access, 2138, "Illegal tenant access")
|
||||
ERROR( invalid_tenant_name, 2134, "Tenant name cannot begin with \\xff" )
|
||||
ERROR( tenant_prefix_allocator_conflict, 2135, "The database already has keys stored at the prefix allocated for the tenant" )
|
||||
ERROR( tenants_disabled, 2136, "Tenants have been disabled in the cluster" )
|
||||
ERROR( unknown_tenant, 2137, "Tenant is not available from this server" )
|
||||
ERROR( illegal_tenant_access, 2138, "Illegal tenant access" )
|
||||
ERROR( invalid_tenant_group_name, 2139, "Tenant group name cannot begin with \\xff" )
|
||||
ERROR( invalid_tenant_configuration, 2140, "Tenant configuration is invalid" )
|
||||
|
||||
// 2200 - errors from bindings and official APIs
|
||||
ERROR( api_version_unset, 2200, "API version is not set" )
|
||||
|
|
|
@ -10,7 +10,7 @@ runSetup = true
|
|||
|
||||
[[test.workload]]
|
||||
testName = 'TenantManagement'
|
||||
maxTenants = 1000
|
||||
maxTenants = 1000
|
||||
testDuration = 60
|
||||
|
||||
[[test.workload]]
|
||||
|
|
|
@ -10,5 +10,5 @@ runSetup = true
|
|||
|
||||
[[test.workload]]
|
||||
testName = 'TenantManagement'
|
||||
maxTenants = 1000
|
||||
testDuration = 60
|
||||
maxTenants = 1000
|
||||
testDuration = 60
|
||||
|
|
Loading…
Reference in New Issue