712 lines
28 KiB
C++
712 lines
28 KiB
C++
/*
|
|
* BulkDumping.cpp
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2026 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/BulkDumping.h"
|
|
#include "fdbclient/BulkLoading.h"
|
|
#include "fdbclient/FDBTypes.h"
|
|
#include "fdbclient/ManagementAPI.h"
|
|
#include "fdbclient/NativeAPI.actor.h"
|
|
#include "fdbclient/RangeLock.h"
|
|
#include "fdbserver/core/Knobs.h"
|
|
#include "fdbserver/tester/workloads.h"
|
|
#include "fdbserver/mocks3/MockS3Server.h"
|
|
#include "fdbserver/mocks3/MockS3ServerChaos.h"
|
|
#include "flow/Error.h"
|
|
#include "flow/IRandom.h"
|
|
#include "flow/Platform.h"
|
|
#include "flow/UnitTest.h"
|
|
#include "fdbrpc/simulator.h"
|
|
|
|
const std::string simulationBulkDumpFolder = joinPath("simfdb", "bulkdump");
|
|
|
|
struct BulkDumping : TestWorkload {
|
|
static constexpr auto NAME = "BulkDumpingWorkload";
|
|
const bool enabled = true;
|
|
bool pass = true;
|
|
int cancelTimes = 0;
|
|
int maxCancelTimes = 0;
|
|
BulkLoadTransportMethod bulkLoadTransportMethod = BulkLoadTransportMethod::CP; // Default to CP method
|
|
std::string jobRoot = "";
|
|
|
|
// Chaos injection options
|
|
bool enableChaos = false;
|
|
double errorRate = 0.1;
|
|
double throttleRate = 0.05;
|
|
double delayRate = 0.1;
|
|
double corruptionRate = 0.01;
|
|
double maxDelay = 2.0;
|
|
|
|
// Timeout configuration
|
|
double jobCompletionTimeout = 1800.0; // Timeout for waiting on bulk dump/load job completion
|
|
double jobSubmitTimeout = 60.0; // Timeout for submitBulkDumpJob/submitBulkLoadJob operations
|
|
|
|
// This workload is not compatible with following workload because they will race in changing the DD mode
|
|
// This workload is not compatible with RandomRangeLock for the conflict in range lock
|
|
void disableFailureInjectionWorkloads(std::set<std::string>& out) const override {
|
|
out.insert({ "RandomMoveKeys",
|
|
"DataLossRecovery",
|
|
"PerpetualWiggleStatsWorkload",
|
|
"PhysicalShardMove",
|
|
"StorageCorruption",
|
|
"StorageServerCheckpointRestoreTest",
|
|
"ValidateStorage",
|
|
"RandomRangeLock",
|
|
"BulkLoading" });
|
|
}
|
|
|
|
explicit BulkDumping(WorkloadContext const& wcx)
|
|
: TestWorkload(wcx), enabled(true), pass(true), cancelTimes(0),
|
|
maxCancelTimes(getOption(options, "maxCancelTimes"_sr, deterministicRandom()->randomInt(0, 2))),
|
|
bulkLoadTransportMethod(
|
|
static_cast<BulkLoadTransportMethod>(getOption(options, "bulkLoadTransportMethod"_sr, 1))),
|
|
jobRoot(getOption(options, "jobRoot"_sr, ""_sr).toString()) {
|
|
maxCancelTimes = 0; // TODO(BulkLoad): allow to cancel job when job ID randomly generated.
|
|
|
|
// Initialize chaos options
|
|
enableChaos = getOption(options, "enableChaos"_sr, false);
|
|
errorRate = getOption(options, "errorRate"_sr, 0.1);
|
|
throttleRate = getOption(options, "throttleRate"_sr, 0.05);
|
|
delayRate = getOption(options, "delayRate"_sr, 0.1);
|
|
corruptionRate = getOption(options, "corruptionRate"_sr, 0.01);
|
|
maxDelay = getOption(options, "maxDelay"_sr, 2.0);
|
|
|
|
// Initialize timeout options
|
|
jobCompletionTimeout = getOption(options, "jobCompletionTimeout"_sr, 1800.0);
|
|
jobSubmitTimeout = getOption(options, "jobSubmitTimeout"_sr, 60.0);
|
|
}
|
|
|
|
Future<Void> setup(Database const& cx) override { return _setup(this, cx); }
|
|
|
|
Future<bool> check(Database const& cx) override { return true; }
|
|
|
|
void getMetrics(std::vector<PerfMetric>& m) override {}
|
|
|
|
Standalone<StringRef> getRandomStringRef() const {
|
|
int stringLength = deterministicRandom()->randomInt(1, 10);
|
|
Standalone<StringRef> stringBuffer = makeString(stringLength);
|
|
deterministicRandom()->randomBytes(mutateString(stringBuffer), stringLength);
|
|
return stringBuffer;
|
|
}
|
|
|
|
KeyRange getRandomRange(BulkDumping* self, KeyRange maxRange) const {
|
|
constexpr int maxRandomRangeAttempts = 100;
|
|
for (int attempt = 0; attempt < maxRandomRangeAttempts; ++attempt) {
|
|
Standalone<StringRef> keyA = self->getRandomStringRef();
|
|
Standalone<StringRef> keyB = self->getRandomStringRef();
|
|
if (!maxRange.contains(keyA) || !maxRange.contains(keyB)) {
|
|
continue;
|
|
}
|
|
KeyRange range = keyA < keyB ? KeyRangeRef(keyA, keyB) : KeyRangeRef(keyB, keyA);
|
|
if (range.empty() || range.singleKeyRange()) {
|
|
continue;
|
|
}
|
|
return range;
|
|
}
|
|
|
|
// Sampling the whole keyspace can never finish for a sufficiently narrow range.
|
|
Key keyA = randomKeyBetween(maxRange);
|
|
Key keyB = randomKeyBetween(maxRange);
|
|
if (keyA == maxRange.end || keyB == maxRange.end || keyA == keyB) {
|
|
return maxRange;
|
|
}
|
|
|
|
KeyRange range = keyA < keyB ? KeyRangeRef(keyA, keyB) : KeyRangeRef(keyB, keyA);
|
|
return range.singleKeyRange() ? maxRange : range;
|
|
}
|
|
|
|
std::map<Key, Value> generateOrderedKVS(BulkDumping* self, KeyRange maxRange, size_t count) {
|
|
std::map<Key, Value> kvs; // ordered
|
|
while (kvs.size() < count) {
|
|
Key key = self->getRandomStringRef();
|
|
if (!maxRange.contains(key)) {
|
|
continue;
|
|
}
|
|
Value val = self->getRandomStringRef();
|
|
auto res = kvs.insert({ key, val });
|
|
if (!res.second) {
|
|
continue;
|
|
}
|
|
}
|
|
return kvs; // ordered
|
|
}
|
|
|
|
Future<Void> setKeys(Database cx, std::map<Key, Value> kvs) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
for (const auto& [key, value] : kvs) {
|
|
tr.set(key, value);
|
|
}
|
|
co_await tr.commit();
|
|
TraceEvent("BulkDumpingWorkLoadSetKey")
|
|
.detail("KeyCount", kvs.size())
|
|
.detail("Version", tr.getCommittedVersion());
|
|
co_return;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Void> waitUntilDumpJobComplete(BulkDumping* self, Database cx) {
|
|
double startTime = now();
|
|
while (true) {
|
|
// Check for timeout to prevent infinite waiting
|
|
if (now() - startTime > self->jobCompletionTimeout) {
|
|
TraceEvent(SevWarnAlways, "BulkDumpingWorkLoadDumpJobTimeout").detail("WaitTime", now() - startTime);
|
|
// Timeout: assume job has completed and proceed
|
|
break;
|
|
}
|
|
// Create a fresh transaction each iteration to avoid stale reads
|
|
Transaction tr(cx);
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
Optional<BulkDumpState> aliveJob = co_await getSubmittedBulkDumpJob(&tr);
|
|
if (!aliveJob.present()) {
|
|
break;
|
|
}
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
if (err.isValid() && err.code() == error_code_actor_cancelled) {
|
|
throw err;
|
|
}
|
|
if (err.isValid()) {
|
|
co_await tr.onError(err);
|
|
}
|
|
co_await delay(30.0);
|
|
}
|
|
}
|
|
|
|
Future<Void> clearDatabase(Database cx) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
tr.clear(normalKeys);
|
|
tr.clear(bulkDumpKeys);
|
|
tr.clear(bulkLoadJobKeys);
|
|
tr.clear(bulkLoadTaskKeys);
|
|
tr.clear(bulkLoadJobHistoryKeys);
|
|
co_await tr.commit();
|
|
break;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Void> clearRangeData(Database cx, KeyRange range) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
tr.clear(range);
|
|
co_await tr.commit();
|
|
break;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
// Return error tasks
|
|
// If allowIntermediateStates is true, tasks in Running/Complete phases are allowed (for timeout scenarios)
|
|
Future<std::vector<BulkLoadTaskState>> validateAllBulkLoadTaskAcknowledgedOrError(
|
|
Database cx,
|
|
UID jobId,
|
|
KeyRange jobRange,
|
|
bool allowIntermediateStates = false) {
|
|
RangeResult rangeResult;
|
|
Key beginKey = jobRange.begin;
|
|
Key endKey = jobRange.end;
|
|
Transaction tr(cx);
|
|
std::vector<BulkLoadTaskState> errorTasks;
|
|
TraceEvent("BulkDumpingWorkLoad")
|
|
.detail("Phase", "ValidateAllBulkLoadTaskAcknowledgedOrError")
|
|
.detail("Job", jobId.toString())
|
|
.detail("Range", jobRange)
|
|
.detail("AllowIntermediateStates", allowIntermediateStates);
|
|
while (beginKey < endKey) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
rangeResult.clear();
|
|
rangeResult = co_await krmGetRanges(&tr, bulkLoadTaskPrefix, KeyRangeRef(beginKey, endKey));
|
|
if (rangeResult.empty()) {
|
|
break;
|
|
}
|
|
for (int i = 0; i < static_cast<int>(rangeResult.size()) - 1; ++i) {
|
|
if (rangeResult[i].value.empty()) {
|
|
continue;
|
|
}
|
|
BulkLoadTaskState bulkLoadTaskState = decodeBulkLoadTaskState(rangeResult[i].value);
|
|
if (!bulkLoadTaskState.isValid()) {
|
|
continue; // Has been cleared by engine
|
|
}
|
|
if (bulkLoadTaskState.getJobId() != jobId) {
|
|
throw bulkload_task_outdated();
|
|
}
|
|
if (bulkLoadTaskState.phase == BulkLoadPhase::Error) {
|
|
TraceEvent(SevWarnAlways, "BulkDumpingWorkLoadBulkLoadTaskHasError")
|
|
.setMaxEventLength(-1)
|
|
.setMaxFieldLength(-1)
|
|
.detail("Task", bulkLoadTaskState.toString());
|
|
errorTasks.push_back(bulkLoadTaskState);
|
|
}
|
|
// Only assert on wrong phases if we're in normal completion mode (not timeout)
|
|
if (!allowIntermediateStates && bulkLoadTaskState.phase != BulkLoadPhase::Acknowledged &&
|
|
bulkLoadTaskState.phase != BulkLoadPhase::Error) {
|
|
TraceEvent(SevError, "BulkDumpingWorkLoadBulkLoadTaskWrongPhase")
|
|
.setMaxEventLength(-1)
|
|
.setMaxFieldLength(-1)
|
|
.detail("Task", bulkLoadTaskState.toString());
|
|
ASSERT(false);
|
|
}
|
|
}
|
|
beginKey = rangeResult.back().key;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
if (err.isValid()) {
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
co_return errorTasks;
|
|
}
|
|
|
|
Future<std::vector<BulkLoadTaskState>> waitUntilLoadJobCompleteOrError(BulkDumping* self,
|
|
Database cx,
|
|
UID jobId,
|
|
KeyRange jobRange) {
|
|
double startTime = now();
|
|
while (true) {
|
|
// Check for timeout to prevent infinite waiting
|
|
if (now() - startTime > self->jobCompletionTimeout) {
|
|
TraceEvent(SevWarnAlways, "BulkDumpingWorkLoadJobTimeout")
|
|
.detail("Job", jobId.toString())
|
|
.detail("Range", jobRange)
|
|
.detail("WaitTime", now() - startTime);
|
|
// Timeout: validate what we have and return (allow intermediate states since job may still be running)
|
|
std::vector<BulkLoadTaskState> errorTasks =
|
|
co_await self->validateAllBulkLoadTaskAcknowledgedOrError(cx, jobId, jobRange, true);
|
|
co_return errorTasks;
|
|
}
|
|
|
|
Optional<BulkLoadJobState> runningJob = co_await getRunningBulkLoadJob(cx);
|
|
if (runningJob.present()) {
|
|
ASSERT(runningJob.get().getJobId() == jobId);
|
|
// During the wait for the job completion, we may inject the job cancellation.
|
|
// We varies the timing of the job cancellation, we trigger the job cancellation with 10% probability at
|
|
// each time. Throughout the entire test, we inject the job cancellation at most maxCancelTimes times to
|
|
// ensure the job can complete fast.
|
|
if (SERVER_KNOBS->BULKLOAD_SIM_FAILURE_INJECTION && self->cancelTimes < self->maxCancelTimes &&
|
|
deterministicRandom()->random01() < 0.1) {
|
|
co_await cancelBulkLoadJob(cx, jobId);
|
|
self->cancelTimes++; // Inject cancellation. Then the bulkload job should run again.
|
|
TraceEvent("BulkDumpingWorkLoad").detail("Phase", "Job Cancelled").detail("Job", jobId.toString());
|
|
co_await self->clearRangeData(cx, jobRange);
|
|
TraceEvent("BulkDumpingWorkLoad")
|
|
.detail("Phase", "Data Cleared")
|
|
.detail("Job", jobId.toString())
|
|
.detail("JobRange", jobRange);
|
|
co_return std::vector<BulkLoadTaskState>();
|
|
}
|
|
co_await delay(10.0);
|
|
continue;
|
|
}
|
|
std::vector<BulkLoadTaskState> errorTasks =
|
|
co_await self->validateAllBulkLoadTaskAcknowledgedOrError(cx, jobId, jobRange);
|
|
co_return errorTasks;
|
|
}
|
|
}
|
|
|
|
Future<Void> validateBulkLoadJobHistory(Database cx,
|
|
UID jobId,
|
|
bool hasError,
|
|
bool bulkDumpRangeContainBulkLoadRange) {
|
|
std::vector<BulkLoadJobState> jobHistory = co_await getBulkLoadJobFromHistory(cx);
|
|
Optional<BulkLoadJobState> jobInHistory;
|
|
for (const auto& job : jobHistory) {
|
|
ASSERT(job.isValid());
|
|
if (job.getJobId() == jobId) {
|
|
ASSERT(!jobInHistory.present());
|
|
jobInHistory = job;
|
|
}
|
|
}
|
|
ASSERT(jobInHistory.present());
|
|
if (hasError || !bulkDumpRangeContainBulkLoadRange) {
|
|
ASSERT(jobInHistory.get().getPhase() == BulkLoadJobPhase::Error);
|
|
ASSERT(jobInHistory.get().getErrorMessage().present());
|
|
if (jobInHistory.get().getErrorMessage().get().find(
|
|
std::to_string(bulkload_dataset_not_cover_required_range().code())) != std::string::npos) {
|
|
ASSERT(!bulkDumpRangeContainBulkLoadRange);
|
|
}
|
|
} else {
|
|
ASSERT(jobInHistory.get().getPhase() == BulkLoadJobPhase::Complete);
|
|
}
|
|
}
|
|
|
|
Future<std::map<Key, Value>> getAllKVSFromDB(Database cx) {
|
|
Transaction tr(cx);
|
|
std::map<Key, Value> kvs;
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
RangeResult kvsRes = co_await tr.getRange(normalKeys, CLIENT_KNOBS->TOO_MANY);
|
|
ASSERT(!kvsRes.more);
|
|
kvs.clear();
|
|
for (auto& kv : kvsRes) {
|
|
auto res = kvs.insert({ kv.key, kv.value });
|
|
ASSERT(res.second);
|
|
}
|
|
break;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
co_return kvs;
|
|
}
|
|
|
|
bool keyContainedInRanges(const Key& key, const std::vector<KeyRange>& ranges) {
|
|
for (const auto& range : ranges) {
|
|
if (range.contains(key)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// kvs is the key value pairs generated initially in the database.
|
|
// newKvs is the key value pairs loaded by the bulk loading job.
|
|
// The workload first does bulkdump which only dumps the data within the bulkDumpJobRange.
|
|
// The workload then does bulkload which loads the data within the bulkLoadJobRange.
|
|
// The bulkLoadJob may be failed with unretryable error. So, some ranges (i.e. ignoreRanges) is not loaded.
|
|
// This function compares the consistency between kvs and newKvs within bulkLoadJobRange and bulkDumpJobRange and
|
|
// outside ignoreRanges. If a key in kvs is outside the bulkDumpJobRange, the newKvs should not contain the key.
|
|
void processCheck(BulkDumping* self,
|
|
std::map<Key, Value> kvs,
|
|
std::map<Key, Value> newKvs,
|
|
KeyRange bulkLoadJobRange,
|
|
KeyRange bulkDumpJobRange,
|
|
std::vector<KeyRange> ignoreRanges) {
|
|
std::vector<KeyValue> kvsToCheck;
|
|
std::vector<KeyValue> newKvsToCheck;
|
|
std::unordered_set<Key> keyOutsideDumpData;
|
|
for (const auto& [key, value] : kvs) {
|
|
if (!bulkDumpJobRange.contains(key)) {
|
|
keyOutsideDumpData.insert(key);
|
|
continue; // kvs may contain keys outside the bulkDumpJobRange
|
|
}
|
|
if (self->keyContainedInRanges(key, ignoreRanges)) {
|
|
continue;
|
|
}
|
|
if (!bulkLoadJobRange.contains(key)) {
|
|
continue; // kvs may contain keys outside the bulkLoadJobRange
|
|
}
|
|
kvsToCheck.push_back(KeyValueRef(key, value));
|
|
}
|
|
for (const auto& [key, value] : newKvs) {
|
|
// newKvs should not contain keys outside the bulkDumpJobRange
|
|
ASSERT(keyOutsideDumpData.find(key) == keyOutsideDumpData.end() && bulkDumpJobRange.contains(key));
|
|
if (self->keyContainedInRanges(key, ignoreRanges)) {
|
|
continue;
|
|
}
|
|
// newKvs should not contain keys outside the bulkLoadJobRange nor bulkDumpJobRange
|
|
ASSERT(bulkLoadJobRange.contains(key));
|
|
ASSERT(bulkDumpJobRange.contains(key));
|
|
newKvsToCheck.push_back(KeyValueRef(key, value));
|
|
}
|
|
if (kvsToCheck != newKvsToCheck) {
|
|
TraceEvent(SevError, "BulkDumpingWorkLoadError")
|
|
.detail("KVS", kvsToCheck.size())
|
|
.detail("NewKVS", newKvsToCheck.size());
|
|
ASSERT(false);
|
|
}
|
|
}
|
|
|
|
// This workload does following:
|
|
// (1) Generate 1000 key value pairs in normalKey space;
|
|
// (2) Randomly select a key range from normalKey space;
|
|
// (3) Submit a bulk dump job with the selected key range;
|
|
// (4) Wait until the bulk dump job completes;
|
|
// (5) Clear the database;
|
|
// (6) Randomly select a key range from normalKey space;
|
|
// (7) Submit a bulk load job with the selected key range;
|
|
// (8) Wait until the bulk load job completes;
|
|
// (9) Validate the loaded data in DB is same as the data in DB before dumping within the bulkdump job range and
|
|
// bulkload job range. Note that the bulkload job can be unretriable error. In this case, we ignore the error range;
|
|
// (10) Validate the bulk load job history.
|
|
Future<Void> start(Database const& cx) override {
|
|
if (clientId != 0) {
|
|
co_return;
|
|
}
|
|
|
|
// Cleanup any leftover state from previous test iterations BEFORE starting work
|
|
// This ensures we start clean even if a previous iteration timed out or crashed
|
|
if (bulkLoadTransportMethod == BulkLoadTransportMethod::BLOBSTORE && g_network->isSimulated()) {
|
|
// Clear MockS3Server storage to prevent memory accumulation over test iterations
|
|
clearMockS3Storage();
|
|
}
|
|
|
|
if (g_network->isSimulated()) {
|
|
// Network partition between CC and DD can cause DD no longer existing,
|
|
// which results in the bulk loading task cannot complete
|
|
// So, this workload disable the network partition
|
|
disableConnectionFailures("BulkDumping");
|
|
}
|
|
|
|
KeyRange bulkDumpJobRange = deterministicRandom()->coinflip() ? normalKeys : getRandomRange(this, normalKeys);
|
|
|
|
bool bulkDumpRangeContainBulkLoadRange = true; // Will set to false if the bulk load job range is not
|
|
// contained in the bulk dump job range. In this case, the bulk load job will be failed fast with error.
|
|
// So, when set to false, skip the processCheck() in the end of the workload.
|
|
// Also, check bulkload job history to ensure the job is failed with the expected error.
|
|
|
|
std::map<Key, Value> kvs = generateOrderedKVS(this, normalKeys, 1000);
|
|
co_await setKeys(cx, kvs);
|
|
|
|
// BulkLoad uses range lock
|
|
co_await registerRangeLockOwner(cx, rangeLockNameForBulkLoad, rangeLockNameForBulkLoad);
|
|
|
|
std::vector<RangeLockOwner> lockOwners = co_await getAllRangeLockOwners(cx);
|
|
ASSERT(lockOwners.size() == 1 && lockOwners[0].getOwnerUniqueId() == rangeLockNameForBulkLoad);
|
|
|
|
// Submit a bulk dump job
|
|
int oldBulkDumpMode = 0;
|
|
TraceEvent("BulkDumpingWorkLoad").detail("Phase", "Setting BulkDump Mode");
|
|
try {
|
|
oldBulkDumpMode = co_await setBulkDumpMode(cx, 1); // Enable bulkDump
|
|
} catch (Error& e) {
|
|
TraceEvent(SevWarnAlways, "BulkDumpingWorkLoadSetDumpModeFailed").error(e);
|
|
throw;
|
|
}
|
|
TraceEvent("BulkDumpingWorkLoad").detail("Phase", "BulkDump Mode Set").detail("OldMode", oldBulkDumpMode);
|
|
std::string dumpFolder = jobRoot.empty() ? simulationBulkDumpFolder : jobRoot;
|
|
BulkDumpState bulkDumpJob =
|
|
createBulkDumpJob(bulkDumpJobRange, dumpFolder, BulkLoadType::SST, bulkLoadTransportMethod);
|
|
TraceEvent("BulkDumpingWorkLoad").detail("Phase", "Submitting Dump Job").detail("Job", bulkDumpJob.getJobId());
|
|
co_await timeoutError(submitBulkDumpJob(cx, bulkDumpJob), jobSubmitTimeout);
|
|
TraceEvent("BulkDumpingWorkLoad")
|
|
.detail("Phase", "Dump Job Submitted")
|
|
.detail("TransportMethod", convertBulkLoadTransportMethodToString(bulkLoadTransportMethod))
|
|
.detail("JobRoot", dumpFolder)
|
|
.detail("Job", bulkDumpJob.toString());
|
|
|
|
// Wait until the dump job completes
|
|
co_await waitUntilDumpJobComplete(this, cx);
|
|
TraceEvent("BulkDumpingWorkLoad").detail("Phase", "Dump Job Complete").detail("Job", bulkDumpJob.toString());
|
|
|
|
// Clear database
|
|
co_await clearDatabase(cx);
|
|
TraceEvent("BulkDumpingWorkLoad").detail("Phase", "Clear DB").detail("Job", bulkDumpJob.toString());
|
|
|
|
// Submit a bulk load job
|
|
int oldBulkLoadMode = 0;
|
|
TraceEvent("BulkDumpingWorkLoad")
|
|
.detail("Phase", "Setting BulkLoad Mode")
|
|
.detail("Job", bulkDumpJob.toString());
|
|
try {
|
|
oldBulkLoadMode = co_await setBulkLoadMode(cx, 1); // Enable bulkLoad
|
|
} catch (Error& e) {
|
|
TraceEvent(SevWarnAlways, "BulkDumpingWorkLoadSetLoadModeFailed")
|
|
.error(e)
|
|
.detail("Job", bulkDumpJob.toString());
|
|
throw;
|
|
}
|
|
TraceEvent("BulkDumpingWorkLoad").detail("Phase", "BulkLoad Mode Set").detail("OldMode", oldBulkLoadMode);
|
|
while (true) {
|
|
// We randomly injects the job cancellation when waiting for the job completion to test the job
|
|
// cancellation. If the job is cancelled, we should re-submit the job.
|
|
bool hasError = false;
|
|
int oldCancelTimes = cancelTimes;
|
|
KeyRange bulkLoadJobRange =
|
|
deterministicRandom()->coinflip()
|
|
? bulkDumpJob.getJobRange()
|
|
: getRandomRange(this, deterministicRandom()->coinflip() ? normalKeys : bulkDumpJobRange);
|
|
UID dataSourceId = bulkDumpJob.getJobId();
|
|
std::string dataSourceRoot = bulkDumpJob.getJobRoot();
|
|
BulkLoadJobState bulkLoadJob =
|
|
createBulkLoadJob(dataSourceId, bulkLoadJobRange, dataSourceRoot, bulkLoadTransportMethod);
|
|
TraceEvent("BulkDumpingWorkLoad").detail("Phase", "Submitting Load Job").detail("JobId", dataSourceId);
|
|
co_await timeoutError(submitBulkLoadJob(cx, bulkLoadJob), jobSubmitTimeout);
|
|
TraceEvent("BulkDumpingWorkLoad")
|
|
.detail("Phase", "Load Job Submitted")
|
|
.detail("JobId", dataSourceId)
|
|
.detail("JobRange", bulkLoadJobRange)
|
|
.detail("JobRoot", dataSourceRoot)
|
|
.detail("TransportMethod", bulkLoadJob.getTransportMethod());
|
|
|
|
// Wait until the load job complete
|
|
std::vector<KeyRange> errorRanges;
|
|
std::vector<BulkLoadTaskState> errorTasks =
|
|
co_await waitUntilLoadJobCompleteOrError(this, cx, bulkLoadJob.getJobId(), bulkLoadJob.getJobRange());
|
|
// waitUntilLoadJobCompleteOrError can cancel the job and set cancelled to true.
|
|
// If this happens, the current job is intentionally cancelled and we should retry the job.
|
|
ASSERT(cancelTimes >= oldCancelTimes);
|
|
if (cancelTimes > oldCancelTimes) {
|
|
// cancelTimes increments when waitUntilLoadJobCompleteOrError injects job cancellation
|
|
co_await delay(deterministicRandom()->random01() * 10.0);
|
|
continue;
|
|
}
|
|
for (const auto& errorTask : errorTasks) {
|
|
errorRanges.push_back(errorTask.getRange());
|
|
hasError = true;
|
|
}
|
|
TraceEvent("BulkDumpingWorkLoad")
|
|
.detail("Phase", "Load Job Complete")
|
|
.detail("BulkLoadJobId", dataSourceId)
|
|
.detail("BulkLoadJobRange", bulkLoadJobRange)
|
|
.detail("BulkLoadJobRoot", dataSourceRoot)
|
|
.detail("BulkLoadTransportMethod", bulkLoadJob.getTransportMethod());
|
|
|
|
// Check the loaded data in DB is same as the data in DB before dumping
|
|
std::map<Key, Value> newKvs = co_await getAllKVSFromDB(cx);
|
|
if (bulkDumpJobRange.contains(bulkLoadJobRange)) {
|
|
processCheck(this, kvs, newKvs, bulkLoadJobRange, bulkDumpJobRange, errorRanges);
|
|
bulkDumpRangeContainBulkLoadRange = true;
|
|
} else {
|
|
TraceEvent(SevWarnAlways, "BulkDumpingWorkLoad")
|
|
.detail("Phase", "SkippedCheck")
|
|
.detail("BulkDumpJobRange", bulkDumpJobRange)
|
|
.detail("BulkLoadJobRange", bulkLoadJobRange);
|
|
bulkDumpRangeContainBulkLoadRange = false;
|
|
}
|
|
|
|
// Acknowledge any error task of the job
|
|
co_await acknowledgeAllErrorBulkLoadTasks(cx, bulkLoadJob.getJobId(), bulkLoadJob.getJobRange());
|
|
co_await validateBulkLoadJobHistory(
|
|
cx, bulkLoadJob.getJobId(), hasError, bulkDumpRangeContainBulkLoadRange);
|
|
break;
|
|
}
|
|
|
|
// Make sure all ranges locked by the workload are unlocked
|
|
std::vector<std::pair<KeyRange, RangeLockState>> res =
|
|
co_await findExclusiveReadLockOnRange(cx, normalKeys, rangeLockNameForBulkLoad);
|
|
ASSERT(res.empty());
|
|
|
|
co_await removeRangeLockOwner(cx, rangeLockNameForBulkLoad);
|
|
|
|
std::vector<RangeLockOwner> lockOwnersAfterRemove = co_await getAllRangeLockOwners(cx);
|
|
ASSERT(lockOwnersAfterRemove.empty());
|
|
}
|
|
|
|
Future<Void> _setup(BulkDumping* self, Database cx) {
|
|
// Only client 0 registers the MockS3Server to avoid duplicates
|
|
if (self->clientId == 0 && self->bulkLoadTransportMethod == BulkLoadTransportMethod::BLOBSTORE) {
|
|
// Check if we're using a local mock server URL pattern
|
|
bool useMockS3 = self->jobRoot.find("127.0.0.1") != std::string::npos ||
|
|
self->jobRoot.find("localhost") != std::string::npos ||
|
|
self->jobRoot.find("mock-s3-server") != std::string::npos;
|
|
|
|
if (useMockS3 && g_network->isSimulated()) {
|
|
// Check if 127.0.0.1:8080 is already registered in simulator's httpHandlers
|
|
std::string serverKey = "127.0.0.1:8080";
|
|
bool alreadyRegistered = g_simulator->httpHandlers.contains(serverKey);
|
|
|
|
if (alreadyRegistered) {
|
|
TraceEvent("BulkDumpingWorkload")
|
|
.detail("Phase", "MockS3Server Already Registered")
|
|
.detail("Address", serverKey)
|
|
.detail("ChaosRequested", self->enableChaos)
|
|
.detail("Reason", "Reusing existing HTTP handler from previous test");
|
|
} else if (self->enableChaos) {
|
|
TraceEvent("BulkDumpingWorkload")
|
|
.detail("Phase", "Starting MockS3ServerChaos")
|
|
.detail("JobRoot", self->jobRoot);
|
|
|
|
// Start MockS3ServerChaos - has internal duplicate detection
|
|
NetworkAddress listenAddress(IPAddress(0x7f000001), 8080);
|
|
co_await startMockS3ServerChaos(listenAddress);
|
|
|
|
TraceEvent("BulkDumpingWorkload")
|
|
.detail("Phase", "MockS3ServerChaos Started")
|
|
.detail("Address", "127.0.0.1:8080");
|
|
} else {
|
|
TraceEvent("BulkDumpingWorkload")
|
|
.detail("Phase", "Registering MockS3Server")
|
|
.detail("JobRoot", self->jobRoot);
|
|
|
|
// Register MockS3Server with persistence enabled
|
|
co_await registerMockS3Server("127.0.0.1", "8080");
|
|
|
|
TraceEvent("BulkDumpingWorkload")
|
|
.detail("Phase", "MockS3Server Registered")
|
|
.detail("Address", "127.0.0.1:8080");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Configure chaos rates for all clients if chaos is enabled
|
|
// This allows each test to have different chaos rates
|
|
if (self->enableChaos && g_network->isSimulated()) {
|
|
auto injector = S3FaultInjector::injector();
|
|
injector->setErrorRate(self->errorRate);
|
|
injector->setThrottleRate(self->throttleRate);
|
|
injector->setDelayRate(self->delayRate);
|
|
injector->setCorruptionRate(self->corruptionRate);
|
|
injector->setMaxDelay(self->maxDelay);
|
|
|
|
TraceEvent("BulkDumpingWorkload")
|
|
.detail("Phase", "Chaos Configured")
|
|
.detail("ClientID", self->clientId)
|
|
.detail("ErrorRate", self->errorRate)
|
|
.detail("ThrottleRate", self->throttleRate)
|
|
.detail("DelayRate", self->delayRate)
|
|
.detail("CorruptionRate", self->corruptionRate);
|
|
}
|
|
}
|
|
};
|
|
|
|
TEST_CASE("/fdbserver/workloads/BulkDumping/boundedNarrowRange") {
|
|
WorkloadContext context;
|
|
context.clientId = 0;
|
|
context.clientCount = 1;
|
|
context.sharedRandomNumber = 1;
|
|
TestWorkloadImpl<BulkDumping> workload(context);
|
|
|
|
KeyRange narrowRange = KeyRangeRef("aaaaaaaa\x01"_sr, "aaaaaaaa\x02"_sr);
|
|
KeyRange generatedRange = workload.getRandomRange(&workload, narrowRange);
|
|
ASSERT(narrowRange.contains(generatedRange));
|
|
ASSERT(!generatedRange.empty());
|
|
ASSERT(!generatedRange.singleKeyRange());
|
|
|
|
KeyRange singleSampleableKeyRange = KeyRangeRef("aaaaaaaaa"_sr, "aaaaaaaaa\x00\x00"_sr);
|
|
KeyRange fallbackRange = workload.getRandomRange(&workload, singleSampleableKeyRange);
|
|
ASSERT(fallbackRange == singleSampleableKeyRange);
|
|
ASSERT(!fallbackRange.empty());
|
|
ASSERT(!fallbackRange.singleKeyRange());
|
|
|
|
return Void();
|
|
}
|
|
|
|
WorkloadFactory<BulkDumping> BulkDumpingFactory;
|