mirror of
https://github.com/apple/foundationdb.git
synced 2025-06-01 18:56:00 +08:00
Some refactoring of tenant code:
* extract tenant management into its own file and namespace * rename the tenant management workload source file * extract tenant special keys functions to a separate file * extract some helper functions to GenericTransactionHelper.h * convert StringRef -> TenantNameRef * move some TenantMapEntry implementation into the cpp file * add some helper functions to decode/encode a tenant mode
This commit is contained in:
parent
542adb4b9f
commit
4bafe77889
fdbcli
fdbclient
CMakeLists.txtDatabaseConfiguration.cppFDBTypes.hGenericManagementAPI.actor.hGenericTransactionHelper.hMultiVersionTransaction.actor.cppMultiVersionTransaction.hSpecialKeySpace.actor.cppTenant.cppTenant.hTenantManagement.actor.hTenantSpecialKeys.actor.cppThreadSafeTransaction.cpp
fdbserver
@ -39,6 +39,7 @@
|
||||
#include "fdbclient/FDBOptions.g.h"
|
||||
#include "fdbclient/SystemData.h"
|
||||
#include "fdbclient/TagThrottle.actor.h"
|
||||
#include "fdbclient/TenantManagement.actor.h"
|
||||
#include "fdbclient/Tuple.h"
|
||||
|
||||
#include "fdbclient/ThreadSafeTransaction.h"
|
||||
@ -1049,7 +1050,7 @@ ACTOR Future<int> cli(CLIOptions opt, LineNoise* plinenoise) {
|
||||
state Database localDb;
|
||||
state Reference<IDatabase> db;
|
||||
state Reference<ITenant> tenant;
|
||||
state Optional<Standalone<StringRef>> tenantName;
|
||||
state Optional<TenantName> tenantName;
|
||||
state Optional<TenantMapEntry> tenantEntry;
|
||||
|
||||
// This tenant is kept empty for operations that perform management tasks (e.g. killing a process)
|
||||
@ -1840,7 +1841,7 @@ ACTOR Future<int> cli(CLIOptions opt, LineNoise* plinenoise) {
|
||||
}
|
||||
} else {
|
||||
Optional<TenantMapEntry> entry =
|
||||
wait(makeInterruptable(ManagementAPI::tryGetTenant(db, tokens[1])));
|
||||
wait(makeInterruptable(TenantAPI::tryGetTenant(db, tokens[1])));
|
||||
if (!entry.present()) {
|
||||
fprintf(stderr, "ERROR: Tenant `%s' does not exist\n", printable(tokens[1]).c_str());
|
||||
is_error = true;
|
||||
|
@ -64,6 +64,7 @@ set(FDBCLIENT_SRCS
|
||||
FluentDSampleIngestor.cpp
|
||||
FileBackupAgent.actor.cpp
|
||||
GenericManagementAPI.actor.h
|
||||
GenericTransactionHelper.h
|
||||
GlobalConfig.h
|
||||
GlobalConfig.actor.h
|
||||
GlobalConfig.actor.cpp
|
||||
@ -153,6 +154,8 @@ set(FDBCLIENT_SRCS
|
||||
TaskBucket.h
|
||||
Tenant.cpp
|
||||
Tenant.h
|
||||
TenantManagement.actor.h
|
||||
TenantSpecialKeys.actor.cpp
|
||||
TestKnobCollection.cpp
|
||||
TestKnobCollection.h
|
||||
ThreadSafeTransaction.cpp
|
||||
|
@ -637,8 +637,7 @@ bool DatabaseConfiguration::setInternal(KeyRef key, ValueRef value) {
|
||||
parse((&type), value);
|
||||
storageMigrationType = (StorageMigrationType::MigrationType)type;
|
||||
} else if (ck == LiteralStringRef("tenant_mode")) {
|
||||
parse((&type), value);
|
||||
tenantMode = (TenantMode::Mode)type;
|
||||
tenantMode = TenantMode::fromValue(value);
|
||||
} else if (ck == LiteralStringRef("proxies")) {
|
||||
overwriteProxiesCount();
|
||||
} else if (ck == LiteralStringRef("blob_granules_enabled")) {
|
||||
|
@ -1373,8 +1373,25 @@ struct TenantMode {
|
||||
return "";
|
||||
}
|
||||
|
||||
Value toValue() const { return ValueRef(format("%d", (int)mode)); }
|
||||
|
||||
static TenantMode fromValue(Optional<ValueRef> val) {
|
||||
if (!val.present()) {
|
||||
return DISABLED;
|
||||
}
|
||||
|
||||
// A failed parsing returns 0 (DISABLED)
|
||||
int num = atoi(val.get().toString().c_str());
|
||||
if (num < 0 || num >= END) {
|
||||
return DISABLED;
|
||||
}
|
||||
|
||||
return static_cast<Mode>(num);
|
||||
}
|
||||
|
||||
uint32_t mode;
|
||||
};
|
||||
|
||||
struct GRVCacheSpace {
|
||||
Version cachedReadVersion;
|
||||
double lastGrvTime;
|
||||
|
@ -129,21 +129,6 @@ bool isCompleteConfiguration(std::map<std::string, std::string> const& options);
|
||||
|
||||
ConfigureAutoResult parseConfig(StatusObject const& status);
|
||||
|
||||
template <typename Transaction, class T>
|
||||
struct transaction_future_type {
|
||||
using type = typename Transaction::template FutureT<T>;
|
||||
};
|
||||
|
||||
template <typename Transaction, class T>
|
||||
struct transaction_future_type<Transaction*, T> {
|
||||
using type = typename transaction_future_type<Transaction, T>::type;
|
||||
};
|
||||
|
||||
template <typename Transaction, class T>
|
||||
struct transaction_future_type<Reference<Transaction>, T> {
|
||||
using type = typename transaction_future_type<Transaction, T>::type;
|
||||
};
|
||||
|
||||
// Management API written in template code to support both IClientAPI and NativeAPI
|
||||
namespace ManagementAPI {
|
||||
|
||||
@ -666,258 +651,6 @@ Future<ConfigurationResult> changeConfig(Reference<DB> db,
|
||||
// used by special keys and fdbcli
|
||||
std::string generateErrorMessage(const CoordinatorsResult& res);
|
||||
|
||||
ACTOR template <class Transaction>
|
||||
Future<Optional<TenantMapEntry>> tryGetTenantTransaction(Transaction tr, TenantName name) {
|
||||
state Key tenantMapKey = name.withPrefix(tenantMapPrefix);
|
||||
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
|
||||
state typename transaction_future_type<Transaction, Optional<Value>>::type tenantFuture = tr->get(tenantMapKey);
|
||||
Optional<Value> val = wait(safeThreadFutureToFuture(tenantFuture));
|
||||
return val.map<TenantMapEntry>([](Optional<Value> v) { return decodeTenantEntry(v.get()); });
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<Optional<TenantMapEntry>> tryGetTenant(Reference<DB> db, TenantName name) {
|
||||
state Reference<typename DB::TransactionT> tr = db->createTransaction();
|
||||
|
||||
loop {
|
||||
try {
|
||||
tr->setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
||||
tr->setOption(FDBTransactionOptions::READ_LOCK_AWARE);
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenantTransaction(tr, name));
|
||||
return entry;
|
||||
} catch (Error& e) {
|
||||
wait(safeThreadFutureToFuture(tr->onError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACTOR template <class Transaction>
|
||||
Future<TenantMapEntry> getTenantTransaction(Transaction tr, TenantName name) {
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenantTransaction(tr, name));
|
||||
if (!entry.present()) {
|
||||
throw tenant_not_found();
|
||||
}
|
||||
|
||||
return entry.get();
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<TenantMapEntry> getTenant(Reference<DB> db, TenantName name) {
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenant(db, name));
|
||||
if (!entry.present()) {
|
||||
throw tenant_not_found();
|
||||
}
|
||||
|
||||
return entry.get();
|
||||
}
|
||||
|
||||
// Creates a tenant with the given name. If the tenant already exists, an empty optional will be returned.
|
||||
// The caller must enforce that the tenant ID be unique from all current and past tenants, and it must also be unique
|
||||
// from all other tenants created in the same transaction.
|
||||
ACTOR template <class Transaction>
|
||||
Future<std::pair<TenantMapEntry, bool>> createTenantTransaction(Transaction tr, TenantNameRef name, int64_t tenantId) {
|
||||
state Key tenantMapKey = name.withPrefix(tenantMapPrefix);
|
||||
|
||||
if (name.startsWith("\xff"_sr)) {
|
||||
throw invalid_tenant_name();
|
||||
}
|
||||
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
|
||||
state Future<Optional<TenantMapEntry>> tenantEntryFuture = tryGetTenantTransaction(tr, name);
|
||||
state typename transaction_future_type<Transaction, Optional<Value>>::type tenantDataPrefixFuture =
|
||||
tr->get(tenantDataPrefixKey);
|
||||
state typename transaction_future_type<Transaction, Optional<Value>>::type tenantModeFuture =
|
||||
tr->get(configKeysPrefix.withSuffix("tenant_mode"_sr));
|
||||
|
||||
Optional<Value> tenantMode = wait(safeThreadFutureToFuture(tenantModeFuture));
|
||||
|
||||
if (!tenantMode.present() || tenantMode.get() == StringRef(format("%d", TenantMode::DISABLED))) {
|
||||
throw tenants_disabled();
|
||||
}
|
||||
|
||||
Optional<TenantMapEntry> tenantEntry = wait(tenantEntryFuture);
|
||||
if (tenantEntry.present()) {
|
||||
return std::make_pair(tenantEntry.get(), false);
|
||||
}
|
||||
|
||||
Optional<Value> tenantDataPrefix = wait(safeThreadFutureToFuture(tenantDataPrefixFuture));
|
||||
if (tenantDataPrefix.present() &&
|
||||
tenantDataPrefix.get().size() + TenantMapEntry::ROOT_PREFIX_SIZE > CLIENT_KNOBS->TENANT_PREFIX_SIZE_LIMIT) {
|
||||
TraceEvent(SevWarnAlways, "TenantPrefixTooLarge")
|
||||
.detail("TenantSubspace", tenantDataPrefix.get())
|
||||
.detail("TenantSubspaceLength", tenantDataPrefix.get().size())
|
||||
.detail("RootPrefixLength", TenantMapEntry::ROOT_PREFIX_SIZE)
|
||||
.detail("MaxTenantPrefixSize", CLIENT_KNOBS->TENANT_PREFIX_SIZE_LIMIT);
|
||||
|
||||
throw client_invalid_operation();
|
||||
}
|
||||
|
||||
state TenantMapEntry newTenant(tenantId, tenantDataPrefix.present() ? (KeyRef)tenantDataPrefix.get() : ""_sr);
|
||||
|
||||
state typename transaction_future_type<Transaction, RangeResult>::type prefixRangeFuture =
|
||||
tr->getRange(prefixRange(newTenant.prefix), 1);
|
||||
RangeResult contents = wait(safeThreadFutureToFuture(prefixRangeFuture));
|
||||
if (!contents.empty()) {
|
||||
throw tenant_prefix_allocator_conflict();
|
||||
}
|
||||
|
||||
tr->set(tenantMapKey, encodeTenantEntry(newTenant));
|
||||
|
||||
return std::make_pair(newTenant, true);
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<TenantMapEntry> createTenant(Reference<DB> db, TenantName name) {
|
||||
state Reference<typename DB::TransactionT> tr = db->createTransaction();
|
||||
|
||||
state bool firstTry = true;
|
||||
loop {
|
||||
try {
|
||||
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
||||
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
||||
state typename DB::TransactionT::template FutureT<Optional<Value>> lastIdFuture = tr->get(tenantLastIdKey);
|
||||
|
||||
if (firstTry) {
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenantTransaction(tr, name));
|
||||
if (entry.present()) {
|
||||
throw tenant_already_exists();
|
||||
}
|
||||
|
||||
firstTry = false;
|
||||
}
|
||||
|
||||
Optional<Value> lastIdVal = wait(safeThreadFutureToFuture(lastIdFuture));
|
||||
int64_t tenantId = lastIdVal.present() ? TenantMapEntry::prefixToId(lastIdVal.get()) + 1 : 0;
|
||||
tr->set(tenantLastIdKey, TenantMapEntry::idToPrefix(tenantId));
|
||||
state std::pair<TenantMapEntry, bool> newTenant = wait(createTenantTransaction(tr, name, tenantId));
|
||||
|
||||
if (BUGGIFY) {
|
||||
throw commit_unknown_result();
|
||||
}
|
||||
|
||||
wait(safeThreadFutureToFuture(tr->commit()));
|
||||
|
||||
if (BUGGIFY) {
|
||||
throw commit_unknown_result();
|
||||
}
|
||||
|
||||
TraceEvent("CreatedTenant")
|
||||
.detail("Tenant", name)
|
||||
.detail("TenantId", newTenant.first.id)
|
||||
.detail("Prefix", newTenant.first.prefix)
|
||||
.detail("Version", tr->getCommittedVersion());
|
||||
|
||||
return newTenant.first;
|
||||
} catch (Error& e) {
|
||||
wait(safeThreadFutureToFuture(tr->onError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACTOR template <class Transaction>
|
||||
Future<Void> deleteTenantTransaction(Transaction tr, TenantNameRef name) {
|
||||
state Key tenantMapKey = name.withPrefix(tenantMapPrefix);
|
||||
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
|
||||
state Optional<TenantMapEntry> tenantEntry = wait(tryGetTenantTransaction(tr, name));
|
||||
if (!tenantEntry.present()) {
|
||||
return Void();
|
||||
}
|
||||
|
||||
state typename transaction_future_type<Transaction, RangeResult>::type prefixRangeFuture =
|
||||
tr->getRange(prefixRange(tenantEntry.get().prefix), 1);
|
||||
RangeResult contents = wait(safeThreadFutureToFuture(prefixRangeFuture));
|
||||
if (!contents.empty()) {
|
||||
throw tenant_not_empty();
|
||||
}
|
||||
|
||||
tr->clear(tenantMapKey);
|
||||
|
||||
return Void();
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<Void> deleteTenant(Reference<DB> db, TenantName name) {
|
||||
state Reference<typename DB::TransactionT> tr = db->createTransaction();
|
||||
|
||||
state bool firstTry = true;
|
||||
loop {
|
||||
try {
|
||||
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
||||
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
||||
|
||||
if (firstTry) {
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenantTransaction(tr, name));
|
||||
if (!entry.present()) {
|
||||
throw tenant_not_found();
|
||||
}
|
||||
|
||||
firstTry = false;
|
||||
}
|
||||
|
||||
wait(deleteTenantTransaction(tr, name));
|
||||
|
||||
if (BUGGIFY) {
|
||||
throw commit_unknown_result();
|
||||
}
|
||||
|
||||
wait(safeThreadFutureToFuture(tr->commit()));
|
||||
|
||||
if (BUGGIFY) {
|
||||
throw commit_unknown_result();
|
||||
}
|
||||
|
||||
TraceEvent("DeletedTenant").detail("Tenant", name).detail("Version", tr->getCommittedVersion());
|
||||
return Void();
|
||||
} catch (Error& e) {
|
||||
wait(safeThreadFutureToFuture(tr->onError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACTOR template <class Transaction>
|
||||
Future<std::map<TenantName, TenantMapEntry>> listTenantsTransaction(Transaction tr,
|
||||
TenantNameRef begin,
|
||||
TenantNameRef end,
|
||||
int limit) {
|
||||
state KeyRange range = KeyRangeRef(begin, end).withPrefix(tenantMapPrefix);
|
||||
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
|
||||
state typename transaction_future_type<Transaction, RangeResult>::type listFuture =
|
||||
tr->getRange(firstGreaterOrEqual(range.begin), firstGreaterOrEqual(range.end), limit);
|
||||
RangeResult results = wait(safeThreadFutureToFuture(listFuture));
|
||||
|
||||
std::map<TenantName, TenantMapEntry> tenants;
|
||||
for (auto kv : results) {
|
||||
tenants[kv.key.removePrefix(tenantMapPrefix)] = decodeTenantEntry(kv.value);
|
||||
}
|
||||
|
||||
return tenants;
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<std::map<TenantName, TenantMapEntry>> listTenants(Reference<DB> db,
|
||||
TenantName begin,
|
||||
TenantName end,
|
||||
int limit) {
|
||||
state Reference<typename DB::TransactionT> tr = db->createTransaction();
|
||||
|
||||
loop {
|
||||
try {
|
||||
tr->setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
||||
tr->setOption(FDBTransactionOptions::READ_LOCK_AWARE);
|
||||
std::map<TenantName, TenantMapEntry> tenants = wait(listTenantsTransaction(tr, begin, end, limit));
|
||||
return tenants;
|
||||
} catch (Error& e) {
|
||||
wait(safeThreadFutureToFuture(tr->onError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace ManagementAPI
|
||||
|
||||
#include "flow/unactorcompiler.h"
|
||||
|
42
fdbclient/GenericTransactionHelper.h
Normal file
42
fdbclient/GenericTransactionHelper.h
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* GenericTransactionHelper.h
|
||||
*
|
||||
* This source file is part of the FoundationDB open source project
|
||||
*
|
||||
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FDBCLIENT_GENERIC_TRANSACTION_HELPER_H
|
||||
#define FDBCLIENT_GENERIC_TRANSACTION_HELPER_H
|
||||
#pragma once
|
||||
|
||||
#include "flow/FastRef.h"
|
||||
|
||||
template <typename Transaction, class T>
|
||||
struct transaction_future_type {
|
||||
using type = typename Transaction::template FutureT<T>;
|
||||
};
|
||||
|
||||
template <typename Transaction, class T>
|
||||
struct transaction_future_type<Transaction*, T> {
|
||||
using type = typename transaction_future_type<Transaction, T>::type;
|
||||
};
|
||||
|
||||
template <typename Transaction, class T>
|
||||
struct transaction_future_type<Reference<Transaction>, T> {
|
||||
using type = typename transaction_future_type<Transaction, T>::type;
|
||||
};
|
||||
|
||||
#endif
|
@ -1340,7 +1340,7 @@ bool MultiVersionTransaction::isValid() {
|
||||
}
|
||||
|
||||
// MultiVersionTenant
|
||||
MultiVersionTenant::MultiVersionTenant(Reference<MultiVersionDatabase> db, StringRef tenantName)
|
||||
MultiVersionTenant::MultiVersionTenant(Reference<MultiVersionDatabase> db, TenantNameRef tenantName)
|
||||
: tenantState(makeReference<TenantState>(db, tenantName)) {}
|
||||
|
||||
MultiVersionTenant::~MultiVersionTenant() {
|
||||
@ -1363,7 +1363,7 @@ ThreadFuture<Void> MultiVersionTenant::waitPurgeGranulesComplete(const KeyRef& p
|
||||
return abortableFuture(f, tenantState->db->dbState->dbVar->get().onChange);
|
||||
}
|
||||
|
||||
MultiVersionTenant::TenantState::TenantState(Reference<MultiVersionDatabase> db, StringRef tenantName)
|
||||
MultiVersionTenant::TenantState::TenantState(Reference<MultiVersionDatabase> db, TenantNameRef tenantName)
|
||||
: tenantVar(new ThreadSafeAsyncVar<Reference<ITenant>>(Reference<ITenant>(nullptr))), tenantName(tenantName), db(db),
|
||||
closed(false) {
|
||||
updateTenant();
|
||||
|
@ -685,7 +685,7 @@ class MultiVersionApi;
|
||||
// it connects with a different version.
|
||||
class MultiVersionTenant final : public ITenant, ThreadSafeReferenceCounted<MultiVersionTenant> {
|
||||
public:
|
||||
MultiVersionTenant(Reference<MultiVersionDatabase> db, StringRef tenantName);
|
||||
MultiVersionTenant(Reference<MultiVersionDatabase> db, TenantNameRef tenantName);
|
||||
~MultiVersionTenant() override;
|
||||
|
||||
Reference<ITransaction> createTransaction() override;
|
||||
@ -699,7 +699,7 @@ public:
|
||||
// A struct that manages the current connection state of the MultiVersionDatabase. This wraps the underlying
|
||||
// IDatabase object that is currently interacting with the cluster.
|
||||
struct TenantState : ThreadSafeReferenceCounted<TenantState> {
|
||||
TenantState(Reference<MultiVersionDatabase> db, StringRef tenantName);
|
||||
TenantState(Reference<MultiVersionDatabase> db, TenantNameRef tenantName);
|
||||
|
||||
// Creates a new underlying tenant object whenever the database connection changes. This change is signaled
|
||||
// to open transactions via an AsyncVar.
|
||||
@ -709,7 +709,7 @@ public:
|
||||
void close();
|
||||
|
||||
Reference<ThreadSafeAsyncVar<Reference<ITenant>>> tenantVar;
|
||||
const Standalone<StringRef> tenantName;
|
||||
const TenantName tenantName;
|
||||
|
||||
Reference<MultiVersionDatabase> db;
|
||||
|
||||
|
@ -2730,123 +2730,3 @@ Future<Optional<std::string>> FailedLocalitiesRangeImpl::commit(ReadYourWritesTr
|
||||
// exclude locality with failed option as true.
|
||||
return excludeLocalityCommitActor(ryw, true);
|
||||
}
|
||||
|
||||
ACTOR Future<RangeResult> getTenantList(ReadYourWritesTransaction* ryw, KeyRangeRef kr, GetRangeLimits limitsHint) {
|
||||
state KeyRef managementPrefix =
|
||||
kr.begin.substr(0,
|
||||
SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin.size() +
|
||||
TenantMapRangeImpl::submoduleRange.begin.size());
|
||||
|
||||
kr = kr.removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin);
|
||||
TenantNameRef beginTenant = kr.begin.removePrefix(TenantMapRangeImpl::submoduleRange.begin);
|
||||
|
||||
TenantNameRef endTenant = kr.end;
|
||||
if (endTenant.startsWith(TenantMapRangeImpl::submoduleRange.begin)) {
|
||||
endTenant = endTenant.removePrefix(TenantMapRangeImpl::submoduleRange.begin);
|
||||
} else {
|
||||
endTenant = "\xff"_sr;
|
||||
}
|
||||
|
||||
std::map<TenantName, TenantMapEntry> tenants =
|
||||
wait(ManagementAPI::listTenantsTransaction(&ryw->getTransaction(), beginTenant, endTenant, limitsHint.rows));
|
||||
|
||||
RangeResult results;
|
||||
for (auto tenant : tenants) {
|
||||
json_spirit::mObject tenantEntry;
|
||||
tenantEntry["id"] = tenant.second.id;
|
||||
tenantEntry["prefix"] = tenant.second.prefix.toString();
|
||||
std::string tenantEntryString = json_spirit::write_string(json_spirit::mValue(tenantEntry));
|
||||
ValueRef tenantEntryBytes(results.arena(), tenantEntryString);
|
||||
results.push_back(results.arena(),
|
||||
KeyValueRef(tenant.first.withPrefix(managementPrefix, results.arena()), tenantEntryBytes));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
TenantMapRangeImpl::TenantMapRangeImpl(KeyRangeRef kr) : SpecialKeyRangeRWImpl(kr) {}
|
||||
|
||||
Future<RangeResult> TenantMapRangeImpl::getRange(ReadYourWritesTransaction* ryw,
|
||||
KeyRangeRef kr,
|
||||
GetRangeLimits limitsHint) const {
|
||||
return getTenantList(ryw, kr, limitsHint);
|
||||
}
|
||||
|
||||
ACTOR Future<Void> createTenants(ReadYourWritesTransaction* ryw, std::vector<TenantNameRef> tenants) {
|
||||
Optional<Value> lastIdVal = wait(ryw->getTransaction().get(tenantLastIdKey));
|
||||
int64_t previousId = lastIdVal.present() ? TenantMapEntry::prefixToId(lastIdVal.get()) : -1;
|
||||
|
||||
std::vector<Future<Void>> createFutures;
|
||||
for (auto tenant : tenants) {
|
||||
createFutures.push_back(
|
||||
success(ManagementAPI::createTenantTransaction(&ryw->getTransaction(), tenant, ++previousId)));
|
||||
}
|
||||
|
||||
ryw->getTransaction().set(tenantLastIdKey, TenantMapEntry::idToPrefix(previousId));
|
||||
wait(waitForAll(createFutures));
|
||||
return Void();
|
||||
}
|
||||
|
||||
ACTOR Future<Void> deleteTenantRange(ReadYourWritesTransaction* ryw,
|
||||
TenantNameRef beginTenant,
|
||||
TenantNameRef endTenant) {
|
||||
std::map<TenantName, TenantMapEntry> tenants = wait(
|
||||
ManagementAPI::listTenantsTransaction(&ryw->getTransaction(), beginTenant, endTenant, CLIENT_KNOBS->TOO_MANY));
|
||||
|
||||
if (tenants.size() == CLIENT_KNOBS->TOO_MANY) {
|
||||
TraceEvent(SevWarn, "DeleteTenantRangeTooLange")
|
||||
.detail("BeginTenant", beginTenant)
|
||||
.detail("EndTenant", endTenant);
|
||||
ryw->setSpecialKeySpaceErrorMsg("too many tenants to range delete");
|
||||
throw special_keys_api_failure();
|
||||
}
|
||||
|
||||
std::vector<Future<Void>> deleteFutures;
|
||||
for (auto tenant : tenants) {
|
||||
deleteFutures.push_back(ManagementAPI::deleteTenantTransaction(&ryw->getTransaction(), tenant.first));
|
||||
}
|
||||
|
||||
wait(waitForAll(deleteFutures));
|
||||
return Void();
|
||||
}
|
||||
|
||||
Future<Optional<std::string>> TenantMapRangeImpl::commit(ReadYourWritesTransaction* ryw) {
|
||||
auto ranges = ryw->getSpecialKeySpaceWriteMap().containedRanges(range);
|
||||
std::vector<TenantNameRef> tenantsToCreate;
|
||||
std::vector<Future<Void>> tenantManagementFutures;
|
||||
for (auto range : ranges) {
|
||||
if (!range.value().first) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TenantNameRef tenantName =
|
||||
range.begin()
|
||||
.removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin)
|
||||
.removePrefix(TenantMapRangeImpl::submoduleRange.begin);
|
||||
|
||||
if (range.value().second.present()) {
|
||||
tenantsToCreate.push_back(tenantName);
|
||||
} else {
|
||||
// For a single key clear, just issue the delete
|
||||
if (KeyRangeRef(range.begin(), range.end()).singleKeyRange()) {
|
||||
tenantManagementFutures.push_back(
|
||||
ManagementAPI::deleteTenantTransaction(&ryw->getTransaction(), tenantName));
|
||||
} else {
|
||||
TenantNameRef endTenant = range.end().removePrefix(
|
||||
SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin);
|
||||
if (endTenant.startsWith(submoduleRange.begin)) {
|
||||
endTenant = endTenant.removePrefix(submoduleRange.begin);
|
||||
} else {
|
||||
endTenant = "\xff"_sr;
|
||||
}
|
||||
tenantManagementFutures.push_back(deleteTenantRange(ryw, tenantName, endTenant));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tenantsToCreate.size()) {
|
||||
tenantManagementFutures.push_back(createTenants(ryw, tenantsToCreate));
|
||||
}
|
||||
|
||||
return tag(waitForAll(tenantManagementFutures), Optional<std::string>());
|
||||
}
|
||||
|
@ -22,6 +22,35 @@
|
||||
#include "fdbclient/Tenant.h"
|
||||
#include "flow/UnitTest.h"
|
||||
|
||||
Key TenantMapEntry::idToPrefix(int64_t id) {
|
||||
int64_t swapped = bigEndian64(id);
|
||||
return StringRef(reinterpret_cast<const uint8_t*>(&swapped), 8);
|
||||
}
|
||||
|
||||
int64_t TenantMapEntry::prefixToId(KeyRef prefix) {
|
||||
ASSERT(prefix.size() == 8);
|
||||
int64_t id = *reinterpret_cast<const int64_t*>(prefix.begin());
|
||||
id = bigEndian64(id);
|
||||
ASSERT(id >= 0);
|
||||
return id;
|
||||
}
|
||||
|
||||
void TenantMapEntry::initPrefix(KeyRef subspace) {
|
||||
ASSERT(id >= 0);
|
||||
prefix = makeString(8 + subspace.size());
|
||||
uint8_t* data = mutateString(prefix);
|
||||
if (subspace.size() > 0) {
|
||||
memcpy(data, subspace.begin(), subspace.size());
|
||||
}
|
||||
int64_t swapped = bigEndian64(id);
|
||||
memcpy(data + subspace.size(), &swapped, 8);
|
||||
}
|
||||
|
||||
TenantMapEntry::TenantMapEntry() : id(-1) {}
|
||||
TenantMapEntry::TenantMapEntry(int64_t id, KeyRef subspace) : id(id) {
|
||||
initPrefix(subspace);
|
||||
}
|
||||
|
||||
TEST_CASE("/fdbclient/TenantMapEntry/Serialization") {
|
||||
TenantMapEntry entry1(1, ""_sr);
|
||||
ASSERT(entry1.prefix == "\x00\x00\x00\x00\x00\x00\x00\x01"_sr);
|
||||
|
@ -32,18 +32,8 @@ typedef Standalone<TenantNameRef> TenantName;
|
||||
struct TenantMapEntry {
|
||||
constexpr static FileIdentifier file_identifier = 12247338;
|
||||
|
||||
static Key idToPrefix(int64_t id) {
|
||||
int64_t swapped = bigEndian64(id);
|
||||
return StringRef(reinterpret_cast<const uint8_t*>(&swapped), 8);
|
||||
}
|
||||
|
||||
static int64_t prefixToId(KeyRef prefix) {
|
||||
ASSERT(prefix.size() == 8);
|
||||
int64_t id = *reinterpret_cast<const int64_t*>(prefix.begin());
|
||||
id = bigEndian64(id);
|
||||
ASSERT(id >= 0);
|
||||
return id;
|
||||
}
|
||||
static Key idToPrefix(int64_t id);
|
||||
static int64_t prefixToId(KeyRef prefix);
|
||||
|
||||
int64_t id;
|
||||
Key prefix;
|
||||
@ -51,20 +41,11 @@ struct TenantMapEntry {
|
||||
constexpr static int ROOT_PREFIX_SIZE = sizeof(id);
|
||||
|
||||
private:
|
||||
void initPrefix(KeyRef subspace) {
|
||||
ASSERT(id >= 0);
|
||||
prefix = makeString(8 + subspace.size());
|
||||
uint8_t* data = mutateString(prefix);
|
||||
if (subspace.size() > 0) {
|
||||
memcpy(data, subspace.begin(), subspace.size());
|
||||
}
|
||||
int64_t swapped = bigEndian64(id);
|
||||
memcpy(data + subspace.size(), &swapped, 8);
|
||||
}
|
||||
void initPrefix(KeyRef subspace);
|
||||
|
||||
public:
|
||||
TenantMapEntry() : id(-1) {}
|
||||
TenantMapEntry(int64_t id, KeyRef subspace) : id(id) { initPrefix(subspace); }
|
||||
TenantMapEntry();
|
||||
TenantMapEntry(int64_t id, KeyRef subspace);
|
||||
|
||||
template <class Ar>
|
||||
void serialize(Ar& ar) {
|
||||
|
290
fdbclient/TenantManagement.actor.h
Normal file
290
fdbclient/TenantManagement.actor.h
Normal file
@ -0,0 +1,290 @@
|
||||
/*
|
||||
* TenantManagement.actor.h
|
||||
*
|
||||
* This source file is part of the FoundationDB open source project
|
||||
*
|
||||
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#if defined(NO_INTELLISENSE) && !defined(FDBCLIENT_TENANT_MANAGEMENT_ACTOR_G_H)
|
||||
#define FDBCLIENT_TENANT_MANAGEMENT_ACTOR_G_H
|
||||
#include "fdbclient/TenantManagement.actor.g.h"
|
||||
#elif !defined(FDBCLIENT_TENANT_MANAGEMENT_ACTOR_H)
|
||||
#define FDBCLIENT_TENANT_MANAGEMENT_ACTOR_H
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include "fdbclient/GenericTransactionHelper.h"
|
||||
#include "fdbclient/SystemData.h"
|
||||
#include "flow/actorcompiler.h" // has to be last include
|
||||
|
||||
namespace TenantAPI {
|
||||
ACTOR template <class Transaction>
|
||||
Future<Optional<TenantMapEntry>> tryGetTenantTransaction(Transaction tr, TenantName name) {
|
||||
state Key tenantMapKey = name.withPrefix(tenantMapPrefix);
|
||||
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
|
||||
state typename transaction_future_type<Transaction, Optional<Value>>::type tenantFuture = tr->get(tenantMapKey);
|
||||
Optional<Value> val = wait(safeThreadFutureToFuture(tenantFuture));
|
||||
return val.map<TenantMapEntry>([](Optional<Value> v) { return decodeTenantEntry(v.get()); });
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<Optional<TenantMapEntry>> tryGetTenant(Reference<DB> db, TenantName name) {
|
||||
state Reference<typename DB::TransactionT> tr = db->createTransaction();
|
||||
|
||||
loop {
|
||||
try {
|
||||
tr->setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
||||
tr->setOption(FDBTransactionOptions::READ_LOCK_AWARE);
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenantTransaction(tr, name));
|
||||
return entry;
|
||||
} catch (Error& e) {
|
||||
wait(safeThreadFutureToFuture(tr->onError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACTOR template <class Transaction>
|
||||
Future<TenantMapEntry> getTenantTransaction(Transaction tr, TenantName name) {
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenantTransaction(tr, name));
|
||||
if (!entry.present()) {
|
||||
throw tenant_not_found();
|
||||
}
|
||||
|
||||
return entry.get();
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<TenantMapEntry> getTenant(Reference<DB> db, TenantName name) {
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenant(db, name));
|
||||
if (!entry.present()) {
|
||||
throw tenant_not_found();
|
||||
}
|
||||
|
||||
return entry.get();
|
||||
}
|
||||
|
||||
// Creates a tenant with the given name. If the tenant already exists, an empty optional will be returned.
|
||||
// The caller must enforce that the tenant ID be unique from all current and past tenants, and it must also be unique
|
||||
// from all other tenants created in the same transaction.
|
||||
ACTOR template <class Transaction>
|
||||
Future<std::pair<TenantMapEntry, bool>> createTenantTransaction(Transaction tr, TenantNameRef name, int64_t tenantId) {
|
||||
state Key tenantMapKey = name.withPrefix(tenantMapPrefix);
|
||||
|
||||
if (name.startsWith("\xff"_sr)) {
|
||||
throw invalid_tenant_name();
|
||||
}
|
||||
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
|
||||
state Future<Optional<TenantMapEntry>> tenantEntryFuture = tryGetTenantTransaction(tr, name);
|
||||
state typename transaction_future_type<Transaction, Optional<Value>>::type tenantDataPrefixFuture =
|
||||
tr->get(tenantDataPrefixKey);
|
||||
state typename transaction_future_type<Transaction, Optional<Value>>::type tenantModeFuture =
|
||||
tr->get(configKeysPrefix.withSuffix("tenant_mode"_sr));
|
||||
|
||||
Optional<Value> tenantMode = wait(safeThreadFutureToFuture(tenantModeFuture));
|
||||
|
||||
if (!tenantMode.present() || tenantMode.get() == StringRef(format("%d", TenantMode::DISABLED))) {
|
||||
throw tenants_disabled();
|
||||
}
|
||||
|
||||
Optional<TenantMapEntry> tenantEntry = wait(tenantEntryFuture);
|
||||
if (tenantEntry.present()) {
|
||||
return std::make_pair(tenantEntry.get(), false);
|
||||
}
|
||||
|
||||
Optional<Value> tenantDataPrefix = wait(safeThreadFutureToFuture(tenantDataPrefixFuture));
|
||||
if (tenantDataPrefix.present() &&
|
||||
tenantDataPrefix.get().size() + TenantMapEntry::ROOT_PREFIX_SIZE > CLIENT_KNOBS->TENANT_PREFIX_SIZE_LIMIT) {
|
||||
TraceEvent(SevWarnAlways, "TenantPrefixTooLarge")
|
||||
.detail("TenantSubspace", tenantDataPrefix.get())
|
||||
.detail("TenantSubspaceLength", tenantDataPrefix.get().size())
|
||||
.detail("RootPrefixLength", TenantMapEntry::ROOT_PREFIX_SIZE)
|
||||
.detail("MaxTenantPrefixSize", CLIENT_KNOBS->TENANT_PREFIX_SIZE_LIMIT);
|
||||
|
||||
throw client_invalid_operation();
|
||||
}
|
||||
|
||||
state TenantMapEntry newTenant(tenantId, tenantDataPrefix.present() ? (KeyRef)tenantDataPrefix.get() : ""_sr);
|
||||
|
||||
state typename transaction_future_type<Transaction, RangeResult>::type prefixRangeFuture =
|
||||
tr->getRange(prefixRange(newTenant.prefix), 1);
|
||||
RangeResult contents = wait(safeThreadFutureToFuture(prefixRangeFuture));
|
||||
if (!contents.empty()) {
|
||||
throw tenant_prefix_allocator_conflict();
|
||||
}
|
||||
|
||||
tr->set(tenantMapKey, encodeTenantEntry(newTenant));
|
||||
|
||||
return std::make_pair(newTenant, true);
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<TenantMapEntry> createTenant(Reference<DB> db, TenantName name) {
|
||||
state Reference<typename DB::TransactionT> tr = db->createTransaction();
|
||||
|
||||
state bool firstTry = true;
|
||||
loop {
|
||||
try {
|
||||
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
||||
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
||||
state typename DB::TransactionT::template FutureT<Optional<Value>> lastIdFuture = tr->get(tenantLastIdKey);
|
||||
|
||||
if (firstTry) {
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenantTransaction(tr, name));
|
||||
if (entry.present()) {
|
||||
throw tenant_already_exists();
|
||||
}
|
||||
|
||||
firstTry = false;
|
||||
}
|
||||
|
||||
Optional<Value> lastIdVal = wait(safeThreadFutureToFuture(lastIdFuture));
|
||||
int64_t tenantId = lastIdVal.present() ? TenantMapEntry::prefixToId(lastIdVal.get()) + 1 : 0;
|
||||
tr->set(tenantLastIdKey, TenantMapEntry::idToPrefix(tenantId));
|
||||
state std::pair<TenantMapEntry, bool> newTenant = wait(createTenantTransaction(tr, name, tenantId));
|
||||
|
||||
if (BUGGIFY) {
|
||||
throw commit_unknown_result();
|
||||
}
|
||||
|
||||
wait(safeThreadFutureToFuture(tr->commit()));
|
||||
|
||||
if (BUGGIFY) {
|
||||
throw commit_unknown_result();
|
||||
}
|
||||
|
||||
TraceEvent("CreatedTenant")
|
||||
.detail("Tenant", name)
|
||||
.detail("TenantId", newTenant.first.id)
|
||||
.detail("Prefix", newTenant.first.prefix)
|
||||
.detail("Version", tr->getCommittedVersion());
|
||||
|
||||
return newTenant.first;
|
||||
} catch (Error& e) {
|
||||
wait(safeThreadFutureToFuture(tr->onError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACTOR template <class Transaction>
|
||||
Future<Void> deleteTenantTransaction(Transaction tr, TenantNameRef name) {
|
||||
state Key tenantMapKey = name.withPrefix(tenantMapPrefix);
|
||||
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
|
||||
state Optional<TenantMapEntry> tenantEntry = wait(tryGetTenantTransaction(tr, name));
|
||||
if (!tenantEntry.present()) {
|
||||
return Void();
|
||||
}
|
||||
|
||||
state typename transaction_future_type<Transaction, RangeResult>::type prefixRangeFuture =
|
||||
tr->getRange(prefixRange(tenantEntry.get().prefix), 1);
|
||||
RangeResult contents = wait(safeThreadFutureToFuture(prefixRangeFuture));
|
||||
if (!contents.empty()) {
|
||||
throw tenant_not_empty();
|
||||
}
|
||||
|
||||
tr->clear(tenantMapKey);
|
||||
|
||||
return Void();
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<Void> deleteTenant(Reference<DB> db, TenantName name) {
|
||||
state Reference<typename DB::TransactionT> tr = db->createTransaction();
|
||||
|
||||
state bool firstTry = true;
|
||||
loop {
|
||||
try {
|
||||
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
||||
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
||||
|
||||
if (firstTry) {
|
||||
Optional<TenantMapEntry> entry = wait(tryGetTenantTransaction(tr, name));
|
||||
if (!entry.present()) {
|
||||
throw tenant_not_found();
|
||||
}
|
||||
|
||||
firstTry = false;
|
||||
}
|
||||
|
||||
wait(deleteTenantTransaction(tr, name));
|
||||
|
||||
if (BUGGIFY) {
|
||||
throw commit_unknown_result();
|
||||
}
|
||||
|
||||
wait(safeThreadFutureToFuture(tr->commit()));
|
||||
|
||||
if (BUGGIFY) {
|
||||
throw commit_unknown_result();
|
||||
}
|
||||
|
||||
TraceEvent("DeletedTenant").detail("Tenant", name).detail("Version", tr->getCommittedVersion());
|
||||
return Void();
|
||||
} catch (Error& e) {
|
||||
wait(safeThreadFutureToFuture(tr->onError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACTOR template <class Transaction>
|
||||
Future<std::map<TenantName, TenantMapEntry>> listTenantsTransaction(Transaction tr,
|
||||
TenantNameRef begin,
|
||||
TenantNameRef end,
|
||||
int limit) {
|
||||
state KeyRange range = KeyRangeRef(begin, end).withPrefix(tenantMapPrefix);
|
||||
|
||||
tr->setOption(FDBTransactionOptions::RAW_ACCESS);
|
||||
|
||||
state typename transaction_future_type<Transaction, RangeResult>::type listFuture =
|
||||
tr->getRange(firstGreaterOrEqual(range.begin), firstGreaterOrEqual(range.end), limit);
|
||||
RangeResult results = wait(safeThreadFutureToFuture(listFuture));
|
||||
|
||||
std::map<TenantName, TenantMapEntry> tenants;
|
||||
for (auto kv : results) {
|
||||
tenants[kv.key.removePrefix(tenantMapPrefix)] = decodeTenantEntry(kv.value);
|
||||
}
|
||||
|
||||
return tenants;
|
||||
}
|
||||
|
||||
ACTOR template <class DB>
|
||||
Future<std::map<TenantName, TenantMapEntry>> listTenants(Reference<DB> db,
|
||||
TenantName begin,
|
||||
TenantName end,
|
||||
int limit) {
|
||||
state Reference<typename DB::TransactionT> tr = db->createTransaction();
|
||||
|
||||
loop {
|
||||
try {
|
||||
tr->setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
||||
tr->setOption(FDBTransactionOptions::READ_LOCK_AWARE);
|
||||
std::map<TenantName, TenantMapEntry> tenants = wait(listTenantsTransaction(tr, begin, end, limit));
|
||||
return tenants;
|
||||
} catch (Error& e) {
|
||||
wait(safeThreadFutureToFuture(tr->onError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace TenantAPI
|
||||
|
||||
#include "flow/unactorcompiler.h"
|
||||
#endif
|
149
fdbclient/TenantSpecialKeys.actor.cpp
Normal file
149
fdbclient/TenantSpecialKeys.actor.cpp
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* TenantSpecialKeys.actor.cpp
|
||||
*
|
||||
* This source file is part of the FoundationDB open source project
|
||||
*
|
||||
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "fdbclient/ActorLineageProfiler.h"
|
||||
#include "fdbclient/FDBOptions.g.h"
|
||||
#include "fdbclient/Knobs.h"
|
||||
#include "fdbclient/DatabaseContext.h"
|
||||
#include "fdbclient/SpecialKeySpace.actor.h"
|
||||
#include "fdbclient/TenantManagement.actor.h"
|
||||
#include "flow/Arena.h"
|
||||
#include "flow/UnitTest.h"
|
||||
#include "flow/actorcompiler.h" // This must be the last #include.
|
||||
|
||||
ACTOR Future<RangeResult> getTenantList(ReadYourWritesTransaction* ryw, KeyRangeRef kr, GetRangeLimits limitsHint) {
|
||||
state KeyRef managementPrefix =
|
||||
kr.begin.substr(0,
|
||||
SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin.size() +
|
||||
TenantMapRangeImpl::submoduleRange.begin.size());
|
||||
|
||||
kr = kr.removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin);
|
||||
TenantNameRef beginTenant = kr.begin.removePrefix(TenantMapRangeImpl::submoduleRange.begin);
|
||||
|
||||
TenantNameRef endTenant = kr.end;
|
||||
if (endTenant.startsWith(TenantMapRangeImpl::submoduleRange.begin)) {
|
||||
endTenant = endTenant.removePrefix(TenantMapRangeImpl::submoduleRange.begin);
|
||||
} else {
|
||||
endTenant = "\xff"_sr;
|
||||
}
|
||||
|
||||
std::map<TenantName, TenantMapEntry> tenants =
|
||||
wait(TenantAPI::listTenantsTransaction(&ryw->getTransaction(), beginTenant, endTenant, limitsHint.rows));
|
||||
|
||||
RangeResult results;
|
||||
for (auto tenant : tenants) {
|
||||
json_spirit::mObject tenantEntry;
|
||||
tenantEntry["id"] = tenant.second.id;
|
||||
tenantEntry["prefix"] = tenant.second.prefix.toString();
|
||||
std::string tenantEntryString = json_spirit::write_string(json_spirit::mValue(tenantEntry));
|
||||
ValueRef tenantEntryBytes(results.arena(), tenantEntryString);
|
||||
results.push_back(results.arena(),
|
||||
KeyValueRef(tenant.first.withPrefix(managementPrefix, results.arena()), tenantEntryBytes));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
TenantMapRangeImpl::TenantMapRangeImpl(KeyRangeRef kr) : SpecialKeyRangeRWImpl(kr) {}
|
||||
|
||||
Future<RangeResult> TenantMapRangeImpl::getRange(ReadYourWritesTransaction* ryw,
|
||||
KeyRangeRef kr,
|
||||
GetRangeLimits limitsHint) const {
|
||||
return getTenantList(ryw, kr, limitsHint);
|
||||
}
|
||||
|
||||
ACTOR Future<Void> createTenants(ReadYourWritesTransaction* ryw, std::vector<TenantNameRef> tenants) {
|
||||
Optional<Value> lastIdVal = wait(ryw->getTransaction().get(tenantLastIdKey));
|
||||
int64_t previousId = lastIdVal.present() ? TenantMapEntry::prefixToId(lastIdVal.get()) : -1;
|
||||
|
||||
std::vector<Future<Void>> createFutures;
|
||||
for (auto tenant : tenants) {
|
||||
createFutures.push_back(
|
||||
success(TenantAPI::createTenantTransaction(&ryw->getTransaction(), tenant, ++previousId)));
|
||||
}
|
||||
|
||||
ryw->getTransaction().set(tenantLastIdKey, TenantMapEntry::idToPrefix(previousId));
|
||||
wait(waitForAll(createFutures));
|
||||
return Void();
|
||||
}
|
||||
|
||||
ACTOR Future<Void> deleteTenantRange(ReadYourWritesTransaction* ryw,
|
||||
TenantNameRef beginTenant,
|
||||
TenantNameRef endTenant) {
|
||||
std::map<TenantName, TenantMapEntry> tenants =
|
||||
wait(TenantAPI::listTenantsTransaction(&ryw->getTransaction(), beginTenant, endTenant, CLIENT_KNOBS->TOO_MANY));
|
||||
|
||||
if (tenants.size() == CLIENT_KNOBS->TOO_MANY) {
|
||||
TraceEvent(SevWarn, "DeleteTenantRangeTooLange")
|
||||
.detail("BeginTenant", beginTenant)
|
||||
.detail("EndTenant", endTenant);
|
||||
ryw->setSpecialKeySpaceErrorMsg("too many tenants to range delete");
|
||||
throw special_keys_api_failure();
|
||||
}
|
||||
|
||||
std::vector<Future<Void>> deleteFutures;
|
||||
for (auto tenant : tenants) {
|
||||
deleteFutures.push_back(TenantAPI::deleteTenantTransaction(&ryw->getTransaction(), tenant.first));
|
||||
}
|
||||
|
||||
wait(waitForAll(deleteFutures));
|
||||
return Void();
|
||||
}
|
||||
|
||||
Future<Optional<std::string>> TenantMapRangeImpl::commit(ReadYourWritesTransaction* ryw) {
|
||||
auto ranges = ryw->getSpecialKeySpaceWriteMap().containedRanges(range);
|
||||
std::vector<TenantNameRef> tenantsToCreate;
|
||||
std::vector<Future<Void>> tenantManagementFutures;
|
||||
for (auto range : ranges) {
|
||||
if (!range.value().first) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TenantNameRef tenantName =
|
||||
range.begin()
|
||||
.removePrefix(SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin)
|
||||
.removePrefix(TenantMapRangeImpl::submoduleRange.begin);
|
||||
|
||||
if (range.value().second.present()) {
|
||||
tenantsToCreate.push_back(tenantName);
|
||||
} else {
|
||||
// For a single key clear, just issue the delete
|
||||
if (KeyRangeRef(range.begin(), range.end()).singleKeyRange()) {
|
||||
tenantManagementFutures.push_back(
|
||||
TenantAPI::deleteTenantTransaction(&ryw->getTransaction(), tenantName));
|
||||
} else {
|
||||
TenantNameRef endTenant = range.end().removePrefix(
|
||||
SpecialKeySpace::getModuleRange(SpecialKeySpace::MODULE::MANAGEMENT).begin);
|
||||
if (endTenant.startsWith(submoduleRange.begin)) {
|
||||
endTenant = endTenant.removePrefix(submoduleRange.begin);
|
||||
} else {
|
||||
endTenant = "\xff"_sr;
|
||||
}
|
||||
tenantManagementFutures.push_back(deleteTenantRange(ryw, tenantName, endTenant));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tenantsToCreate.size()) {
|
||||
tenantManagementFutures.push_back(createTenants(ryw, tenantsToCreate));
|
||||
}
|
||||
|
||||
return tag(waitForAll(tenantManagementFutures), Optional<std::string>());
|
||||
}
|
@ -175,7 +175,7 @@ Reference<ITransaction> ThreadSafeTenant::createTransaction() {
|
||||
|
||||
ThreadFuture<Key> ThreadSafeTenant::purgeBlobGranules(const KeyRangeRef& keyRange, Version purgeVersion, bool force) {
|
||||
DatabaseContext* db = this->db->db;
|
||||
Standalone<StringRef> tenantName = this->name;
|
||||
TenantName tenantName = this->name;
|
||||
KeyRange range = keyRange;
|
||||
return onMainThread([db, range, purgeVersion, tenantName, force]() -> Future<Key> {
|
||||
return db->purgeBlobGranules(range, purgeVersion, tenantName, force);
|
||||
|
@ -638,7 +638,7 @@ private:
|
||||
if (m.param1.startsWith(tenantMapPrefix)) {
|
||||
if (tenantMap) {
|
||||
ASSERT(version != invalidVersion);
|
||||
Standalone<StringRef> tenantName = m.param1.removePrefix(tenantMapPrefix);
|
||||
TenantName tenantName = m.param1.removePrefix(tenantMapPrefix);
|
||||
TenantMapEntry tenantEntry = decodeTenantEntry(m.param2);
|
||||
|
||||
TraceEvent("CommitProxyInsertTenant", dbgid).detail("Tenant", tenantName).detail("Version", version);
|
||||
|
@ -844,7 +844,7 @@ ACTOR Future<Void> monitorClientRanges(Reference<BlobManagerData> bmData) {
|
||||
std::vector<std::pair<TenantName, TenantMapEntry>> tenants;
|
||||
std::vector<Key> prefixes;
|
||||
for (auto& it : tenantResults) {
|
||||
StringRef tenantName = it.key.removePrefix(tenantMapPrefix);
|
||||
TenantNameRef tenantName = it.key.removePrefix(tenantMapPrefix);
|
||||
TenantMapEntry entry = decodeTenantEntry(it.value);
|
||||
tenants.push_back(std::pair(tenantName, entry));
|
||||
prefixes.push_back(entry.prefix);
|
||||
|
@ -3197,7 +3197,7 @@ ACTOR Future<Void> monitorTenants(Reference<BlobWorkerData> bwData) {
|
||||
std::vector<std::pair<TenantName, TenantMapEntry>> tenants;
|
||||
for (auto& it : tenantResults) {
|
||||
// FIXME: handle removing/moving tenants!
|
||||
StringRef tenantName = it.key.removePrefix(tenantMapPrefix);
|
||||
TenantNameRef tenantName = it.key.removePrefix(tenantMapPrefix);
|
||||
TenantMapEntry entry = decodeTenantEntry(it.value);
|
||||
tenants.push_back(std::pair(tenantName, entry));
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ set(FDBSERVER_SRCS
|
||||
workloads/TagThrottleApi.actor.cpp
|
||||
workloads/TargetedKill.actor.cpp
|
||||
workloads/TaskBucketCorrectness.actor.cpp
|
||||
workloads/TenantManagement.actor.cpp
|
||||
workloads/TenantManagementWorkload.actor.cpp
|
||||
workloads/ThreadSafety.actor.cpp
|
||||
workloads/Throttling.actor.cpp
|
||||
workloads/Throughput.actor.cpp
|
||||
@ -346,7 +346,7 @@ add_library(fdb_sqlite STATIC
|
||||
sqlite/sqlite3.amalgamation.c)
|
||||
target_include_directories(fdb_sqlite PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/sqlite)
|
||||
|
||||
if (WITH_ROCKSDB_EXPERIMENTAL)
|
||||
if(WITH_ROCKSDB_EXPERIMENTAL)
|
||||
add_definitions(-DSSD_ROCKSDB_EXPERIMENTAL)
|
||||
|
||||
include(CompileRocksDB)
|
||||
@ -354,7 +354,7 @@ if (WITH_ROCKSDB_EXPERIMENTAL)
|
||||
# statically, so find the static library here.
|
||||
find_library(lz4_STATIC_LIBRARIES
|
||||
NAMES liblz4.a REQUIRED)
|
||||
if (WITH_LIBURING)
|
||||
if(WITH_LIBURING)
|
||||
find_package(uring)
|
||||
endif()
|
||||
endif()
|
||||
@ -374,7 +374,7 @@ target_include_directories(fdbserver PRIVATE
|
||||
${CMAKE_BINARY_DIR}/bindings/c
|
||||
${CMAKE_CURRENT_BINARY_DIR}/workloads
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/workloads)
|
||||
if (WITH_ROCKSDB_EXPERIMENTAL)
|
||||
if(WITH_ROCKSDB_EXPERIMENTAL)
|
||||
add_dependencies(fdbserver rocksdb)
|
||||
if(WITH_LIBURING)
|
||||
target_include_directories(fdbserver PRIVATE ${ROCKSDB_INCLUDE_DIR} ${uring_INCLUDE_DIR})
|
||||
@ -392,7 +392,7 @@ endif()
|
||||
target_link_libraries(fdbserver PRIVATE toml11_target jemalloc rapidjson)
|
||||
# target_compile_definitions(fdbserver PRIVATE -DENABLE_SAMPLING)
|
||||
|
||||
if (GPERFTOOLS_FOUND)
|
||||
if(GPERFTOOLS_FOUND)
|
||||
target_link_libraries(fdbserver PRIVATE gperftools)
|
||||
endif()
|
||||
|
||||
|
@ -31,6 +31,7 @@
|
||||
#include "fdbclient/ClusterInterface.h"
|
||||
#include "fdbclient/NativeAPI.actor.h"
|
||||
#include "fdbclient/SystemData.h"
|
||||
#include "fdbclient/TenantManagement.actor.h"
|
||||
#include "fdbserver/KnobProtectiveGroups.h"
|
||||
#include "fdbserver/TesterInterface.actor.h"
|
||||
#include "fdbserver/WorkerInterface.actor.h"
|
||||
@ -779,7 +780,7 @@ ACTOR Future<Void> clearData(Database cx) {
|
||||
// it should disable the default tenant.
|
||||
if (!rangeResult.empty()) {
|
||||
if (cx->defaultTenant.present()) {
|
||||
TenantMapEntry entry = wait(ManagementAPI::getTenant(cx.getReference(), cx->defaultTenant.get()));
|
||||
TenantMapEntry entry = wait(TenantAPI::getTenant(cx.getReference(), cx->defaultTenant.get()));
|
||||
tenantPrefix = entry.prefix;
|
||||
}
|
||||
|
||||
@ -1629,7 +1630,7 @@ ACTOR Future<Void> runTests(Reference<AsyncVar<Optional<struct ClusterController
|
||||
std::vector<Future<Void>> tenantFutures;
|
||||
for (auto tenant : tenantsToCreate) {
|
||||
TraceEvent("CreatingTenant").detail("Tenant", tenant);
|
||||
tenantFutures.push_back(success(ManagementAPI::createTenant(cx.getReference(), tenant)));
|
||||
tenantFutures.push_back(success(TenantAPI::createTenant(cx.getReference(), tenant)));
|
||||
}
|
||||
|
||||
wait(waitForAll(tenantFutures));
|
||||
|
@ -30,6 +30,7 @@
|
||||
#include "fdbclient/NativeAPI.actor.h"
|
||||
#include "fdbclient/ReadYourWrites.h"
|
||||
#include "fdbclient/SystemData.h"
|
||||
#include "fdbclient/TenantManagement.actor.h"
|
||||
#include "fdbserver/BlobGranuleServerCommon.actor.h"
|
||||
#include "fdbserver/BlobGranuleValidation.actor.h"
|
||||
#include "fdbserver/Knobs.h"
|
||||
@ -207,7 +208,7 @@ struct BlobGranuleCorrectnessWorkload : TestWorkload {
|
||||
fmt::print("Setting up blob granule range for tenant {0}\n", name.printable());
|
||||
}
|
||||
|
||||
TenantMapEntry entry = wait(ManagementAPI::createTenant(cx.getReference(), name));
|
||||
TenantMapEntry entry = wait(TenantAPI::createTenant(cx.getReference(), name));
|
||||
|
||||
if (BGW_DEBUG) {
|
||||
fmt::print("Set up blob granule range for tenant {0}: {1}\n", name.printable(), entry.prefix.printable());
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "fdbclient/FDBOptions.g.h"
|
||||
#include "fdbserver/TesterInterface.actor.h"
|
||||
#include "fdbclient/GenericManagementAPI.actor.h"
|
||||
#include "fdbclient/TenantManagement.actor.h"
|
||||
#include "fdbclient/ThreadSafeTransaction.h"
|
||||
#include "flow/ActorCollection.h"
|
||||
#include "fdbserver/workloads/workloads.actor.h"
|
||||
@ -225,7 +226,7 @@ struct FuzzApiCorrectnessWorkload : TestWorkload {
|
||||
|
||||
// The last tenant will not be created
|
||||
if (i < self->numTenants) {
|
||||
tenantFutures.push_back(::success(ManagementAPI::createTenant(cx.getReference(), tenantName)));
|
||||
tenantFutures.push_back(::success(TenantAPI::createTenant(cx.getReference(), tenantName)));
|
||||
self->createdTenants.insert(tenantName);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* TenantManagement.actor.cpp
|
||||
* TenantManagementWorkload.actor.cpp
|
||||
*
|
||||
* This source file is part of the FoundationDB open source project
|
||||
*
|
||||
@ -21,7 +21,7 @@
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include "fdbclient/FDBOptions.g.h"
|
||||
#include "fdbclient/GenericManagementAPI.actor.h"
|
||||
#include "fdbclient/TenantManagement.actor.h"
|
||||
#include "fdbrpc/simulator.h"
|
||||
#include "fdbserver/workloads/workloads.actor.h"
|
||||
#include "fdbserver/Knobs.h"
|
||||
@ -163,7 +163,7 @@ struct TenantManagementWorkload : TestWorkload {
|
||||
wait(tr->commit());
|
||||
} else if (operationType == OperationType::MANAGEMENT_DATABASE) {
|
||||
ASSERT(tenantsToCreate.size() == 1);
|
||||
wait(success(ManagementAPI::createTenant(cx.getReference(), *tenantsToCreate.begin())));
|
||||
wait(success(TenantAPI::createTenant(cx.getReference(), *tenantsToCreate.begin())));
|
||||
} else {
|
||||
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
||||
|
||||
@ -172,8 +172,7 @@ struct TenantManagementWorkload : TestWorkload {
|
||||
|
||||
std::vector<Future<Void>> createFutures;
|
||||
for (auto tenant : tenantsToCreate) {
|
||||
createFutures.push_back(
|
||||
success(ManagementAPI::createTenantTransaction(tr, tenant, ++previousId)));
|
||||
createFutures.push_back(success(TenantAPI::createTenantTransaction(tr, tenant, ++previousId)));
|
||||
}
|
||||
tr->set(tenantLastIdKey, TenantMapEntry::idToPrefix(previousId));
|
||||
wait(waitForAll(createFutures));
|
||||
@ -192,8 +191,7 @@ struct TenantManagementWorkload : TestWorkload {
|
||||
continue;
|
||||
}
|
||||
|
||||
state Optional<TenantMapEntry> entry =
|
||||
wait(ManagementAPI::tryGetTenant(cx.getReference(), *tenantItr));
|
||||
state Optional<TenantMapEntry> entry = wait(TenantAPI::tryGetTenant(cx.getReference(), *tenantItr));
|
||||
ASSERT(entry.present());
|
||||
ASSERT(entry.get().id > self->maxId);
|
||||
ASSERT(entry.get().prefix.startsWith(self->tenantSubspace));
|
||||
@ -340,13 +338,13 @@ struct TenantManagementWorkload : TestWorkload {
|
||||
} else if (operationType == OperationType::MANAGEMENT_DATABASE) {
|
||||
ASSERT(tenants.size() == 1);
|
||||
for (tenantIndex = 0; tenantIndex != tenants.size(); ++tenantIndex) {
|
||||
wait(ManagementAPI::deleteTenant(cx.getReference(), tenants[tenantIndex]));
|
||||
wait(TenantAPI::deleteTenant(cx.getReference(), tenants[tenantIndex]));
|
||||
}
|
||||
} else {
|
||||
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
||||
std::vector<Future<Void>> deleteFutures;
|
||||
for (tenantIndex = 0; tenantIndex != tenants.size(); ++tenantIndex) {
|
||||
deleteFutures.push_back(ManagementAPI::deleteTenantTransaction(tr, tenants[tenantIndex]));
|
||||
deleteFutures.push_back(TenantAPI::deleteTenantTransaction(tr, tenants[tenantIndex]));
|
||||
}
|
||||
|
||||
wait(waitForAll(deleteFutures));
|
||||
@ -452,11 +450,11 @@ struct TenantManagementWorkload : TestWorkload {
|
||||
}
|
||||
entry = TenantManagementWorkload::jsonToTenantMapEntry(value.get());
|
||||
} else if (operationType == OperationType::MANAGEMENT_DATABASE) {
|
||||
TenantMapEntry _entry = wait(ManagementAPI::getTenant(cx.getReference(), tenant));
|
||||
TenantMapEntry _entry = wait(TenantAPI::getTenant(cx.getReference(), tenant));
|
||||
entry = _entry;
|
||||
} else {
|
||||
tr->setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
||||
TenantMapEntry _entry = wait(ManagementAPI::getTenantTransaction(tr, tenant));
|
||||
TenantMapEntry _entry = wait(TenantAPI::getTenantTransaction(tr, tenant));
|
||||
entry = _entry;
|
||||
}
|
||||
ASSERT(alreadyExists);
|
||||
@ -510,12 +508,12 @@ struct TenantManagementWorkload : TestWorkload {
|
||||
}
|
||||
} else if (operationType == OperationType::MANAGEMENT_DATABASE) {
|
||||
std::map<TenantName, TenantMapEntry> _tenants =
|
||||
wait(ManagementAPI::listTenants(cx.getReference(), beginTenant, endTenant, limit));
|
||||
wait(TenantAPI::listTenants(cx.getReference(), beginTenant, endTenant, limit));
|
||||
tenants = _tenants;
|
||||
} else {
|
||||
tr->setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
||||
std::map<TenantName, TenantMapEntry> _tenants =
|
||||
wait(ManagementAPI::listTenantsTransaction(tr, beginTenant, endTenant, limit));
|
||||
wait(TenantAPI::listTenantsTransaction(tr, beginTenant, endTenant, limit));
|
||||
tenants = _tenants;
|
||||
}
|
||||
|
||||
@ -601,7 +599,7 @@ struct TenantManagementWorkload : TestWorkload {
|
||||
|
||||
loop {
|
||||
std::map<TenantName, TenantMapEntry> tenants =
|
||||
wait(ManagementAPI::listTenants(cx.getReference(), beginTenant, endTenant, 1000));
|
||||
wait(TenantAPI::listTenants(cx.getReference(), beginTenant, endTenant, 1000));
|
||||
|
||||
TenantNameRef lastTenant;
|
||||
for (auto tenant : tenants) {
|
@ -21,6 +21,7 @@
|
||||
#include "fdbclient/ClusterConnectionMemoryRecord.h"
|
||||
#include "fdbclient/ManagementAPI.actor.h"
|
||||
#include "fdbclient/NativeAPI.actor.h"
|
||||
#include "fdbclient/TenantManagement.actor.h"
|
||||
#include "fdbserver/TesterInterface.actor.h"
|
||||
#include "fdbclient/ReadYourWrites.h"
|
||||
#include "flow/ActorCollection.h"
|
||||
@ -118,7 +119,7 @@ struct WriteDuringReadWorkload : TestWorkload {
|
||||
// If we are operating in the default tenant but enable raw access, we should only write keys
|
||||
// in the tenant's key-space.
|
||||
if (self->useSystemKeys && cx->defaultTenant.present() && self->keyPrefix < systemKeys.begin) {
|
||||
TenantMapEntry entry = wait(ManagementAPI::getTenant(cx.getReference(), cx->defaultTenant.get()));
|
||||
TenantMapEntry entry = wait(TenantAPI::getTenant(cx.getReference(), cx->defaultTenant.get()));
|
||||
self->keyPrefix = entry.prefix.withSuffix(self->keyPrefix).toString();
|
||||
}
|
||||
return Void();
|
||||
|
Loading…
x
Reference in New Issue
Block a user