foundationdb/fdbclient/FileBackupAgent.cpp

8425 lines
338 KiB
C++

/*
* FileBackupAgent.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/CommitProxyInterface.h"
#include "fdbclient/DatabaseConfiguration.h"
#include "fdbrpc/simulator.h"
#include "flow/EncryptUtils.h"
#include "flow/FastRef.h"
#include "flow/flow.h"
#include "fmt/format.h"
#include "fdbclient/BackupAgent.h"
#include "fdbclient/BackupContainer.h"
#include "fdbclient/BackupContainerFileSystem.h"
#include "fdbclient/BulkDumping.h"
#include "fdbclient/BulkLoading.h"
#include "fdbclient/ClientBooleanParams.h"
#include "fdbclient/DatabaseContext.h"
#include "fdbclient/FDBTypes.h"
#include "fdbclient/JsonBuilder.h"
#include "fdbclient/JSONDoc.h"
#include "fdbclient/KeyBackedTypes.h"
#include "fdbclient/KeyRangeMap.h"
#include "fdbclient/Knobs.h"
#include "fdbclient/ManagementAPI.h"
#include "fdbclient/RangeLock.h"
#include "PartitionedLogIterator.h"
#include "fdbclient/Status.h"
#include "fdbclient/SystemData.h"
#include "fdbclient/TaskBucket.h"
#include "FileBackupAgentFileFormat.h"
#include "flow/network.h"
#include "flow/Trace.h"
#include "flow/Util.h"
#include <cinttypes>
#include <cstdint>
#include <ctime>
#include <climits>
#include "flow/IAsyncFile.h"
#include "flow/genericactors.actor.h"
#include "flow/Hash3.h"
#include "flow/xxhash.h"
#include <memory>
#include <numeric>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <algorithm>
#include <unordered_map>
#include <utility>
// Counters to verify BulkDump/BulkLoad were actually used (for test assertions)
std::atomic<int> g_bulkDumpTaskCompleteCount(0);
std::atomic<int> g_bulkLoadRestoreTaskCompleteCount(0);
// Helper function to monitor BulkDump job completion
// Returns true if job completed successfully, false if timed out
Future<bool> monitorBulkDumpJobCompletion(Database cx, UID jobId, double timeoutDuration, double pollInterval) {
double timeoutStart = now();
Transaction tr(cx);
while (true) {
Error err;
try {
Optional<BulkDumpState> currentJob = co_await getSubmittedBulkDumpJob(&tr);
bool stillRunning = currentJob.present() && currentJob.get().getJobId() == jobId;
if (!stillRunning) {
co_return true; // Job completed successfully
}
if (now() - timeoutStart > timeoutDuration) {
co_return false; // Timed out
}
co_await delay(pollInterval);
tr.reset();
continue;
} catch (Error& e) {
err = e;
}
co_await tr.onError(err);
}
}
// Verify that a complete BulkDump dataset exists for BulkLoad restore
// Returns true if dataset is complete, false if incomplete
// JobId corresponds to a single BulkDump job which represents one snapshot at a specific version
Future<bool> verifyBulkDumpDatasetCompleteness(Reference<IBackupContainer> bc, std::string bulkDumpJobId) {
try {
if (bulkDumpJobId.empty()) {
TraceEvent(SevWarn, "BulkLoadVerifyDatasetEmptyJobId");
co_return false;
}
// Check if job-specific directory exists: bulkdump_data/<job-uuid>/
// BulkDump stores data under data/<container>/bulkdump_data/ via getBackupDataPath(),
// which is consistent with where BackupContainer stores other files (logs, ranges, etc.)
std::string jobDirectoryPath = "bulkdump_data/" + bulkDumpJobId + "/";
Error err;
try {
// Try to list files in the job directory to verify it exists and has content
Reference<BackupContainerFileSystem> bcfs = bc.castTo<BackupContainerFileSystem>();
if (bcfs) {
// Standard listFiles works because BulkDump now writes under data/<container>/
BackupContainerFileSystem::FilesAndSizesT files = co_await bcfs->listFiles(jobDirectoryPath);
if (files.empty()) {
TraceEvent(SevWarn, "BulkLoadVerifyDatasetJobDirectoryEmpty")
.detail("JobDirectoryPath", jobDirectoryPath)
.detail("BulkDumpJobId", bulkDumpJobId);
co_return false;
}
TraceEvent("BulkLoadVerifyDatasetJobDirectoryFound")
.detail("JobDirectoryPath", jobDirectoryPath)
.detail("BulkDumpJobId", bulkDumpJobId)
.detail("FileCount", files.size());
// Basic verification: job directory exists and contains files
// More detailed verification (parsing shard manifests) is delegated to BulkLoad system
co_return true;
} else {
// Cannot verify - backup container doesn't support file listing
// Use SevWarn (not SevError) to avoid abort in simulation
TraceEvent(SevWarn, "BulkLoadVerifyDatasetCannotCast")
.detail("BulkDumpJobId", bulkDumpJobId)
.detail("Note", "BackupContainer does not support file listing required for verification");
co_return false;
}
} catch (Error& e) {
if (e.code() == error_code_file_not_found) {
TraceEvent(SevWarn, "BulkLoadVerifyDatasetJobDirectoryNotFound")
.detail("JobDirectoryPath", jobDirectoryPath)
.detail("BulkDumpJobId", bulkDumpJobId);
co_return false;
}
throw;
}
} catch (Error& e) {
if (e.code() == error_code_actor_cancelled) {
throw;
}
TraceEvent(SevWarn, "BulkLoadVerifyDatasetError").error(e).detail("BulkDumpJobId", bulkDumpJobId);
co_return false;
}
}
// Note: BulkLoad configuration validation (shard_encode_location_metadata, enable_read_lock_on_range)
// is performed by the BulkLoad system on the server side. Client-side validation is not possible
// because SERVER_KNOBS are not accessible from fdbclient.
Optional<std::string> fileBackupAgentProxy = Optional<std::string>();
static std::string versionToString(Optional<Version> version) {
if (version.present())
return std::to_string(version.get());
else
return "N/A";
}
static std::string timeStampToString(Optional<int64_t> epochs) {
if (!epochs.present())
return "N/A";
return BackupAgentBase::formatTime(epochs.get());
}
static Future<Optional<int64_t>> getTimestampFromVersion(Optional<Version> ver,
Reference<ReadYourWritesTransaction> tr) {
if (!ver.present())
return Optional<int64_t>();
return timeKeeperEpochsFromVersion(ver.get(), tr);
}
// Time format :
// <= 59 seconds
// <= 59.99 minutes
// <= 23.99 hours
// N.NN days
std::string secondsToTimeFormat(int64_t seconds) {
if (seconds >= 86400)
return format("%.2f day(s)", seconds / 86400.0);
else if (seconds >= 3600)
return format("%.2f hour(s)", seconds / 3600.0);
else if (seconds >= 60)
return format("%.2f minute(s)", seconds / 60.0);
else
return format("%lld second(s)", seconds);
}
const Key FileBackupAgent::keyLastRestorable = "last_restorable"_sr;
// For convenience
using ERestoreState = FileBackupAgent::ERestoreState;
StringRef FileBackupAgent::restoreStateText(ERestoreState id) {
switch (id) {
case ERestoreState::UNINITIALIZED:
return "uninitialized"_sr;
case ERestoreState::QUEUED:
return "queued"_sr;
case ERestoreState::STARTING:
return "starting"_sr;
case ERestoreState::RUNNING:
return "running"_sr;
case ERestoreState::COMPLETED:
return "completed"_sr;
case ERestoreState::ABORTED:
return "aborted"_sr;
default:
return "Unknown"_sr;
}
}
Key FileBackupAgent::getPauseKey() {
FileBackupAgent backupAgent;
return backupAgent.taskBucket->getPauseKey();
}
Future<std::vector<KeyBackedTag>> TagUidMap::getAll_impl(TagUidMap* tagsMap,
Reference<ReadYourWritesTransaction> tr,
Snapshot snapshot) {
Key prefix = tagsMap->prefix; // Copying it here as tagsMap lifetime is not tied to this actor
TagMap::RangeResultType tagPairs = co_await tagsMap->getRange(tr, std::string(), {}, 1e6, snapshot);
std::vector<KeyBackedTag> results;
for (auto& p : tagPairs.results)
results.push_back(KeyBackedTag(p.first, prefix));
co_return results;
}
KeyBackedTag::KeyBackedTag(std::string tagName, StringRef tagMapPrefix)
: KeyBackedProperty<UidAndAbortedFlagT>(TagUidMap(tagMapPrefix).getProperty(tagName)), tagName(tagName),
tagMapPrefix(tagMapPrefix) {}
// Lists all backups and find if any partitioned backup is running.
Future<bool> anyPartitionedBackupRunning(Reference<ReadYourWritesTransaction> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
std::vector<KeyBackedTag> tags = co_await getAllBackupTags(tr);
std::vector<Future<Optional<UidAndAbortedFlagT>>> futures;
for (const auto& tag : tags) {
futures.push_back(tag.get(tr));
}
co_await waitForAll(futures);
int i = 0;
for (i = 0; i < futures.size(); i++) {
if (futures[i].get().present()) {
Optional<MutationLogType> mutationLogType;
EBackupState eState;
BackupConfig config(futures[i].get().get().first);
co_await (store(eState, config.stateEnum().getD(tr, Snapshot::False, EBackupState::STATE_NEVERRAN)) &&
store(mutationLogType, config.mutationLogType().get(tr)));
if (FileBackupAgent::isRunnable(eState) &&
mutationLogType.orDefault(MutationLogType::DEFAULT) == MutationLogType::PARTITIONED_LOG) {
co_return true;
}
}
}
co_return false;
}
// Lists all backups and find if any range-partitioned backup is running.
Future<bool> anyRangePartitionedBackupRunning(Reference<ReadYourWritesTransaction> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
std::vector<KeyBackedTag> tags = co_await getAllBackupTags(tr);
std::vector<Future<Optional<UidAndAbortedFlagT>>> futures;
for (const auto& tag : tags) {
futures.push_back(tag.get(tr));
}
co_await waitForAll(futures);
int i = 0;
for (i = 0; i < futures.size(); i++) {
if (futures[i].get().present()) {
Optional<MutationLogType> mutationLogType;
EBackupState eState;
BackupConfig config(futures[i].get().get().first);
co_await (store(eState, config.stateEnum().getD(tr, Snapshot::False, EBackupState::STATE_NEVERRAN)) &&
store(mutationLogType, config.mutationLogType().get(tr)));
if (FileBackupAgent::isRunnable(eState) &&
mutationLogType.orDefault(MutationLogType::DEFAULT) == MutationLogType::RANGE_PARTITIONED_LOG) {
co_return true;
}
}
}
co_return false;
}
class RestoreConfig : public KeyBackedTaskConfig {
public:
explicit RestoreConfig(UID uid = UID()) : KeyBackedTaskConfig(fileRestorePrefixRange.begin, uid) {}
explicit RestoreConfig(Reference<Task> task) : KeyBackedTaskConfig(fileRestorePrefixRange.begin, task) {}
KeyBackedProperty<ERestoreState> stateEnum() { return configSpace.pack(__FUNCTION__sr); }
Future<StringRef> stateText(Reference<ReadYourWritesTransaction> tr) {
return map(stateEnum().getD(tr),
[](ERestoreState s) -> StringRef { return FileBackupAgent::restoreStateText(s); });
}
KeyBackedProperty<Key> addPrefix() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<Key> removePrefix() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<bool> onlyApplyMutationLogs() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<bool> inconsistentSnapshotOnly() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<bool> unlockDBAfterRestore() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<MutationLogType> mutationLogType() { return configSpace.pack(__FUNCTION__sr); }
// BulkLoad integration properties
KeyBackedProperty<bool> useRangeFileRestore() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<std::string> bulkDumpJobId() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<Key> bulkLoadCompleteFuture() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<bool> bulkLoadComplete() { return configSpace.pack(__FUNCTION__sr); }
// Original BulkLoad mode before restore enabled it - used to restore state after completion/crash
KeyBackedProperty<int> originalBulkLoadMode() { return configSpace.pack(__FUNCTION__sr); }
// XXX: Remove restoreRange() once it is safe to remove. It has been changed to restoreRanges
KeyBackedProperty<KeyRange> restoreRange() { return configSpace.pack(__FUNCTION__sr); }
// XXX: Changed to restoreRangeSet. It can be removed.
KeyBackedProperty<std::vector<KeyRange>> restoreRanges() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedSet<KeyRange> restoreRangeSet() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<Key> batchFuture() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<Version> beginVersion() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<Version> restoreVersion() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<Version> firstConsistentVersion() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedProperty<Reference<IBackupContainer>> sourceContainer() { return configSpace.pack(__FUNCTION__sr); }
// Get the source container as a bare URL, without creating a container instance
KeyBackedProperty<Value> sourceContainerURL() { return configSpace.pack("sourceContainer"_sr); }
// Total bytes written by all log and range restore tasks.
KeyBackedBinaryValue<int64_t> bytesWritten() { return configSpace.pack(__FUNCTION__sr); }
// File blocks that have had tasks created for them by the Dispatch task
KeyBackedBinaryValue<int64_t> filesBlocksDispatched() { return configSpace.pack(__FUNCTION__sr); }
// File blocks whose tasks have finished
KeyBackedBinaryValue<int64_t> fileBlocksFinished() { return configSpace.pack(__FUNCTION__sr); }
// Total number of files in the fileMap
KeyBackedBinaryValue<int64_t> fileCount() { return configSpace.pack(__FUNCTION__sr); }
// Total number of file blocks in the fileMap
KeyBackedBinaryValue<int64_t> fileBlockCount() { return configSpace.pack(__FUNCTION__sr); }
// BulkLoad sub-phase task counts for detailed progress tracking
KeyBackedBinaryValue<int64_t> bulkLoadSubmittedTasks() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedBinaryValue<int64_t> bulkLoadTriggeredTasks() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedBinaryValue<int64_t> bulkLoadRunningTasks() { return configSpace.pack(__FUNCTION__sr); }
KeyBackedBinaryValue<int64_t> bulkLoadTotalTasks() { return configSpace.pack(__FUNCTION__sr); }
Future<std::vector<KeyRange>> getRestoreRangesOrDefault(Reference<ReadYourWritesTransaction> tr) {
return getRestoreRangesOrDefault_impl(this, tr);
}
static Future<std::vector<KeyRange>> getRestoreRangesOrDefault_impl(RestoreConfig* self,
Reference<ReadYourWritesTransaction> tr) {
std::vector<KeyRange> ranges;
int batchSize = buggify() ? 1 : CLIENT_KNOBS->RESTORE_RANGES_READ_BATCH;
Optional<KeyRange> begin;
Arena arena;
while (true) {
KeyBackedSet<KeyRange>::RangeResultType rangeResult =
co_await self->restoreRangeSet().getRange(tr, begin, {}, batchSize);
ranges.insert(ranges.end(), rangeResult.results.begin(), rangeResult.results.end());
if (!rangeResult.more) {
break;
}
ASSERT(!rangeResult.results.empty());
begin = KeyRangeRef(KeyRef(arena, ranges.back().begin), keyAfter(ranges.back().end, arena));
}
// fall back to original fields if the new field is empty
if (ranges.empty()) {
std::vector<KeyRange> _ranges = co_await self->restoreRanges().getD(tr);
ranges = _ranges;
if (ranges.empty()) {
KeyRange range = co_await self->restoreRange().getD(tr);
ranges.push_back(range);
}
}
co_return ranges;
}
// Describes a file to load blocks from during restore. Ordered by version and then fileName to enable
// incrementally advancing through the map, saving the version and path of the next starting point.
struct RestoreFile {
Version version; // this is beginVersion, not endVersion
std::string fileName;
bool isRange{ false }; // false for log file
int64_t blockSize{ 0 };
int64_t fileSize{ 0 };
Version endVersion{ ::invalidVersion }; // not meaningful for range files
int64_t tagId = -1; // only meaningful to log files, Log router tag. Non-negative for new backup format.
int64_t totalTags = -1; // only meaningful to log files, Total number of log router tags.
Tuple pack() const {
return Tuple::makeTuple(
version, fileName, (int64_t)isRange, fileSize, blockSize, endVersion, tagId, totalTags);
}
static RestoreFile unpack(Tuple const& t) {
RestoreFile r;
int i = 0;
r.version = t.getInt(i++);
r.fileName = t.getString(i++).toString();
r.isRange = t.getInt(i++) != 0;
r.fileSize = t.getInt(i++);
r.blockSize = t.getInt(i++);
r.endVersion = t.getInt(i++);
r.tagId = t.getInt(i++);
r.totalTags = t.getInt(i++);
return r;
}
};
using FileSetT = KeyBackedSet<RestoreFile>;
FileSetT fileSet() { return configSpace.pack(__FUNCTION__sr); }
FileSetT logFileSet() { return configSpace.pack(__FUNCTION__sr); }
FileSetT rangeFileSet() { return configSpace.pack(__FUNCTION__sr); }
Future<bool> isRunnable(Reference<ReadYourWritesTransaction> tr) {
return map(stateEnum().getD(tr), [](ERestoreState s) -> bool {
return s != ERestoreState::ABORTED && s != ERestoreState::COMPLETED && s != ERestoreState::UNINITIALIZED;
});
}
Future<Void> logError(Database cx, Error e, std::string const& details, void* taskInstance = nullptr) {
if (!uid.isValid()) {
TraceEvent(SevError, "FileRestoreErrorNoUID").error(e).detail("Description", details);
return Void();
}
TraceEvent t(SevWarn, "FileRestoreError");
t.error(e)
.detail("RestoreUID", uid)
.detail("Description", details)
.detail("TaskInstance", (uint64_t)taskInstance);
// key_not_found could happen
if (e.code() == error_code_key_not_found)
t.backtrace();
return updateErrorInfo(cx, e, details);
}
Key mutationLogPrefix() { return uidPrefixKey(applyLogKeys.begin, uid); }
Key applyMutationsMapPrefix() { return uidPrefixKey(applyMutationsKeyVersionMapRange.begin, uid); }
static Future<int64_t> getApplyVersionLag_impl(Reference<ReadYourWritesTransaction> tr, UID uid) {
Future<Optional<Value>> beginVal = tr->get(uidPrefixKey(applyMutationsBeginRange.begin, uid), Snapshot::True);
Future<Optional<Value>> endVal = tr->get(uidPrefixKey(applyMutationsEndRange.begin, uid), Snapshot::True);
co_await (success(beginVal) && success(endVal));
if (!beginVal.get().present() || !endVal.get().present())
co_return 0;
Version beginVersion = BinaryReader::fromStringRef<Version>(beginVal.get().get(), Unversioned());
Version endVersion = BinaryReader::fromStringRef<Version>(endVal.get().get(), Unversioned());
co_return endVersion - beginVersion;
}
Future<int64_t> getApplyVersionLag(Reference<ReadYourWritesTransaction> tr) {
return getApplyVersionLag_impl(tr, uid);
}
void initApplyMutations(Reference<ReadYourWritesTransaction> tr,
Key addPrefix,
Key removePrefix,
OnlyApplyMutationLogs onlyApplyMutationLogs) {
// Set these because they have to match the applyMutations values.
this->addPrefix().set(tr, addPrefix);
this->removePrefix().set(tr, removePrefix);
clearApplyMutationsKeys(tr);
// Initialize add/remove prefix, range version map count and set the map's start key to InvalidVersion
tr->set(uidPrefixKey(applyMutationsAddPrefixRange.begin, uid), addPrefix);
tr->set(uidPrefixKey(applyMutationsRemovePrefixRange.begin, uid), removePrefix);
int64_t startCount = 0;
tr->set(uidPrefixKey(applyMutationsKeyVersionCountRange.begin, uid), StringRef((uint8_t*)&startCount, 8));
Key mapStart = uidPrefixKey(applyMutationsKeyVersionMapRange.begin, uid);
tr->set(mapStart, BinaryWriter::toValue<Version>(invalidVersion, Unversioned()));
}
void clearApplyMutationsKeys(Reference<ReadYourWritesTransaction> tr) {
tr->setOption(FDBTransactionOptions::COMMIT_ON_FIRST_PROXY);
// Clear add/remove prefix keys
tr->clear(uidPrefixKey(applyMutationsAddPrefixRange.begin, uid));
tr->clear(uidPrefixKey(applyMutationsRemovePrefixRange.begin, uid));
// Clear range version map and count key
tr->clear(uidPrefixKey(applyMutationsKeyVersionCountRange.begin, uid));
Key mapStart = uidPrefixKey(applyMutationsKeyVersionMapRange.begin, uid);
tr->clear(KeyRangeRef(mapStart, strinc(mapStart)));
// Clear any loaded mutations that have not yet been applied
Key mutationPrefix = mutationLogPrefix();
tr->clear(KeyRangeRef(mutationPrefix, strinc(mutationPrefix)));
// Clear end and begin versions (intentionally in this order)
tr->clear(uidPrefixKey(applyMutationsEndRange.begin, uid));
tr->clear(uidPrefixKey(applyMutationsBeginRange.begin, uid));
}
void setApplyBeginVersion(Reference<ReadYourWritesTransaction> tr, Version ver) {
tr->set(uidPrefixKey(applyMutationsBeginRange.begin, uid), BinaryWriter::toValue(ver, Unversioned()));
}
Future<Version> getApplyBeginVersion(Reference<ReadYourWritesTransaction> tr) {
return map(tr->get(uidPrefixKey(applyMutationsBeginRange.begin, uid)),
[=](Optional<Value> const& value) -> Version {
return value.present() ? BinaryReader::fromStringRef<Version>(value.get(), Unversioned()) : 0;
});
}
void setApplyEndVersion(Reference<ReadYourWritesTransaction> tr, Version ver) {
tr->set(uidPrefixKey(applyMutationsEndRange.begin, uid), BinaryWriter::toValue(ver, Unversioned()));
}
Future<Version> getApplyEndVersion(Reference<ReadYourWritesTransaction> tr) {
return map(tr->get(uidPrefixKey(applyMutationsEndRange.begin, uid)),
[=](Optional<Value> const& value) -> Version {
return value.present() ? BinaryReader::fromStringRef<Version>(value.get(), Unversioned()) : 0;
});
}
static Future<Version> getCurrentVersion_impl(RestoreConfig* self, Reference<ReadYourWritesTransaction> tr) {
ERestoreState status = co_await self->stateEnum().getD(tr);
Version version = -1;
if (status == ERestoreState::RUNNING) {
version = co_await self->getApplyBeginVersion(tr);
} else if (status == ERestoreState::COMPLETED) {
version = co_await self->restoreVersion().getD(tr);
}
co_return version;
}
Future<Version> getCurrentVersion(Reference<ReadYourWritesTransaction> tr) {
return getCurrentVersion_impl(this, tr);
}
static Future<std::string> getProgress_impl(RestoreConfig restore, Reference<ReadYourWritesTransaction> tr);
Future<std::string> getProgress(Reference<ReadYourWritesTransaction> tr) { return getProgress_impl(*this, tr); }
static Future<std::string> getFullStatus_impl(RestoreConfig restore, Reference<ReadYourWritesTransaction> tr);
Future<std::string> getFullStatus(Reference<ReadYourWritesTransaction> tr) { return getFullStatus_impl(*this, tr); }
};
using RestoreFile = RestoreConfig::RestoreFile;
// Helper to count bulkload task progress for a job
// Returns: <completedTasks, submittedTasks, triggeredTasks, runningTasks, totalTasks, completedBytes>
// Sub-phases help track progress during the long "running" period:
// - Submitted: task created, waiting to be picked up by DD
// - Triggered: assigned to storage server, waiting to start
// - Running: storage server actively downloading/ingesting SST files
Future<std::tuple<int64_t, int64_t, int64_t, int64_t, int64_t, int64_t>> getBulkLoadTaskProgress(Database cx,
UID jobId) {
Transaction tr(cx);
Key readBegin = normalKeys.begin;
Key readEnd = normalKeys.end;
int64_t completedTasks = 0;
int64_t submittedTasks = 0;
int64_t triggeredTasks = 0;
int64_t runningTasks = 0;
int64_t totalTasks = 0;
int64_t completedBytes = 0;
while (readBegin < readEnd) {
Error err;
try {
tr.setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
RangeResult rangeResult = co_await krmGetRanges(&tr, bulkLoadTaskPrefix, KeyRangeRef(readBegin, readEnd));
if (rangeResult.empty()) {
break;
}
for (int i = 0; i < static_cast<int>(rangeResult.size()) - 1; ++i) {
if (rangeResult[i].value.empty()) {
continue;
}
BulkLoadTaskState task = decodeBulkLoadTaskState(rangeResult[i].value);
if (task.getJobId() != jobId) {
// Different job, stop counting
co_return std::make_tuple(
completedTasks, submittedTasks, triggeredTasks, runningTasks, totalTasks, completedBytes);
}
int manifestCount = task.getManifests().size();
totalTasks += manifestCount;
if (task.phase == BulkLoadPhase::Complete) {
completedTasks += manifestCount;
// Sum bytes from manifest data sizes
for (const auto& manifest : task.getManifests()) {
completedBytes += manifest.getTotalBytes();
}
} else if (task.phase == BulkLoadPhase::Submitted) {
submittedTasks += manifestCount;
} else if (task.phase == BulkLoadPhase::Triggered) {
triggeredTasks += manifestCount;
} else if (task.phase == BulkLoadPhase::Running) {
runningTasks += manifestCount;
}
}
readBegin = rangeResult.back().key;
} catch (Error& e) {
err = e;
}
if (err.isValid() && err.code() != error_code_success) {
TraceEvent(SevWarn, "BulkLoadTaskProgressRetry").error(err).detail("JobId", jobId);
co_await tr.onError(err);
}
}
co_return std::make_tuple(completedTasks, submittedTasks, triggeredTasks, runningTasks, totalTasks, completedBytes);
}
// Monitor BulkLoad job completion and update restore progress counters
// restoreUid is used to update the RestoreConfig progress
Future<bool> monitorBulkLoadJobCompletionWithProgress(Database cx,
UID jobId,
UID restoreUid,
int64_t totalBlocks,
double timeoutDuration,
double pollInterval,
bool lockAware) {
double timeoutStart = now();
RestoreConfig restore(restoreUid);
while (true) {
Optional<BulkLoadJobState> currentJob = co_await getRunningBulkLoadJob(cx, lockAware);
bool stillRunning = currentJob.present() && currentJob.get().getJobId() == jobId;
if (!stillRunning) {
co_return true;
}
// Update progress based on completed bulkload tasks
try {
auto [completed, submitted, triggered, running, total, bytes] = co_await getBulkLoadTaskProgress(cx, jobId);
if (total > 0) {
// For bulkload restores, fileBlockCount is 0, so use task count as "blocks"
// This provides meaningful progress tracking for the restore status display
// Include all in-progress tasks in dispatched count to show scheduling progress
int64_t inProgress = submitted + triggered + running;
int64_t effectiveTotalBlocks = totalBlocks > 0 ? totalBlocks : total;
int64_t blocksFinished = totalBlocks > 0 ? (totalBlocks * completed) / total : completed;
int64_t blocksDispatched =
totalBlocks > 0 ? (totalBlocks * (completed + inProgress)) / total : (completed + inProgress);
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
if (lockAware) {
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
}
restore.fileBlocksFinished().set(tr, blocksFinished);
restore.filesBlocksDispatched().set(tr, blocksDispatched);
restore.fileBlockCount().set(tr, effectiveTotalBlocks);
restore.bytesWritten().set(tr, bytes);
// Store sub-phase counts for detailed progress display
restore.bulkLoadSubmittedTasks().set(tr, submitted);
restore.bulkLoadTriggeredTasks().set(tr, triggered);
restore.bulkLoadRunningTasks().set(tr, running);
restore.bulkLoadTotalTasks().set(tr, total);
co_await tr->commit();
TraceEvent("BulkLoadRestoreProgress")
.detail("RestoreUID", restoreUid)
.detail("JobId", jobId)
.detail("CompletedTasks", completed)
.detail("SubmittedTasks", submitted)
.detail("TriggeredTasks", triggered)
.detail("RunningTasks", running)
.detail("TotalTasks", total)
.detail("BlocksFinished", blocksFinished)
.detail("BlocksDispatched", blocksDispatched)
.detail("EffectiveTotalBlocks", effectiveTotalBlocks)
.detail("BytesWritten", bytes);
}
} catch (Error& e) {
if (e.code() == error_code_actor_cancelled) {
throw;
}
// Log but don't fail - progress updates are best-effort
TraceEvent(SevWarn, "BulkLoadRestoreProgressError").error(e).detail("JobId", jobId);
}
if (now() - timeoutStart > timeoutDuration) {
co_return false; // Timed out
}
co_await delay(pollInterval);
}
}
Future<std::string> RestoreConfig::getProgress_impl(RestoreConfig restore, Reference<ReadYourWritesTransaction> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Future<int64_t> fileCount = restore.fileCount().getD(tr);
Future<int64_t> fileBlockCount = restore.fileBlockCount().getD(tr);
Future<int64_t> fileBlocksDispatched = restore.filesBlocksDispatched().getD(tr);
Future<int64_t> fileBlocksFinished = restore.fileBlocksFinished().getD(tr);
Future<int64_t> bytesWritten = restore.bytesWritten().getD(tr);
Future<StringRef> status = restore.stateText(tr);
Future<Version> currentVersion = restore.getCurrentVersion(tr);
Future<Version> lag = restore.getApplyVersionLag(tr);
Future<Version> firstConsistentVersion = restore.firstConsistentVersion().getD(tr);
Future<std::string> tag = restore.tag().getD(tr);
Future<std::pair<std::string, Version>> lastError = restore.lastError().getD(tr);
Future<int64_t> submittedTasks = restore.bulkLoadSubmittedTasks().getD(tr);
Future<int64_t> triggeredTasks = restore.bulkLoadTriggeredTasks().getD(tr);
Future<int64_t> runningTasks = restore.bulkLoadRunningTasks().getD(tr);
Future<int64_t> totalTasks = restore.bulkLoadTotalTasks().getD(tr);
Future<Optional<bool>> useRangeFileRestore = restore.useRangeFileRestore().get(tr);
UID uid = restore.getUid();
co_await (success(fileCount) && success(fileBlockCount) && success(fileBlocksDispatched) &&
success(fileBlocksFinished) && success(bytesWritten) && success(status) && success(currentVersion) &&
success(lag) && success(firstConsistentVersion) && success(tag) && success(lastError) &&
success(submittedTasks) && success(triggeredTasks) && success(runningTasks) && success(totalTasks) &&
success(useRangeFileRestore));
bool useRangeFile = !useRangeFileRestore.get().present() || useRangeFileRestore.get().get();
std::string errstr = "None";
if (lastError.get().second != 0) {
errstr = format("'%s' %" PRId64 "s ago.\n",
lastError.get().first.c_str(),
(tr->getReadVersion().get() - lastError.get().second) / CLIENT_KNOBS->CORE_VERSIONSPERSECOND);
}
TraceEvent("FileRestoreProgress")
.detail("RestoreUID", uid)
.detail("Tag", tag.get())
.detail("State", status.get().toString())
.detail("FileCount", fileCount.get())
.detail("FileBlocksFinished", fileBlocksFinished.get())
.detail("FileBlocksTotal", fileBlockCount.get())
.detail("FileBlocksInProgress", fileBlocksDispatched.get() - fileBlocksFinished.get())
.detail("SubmittedTasks", submittedTasks.get())
.detail("TriggeredTasks", triggeredTasks.get())
.detail("RunningTasks", runningTasks.get())
.detail("TotalTasks", totalTasks.get())
.detail("BytesWritten", bytesWritten.get())
.detail("CurrentVersion", currentVersion.get())
.detail("FirstConsistentVersion", firstConsistentVersion.get())
.detail("ApplyLag", lag.get());
std::string progressStr;
if (useRangeFile) {
progressStr = format("Tag: %s UID: %s State: %s\n",
tag.get().c_str(),
uid.toString().c_str(),
status.get().toString().c_str());
progressStr += format(" Blocks: %lld/%lld complete\n", fileBlocksFinished.get(), fileBlockCount.get());
progressStr += format(" Files: %lld\n", fileCount.get());
progressStr += format(" Bytes written: %s\n", formatBytesHumanReadable(bytesWritten.get()).c_str());
progressStr += format(" Apply version lag: %s\n", versionToString(lag.get()).c_str());
} else {
progressStr = format("Tag: %s UID: %s State: %s\n",
tag.get().c_str(),
uid.toString().c_str(),
status.get().toString().c_str());
progressStr += format(" Tasks submitted: %lld triggered: %lld running: %lld\n",
submittedTasks.get(),
triggeredTasks.get(),
runningTasks.get());
progressStr += format(" Tasks triggered: %lld / %lld total\n", triggeredTasks.get(), totalTasks.get());
progressStr += format(" Bytes written: %s\n", formatBytesHumanReadable(bytesWritten.get()).c_str());
double avgBytesPerTask = triggeredTasks.get() > 0 ? (double)bytesWritten.get() / triggeredTasks.get() : 0;
if (avgBytesPerTask > 0) {
progressStr += format(" Avg bytes/task: %s\n", formatBytesHumanReadable((int64_t)avgBytesPerTask).c_str());
}
}
co_return progressStr;
}
Future<std::string> RestoreConfig::getFullStatus_impl(RestoreConfig restore, Reference<ReadYourWritesTransaction> tr) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Future<std::vector<KeyRange>> ranges = restore.getRestoreRangesOrDefault(tr);
Future<Key> addPrefix = restore.addPrefix().getD(tr);
Future<Key> removePrefix = restore.removePrefix().getD(tr);
Future<Key> url = restore.sourceContainerURL().getD(tr);
Future<Version> restoreVersion = restore.restoreVersion().getD(tr);
Future<std::string> progress = restore.getProgress(tr);
Future<ERestoreState> restoreState = restore.stateEnum().getD(tr);
Future<Optional<bool>> useRangeFileRestore = restore.useRangeFileRestore().get(tr);
Future<Optional<bool>> bulkLoadComplete = restore.bulkLoadComplete().get(tr);
// restore might no longer be valid after the first wait so make sure it is not needed anymore.
co_await (success(ranges) && success(addPrefix) && success(removePrefix) && success(url) &&
success(restoreVersion) && success(progress) && success(restoreState) && success(useRangeFileRestore) &&
success(bulkLoadComplete));
std::string returnStr;
returnStr = format("%s URL: %s", progress.get().c_str(), url.get().toString().c_str());
for (auto& range : ranges.get()) {
returnStr += format(" Range: '%s'-'%s'", printable(range.begin).c_str(), printable(range.end).c_str());
}
returnStr += format(" AddPrefix: '%s' RemovePrefix: '%s' Version: %lld",
printable(addPrefix.get()).c_str(),
printable(removePrefix.get()).c_str(),
restoreVersion.get());
// Add enhanced status fields for BulkLoad integration
bool usingBulkLoad = useRangeFileRestore.get().present() && !useRangeFileRestore.get().get();
std::string snapshotMethod = usingBulkLoad ? "bulkload" : "rangefile";
returnStr += format(" Snapshot Method: %s", snapshotMethod.c_str());
// Add phase status information
ERestoreState currentState = restoreState.get();
bool bulkLoadDone = bulkLoadComplete.get().present() && bulkLoadComplete.get().get();
if (currentState == ERestoreState::RUNNING) {
if (usingBulkLoad) {
std::string snapshotPhase = bulkLoadDone ? "complete" : "in_progress";
returnStr += format(" Snapshot Phase: %s", snapshotPhase.c_str());
std::string mutationPhase = bulkLoadDone ? "in_progress" : "not_started";
returnStr += format(" Mutation Log Phase: %s", mutationPhase.c_str());
} else {
returnStr += " Snapshot Phase: in_progress Mutation Log Phase: in_progress";
}
} else if (currentState == ERestoreState::COMPLETED) {
returnStr += " Snapshot Phase: complete Mutation Log Phase: complete";
}
co_return returnStr;
}
// two buffers are alternatively serving data and reading data from file
// thus when one buffer is serving data through peek()
// the other buffer is reading data from file to provide pipelining.
class TwoBuffers : public ReferenceCounted<TwoBuffers>, NonCopyable {
public:
class IteratorBuffer : public ReferenceCounted<IteratorBuffer> {
public:
std::shared_ptr<char[]> data;
// has_value means there is data, otherwise it means there is no data being fetched or ready
// is_valid means data is being fetched, is_ready means data is ready
std::optional<Future<Void>> fetchingData;
size_t size;
int index;
int capacity;
explicit IteratorBuffer(int _capacity) {
capacity = _capacity;
data = std::shared_ptr<char[]>(new char[capacity]());
fetchingData.reset();
size = 0;
}
bool is_valid() { return fetchingData.has_value(); }
void reset() {
size = 0;
index = 0;
fetchingData.reset();
}
};
TwoBuffers(int capacity, Reference<IBackupContainer> _bc, std::vector<RestoreConfig::RestoreFile>& _files, int tag);
// ready need to be called first before calling peek
// because a shared_ptr cannot be wrapped by a Future
// this method ensures the current buffer has available data
Future<Void> ready();
static Future<Void> ready(Reference<TwoBuffers> self);
// fill buffer[index] with the next block of file
// it has side effects to change currentFileIndex and currentFilePosition
static Future<Void> readNextBlock(Reference<TwoBuffers> self, int index);
// peek can only be called after ready is called
// it returns the pointer to the active buffer
std::shared_ptr<char[]> peek();
int getFileIndex();
void setFileIndex(int);
bool hasNext();
void reset();
// discard the current buffer and swap to the next one
void discardAndSwap();
// try to fill the buffer[index]
// but no-op if the buffer have valid data or it is actively being filled
void fillBufferIfAbsent(int index);
size_t getBufferSize();
private:
Reference<IteratorBuffer> buffers[2]; // Two buffers for alternating
size_t bufferCapacity; // Size of each buffer in bytes
Reference<IBackupContainer> bc;
std::vector<RestoreConfig::RestoreFile> files;
int tag;
int cur; // Index of the current active buffer (0 or 1)
size_t currentFileIndex; // Index of the current file being read
size_t currentFilePosition; // Current read position in the current file
};
TwoBuffers::TwoBuffers(int capacity,
Reference<IBackupContainer> _bc,
std::vector<RestoreConfig::RestoreFile>& _files,
int _tag)
: currentFileIndex(0), currentFilePosition(0), cur(0), bufferCapacity(capacity), files(_files), bc(_bc), tag(_tag) {
buffers[0] = makeReference<IteratorBuffer>(capacity);
buffers[1] = makeReference<IteratorBuffer>(capacity);
}
bool TwoBuffers::hasNext() {
// if it is being load (valid but not ready, what would be the size?)
while (currentFileIndex < files.size() && currentFilePosition >= files[currentFileIndex].fileSize) {
currentFileIndex++;
currentFilePosition = 0;
}
if (buffers[0]->is_valid() || buffers[1]->is_valid()) {
return true;
}
return currentFileIndex != files.size();
}
Future<Void> TwoBuffers::ready() {
return ready(Reference<TwoBuffers>::addRef(this));
}
Future<Void> TwoBuffers::ready(Reference<TwoBuffers> self) {
// if cur is not ready, then wait
if (!self->hasNext()) {
co_return;
}
// try to fill the current buffer, and wait before it is filled
self->fillBufferIfAbsent(self->cur);
co_await self->buffers[self->cur]->fetchingData.value();
// try to fill the next buffer, do not wait for the filling
if (self->hasNext()) {
self->fillBufferIfAbsent(1 - self->cur);
}
}
std::shared_ptr<char[]> TwoBuffers::peek() {
return buffers[cur]->data;
}
int TwoBuffers::getFileIndex() {
return buffers[cur]->index;
}
void TwoBuffers::setFileIndex(int newIndex) {
if (newIndex < 0 || newIndex >= files.size()) {
TraceEvent(SevError, "TwoBuffersFileIndexOutOfBound")
.detail("FilesSize", files.size())
.detail("NewIndex", newIndex)
.log();
}
currentFileIndex = newIndex;
}
void TwoBuffers::discardAndSwap() {
// invalidate cur and change cur to next
buffers[cur]->fetchingData.reset();
cur = 1 - cur;
}
void TwoBuffers::reset() {
// invalidate cur and change cur to next
buffers[0]->reset();
buffers[1]->reset();
cur = 0;
currentFileIndex = 0;
currentFilePosition = 0;
}
size_t TwoBuffers::getBufferSize() {
return buffers[cur]->size;
}
// only one readNextBlock can be run at a single time, otherwie the same block might be loaded twice
Future<Void> TwoBuffers::readNextBlock(Reference<TwoBuffers> self, int index) {
if (self->currentFileIndex >= self->files.size()) {
TraceEvent(SevError, "ReadNextBlockOutOfBound")
.detail("FileIndex", self->currentFileIndex)
.detail("Tag", self->tag)
.detail("Position", self->currentFilePosition)
.detail("FileSize", self->files[self->currentFileIndex].fileSize)
.detail("FilesCount", self->files.size())
.log();
co_return;
}
Reference<IAsyncFile> asyncFile = co_await self->bc->readFile(self->files[self->currentFileIndex].fileName);
size_t fileSize = self->files[self->currentFileIndex].fileSize;
size_t remaining = fileSize - self->currentFilePosition;
size_t bytesToRead = std::min(self->bufferCapacity, remaining);
int bytesRead = co_await asyncFile->read(
static_cast<void*>(self->buffers[index]->data.get()), bytesToRead, self->currentFilePosition);
if (bytesRead != bytesToRead)
throw restore_bad_read();
self->buffers[index]->index = self->currentFileIndex;
self->buffers[index]->size = bytesRead; // Set to actual bytes read
self->currentFilePosition += bytesRead;
}
void TwoBuffers::fillBufferIfAbsent(int index) {
if (buffers[index]->is_valid()) {
// if this buffer is valid, then do not overwrite it
return;
}
if (currentFileIndex == files.size()) {
// quit if no more contents
return;
}
auto self = Reference<TwoBuffers>::addRef(this);
self->buffers[index]->fetchingData = readNextBlock(self, index);
return;
}
bool endOfBlock(char* start, int offset) {
const unsigned char paddingChar = '\xff';
return (unsigned char)*(start + offset) == paddingChar;
}
class PartitionedLogIteratorSimple : public PartitionedLogIterator {
public:
const int BATCH_READ_BLOCK_COUNT = 1;
const int BLOCK_SIZE = CLIENT_KNOBS->BACKUP_LOGFILE_BLOCK_SIZE;
const int mutationHeaderBytes = sizeof(int64_t) + sizeof(int32_t) + sizeof(int32_t);
Reference<IBackupContainer> bc;
size_t bufferCapacity;
int tag;
std::vector<RestoreConfig::RestoreFile> files;
size_t bufferOffset; // Current read offset
int bufferSize;
int fileOffset;
int fileIndex;
std::shared_ptr<char[]> buffer;
std::vector<Version> endVersions;
PartitionedLogIteratorSimple(Reference<IBackupContainer> _bc,
int _tag,
std::vector<RestoreConfig::RestoreFile> _files,
std::vector<Version> _endVersions);
bool hasNext() override;
Future<Void> loadNextBlock();
static Future<Void> loadNextBlock(Reference<PartitionedLogIteratorSimple> self);
void removeBlockHeader();
Standalone<VectorRef<VersionedMutation>> consumeData(Version firstVersion);
// find the next version without advanding the iterator
Future<Version> peekNextVersion() override;
static Future<Version> peekNextVersion(Reference<PartitionedLogIteratorSimple> iterator);
// get all the mutations of next version and advance the iterator
// this might issue multiple consumeData() if the data of a version cross buffer boundary
Future<Standalone<VectorRef<VersionedMutation>>> getNext() override;
static Future<Standalone<VectorRef<VersionedMutation>>> getNext(Reference<PartitionedLogIteratorSimple> iterator);
};
PartitionedLogIteratorSimple::PartitionedLogIteratorSimple(Reference<IBackupContainer> _bc,
int _tag,
std::vector<RestoreConfig::RestoreFile> _files,
std::vector<Version> _endVersions)
: bc(_bc), tag(_tag), endVersions(_endVersions), files(std::move(_files)), bufferOffset(0) {
bufferCapacity = static_cast<size_t>(BATCH_READ_BLOCK_COUNT) * BLOCK_SIZE;
buffer = std::shared_ptr<char[]>(new char[bufferCapacity]());
fileOffset = 0;
fileIndex = 0;
bufferSize = 0;
}
// it will set fileOffset and fileIndex
bool PartitionedLogIteratorSimple::hasNext() {
if (bufferOffset < bufferSize) {
return true;
}
while (fileIndex < files.size() && fileOffset >= files[fileIndex].fileSize) {
TraceEvent("ReachEndOfLogFiles")
.detail("BufferOffset", bufferOffset)
.detail("BufferSize", bufferSize)
.detail("FileOffset", fileOffset)
.detail("FileSize", files[fileIndex].fileSize)
.detail("FileName", files[fileIndex].fileName)
.detail("Tag", tag)
.detail("Index", fileIndex)
.log();
fileOffset = 0;
fileIndex++;
}
return fileIndex < files.size() && fileOffset < files[fileIndex].fileSize;
}
void PartitionedLogIteratorSimple::removeBlockHeader() {
if (bufferOffset % BLOCK_SIZE == 0) {
bufferOffset += sizeof(uint32_t);
}
}
Standalone<VectorRef<VersionedMutation>> PartitionedLogIteratorSimple::consumeData(Version firstVersion) {
Standalone<VectorRef<VersionedMutation>> mutations = Standalone<VectorRef<VersionedMutation>>();
char* start = buffer.get();
bool foundNewVersion = false;
while (bufferOffset < bufferSize) {
while (bufferOffset < bufferSize && !endOfBlock(start, bufferOffset)) {
// for each block
removeBlockHeader();
// encoding format:
// wr << bigEndian64(message.version.version) << bigEndian32(message.version.sub) <<
// bigEndian32(mutation.size());
Version version;
std::memcpy(&version, start + bufferOffset, sizeof(Version));
version = bigEndian64(version);
if (version != firstVersion) {
foundNewVersion = true;
break; // Different version, stop here
}
int32_t subsequence;
std::memcpy(&subsequence, start + bufferOffset + sizeof(Version), sizeof(int32_t));
subsequence = bigEndian32(subsequence);
int32_t mutationSize;
std::memcpy(&mutationSize, start + bufferOffset + sizeof(Version) + sizeof(int32_t), sizeof(int32_t));
mutationSize = bigEndian32(mutationSize);
// assumption: the entire mutation is within the buffer
size_t mutationTotalSize = mutationHeaderBytes + mutationSize;
ASSERT(bufferOffset + mutationTotalSize <= bufferSize);
// transform from stringref to mutationref here
Standalone<StringRef> mutationData = makeString(mutationSize);
std::memcpy(mutateString(mutationData), start + bufferOffset + mutationHeaderBytes, mutationSize);
ArenaReader reader(mutationData.arena(), mutationData, AssumeVersion(g_network->protocolVersion()));
MutationRef mutation;
reader >> mutation;
VersionedMutation vm;
vm.version = version;
vm.subsequence = subsequence;
vm.mutation = mutation;
mutations.push_back_deep(mutations.arena(), vm);
// Move the bufferOffset to include this mutation
bufferOffset += mutationTotalSize;
}
if (bufferOffset < bufferSize && endOfBlock(start, bufferOffset)) {
// there are paddings
int remain = BLOCK_SIZE - (bufferOffset % BLOCK_SIZE);
bufferOffset += remain;
}
if (foundNewVersion) {
break;
}
}
return mutations;
}
Future<Void> PartitionedLogIteratorSimple::loadNextBlock() {
return loadNextBlock(Reference<PartitionedLogIteratorSimple>::addRef(this));
}
Future<Void> PartitionedLogIteratorSimple::loadNextBlock(Reference<PartitionedLogIteratorSimple> self) {
if (self->bufferOffset < self->bufferSize) {
// do nothing
co_return;
}
if (!self->hasNext()) {
co_return;
}
Reference<IAsyncFile> asyncFile = co_await self->bc->readFile(self->files[self->fileIndex].fileName);
size_t fileSize = self->files[self->fileIndex].fileSize;
size_t remaining = fileSize - self->fileOffset;
size_t bytesToRead = std::min(self->bufferCapacity, remaining);
int bytesRead = co_await asyncFile->read(static_cast<void*>((self->buffer.get())), bytesToRead, self->fileOffset);
if (bytesRead != bytesToRead)
throw restore_bad_read();
self->bufferSize = bytesRead; // Set to actual bytes read
self->bufferOffset = 0; // Reset bufferOffset for the new data
self->fileOffset += bytesRead;
}
Future<Version> PartitionedLogIteratorSimple::peekNextVersion() {
return peekNextVersion(Reference<PartitionedLogIteratorSimple>::addRef(this));
}
Future<Version> PartitionedLogIteratorSimple::peekNextVersion(Reference<PartitionedLogIteratorSimple> self) {
// Read the first mutation's version
if (!self->hasNext()) {
co_return Version(0);
}
co_await self->loadNextBlock();
self->removeBlockHeader();
Version version{ 0 };
std::memcpy(&version, self->buffer.get() + self->bufferOffset, sizeof(Version));
version = bigEndian64(version);
while (self->fileIndex < self->endVersions.size() - 1 && version >= self->endVersions[self->fileIndex]) {
TraceEvent("SimpleIteratorFindOverlapAndSkip")
.detail("Version", version)
.detail("FileIndex", self->fileIndex)
.log();
self->bufferOffset = 0;
self->bufferSize = 0;
self->fileOffset = 0;
self->fileIndex += 1;
co_await self->loadNextBlock();
self->removeBlockHeader();
std::memcpy(&version, self->buffer.get() + self->bufferOffset, sizeof(Version));
version = bigEndian64(version);
}
co_return version;
}
Future<Standalone<VectorRef<VersionedMutation>>> PartitionedLogIteratorSimple::getNext(
Reference<PartitionedLogIteratorSimple> self) {
Standalone<VectorRef<VersionedMutation>> mutations;
if (!self->hasNext()) {
TraceEvent(SevWarn, "SimpleIteratorExhausted")
.detail("BufferOffset", self->bufferOffset)
.detail("BufferSize", self->bufferSize)
.detail("Tag", self->tag)
.log();
co_return mutations;
}
Version firstVersion = co_await self->peekNextVersion();
Standalone<VectorRef<VersionedMutation>> firstBatch = self->consumeData(firstVersion);
mutations = firstBatch;
// If the current buffer is fully consumed, then we need to check the next buffer in case
// the version is sliced across this buffer boundary
while (self->bufferOffset >= self->bufferSize) {
// data for one version cannot exceed single buffer size
// if hitting the end of a batch, check the next batch in case version is
if (self->hasNext()) {
// now this is run for each block, but it is not necessary if it is the last block of a file
// cannot check hasMoreData here because other buffer might have the last piece
co_await self->loadNextBlock();
Standalone<VectorRef<VersionedMutation>> batch = self->consumeData(firstVersion);
for (const VersionedMutation& vm : batch) {
mutations.push_back_deep(mutations.arena(), vm);
}
} else {
break;
}
}
co_return mutations;
}
Future<Standalone<VectorRef<VersionedMutation>>> PartitionedLogIteratorSimple::getNext() {
return getNext(Reference<PartitionedLogIteratorSimple>::addRef(this));
}
class PartitionedLogIteratorTwoBuffers : public PartitionedLogIterator {
private:
Reference<TwoBuffers> twobuffer;
// consume single version data upto the end of the current batch
// stop if seeing a different version from the parameter.
// it has side effects to update bufferOffset after reading the data
Future<Standalone<VectorRef<VersionedMutation>>> consumeData(Version firstVersion);
static Future<Standalone<VectorRef<VersionedMutation>>> consumeData(
Reference<PartitionedLogIteratorTwoBuffers> self,
Version v);
// each block has a format of {<header>[mutations]<padding>}, need to skip the header to read mutations
// this method check if bufferOffset is at the boundary and advance it if necessary
void removeBlockHeader();
public:
// read up to a fixed number of block count
// noted that each version has to be contained within 2 blocks
const int BATCH_READ_BLOCK_COUNT = 1;
const int BLOCK_SIZE = CLIENT_KNOBS->BACKUP_LOGFILE_BLOCK_SIZE;
const int mutationHeaderBytes = sizeof(int64_t) + sizeof(int32_t) + sizeof(int32_t);
Reference<IBackupContainer> bc;
int tag;
std::vector<RestoreConfig::RestoreFile> files;
std::vector<Version> endVersions;
bool hasMoreData; // Flag indicating if more data is available
size_t bufferOffset; // Current read offset
// empty means no data, future is valid but not ready means being fetched
// future is ready means it currently holds data
PartitionedLogIteratorTwoBuffers(Reference<IBackupContainer> _bc,
int _tag,
std::vector<RestoreConfig::RestoreFile> _files,
std::vector<Version> _endVersions);
// whether there are more contents for this tag in all files specified
bool hasNext() override;
// find the next version without advanding the iterator
Future<Version> peekNextVersion() override;
static Future<Version> peekNextVersion(Reference<PartitionedLogIteratorTwoBuffers> iterator);
// get all the mutations of next version and advance the iterator
// this might issue multiple consumeData() if the data of a version cross buffer boundary
Future<Standalone<VectorRef<VersionedMutation>>> getNext() override;
static Future<Standalone<VectorRef<VersionedMutation>>> getNext(
Reference<PartitionedLogIteratorTwoBuffers> iterator);
};
Future<Standalone<VectorRef<VersionedMutation>>> PartitionedLogIteratorTwoBuffers::consumeData(Version firstVersion) {
return consumeData(Reference<PartitionedLogIteratorTwoBuffers>::addRef(this), firstVersion);
}
Future<Standalone<VectorRef<VersionedMutation>>> PartitionedLogIteratorTwoBuffers::consumeData(
Reference<PartitionedLogIteratorTwoBuffers> self,
Version firstVersion) {
Standalone<VectorRef<VersionedMutation>> mutations = Standalone<VectorRef<VersionedMutation>>();
co_await self->twobuffer->ready();
std::shared_ptr<char[]> start = self->twobuffer->peek();
int size = self->twobuffer->getBufferSize();
bool foundNewVersion = false;
while (self->bufferOffset < size) {
while (self->bufferOffset < size && !endOfBlock(start.get(), self->bufferOffset)) {
// for each block
self->removeBlockHeader();
// encoding is:
// wr << bigEndian64(message.version.version) << bigEndian32(message.version.sub) <<
// bigEndian32(mutation.size());
Version version;
std::memcpy(&version, start.get() + self->bufferOffset, sizeof(Version));
version = bigEndian64(version);
if (version != firstVersion) {
foundNewVersion = true;
break; // Different version, stop here
}
int32_t subsequence;
std::memcpy(&subsequence, start.get() + self->bufferOffset + sizeof(Version), sizeof(int32_t));
subsequence = bigEndian32(subsequence);
int32_t mutationSize;
std::memcpy(
&mutationSize, start.get() + self->bufferOffset + sizeof(Version) + sizeof(int32_t), sizeof(int32_t));
mutationSize = bigEndian32(mutationSize);
// assumption: the entire mutation is within the buffer
size_t mutationTotalSize = self->mutationHeaderBytes + mutationSize;
ASSERT(self->bufferOffset + mutationTotalSize <= size);
Standalone<StringRef> mutationData = makeString(mutationSize);
std::memcpy(
mutateString(mutationData), start.get() + self->bufferOffset + self->mutationHeaderBytes, mutationSize);
// transform from stringref to mutationref here
ArenaReader reader(mutationData.arena(), mutationData, AssumeVersion(g_network->protocolVersion()));
MutationRef mutation;
reader >> mutation;
VersionedMutation vm;
vm.version = version;
vm.subsequence = subsequence;
vm.mutation = mutation;
mutations.push_back_deep(mutations.arena(), vm);
// Move the bufferOffset to include this mutation
self->bufferOffset += mutationTotalSize;
}
if (self->bufferOffset < size && endOfBlock(start.get(), self->bufferOffset)) {
// there are paddings, skip them
int remain = self->BLOCK_SIZE - (self->bufferOffset % self->BLOCK_SIZE);
self->bufferOffset += remain;
}
if (foundNewVersion) {
break;
}
}
co_return mutations;
}
void PartitionedLogIteratorTwoBuffers::removeBlockHeader() {
if (bufferOffset % BLOCK_SIZE == 0) {
bufferOffset += sizeof(uint32_t);
}
}
PartitionedLogIteratorTwoBuffers::PartitionedLogIteratorTwoBuffers(Reference<IBackupContainer> _bc,
int _tag,
std::vector<RestoreConfig::RestoreFile> _files,
std::vector<Version> _endVersions)
: bc(_bc), tag(_tag), files(std::move(_files)), endVersions(_endVersions), bufferOffset(0) {
int bufferCapacity = BATCH_READ_BLOCK_COUNT * BLOCK_SIZE;
twobuffer = makeReference<TwoBuffers>(bufferCapacity, _bc, files, tag);
}
bool PartitionedLogIteratorTwoBuffers::hasNext() {
return twobuffer->hasNext();
}
Future<Version> PartitionedLogIteratorTwoBuffers::peekNextVersion() {
return peekNextVersion(Reference<PartitionedLogIteratorTwoBuffers>::addRef(this));
}
Future<Version> PartitionedLogIteratorTwoBuffers::peekNextVersion(Reference<PartitionedLogIteratorTwoBuffers> self) {
// Read the first mutation's version
std::shared_ptr<char[]> start;
Version version{ 0 };
int fileIndex{ 0 };
if (!self->hasNext()) {
co_return Version(0);
}
co_await self->twobuffer->ready();
start = self->twobuffer->peek();
self->removeBlockHeader();
std::memcpy(&version, start.get() + self->bufferOffset, sizeof(Version));
version = bigEndian64(version);
fileIndex = self->twobuffer->getFileIndex();
while (fileIndex < self->endVersions.size() - 1 && version >= self->endVersions[fileIndex]) {
TraceEvent("RestoreLogFilesFoundOverlapAndSkip")
.detail("Version", version)
.detail("FileIndex", fileIndex)
.log();
// need to read from next file in the case of overlap range versions between log files
self->twobuffer->reset();
self->bufferOffset = 0;
self->twobuffer->setFileIndex(fileIndex + 1);
co_await self->twobuffer->ready();
start = self->twobuffer->peek();
self->removeBlockHeader();
std::memcpy(&version, start.get() + self->bufferOffset, sizeof(Version));
version = bigEndian64(version);
fileIndex = self->twobuffer->getFileIndex();
}
co_return version;
}
Future<Standalone<VectorRef<VersionedMutation>>> PartitionedLogIteratorTwoBuffers::getNext(
Reference<PartitionedLogIteratorTwoBuffers> self) {
Standalone<VectorRef<VersionedMutation>> mutations;
if (!self->hasNext()) {
TraceEvent(SevWarn, "IteratorExhausted").log();
co_return mutations;
}
Version firstVersion = co_await self->peekNextVersion();
Standalone<VectorRef<VersionedMutation>> firstBatch = co_await self->consumeData(firstVersion);
mutations = firstBatch;
// If the current buffer is fully consumed, then we need to check the next buffer in case
// the version is sliced across this buffer boundary
while (self->bufferOffset >= self->twobuffer->getBufferSize()) {
self->twobuffer->discardAndSwap();
self->bufferOffset = 0;
// data for one version cannot exceed single buffer size
// if hitting the end of a batch, check the next batch in case version is
if (self->twobuffer->hasNext()) {
// now this is run for each block, but it is not necessary if it is the last block of a file
// cannot check hasMoreData here because other buffer might have the last piece
Version nextVersion = co_await self->peekNextVersion();
if (nextVersion != firstVersion) {
break;
}
Standalone<VectorRef<VersionedMutation>> batch = co_await self->consumeData(firstVersion);
for (const VersionedMutation& vm : batch) {
mutations.push_back_deep(mutations.arena(), vm);
}
} else {
break;
}
}
co_return mutations;
}
Future<Standalone<VectorRef<VersionedMutation>>> PartitionedLogIteratorTwoBuffers::getNext() {
return getNext(Reference<PartitionedLogIteratorTwoBuffers>::addRef(this));
}
FileBackupAgent::FileBackupAgent()
: subspace(Subspace(fileBackupPrefixRange.begin))
// The other subspaces have logUID -> value
,
config(subspace.get(BackupAgentBase::keyConfig)), lastRestorable(subspace.get(FileBackupAgent::keyLastRestorable)),
taskBucket(new TaskBucket(subspace.get(BackupAgentBase::keyTasks),
AccessSystemKeys::True,
PriorityBatch::False,
LockAware::True)),
futureBucket(new FutureBucket(subspace.get(BackupAgentBase::keyFutures), AccessSystemKeys::True, LockAware::True)) {
}
namespace fileBackup {
static Reference<IBackupContainer> getBackupContainerWithProxy(Reference<IBackupContainer> _bc) {
Reference<IBackupContainer> bc = IBackupContainer::openContainer(
_bc->getURL(), fileBackupAgentProxy, _bc->getEncryptionKeyFileName(), _bc->getEncryptionBlockSize());
return bc;
}
Future<Void> checkTaskVersion(Database cx, Reference<Task> task, StringRef name, uint32_t version) {
uint32_t taskVersion = task->getVersion();
if (taskVersion > version) {
Error err = task_invalid_version();
TraceEvent(SevWarn, "BA_BackupRangeTaskFuncExecute")
.detail("TaskVersion", taskVersion)
.detail("Name", name)
.detail("Version", version);
if (KeyBackedTaskConfig::TaskParams.uid().exists(task)) {
std::string msg = format("%s task version `%lu' is greater than supported version `%lu'",
task->params[Task::reservedTaskParamKeyType].toString().c_str(),
(unsigned long)taskVersion,
(unsigned long)version);
co_await BackupConfig(task).logError(cx, err, msg);
}
throw err;
}
}
static Future<Void> abortFiveZeroBackup(FileBackupAgent* backupAgent,
Reference<ReadYourWritesTransaction> tr,
std::string tagName) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Subspace tagNames = backupAgent->subspace.get(BackupAgentBase::keyTagName);
Optional<Value> uidStr = co_await tr->get(tagNames.pack(Key(tagName)));
if (!uidStr.present()) {
TraceEvent(SevWarn, "FileBackupAbortIncompatibleBackup_TagNotFound").detail("TagName", tagName.c_str());
co_return;
}
UID uid = BinaryReader::fromStringRef<UID>(uidStr.get(), Unversioned());
Subspace statusSpace = backupAgent->subspace.get(BackupAgentBase::keyStates).get(uid.toString());
Subspace globalConfig = backupAgent->subspace.get(BackupAgentBase::keyConfig).get(uid.toString());
Subspace newConfigSpace(uidPrefixKey("uid->config/"_sr.withPrefix(fileBackupPrefixRange.begin), uid));
Optional<Value> statusStr = co_await tr->get(statusSpace.pack(FileBackupAgent::keyStateStatus));
EBackupState status =
!statusStr.present() ? EBackupState::STATE_NEVERRAN : BackupAgentBase::getState(statusStr.get().toString());
TraceEvent(SevInfo, "FileBackupAbortIncompatibleBackup")
.detail("TagName", tagName.c_str())
.detail("Status", BackupAgentBase::getStateText(status));
// Clear the folder id to prevent future tasks from executing at all
tr->clear(singleKeyRange(StringRef(globalConfig.pack(FileBackupAgent::keyFolderId))));
// Clear the mutations logging config and data
Key configPath = uidPrefixKey(logRangesRange.begin, uid);
Key logsPath = uidPrefixKey(backupLogKeys.begin, uid);
tr->clear(KeyRangeRef(configPath, strinc(configPath)));
tr->clear(KeyRangeRef(logsPath, strinc(logsPath)));
// Clear the new-style config space
tr->clear(newConfigSpace.range());
Key statusKey = StringRef(statusSpace.pack(FileBackupAgent::keyStateStatus));
// Set old style state key to Aborted if it was Runnable
if (backupAgent->isRunnable(status))
tr->set(statusKey, StringRef(FileBackupAgent::getStateText(EBackupState::STATE_ABORTED)));
}
struct AbortFiveZeroBackupTask : TaskFuncBase {
static StringRef name;
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
FileBackupAgent backupAgent;
std::string tagName = task->params[BackupAgentBase::keyConfigBackupTag].toString();
TraceEvent(SevInfo, "FileBackupCancelOldTask")
.detail("Task", task->params[Task::reservedTaskParamKeyType])
.detail("TagName", tagName);
co_await abortFiveZeroBackup(&backupAgent, tr, tagName);
co_await taskBucket->finish(tr, task);
co_return;
}
StringRef getName() const override {
TraceEvent(SevError, "FileBackupError")
.detail("Cause", "AbortFiveZeroBackupTaskFunc::name() should never be called");
ASSERT(false);
return StringRef();
}
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return Future<Void>(Void());
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef AbortFiveZeroBackupTask::name = "abort_legacy_backup"_sr;
REGISTER_TASKFUNC(AbortFiveZeroBackupTask);
REGISTER_TASKFUNC_ALIAS(AbortFiveZeroBackupTask, file_backup_diff_logs);
REGISTER_TASKFUNC_ALIAS(AbortFiveZeroBackupTask, file_backup_log_range);
REGISTER_TASKFUNC_ALIAS(AbortFiveZeroBackupTask, file_backup_logs);
REGISTER_TASKFUNC_ALIAS(AbortFiveZeroBackupTask, file_backup_range);
REGISTER_TASKFUNC_ALIAS(AbortFiveZeroBackupTask, file_backup_restorable);
REGISTER_TASKFUNC_ALIAS(AbortFiveZeroBackupTask, file_finish_full_backup);
REGISTER_TASKFUNC_ALIAS(AbortFiveZeroBackupTask, file_finished_full_backup);
REGISTER_TASKFUNC_ALIAS(AbortFiveZeroBackupTask, file_start_full_backup);
static Future<Void> abortFiveOneBackup(FileBackupAgent* backupAgent,
Reference<ReadYourWritesTransaction> tr,
std::string tagName) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
KeyBackedTag tag = makeBackupTag(tagName);
UidAndAbortedFlagT current = co_await tag.getOrThrow(tr, Snapshot::False, backup_unneeded());
BackupConfig config(current.first);
EBackupState status = co_await config.stateEnum().getD(tr, Snapshot::False, EBackupState::STATE_NEVERRAN);
if (!backupAgent->isRunnable(status)) {
throw backup_unneeded();
}
TraceEvent(SevInfo, "FBA_AbortFileOneBackup")
.detail("TagName", tagName.c_str())
.detail("Status", BackupAgentBase::getStateText(status));
// Cancel backup task through tag
co_await tag.cancel(tr);
Key configPath = uidPrefixKey(logRangesRange.begin, config.getUid());
Key logsPath = uidPrefixKey(backupLogKeys.begin, config.getUid());
tr->clear(KeyRangeRef(configPath, strinc(configPath)));
tr->clear(KeyRangeRef(logsPath, strinc(logsPath)));
config.stateEnum().set(tr, EBackupState::STATE_ABORTED);
}
struct AbortFiveOneBackupTask : TaskFuncBase {
static StringRef name;
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
FileBackupAgent backupAgent;
BackupConfig config(task);
std::string tagName = co_await config.tag().getOrThrow(tr);
TraceEvent(SevInfo, "FileBackupCancelFiveOneTask")
.detail("Task", task->params[Task::reservedTaskParamKeyType])
.detail("TagName", tagName);
co_await abortFiveOneBackup(&backupAgent, tr, tagName);
co_await taskBucket->finish(tr, task);
}
StringRef getName() const override {
TraceEvent(SevError, "FileBackupError")
.detail("Cause", "AbortFiveOneBackupTaskFunc::name() should never be called");
ASSERT(false);
return StringRef();
}
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return Future<Void>(Void());
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef AbortFiveOneBackupTask::name = "abort_legacy_backup_5.2"_sr;
REGISTER_TASKFUNC(AbortFiveOneBackupTask);
REGISTER_TASKFUNC_ALIAS(AbortFiveOneBackupTask, file_backup_write_range);
REGISTER_TASKFUNC_ALIAS(AbortFiveOneBackupTask, file_backup_dispatch_ranges);
REGISTER_TASKFUNC_ALIAS(AbortFiveOneBackupTask, file_backup_write_logs);
REGISTER_TASKFUNC_ALIAS(AbortFiveOneBackupTask, file_backup_erase_logs);
REGISTER_TASKFUNC_ALIAS(AbortFiveOneBackupTask, file_backup_dispatch_logs);
REGISTER_TASKFUNC_ALIAS(AbortFiveOneBackupTask, file_backup_finished);
REGISTER_TASKFUNC_ALIAS(AbortFiveOneBackupTask, file_backup_write_snapshot_manifest);
REGISTER_TASKFUNC_ALIAS(AbortFiveOneBackupTask, file_backup_start);
std::function<void(Reference<Task>)> NOP_SETUP_TASK_FN = [](Reference<Task> task) { /* NOP */ };
static Future<Key> addBackupTask(StringRef name,
uint32_t version,
Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
TaskCompletionKey completionKey,
BackupConfig config,
Reference<TaskFuture> waitFor = Reference<TaskFuture>(),
std::function<void(Reference<Task>)> setupTaskFn = NOP_SETUP_TASK_FN,
int priority = 0,
SetValidation setValidation = SetValidation::True) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Key doneKey = co_await completionKey.get(tr, taskBucket);
Reference<Task> task(new Task(name, version, doneKey, priority));
// Bind backup config to new task
// allow this new task to find the config(keyspace) of the parent task
co_await config.toTask(tr, task, setValidation);
// Set task specific params
setupTaskFn(task);
if (!waitFor) {
co_return taskBucket->addTask(tr, task);
}
co_await waitFor->onSetAddTask(tr, taskBucket, task);
co_return "OnSetAddTask"_sr;
}
// Clears the backup ID from "backupStartedKey" to pause backup workers.
static Future<Void> clearBackupStartID(Reference<ReadYourWritesTransaction> tr, UID backupUid) {
// If backup worker is not enabled, exit early.
Optional<Value> started = co_await tr->get(backupStartedKey);
std::vector<std::pair<UID, Version>> ids;
if (started.present()) {
ids = decodeBackupStartedValue(started.get());
}
auto it =
std::find_if(ids.begin(), ids.end(), [=](const std::pair<UID, Version>& p) { return p.first == backupUid; });
if (it != ids.end()) {
ids.erase(it);
}
if (ids.empty()) {
TraceEvent("ClearBackup").detail("BackupID", backupUid);
tr->clear(backupStartedKey);
} else {
tr->set(backupStartedKey, encodeBackupStartedValue(ids));
}
}
// Backup and Restore taskFunc definitions will inherit from one of the following classes which
// servers to catch and log to the appropriate config any error that execute/finish didn't catch and log.
struct RestoreTaskFuncBase : TaskFuncBase {
Future<Void> handleError(Database cx, Reference<Task> task, Error const& error) final {
return RestoreConfig(task).logError(
cx,
error,
format("'%s' on '%s'", error.what(), task->params[Task::reservedTaskParamKeyType].printable().c_str()));
}
virtual std::string toString(Reference<Task> task) const { return ""; }
};
struct BackupTaskFuncBase : TaskFuncBase {
Future<Void> handleError(Database cx, Reference<Task> task, Error const& error) final {
return BackupConfig(task).logError(
cx,
error,
format("'%s' on '%s'", error.what(), task->params[Task::reservedTaskParamKeyType].printable().c_str()));
}
virtual std::string toString(Reference<Task> task) const { return ""; }
};
static Future<Standalone<VectorRef<KeyRef>>> getBlockOfShards(Reference<ReadYourWritesTransaction> tr,
Key beginKey,
Key endKey,
int limit) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Standalone<VectorRef<KeyRef>> results;
RangeResult values = co_await tr->getRange(
KeyRangeRef(keyAfter(beginKey.withPrefix(keyServersPrefix)), endKey.withPrefix(keyServersPrefix)), limit);
for (auto& s : values) {
KeyRef k = s.key.removePrefix(keyServersPrefix);
results.push_back_deep(results.arena(), k);
}
co_return results;
}
struct BackupRangeTaskFunc : BackupTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
static struct {
static TaskParam<Key> beginKey() { return __FUNCTION__sr; }
static TaskParam<Key> endKey() { return __FUNCTION__sr; }
static TaskParam<bool> addBackupRangeTasks() { return __FUNCTION__sr; }
} Params;
std::string toString(Reference<Task> task) const override {
return format("beginKey '%s' endKey '%s' addTasks %d",
Params.beginKey().get(task).printable().c_str(),
Params.endKey().get(task).printable().c_str(),
Params.addBackupRangeTasks().get(task));
}
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
// Finish (which flushes/syncs) the file, and then in a single transaction, make some range backup progress
// durable. This means:
// - increment the backup config's range bytes written
// - update the range file map
// - update the task begin key
// - save/extend the task with the new params
// Returns whether or not the caller should continue executing the task.
static Future<bool> finishRangeFile(Reference<IBackupFile> file,
Database cx,
Reference<Task> task,
Reference<TaskBucket> taskBucket,
KeyRange range,
Version version) {
co_await file->finish();
TraceEvent("BackupRangeFileFinished")
.detail("BackupUID", BackupConfig(task).getUid())
.detail("FileName", file->getFileName())
.detail("FileSize", file->size())
.detail("RangeBegin", range.begin.printable())
.detail("RangeEnd", range.end.printable())
.detail("RangeEmpty", range.empty())
.detail("Version", version);
// Ignore empty ranges.
if (range.empty())
co_return false;
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
BackupConfig backup(task);
bool usedFile = false;
// Avoid unnecessary conflict by prevent taskbucket's automatic timeout extension
// because the following transaction loop extends and updates the task.
co_await task->extendMutex.take();
FlowLock::Releaser releaser(task->extendMutex, 1);
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
// Update the start key of the task so if this transaction completes but the task then fails
// when it is restarted it will continue where this execution left off.
Params.beginKey().set(task, range.end);
// Save and extend the task with the new begin parameter
Version newTimeout = co_await taskBucket->extendTimeout(tr, task, UpdateParams::True);
// Update the range bytes written in the backup config
backup.rangeBytesWritten().atomicOp(tr, file->size(), MutationRef::AddValue);
backup.snapshotRangeFileCount().atomicOp(tr, 1, MutationRef::AddValue);
// See if there is already a file for this key which has an earlier begin, update the map if not.
Optional<BackupConfig::RangeSlice> s = co_await backup.snapshotRangeFileMap().get(tr, range.end);
if (!s.present() || s.get().begin >= range.begin) {
backup.snapshotRangeFileMap().set(
tr, range.end, { range.begin, version, file->getFileName(), file->size() });
usedFile = true;
}
co_await tr->commit();
task->timeoutVersion = newTimeout;
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
co_return usedFile;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
int priority,
Key begin,
Key end,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>(),
Version scheduledVersion = invalidVersion) {
Key key = co_await addBackupTask(
BackupRangeTaskFunc::name,
BackupRangeTaskFunc::version,
tr,
taskBucket,
completionKey,
BackupConfig(parentTask),
waitFor,
[=](Reference<Task> task) {
Params.beginKey().set(task, begin);
Params.endKey().set(task, end);
Params.addBackupRangeTasks().set(task, false);
if (scheduledVersion != invalidVersion)
ReservedTaskParams::scheduledVersion().set(task, scheduledVersion);
},
priority);
co_return key;
}
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
Reference<FlowLock> lock(new FlowLock(CLIENT_KNOBS->BACKUP_LOCK_BYTES));
co_await checkTaskVersion(cx, task, BackupRangeTaskFunc::name, BackupRangeTaskFunc::version);
Key beginKey = Params.beginKey().get(task);
Key endKey = Params.endKey().get(task);
TraceEvent("FileBackupRangeStart")
.suppressFor(60)
.detail("BackupUID", BackupConfig(task).getUid())
.detail("BeginKey", Params.beginKey().get(task).printable())
.detail("EndKey", Params.endKey().get(task).printable())
.detail("TaskKey", task->key.printable());
// When a key range task saves the last chunk of progress and then the executor dies, when the task
// continues its beginKey and endKey will be equal but there is no work to be done.
if (beginKey == endKey)
co_return;
// Find out if there is a shard boundary in(beginKey, endKey)
Standalone<VectorRef<KeyRef>> keys = co_await runRYWTransaction(
cx, [=](Reference<ReadYourWritesTransaction> tr) { return getBlockOfShards(tr, beginKey, endKey, 1); });
if (!keys.empty()) {
Params.addBackupRangeTasks().set(task, true);
co_return;
}
// Read everything from beginKey to endKey, write it to an output file, run the output file processor, and
// then set on_done. If we are still writing after X seconds, end the output file and insert a new
// backup_range task for the remainder.
Reference<IBackupFile> outFile;
Version outVersion = invalidVersion;
Key lastKey;
// retrieve kvData
PromiseStream<RangeResultWithVersion> results;
Future<Void> rc = readCommitted(cx,
results,
lock,
KeyRangeRef(beginKey, endKey),
Terminator::True,
AccessSystemKeys::True,
LockAware::True,
ReadLowPriority(CLIENT_KNOBS->BACKUP_READS_USE_LOW_PRIORITY));
std::unique_ptr<IRangeFileWriter> rangeFile;
BackupConfig backup(task);
Arena arena;
DatabaseConfiguration config = co_await getDatabaseConfiguration(cx);
// Don't need to check keepRunning(task) here because we will do that while finishing each output file, but
// if bc is false then clearly the backup is no longer in progress
Reference<IBackupContainer> _bc = co_await backup.backupContainer().getD(cx.getReference());
if (!_bc) {
co_return;
}
Reference<IBackupContainer> bc = getBackupContainerWithProxy(_bc);
bool done = false;
int64_t nrKeys = 0;
while (true) {
RangeResultWithVersion values;
try {
RangeResultWithVersion _values = co_await results.getFuture();
values = _values;
lock->release(values.first.expectedSize());
} catch (Error& e) {
if (e.code() == error_code_end_of_stream)
done = true;
else
throw;
}
// If we've seen a new read version OR hit the end of the stream, then if we were writing a file finish
// it.
if (values.second != outVersion || done) {
if (outFile) {
CODE_PROBE(outVersion != invalidVersion, "Backup range task wrote multiple versions");
Key nextKey = done ? endKey : keyAfter(lastKey);
co_await rangeFile->writeKey(nextKey);
if (buggify()) {
co_await rangeFile->padEnd(true);
}
co_await rangeFile->finish();
bool usedFile = co_await finishRangeFile(
outFile, cx, task, taskBucket, KeyRangeRef(beginKey, nextKey), outVersion);
TraceEvent("FileBackupWroteRangeFile")
.suppressFor(60)
.detail("BackupUID", backup.getUid())
.detail("Size", outFile->size())
.detail("Keys", nrKeys)
.detail("ReadVersion", outVersion)
.detail("BeginKey", beginKey.printable())
.detail("EndKey", nextKey.printable())
.detail("AddedFileToMap", usedFile);
nrKeys = 0;
beginKey = nextKey;
}
if (done)
co_return;
// Start writing a new file after verifying this task should keep running as of a new read version
// (which must be >= outVersion)
outVersion = values.second;
// block size must be at least large enough for 3 max size keys and 2 max size values + overhead so
// 250k conservatively.
int blockSize = buggify() ? deterministicRandom()->randomInt(250e3, 4e6)
: CLIENT_KNOBS->BACKUP_RANGEFILE_BLOCK_SIZE;
Version snapshotBeginVersion{ 0 };
int64_t snapshotRangeFileCount{ 0 };
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
co_await (taskBucket->keepRunning(tr, task) &&
storeOrThrow(snapshotBeginVersion, backup.snapshotBeginVersion().get(tr)) &&
store(snapshotRangeFileCount, backup.snapshotRangeFileCount().getD(tr)));
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
Reference<IBackupFile> f =
co_await bc->writeRangeFile(snapshotBeginVersion, snapshotRangeFileCount, outVersion, blockSize);
outFile = f;
// Initialize range file writer and write begin key
rangeFile = std::make_unique<RangeFileWriter>(outFile, blockSize);
co_await rangeFile->writeKey(beginKey);
}
// write kvData to file, update lastKey and key count
if (!values.first.empty()) {
for (size_t i = 0; i < values.first.size(); ++i) {
co_await rangeFile->writeKV(values.first[i].key, values.first[i].value);
}
lastKey = values.first.back().key;
nrKeys += values.first.size();
}
}
}
static Future<Void> startBackupRangeInternal(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task,
Reference<TaskFuture> onDone) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Key nextKey = Params.beginKey().get(task);
Key endKey = Params.endKey().get(task);
Standalone<VectorRef<KeyRef>> keys =
co_await getBlockOfShards(tr, nextKey, endKey, CLIENT_KNOBS->BACKUP_SHARD_TASK_LIMIT);
std::vector<Future<Key>> addTaskVector;
for (const auto& splitKey : keys) {
if (nextKey != splitKey) {
addTaskVector.push_back(addTask(
tr, taskBucket, task, task->getPriority(), nextKey, splitKey, TaskCompletionKey::joinWith(onDone)));
TraceEvent("FileBackupRangeSplit")
.suppressFor(60)
.detail("BackupUID", BackupConfig(task).getUid())
.detail("BeginKey", Params.beginKey().get(task).printable())
.detail("EndKey", Params.endKey().get(task).printable())
.detail("SliceBeginKey", nextKey.printable())
.detail("SliceEndKey", splitKey.printable());
}
nextKey = splitKey;
}
co_await waitForAll(addTaskVector);
if (nextKey != endKey) {
// Add task to cover nextKey to the end, using the priority of the current task
co_await addTask(tr,
taskBucket,
task,
task->getPriority(),
nextKey,
endKey,
TaskCompletionKey::joinWith(onDone),
Reference<TaskFuture>(),
task->getPriority());
}
co_return;
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
Reference<TaskFuture> taskFuture = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
if (Params.addBackupRangeTasks().get(task)) {
co_await startBackupRangeInternal(tr, taskBucket, futureBucket, task, taskFuture);
} else {
co_await taskFuture->set(tr, taskBucket);
}
co_await taskBucket->finish(tr, task);
TraceEvent("FileBackupRangeFinish")
.suppressFor(60)
.detail("BackupUID", BackupConfig(task).getUid())
.detail("BeginKey", Params.beginKey().get(task).printable())
.detail("EndKey", Params.endKey().get(task).printable())
.detail("TaskKey", task->key.printable());
co_return;
}
};
StringRef BackupRangeTaskFunc::name = "file_backup_write_range_5.2"_sr;
REGISTER_TASKFUNC(BackupRangeTaskFunc);
struct BackupSnapshotDispatchTask : BackupTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
static struct {
// Set by Execute, used by Finish
static TaskParam<int64_t> shardsBehind() { return __FUNCTION__sr; }
// Set by Execute, used by Finish
static TaskParam<bool> snapshotFinished() { return __FUNCTION__sr; }
// Set by Execute, used by Finish
static TaskParam<Version> nextDispatchVersion() { return __FUNCTION__sr; }
} Params;
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
int priority,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>(),
Version scheduledVersion = invalidVersion) {
Key key = co_await addBackupTask(
name,
version,
tr,
taskBucket,
completionKey,
BackupConfig(parentTask),
waitFor,
[=](Reference<Task> task) {
if (scheduledVersion != invalidVersion)
ReservedTaskParams::scheduledVersion().set(task, scheduledVersion);
},
priority);
co_return key;
}
enum DispatchState { SKIP = 0, DONE = 1, NOT_DONE_MIN = 2 };
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
Reference<FlowLock> lock(new FlowLock(CLIENT_KNOBS->BACKUP_LOCK_BYTES));
co_await checkTaskVersion(cx, task, name, version);
double startTime = timer();
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
// The shard map will use 3 values classes. Exactly SKIP, exactly DONE, then any number >= NOT_DONE_MIN
// which will mean not done. This is to enable an efficient coalesce() call to squash adjacent ranges which
// are not yet finished to enable efficiently finding random database shards which are not done.
int notDoneSequence = NOT_DONE_MIN;
KeyRangeMap<int> shardMap(notDoneSequence++);
Key beginKey = allKeys.begin;
// Read all shard boundaries and add them to the map
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Future<Standalone<VectorRef<KeyRef>>> shardBoundaries =
getBlockOfShards(tr, beginKey, allKeys.end, CLIENT_KNOBS->TOO_MANY);
co_await (success(shardBoundaries) && taskBucket->keepRunning(tr, task));
if (shardBoundaries.get().empty())
break;
for (auto& boundary : shardBoundaries.get()) {
shardMap.rawInsert(boundary, notDoneSequence++);
}
beginKey = keyAfter(shardBoundaries.get().back());
tr->reset();
continue;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
// Read required stuff from backup config
BackupConfig config(task);
Version recentReadVersion{ 0 };
Version snapshotBeginVersion{ 0 };
Version snapshotTargetEndVersion{ 0 };
int64_t snapshotIntervalSeconds{ 0 };
Optional<Version> latestSnapshotEndVersion;
std::vector<KeyRange> backupRanges;
Optional<Key> snapshotBatchFutureKey;
Reference<TaskFuture> snapshotBatchFuture;
Optional<int64_t> snapshotBatchSize;
tr->reset();
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
co_await (store(snapshotBeginVersion, config.snapshotBeginVersion().getOrThrow(tr)) &&
store(snapshotTargetEndVersion, config.snapshotTargetEndVersion().getOrThrow(tr)) &&
store(backupRanges, config.backupRanges().getOrThrow(tr)) &&
store(snapshotIntervalSeconds, config.snapshotIntervalSeconds().getOrThrow(tr))
// The next two parameters are optional
&& store(snapshotBatchFutureKey, config.snapshotBatchFuture().get(tr)) &&
store(snapshotBatchSize, config.snapshotBatchSize().get(tr)) &&
store(latestSnapshotEndVersion, config.latestSnapshotEndVersion().get(tr)) &&
store(recentReadVersion, tr->getReadVersion()) && taskBucket->keepRunning(tr, task));
// If the snapshot batch future key does not exist, this is the first execution of this dispatch
// task so
// - create and set the snapshot batch future key
// - initialize the batch size to 0
// - initialize the target snapshot end version if it is not yet set
// - commit
if (!snapshotBatchFutureKey.present()) {
snapshotBatchFuture = futureBucket->future(tr);
config.snapshotBatchFuture().set(tr, snapshotBatchFuture->pack());
snapshotBatchSize = 0;
config.snapshotBatchSize().set(tr, snapshotBatchSize.get());
// The dispatch of this batch can take multiple separate executions if the executor fails
// so store a completion key for the dispatch finish() to set when dispatching the batch is
// done.
TaskCompletionKey dispatchCompletionKey = TaskCompletionKey::joinWith(snapshotBatchFuture);
// this is a bad hack - but flow doesn't work well with lambda functions and capturing
// state variables...
auto cfg = &config;
auto tx = &tr;
co_await map(dispatchCompletionKey.get(tr, taskBucket), [cfg, tx](Key const& k) {
cfg->snapshotBatchDispatchDoneKey().set(*tx, k);
return Void();
});
co_await tr->commit();
} else {
ASSERT(snapshotBatchSize.present());
// Batch future key exists in the config so create future from it
snapshotBatchFuture = makeReference<TaskFuture>(futureBucket, snapshotBatchFutureKey.get());
}
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
// Read all dispatched ranges
std::vector<std::pair<Key, bool>> dispatchBoundaries;
tr->reset();
beginKey = allKeys.begin;
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Future<BackupConfig::RangeDispatchMapT::RangeResultType> bounds =
config.snapshotRangeDispatchMap().getRange(
tr, beginKey, keyAfter(allKeys.end), CLIENT_KNOBS->TOO_MANY);
co_await (success(bounds) && taskBucket->keepRunning(tr, task) &&
store(recentReadVersion, tr->getReadVersion()));
if (!bounds.get().results.empty()) {
dispatchBoundaries.reserve(dispatchBoundaries.size() + bounds.get().results.size());
dispatchBoundaries.insert(
dispatchBoundaries.end(), bounds.get().results.begin(), bounds.get().results.end());
}
if (!bounds.get().more) {
break;
}
beginKey = keyAfter(bounds.get().results.back().first);
tr->reset();
continue;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
// The next few sections involve combining the results above. Yields are used after operations
// that could have operated on many thousands of things and in loops which could have many
// thousands of iterations.
// Declare some common iterators which must be state vars and will be used multiple times.
int i{ 0 };
RangeMap<Key, int, KeyRangeRef>::iterator iShard;
RangeMap<Key, int, KeyRangeRef>::iterator iShardEnd;
// Set anything inside a dispatched range to DONE.
// Also ensure that the boundary value are true, false, [true, false]...
if (!dispatchBoundaries.empty()) {
bool lastValue = false;
Key lastKey;
for (i = 0; i < dispatchBoundaries.size(); ++i) {
const std::pair<Key, bool>& boundary = dispatchBoundaries[i];
// Values must alternate
ASSERT(boundary.second == !lastValue);
// If this was the end of a dispatched range
if (!boundary.second) {
// Ensure that the dispatched boundaries exist AND set all shard ranges in the dispatched range
// to DONE.
RangeMap<Key, int, KeyRangeRef>::Ranges shardRanges =
shardMap.modify(KeyRangeRef(lastKey, boundary.first));
iShard = shardRanges.begin();
iShardEnd = shardRanges.end();
for (; iShard != iShardEnd; ++iShard) {
iShard->value() = DONE;
co_await yield();
}
}
lastValue = dispatchBoundaries[i].second;
lastKey = dispatchBoundaries[i].first;
co_await yield();
}
ASSERT(lastValue == false);
}
// Set anything outside the backup ranges to SKIP. We can use insert() here instead of modify()
// because it's OK to delete shard boundaries in the skipped ranges.
if (!backupRanges.empty()) {
shardMap.insert(KeyRangeRef(allKeys.begin, backupRanges.front().begin), SKIP);
co_await yield();
for (i = 0; i < backupRanges.size() - 1; ++i) {
shardMap.insert(KeyRangeRef(backupRanges[i].end, backupRanges[i + 1].begin), SKIP);
co_await yield();
}
shardMap.insert(KeyRangeRef(backupRanges.back().end, allKeys.end), SKIP);
co_await yield();
}
int countShardsDone = 0;
int countShardsNotDone = 0;
// Scan through the shard map, counting the DONE and NOT_DONE shards.
RangeMap<Key, int, KeyRangeRef>::Ranges shardRanges = shardMap.ranges();
iShard = shardRanges.begin();
iShardEnd = shardRanges.end();
for (; iShard != iShardEnd; ++iShard) {
if (iShard->value() == DONE) {
++countShardsDone;
} else if (iShard->value() >= NOT_DONE_MIN) {
++countShardsNotDone;
}
co_await yield();
}
// Coalesce the shard map to make random selection below more efficient.
shardMap.coalesce(allKeys);
co_await yield();
// In this context "all" refers to all of the shards relevant for this particular backup
int countAllShards = countShardsDone + countShardsNotDone;
// Log backup ranges and shard counts for debugging mode=BOTH issues
TraceEvent("FileBackupSnapshotDispatchShardCount")
.detail("BackupUID", config.getUid())
.detail("BackupRangesCount", backupRanges.size())
.detail("FirstRangeBegin", backupRanges.empty() ? ""_sr : backupRanges.front().begin.printable())
.detail("FirstRangeEnd", backupRanges.empty() ? ""_sr : backupRanges.front().end.printable())
.detail("CountAllShards", countAllShards)
.detail("CountShardsDone", countShardsDone)
.detail("CountShardsNotDone", countShardsNotDone)
.detail("LatestSnapshotEndVersion", latestSnapshotEndVersion.orDefault(-1));
// NOTE: Don't finish here even if countShardsNotDone == 0. We need to dispatch tasks first.
// The completion check after dispatch (with dispatchedInThisIteration guard) prevents
// finishing in the same iteration we dispatch the last tasks.
if (countShardsNotDone == 0) {
TraceEvent("FileBackupSnapshotDispatchAllDoneBeforeDispatch")
.detail("BackupUID", config.getUid())
.detail("Note", "Will check again after dispatch loop");
}
// Decide when the next snapshot dispatch should run.
Version nextDispatchVersion{ 0 };
// In simulation, use snapshot interval / 5 to ensure multiple dispatches run
// Otherwise, use the knob for the number of seconds between snapshot dispatch tasks.
if (g_network->isSimulated()) {
nextDispatchVersion =
recentReadVersion + CLIENT_KNOBS->CORE_VERSIONSPERSECOND * (snapshotIntervalSeconds / 5.0);
} else {
nextDispatchVersion = recentReadVersion + CLIENT_KNOBS->CORE_VERSIONSPERSECOND *
CLIENT_KNOBS->BACKUP_SNAPSHOT_DISPATCH_INTERVAL_SEC;
}
// If nextDispatchVersion is greater than snapshotTargetEndVersion (which could be in the past) then just
// use the greater of recentReadVersion or snapshotTargetEndVersion. Any range tasks created in this
// dispatch will be scheduled at a random time between recentReadVersion and nextDispatchVersion, so
// nextDispatchVersion shouldn't be less than recentReadVersion.
if (nextDispatchVersion > snapshotTargetEndVersion)
nextDispatchVersion = std::max(recentReadVersion, snapshotTargetEndVersion);
Params.nextDispatchVersion().set(task, nextDispatchVersion);
// Calculate number of shards that should be done before the next interval end
// timeElapsed is between 0 and 1 and represents what portion of the shards we should have completed by now
double timeElapsed;
Version snapshotScheduledVersionInterval = snapshotTargetEndVersion - snapshotBeginVersion;
if (snapshotTargetEndVersion > snapshotBeginVersion) {
timeElapsed = std::min(
1.0, (double)(nextDispatchVersion - snapshotBeginVersion) / (snapshotScheduledVersionInterval));
} else {
timeElapsed = 1.0;
}
int countExpectedShardsDone = countAllShards * timeElapsed;
int countShardsToDispatch = std::max<int>(0, countExpectedShardsDone - countShardsDone);
// Calculate the number of shards that would have been dispatched by a normal (on-schedule)
// BackupSnapshotDispatchTask given the dispatch window and the start and expected-end versions of the
// current snapshot.
int64_t dispatchWindow = nextDispatchVersion - recentReadVersion;
// If the scheduled snapshot interval is 0 (such as for initial, as-fast-as-possible snapshot) then all
// shards are considered late
int countShardsExpectedPerNormalWindow;
if (snapshotScheduledVersionInterval == 0) {
countShardsExpectedPerNormalWindow = 0;
} else {
// A dispatchWindow of 0 means the target end version is <= now which also results in all shards being
// considered late
countShardsExpectedPerNormalWindow =
(double(dispatchWindow) / snapshotScheduledVersionInterval) * countAllShards;
}
// The number of shards 'behind' the snapshot is the count of how may additional shards beyond normal are
// being dispatched, if any.
int countShardsBehind =
std::max<int64_t>(0, countShardsToDispatch + snapshotBatchSize.get() - countShardsExpectedPerNormalWindow);
Params.shardsBehind().set(task, countShardsBehind);
TraceEvent("FileBackupSnapshotDispatchStats")
.detail("BackupUID", config.getUid())
.detail("AllShards", countAllShards)
.detail("ShardsDone", countShardsDone)
.detail("ShardsNotDone", countShardsNotDone)
.detail("ExpectedShardsDone", countExpectedShardsDone)
.detail("ShardsToDispatch", countShardsToDispatch)
.detail("ShardsBehind", countShardsBehind)
.detail("SnapshotBeginVersion", snapshotBeginVersion)
.detail("SnapshotTargetEndVersion", snapshotTargetEndVersion)
.detail("NextDispatchVersion", nextDispatchVersion)
.detail("CurrentVersion", recentReadVersion)
.detail("TimeElapsed", timeElapsed)
.detail("SnapshotIntervalSeconds", snapshotIntervalSeconds);
// Track whether we dispatched any tasks in this iteration
bool dispatchedInThisIteration = false;
// Dispatch random shards to catch up to the expected progress
while (countShardsToDispatch > 0) {
// First select ranges to add
std::vector<KeyRange> rangesToAdd;
// Limit number of tasks added per transaction
int taskBatchSize = buggify() ? deterministicRandom()->randomInt(1, countShardsToDispatch + 1)
: CLIENT_KNOBS->BACKUP_DISPATCH_ADDTASK_SIZE;
int added = 0;
while (countShardsToDispatch > 0 && added < taskBatchSize && shardMap.size() > 0) {
// Get a random range.
auto it = shardMap.randomRange();
// Find a NOT_DONE range and add it to rangesToAdd
while (1) {
if (it->value() >= NOT_DONE_MIN) {
rangesToAdd.push_back(it->range());
it->value() = DONE;
shardMap.coalesce(Key(it->begin()));
++added;
++countShardsDone;
--countShardsToDispatch;
--countShardsNotDone;
break;
}
if (it->end() == shardMap.mapEnd)
break;
++it;
}
}
int64_t oldBatchSize = snapshotBatchSize.get();
int64_t newBatchSize = oldBatchSize + rangesToAdd.size();
// Now add the selected ranges in a single transaction.
tr->reset();
while (true) {
Error err;
try {
TraceEvent("FileBackupSnapshotDispatchAddingTasks")
.suppressFor(2)
.detail("TasksToAdd", rangesToAdd.size())
.detail("NewBatchSize", newBatchSize);
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
// For each range, make sure it isn't set in the dispatched range map.
std::vector<Future<Optional<bool>>> beginReads;
std::vector<Future<Optional<bool>>> endReads;
for (auto& range : rangesToAdd) {
beginReads.push_back(config.snapshotRangeDispatchMap().get(tr, range.begin));
endReads.push_back(config.snapshotRangeDispatchMap().get(tr, range.end));
}
co_await (store(snapshotBatchSize.get(), config.snapshotBatchSize().getOrThrow(tr)) &&
waitForAll(beginReads) && waitForAll(endReads) && taskBucket->keepRunning(tr, task));
// Snapshot batch size should be either oldBatchSize or newBatchSize. If new, this transaction
// is already done.
if (snapshotBatchSize.get() == newBatchSize) {
break;
} else {
ASSERT(snapshotBatchSize.get() == oldBatchSize);
config.snapshotBatchSize().set(tr, newBatchSize);
snapshotBatchSize = newBatchSize;
config.snapshotDispatchLastShardsBehind().set(tr, Params.shardsBehind().get(task));
config.snapshotDispatchLastVersion().set(tr, tr->getReadVersion().get());
}
std::vector<Future<Void>> addTaskFutures;
for (i = 0; i < beginReads.size(); ++i) {
KeyRange& range = rangesToAdd[i];
// This loop might have made changes to begin or end boundaries in a prior
// iteration. If so, the updated values exist in the RYW cache so re-read both entries.
Optional<bool> beginValue = config.snapshotRangeDispatchMap().get(tr, range.begin).get();
Optional<bool> endValue = config.snapshotRangeDispatchMap().get(tr, range.end).get();
ASSERT(!beginValue.present() || !endValue.present() || beginValue != endValue);
// If begin is present, it must be a range end so value must be false
// If end is present, it must be a range begin so value must be true
if ((!beginValue.present() || !beginValue.get()) && (!endValue.present() || endValue.get())) {
if (beginValue.present()) {
config.snapshotRangeDispatchMap().erase(tr, range.begin);
} else {
config.snapshotRangeDispatchMap().set(tr, range.begin, true);
}
if (endValue.present()) {
config.snapshotRangeDispatchMap().erase(tr, range.end);
} else {
config.snapshotRangeDispatchMap().set(tr, range.end, false);
}
Version scheduledVersion = invalidVersion;
// If the next dispatch version is in the future, choose a random version at which to
// start the new task.
if (nextDispatchVersion > recentReadVersion)
scheduledVersion = recentReadVersion + deterministicRandom()->random01() *
(nextDispatchVersion - recentReadVersion);
// Range tasks during the initial snapshot should run at a higher priority
int priority = latestSnapshotEndVersion.present() ? 0 : 1;
addTaskFutures.push_back(
success(BackupRangeTaskFunc::addTask(tr,
taskBucket,
task,
priority,
range.begin,
range.end,
TaskCompletionKey::joinWith(snapshotBatchFuture),
Reference<TaskFuture>(),
scheduledVersion)));
TraceEvent("FileBackupSnapshotRangeDispatched")
.suppressFor(2)
.detail("BackupUID", config.getUid())
.detail("CurrentVersion", recentReadVersion)
.detail("ScheduledVersion", scheduledVersion)
.detail("BeginKey", range.begin.printable())
.detail("EndKey", range.end.printable());
} else {
// This shouldn't happen because if the transaction was already done or if another
// execution of this task is making progress it should have been detected above.
ASSERT(false);
}
}
co_await waitForAll(addTaskFutures);
co_await tr->commit();
dispatchedInThisIteration = true;
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
}
// Only finish if all shards are done AND we didn't dispatch any tasks this iteration.
// This prevents the bug where we mark snapshot finished immediately after dispatching
// the last batch of tasks, before they actually complete.
if (countShardsNotDone == 0 && !dispatchedInThisIteration) {
TraceEvent("FileBackupSnapshotDispatchFinished")
.detail("BackupUID", config.getUid())
.detail("AllShards", countAllShards)
.detail("ShardsDone", countShardsDone)
.detail("ShardsNotDone", countShardsNotDone)
.detail("SnapshotBeginVersion", snapshotBeginVersion)
.detail("SnapshotTargetEndVersion", snapshotTargetEndVersion)
.detail("CurrentVersion", recentReadVersion)
.detail("SnapshotIntervalSeconds", snapshotIntervalSeconds)
.detail("DispatchTimeSeconds", timer() - startTime);
Params.snapshotFinished().set(task, true);
}
co_return;
}
// This function is just a wrapper for BackupSnapshotManifest::addTask() which is defined below.
// The BackupSnapshotDispatchTask and BackupSnapshotManifest tasks reference each other so in order to keep
// their execute and finish phases defined together inside their class definitions this wrapper is declared here
// but defined after BackupSnapshotManifest is defined.
static Future<Key> addSnapshotManifestTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>());
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
BackupConfig config(task);
// Get the batch future and dispatch done keys, then clear them.
Key snapshotBatchFutureKey;
Key snapshotBatchDispatchDoneKey;
co_await (store(snapshotBatchFutureKey, config.snapshotBatchFuture().getOrThrow(tr)) &&
store(snapshotBatchDispatchDoneKey, config.snapshotBatchDispatchDoneKey().getOrThrow(tr)));
Reference<TaskFuture> snapshotBatchFuture = futureBucket->unpack(snapshotBatchFutureKey);
Reference<TaskFuture> snapshotBatchDispatchDoneFuture = futureBucket->unpack(snapshotBatchDispatchDoneKey);
config.snapshotBatchFuture().clear(tr);
config.snapshotBatchDispatchDoneKey().clear(tr);
config.snapshotBatchSize().clear(tr);
// Update shardsBehind here again in case the execute phase did not actually have to create any shard tasks
config.snapshotDispatchLastShardsBehind().set(tr, Params.shardsBehind().getOrDefault(task, 0));
config.snapshotDispatchLastVersion().set(tr, tr->getReadVersion().get());
Reference<TaskFuture> snapshotFinishedFuture = task->getDoneFuture(futureBucket);
bool snapshotFinished = Params.snapshotFinished().getOrDefault(task, false);
TraceEvent("FileBackupSnapshotDispatchFinish")
.detail("BackupUID", config.getUid())
.detail("SnapshotFinished", snapshotFinished)
.detail("ShardsBehind", Params.shardsBehind().getOrDefault(task, 0))
.detail("NextDispatchVersion", Params.nextDispatchVersion().getOrDefault(task, -1));
// If the snapshot is finished, the next task is to write a snapshot manifest, otherwise it's another
// snapshot dispatch task. In either case, the task should wait for snapshotBatchFuture. The snapshot done
// key, passed to the current task, is also passed on.
if (snapshotFinished) {
TraceEvent("FileBackupSnapshotDispatchAddingManifestTask").detail("BackupUID", config.getUid());
co_await addSnapshotManifestTask(
tr, taskBucket, task, TaskCompletionKey::signal(snapshotFinishedFuture), snapshotBatchFuture);
} else {
co_await addTask(tr,
taskBucket,
task,
1,
TaskCompletionKey::signal(snapshotFinishedFuture),
snapshotBatchFuture,
Params.nextDispatchVersion().get(task));
}
// This snapshot batch is finished, so set the batch done future.
co_await snapshotBatchDispatchDoneFuture->set(tr, taskBucket);
co_await taskBucket->finish(tr, task);
co_return;
}
};
StringRef BackupSnapshotDispatchTask::name = "file_backup_dispatch_ranges_5.2"_sr;
REGISTER_TASKFUNC(BackupSnapshotDispatchTask);
struct BackupLogRangeTaskFunc : BackupTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
static struct {
static TaskParam<bool> addBackupLogRangeTasks() { return __FUNCTION__sr; }
static TaskParam<int64_t> fileSize() { return __FUNCTION__sr; }
static TaskParam<Version> beginVersion() { return __FUNCTION__sr; }
static TaskParam<Version> endVersion() { return __FUNCTION__sr; }
} Params;
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
Reference<FlowLock> lock(new FlowLock(CLIENT_KNOBS->BACKUP_LOCK_BYTES));
co_await checkTaskVersion(cx, task, BackupLogRangeTaskFunc::name, BackupLogRangeTaskFunc::version);
Version beginVersion = Params.beginVersion().get(task);
Version endVersion = Params.endVersion().get(task);
BackupConfig config(task);
Reference<IBackupContainer> bc;
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
while (true) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
// Wait for the read version to pass endVersion
{
Error err;
try {
co_await taskBucket->keepRunning(tr, task);
if (!bc) {
// Backup container must be present if we're still here
Reference<IBackupContainer> _bc = co_await config.backupContainer().getOrThrow(tr);
bc = getBackupContainerWithProxy(_bc);
}
Version currentVersion = tr->getReadVersion().get();
if (endVersion < currentVersion)
break;
co_await delay(
std::max(CLIENT_KNOBS->BACKUP_RANGE_MINWAIT,
(double)(endVersion - currentVersion) / CLIENT_KNOBS->CORE_VERSIONSPERSECOND));
tr->reset();
continue;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
}
Key destUidValue = co_await config.destUidValue().getOrThrow(tr);
// Get the set of key ranges that hold mutations for (beginVersion, endVersion). They will be queried in
// parallel below and there is a limit on how many we want to process in a single BackupLogRangeTask so if
// that limit is exceeded then set the addBackupLogRangeTasks boolean in Params and stop, signalling the
// finish() step to break up the (beginVersion, endVersion) range into smaller intervals which are then
// processed by individual BackupLogRangeTasks.
Standalone<VectorRef<KeyRangeRef>> ranges = getLogRanges(beginVersion, endVersion, destUidValue);
if (ranges.size() > CLIENT_KNOBS->BACKUP_MAX_LOG_RANGES) {
Params.addBackupLogRangeTasks().set(task, true);
co_return;
}
// Block size must be at least large enough for 1 max size key, 1 max size value, and overhead, so
// conservatively 125k.
int blockSize =
buggify() ? deterministicRandom()->randomInt(125e3, 4e6) : CLIENT_KNOBS->BACKUP_LOGFILE_BLOCK_SIZE;
Reference<IBackupFile> outFile = co_await bc->writeLogFile(beginVersion, endVersion, blockSize);
LogFileWriter logFile(outFile, blockSize);
// Query all key ranges covering (beginVersion, endVersion) in parallel, writing their results to the
// results promise stream as they are received. Note that this means the records read from the results
// stream are not likely to be in increasing Version order.
PromiseStream<RangeResultWithVersion> results;
std::vector<Future<Void>> rc;
for (auto& range : ranges) {
rc.push_back(readCommitted(cx,
results,
lock,
range,
Terminator::False,
AccessSystemKeys::True,
LockAware::True,
ReadLowPriority(CLIENT_KNOBS->BACKUP_READS_USE_LOW_PRIORITY)));
}
Future<Void> sendEOS = map(errorOr(waitForAll(rc)), [=](ErrorOr<Void> const& result) mutable {
if (result.isError())
results.sendError(result.getError());
else
results.sendError(end_of_stream());
return Void();
});
Version lastVersion{ 0 };
{
Error caughtErr;
bool hasCaughtErr = false;
try {
while (true) {
RangeResultWithVersion r = co_await results.getFuture();
lock->release(r.first.expectedSize());
for (int i = 0; i < r.first.size(); ++i) {
// Remove the backupLogPrefix + UID bytes from the key
co_await logFile.writeKV(r.first[i].key.substr(backupLogPrefixBytes + 16), r.first[i].value);
lastVersion = r.second;
}
}
} catch (Error& e) {
caughtErr = e;
hasCaughtErr = true;
}
if (hasCaughtErr) {
if (caughtErr.code() == error_code_actor_cancelled)
throw caughtErr;
if (caughtErr.code() != error_code_end_of_stream) {
Error err = caughtErr;
co_await config.logError(
cx, err, format("Failed to write to file `%s'", outFile->getFileName().c_str()));
throw err;
}
}
}
// Make sure this task is still alive, if it's not then the data read above could be incomplete.
co_await taskBucket->keepRunning(cx, task);
co_await outFile->finish();
TraceEvent("FileBackupWroteLogFile")
.suppressFor(60)
.detail("BackupUID", config.getUid())
.detail("Size", outFile->size())
.detail("BeginVersion", beginVersion)
.detail("EndVersion", endVersion)
.detail("LastReadVersion", lastVersion);
Params.fileSize().set(task, outFile->size());
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
int priority,
Version beginVersion,
Version endVersion,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key key = co_await addBackupTask(
BackupLogRangeTaskFunc::name,
BackupLogRangeTaskFunc::version,
tr,
taskBucket,
completionKey,
BackupConfig(parentTask),
waitFor,
[=](Reference<Task> task) {
Params.beginVersion().set(task, beginVersion);
Params.endVersion().set(task, endVersion);
Params.addBackupLogRangeTasks().set(task, false);
},
priority);
co_return key;
}
static Future<Void> startBackupLogRangeInternal(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task,
Reference<TaskFuture> taskFuture,
Version beginVersion,
Version endVersion) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
std::vector<Future<Key>> addTaskVector;
int tasks = 0;
for (int64_t vblock = beginVersion / CLIENT_KNOBS->LOG_RANGE_BLOCK_SIZE;
vblock < (endVersion + CLIENT_KNOBS->LOG_RANGE_BLOCK_SIZE - 1) / CLIENT_KNOBS->LOG_RANGE_BLOCK_SIZE;
vblock += CLIENT_KNOBS->BACKUP_MAX_LOG_RANGES) {
Version bv = std::max(beginVersion, vblock * CLIENT_KNOBS->LOG_RANGE_BLOCK_SIZE);
if (tasks >= CLIENT_KNOBS->BACKUP_SHARD_TASK_LIMIT) {
addTaskVector.push_back(addTask(tr,
taskBucket,
task,
task->getPriority(),
bv,
endVersion,
TaskCompletionKey::joinWith(taskFuture)));
break;
}
Version ev = std::min(endVersion,
(vblock + CLIENT_KNOBS->BACKUP_MAX_LOG_RANGES) * CLIENT_KNOBS->LOG_RANGE_BLOCK_SIZE);
addTaskVector.push_back(
addTask(tr, taskBucket, task, task->getPriority(), bv, ev, TaskCompletionKey::joinWith(taskFuture)));
tasks++;
}
co_await waitForAll(addTaskVector);
co_return;
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
Version beginVersion = Params.beginVersion().get(task);
Version endVersion = Params.endVersion().get(task);
Reference<TaskFuture> taskFuture = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
BackupConfig config(task);
if (Params.fileSize().exists(task)) {
config.logBytesWritten().atomicOp(tr, Params.fileSize().get(task), MutationRef::AddValue);
}
if (Params.addBackupLogRangeTasks().get(task)) {
co_await startBackupLogRangeInternal(
tr, taskBucket, futureBucket, task, taskFuture, beginVersion, endVersion);
} else {
co_await taskFuture->set(tr, taskBucket);
}
co_await taskBucket->finish(tr, task);
co_return;
}
};
StringRef BackupLogRangeTaskFunc::name = "file_backup_write_logs_5.2"_sr;
REGISTER_TASKFUNC(BackupLogRangeTaskFunc);
// This task stopped being used in 6.2, however the code remains here to handle upgrades.
struct EraseLogRangeTaskFunc : BackupTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
StringRef getName() const override { return name; };
static struct {
static TaskParam<Version> beginVersion() { return __FUNCTION__sr; }
static TaskParam<Version> endVersion() { return __FUNCTION__sr; }
static TaskParam<Key> destUidValue() { return __FUNCTION__sr; }
} Params;
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
UID logUid,
TaskCompletionKey completionKey,
Key destUidValue,
Version endVersion = 0,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key key = co_await addBackupTask(
EraseLogRangeTaskFunc::name,
EraseLogRangeTaskFunc::version,
tr,
taskBucket,
completionKey,
BackupConfig(logUid),
waitFor,
[=](Reference<Task> task) {
Params.beginVersion().set(task,
1); // FIXME: remove in 6.X, only needed for 5.2 backward compatibility
Params.endVersion().set(task, endVersion);
Params.destUidValue().set(task, destUidValue);
},
0,
SetValidation::False);
co_return key;
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
Reference<TaskFuture> taskFuture = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
co_await checkTaskVersion(tr->getDatabase(), task, EraseLogRangeTaskFunc::name, EraseLogRangeTaskFunc::version);
Version endVersion = Params.endVersion().get(task);
Key destUidValue = Params.destUidValue().get(task);
BackupConfig config(task);
Key logUidValue = config.getUidAsKey();
co_await (
taskFuture->set(tr, taskBucket) && taskBucket->finish(tr, task) &&
eraseLogData(
tr, logUidValue, destUidValue, endVersion != 0 ? Optional<Version>(endVersion) : Optional<Version>()));
co_return;
}
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return Void();
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef EraseLogRangeTaskFunc::name = "file_backup_erase_logs_5.2"_sr;
REGISTER_TASKFUNC(EraseLogRangeTaskFunc);
struct BackupLogsDispatchTask : BackupTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
static struct {
static TaskParam<Version> prevBeginVersion() { return __FUNCTION__sr; }
static TaskParam<Version> beginVersion() { return __FUNCTION__sr; }
} Params;
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
co_await checkTaskVersion(
tr->getDatabase(), task, BackupLogsDispatchTask::name, BackupLogsDispatchTask::version);
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Reference<TaskFuture> onDone = task->getDoneFuture(futureBucket);
Version prevBeginVersion = Params.prevBeginVersion().get(task);
Version beginVersion = Params.beginVersion().get(task);
BackupConfig config(task);
config.latestLogEndVersion().set(tr, beginVersion);
bool stopWhenDone{ false };
Optional<Version> restorableVersion;
EBackupState backupState;
Optional<std::string> tag;
Optional<Version> latestSnapshotEndVersion;
Optional<MutationLogType> mutationLogType;
co_await (store(stopWhenDone, config.stopWhenDone().getOrThrow(tr)) &&
store(restorableVersion, config.getLatestRestorableVersion(tr)) &&
store(backupState, config.stateEnum().getOrThrow(tr)) && store(tag, config.tag().get(tr)) &&
store(latestSnapshotEndVersion, config.latestSnapshotEndVersion().get(tr)) &&
store(mutationLogType, config.mutationLogType().get(tr)));
// If restorable, update the last restorable version for this tag
if (restorableVersion.present() && tag.present()) {
FileBackupAgent().setLastRestorable(tr, StringRef(tag.get()), restorableVersion.get());
}
// If the backup is restorable but the state is not differential then set state to differential
if (restorableVersion.present() && backupState != EBackupState::STATE_RUNNING_DIFFERENTIAL)
config.stateEnum().set(tr, EBackupState::STATE_RUNNING_DIFFERENTIAL);
// If stopWhenDone is set and there is a restorable version, set the done future and do not create further
// tasks.
if (stopWhenDone && restorableVersion.present()) {
co_await (onDone->set(tr, taskBucket) && taskBucket->finish(tr, task));
TraceEvent("FileBackupLogsDispatchDone")
.detail("BackupUID", config.getUid())
.detail("BeginVersion", beginVersion)
.detail("RestorableVersion", restorableVersion.orDefault(-1));
co_return;
}
Version endVersion =
std::max<Version>(tr->getReadVersion().get() + 1,
beginVersion + static_cast<Version>(CLIENT_KNOBS->BACKUP_MAX_LOG_RANGES - 1) *
CLIENT_KNOBS->LOG_RANGE_BLOCK_SIZE);
TraceEvent("FileBackupLogDispatch")
.suppressFor(60)
.detail("BeginVersion", beginVersion)
.detail("EndVersion", endVersion)
.detail("RestorableVersion", restorableVersion.orDefault(-1));
Reference<TaskFuture> logDispatchBatchFuture = futureBucket->future(tr);
// If a snapshot has ended for this backup then mutations are higher priority to reduce backup lag
int priority = latestSnapshotEndVersion.present() ? 1 : 0;
if (!mutationLogType.present() || mutationLogType.get() == MutationLogType::DEFAULT) {
// Add the initial log range task to read/copy the mutations and the next logs dispatch task which will
// run after this batch is done
// read blog/ prefix and write those (param1, param2) into files
co_await BackupLogRangeTaskFunc::addTask(tr,
taskBucket,
task,
priority,
beginVersion,
endVersion,
TaskCompletionKey::joinWith(logDispatchBatchFuture));
// issue the next key range
co_await BackupLogsDispatchTask::addTask(tr,
taskBucket,
task,
priority,
beginVersion,
endVersion,
TaskCompletionKey::signal(onDone),
logDispatchBatchFuture);
// Do not erase at the first time
if (prevBeginVersion > 0) {
Key destUidValue = co_await config.destUidValue().getOrThrow(tr);
co_await eraseLogData(tr, config.getUidAsKey(), destUidValue, Optional<Version>(beginVersion));
}
} else {
// Skip mutation copy and erase backup mutations. Just check back periodically.
Version scheduledVersion = tr->getReadVersion().get() +
CLIENT_KNOBS->BACKUP_POLL_PROGRESS_SECONDS * CLIENT_KNOBS->VERSIONS_PER_SECOND;
co_await BackupLogsDispatchTask::addTask(tr,
taskBucket,
task,
1,
beginVersion,
endVersion,
TaskCompletionKey::signal(onDone),
Reference<TaskFuture>(),
scheduledVersion);
}
co_await taskBucket->finish(tr, task);
TraceEvent("FileBackupLogsDispatchContinuing")
.suppressFor(60)
.detail("BackupUID", config.getUid())
.detail("BeginVersion", beginVersion)
.detail("EndVersion", endVersion);
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
int priority,
Version prevBeginVersion,
Version beginVersion,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>(),
Version scheduledVersion = invalidVersion) {
Key key = co_await addBackupTask(
BackupLogsDispatchTask::name,
BackupLogsDispatchTask::version,
tr,
taskBucket,
completionKey,
BackupConfig(parentTask),
waitFor,
[=](Reference<Task> task) {
Params.prevBeginVersion().set(task, prevBeginVersion);
Params.beginVersion().set(task, beginVersion);
if (scheduledVersion != invalidVersion) {
ReservedTaskParams::scheduledVersion().set(task, scheduledVersion);
}
},
priority);
co_return key;
}
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return Void();
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef BackupLogsDispatchTask::name = "file_backup_dispatch_logs_5.2"_sr;
REGISTER_TASKFUNC(BackupLogsDispatchTask);
struct FileBackupFinishedTask : BackupTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
StringRef getName() const override { return name; };
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
co_await checkTaskVersion(
tr->getDatabase(), task, FileBackupFinishedTask::name, FileBackupFinishedTask::version);
BackupConfig backup(task);
UID uid = backup.getUid();
tr->setOption(FDBTransactionOptions::COMMIT_ON_FIRST_PROXY);
Key destUidValue = co_await backup.destUidValue().getOrThrow(tr);
co_await (eraseLogData(tr, backup.getUidAsKey(), destUidValue) && clearBackupStartID(tr, uid));
backup.stateEnum().set(tr, EBackupState::STATE_COMPLETED);
co_await taskBucket->finish(tr, task);
TraceEvent("FileBackupFinished").detail("BackupUID", uid);
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key key = co_await addBackupTask(FileBackupFinishedTask::name,
FileBackupFinishedTask::version,
tr,
taskBucket,
completionKey,
BackupConfig(parentTask),
waitFor);
co_return key;
}
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return Void();
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef FileBackupFinishedTask::name = "file_backup_finished_5.2"_sr;
REGISTER_TASKFUNC(FileBackupFinishedTask);
struct BackupSnapshotManifest : BackupTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
static struct {
static TaskParam<Version> endVersion() { return __FUNCTION__sr; }
static TaskParam<int64_t> totalBytes() { return __FUNCTION__sr; }
} Params;
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
BackupConfig config(task);
Reference<IBackupContainer> bc;
DatabaseConfiguration dbConfig;
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
// Read the entire range file map into memory, then walk it backwards from its last entry to produce a list
// of non overlapping key range files
std::map<Key, BackupConfig::RangeSlice> localmap;
Key startKey;
int batchSize = buggify() ? 1 : 1000000;
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
co_await taskBucket->keepRunning(tr, task);
dbConfig = co_await getDatabaseConfiguration(cx);
if (!bc) {
// Backup container must be present if we're still here
Reference<IBackupContainer> _bc = co_await config.backupContainer().getOrThrow(tr);
bc = getBackupContainerWithProxy(_bc);
}
BackupConfig::RangeFileMapT::RangeResultType rangeresults =
co_await config.snapshotRangeFileMap().getRange(tr, startKey, {}, batchSize);
for (auto& p : rangeresults.results) {
localmap.insert(p);
}
if (!rangeresults.more) {
break;
}
startKey = keyAfter(rangeresults.results.back().first);
tr->reset();
continue;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
std::vector<std::string> files;
std::vector<std::pair<Key, Key>> beginEndKeys;
Version maxVer = 0;
Version minVer = std::numeric_limits<Version>::max();
int64_t totalBytes = 0;
if (!localmap.empty()) {
// Get iterator that points to greatest key, start there.
auto ri = localmap.rbegin();
auto i = (++ri).base();
while (1) {
const BackupConfig::RangeSlice& r = i->second;
// Add file to final file list
files.push_back(r.fileName);
// Add (beginKey, endKey) pairs to the list
beginEndKeys.emplace_back(i->second.begin, i->first);
// Update version range seen
if (r.version < minVer)
minVer = r.version;
if (r.version > maxVer)
maxVer = r.version;
// Update total bytes counted.
totalBytes += r.fileSize;
// Jump to file that either ends where this file begins or has the greatest end that is less than
// the begin of this file. In other words find the map key that is <= begin of this file. To do
// this find the first end strictly greater than begin and then back up one.
i = localmap.upper_bound(i->second.begin);
// If we get begin then we're done, there are no more ranges that end at or before the last file's
// begin
if (i == localmap.begin())
break;
--i;
}
}
// Log what range files were found for debugging mode=BOTH issues
TraceEvent("BackupSnapshotManifestRangeFileSummary")
.detail("BackupUID", config.getUid())
.detail("LocalMapSize", localmap.size())
.detail("FilesFound", files.size())
.detail("TotalBytes", totalBytes)
.detail("MinVersion", minVer == std::numeric_limits<Version>::max() ? -1 : minVer)
.detail("MaxVersion", maxVer);
Params.endVersion().set(task, maxVer);
Params.totalBytes().set(task, totalBytes);
// Avoid keyRange filtering optimization for 'manifest' files
co_await bc->writeKeyspaceSnapshotFile(files, beginEndKeys, totalBytes, IncludeKeyRangeMap::True);
TraceEvent(SevInfo, "FileBackupWroteSnapshotManifest")
.detail("BackupUID", config.getUid())
.detail("BeginVersion", minVer)
.detail("EndVersion", maxVer)
.detail("TotalBytes", totalBytes);
co_return;
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
co_await checkTaskVersion(
tr->getDatabase(), task, BackupSnapshotManifest::name, BackupSnapshotManifest::version);
BackupConfig config(task);
// Set the latest snapshot end version, which was set during the execute phase
config.latestSnapshotEndVersion().set(tr, Params.endVersion().get(task));
bool stopWhenDone{ false };
EBackupState backupState;
Optional<Version> restorableVersion;
Optional<Version> firstSnapshotEndVersion;
Optional<std::string> tag;
co_await (store(stopWhenDone, config.stopWhenDone().getOrThrow(tr)) &&
store(backupState, config.stateEnum().getOrThrow(tr)) &&
store(restorableVersion, config.getLatestRestorableVersion(tr)) &&
store(firstSnapshotEndVersion, config.firstSnapshotEndVersion().get(tr)) &&
store(tag, config.tag().get(tr)));
// If restorable, update the last restorable version for this tag
if (restorableVersion.present() && tag.present()) {
FileBackupAgent().setLastRestorable(tr, StringRef(tag.get()), restorableVersion.get());
}
// Always set firstSnapshotEndVersion if not already set
// This is required for getLatestRestorableVersion() to work correctly
if (!firstSnapshotEndVersion.present()) {
config.firstSnapshotEndVersion().set(tr, Params.endVersion().get(task));
}
// If the backup is restorable and the state isn't differential the set state to differential
if (restorableVersion.present() && backupState != EBackupState::STATE_RUNNING_DIFFERENTIAL)
config.stateEnum().set(tr, EBackupState::STATE_RUNNING_DIFFERENTIAL);
// Unless we are to stop, start the next snapshot using the default interval
Reference<TaskFuture> snapshotDoneFuture = task->getDoneFuture(futureBucket);
if (!stopWhenDone) {
co_await (config.initNewSnapshot(tr) &&
success(BackupSnapshotDispatchTask::addTask(
tr, taskBucket, task, 1, TaskCompletionKey::signal(snapshotDoneFuture))));
} else {
// Set the done future as the snapshot is now complete.
co_await snapshotDoneFuture->set(tr, taskBucket);
}
co_await taskBucket->finish(tr, task);
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key key = co_await addBackupTask(BackupSnapshotManifest::name,
BackupSnapshotManifest::version,
tr,
taskBucket,
completionKey,
BackupConfig(parentTask),
waitFor,
NOP_SETUP_TASK_FN,
1);
co_return key;
}
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef BackupSnapshotManifest::name = "file_backup_write_snapshot_manifest_5.2"_sr;
REGISTER_TASKFUNC(BackupSnapshotManifest);
Future<Key> BackupSnapshotDispatchTask::addSnapshotManifestTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor) {
return BackupSnapshotManifest::addTask(tr, taskBucket, parentTask, completionKey, waitFor);
}
// BulkDumpTaskFunc: Creates BulkDump snapshots during backup
// Must be defined before StartFullBackupTaskFunc which references it
struct BulkDumpTaskFunc : BackupTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
static struct {
static TaskParam<Version> snapshotVersion() { return __FUNCTION__sr; }
static TaskParam<std::string> bulkDumpJobId() { return __FUNCTION__sr; }
static TaskParam<bool> timeoutOccurred() { return __FUNCTION__sr; }
} Params;
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
BackupConfig config(task);
Version snapshotVersion = Params.snapshotVersion().get(task);
std::string jobId = Params.bulkDumpJobId().getOrDefault(task, "");
TraceEvent("BulkDumpTaskStart")
.detail("BackupUID", config.getUid())
.detail("SnapshotVersion", snapshotVersion)
.detail("BulkDumpJobId", jobId);
// Declare state variables before try block so they're accessible in catch block
std::vector<KeyRange> backupRanges;
Reference<IBackupContainer> bc;
int originalBulkDumpMode = 0;
BulkLoadTransportMethod transportMethod = BulkLoadTransportMethod::CP;
BulkDumpState bulkDumpJob;
bool jobAlreadyRunning = false;
{
Error savedError;
try {
// Submit BulkDump job via ManagementAPI
// This is a black box delegation to the existing BulkDump system
// Get backup ranges from config
std::vector<KeyRange> ranges = co_await config.backupRanges().getOrThrow(cx.getReference());
backupRanges = ranges;
Reference<IBackupContainer> container = co_await config.backupContainer().getOrThrow(cx.getReference());
bc = container;
// Read the original BulkDump mode from config (saved by StartFullBackupTaskFunc before task creation).
// This is persisted in the database so we can restore the correct mode even after a crash.
int mode = co_await config.originalBulkDumpMode().getD(cx.getReference(), Snapshot::False, 0);
originalBulkDumpMode = mode;
// Determine transport method from backup URL
transportMethod =
isBlobstoreUrl(bc->getURL()) ? BulkLoadTransportMethod::BLOBSTORE : BulkLoadTransportMethod::CP;
// Check if there's already a running BulkDump job (e.g., from a previous task attempt or another
// agent). If so, we'll monitor that job instead of submitting a new one. This prevents the "Conflict to
// a running BulkDump job" error that occurs when multiple backup agents try to run the same task.
Transaction checkTr(cx);
while (true) {
Error err;
try {
checkTr.setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
checkTr.setOption(FDBTransactionOptions::LOCK_AWARE);
Optional<BulkDumpState> existingJob = co_await getSubmittedBulkDumpJob(&checkTr);
if (existingJob.present()) {
bulkDumpJob = existingJob.get();
jobAlreadyRunning = true;
TraceEvent("BulkDumpTaskUsingExistingJob")
.detail("BackupUID", config.getUid())
.detail("ExistingJobId", bulkDumpJob.getJobId());
}
break;
} catch (Error& e) {
err = e;
}
co_await checkTr.onError(err);
}
if (!jobAlreadyRunning) {
// Enable BulkDump mode at the DD level before submitting the job
co_await setBulkDumpMode(cx, 1);
// Configure BulkDump job for the full keyspace
// BulkDump/BulkLoad requires the load range to be a subset of the dump range.
// Using normalKeys for both ensures compatibility regardless of user-specified ranges.
// Store data under data/<container>/bulkdump_data/ to be consistent with backup container layout
std::string bulkDumpRoot = getBackupDataPath(bc->getURL(), "bulkdump_data");
bulkDumpJob = createBulkDumpJob(normalKeys, bulkDumpRoot, BulkLoadType::SST, transportMethod);
// Submit the BulkDump job
co_await submitBulkDumpJob(cx, bulkDumpJob);
// Set ownership so bulkdump status shows it belongs to this backup
std::string backupTag = co_await config.tag().getOrThrow(cx.getReference());
BulkDumpOwnerInfo ownerInfo(config.getUid(), "backup", backupTag, now());
co_await setBulkDumpOwner(cx, bulkDumpJob.getJobId(), ownerInfo);
}
// Store job ID for monitoring
Params.bulkDumpJobId().set(task, bulkDumpJob.getJobId().toString());
// Monitor BulkDump progress - timeout is configurable for large datasets
bool completed = co_await monitorBulkDumpJobCompletion(cx,
bulkDumpJob.getJobId(),
CLIENT_KNOBS->BULKDUMP_JOB_TIMEOUT,
5.0); // Poll every 5 seconds
if (!completed) {
TraceEvent(SevWarn, "BulkDumpTaskTimeout")
.detail("BackupUID", config.getUid())
.detail("BulkDumpJobId", bulkDumpJob.getJobId())
.detail("TimeoutDuration", 300.0);
Params.timeoutOccurred().set(task, true);
}
TraceEvent("BulkDumpTaskComplete")
.detail("BackupUID", config.getUid())
.detail("BulkDumpJobId", bulkDumpJob.getJobId())
.detail("TimeoutOccurred", Params.timeoutOccurred().getOrDefault(task, false))
.detail("JobAlreadyRunning", jobAlreadyRunning);
// Restore original BulkDump mode after job completes, but only if:
// 1. We actually enabled the mode (not if we just monitored an existing job)
// 2. The original mode was different from what we set
if (!jobAlreadyRunning && originalBulkDumpMode != 1) {
co_await setBulkDumpMode(cx, originalBulkDumpMode);
}
// Write the keyspace snapshot file to mark the backup as complete
// This is essential for the restore process to find the snapshot
if (completed) {
// Verify that BulkDump data was actually written before writing snapshot
bool datasetComplete =
co_await verifyBulkDumpDatasetCompleteness(bc, bulkDumpJob.getJobId().toString());
if (!datasetComplete) {
TraceEvent(SevError, "BulkDumpTaskDatasetIncomplete")
.detail("BackupUID", config.getUid())
.detail("BulkDumpJobId", bulkDumpJob.getJobId());
throw backup_error();
}
// Build beginEndKeys from backup ranges
std::vector<std::pair<Key, Key>> beginEndKeys;
for (const auto& range : backupRanges) {
beginEndKeys.emplace_back(range.begin, range.end);
}
// Create BulkDump snapshot metadata
// Note: totalBytes and totalKeys are set to 0 here. Unlike traditional range file backups
// which accumulate fileSize from each RangeSlice, BulkDump writes its data to manifest files
// in the backup container (one per shard). To get accurate byte/key counts, we would need to:
// 1. List all manifest files under bc->getURL()/bulkDumpJobId/
// 2. Read each BulkLoadManifest and call getTotalBytes()/getKeyCount()
// 3. Aggregate the totals
// For now, zeros are acceptable since the critical field is bulkDumpJobId which BulkLoad
// uses to locate the data. TODO: Fix. The byte counts are informational for status display.
SnapshotMetadata metadata =
SnapshotMetadata::bulkDump(bulkDumpJob.getJobId().toString(), snapshotVersion, 0, 0);
// Write the snapshot file - empty file list since BulkDump uses job manifests, not range files
co_await bc->writeKeyspaceSnapshotFile({}, // No individual range files for BulkDump
beginEndKeys,
0, // See comment above about totalBytes
IncludeKeyRangeMap::False,
metadata);
TraceEvent("BulkDumpTaskWroteSnapshot")
.detail("BackupUID", config.getUid())
.detail("BulkDumpJobId", bulkDumpJob.getJobId())
.detail("SnapshotVersion", snapshotVersion)
.detail("RangeCount", backupRanges.size());
}
// Increment counter for test assertions
g_bulkDumpTaskCompleteCount.fetch_add(1);
co_return;
} catch (Error& e) {
savedError = e;
}
TraceEvent(SevWarn, "BulkDumpTaskError")
.error(savedError)
.detail("BackupUID", config.getUid())
.detail("SnapshotVersion", snapshotVersion)
.detail("JobAlreadyRunning", jobAlreadyRunning);
// Restore original BulkDump mode on error, but only if we were the ones who enabled it
try {
if (!jobAlreadyRunning && originalBulkDumpMode != 1) {
co_await setBulkDumpMode(cx, originalBulkDumpMode);
}
} catch (Error& e2) {
if (e2.code() == error_code_actor_cancelled) {
throw;
}
TraceEvent(SevWarn, "BulkDumpTaskRestoreModeError").error(e2);
}
throw savedError;
}
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
BackupConfig config(task);
Version snapshotVersion = Params.snapshotVersion().get(task);
std::string jobId = Params.bulkDumpJobId().getOrDefault(task, "");
TraceEvent("BulkDumpTaskFinishStart")
.detail("BackupUID", config.getUid())
.detail("SnapshotVersion", snapshotVersion)
.detail("BulkDumpJobId", jobId);
// Set latestSnapshotEndVersion so BackupLogsDispatchTask knows we're restorable
// This is critical for the backup to complete when using BulkDump
config.latestSnapshotEndVersion().set(tr, snapshotVersion);
// Set bulkDumpSnapshotEndVersion to track that BulkDump data is available
// This is used by getLatestRestorableVersion() for mode=BOTH to ensure both
// rangefile and bulkdump data exist before marking backup as restorable
config.bulkDumpSnapshotEndVersion().set(tr, snapshotVersion);
// CRITICAL: Set bulkDumpJobId on the backup CONFIG so that status checks
// and restore operations can find it. Without this, the backup status shows
// "BulkLoad Compatible: no" and bulkload restore fails with "Missing backup data".
if (!jobId.empty()) {
config.bulkDumpJobId().set(tr, jobId);
}
// Set firstSnapshotEndVersion if not already set, BUT only if we're NOT in
// mode=BOTH. In mode=BOTH, firstSnapshotEndVersion tracks the rangefile snapshot
// completion and must only be set by BackupSnapshotManifest::_finish().
// Setting it here would make getLatestRestorableVersion() think both snapshots
// are complete when only bulkdump is done.
Optional<int> mode = co_await config.snapshotMode().get(tr);
if (!mode.present() || mode.get() != 2) {
Optional<Version> firstSnapshotEnd = co_await config.firstSnapshotEndVersion().get(tr);
if (!firstSnapshotEnd.present()) {
config.firstSnapshotEndVersion().set(tr, snapshotVersion);
}
}
TraceEvent("BulkDumpTaskFinishSetVersions")
.detail("BackupUID", config.getUid())
.detail("SnapshotVersion", snapshotVersion)
.detail("Mode", mode.present() ? mode.get() : 0);
Reference<TaskFuture> taskFuture = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
co_await taskFuture->set(tr, taskBucket);
co_await taskBucket->finish(tr, task);
TraceEvent("BulkDumpTaskFinish")
.detail("BackupUID", config.getUid())
.detail("SnapshotVersion", snapshotVersion);
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
Version snapshotVersion,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key key = co_await addBackupTask(BulkDumpTaskFunc::name,
BulkDumpTaskFunc::version,
tr,
taskBucket,
completionKey,
BackupConfig(parentTask),
waitFor,
[=](Reference<Task> task) {
Params.snapshotVersion().set(task, snapshotVersion);
Params.timeoutOccurred().set(task, false);
});
co_return key;
}
};
StringRef BulkDumpTaskFunc::name = "bulk_dump_snapshot_5.2"_sr;
REGISTER_TASKFUNC(BulkDumpTaskFunc);
struct StartFullBackupTaskFunc : BackupTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
static struct {
static TaskParam<Version> beginVersion() { return __FUNCTION__sr; }
} Params;
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
co_await checkTaskVersion(cx, task, StartFullBackupTaskFunc::name, StartFullBackupTaskFunc::version);
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
BackupConfig config(task);
Future<Optional<MutationLogType>> mutationLogType;
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
mutationLogType = config.mutationLogType().get(tr);
co_await mutationLogType;
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
// Enable the appropriate backup worker type if not already enabled.
if (mutationLogType.get().present()) {
if (mutationLogType.get().get() == MutationLogType::PARTITIONED_LOG) {
co_await enableBackupWorker(cx);
} else if (mutationLogType.get().get() == MutationLogType::RANGE_PARTITIONED_LOG) {
co_await enableRangePartitionedBackupWorker(cx);
}
}
// Get start version after backup worker are enabled
while (true) {
Error err;
try {
tr->reset();
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Future<Version> startVersionFuture = tr->getReadVersion();
co_await startVersionFuture;
Params.beginVersion().set(task, startVersionFuture.get());
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
// Set the "backupStartedKey" and wait for all backup worker started
tr->reset();
while (true) {
Future<Void> watchFuture;
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Future<Void> keepRunning = taskBucket->keepRunning(tr, task);
Future<Optional<Value>> started = tr->get(backupStartedKey);
Future<Optional<Value>> taskStarted = tr->get(config.allWorkerStarted().key);
mutationLogType = config.mutationLogType().get(tr);
co_await (success(started) && success(taskStarted) && success(mutationLogType));
if (!mutationLogType.get().present() || mutationLogType.get().get() == MutationLogType::DEFAULT) {
co_return; // Skip if not using partitioned or range partitioned logs
}
std::vector<std::pair<UID, Version>> ids;
if (started.get().present()) {
ids = decodeBackupStartedValue(started.get().get());
}
// First range-partitioned backup on this cluster: ask DD to compute the partition list.
if (ids.empty() && mutationLogType.get().get() == MutationLogType::RANGE_PARTITIONED_LOG) {
tr->set(backupPartitionRequiredKey, backupPartitionRequiredValue(1));
}
const UID uid = config.getUid();
auto it = std::find_if(
ids.begin(), ids.end(), [uid](const std::pair<UID, Version>& p) { return p.first == uid; });
if (it == ids.end()) {
ids.emplace_back(uid, Params.beginVersion().get(task));
} else {
Params.beginVersion().set(task, it->second);
}
tr->set(backupStartedKey, encodeBackupStartedValue(ids));
// Only PartitionedLog workers set BackupConfig.allWorkerStarted, so watch it for that mutation log
// type.
const bool isPartitionedLog = mutationLogType.get().get() == MutationLogType::PARTITIONED_LOG;
// The task may be restarted. Set the watch if started key has NOT been set.
if (isPartitionedLog && !taskStarted.get().present()) {
watchFuture = tr->watch(config.allWorkerStarted().key);
}
co_await keepRunning;
co_await tr->commit();
if (isPartitionedLog && !taskStarted.get().present()) {
co_await watchFuture;
}
co_return;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
BackupConfig config(task);
Version beginVersion = Params.beginVersion().get(task);
Future<std::vector<KeyRange>> backupRangesFuture = config.backupRanges().getOrThrow(tr);
Future<Key> destUidValueFuture = config.destUidValue().getOrThrow(tr);
Future<Optional<MutationLogType>> mutationLogType = config.mutationLogType().get(tr);
Future<Optional<bool>> incrementalBackupOnly = config.incrementalBackupOnly().get(tr);
co_await (success(backupRangesFuture) && success(destUidValueFuture) && success(mutationLogType) &&
success(incrementalBackupOnly));
std::vector<KeyRange> backupRanges = backupRangesFuture.get();
Key destUidValue = destUidValueFuture.get();
// Start logging the mutations for the specified ranges of the tag if needed
if (!mutationLogType.get().present() || mutationLogType.get().get() == MutationLogType::DEFAULT) {
for (auto& backupRange : backupRanges) {
config.startMutationLogs(tr, backupRange, destUidValue);
}
}
// Properties folder is created here to ensure backup container exists because localDirectory doesn't create
// container during in constructor.
Reference<IBackupContainer> bc = co_await config.backupContainer().getOrThrow(tr);
co_await bc->writeEncryptionMetadata(bc->getEncryptionBlockSize());
config.stateEnum().set(tr, EBackupState::STATE_RUNNING);
Reference<TaskFuture> backupFinished = futureBucket->future(tr);
// Initialize the initial snapshot and create tasks to continually write logs and snapshots.
Optional<int64_t> initialSnapshotIntervalSeconds = co_await config.initialSnapshotIntervalSeconds().get(tr);
co_await config.initNewSnapshot(tr, initialSnapshotIntervalSeconds.orDefault(0));
// Using priority 1 for both of these to at least start both tasks soon
// Do not add snapshot task if we only want the incremental backup
if (!incrementalBackupOnly.get().present() || !incrementalBackupOnly.get().get()) {
// Check snapshot mode: 0=RANGEFILE, 1=BULKDUMP, 2=BOTH
int snapshotModeValue = co_await config.snapshotMode().getD(tr, Snapshot::False, 0);
TraceEvent("StartFullBackupTaskSnapshotMode")
.detail("BackupUID", config.getUid())
.detail("SnapshotModeValue", snapshotModeValue)
.detail("IncrementalBackupOnly",
incrementalBackupOnly.get().present() && incrementalBackupOnly.get().get());
// Add BulkDump task if mode is BULKDUMP(1) or BOTH(2)
if (snapshotModeValue == 1 || snapshotModeValue == 2) {
// Save original BulkDump mode BEFORE creating the task, so it's persisted in the database.
// This allows crash recovery to restore the correct mode even if the task restarts.
int currentBulkDumpMode = co_await getBulkDumpMode(tr->getDatabase());
config.originalBulkDumpMode().set(tr, currentBulkDumpMode);
co_await BulkDumpTaskFunc::addTask(
tr, taskBucket, task, beginVersion, TaskCompletionKey::joinWith(backupFinished));
}
// Add traditional range file snapshot if mode is RANGEFILE(0) or BOTH(2)
if (snapshotModeValue == 0 || snapshotModeValue == 2) {
co_await BackupSnapshotDispatchTask::addTask(
tr, taskBucket, task, 1, TaskCompletionKey::joinWith(backupFinished));
}
}
co_await BackupLogsDispatchTask::addTask(
tr, taskBucket, task, 1, 0, beginVersion, TaskCompletionKey::joinWith(backupFinished));
// If a clean stop is requested, the log and snapshot tasks will quit after the backup is restorable, then
// the following task will clean up and set the completed state.
co_await FileBackupFinishedTask::addTask(tr, taskBucket, task, TaskCompletionKey::noSignal(), backupFinished);
co_await taskBucket->finish(tr, task);
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
UID uid,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key key = co_await addBackupTask(StartFullBackupTaskFunc::name,
StartFullBackupTaskFunc::version,
tr,
taskBucket,
completionKey,
BackupConfig(uid),
waitFor);
co_return key;
}
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef StartFullBackupTaskFunc::name = "file_backup_start_5.2"_sr;
REGISTER_TASKFUNC(StartFullBackupTaskFunc);
// BulkLoadRestoreTaskFunc: Restores data using BulkLoad instead of traditional range file restore
// This task is used when useRangeFileRestore=false is specified
struct BulkLoadRestoreTaskFunc : RestoreTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
static struct {
static TaskParam<Version> restoreVersion() { return __FUNCTION__sr; }
static TaskParam<std::string> backupUrl() { return __FUNCTION__sr; }
static TaskParam<std::string> bulkDumpJobId() { return __FUNCTION__sr; }
} Params;
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig restore(task);
Version restoreVersion = Params.restoreVersion().get(task);
std::string backupUrl = Params.backupUrl().get(task);
std::string bulkDumpJobId = Params.bulkDumpJobId().getOrDefault(task, "");
TraceEvent("BulkLoadRestoreTaskStart")
.detail("RestoreUID", restore.getUid())
.detail("RestoreVersion", restoreVersion)
.detail("BackupUrl", backupUrl)
.detail("BulkDumpJobId", bulkDumpJobId);
// Declare state variable before try block so it's accessible in catch block
int originalBulkLoadMode = 0;
{
Error savedError;
try {
// Open backup container for metadata access
Reference<IBackupContainer> bcRef = IBackupContainer::openContainer(backupUrl, {}, {}, 0);
// Get restore ranges using a transaction
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
std::vector<KeyRange> restoreRanges = co_await restore.getRestoreRangesOrDefault(tr);
// If bulkDumpJobId is empty, read it from the snapshot file metadata
if (bulkDumpJobId.empty()) {
// Use the backup container we already opened to find the snapshot file for our restore version
auto* bcfsPtr = dynamic_cast<BackupContainerFileSystem*>(bcRef.getPtr());
if (bcfsPtr != nullptr) {
Reference<BackupContainerFileSystem> bcfs =
Reference<BackupContainerFileSystem>::addRef(bcfsPtr);
std::vector<KeyspaceSnapshotFile> snapshots = co_await bcfs->listKeyspaceSnapshots();
int snapIdx = snapshots.size() - 1;
while (snapIdx >= 0 && bulkDumpJobId.empty()) {
// For BulkDump snapshots: beginVersion == endVersion == snapshotVersion
// Accept any snapshot that is at or before our restore version
if (snapshots[snapIdx].endVersion <= restoreVersion) {
// Read the snapshot file to get bulkDumpJobId
std::string snapFileName = snapshots[snapIdx].fileName;
Reference<IAsyncFile> snapFile = co_await bcfs->readFile(snapFileName);
int64_t snapSize = co_await snapFile->size();
Standalone<StringRef> snapBuf = makeString(snapSize);
int bytesRead = co_await snapFile->read(mutateString(snapBuf), snapSize, 0);
if (bytesRead == snapSize) {
json_spirit::mValue json;
if (json_spirit::read_string(snapBuf.toString(), json)) {
JSONDoc doc(json);
std::string jobId;
if (doc.tryGet("bulkDumpJobId", jobId) && !jobId.empty()) {
bulkDumpJobId = jobId;
TraceEvent("BulkLoadRestoreFoundJobId")
.detail("RestoreUID", restore.getUid())
.detail("BulkDumpJobId", bulkDumpJobId)
.detail("SnapshotFile", snapFileName);
}
}
}
}
snapIdx--;
}
}
}
// Create BulkLoad job from the BulkDump data
// BulkDump stores data under data/<container>/bulkdump_data/ subdirectory.
// DD appends the jobId via getBulkLoadJobRoot(jobRoot, jobId) when accessing files.
std::string jobRoot = getBackupDataPath(backupUrl, "bulkdump_data");
UID dumpJobUid;
if (!bulkDumpJobId.empty()) {
dumpJobUid = UID::fromString(bulkDumpJobId);
} else {
// No BulkDump job ID found - this is a permanent error for BulkLoad restore.
// Abort the restore immediately instead of retrying forever.
TraceEvent(SevError, "BulkLoadRestoreNoBulkDumpJobId")
.detail("RestoreUID", restore.getUid())
.detail("BackupUrl", backupUrl)
.detail("Action", "Aborting restore - backup is not BulkLoad compatible");
co_await restore.logError(
cx, restore_missing_data(), "BulkLoad restore failed: backup has no bulkdump data", nullptr);
// Abort the restore by setting state to ABORTED
Reference<ReadYourWritesTransaction> abortTr(new ReadYourWritesTransaction(cx));
abortTr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
abortTr->setOption(FDBTransactionOptions::LOCK_AWARE);
restore.stateEnum().set(abortTr, ERestoreState::ABORTED);
co_await abortTr->commit();
throw restore_missing_data();
}
// Verify BulkDump dataset completeness before proceeding
bool datasetComplete = co_await verifyBulkDumpDatasetCompleteness(bcRef, bulkDumpJobId);
if (!datasetComplete) {
// Dataset is incomplete - abort the restore permanently
TraceEvent(SevError, "BulkLoadRestoreDatasetIncomplete")
.detail("RestoreUID", restore.getUid())
.detail("BulkDumpJobId", bulkDumpJobId)
.detail("BackupUrl", backupUrl)
.detail("Action", "Aborting restore - bulkdump dataset is incomplete");
co_await restore.logError(
cx, restore_missing_data(), "BulkLoad restore failed: bulkdump dataset incomplete", nullptr);
// Abort the restore by setting state to ABORTED
Reference<ReadYourWritesTransaction> abortTr(new ReadYourWritesTransaction(cx));
abortTr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
abortTr->setOption(FDBTransactionOptions::LOCK_AWARE);
restore.stateEnum().set(abortTr, ERestoreState::ABORTED);
co_await abortTr->commit();
throw restore_missing_data();
}
TraceEvent("BulkLoadRestoreDatasetVerified")
.detail("RestoreUID", restore.getUid())
.detail("BulkDumpJobId", bulkDumpJobId);
// Determine transport method from backup URL
BulkLoadTransportMethod loadTransportMethod =
isBlobstoreUrl(backupUrl) ? BulkLoadTransportMethod::BLOBSTORE : BulkLoadTransportMethod::CP;
// BulkLoad range must match the BulkDump range (normalKeys)
// The actual restore ranges will be applied via mutation log replay
BulkLoadJobState bulkLoadJob = createBulkLoadJob(dumpJobUid, normalKeys, jobRoot, loadTransportMethod);
TraceEvent("BulkLoadRestoreJobCreated")
.detail("RestoreUID", restore.getUid())
.detail("BulkLoadJobId", bulkLoadJob.getJobId())
.detail("JobRoot", jobRoot);
// Register the BulkLoad range lock owner (required for range locking)
co_await registerRangeLockOwner(cx, "BulkLoad", "BulkLoad restore operation");
TraceEvent("BulkLoadRestoreRegisteredLockOwner")
.detail("RestoreUID", restore.getUid())
.detail("Owner", "BulkLoad");
// TODO(BulkLoad): no precondition validation happens here today.
// submitBulkLoadJob() does not check that the cluster has
// shard_encode_location_metadata=1 and enable_read_lock_on_range=1, nor
// that the storage engine supports SST ingestion. If those preconditions
// are not met, setBulkLoadMode + submitBulkLoadJob both succeed but the
// Data Distributor never dispatches any tasks, leaving the restore in
// "State: running, Tasks: 0/0" indefinitely. Validation needs to live
// somewhere with real cluster-side knob visibility (DD or a commit proxy);
// it cannot be done from fdbclient because SERVER_KNOBS here are this
// process's local defaults, not the running cluster's actual config.
// Read the original BulkLoad mode from config (saved by StartFullRestoreTaskFunc before task creation).
// This is persisted in the database so we can restore the correct mode even after a crash.
int mode = co_await restore.originalBulkLoadMode().getD(cx.getReference(), Snapshot::False, 0);
originalBulkLoadMode = mode;
// Enable BulkLoad mode at DD level so the job will be processed
co_await setBulkLoadMode(cx, 1);
TraceEvent("BulkLoadRestoreEnabledMode")
.detail("RestoreUID", restore.getUid())
.detail("BulkLoadJobId", bulkLoadJob.getJobId());
// Submit BulkLoad job (must be lockAware since DB is locked during restore)
co_await submitBulkLoadJob(cx, bulkLoadJob, true /* lockAware */);
TraceEvent("BulkLoadRestoreJobSubmitted")
.detail("RestoreUID", restore.getUid())
.detail("BulkLoadJobId", bulkLoadJob.getJobId());
// Get total block count for progress tracking
int64_t totalBlocks = co_await restore.fileBlockCount().getD(cx.getReference(), Snapshot::False, 0);
// Monitor BulkLoad progress - timeout is configurable for large datasets
// Must be lockAware since DB is locked during restore
// Use the progress-tracking version to update restore counters
bool completed = co_await monitorBulkLoadJobCompletionWithProgress(cx,
bulkLoadJob.getJobId(),
restore.getUid(),
totalBlocks,
CLIENT_KNOBS->BULKLOAD_JOB_TIMEOUT,
5.0, // Poll every 5 seconds
true); // lockAware
if (!completed) {
TraceEvent(SevWarn, "BulkLoadRestoreTimeout")
.detail("RestoreUID", restore.getUid())
.detail("BulkLoadJobId", bulkLoadJob.getJobId())
.detail("TimeoutDuration", CLIENT_KNOBS->BULKLOAD_JOB_TIMEOUT);
// Restore original BulkLoad mode before throwing
if (originalBulkLoadMode != 1) {
co_await setBulkLoadMode(cx, originalBulkLoadMode);
}
throw timed_out();
}
// Restore original BulkLoad mode now that the job is complete
if (originalBulkLoadMode != 1) {
co_await setBulkLoadMode(cx, originalBulkLoadMode);
}
TraceEvent("BulkLoadRestoreTaskComplete")
.detail("RestoreUID", restore.getUid())
.detail("BulkLoadJobId", bulkLoadJob.getJobId())
.detail("RestoreVersion", restoreVersion);
// Increment counter for test assertions
g_bulkLoadRestoreTaskCompleteCount.fetch_add(1);
co_return;
} catch (Error& e) {
savedError = e;
}
TraceEvent(SevWarn, "BulkLoadRestoreTaskError")
.error(savedError)
.detail("RestoreUID", restore.getUid())
.detail("BackupUrl", backupUrl);
// Restore original BulkLoad mode on error
try {
if (originalBulkLoadMode != 1) {
co_await setBulkLoadMode(cx, originalBulkLoadMode);
}
} catch (Error& e2) {
if (e2.code() == error_code_actor_cancelled) {
throw;
}
TraceEvent(SevWarn, "BulkLoadRestoreRestoreModeError").error(e2);
}
throw savedError;
}
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig restore(task);
// Mark BulkLoad phase as complete
restore.bulkLoadComplete().set(tr, true);
// Update fileBlocksFinished and filesBlocksDispatched to match fileBlockCount
// so the restore progress tracker knows all range data has been loaded.
// BulkLoad doesn't use the traditional block dispatching mechanism.
int64_t totalBlocks = co_await restore.fileBlockCount().getD(tr, Snapshot::False, 0);
int64_t finishedBlocks = co_await restore.fileBlocksFinished().getD(tr, Snapshot::False, 0);
if (finishedBlocks < totalBlocks) {
// Set both dispatched and finished to match total to indicate all range data is done
restore.filesBlocksDispatched().set(tr, totalBlocks);
restore.fileBlocksFinished().set(tr, totalBlocks);
}
// Set firstConsistentVersion if not already set
// For BulkLoad restore, this is the snapshot version from the BulkDump
Version firstConsistentVer =
co_await restore.firstConsistentVersion().getD(tr, Snapshot::False, invalidVersion);
if (firstConsistentVer == invalidVersion) {
// Get the restore version as the first consistent version
Version restoreVer = co_await restore.restoreVersion().getD(tr, Snapshot::False, invalidVersion);
if (restoreVer != invalidVersion) {
restore.firstConsistentVersion().set(tr, restoreVer);
}
}
TraceEvent("BulkLoadRestoreTaskFinished")
.detail("RestoreUID", restore.getUid())
.detail("TotalBlocks", totalBlocks)
.detail("FinishedBlocks", finishedBlocks);
Reference<TaskFuture> taskFuture = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
co_await taskFuture->set(tr, taskBucket);
co_await taskBucket->finish(tr, task);
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
Version restoreVersion,
std::string backupUrl,
std::string bulkDumpJobId,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key doneKey = co_await completionKey.get(tr, taskBucket);
Reference<Task> task(new Task(BulkLoadRestoreTaskFunc::name, BulkLoadRestoreTaskFunc::version, doneKey));
// Set task parameters
Params.restoreVersion().set(task, restoreVersion);
Params.backupUrl().set(task, backupUrl);
Params.bulkDumpJobId().set(task, bulkDumpJobId);
// Get restore config from parent task and bind it to new task
co_await RestoreConfig(parentTask).toTask(tr, task);
if (!waitFor) {
co_return taskBucket->addTask(tr, task);
}
co_await waitFor->onSetAddTask(tr, taskBucket, task);
co_return "OnSetAddTask"_sr;
}
};
StringRef BulkLoadRestoreTaskFunc::name = "bulk_load_restore_5.2"_sr;
REGISTER_TASKFUNC(BulkLoadRestoreTaskFunc);
struct RestoreCompleteTaskFunc : RestoreTaskFuncBase {
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
co_await checkTaskVersion(tr->getDatabase(), task, name, version);
RestoreConfig restore(task);
restore.stateEnum().set(tr, ERestoreState::COMPLETED);
bool unlockDB = co_await restore.unlockDBAfterRestore().getD(tr, Snapshot::False, true);
tr->atomicOp(metadataVersionKey, metadataVersionRequiredValue, MutationRef::SetVersionstampedValue);
// Clear the file map now since it could be huge.
restore.fileSet().clear(tr);
// TODO: Validate that the range version map has exactly the restored ranges in it. This means that for
// any restore operation the ranges to restore must be within the backed up ranges, otherwise from the
// restore perspective it will appear that some key ranges were missing and so the backup set is incomplete
// and the restore has failed. This validation cannot be done currently because Restore only supports a
// single restore range but backups can have many ranges.
// Clear the applyMutations stuff, including any unapplied mutations from versions beyond the restored
// version.
restore.clearApplyMutationsKeys(tr);
co_await taskBucket->finish(tr, task);
if (unlockDB) {
co_await unlockDatabase(tr, restore.getUid());
}
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key doneKey = co_await completionKey.get(tr, taskBucket);
Reference<Task> task(new Task(RestoreCompleteTaskFunc::name, RestoreCompleteTaskFunc::version, doneKey));
// Get restore config from parent task and bind it to new task
co_await RestoreConfig(parentTask).toTask(tr, task);
if (!waitFor) {
co_return taskBucket->addTask(tr, task);
}
co_await waitFor->onSetAddTask(tr, taskBucket, task);
co_return "OnSetAddTask"_sr;
}
static StringRef name;
static constexpr uint32_t version = 1;
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return Void();
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef RestoreCompleteTaskFunc::name = "restore_complete"_sr;
REGISTER_TASKFUNC(RestoreCompleteTaskFunc);
struct RestoreFileTaskFuncBase : RestoreTaskFuncBase {
struct InputParams {
static TaskParam<RestoreFile> inputFile() { return __FUNCTION__sr; }
static TaskParam<int64_t> readOffset() { return __FUNCTION__sr; }
static TaskParam<int64_t> readLen() { return __FUNCTION__sr; }
} Params;
std::string toString(Reference<Task> task) const override {
return format("fileName '%s' readLen %lld readOffset %lld",
Params.inputFile().get(task).fileName.c_str(),
Params.readLen().get(task),
Params.readOffset().get(task));
}
};
struct RestoreRangeTaskFunc : RestoreFileTaskFuncBase {
static struct : InputParams {
// The range of data that the (possibly empty) data represented, which is set if it intersects the target
// restore range
static TaskParam<KeyRange> originalFileRange() { return __FUNCTION__sr; }
static TaskParam<std::vector<KeyRange>> originalFileRanges() { return __FUNCTION__sr; }
static std::vector<KeyRange> getOriginalFileRanges(Reference<Task> task) {
if (originalFileRanges().exists(task)) {
return Params.originalFileRanges().get(task);
} else {
std::vector<KeyRange> range;
if (originalFileRange().exists(task))
range.push_back(Params.originalFileRange().get(task));
return range;
}
}
} Params;
std::string toString(Reference<Task> task) const override {
std::string returnStr = RestoreFileTaskFuncBase::toString(task);
for (auto& range : Params.getOriginalFileRanges(task))
returnStr += format(" originalFileRange '%s'", printable(range).c_str());
return returnStr;
}
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig restore(task);
RestoreFile rangeFile = Params.inputFile().get(task);
int64_t readOffset = Params.readOffset().get(task);
int64_t readLen = Params.readLen().get(task);
TraceEvent("FileRestoreRangeStart")
.suppressFor(60)
.detail("RestoreUID", restore.getUid())
.detail("FileName", rangeFile.fileName)
.detail("FileVersion", rangeFile.version)
.detail("FileSize", rangeFile.fileSize)
.detail("ReadOffset", readOffset)
.detail("ReadLen", readLen);
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
Future<Reference<IBackupContainer>> bc;
Future<std::vector<KeyRange>> restoreRanges;
Future<Key> addPrefix;
Future<Key> removePrefix;
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Reference<IBackupContainer> _bc = co_await restore.sourceContainer().getOrThrow(tr);
bc = getBackupContainerWithProxy(_bc);
restoreRanges = restore.getRestoreRangesOrDefault(tr);
addPrefix = restore.addPrefix().getD(tr);
removePrefix = restore.removePrefix().getD(tr);
co_await taskBucket->keepRunning(tr, task);
co_await (success(bc) && success(restoreRanges) && success(addPrefix) && success(removePrefix) &&
checkTaskVersion(tr->getDatabase(), task, name, version));
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
Reference<IAsyncFile> inFile = co_await bc.get()->readFile(rangeFile.fileName);
Standalone<VectorRef<KeyValueRef>> blockData;
try {
// data is each real KV, not encoded mutations
Standalone<VectorRef<KeyValueRef>> data = co_await decodeRangeFileBlock(inFile, readOffset, readLen, cx);
blockData = data;
} catch (Error& e) {
if (e.code() == error_code_encrypt_keys_fetch_failed || e.code() == error_code_encrypt_key_not_found) {
co_return;
}
throw;
}
Arena arena;
DatabaseConfiguration config = co_await getDatabaseConfiguration(cx);
// First and last key are the range for this file
KeyRange fileRange;
std::vector<KeyRange> originalFileRanges;
// If fileRange doesn't intersect restore range then we're done.
int index{ 0 };
for (index = 0; index < restoreRanges.get().size(); index++) {
auto& restoreRange = restoreRanges.get()[index];
fileRange = KeyRangeRef(blockData.front().key, blockData.back().key);
if (!fileRange.intersects(restoreRange))
continue;
// We know the file range intersects the restore range but there could still be keys outside the restore
// range. Find the subvector of kv pairs that intersect the restore range. Note that the first and last
// keys are just the range endpoints for this file
int rangeStart = 1;
int rangeEnd = blockData.size() - 1;
// Slide start forward, stop if something in range is found
while (rangeStart < rangeEnd && !restoreRange.contains(blockData[rangeStart].key))
++rangeStart;
// Side end backward, stop if something in range is found
while (rangeEnd > rangeStart && !restoreRange.contains(blockData[rangeEnd - 1].key))
--rangeEnd;
VectorRef<KeyValueRef> data = blockData.slice(rangeStart, rangeEnd);
// Shrink file range to be entirely within restoreRange and translate it to the new prefix
// First, use the untranslated file range to create the shrunk original file range which must be used in
// the kv range version map for applying mutations
KeyRange originalFileRange =
KeyRangeRef(std::max(fileRange.begin, restoreRange.begin), std::min(fileRange.end, restoreRange.end));
originalFileRanges.push_back(originalFileRange);
// Now shrink and translate fileRange
Key fileEnd = std::min(fileRange.end, restoreRange.end);
if (fileEnd == (removePrefix.get().empty() ? allKeys.end : strinc(removePrefix.get()))) {
fileEnd = addPrefix.get().empty() ? allKeys.end : strinc(addPrefix.get());
} else {
fileEnd = fileEnd.removePrefix(removePrefix.get()).withPrefix(addPrefix.get());
}
fileRange = KeyRangeRef(std::max(fileRange.begin, restoreRange.begin)
.removePrefix(removePrefix.get())
.withPrefix(addPrefix.get()),
fileEnd);
int start = 0;
int end = data.size();
int dataSizeLimit =
buggify() ? deterministicRandom()->randomInt(256 * 1024, 10e6) : CLIENT_KNOBS->RESTORE_WRITE_TX_SIZE;
tr->reset();
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
int i = start;
int txBytes = 0;
int iend = start;
// find iend that results in the desired transaction size
for (; iend < end && txBytes < dataSizeLimit; ++iend) {
txBytes += data[iend].key.expectedSize();
txBytes += data[iend].value.expectedSize();
}
// Clear the range we are about to set.
// If start == 0 then use fileBegin for the start of the range, else data[start]
// If iend == end then use fileEnd for the end of the range, else data[iend]
// clear the raw key(without alog prefix)
KeyRange trRange = KeyRangeRef(
(start == 0) ? fileRange.begin
: data[start].key.removePrefix(removePrefix.get()).withPrefix(addPrefix.get()),
(iend == end) ? fileRange.end
: data[iend].key.removePrefix(removePrefix.get()).withPrefix(addPrefix.get()));
tr->clear(trRange);
for (; i < iend; ++i) {
tr->setOption(FDBTransactionOptions::NEXT_WRITE_NO_WRITE_CONFLICT_RANGE);
tr->set(data[i].key.removePrefix(removePrefix.get()).withPrefix(addPrefix.get()),
data[i].value);
}
// Add to bytes written count
restore.bytesWritten().atomicOp(tr, txBytes, MutationRef::Type::AddValue);
Future<Void> checkLock = checkDatabaseLock(tr, restore.getUid());
co_await taskBucket->keepRunning(tr, task);
co_await checkLock;
co_await tr->commit();
TraceEvent("FileRestoreCommittedRange")
.suppressFor(60)
.detail("RestoreUID", restore.getUid())
.detail("FileName", rangeFile.fileName)
.detail("FileVersion", rangeFile.version)
.detail("FileSize", rangeFile.fileSize)
.detail("ReadOffset", readOffset)
.detail("ReadLen", readLen)
.detail("CommitVersion", tr->getCommittedVersion())
.detail("BeginRange", trRange.begin)
.detail("EndRange", trRange.end)
.detail("StartIndex", start)
.detail("EndIndex", i)
.detail("DataSize", data.size())
.detail("Bytes", txBytes)
.detail("OriginalFileRange", originalFileRange);
// Commit succeeded, so advance starting point
start = i;
if (start == end)
break;
tr->reset();
continue;
} catch (Error& e) {
err = e;
}
if (err.code() == error_code_transaction_too_large)
dataSizeLimit /= 2;
else
co_await tr->onError(err);
}
}
if (!originalFileRanges.empty()) {
if (buggify() && restoreRanges.get().size() == 1) {
Params.originalFileRange().set(task, originalFileRanges[0]);
} else {
Params.originalFileRanges().set(task, originalFileRanges);
}
}
co_return;
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig restore(task);
restore.fileBlocksFinished().atomicOp(tr, 1, MutationRef::Type::AddValue);
// Update the KV range map if originalFileRange is set
std::vector<Future<Void>> updateMap;
std::vector<KeyRange> ranges = Params.getOriginalFileRanges(task);
// if to restore((a, b), (e, f), (x, y)), then there are 3 ranges
for (auto& range : ranges) {
Value versionEncoded = BinaryWriter::toValue(Params.inputFile().get(task).version, Unversioned());
updateMap.push_back(krmSetRange(tr, restore.applyMutationsMapPrefix(), range, versionEncoded));
}
Reference<TaskFuture> taskFuture = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
co_await (taskFuture->set(tr, taskBucket) && taskBucket->finish(tr, task) && waitForAll(updateMap));
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
RestoreFile rf,
int64_t offset,
int64_t len,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key doneKey = co_await completionKey.get(tr, taskBucket);
Reference<Task> task(new Task(RestoreRangeTaskFunc::name, RestoreRangeTaskFunc::version, doneKey));
// Create a restore config from the current task and bind it to the new task.
co_await RestoreConfig(parentTask).toTask(tr, task);
Params.inputFile().set(task, rf);
Params.readOffset().set(task, offset);
Params.readLen().set(task, len);
if (!waitFor) {
co_return taskBucket->addTask(tr, task);
}
co_await waitFor->onSetAddTask(tr, taskBucket, task);
co_return "OnSetAddTask"_sr;
}
static StringRef name;
static constexpr uint32_t version = 1;
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef RestoreRangeTaskFunc::name = "restore_range_data"_sr;
REGISTER_TASKFUNC(RestoreRangeTaskFunc);
// Decodes a mutation log key, which contains (hash, commitVersion, chunkNumber) and
// returns (commitVersion, chunkNumber)
std::pair<Version, int32_t> decodeMutationLogKey(const StringRef& key) {
ASSERT(key.size() == sizeof(uint8_t) + sizeof(Version) + sizeof(int32_t));
uint8_t hash;
Version version;
int32_t part;
BinaryReader rd(key, Unversioned());
rd >> hash >> version >> part;
version = bigEndian64(version);
part = bigEndian32(part);
int32_t v = version / CLIENT_KNOBS->LOG_RANGE_BLOCK_SIZE;
ASSERT(((uint8_t)hashlittle(&v, sizeof(v), 0)) == hash);
return std::make_pair(version, part);
}
// Decodes an encoded list of mutations in the format of:
// [includeVersion:uint64_t][val_length:uint32_t][mutation_1][mutation_2]...[mutation_k],
// where a mutation is encoded as:
// [type:uint32_t][keyLength:uint32_t][valueLength:uint32_t][param1][param2]
// noted version needs to be included here(0x0FDB00A200090001)
std::vector<MutationRef> decodeMutationLogValue(const StringRef& value) {
StringRefReader reader(value, restore_corrupted_data());
Version protocolVersion = reader.consume<uint64_t>();
if (protocolVersion <= 0x0FDB00A200090001) {
throw incompatible_protocol_version();
}
uint32_t val_length = reader.consume<uint32_t>();
if (val_length != value.size() - sizeof(uint64_t) - sizeof(uint32_t)) {
TraceEvent(SevError, "FileRestoreLogValueError")
.detail("ValueLen", val_length)
.detail("ValueSize", value.size())
.detail("Value", printable(value));
}
std::vector<MutationRef> mutations;
while (1) {
if (reader.eof())
break;
// Deserialization of a MutationRef, which was packed by MutationListRef::push_back_deep()
uint32_t type, p1len, p2len;
type = reader.consume<uint32_t>();
p1len = reader.consume<uint32_t>();
p2len = reader.consume<uint32_t>();
const uint8_t* key = reader.consume(p1len);
const uint8_t* val = reader.consume(p2len);
mutations.emplace_back((MutationRef::Type)type, StringRef(key, p1len), StringRef(val, p2len));
}
return mutations;
}
void AccumulatedMutations::addChunk(int chunkNumber, const KeyValueRef& kv) {
// here it validates that partition(chunk) number has to be continuous
if (chunkNumber == lastChunkNumber + 1) {
lastChunkNumber = chunkNumber;
serializedMutations += kv.value.toString();
} else {
lastChunkNumber = -2;
serializedMutations.clear();
}
kvs.push_back(kv);
}
bool AccumulatedMutations::isComplete() const {
if (lastChunkNumber >= 0) {
StringRefReader reader(serializedMutations, restore_corrupted_data());
Version protocolVersion = reader.consume<uint64_t>();
if (protocolVersion <= 0x0FDB00A200090001) {
throw incompatible_protocol_version();
}
uint32_t vLen = reader.consume<uint32_t>();
return vLen == reader.remainder().size();
}
return false;
}
// Returns true if a complete chunk contains any MutationRefs which intersect with any
// range in ranges.
// It is undefined behavior to run this if isComplete() does not return true.
bool AccumulatedMutations::matchesAnyRange(const RangeMapFilters& filters) const {
// decode param2, so that each actual mutations are in mutations variable
std::vector<MutationRef> mutations = decodeMutationLogValue(serializedMutations);
for (auto& m : mutations) {
if (filters.match(m)) {
return true;
}
}
return false;
}
bool RangeMapFilters::match(const MutationRef& m) const {
if (isSingleKeyMutation((MutationRef::Type)m.type)) {
if (match(singleKeyRange(m.param1))) {
return true;
}
} else if (m.type == MutationRef::ClearRange) {
if (match(KeyRangeRef(m.param1, m.param2))) {
return true;
}
} else {
ASSERT(false);
}
return false;
}
bool RangeMapFilters::match(const KeyValueRef& kv) const {
return match(singleKeyRange(kv.key));
}
bool RangeMapFilters::match(const KeyRangeRef& range) const {
auto ranges = rangeMap.intersectingRanges(range);
for (const auto& r : ranges) {
if (r.cvalue() == 1) {
return true;
}
}
return false;
}
// Returns a vector of filtered KV refs from data which are either part of incomplete mutation groups OR complete
// and have data relevant to one of the KV ranges in ranges
std::vector<KeyValueRef> filterLogMutationKVPairs(VectorRef<KeyValueRef> data, const RangeMapFilters& filters) {
std::unordered_map<Version, AccumulatedMutations> mutationBlocksByVersion;
// group mutations by version
for (auto& kv : data) {
// each kv is a [param1, param2]
auto versionAndChunkNumber = decodeMutationLogKey(kv.key);
mutationBlocksByVersion[versionAndChunkNumber.first].addChunk(versionAndChunkNumber.second, kv);
}
std::vector<KeyValueRef> output;
// then add each version to the output, and now each K in output is also a KeyValueRef,
// but mutations of the same versions stay together
for (auto& vb : mutationBlocksByVersion) {
AccumulatedMutations& m = vb.second;
// If the mutations are incomplete or match one of the ranges, include in results.
if (!m.isComplete() || m.matchesAnyRange(filters)) {
output.insert(output.end(), m.kvs.begin(), m.kvs.end());
}
}
return output;
}
struct RestoreLogDataTaskFunc : RestoreFileTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
StringRef getName() const override { return name; };
static struct : InputParams {
} Params;
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig restore(task);
RestoreFile logFile = Params.inputFile().get(task);
int64_t readOffset = Params.readOffset().get(task);
int64_t readLen = Params.readLen().get(task);
TraceEvent("FileRestoreLogStart")
.suppressFor(60)
.detail("RestoreUID", restore.getUid())
.detail("FileName", logFile.fileName)
.detail("FileBeginVersion", logFile.version)
.detail("FileEndVersion", logFile.endVersion)
.detail("FileSize", logFile.fileSize)
.detail("ReadOffset", readOffset)
.detail("ReadLen", readLen);
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
Reference<IBackupContainer> bc;
std::vector<KeyRange> ranges;
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Reference<IBackupContainer> _bc = co_await restore.sourceContainer().getOrThrow(tr);
bc = getBackupContainerWithProxy(_bc);
ranges = co_await restore.getRestoreRangesOrDefault(tr);
co_await checkTaskVersion(tr->getDatabase(), task, name, version);
co_await taskBucket->keepRunning(tr, task);
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
Key mutationLogPrefix = restore.mutationLogPrefix();
Reference<IAsyncFile> inFile = co_await bc->readFile(logFile.fileName);
Standalone<VectorRef<KeyValueRef>> dataOriginal =
co_await decodeMutationLogFileBlock(inFile, readOffset, readLen);
// Filter the KV pairs extracted from the log file block to remove any records known to not be needed for
// this restore based on the restore range set.
RangeMapFilters filters(ranges);
std::vector<KeyValueRef> dataFiltered = filterLogMutationKVPairs(dataOriginal, filters);
int start = 0;
int end = dataFiltered.size();
int dataSizeLimit =
buggify() ? deterministicRandom()->randomInt(256 * 1024, 10e6) : CLIENT_KNOBS->RESTORE_WRITE_TX_SIZE;
tr->reset();
while (true) {
Error err;
try {
if (start == end)
co_return;
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
int i = start;
int txBytes = 0;
for (; i < end && txBytes < dataSizeLimit; ++i) {
Key k = dataFiltered[i].key.withPrefix(mutationLogPrefix);
ValueRef v = dataFiltered[i].value; // each KV is a [param1 with added prefix -> param2]
tr->set(k, v);
txBytes += k.expectedSize();
txBytes += v.expectedSize();
}
Future<Void> checkLock = checkDatabaseLock(tr, restore.getUid());
co_await taskBucket->keepRunning(tr, task);
co_await checkLock;
// Add to bytes written count
restore.bytesWritten().atomicOp(tr, txBytes, MutationRef::Type::AddValue);
co_await tr->commit();
TraceEvent("FileRestoreCommittedLog")
.suppressFor(60)
.detail("RestoreUID", restore.getUid())
.detail("FileName", logFile.fileName)
.detail("FileBeginVersion", logFile.version)
.detail("FileEndVersion", logFile.endVersion)
.detail("FileSize", logFile.fileSize)
.detail("ReadOffset", readOffset)
.detail("ReadLen", readLen)
.detail("CommitVersion", tr->getCommittedVersion())
.detail("StartIndex", start)
.detail("EndIndex", i)
.detail("RecordCountOriginal", dataOriginal.size())
.detail("RecordCountFiltered", dataFiltered.size())
.detail("Bytes", txBytes);
// Commit succeeded, so advance starting point
start = i;
tr->reset();
continue;
} catch (Error& e) {
err = e;
}
if (err.code() == error_code_transaction_too_large)
dataSizeLimit /= 2;
else
co_await tr->onError(err);
}
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig(task).fileBlocksFinished().atomicOp(tr, 1, MutationRef::Type::AddValue);
Reference<TaskFuture> taskFuture = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
// TODO: Check to see if there is a leak in the FutureBucket since an invalid task (validation key fails)
// will never set its taskFuture.
co_await (taskFuture->set(tr, taskBucket) && taskBucket->finish(tr, task));
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
RestoreFile lf,
int64_t offset,
int64_t len,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key doneKey = co_await completionKey.get(tr, taskBucket);
Reference<Task> task(new Task(RestoreLogDataTaskFunc::name, RestoreLogDataTaskFunc::version, doneKey));
// Create a restore config from the current task and bind it to the new task.
// RestoreConfig(parentTask) creates prefix of : fileRestorePrefixRange.begin/uid->config/[uid]
co_await RestoreConfig(parentTask).toTask(tr, task);
Params.inputFile().set(task, lf);
Params.readOffset().set(task, offset);
Params.readLen().set(task, len);
if (!waitFor) {
co_return taskBucket->addTask(tr, task);
}
co_await waitFor->onSetAddTask(tr, taskBucket, task);
co_return "OnSetAddTask"_sr;
}
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef RestoreLogDataTaskFunc::name = "restore_log_data"_sr;
REGISTER_TASKFUNC(RestoreLogDataTaskFunc);
// type|kLen|vLen|Key|Value
// ref: decodeBackupLogValue()
Standalone<StringRef> transformMutationToOldFormat(MutationRef m) {
BinaryWriter bw(Unversioned());
uint32_t len1, len2, type;
type = m.type;
len1 = m.param1.size();
len2 = m.param2.size();
bw << type;
bw << len1;
bw << len2;
// do not use <<, it is overloaded for stringref to write its size first
bw.serializeBytes(m.param1);
bw.serializeBytes(m.param2);
return bw.toValue();
}
// this method takes a version and a list of list of mutations of this verison,
// each list is returned from a iterator sorted by sub
// it will first add all mutations in subsequence order
// then combine them in old-format (param1, parma2) and return
// this method assumes that iterator can return a list of mutations
/*
mutations are serialized in file as below format:
`<BlockHeader>`
`<Version_1><Subseq_1><Mutation1_len><Mutation1>`
`<Version_2><Subseq_2><Mutation2_len><Mutation2>`
`…`
`<Padding>
for now, assume each iterator returns a vector<pair<subsequence, mutation> >
noted that the mutation's arena has to be valid during the execution
according to BackupWorker::addMutation, version 64-bit, sub is 32-bit and mutation length is 32-bit So iterator
will combine all mutations in the same version and return a vector iterator should also return the subsequence
together with each mutation as here we will do another mergeSort for subsequence again to decide the order and here
we will decode the stringref
*/
Standalone<VectorRef<KeyValueRef>> generateOldFormatMutations(
Version commitVersion,
const std::vector<Standalone<VectorRef<VersionedMutation>>>& newFormatMutations) {
Standalone<VectorRef<KeyValueRef>> results;
std::vector<Standalone<VectorRef<KeyValueRef>>> oldFormatMutations;
// mergeSort subversion here
// just do a global sort for everyone
int32_t totalBytes = 0;
std::map<uint32_t, std::vector<Standalone<StringRef>>> mutationsBySub;
std::map<uint32_t, std::vector<Standalone<MutationRef>>> tmpMap;
for (auto& eachTagMutations : newFormatMutations) {
for (auto& vm : eachTagMutations) {
uint32_t sub = vm.subsequence;
Standalone<StringRef> mutationOldFormat = transformMutationToOldFormat(vm.mutation);
mutationsBySub[sub].push_back(mutationOldFormat);
tmpMap[sub].push_back(vm.mutation);
totalBytes += mutationOldFormat.size();
}
}
// the list of param2 needs to have the first 64 bites as 0x0FDB00A200090001
BinaryWriter param2Writer(IncludeVersion(ProtocolVersion::withBackupMutations()));
param2Writer << totalBytes;
for (auto& mutationsForSub : mutationsBySub) {
// concatenate them to param2Str
for (auto& m : mutationsForSub.second) {
// refer to transformMutationToOldFormat
param2Writer.serializeBytes(m);
}
}
Key param2Concat = param2Writer.toValue();
// deal with param1
int32_t hashBase = commitVersion / CLIENT_KNOBS->LOG_RANGE_BLOCK_SIZE;
BinaryWriter wrParam1(Unversioned()); // hash/commitVersion/part
wrParam1 << (uint8_t)hashlittle(&hashBase, sizeof(hashBase), 0);
wrParam1 << bigEndian64(commitVersion);
uint32_t* partBuffer = nullptr;
// TODO: re-use the similar logic in CommitProxyServer::addBackupMutations
// generate a list of (param1, param2)
// param2 has format: length_of_the_mutation_group | encoded_mutation_1 | … | encoded_mutation_k
// each mutation has format type|kLen|vLen|Key|Value
for (int part = 0; part * CLIENT_KNOBS->MUTATION_BLOCK_SIZE < param2Concat.size(); part++) {
KeyValueRef backupKV;
// Assign the second parameter as the part
backupKV.value = getBackupValue(param2Concat, part);
Key key = getBackupKey(wrParam1, &partBuffer, part); // holds the memory
backupKV.key = key;
results.push_back_deep(results.arena(), backupKV);
}
return results;
}
struct RestoreLogDataPartitionedTaskFunc : RestoreFileTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
StringRef getName() const override { return name; };
static struct {
static TaskParam<int64_t> maxTagID() { return __FUNCTION__sr; }
static TaskParam<Version> beginVersion() { return __FUNCTION__sr; }
static TaskParam<Version> endVersion() { return __FUNCTION__sr; }
static TaskParam<int64_t> bytesWritten() { return __FUNCTION__sr; }
static TaskParam<std::vector<RestoreConfig::RestoreFile>> logs() { return __FUNCTION__sr; }
} Params;
// From all iterators, find the minimum version and return it together with if this version has data.
static Future<std::pair<Version, bool>> findNextVersion(std::vector<Reference<PartitionedLogIterator>> iterators) {
Version minVersion = std::numeric_limits<int64_t>::max();
bool atLeastOneIteratorHasNext = false;
for (int k = 0; k < iterators.size(); k++) {
if (!iterators[k]->hasNext()) {
continue;
}
atLeastOneIteratorHasNext = true;
Version v = co_await iterators[k]->peekNextVersion();
minVersion = std::min(minVersion, v);
}
co_return std::make_pair(minVersion, atLeastOneIteratorHasNext);
}
// Reads from all iterators and sends a list of mutations for the given version
// to the mutationStream for consumption. This actor reads as fast as possible,
// which is fine since the task is working on the version range of size
// RESTORE_PARTITIONED_BATCH_VERSION_SIZE, thus limiting the amount of memory used.
static Future<Void> readLogData(PromiseStream<Standalone<VectorRef<KeyValueRef>>> mutationStream,
std::vector<Reference<PartitionedLogIterator>> iterators,
Version begin,
Version end) {
// mergeSort all iterator until all are exhausted
// it stores all mutations for the next min version, in new format
try {
bool atLeastOneIteratorHasNext = true;
Version minVersion{ 0 };
Version lastMinVersion = invalidVersion;
while (atLeastOneIteratorHasNext) {
std::pair<Version, bool> minVersionAndHasNext = co_await findNextVersion(iterators);
minVersion = minVersionAndHasNext.first;
atLeastOneIteratorHasNext = minVersionAndHasNext.second;
ASSERT_LT(lastMinVersion, minVersion);
lastMinVersion = minVersion;
if (atLeastOneIteratorHasNext) {
std::vector<Standalone<VectorRef<VersionedMutation>>> mutationsSingleVersion =
co_await getMutationsForVersion(iterators, minVersion);
if (minVersion < begin) {
// skip generating mutations, because this is not within desired range
// this is already handled by the previous taskfunc
continue;
} else if (minVersion >= end) {
// all valid data has been consumed
break;
}
// transform from new format to old format(param1, param2) for this version.
// This transformation has to be done version by version.
Standalone<VectorRef<KeyValueRef>> oldFormatMutations =
generateOldFormatMutations(minVersion, mutationsSingleVersion);
mutationStream.send(oldFormatMutations);
}
}
mutationStream.sendError(end_of_stream());
} catch (Error& e) {
TraceEvent(SevWarn, "FileRestoreLogReadError").error(e);
mutationStream.sendError(e);
}
co_return;
}
// Reads from all iterators and returns a list of mutations for the given version.
static Future<std::vector<Standalone<VectorRef<VersionedMutation>>>> getMutationsForVersion(
std::vector<Reference<PartitionedLogIterator>> iterators,
Version minVersion) {
std::vector<Standalone<VectorRef<VersionedMutation>>> mutationsSingleVersion;
for (int k = 0; k < iterators.size(); k++) {
if (!iterators[k]->hasNext()) {
continue;
}
Version v = co_await iterators[k]->peekNextVersion();
if (v == minVersion) {
Standalone<VectorRef<VersionedMutation>> tmp = co_await iterators[k]->getNext();
mutationsSingleVersion.push_back(tmp);
}
}
co_return mutationsSingleVersion;
}
// Writes backup mutations to the database
static Future<Void> writeMutations(Database cx,
std::vector<Standalone<VectorRef<KeyValueRef>>> mutations,
Key mutationLogPrefix,
Reference<Task> task,
Reference<TaskBucket> taskBucket) {
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
Standalone<VectorRef<KeyValueRef>> oldFormatMutations;
int mutationIndex = 0;
int mutationCount = 0;
int txBytes = 0;
int txBytesLimit = CLIENT_KNOBS->RESTORE_WRITE_TX_SIZE;
for (const auto& data : mutations) {
oldFormatMutations.append(oldFormatMutations.arena(), data.begin(), data.size());
}
int totalMutation = oldFormatMutations.size();
// multiple transactions are needed, so this has to be in a _execute method rather than
// a _finish method
// this transaction does blind writes, so they are idempotent operations even if multiple instances of
// the same tasks are running these transactions.
while (true) {
Error err;
try {
if (mutationIndex == totalMutation) {
break;
}
txBytes = 0;
mutationCount = 0;
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
while (mutationIndex + mutationCount < totalMutation && txBytes < txBytesLimit) {
Key k = oldFormatMutations[mutationIndex + mutationCount].key.withPrefix(mutationLogPrefix);
ValueRef v = oldFormatMutations[mutationIndex + mutationCount]
.value; // each KV is a [param1 with added prefix -> param2]
tr->set(k, v);
txBytes += k.expectedSize();
txBytes += v.expectedSize();
++mutationCount;
}
co_await taskBucket->keepRunning(tr, task);
co_await tr->commit();
int64_t oldBytes = Params.bytesWritten().get(task);
Params.bytesWritten().set(task, oldBytes + txBytes);
DisabledTraceEvent("FileRestorePartitionedLogCommittData")
.detail("MutationIndex", mutationIndex)
.detail("MutationCount", mutationCount)
.detail("TotalMutation", totalMutation)
.detail("Bytes", txBytes);
mutationIndex += mutationCount; // update mutationIndex after the commit succeeds
continue;
} catch (Error& e) {
err = e;
}
if (err.code() == error_code_transaction_too_large) {
txBytesLimit /= 2;
} else {
co_await tr->onError(err);
}
}
co_return;
}
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig restore(task);
int64_t maxTagID = Params.maxTagID().get(task);
std::vector<RestoreConfig::RestoreFile> logs = Params.logs().get(task);
Version begin = Params.beginVersion().get(task);
Version end = Params.endVersion().get(task);
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
Reference<IBackupContainer> bc;
std::vector<KeyRange> ranges; // this is the actual KV, not version
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Reference<IBackupContainer> _bc = co_await restore.sourceContainer().getOrThrow(tr);
bc = getBackupContainerWithProxy(_bc);
ranges = co_await restore.getRestoreRangesOrDefault(tr);
co_await checkTaskVersion(tr->getDatabase(), task, name, version);
co_await taskBucket->keepRunning(tr, task);
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
std::vector<std::vector<RestoreConfig::RestoreFile>> filesByTag(maxTagID + 1);
std::vector<std::vector<Version>> fileEndVersionByTag(maxTagID + 1);
for (RestoreConfig::RestoreFile& f : logs) {
// find the tag, aggregate files by tags
if (f.tagId == -1) {
// inconsistent data
TraceEvent(SevError, "PartitionedLogFileNoTag")
.detail("FileName", f.fileName)
.detail("FileSize", f.fileSize)
.log();
} else {
filesByTag[f.tagId].push_back(f);
}
}
for (int i = 0; i < maxTagID + 1; i++) {
std::vector<RestoreConfig::RestoreFile>& files = filesByTag[i];
int cnt = files.size();
fileEndVersionByTag[i].resize(cnt);
for (int j = 0; j < cnt; j++) {
// [10, 20), [18, 30) -> endVersion is (18, 30) for 2 files so we have [10, 18) and [18, 30)
// greedy algorithm, because sorted by beginVersion and to minimize duplicate reading
if (j != cnt - 1 && files[j].endVersion < files[j + 1].version) {
TraceEvent(SevError, "NonContinuousLog").detail("Tag", i).detail("Index", j).log();
}
fileEndVersionByTag[i][j] = (j == cnt - 1 ? end : files[j + 1].version);
}
}
std::vector<Reference<PartitionedLogIterator>> iterators(maxTagID + 1);
// for each tag, create an iterator
for (int k = 0; k < filesByTag.size(); k++) {
iterators[k] =
makeReference<PartitionedLogIteratorTwoBuffers>(bc, k, filesByTag[k], fileEndVersionByTag[k]);
}
DisabledTraceEvent("FileRestorePartitionedLogDataExeStart")
.detail("BeginVersion", begin)
.detail("EndVersion", end)
.detail("Files", logs.size());
// Converted backup mutations
PromiseStream<Standalone<VectorRef<KeyValueRef>>> mutationStream;
Future<Void> reader = readLogData(mutationStream, iterators, begin, end);
std::vector<Standalone<VectorRef<KeyValueRef>>> mutations;
int64_t totalBytes = 0;
Standalone<VectorRef<KeyValueRef>> oneVersionData;
while (true) {
Error err;
try {
Standalone<VectorRef<KeyValueRef>> _data = co_await mutationStream.getFuture();
oneVersionData = _data;
// batching mutations from multiple versions together before writing to the database
int64_t bytes = oneVersionData.expectedSize();
if (totalBytes + bytes > CLIENT_KNOBS->RESTORE_WRITE_TX_SIZE) {
co_await writeMutations(cx, mutations, restore.mutationLogPrefix(), task, taskBucket);
mutations.clear();
totalBytes = 0;
}
mutations.push_back(oneVersionData);
totalBytes += bytes;
continue;
} catch (Error& e) {
err = e;
}
if (err.code() == error_code_end_of_stream) {
if (!mutations.empty()) {
co_await writeMutations(cx, mutations, restore.mutationLogPrefix(), task, taskBucket);
}
break;
} else {
throw err;
}
}
DisabledTraceEvent("FileRestorePartitionedLogDataExeDone")
.detail("BeginVersion", begin)
.detail("EndVersion", end)
.detail("Files", logs.size());
co_return;
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
int64_t logBytesWritten = Params.bytesWritten().get(task);
RestoreConfig(task).bytesWritten().atomicOp(tr, logBytesWritten, MutationRef::Type::AddValue);
int64_t blocks =
(logBytesWritten + CLIENT_KNOBS->BACKUP_LOGFILE_BLOCK_SIZE - 1) / CLIENT_KNOBS->BACKUP_LOGFILE_BLOCK_SIZE;
// When dispatching, we don't know how many blocks are there, so we have to do it here
RestoreConfig(task).filesBlocksDispatched().atomicOp(tr, blocks, MutationRef::Type::AddValue);
RestoreConfig(task).fileBlocksFinished().atomicOp(tr, blocks, MutationRef::Type::AddValue);
DisabledTraceEvent("FileRestorePartitionedLogCommittedData")
.detail("Blocks", blocks)
.detail("LogBytes", logBytesWritten);
Reference<TaskFuture> taskFuture = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
co_await (taskFuture->set(tr, taskBucket) && taskBucket->finish(tr, task));
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
int64_t maxTagID,
std::vector<RestoreConfig::RestoreFile> logs,
Version begin,
Version end,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key doneKey = co_await completionKey.get(tr, taskBucket);
Reference<Task> task(
new Task(RestoreLogDataPartitionedTaskFunc::name, RestoreLogDataPartitionedTaskFunc::version, doneKey));
// Create a restore config from the current task and bind it to the new task.
// RestoreConfig(parentTask) createsa prefix of : fileRestorePrefixRange.begin/uid->config/[uid]
co_await RestoreConfig(parentTask).toTask(tr, task);
Params.maxTagID().set(task, maxTagID);
Params.beginVersion().set(task, begin);
Params.endVersion().set(task, end);
Params.logs().set(task, logs);
Params.bytesWritten().set(task, 0);
if (!waitFor) {
co_return taskBucket->addTask(tr, task);
}
co_await waitFor->onSetAddTask(tr, taskBucket, task);
co_return "OnSetAddTask"_sr;
}
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef RestoreLogDataPartitionedTaskFunc::name = "restore_log_data_partitioned"_sr;
REGISTER_TASKFUNC(RestoreLogDataPartitionedTaskFunc);
// each task can be partitioned to smaller ranges because commit proxy would
// only start to commit alog/ prefix mutations to original prefix when
// the final version is set, but do it in a single task for now for simplicity
struct RestoreDispatchPartitionedTaskFunc : RestoreTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
StringRef getName() const override { return name; };
static struct {
static TaskParam<Version> beginVersion() { return __FUNCTION__sr; }
static TaskParam<Version> firstVersion() { return __FUNCTION__sr; }
static TaskParam<Version> endVersion() { return __FUNCTION__sr; }
} Params;
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig restore(task);
Version beginVersion = Params.beginVersion().get(task);
Version firstVersion = Params.firstVersion().get(task);
Version endVersion = Params.endVersion().get(task);
Reference<IBackupContainer> _bc = co_await restore.sourceContainer().getOrThrow(tr);
Reference<IBackupContainer> bc = getBackupContainerWithProxy(_bc);
Reference<TaskFuture> onDone = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
Version restoreVersion{ 0 };
int fileLimit = 1000;
co_await (store(restoreVersion, restore.restoreVersion().getOrThrow(tr)) &&
checkTaskVersion(tr->getDatabase(), task, name, version));
// if current is [40, 50] and restore version is 50, we need another [50, 51] task to process data at version 50
Version nextEndVersion =
std::min(restoreVersion + 1, endVersion + CLIENT_KNOBS->RESTORE_PARTITIONED_BATCH_VERSION_SIZE);
// update the apply mutations end version so the mutations from the previous batch can be applied.
// Only do this once beginVersion is > 0 (it will be 0 for the initial dispatch).
if (beginVersion > firstVersion) {
// if the last file is [80, 100] and the restoreVersion is 90, we should use 90 here
// this is an additional taskFunc after last file
restore.setApplyEndVersion(tr, std::min(beginVersion, restoreVersion + 1));
}
// The applyLag must be retrieved AFTER potentially updating the apply end version.
int64_t applyLag = co_await restore.getApplyVersionLag(tr);
// this is to guarantee commit proxy is catching up doing apply alog -> normal key
// with this backupFile -> alog process
// If starting a new batch and the apply lag is too large then re-queue and wait
if (applyLag > (buggify() ? 1 : CLIENT_KNOBS->CORE_VERSIONSPERSECOND * 300)) {
// Wait a small amount of time and then re-add this same task.
co_await delay(FLOW_KNOBS->PREVENT_FAST_SPIN_DELAY);
co_await RestoreDispatchPartitionedTaskFunc::addTask(
tr, taskBucket, task, firstVersion, beginVersion, endVersion);
TraceEvent("FileRestorePartitionDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("ApplyLag", applyLag)
.detail("Decision", "too_far_behind");
co_await taskBucket->finish(tr, task);
co_return;
}
// Get a batch of files. We're targeting batchSize blocks(30k) being dispatched so query for batchSize(150)
// files (each of which is 0 or more blocks).
// lets say files have [10, 20), [20, 30) then if our range is [15, 25], we need to include both files,
// because [15, 20] is included in the first file, and [20, 25] is included in the second file
// say we have b and e
// as a result, the first file(inclusive): largest file whose begin <= b,
// the last file(exclusive): smallest file whose begin > e
// because of the encoding, version comes before the type of files(log or range),
// begin needs to start from the beginning --
// if we have a 3 files, (log,100,200), (range, 180), (log, 200, 300), then if we want to restore[190, 250]
// it would stop at (range, 180) and not looking at (log, 100, 200)
Optional<RestoreConfig::RestoreFile> beginLogInclude = Optional<RestoreConfig::RestoreFile>{};
// because of the encoding of RestoreFile, we use greaterThanOrEqual(end + 1) instead of greaterThan(end)
// because RestoreFile::pack has the version at the most significant position, and keyAfter(end) does not result
// in a end+1
Optional<RestoreConfig::RestoreFile> endLogExclude = co_await restore.logFileSet().seekGreaterOrEqual(
tr, RestoreConfig::RestoreFile({ endVersion + 1, "", false }));
RestoreConfig::FileSetT::RangeResultType logFiles =
co_await restore.logFileSet().getRange(tr, beginLogInclude, endLogExclude, fileLimit);
Optional<RestoreConfig::RestoreFile> beginRangeInclude = co_await restore.rangeFileSet().seekGreaterOrEqual(
tr, RestoreConfig::RestoreFile({ beginVersion, "", true }));
// greaterThanOrEqual(end + 1) instead of greaterThan(end)
// because RestoreFile::pack has the version at the most significant position, and keyAfter(end) does not result
// in a end+1
Optional<RestoreConfig::RestoreFile> endRangeExclude = co_await restore.rangeFileSet().seekGreaterOrEqual(
tr, RestoreConfig::RestoreFile({ endVersion + 1, "", true }));
RestoreConfig::FileSetT::RangeResultType rangeFiles =
co_await restore.rangeFileSet().getRange(tr, beginRangeInclude, endRangeExclude, fileLimit);
int64_t maxTagID = 0;
std::vector<RestoreConfig::RestoreFile> logs;
std::vector<RestoreConfig::RestoreFile> ranges;
for (const auto& f : logFiles.results) {
if (f.endVersion > beginVersion) {
// skip all files whose endVersion is smaller or equal to beginVersion
logs.push_back(f);
maxTagID = std::max(maxTagID, f.tagId);
}
}
for (const auto& f : rangeFiles.results) {
// the getRange might get out-of-bound range file because log files need them to work
if (f.version >= beginVersion && f.version < endVersion) {
ranges.push_back(f);
}
}
// allPartsDone will be set once all block tasks in the current batch are finished.
// create a new future for the new batch
Reference<TaskFuture> allPartsDone = futureBucket->future(tr);
restore.batchFuture().set(tr, allPartsDone->pack());
// if there are no files, if i am not the last batch, then on to the next batch
// if there are no files and i am the last batch, then just wait for applying to finish
// do we need this files.results.size() == 0 at all?
if (beginVersion > restoreVersion) {
if (applyLag == 0) {
// i am the last batch
// If apply lag is 0 then we are done so create the completion task
co_await RestoreCompleteTaskFunc::addTask(tr, taskBucket, task, TaskCompletionKey::noSignal());
TraceEvent("FileRestorePartitionDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("ApplyLag", applyLag)
.detail("Decision", "restore_complete");
} else {
// i am the last batch, and applyLag is not zero, then I will create another dummy task to wait
// for apply log to be zero, then it will go into the branch above.
// Applying of mutations is not yet finished so wait a small amount of time and then re-add this
// same task.
// this is only to create a dummy one wait for it to finish
co_await delay(FLOW_KNOBS->PREVENT_FAST_SPIN_DELAY);
co_await RestoreDispatchPartitionedTaskFunc::addTask(
tr, taskBucket, task, firstVersion, beginVersion, endVersion);
TraceEvent("FileRestorePartitionDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("ApplyLag", applyLag)
.detail("Decision", "apply_still_behind");
}
co_await taskBucket->finish(tr, task);
co_return;
}
// if we reach here, this batch is not empty(i.e. we have range and/or mutation files in this)
// Start moving through the file list and queuing up blocks. Only queue up to RESTORE_DISPATCH_ADDTASK_SIZE
// blocks per Dispatch task and target batchSize total per batch but a batch must end on a complete version
// boundary so exceed the limit if necessary to reach the end of a version of files.
std::vector<Future<Key>> addTaskFutures;
int i = 0;
// need to process all range files, keep using the same RestoreRangeTaskFunc as non-partitioned restore.
// this can be done first, because they are not overlap within a restore uid
// each task will read the file, restore those key to their original keys after clear that range
// also it will update the keyVersionMap[key -> versionFromRangeFile]
// by this time, corresponding mutation files within the same version range has not been applied yet
// because they are waiting for the singal of this RestoreDispatchPartitionedTaskFunc
// when log are being applied, they will compare version of key to the keyVersionMap updated by range file
// after each RestoreDispatchPartitionedTaskFunc, keyVersionMap will be clear if mutation version is larger.
for (; i < ranges.size(); ++i) {
RestoreConfig::RestoreFile& f = ranges[i];
// For each block of the file
for (int64_t j = 0; j < f.fileSize; j += f.blockSize) {
addTaskFutures.push_back(RestoreRangeTaskFunc::addTask(tr,
taskBucket,
task,
f,
j,
std::min<int64_t>(f.blockSize, f.fileSize - j),
TaskCompletionKey::joinWith(allPartsDone)));
}
}
addTaskFutures.push_back(RestoreLogDataPartitionedTaskFunc::addTask(
tr, taskBucket, task, maxTagID, logs, beginVersion, endVersion, TaskCompletionKey::joinWith(allPartsDone)));
// even if file exsists, but they are empty, in this case just start the next batch
addTaskFutures.push_back(RestoreDispatchPartitionedTaskFunc::addTask(tr,
taskBucket,
task,
firstVersion,
endVersion,
nextEndVersion,
TaskCompletionKey::noSignal(),
allPartsDone));
co_await waitForAll(addTaskFutures);
co_await taskBucket->finish(tr, task);
TraceEvent("FileRestorePartitionDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("EndVersion", endVersion)
.detail("NextEndVersion", nextEndVersion)
.detail("ApplyLag", applyLag)
.detail("Decision", "dispatch_batch_complete");
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
Version firstVersion,
Version beginVersion,
Version endVersion,
TaskCompletionKey completionKey = TaskCompletionKey::noSignal(),
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key doneKey = co_await completionKey.get(tr, taskBucket);
// Use high priority for dispatch tasks that have to queue more blocks for the current batch
unsigned int priority = 0;
Reference<Task> task(new Task(
RestoreDispatchPartitionedTaskFunc::name, RestoreDispatchPartitionedTaskFunc::version, doneKey, priority));
// Create a config from the parent task and bind it to the new task
co_await RestoreConfig(parentTask).toTask(tr, task);
Params.firstVersion().set(task, firstVersion);
Params.beginVersion().set(task, beginVersion);
Params.endVersion().set(task, endVersion);
if (!waitFor) {
co_return taskBucket->addTask(tr, task);
}
co_await waitFor->onSetAddTask(tr, taskBucket, task);
co_return "OnSetAddTask"_sr;
}
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return Void();
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef RestoreDispatchPartitionedTaskFunc::name = "restore_dispatch_partitioned"_sr;
REGISTER_TASKFUNC(RestoreDispatchPartitionedTaskFunc);
struct RestoreDispatchTaskFunc : RestoreTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
StringRef getName() const override { return name; };
static struct {
static TaskParam<Version> beginVersion() { return __FUNCTION__sr; }
static TaskParam<std::string> beginFile() { return __FUNCTION__sr; }
static TaskParam<int64_t> beginBlock() { return __FUNCTION__sr; }
static TaskParam<int64_t> batchSize() { return __FUNCTION__sr; }
static TaskParam<int64_t> remainingInBatch() { return __FUNCTION__sr; }
} Params;
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig restore(task);
Version beginVersion = Params.beginVersion().get(task);
Reference<TaskFuture> onDone = futureBucket->unpack(task->params[Task::reservedTaskParamKeyDone]);
int64_t remainingInBatch = Params.remainingInBatch().get(task);
bool addingToExistingBatch = remainingInBatch > 0;
Version restoreVersion{ 0 };
co_await (store(restoreVersion, restore.restoreVersion().getOrThrow(tr)) &&
checkTaskVersion(tr->getDatabase(), task, name, version));
// If not adding to an existing batch then update the apply mutations end version so the mutations from the
// previous batch can be applied. Only do this once beginVersion is > 0 (it will be 0 for the initial
// dispatch).
if (!addingToExistingBatch && beginVersion > 0) {
// unblock apply alog to normal key space
// if the last file is [80, 100] and the restoreVersion is 90, we should use 90 here
// this call an additional call after last file
restore.setApplyEndVersion(tr, std::min(beginVersion, restoreVersion + 1));
}
// The applyLag must be retrieved AFTER potentially updating the apply end version.
int64_t applyLag = co_await restore.getApplyVersionLag(tr);
int64_t batchSize = Params.batchSize().get(task);
// If starting a new batch and the apply lag is too large then re-queue and wait
if (!addingToExistingBatch && applyLag > (buggify() ? 1 : CLIENT_KNOBS->CORE_VERSIONSPERSECOND * 300)) {
// Wait a small amount of time and then re-add this same task.
co_await delay(FLOW_KNOBS->PREVENT_FAST_SPIN_DELAY);
co_await RestoreDispatchTaskFunc::addTask(
tr, taskBucket, task, beginVersion, "", 0, batchSize, remainingInBatch);
TraceEvent("FileRestoreDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("ApplyLag", applyLag)
.detail("BatchSize", batchSize)
.detail("Decision", "too_far_behind");
co_await taskBucket->finish(tr, task);
co_return;
}
// need beginFile to handle stop in the middle of version case
std::string beginFile = Params.beginFile().getOrDefault(task);
// Get a batch of files. We're targeting batchSize blocks being dispatched so query for batchSize files
// (each of which is 0 or more blocks).
int taskBatchSize = buggify() ? 1 : CLIENT_KNOBS->RESTORE_DISPATCH_ADDTASK_SIZE;
RestoreConfig::FileSetT::RangeResultType files = co_await restore.fileSet().getRange(
tr, Optional<RestoreConfig::RestoreFile>({ beginVersion, beginFile }), {}, taskBatchSize);
// allPartsDone will be set once all block tasks in the current batch are finished.
Reference<TaskFuture> allPartsDone;
// If adding to existing batch then join the new block tasks to the existing batch future
if (addingToExistingBatch) {
Key fKey = co_await restore.batchFuture().getD(tr);
allPartsDone = makeReference<TaskFuture>(futureBucket, fKey);
} else {
// Otherwise create a new future for the new batch
allPartsDone = futureBucket->future(tr);
restore.batchFuture().set(tr, allPartsDone->pack());
// Set batch quota remaining to batch size
remainingInBatch = batchSize;
}
// If there were no files to load then this batch is done and restore is almost done.
if (files.results.empty()) {
// If adding to existing batch then blocks could be in progress so create a new Dispatch task that waits
// for them to finish
if (addingToExistingBatch) {
// Setting next begin to restoreVersion + 1 so that any files in the file map at the restore version
// won't be dispatched again.
co_await RestoreDispatchTaskFunc::addTask(tr,
taskBucket,
task,
restoreVersion + 1,
"",
0,
batchSize,
0,
TaskCompletionKey::noSignal(),
allPartsDone);
TraceEvent("FileRestoreDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("BeginFile", Params.beginFile().get(task))
.detail("BeginBlock", Params.beginBlock().get(task))
.detail("RestoreVersion", restoreVersion)
.detail("ApplyLag", applyLag)
.detail("Decision", "end_of_final_batch");
} else if (beginVersion < restoreVersion) {
// If beginVersion is less than restoreVersion then do one more dispatch task to get there
// there are no more files between beginVersion and restoreVersion
co_await RestoreDispatchTaskFunc::addTask(tr, taskBucket, task, restoreVersion, "", 0, batchSize);
TraceEvent("FileRestoreDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("BeginFile", Params.beginFile().get(task))
.detail("BeginBlock", Params.beginBlock().get(task))
.detail("RestoreVersion", restoreVersion)
.detail("ApplyLag", applyLag)
.detail("Decision", "apply_to_restore_version");
} else if (applyLag == 0) {
// If apply lag is 0 then we are done so create the completion task
co_await RestoreCompleteTaskFunc::addTask(tr, taskBucket, task, TaskCompletionKey::noSignal());
TraceEvent("FileRestoreDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("BeginFile", Params.beginFile().get(task))
.detail("BeginBlock", Params.beginBlock().get(task))
.detail("ApplyLag", applyLag)
.detail("Decision", "restore_complete");
} else {
// Applying of mutations is not yet finished so wait a small amount of time and then re-add this
// same task.
// this is only to create a dummy one wait for it to finish
co_await delay(FLOW_KNOBS->PREVENT_FAST_SPIN_DELAY);
co_await RestoreDispatchTaskFunc::addTask(tr, taskBucket, task, beginVersion, "", 0, batchSize);
TraceEvent("FileRestoreDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("ApplyLag", applyLag)
.detail("Decision", "apply_still_behind");
}
// If adding to existing batch then task is joined with a batch future so set done future
// Note that this must be done after joining at least one task with the batch future in case all other
// blockers already finished.
Future<Void> setDone = addingToExistingBatch ? onDone->set(tr, taskBucket) : Void();
co_await (taskBucket->finish(tr, task) && setDone);
co_return;
}
// Start moving through the file list and queuing up blocks. Only queue up to RESTORE_DISPATCH_ADDTASK_SIZE
// blocks per Dispatch task and target batchSize total per batch but a batch must end on a complete version
// boundary so exceed the limit if necessary to reach the end of a version of files.
std::vector<Future<Key>> addTaskFutures;
Version endVersion = files.results[0].version;
int blocksDispatched = 0;
int64_t beginBlock = Params.beginBlock().getOrDefault(task);
int i = 0;
// for each file
// not creating a new task at this level because restore files are read back together -- both range and log
// so i have to process range files anyway.
for (; i < files.results.size(); ++i) {
RestoreConfig::RestoreFile& f = files.results[i];
// Here we are "between versions" (prior to adding the first block of the first file of a new version)
// so this is an opportunity to end the current dispatch batch (which must end on a version boundary) if
// the batch size has been reached or exceeded
if (f.version != endVersion && remainingInBatch <= 0) {
// Next start will be at the first version after endVersion at the first file first block
++endVersion;
// beginFile set to empty to indicate we are not in the middle of a range
// by middle of a range, we mean that we have rangeFile v=80, and logFile v=[80, 100],
// then we have to include this log file too in this batch
beginFile = "";
beginBlock = 0;
break;
}
// Set the starting point for the next task in case we stop inside this file
endVersion = f.version;
beginFile = f.fileName;
int64_t j = beginBlock * f.blockSize;
// For each block of the file
for (; j < f.fileSize; j += f.blockSize) {
// Stop if we've reached the addtask limit
if (blocksDispatched == taskBatchSize)
break;
if (f.isRange) {
addTaskFutures.push_back(
RestoreRangeTaskFunc::addTask(tr,
taskBucket,
task,
f,
j,
std::min<int64_t>(f.blockSize, f.fileSize - j),
TaskCompletionKey::joinWith(allPartsDone)));
} else {
addTaskFutures.push_back(
RestoreLogDataTaskFunc::addTask(tr,
taskBucket,
task,
f,
j,
std::min<int64_t>(f.blockSize, f.fileSize - j),
TaskCompletionKey::joinWith(allPartsDone)));
}
// Increment beginBlock for the file and total blocks dispatched for this task
++beginBlock;
++blocksDispatched;
--remainingInBatch;
}
// Stop if we've reached the addtask limit
if (blocksDispatched == taskBatchSize)
break;
// We just completed an entire file so the next task should start at the file after this one within
// endVersion (or later) if this iteration ends up being the last for this task
beginFile = beginFile + '\x00';
beginBlock = 0;
TraceEvent("FileRestoreDispatchedFile")
.suppressFor(60)
.detail("RestoreUID", restore.getUid())
.detail("FileName", f.fileName);
}
// If no blocks were dispatched then the next dispatch task should run now and be joined with the
// allPartsDone future
if (blocksDispatched == 0) {
std::string decision;
// If no files were dispatched either then the batch size wasn't large enough to catch all of the files
// at the next lowest non-dispatched version, so increase the batch size.
if (i == 0) {
batchSize *= 2;
decision = "increased_batch_size";
} else {
decision = "all_files_were_empty";
}
TraceEvent("FileRestoreDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("BeginFile", Params.beginFile().get(task))
.detail("BeginBlock", Params.beginBlock().get(task))
.detail("EndVersion", endVersion)
.detail("ApplyLag", applyLag)
.detail("BatchSize", batchSize)
.detail("Decision", decision)
.detail("RemainingInBatch", remainingInBatch);
co_await RestoreDispatchTaskFunc::addTask(tr,
taskBucket,
task,
endVersion,
beginFile,
beginBlock,
batchSize,
remainingInBatch,
TaskCompletionKey::joinWith((allPartsDone)));
// If adding to existing batch then task is joined with a batch future so set done future.
// Note that this must be done after joining at least one task with the batch future in case all other
// blockers already finished.
Future<Void> setDone = addingToExistingBatch ? onDone->set(tr, taskBucket) : Void();
co_await (setDone && taskBucket->finish(tr, task));
co_return;
}
// Increment the number of blocks dispatched in the restore config
restore.filesBlocksDispatched().atomicOp(tr, blocksDispatched, MutationRef::Type::AddValue);
// If beginFile is not empty then we had to stop in the middle of a version (possibly within a file) so we
// cannot end the batch here because we do not know if we got all of the files and blocks from the last
// version queued, so make sure remainingInBatch is at least 1.
if (!beginFile.empty()) {
// this is to make sure if we stop in the middle of a version, we do not end this batch
// instead next RestoreDispatchTaskFunc should have addingToExistingBatch as true
// thus they are considered the same batch and alog will be committed only when all of them succeed
remainingInBatch = std::max<int64_t>(1, remainingInBatch);
}
// If more blocks need to be dispatched in this batch then add a follow-on task that is part of the
// allPartsDone group which will won't wait to run and will add more block tasks.
if (remainingInBatch > 0) {
addTaskFutures.push_back(RestoreDispatchTaskFunc::addTask(tr,
taskBucket,
task,
endVersion,
beginFile,
beginBlock,
batchSize,
remainingInBatch,
TaskCompletionKey::joinWith(allPartsDone)));
} else { // Otherwise, add a follow-on task to continue after all previously dispatched blocks are done
addTaskFutures.push_back(RestoreDispatchTaskFunc::addTask(tr,
taskBucket,
task,
endVersion,
beginFile,
beginBlock,
batchSize,
0,
TaskCompletionKey::noSignal(),
allPartsDone));
}
co_await waitForAll(addTaskFutures);
// If adding to existing batch then task is joined with a batch future so set done future.
Future<Void> setDone = addingToExistingBatch ? onDone->set(tr, taskBucket) : Void();
co_await (setDone && taskBucket->finish(tr, task));
TraceEvent("FileRestoreDispatch")
.detail("RestoreUID", restore.getUid())
.detail("BeginVersion", beginVersion)
.detail("BeginFile", Params.beginFile().get(task))
.detail("BeginBlock", Params.beginBlock().get(task))
.detail("EndVersion", endVersion)
.detail("ApplyLag", applyLag)
.detail("BatchSize", batchSize)
.detail("Decision", "dispatched_files")
.detail("FilesDispatched", i)
.detail("BlocksDispatched", blocksDispatched)
.detail("RemainingInBatch", remainingInBatch);
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<Task> parentTask,
Version beginVersion,
std::string beginFile,
int64_t beginBlock,
int64_t batchSize,
int64_t remainingInBatch = 0,
TaskCompletionKey completionKey = TaskCompletionKey::noSignal(),
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
Key doneKey = co_await completionKey.get(tr, taskBucket);
// Use high priority for dispatch tasks that have to queue more blocks for the current batch
auto priority = (remainingInBatch > 0) ? 1u : 0u;
Reference<Task> task(
new Task(RestoreDispatchTaskFunc::name, RestoreDispatchTaskFunc::version, doneKey, priority));
// Create a config from the parent task and bind it to the new task
co_await RestoreConfig(parentTask).toTask(tr, task);
Params.beginVersion().set(task, beginVersion);
Params.batchSize().set(task, batchSize);
Params.remainingInBatch().set(task, remainingInBatch);
Params.beginBlock().set(task, beginBlock);
Params.beginFile().set(task, beginFile);
if (!waitFor) {
co_return taskBucket->addTask(tr, task);
}
co_await waitFor->onSetAddTask(tr, taskBucket, task);
co_return "OnSetAddTask"_sr;
}
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return Void();
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef RestoreDispatchTaskFunc::name = "restore_dispatch"_sr;
REGISTER_TASKFUNC(RestoreDispatchTaskFunc);
Future<std::string> restoreStatus(Reference<ReadYourWritesTransaction> tr, Key tagName) {
tr->setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
std::vector<KeyBackedTag> tags;
if (tagName.empty()) {
std::vector<KeyBackedTag> t = co_await getAllRestoreTags(tr);
tags = t;
} else {
tags.push_back(makeRestoreTag(tagName.toString()));
}
// If no tags found, return helpful message
if (tags.empty()) {
co_return "No restores found.\n\n"
"To start a restore:\n"
" fdbrestore start -r <BACKUP_URL> [-t <TAG>]\n";
}
std::string result;
for (int i = 0; i < tags.size(); ++i) {
UidAndAbortedFlagT u = co_await tags[i].getD(tr);
std::string s = co_await RestoreConfig(u.first).getFullStatus(tr);
result.append(s);
result.append("\n\n");
}
co_return result;
}
Future<ERestoreState> abortRestore(Reference<ReadYourWritesTransaction> tr, Key tagName) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
tr->setOption(FDBTransactionOptions::COMMIT_ON_FIRST_PROXY);
KeyBackedTag tag = makeRestoreTag(tagName.toString());
Optional<UidAndAbortedFlagT> current = co_await tag.get(tr);
if (!current.present())
co_return ERestoreState::UNINITIALIZED;
RestoreConfig restore(current.get().first);
ERestoreState status = co_await restore.stateEnum().getD(tr);
bool runnable = co_await restore.isRunnable(tr);
if (!runnable)
co_return status;
restore.stateEnum().set(tr, ERestoreState::ABORTED);
// Clear all of the ApplyMutations stuff
restore.clearApplyMutationsKeys(tr);
// Cancel the backup tasks on this tag
co_await tag.cancel(tr);
co_await unlockDatabase(tr, current.get().first);
co_return ERestoreState::ABORTED;
}
Future<ERestoreState> abortRestore(Database cx, Key tagName) {
auto tr = makeReference<ReadYourWritesTransaction>(cx);
while (true) {
Error err;
try {
ERestoreState estate = co_await abortRestore(tr, tagName);
if (estate != ERestoreState::ABORTED) {
co_return estate;
}
co_await tr->commit();
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
tr = makeReference<ReadYourWritesTransaction>(cx);
// Commit a dummy transaction before returning success, to ensure the mutation applier has stopped submitting
// mutations
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
tr->setOption(FDBTransactionOptions::COMMIT_ON_FIRST_PROXY);
tr->addReadConflictRange(singleKeyRange(KeyRef()));
tr->addWriteConflictRange(singleKeyRange(KeyRef()));
co_await tr->commit();
co_return ERestoreState::ABORTED;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
}
struct StartFullRestoreTaskFunc : RestoreTaskFuncBase {
static StringRef name;
static constexpr uint32_t version = 1;
static struct {
static TaskParam<Version> firstVersion() { return __FUNCTION__sr; }
} Params;
// Find all files needed for the restore and save them in the RestoreConfig for the task.
// Update the total number of files and blocks and change state to starting.
static Future<Void> _execute(Database cx,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
RestoreConfig restore(task);
Version restoreVersion{ 0 };
Version beginVersion{ 0 };
Reference<IBackupContainer> bc;
std::vector<KeyRange> ranges;
bool logsOnly{ false };
bool inconsistentSnapshotOnly{ false };
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
co_await checkTaskVersion(tr->getDatabase(), task, name, version);
beginVersion = co_await restore.beginVersion().getD(tr, Snapshot::False, ::invalidVersion);
restoreVersion = co_await restore.restoreVersion().getOrThrow(tr);
ranges = co_await restore.getRestoreRangesOrDefault(tr);
logsOnly = co_await restore.onlyApplyMutationLogs().getD(tr, Snapshot::False, false);
inconsistentSnapshotOnly = co_await restore.inconsistentSnapshotOnly().getD(tr, Snapshot::False, false);
co_await taskBucket->keepRunning(tr, task);
ERestoreState oldState = co_await restore.stateEnum().getD(tr);
if (oldState != ERestoreState::QUEUED && oldState != ERestoreState::STARTING) {
co_await restore.logError(cx,
restore_error(),
format("StartFullRestore: Encountered unexpected state(%d)", oldState),
nullptr);
co_return;
}
restore.stateEnum().set(tr, ERestoreState::STARTING);
restore.fileSet().clear(tr);
restore.fileBlockCount().clear(tr);
restore.fileCount().clear(tr);
Reference<IBackupContainer> _bc = co_await restore.sourceContainer().getOrThrow(tr);
bc = getBackupContainerWithProxy(_bc);
co_await tr->commit();
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
tr->reset();
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Version destVersion = co_await tr->getReadVersion();
TraceEvent("FileRestoreVersionUpgrade")
.detail("RestoreVersion", restoreVersion)
.detail("Dest", destVersion);
if (destVersion <= restoreVersion) {
CODE_PROBE(true, "Forcing restored cluster to higher version");
tr->set(minRequiredCommitVersionKey, BinaryWriter::toValue(restoreVersion + 1, Unversioned()));
co_await tr->commit();
tr->reset();
continue;
} else {
break;
}
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
Version firstConsistentVersion = invalidVersion;
if (beginVersion == invalidVersion) {
beginVersion = 0;
}
Standalone<VectorRef<KeyRangeRef>> keyRangesFilter;
for (auto const& r : ranges) {
keyRangesFilter.push_back_deep(keyRangesFilter.arena(), KeyRangeRef(r));
}
Optional<RestorableFileSet> restorable =
co_await bc->getRestoreSet(restoreVersion, keyRangesFilter, logsOnly, beginVersion);
if (!restorable.present())
throw restore_missing_data();
// Convert the two lists in restorable (logs and ranges) to a single list of RestoreFiles.
// Order does not matter, they will be put in order when written to the restoreFileMap below.
std::vector<RestoreConfig::RestoreFile> files;
std::vector<RestoreConfig::RestoreFile> logFiles;
std::vector<RestoreConfig::RestoreFile> rangeFiles;
if (!logsOnly) {
beginVersion = restorable.get().snapshot.beginVersion;
if (!inconsistentSnapshotOnly) {
for (const RangeFile& f : restorable.get().ranges) {
files.push_back({ f.version, f.fileName, true, f.blockSize, f.fileSize });
rangeFiles.push_back({ f.version, f.fileName, true, f.blockSize, f.fileSize });
// In a restore with both snapshots and logs, the firstConsistentVersion is the highest version
// of any range file.
firstConsistentVersion = std::max(firstConsistentVersion, f.version);
}
} else {
for (int i = 0; i < restorable.get().ranges.size(); ++i) {
const RangeFile& f = restorable.get().ranges[i];
files.push_back({ f.version, f.fileName, true, f.blockSize, f.fileSize });
rangeFiles.push_back({ f.version, f.fileName, true, f.blockSize, f.fileSize });
// In inconsistentSnapshotOnly mode, if all range files have the same version, then it is the
// firstConsistentVersion, otherwise unknown (use -1).
if (i != 0 && f.version != firstConsistentVersion) {
firstConsistentVersion = invalidVersion;
} else {
firstConsistentVersion = f.version;
}
}
}
} else {
// In logs-only (incremental) mode, the firstConsistentVersion should just be restore.beginVersion().
firstConsistentVersion = beginVersion;
}
if (!inconsistentSnapshotOnly) {
for (const LogFile& f : restorable.get().logs) {
files.push_back(
{ f.beginVersion, f.fileName, false, f.blockSize, f.fileSize, f.endVersion, f.tagId, f.totalTags });
logFiles.push_back(
{ f.beginVersion, f.fileName, false, f.blockSize, f.fileSize, f.endVersion, f.tagId, f.totalTags });
}
}
// First version for which log data should be applied
Params.firstVersion().set(task, beginVersion);
tr->reset();
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
restore.firstConsistentVersion().set(tr, firstConsistentVersion);
co_await tr->commit();
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
// add log files
auto logStart = logFiles.begin();
auto logEnd = logFiles.end();
int txBytes = 0;
tr->reset();
while (logStart != logEnd) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
co_await taskBucket->keepRunning(tr, task);
auto logIt = logStart;
txBytes = 0;
int logFileCount = 0;
auto fileSet = restore.logFileSet();
// TODO: split files into multiple keys, because files can be in the order of 10k or 100k, which
// probably can't fit due to size limit for a value in FDB. as a result, fileSet has everything,
// including [beginVersion, endVersion] for each tag
for (; logIt != logEnd && txBytes < 1e6; ++logIt) {
txBytes += fileSet.insert(tr, *logIt);
++logFileCount;
}
co_await tr->commit();
TraceEvent("FileRestoreLoadedLogFiles")
.detail("RestoreUID", restore.getUid())
.detail("FileCount", logFileCount)
.detail("TransactionBytes", txBytes);
logStart = logIt;
tr->reset();
continue;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
auto rangeStart = rangeFiles.begin();
auto rangeEnd = rangeFiles.end();
tr->reset();
while (rangeStart != rangeEnd) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
co_await taskBucket->keepRunning(tr, task);
auto rangeIt = rangeStart;
txBytes = 0;
int rangeFileCount = 0;
auto fileSet = restore.rangeFileSet();
// as a result, fileSet has everything, including [beginVersion, endVersion] for each tag
for (; rangeIt != rangeEnd && txBytes < 1e6; ++rangeIt) {
txBytes += fileSet.insert(tr, *rangeIt);
// handle the remaining
++rangeFileCount;
}
co_await tr->commit();
TraceEvent("FileRestoreLoadedRangeFiles")
.detail("RestoreUID", restore.getUid())
.detail("FileCount", rangeFileCount)
.detail("TransactionBytes", txBytes);
rangeStart = rangeIt;
tr->reset();
continue;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
// add files
auto start = files.begin();
auto end = files.end();
tr->reset();
while (start != end) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
co_await taskBucket->keepRunning(tr, task);
auto it = start;
txBytes = 0;
int nFileBlocks = 0;
int nFiles = 0;
auto fileSet = restore.fileSet();
// as a result, fileSet has everything, including [beginVersion, endVersion] for each tag
for (; it != end && txBytes < 1e6; ++it) {
txBytes += fileSet.insert(tr, *it);
// handle the remaining
nFileBlocks += (it->fileSize + it->blockSize - 1) / it->blockSize;
++nFiles;
}
restore.fileCount().atomicOp(tr, nFiles, MutationRef::Type::AddValue);
restore.fileBlockCount().atomicOp(tr, nFileBlocks, MutationRef::Type::AddValue);
co_await tr->commit();
TraceEvent("FileRestoreLoadedFiles")
.detail("RestoreUID", restore.getUid())
.detail("FileCount", nFiles)
.detail("FileBlockCount", nFileBlocks)
.detail("TransactionBytes", txBytes);
start = it;
tr->reset();
continue;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
co_return;
}
static Future<Void> _finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
Reference<FutureBucket> futureBucket,
Reference<Task> task) {
RestoreConfig restore(task);
MutationLogType mutationLogType{ MutationLogType::DEFAULT };
Version restoreVersion{ 0 };
Version firstVersion = Params.firstVersion().getOrDefault(task, invalidVersion);
bool useRangeFileRestore = false;
if (firstVersion == invalidVersion) {
co_await restore.logError(
tr->getDatabase(), restore_missing_data(), "StartFullRestore: The backup had no data.", nullptr);
std::string tag = co_await restore.tag().getD(tr);
co_await abortRestore(tr, StringRef(tag));
co_return;
}
restore.stateEnum().set(tr, ERestoreState::RUNNING);
// Check if using traditional rangefile restore instead of BulkLoad
// Default to true (traditional rangefile restore) for backward compatibility
Optional<bool> rangeFileRestore = co_await restore.useRangeFileRestore().get(tr);
useRangeFileRestore = !rangeFileRestore.present() || rangeFileRestore.get();
// Set applyMutation versions
restore.setApplyBeginVersion(tr, firstVersion);
restore.setApplyEndVersion(tr, firstVersion);
// Apply range data using either BulkLoad or traditional range file restore
mutationLogType = co_await restore.mutationLogType().getD(tr, Snapshot::False, MutationLogType::DEFAULT);
restoreVersion = co_await restore.restoreVersion().getOrThrow(tr);
if (!useRangeFileRestore) {
// Use BulkLoad for range data restoration, then apply logs
Reference<IBackupContainer> bc = co_await restore.sourceContainer().getOrThrow(tr);
// Get the BulkDump job ID - this should have been stored by the backup
// when it completed the BulkDump phase
Optional<std::string> bulkDumpJobIdOpt = co_await restore.bulkDumpJobId().get(tr);
std::string bulkDumpJobId = bulkDumpJobIdOpt.present() ? bulkDumpJobIdOpt.get() : "";
// Save original BulkLoad mode BEFORE creating the task, so it's persisted in the database.
// This allows crash recovery to restore the correct mode even if the task restarts.
int currentBulkLoadMode = co_await getBulkLoadMode(tr->getDatabase());
restore.originalBulkLoadMode().set(tr, currentBulkLoadMode);
TraceEvent("StartFullRestoreUsingBulkLoad")
.detail("RestoreUID", restore.getUid())
.detail("BackupUrl", bc->getURL())
.detail("BulkDumpJobId", bulkDumpJobId)
.detail("RestoreVersion", restoreVersion);
// Create a future that BulkLoad will signal when done
Reference<TaskFuture> bulkLoadDone = futureBucket->future(tr);
// Add BulkLoad task for range data
co_await BulkLoadRestoreTaskFunc::addTask(tr,
taskBucket,
task,
restoreVersion,
bc->getURL(),
bulkDumpJobId,
TaskCompletionKey::signal(bulkLoadDone));
// After BulkLoad completes, run RestoreDispatch to apply mutation logs
// Set onlyApplyMutationLogs so it only processes logs, not range files
restore.onlyApplyMutationLogs().set(tr, true);
// Add RestoreDispatch task that waits for BulkLoad to complete
co_await RestoreDispatchTaskFunc::addTask(tr,
taskBucket,
task,
0,
"",
0,
CLIENT_KNOBS->RESTORE_DISPATCH_BATCH_SIZE,
0,
TaskCompletionKey::noSignal(),
bulkLoadDone);
} else if (mutationLogType == MutationLogType::PARTITIONED_LOG) {
// Traditional restore with partitioned logs
Version endVersion =
std::min(firstVersion + CLIENT_KNOBS->RESTORE_PARTITIONED_BATCH_VERSION_SIZE, restoreVersion);
co_await RestoreDispatchPartitionedTaskFunc::addTask(
tr, taskBucket, task, firstVersion, firstVersion, endVersion);
} else {
// Traditional restore with non-partitioned logs
co_await RestoreDispatchTaskFunc::addTask(
tr, taskBucket, task, 0, "", 0, CLIENT_KNOBS->RESTORE_DISPATCH_BATCH_SIZE);
}
// Initialize apply mutations map.
Future<Optional<bool>> logsOnly = restore.onlyApplyMutationLogs().get(tr);
co_await logsOnly;
if (logsOnly.get().present() && logsOnly.get().get()) {
// If this is an incremental restore, we need to set the applyMutationsMapPrefix
// to the earliest log version so no mutations are missed
Value versionEncoded = BinaryWriter::toValue(Params.firstVersion().get(task), Unversioned());
co_await krmSetRange(tr, restore.applyMutationsMapPrefix(), normalKeys, versionEncoded);
}
co_await taskBucket->finish(tr, task);
co_return;
}
static Future<Key> addTask(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> taskBucket,
UID uid,
TaskCompletionKey completionKey,
Reference<TaskFuture> waitFor = Reference<TaskFuture>()) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Key doneKey = co_await completionKey.get(tr, taskBucket);
Reference<Task> task(new Task(StartFullRestoreTaskFunc::name, StartFullRestoreTaskFunc::version, doneKey));
RestoreConfig restore(uid);
// Bind the restore config to the new task
co_await restore.toTask(tr, task);
if (!waitFor) {
co_return taskBucket->addTask(tr, task);
}
co_await waitFor->onSetAddTask(tr, taskBucket, task);
co_return "OnSetAddTask"_sr;
}
StringRef getName() const override { return name; };
Future<Void> execute(Database cx,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _execute(cx, tb, fb, task);
};
Future<Void> finish(Reference<ReadYourWritesTransaction> tr,
Reference<TaskBucket> tb,
Reference<FutureBucket> fb,
Reference<Task> task) override {
return _finish(tr, tb, fb, task);
};
};
StringRef StartFullRestoreTaskFunc::name = "restore_start"_sr;
REGISTER_TASKFUNC(StartFullRestoreTaskFunc);
} // namespace fileBackup
struct LogInfo : public ReferenceCounted<LogInfo> {
std::string fileName;
Reference<IAsyncFile> logFile;
Version beginVersion;
Version endVersion;
int64_t offset;
LogInfo() : offset(0) {}
};
class FileBackupAgentImpl {
public:
// This method will return the final status of the backup at tag, and return the URL that was used on the tag
// when that status value was read.
static Future<EBackupState> waitBackup(FileBackupAgent* backupAgent,
Database cx,
std::string tagName,
StopWhenDone stopWhenDone,
Reference<IBackupContainer>* pContainer = nullptr,
UID* pUID = nullptr) {
std::string backTrace;
KeyBackedTag tag = makeBackupTag(tagName);
while (true) {
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Error err;
try {
Optional<UidAndAbortedFlagT> oldUidAndAborted = co_await tag.get(tr);
if (!oldUidAndAborted.present()) {
co_return EBackupState::STATE_NEVERRAN;
}
BackupConfig config(oldUidAndAborted.get().first);
EBackupState status =
co_await config.stateEnum().getD(tr, Snapshot::False, EBackupState::STATE_NEVERRAN);
// Break, if one of the following is true
// - no longer runnable
// - in differential mode (restorable) and stopWhenDone is not enabled
if (!FileBackupAgent::isRunnable(status) ||
((!stopWhenDone) && (EBackupState::STATE_RUNNING_DIFFERENTIAL == status))) {
if (pContainer != nullptr) {
Reference<IBackupContainer> c =
co_await config.backupContainer().getOrThrow(tr, Snapshot::False, backup_invalid_info());
*pContainer = fileBackup::getBackupContainerWithProxy(c);
}
if (pUID != nullptr) {
*pUID = oldUidAndAborted.get().first;
}
co_return status;
}
Future<Void> watchFuture = tr->watch(config.stateEnum().key);
co_await tr->commit();
co_await watchFuture;
continue;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
}
static Future<Void> submitBackup(FileBackupAgent* backupAgent,
Reference<ReadYourWritesTransaction> tr,
Key outContainer,
Optional<std::string> proxy,
int initialSnapshotIntervalSeconds,
int snapshotIntervalSeconds,
std::string tagName,
Standalone<VectorRef<KeyRangeRef>> backupRanges,
StopWhenDone stopWhenDone,
MutationLogType mutationLogType,
IncrementalBackupOnly incrementalBackupOnly,
Optional<std::string> encryptionKeyFileName,
int encryptionBlockSize,
int snapshotMode) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
tr->setOption(FDBTransactionOptions::COMMIT_ON_FIRST_PROXY);
TraceEvent(SevInfo, "FBA_SubmitBackup")
.detail("TagName", tagName.c_str())
.detail("StopWhenDone", stopWhenDone)
.detail("MutationLogType", mutationLogType)
.detail("OutContainer", outContainer.toString())
.detail("EncryptionKeyFileName", encryptionKeyFileName.present() ? encryptionKeyFileName.get() : "None")
.detail("EncryptionBlockSize", encryptionBlockSize);
KeyBackedTag tag = makeBackupTag(tagName);
Optional<UidAndAbortedFlagT> uidAndAbortedFlag = co_await tag.get(tr);
if (uidAndAbortedFlag.present()) {
BackupConfig prevConfig(uidAndAbortedFlag.get().first);
EBackupState prevBackupStatus =
co_await prevConfig.stateEnum().getD(tr, Snapshot::False, EBackupState::STATE_NEVERRAN);
if (FileBackupAgent::isRunnable(prevBackupStatus)) {
throw backup_duplicate();
}
// Now is time to clear prev backup config space. We have no more use for it.
prevConfig.clear(tr);
}
// PARTITIONED_LOG and RANGE_PARTITIONED_LOG backups are mutually exclusive: backup worker
// recruitment is determined by the active non-default type, so both cannot run concurrently.
if (mutationLogType == MutationLogType::PARTITIONED_LOG) {
if (co_await anyRangePartitionedBackupRunning(tr)) {
TraceEvent(SevError, "FBA_SubmitBackupMutationLogTypeConflict")
.detail("TagName", tagName)
.detail("RequestedType", mutationLogTypeToString(MutationLogType::PARTITIONED_LOG))
.detail("ConflictingType", mutationLogTypeToString(MutationLogType::RANGE_PARTITIONED_LOG));
fprintf(stderr,
"ERROR: Cannot start a backup with mutation-log-type `partitioned-log' while a "
"range-partitioned-log backup is running.\n");
throw backup_error();
}
} else if (mutationLogType == MutationLogType::RANGE_PARTITIONED_LOG) {
if (co_await anyPartitionedBackupRunning(tr)) {
TraceEvent(SevError, "FBA_SubmitBackupMutationLogTypeConflict")
.detail("TagName", tagName)
.detail("RequestedType", mutationLogTypeToString(MutationLogType::RANGE_PARTITIONED_LOG))
.detail("ConflictingType", mutationLogTypeToString(MutationLogType::PARTITIONED_LOG));
fprintf(stderr,
"ERROR: Cannot start a backup with mutation-log-type `range-partitioned-log' while a "
"partitioned-log backup is running.\n");
throw backup_error();
}
}
BackupConfig config(deterministicRandom()->randomUniqueID());
UID uid = config.getUid();
// This check will ensure that current backupUid is later than the last backup Uid
Standalone<StringRef> nowStr = BackupAgentBase::getCurrentTime();
std::string backupContainer = outContainer.toString();
// To be consistent with directory handling behavior since FDB backup was first released, if the container
// string describes a local directory then "/backup-<timestamp>" will be added to it.
if (backupContainer.find("file://") == 0) {
backupContainer = joinPath(backupContainer, std::string("backup-") + nowStr.toString());
}
Reference<IBackupContainer> bc =
IBackupContainer::openContainer(backupContainer, proxy, encryptionKeyFileName, encryptionBlockSize);
try {
// Use longer timeout for blobstore:// URLs in simulation to handle slow S3 mock operations
double createTimeout = (g_network->isSimulated() && isBlobstoreUrl(backupContainer)) ? 300.0 : 30.0;
co_await timeoutError(bc->create(), createTimeout);
} catch (Error& e) {
if (e.code() == error_code_actor_cancelled)
throw;
fprintf(stderr, "ERROR: Could not create backup container: %s\n", e.what());
throw backup_error();
}
Optional<Value> lastBackupTimestamp = co_await backupAgent->lastBackupTimestamp().get(tr);
if ((lastBackupTimestamp.present()) && (lastBackupTimestamp.get() >= nowStr)) {
fprintf(stderr,
"ERROR: The last backup `%s' happened in the future.\n",
printable(lastBackupTimestamp.get()).c_str());
throw backup_error();
}
KeyRangeMap<int> backupRangeSet;
for (auto& backupRange : backupRanges) {
backupRangeSet.insert(backupRange, 1);
}
backupRangeSet.coalesce(allKeys);
std::vector<KeyRange> normalizedRanges;
for (auto& backupRange : backupRangeSet.ranges()) {
if (backupRange.value()) {
normalizedRanges.push_back(KeyRange(KeyRangeRef(backupRange.range().begin, backupRange.range().end)));
}
}
config.clear(tr);
Key destUidValue(BinaryWriter::toValue(uid, Unversioned()));
if (normalizedRanges.size() == 1 || isDefaultBackup(normalizedRanges)) {
RangeResult existingDestUidValues = co_await tr->getRange(
KeyRangeRef(destUidLookupPrefix, strinc(destUidLookupPrefix)), CLIENT_KNOBS->TOO_MANY);
bool found = false;
KeyRangeRef targetRange =
normalizedRanges.size() == 1 ? normalizedRanges[0] : getDefaultBackupSharedRange();
for (auto it : existingDestUidValues) {
KeyRange uidRange =
BinaryReader::fromStringRef<KeyRange>(it.key.removePrefix(destUidLookupPrefix), IncludeVersion());
if (uidRange == targetRange) {
destUidValue = it.value;
found = true;
CODE_PROBE(isDefaultBackup(normalizedRanges), "Backup mutation sharing with default backup");
break;
}
}
if (!found) {
destUidValue = BinaryWriter::toValue(deterministicRandom()->randomUniqueID(), Unversioned());
tr->set(BinaryWriter::toValue(targetRange, IncludeVersion(ProtocolVersion::withSharedMutations()))
.withPrefix(destUidLookupPrefix),
destUidValue);
}
}
tr->set(config.getUidAsKey().withPrefix(destUidValue).withPrefix(backupLatestVersionsPrefix),
BinaryWriter::toValue<Version>(tr->getReadVersion().get(), Unversioned()));
config.destUidValue().set(tr, destUidValue);
// Point the tag to this new uid
tag.set(tr, { uid, false });
backupAgent->lastBackupTimestamp().set(tr, nowStr);
// Set the backup keys
config.tag().set(tr, tagName);
config.stateEnum().set(tr, EBackupState::STATE_SUBMITTED);
config.backupContainer().set(tr, bc);
config.stopWhenDone().set(tr, stopWhenDone);
config.backupRanges().set(tr, normalizedRanges);
config.initialSnapshotIntervalSeconds().set(tr, initialSnapshotIntervalSeconds);
config.snapshotIntervalSeconds().set(tr, snapshotIntervalSeconds);
config.mutationLogType().set(tr, mutationLogType);
config.incrementalBackupOnly().set(tr, incrementalBackupOnly);
config.snapshotMode().set(tr, snapshotMode);
Key taskKey = co_await fileBackup::StartFullBackupTaskFunc::addTask(
tr, backupAgent->taskBucket, uid, TaskCompletionKey::noSignal());
co_return;
}
static Future<Void> submitRestore(FileBackupAgent* backupAgent,
Reference<ReadYourWritesTransaction> tr,
Key tagName,
Key backupURL,
Optional<std::string> proxy,
Standalone<VectorRef<KeyRangeRef>> ranges,
Version restoreVersion,
Key addPrefix,
Key removePrefix,
LockDB lockDB,
UnlockDB unlockDB,
OnlyApplyMutationLogs onlyApplyMutationLogs,
InconsistentSnapshotOnly inconsistentSnapshotOnly,
Version beginVersion,
UID uid,
MutationLogType mutationLogType,
Optional<std::string> encryptionKeyFileName,
int encryptionBlockSize,
bool useRangeFileRestore = true) {
KeyRangeMap<int> restoreRangeSet;
for (auto& range : ranges) {
restoreRangeSet.insert(range, 1);
}
restoreRangeSet.coalesce(allKeys);
std::vector<KeyRange> restoreRanges;
for (auto& restoreRange : restoreRangeSet.ranges()) {
if (restoreRange.value()) {
restoreRanges.push_back(KeyRange(KeyRangeRef(restoreRange.range().begin, restoreRange.range().end)));
}
}
for (auto& restoreRange : restoreRanges) {
ASSERT(restoreRange.begin.startsWith(removePrefix) && restoreRange.end.startsWith(removePrefix));
}
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
// Get old restore config for this tag
KeyBackedTag tag = makeRestoreTag(tagName.toString());
Optional<UidAndAbortedFlagT> oldUidAndAborted = co_await tag.get(tr);
if (oldUidAndAborted.present()) {
if (oldUidAndAborted.get().first == uid) {
if (oldUidAndAborted.get().second) {
throw restore_duplicate_uid();
} else {
co_return;
}
}
RestoreConfig oldRestore(oldUidAndAborted.get().first);
// Make sure old restore for this tag is not runnable
bool runnable = co_await oldRestore.isRunnable(tr);
if (runnable) {
throw restore_duplicate_tag();
}
// Clear the old restore config
oldRestore.clear(tr);
}
// Bulkload restore (useRangeFileRestore=false) overwrites each shard via the range-lock
// mechanism in DD, so a non-empty destination is expected and required — skip the precheck.
if (!onlyApplyMutationLogs && useRangeFileRestore) {
int index{ 0 };
for (index = 0; index < restoreRanges.size(); index++) {
KeyRange restoreIntoRange = KeyRangeRef(restoreRanges[index].begin, restoreRanges[index].end)
.removePrefix(removePrefix)
.withPrefix(addPrefix);
RangeResult existingRows = co_await tr->getRange(restoreIntoRange, 1);
// Allow restoring over existing data only when using the validation restore prefix.
// validateRestoreLogKeys.begin (\xff\x02/rlog/) is the designated prefix for validation restores.
// Using any other prefix with existing data could corrupt user data.
if (!existingRows.empty() && addPrefix != validateRestoreLogKeys.begin) {
throw restore_destination_not_empty();
}
}
}
// Make new restore config
RestoreConfig restore(uid);
// Point the tag to the new uid
tag.set(tr, { uid, false });
Reference<IBackupContainer> bc =
IBackupContainer::openContainer(backupURL.toString(), proxy, encryptionKeyFileName, encryptionBlockSize);
// Configure the new restore
restore.tag().set(tr, tagName.toString());
restore.sourceContainer().set(tr, bc);
restore.stateEnum().set(tr, ERestoreState::QUEUED);
restore.restoreVersion().set(tr, restoreVersion);
restore.onlyApplyMutationLogs().set(tr, onlyApplyMutationLogs);
restore.inconsistentSnapshotOnly().set(tr, inconsistentSnapshotOnly);
restore.beginVersion().set(tr, beginVersion);
restore.unlockDBAfterRestore().set(tr, unlockDB);
restore.mutationLogType().set(tr, mutationLogType);
restore.useRangeFileRestore().set(tr, useRangeFileRestore);
if (buggify() && restoreRanges.size() == 1) {
restore.restoreRange().set(tr, restoreRanges[0]);
} else {
for (auto& range : restoreRanges) {
restore.restoreRangeSet().insert(tr, range);
}
}
// this also sets restore.add/removePrefix.
restore.initApplyMutations(tr, addPrefix, removePrefix, onlyApplyMutationLogs);
Key taskKey = co_await fileBackup::StartFullRestoreTaskFunc::addTask(
tr, backupAgent->taskBucket, uid, TaskCompletionKey::noSignal());
if (lockDB)
co_await lockDatabase(tr, uid);
else
co_await checkDatabaseLock(tr, uid);
co_return;
}
// This method will return the final status of the backup
static Future<ERestoreState> waitRestore(Database cx, Key tagName, Verbose verbose) {
ERestoreState status;
while (true) {
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
Error err;
try {
tr->setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
KeyBackedTag tag = makeRestoreTag(tagName.toString());
Optional<UidAndAbortedFlagT> current = co_await tag.get(tr);
if (!current.present()) {
if (verbose) {
printf("waitRestore: Tag: %s State: %s\n",
tagName.toString().c_str(),
FileBackupAgent::restoreStateText(ERestoreState::UNINITIALIZED).toString().c_str());
}
co_return ERestoreState::UNINITIALIZED;
}
RestoreConfig restore(current.get().first);
if (verbose) {
std::string details = co_await restore.getProgress(tr);
printf("%s\n", details.c_str());
}
ERestoreState status_ = co_await restore.stateEnum().getD(tr);
status = status_;
bool runnable = co_await restore.isRunnable(tr);
// State won't change from here
if (!runnable)
break;
// Wait for a change
Future<Void> watchFuture = tr->watch(restore.stateEnum().key);
co_await tr->commit();
if (verbose)
co_await (watchFuture || delay(1));
else
co_await watchFuture;
continue;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
co_return status;
}
static Future<Void> discontinueBackup(FileBackupAgent* backupAgent,
Reference<ReadYourWritesTransaction> tr,
Key tagName) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
KeyBackedTag tag = makeBackupTag(tagName.toString());
UidAndAbortedFlagT current = co_await tag.getOrThrow(tr, Snapshot::False, backup_unneeded());
BackupConfig config(current.first);
EBackupState status = co_await config.stateEnum().getD(tr, Snapshot::False, EBackupState::STATE_NEVERRAN);
if (!FileBackupAgent::isRunnable(status)) {
throw backup_unneeded();
}
// If the backup is already restorable then 'mostly' abort it - cancel all tasks via the tag
// and clear the mutation logging config and data - but set its state as COMPLETED instead of ABORTED.
Optional<Version> latestRestorableVersion = co_await config.getLatestRestorableVersion(tr);
TraceEvent(SevInfo, "FBA_DiscontinueBackup")
.detail("AlreadyRestorable", latestRestorableVersion.present() ? "Yes" : "No")
.detail("TagName", tag.tagName.c_str())
.detail("Status", BackupAgentBase::getStateText(status));
if (latestRestorableVersion.present()) {
// Cancel all backup tasks through tag
co_await tag.cancel(tr);
tr->setOption(FDBTransactionOptions::COMMIT_ON_FIRST_PROXY);
Key destUidValue = co_await config.destUidValue().getOrThrow(tr);
co_await tr->getReadVersion();
co_await (eraseLogData(tr, config.getUidAsKey(), destUidValue) &&
fileBackup::clearBackupStartID(tr, config.getUid()));
config.stateEnum().set(tr, EBackupState::STATE_COMPLETED);
co_return;
}
bool stopWhenDone = co_await config.stopWhenDone().getOrThrow(tr);
if (stopWhenDone) {
throw backup_duplicate();
}
config.stopWhenDone().set(tr, true);
co_return;
}
static Future<Void> abortBackup(FileBackupAgent* backupAgent,
Reference<ReadYourWritesTransaction> tr,
std::string tagName) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
KeyBackedTag tag = makeBackupTag(tagName);
UidAndAbortedFlagT current = co_await tag.getOrThrow(tr, Snapshot::False, backup_unneeded());
BackupConfig config(current.first);
Key destUidValue = co_await config.destUidValue().getOrThrow(tr);
EBackupState status = co_await config.stateEnum().getD(tr, Snapshot::False, EBackupState::STATE_NEVERRAN);
if (!backupAgent->isRunnable(status)) {
throw backup_unneeded();
}
TraceEvent(SevInfo, "FBA_AbortBackup")
.detail("TagName", tagName.c_str())
.detail("Status", BackupAgentBase::getStateText(status));
co_await tag.cancel(tr);
co_await (eraseLogData(tr, config.getUidAsKey(), destUidValue) &&
fileBackup::clearBackupStartID(tr, config.getUid()));
config.stateEnum().set(tr, EBackupState::STATE_ABORTED);
co_return;
}
static Future<Void> checkAndDisableBackupWorkers(Database cx) {
bool running = co_await runRYWTransaction(
cx, [=](Reference<ReadYourWritesTransaction> tr) { return anyPartitionedBackupRunning(tr); });
if (!running) {
co_await disableBackupWorker(cx);
}
co_return;
}
static Future<Void> checkAndDisableRangePartitionedBackupWorkers(Database cx) {
bool running = co_await runRYWTransaction(cx, [=](Reference<ReadYourWritesTransaction> tr) -> Future<bool> {
bool r = co_await anyRangePartitionedBackupRunning(tr);
if (!r) {
// Last range-partitioned backup just finished. Ask DD to clear the partition list.
tr->set(backupPartitionRequiredKey, backupPartitionRequiredValue(2));
}
co_return r;
});
if (!running) {
co_await disableRangePartitionedBackupWorker(cx);
}
co_return;
}
static Future<Void> changePause(FileBackupAgent* backupAgent, Database db, bool pause) {
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(db));
while (true) {
Error err;
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
tr->setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
try {
// This is the pause key is in the task bucket, for backup agents
backupAgent->taskBucket->changePause(tr, pause);
// This is backup workers' pause key.
tr->set(backupPausedKey, pause ? "1"_sr : "0"_sr);
co_await tr->commit();
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
TraceEvent("FileBackupAgentChangePaused").detail("Action", pause ? "Paused" : "Resumed");
co_return;
}
struct TimestampedVersion {
Optional<Version> version;
Optional<int64_t> epochs;
bool present() const { return version.present(); }
JsonBuilderObject toJSON() const {
JsonBuilderObject doc;
if (version.present()) {
doc.setKey("Version", version.get());
if (epochs.present()) {
doc.setKey("EpochSeconds", epochs.get());
doc.setKey("Timestamp", timeStampToString(epochs));
}
}
return doc;
}
};
// Helper actor for generating status
// If f is present, lookup epochs using timekeeper and tr, return TimestampedVersion
static Future<TimestampedVersion> getTimestampedVersion(Reference<ReadYourWritesTransaction> tr,
Future<Optional<Version>> f) {
TimestampedVersion tv;
tv.version = co_await f;
if (tv.version.present()) {
tv.epochs = co_await timeKeeperEpochsFromVersion(tv.version.get(), tr);
}
co_return tv;
}
static Future<std::string> getStatusJSON(FileBackupAgent* backupAgent, Database cx, std::string tagName) {
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
while (true) {
Error err;
try {
JsonBuilderObject doc;
doc.setKey("SchemaVersion", "1.0.0");
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
KeyBackedTag tag = makeBackupTag(tagName);
Optional<UidAndAbortedFlagT> uidAndAbortedFlag;
Optional<Value> paused;
Version recentReadVersion{ 0 };
co_await (store(paused, tr->get(backupAgent->taskBucket->getPauseKey())) &&
store(uidAndAbortedFlag, tag.get(tr)) && store(recentReadVersion, tr->getReadVersion()));
doc.setKey("BackupAgentsPaused", paused.present());
doc.setKey("Tag", tag.tagName);
if (uidAndAbortedFlag.present()) {
doc.setKey("UID", uidAndAbortedFlag.get().first.toString());
BackupConfig config(uidAndAbortedFlag.get().first);
EBackupState backupState =
co_await config.stateEnum().getD(tr, Snapshot::False, EBackupState::STATE_NEVERRAN);
JsonBuilderObject statusDoc;
statusDoc.setKey("Name", BackupAgentBase::getStateName(backupState));
statusDoc.setKey("Description", BackupAgentBase::getStateText(backupState));
statusDoc.setKey("Completed", backupState == EBackupState::STATE_COMPLETED);
statusDoc.setKey("Running", BackupAgentBase::isRunnable(backupState));
doc.setKey("Status", statusDoc);
Future<Void> done = Void();
if (backupState != EBackupState::STATE_NEVERRAN) {
Reference<IBackupContainer> bc;
TimestampedVersion latestRestorable;
co_await (
store(latestRestorable, getTimestampedVersion(tr, config.getLatestRestorableVersion(tr))) &&
store(bc, config.backupContainer().getOrThrow(tr)));
bc = fileBackup::getBackupContainerWithProxy(bc);
doc.setKey("Restorable", latestRestorable.present());
if (latestRestorable.present()) {
JsonBuilderObject o = latestRestorable.toJSON();
if (backupState != EBackupState::STATE_COMPLETED) {
o.setKey("LagSeconds",
(recentReadVersion - latestRestorable.version.get()) /
CLIENT_KNOBS->CORE_VERSIONSPERSECOND);
}
doc.setKey("LatestRestorablePoint", o);
}
doc.setKey("DestinationURL", bc->getURL());
}
if (backupState == EBackupState::STATE_RUNNING_DIFFERENTIAL ||
backupState == EBackupState::STATE_RUNNING) {
int64_t snapshotInterval{ 0 };
int64_t logBytesWritten{ 0 };
int64_t rangeBytesWritten{ 0 };
bool stopWhenDone{ false };
TimestampedVersion snapshotBegin;
TimestampedVersion snapshotTargetEnd;
TimestampedVersion latestLogEnd;
TimestampedVersion latestSnapshotEnd;
TimestampedVersion snapshotLastDispatch;
Optional<int64_t> snapshotLastDispatchShardsBehind;
co_await (
store(snapshotInterval, config.snapshotIntervalSeconds().getOrThrow(tr)) &&
store(logBytesWritten, config.logBytesWritten().getD(tr)) &&
store(rangeBytesWritten, config.rangeBytesWritten().getD(tr)) &&
store(stopWhenDone, config.stopWhenDone().getOrThrow(tr)) &&
store(snapshotBegin, getTimestampedVersion(tr, config.snapshotBeginVersion().get(tr))) &&
store(snapshotTargetEnd,
getTimestampedVersion(tr, config.snapshotTargetEndVersion().get(tr))) &&
store(latestLogEnd, getTimestampedVersion(tr, config.latestLogEndVersion().get(tr))) &&
store(latestSnapshotEnd,
getTimestampedVersion(tr, config.latestSnapshotEndVersion().get(tr))) &&
store(snapshotLastDispatch,
getTimestampedVersion(tr, config.snapshotDispatchLastVersion().get(tr))) &&
store(snapshotLastDispatchShardsBehind, config.snapshotDispatchLastShardsBehind().get(tr)));
doc.setKey("StopAfterSnapshot", stopWhenDone);
doc.setKey("SnapshotIntervalSeconds", snapshotInterval);
doc.setKey("LogBytesWritten", logBytesWritten);
doc.setKey("RangeBytesWritten", rangeBytesWritten);
if (latestLogEnd.present()) {
doc.setKey("LatestLogEnd", latestLogEnd.toJSON());
}
if (latestSnapshotEnd.present()) {
doc.setKey("LatestSnapshotEnd", latestSnapshotEnd.toJSON());
}
JsonBuilderObject snapshot;
if (snapshotBegin.present()) {
snapshot.setKey("Begin", snapshotBegin.toJSON());
if (snapshotTargetEnd.present()) {
snapshot.setKey("EndTarget", snapshotTargetEnd.toJSON());
Version interval = snapshotTargetEnd.version.get() - snapshotBegin.version.get();
snapshot.setKey("IntervalSeconds", interval / CLIENT_KNOBS->CORE_VERSIONSPERSECOND);
Version elapsed = recentReadVersion - snapshotBegin.version.get();
double progress = (interval > 0) ? (100.0 * elapsed / interval) : 100;
snapshot.setKey("ExpectedProgress", progress);
}
JsonBuilderObject dispatchDoc = snapshotLastDispatch.toJSON();
if (snapshotLastDispatchShardsBehind.present()) {
dispatchDoc.setKey("ShardsBehind", snapshotLastDispatchShardsBehind.get());
}
snapshot.setKey("LastDispatch", dispatchDoc);
}
doc.setKey("CurrentSnapshot", snapshot);
}
// Add snapshot mode information
Optional<int> snapshotModeOpt = co_await config.snapshotMode().get(tr);
int snapshotModeValue = snapshotModeOpt.present() ? snapshotModeOpt.get() : 0;
std::string snapshotModeText;
switch (snapshotModeValue) {
case 0:
snapshotModeText = "rangefile";
break;
case 1:
snapshotModeText = "bulkdump";
break;
case 2:
snapshotModeText = "both";
break;
default:
TraceEvent(SevError, "BackupInvalidSnapshotMode")
.detail("BackupUID", config.getUid())
.detail("SnapshotModeValue", snapshotModeValue)
.detail("ValidValues", "0=rangefile, 1=bulkdump, 2=both");
throw backup_error();
}
doc.setKey("SnapshotMode", snapshotModeText);
// Check if backup is BulkLoad compatible
Optional<std::string> bulkDumpJobIdOpt = co_await config.bulkDumpJobId().get(tr);
doc.setKey("BulkLoadCompatible", bulkDumpJobIdOpt.present() && !bulkDumpJobIdOpt.get().empty());
// If using bulkdump mode, get bulkdump progress
if (snapshotModeValue == 1 || snapshotModeValue == 2) {
Optional<BulkDumpProgress> bulkDumpProgressOpt = co_await getBulkDumpProgress(cx);
if (bulkDumpProgressOpt.present()) {
BulkDumpProgress bdProgress = bulkDumpProgressOpt.get();
JsonBuilderObject bdDoc;
bdDoc.setKey("JobId", bdProgress.jobId.toString());
bdDoc.setKey("TotalTasks", bdProgress.totalTasks);
bdDoc.setKey("CompleteTasks", bdProgress.completeTasks);
bdDoc.setKey("RunningTasks", bdProgress.runningTasks);
bdDoc.setKey("ErrorTasks", bdProgress.errorTasks);
bdDoc.setKey("ProgressPercent", bdProgress.progressPercent());
bdDoc.setKey("TotalBytes", bdProgress.totalBytes);
bdDoc.setKey("CompletedBytes", bdProgress.completedBytes);
bdDoc.setKey("AvgBytesPerSecond", bdProgress.avgBytesPerSecond());
if (bdProgress.etaSeconds().present()) {
bdDoc.setKey("EstimatedSecondsRemaining", bdProgress.etaSeconds().get());
}
bdDoc.setKey("ElapsedSeconds", bdProgress.elapsedSeconds);
// Add stalled tasks if any
if (!bdProgress.stalledTasks.empty()) {
JsonBuilderArray stalledArray;
for (const auto& stalled : bdProgress.stalledTasks) {
JsonBuilderObject stalledDoc;
stalledDoc.setKey("TaskId", stalled.taskId.toString());
stalledDoc.setKey("Range", stalled.range.toString());
stalledDoc.setKey("StalledSeconds", stalled.stalledSeconds);
stalledDoc.setKey("RestartCount", stalled.restartCount);
if (!stalled.lastError.empty()) {
stalledDoc.setKey("LastError", stalled.lastError);
}
stalledArray.push_back(stalledDoc);
}
bdDoc.setKey("StalledTasks", stalledArray);
}
doc.setKey("BulkDumpProgress", bdDoc);
}
}
KeyBackedMap<int64_t, std::pair<std::string, Version>>::RangeResultType errors =
co_await config.lastErrorPerType().getRange(
tr, 0, std::numeric_limits<int>::max(), CLIENT_KNOBS->TOO_MANY);
JsonBuilderArray errorList;
for (auto& e : errors.results) {
std::string msg = e.second.first;
Version ver = e.second.second;
JsonBuilderObject errDoc;
errDoc.setKey("Message", msg.c_str());
errDoc.setKey("RelativeSeconds",
(ver - recentReadVersion) / CLIENT_KNOBS->CORE_VERSIONSPERSECOND);
}
doc.setKey("Errors", errorList);
}
co_return doc.getJson();
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
}
static Future<std::string> getStatus(FileBackupAgent* backupAgent,
Database cx,
ShowErrors showErrors,
std::string tagName) {
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
std::string statusText;
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
KeyBackedTag tag;
BackupConfig config;
EBackupState backupState;
statusText = "";
tag = makeBackupTag(tagName);
Optional<UidAndAbortedFlagT> uidAndAbortedFlag = co_await tag.get(tr);
Future<Optional<Value>> fPaused = tr->get(backupAgent->taskBucket->getPauseKey());
if (uidAndAbortedFlag.present()) {
config = BackupConfig(uidAndAbortedFlag.get().first);
EBackupState status =
co_await config.stateEnum().getD(tr, Snapshot::False, EBackupState::STATE_NEVERRAN);
backupState = status;
}
if (!uidAndAbortedFlag.present() || backupState == EBackupState::STATE_NEVERRAN) {
statusText += "No previous backups found on tag '" + tagName + "'.\n";
statusText += "Use 'fdbbackup tags' to list all backup tags.\n";
} else {
std::string backupStatus(BackupAgentBase::getStateText(backupState));
Reference<IBackupContainer> bc;
Optional<Version> latestRestorableVersion;
Version recentReadVersion{ 0 };
co_await (store(latestRestorableVersion, config.getLatestRestorableVersion(tr)) &&
store(bc, config.backupContainer().getOrThrow(tr)) &&
store(recentReadVersion, tr->getReadVersion()));
bc = fileBackup::getBackupContainerWithProxy(bc);
bool snapshotProgress = false;
switch (backupState) {
case EBackupState::STATE_SUBMITTED:
statusText += "The backup on tag `" + tagName + "' is in progress (just started) to " +
bc->getURL() + ".\n";
break;
case EBackupState::STATE_RUNNING:
statusText += "The backup on tag `" + tagName + "' is in progress to " + bc->getURL() + ".\n";
snapshotProgress = true;
break;
case EBackupState::STATE_RUNNING_DIFFERENTIAL:
statusText += "The backup on tag `" + tagName + "' is restorable but continuing to " +
bc->getURL() + ".\n";
snapshotProgress = true;
break;
case EBackupState::STATE_COMPLETED:
statusText += "The previous backup on tag `" + tagName + "' at " + bc->getURL() +
" completed at version " + format("%lld", latestRestorableVersion.orDefault(-1)) +
".\n";
break;
default:
statusText += "The previous backup on tag `" + tagName + "' at " + bc->getURL() + " " +
backupStatus + ".\n";
break;
}
statusText += format("BackupUID: %s\n", uidAndAbortedFlag.get().first.toString().c_str());
statusText += format("BackupURL: %s\n", bc->getURL().c_str());
// Add enhanced status fields for BulkLoad integration
Optional<int> snapshotModeOpt = co_await config.snapshotMode().get(tr);
int snapshotModeValue = snapshotModeOpt.present() ? snapshotModeOpt.get() : 0;
std::string snapshotModeText;
switch (snapshotModeValue) {
case 0:
snapshotModeText = "rangefile";
break;
case 1:
snapshotModeText = "bulkdump";
break;
case 2:
snapshotModeText = "both";
break;
default:
TraceEvent(SevError, "BackupInvalidSnapshotMode")
.detail("BackupUID", config.getUid())
.detail("SnapshotModeValue", snapshotModeValue)
.detail("ValidValues", "0=rangefile, 1=bulkdump, 2=both");
throw backup_error();
}
statusText += format("Snapshot Mode: %s\n", snapshotModeText.c_str());
// Check if backup is BulkLoad compatible (has bulkdump_data/).
// In rangefile mode no bulkdump task ever runs, so the answer is
// always "no" and the line carries no information — skip it.
if (snapshotModeValue != 0) {
Optional<std::string> bulkDumpJobIdOpt = co_await config.bulkDumpJobId().get(tr);
bool bulkLoadCompatible = bulkDumpJobIdOpt.present() && !bulkDumpJobIdOpt.get().empty();
statusText += format("BulkLoad Compatible: %s\n", bulkLoadCompatible ? "yes" : "no");
}
bool showBulkDump = (snapshotModeValue == 1 || snapshotModeValue == 2);
bool showRangeFile = (snapshotModeValue == 0 || snapshotModeValue == 2);
if (snapshotProgress) {
int64_t snapshotInterval{ 0 };
Version snapshotBeginVersion{ 0 };
Version snapshotTargetEndVersion{ 0 };
Optional<Version> latestSnapshotEndVersion;
Optional<Version> latestLogEndVersion;
Optional<int64_t> logBytesWritten;
Optional<int64_t> rangeBytesWritten;
Optional<int64_t> latestSnapshotEndVersionTimestamp;
Optional<int64_t> latestLogEndVersionTimestamp;
Optional<int64_t> snapshotBeginVersionTimestamp;
Optional<int64_t> snapshotTargetEndVersionTimestamp;
bool stopWhenDone{ false };
co_await (store(snapshotBeginVersion, config.snapshotBeginVersion().getOrThrow(tr)) &&
store(snapshotTargetEndVersion, config.snapshotTargetEndVersion().getOrThrow(tr)) &&
store(snapshotInterval, config.snapshotIntervalSeconds().getOrThrow(tr)) &&
store(logBytesWritten, config.logBytesWritten().get(tr)) &&
store(rangeBytesWritten, config.rangeBytesWritten().get(tr)) &&
store(latestLogEndVersion, config.latestLogEndVersion().get(tr)) &&
store(latestSnapshotEndVersion, config.latestSnapshotEndVersion().get(tr)) &&
store(stopWhenDone, config.stopWhenDone().getOrThrow(tr)));
co_await (
store(latestSnapshotEndVersionTimestamp,
getTimestampFromVersion(latestSnapshotEndVersion, tr)) &&
store(latestLogEndVersionTimestamp, getTimestampFromVersion(latestLogEndVersion, tr)) &&
store(snapshotBeginVersionTimestamp,
timeKeeperEpochsFromVersion(snapshotBeginVersion, tr)) &&
store(snapshotTargetEndVersionTimestamp,
timeKeeperEpochsFromVersion(snapshotTargetEndVersion, tr)));
if (showBulkDump) {
Optional<BulkDumpProgress> bulkDumpProgressOpt = co_await getBulkDumpProgress(cx);
statusText += "\nBulkDump Snapshot:\n";
if (bulkDumpProgressOpt.present()) {
BulkDumpProgress bdProgress = bulkDumpProgressOpt.get();
statusText += format(" Tasks: %d/%d complete (%.1f%%)\n",
bdProgress.completeTasks,
bdProgress.totalTasks,
bdProgress.progressPercent());
statusText += format(
" Bytes: %s\n",
formatBytesProgress(bdProgress.completedBytes, bdProgress.totalBytes).c_str());
double throughput = bdProgress.avgBytesPerSecond();
if (throughput > 0) {
statusText += format(" Throughput: %.1f MB/s\n", throughput / 1048576.0);
}
if (bdProgress.elapsedSeconds > 0) {
statusText +=
format(" Elapsed: %s\n",
formatDurationHumanReadable((int)bdProgress.elapsedSeconds).c_str());
}
if (bdProgress.etaSeconds().present()) {
statusText +=
format(" ETA: %s\n",
formatDurationHumanReadable((int)bdProgress.etaSeconds().get()).c_str());
}
if (!bdProgress.stalledTasks.empty()) {
statusText += format("\nWARNING: %zu stalled tasks (no progress > 60s):\n",
bdProgress.stalledTasks.size());
for (const auto& stalled : bdProgress.stalledTasks) {
statusText += format(" Task %s: %s, stalled %.0fs, %d restarts\n",
stalled.taskId.shortString().c_str(),
stalled.range.toString().c_str(),
stalled.stalledSeconds,
stalled.restartCount);
if (!stalled.lastError.empty()) {
statusText +=
format(" Last error: %s\n", stalled.lastError.c_str());
}
}
}
} else {
statusText += " Status: pending\n";
}
}
if (showRangeFile) {
statusText += "\nRangefile Snapshot:\n";
statusText += format(" Bytes written: %s\n",
formatBytesHumanReadable(rangeBytesWritten.orDefault(0)).c_str());
if (backupState == EBackupState::STATE_RUNNING_DIFFERENTIAL) {
double pct = 100.0 * (recentReadVersion - snapshotBeginVersion) /
(snapshotTargetEndVersion - snapshotBeginVersion);
statusText += format(" Progress: %.2f%%\n", pct);
} else {
statusText += " Status: Initial snapshot still running\n";
}
statusText +=
format(" Started: %s\n", timeStampToString(snapshotBeginVersionTimestamp).c_str());
}
statusText += format("\nMutation Logs:\n");
statusText += format(" Bytes written: %s\n",
formatBytesHumanReadable(logBytesWritten.orDefault(0)).c_str());
statusText += format(" Last complete version: %s (%s)\n",
versionToString(latestLogEndVersion).c_str(),
timeStampToString(latestLogEndVersionTimestamp).c_str());
statusText += format("\nSnapshot interval is %lld seconds.\n", snapshotInterval);
}
// Append the errors, if requested
if (showErrors) {
KeyBackedMap<int64_t, std::pair<std::string, Version>>::RangeResultType errors =
co_await config.lastErrorPerType().getRange(
tr, 0, std::numeric_limits<int>::max(), CLIENT_KNOBS->TOO_MANY);
std::string recentErrors;
std::string pastErrors;
for (auto& e : errors.results) {
Version v = e.second.second;
std::string msg = format(
"%s ago : %s\n",
secondsToTimeFormat((recentReadVersion - v) / CLIENT_KNOBS->CORE_VERSIONSPERSECOND)
.c_str(),
e.second.first.c_str());
// If error version is at or more recent than the latest restorable version then it
// could be inhibiting progress
if (v >= latestRestorableVersion.orDefault(0)) {
recentErrors += msg;
} else {
pastErrors += msg;
}
}
if (!recentErrors.empty()) {
if (latestRestorableVersion.present()) {
statusText +=
format("Recent Errors (since latest restorable point %s ago)\n",
secondsToTimeFormat((recentReadVersion - latestRestorableVersion.get()) /
CLIENT_KNOBS->CORE_VERSIONSPERSECOND)
.c_str()) +
recentErrors;
} else {
statusText += "Recent Errors (since initialization)\n" + recentErrors;
}
}
if (!pastErrors.empty())
statusText += "Older Errors\n" + pastErrors;
}
}
Optional<Value> paused = co_await fPaused;
if (paused.present()) {
statusText += format("\nAll backup agents have been paused.\n");
}
break;
} catch (Error& e) {
err = e;
}
co_await tr->onError(err);
}
co_return statusText;
}
static Future<Optional<Version>> getLastRestorable(FileBackupAgent* backupAgent,
Reference<ReadYourWritesTransaction> tr,
Key tagName,
Snapshot snapshot) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
Optional<Value> version = co_await tr->get(backupAgent->lastRestorable.pack(tagName), snapshot);
co_return (version.present())
? Optional<Version>(BinaryReader::fromStringRef<Version>(version.get(), Unversioned()))
: Optional<Version>();
}
static StringRef read(StringRef& data, int bytes) {
if (bytes > data.size())
throw restore_error();
StringRef r = data.substr(0, bytes);
data = data.substr(bytes);
return r;
}
// Submits the restore request to the database and throws "restore_invalid_version" error if
// restore is not possible. Parameters:
// cx: the database to be restored to
// cxOrig: if present, is used to resolve the restore timestamp into a version.
// tagName: restore tag
// url: the backup container's URL that contains all backup files
// ranges: the restored key ranges; if empty, restore all key ranges in the backup
// waitForComplete: if set, wait until the restore is completed before returning; otherwise,
// return when the request is submitted to the database.
// targetVersion: the version to be restored.
// verbose: print verbose information.
// addPrefix: each key is added this prefix during restore.
// removePrefix: for each key to be restored, remove this prefix first.
// lockDB: if set lock the database with randomUid before performing restore;
// otherwise, check database is locked with the randomUid
// onlyApplyMutationLogs: only perform incremental restore, by only applying mutation logs
// inconsistentSnapshotOnly: Ignore mutation log files during the restore to speedup the process.
// When set to true, gives an inconsistent snapshot, thus not recommended
// beginVersions: restore's begin version for each range
// randomUid: the UID for lock the database
static Future<Version> restore(FileBackupAgent* backupAgent,
Database cx,
Optional<Database> cxOrig,
Key tagName,
Key url,
Optional<std::string> proxy,
Standalone<VectorRef<KeyRangeRef>> ranges,
Standalone<VectorRef<Version>> beginVersions,
WaitForComplete waitForComplete,
Version targetVersion,
Verbose verbose,
Key addPrefix,
Key removePrefix,
LockDB lockDB,
UnlockDB unlockDB,
OnlyApplyMutationLogs onlyApplyMutationLogs,
InconsistentSnapshotOnly inconsistentSnapshotOnly,
Optional<std::string> encryptionKeyFileName,
UID randomUid,
bool useRangeFileRestore = true) {
// The restore command line tool won't allow ranges to be empty, but correctness workloads somehow might.
if (ranges.empty()) {
throw restore_error();
}
std::string urlStr = url.toString();
Reference<IBackupContainer> bc =
IBackupContainer::openContainer(urlStr, proxy, /*encryptionKeyFileName=*/{}, /*encryptionBlockSize=*/0);
// For blobstore:// URLs, use invalidVersion to allow describeBackup to write missing version properties
// This is needed for S3 where metadata may not be immediately consistent
BackupDescription desc = co_await bc->describeBackup(true, isBlobstoreUrl(urlStr) ? invalidVersion : 0);
if (desc.fileLevelEncryption && !encryptionKeyFileName.present()) {
fprintf(stderr, "ERROR: Backup is encrypted, please provide the encryption key file path.\n");
throw restore_error();
} else if (!desc.fileLevelEncryption && encryptionKeyFileName.present()) {
fprintf(stderr, "ERROR: Backup is not encrypted, please remove the encryption key file path.\n");
throw restore_error();
}
if (desc.fileLevelEncryption) {
bc = IBackupContainer::openContainer(urlStr, proxy, encryptionKeyFileName, desc.encryptionBlockSize);
// openContainer may return a cached container that has blockSize=0 (seeded earlier without blockSize).
// Set it explicitly to ensure the correct value is used.
bc->setEncryptionBlockSize(desc.encryptionBlockSize);
}
if (cxOrig.present()) {
co_await desc.resolveVersionTimes(cxOrig.get());
}
printf("Backup Description\n%s", desc.toString().c_str());
if (targetVersion == invalidVersion && desc.maxRestorableVersion.present())
targetVersion = desc.maxRestorableVersion.get();
if (targetVersion == invalidVersion && onlyApplyMutationLogs && desc.contiguousLogEnd.present()) {
targetVersion = desc.contiguousLogEnd.get() - 1;
}
Version beginVersion = invalidVersion; // min begin version for all ranges
if (!beginVersions.empty()) {
beginVersion = *std::min_element(beginVersions.begin(), beginVersions.end());
}
Optional<RestorableFileSet> restoreSet =
co_await bc->getRestoreSet(targetVersion, ranges, onlyApplyMutationLogs, beginVersion);
if (!restoreSet.present()) {
TraceEvent(SevWarn, "FileBackupAgentRestoreNotPossible")
.detail("BackupContainer", bc->getURL())
.detail("BeginVersion", beginVersion)
.detail("TargetVersion", targetVersion);
throw restore_invalid_version();
}
if (verbose) {
printf("Restoring backup to version: %lld\n", (long long)targetVersion);
}
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(cx));
while (true) {
Error err;
try {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
co_await submitRestore(backupAgent,
tr,
tagName,
url,
proxy,
ranges,
targetVersion,
addPrefix,
removePrefix,
lockDB,
unlockDB,
onlyApplyMutationLogs,
inconsistentSnapshotOnly,
beginVersion,
randomUid,
desc.mutationLogType,
encryptionKeyFileName,
desc.encryptionBlockSize,
useRangeFileRestore);
co_await tr->commit();
break;
} catch (Error& e) {
err = e;
}
if (err.code() == error_code_restore_duplicate_tag) {
throw err;
}
co_await tr->onError(err);
}
if (waitForComplete) {
ERestoreState finalState = co_await waitRestore(cx, tagName, verbose);
if (finalState != ERestoreState::COMPLETED)
throw restore_error();
}
co_return targetVersion;
}
// used for correctness only, locks the database before discontinuing the backup and that same lock is then used
// while doing the restore. the tagname of the backup must be the same as the restore.
static Future<Version> atomicRestore(FileBackupAgent* backupAgent,
Database cx,
Key tagName,
Standalone<VectorRef<KeyRangeRef>> ranges,
Key addPrefix,
Key removePrefix) {
auto ryw_tr = makeReference<ReadYourWritesTransaction>(cx);
BackupConfig backupConfig;
DatabaseConfiguration config = co_await getDatabaseConfiguration(cx);
while (true) {
Error err;
try {
ryw_tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
ryw_tr->setOption(FDBTransactionOptions::LOCK_AWARE);
KeyBackedTag tag = makeBackupTag(tagName.toString());
UidAndAbortedFlagT uidFlag = co_await tag.getOrThrow(ryw_tr);
backupConfig = BackupConfig(uidFlag.first);
EBackupState status = co_await backupConfig.stateEnum().getOrThrow(ryw_tr);
if (status != EBackupState::STATE_RUNNING_DIFFERENTIAL) {
throw backup_duplicate();
}
break;
} catch (Error& e) {
err = e;
}
co_await ryw_tr->onError(err);
}
// Lock src, record commit version
Transaction tr(cx);
Version commitVersion{ 0 };
UID randomUid = deterministicRandom()->randomUniqueID();
while (true) {
Error err;
try {
// We must get a commit version so add a conflict range that won't likely cause conflicts
// but will ensure that the transaction is actually submitted.
tr.addWriteConflictRange(backupConfig.snapshotRangeDispatchMap().subspace);
co_await lockDatabase(&tr, randomUid);
co_await tr.commit();
commitVersion = tr.getCommittedVersion();
TraceEvent("AS_Locked").detail("CommitVer", commitVersion);
break;
} catch (Error& e) {
err = e;
}
co_await tr.onError(err);
}
ryw_tr->reset();
while (true) {
Error err;
try {
Optional<Version> restoreVersion = co_await backupConfig.getLatestRestorableVersion(ryw_tr);
if (restoreVersion.present() && restoreVersion.get() >= commitVersion) {
TraceEvent("AS_RestoreVersion").detail("RestoreVer", restoreVersion.get());
break;
} else {
ryw_tr->reset();
co_await delay(0.2);
continue;
}
} catch (Error& e) {
err = e;
}
co_await ryw_tr->onError(err);
}
ryw_tr->reset();
while (true) {
Error err;
try {
co_await discontinueBackup(backupAgent, ryw_tr, tagName);
co_await ryw_tr->commit();
TraceEvent("AS_DiscontinuedBackup").log();
break;
} catch (Error& e) {
err = e;
}
if (err.code() == error_code_backup_unneeded || err.code() == error_code_backup_duplicate) {
break;
}
co_await ryw_tr->onError(err);
}
co_await waitBackup(backupAgent, cx, tagName.toString(), StopWhenDone::True);
TraceEvent("AS_BackupStopped").log();
ryw_tr->reset();
while (true) {
Error err;
try {
ryw_tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
ryw_tr->setOption(FDBTransactionOptions::LOCK_AWARE);
for (auto& range : ranges) {
ryw_tr->addReadConflictRange(range);
ryw_tr->clear(range);
}
co_await ryw_tr->commit();
TraceEvent("AS_ClearedRange").log();
break;
} catch (Error& e) {
err = e;
}
co_await ryw_tr->onError(err);
}
Reference<IBackupContainer> bc = co_await backupConfig.backupContainer().getOrThrow(cx.getReference());
bc = fileBackup::getBackupContainerWithProxy(bc);
TraceEvent("AS_StartRestore").log();
Standalone<VectorRef<KeyRangeRef>> restoreRange;
Standalone<VectorRef<KeyRangeRef>> systemRestoreRange;
for (auto r : ranges) {
restoreRange.push_back_deep(restoreRange.arena(), r);
}
if (!systemRestoreRange.empty()) {
// restore system keys
co_await restore(backupAgent,
cx,
cx,
"system_restore"_sr,
KeyRef(bc->getURL()),
bc->getProxy(),
systemRestoreRange,
{},
WaitForComplete::True,
::invalidVersion,
Verbose::True,
addPrefix,
removePrefix,
LockDB::True,
UnlockDB::False,
OnlyApplyMutationLogs::False,
InconsistentSnapshotOnly::False,
{},
randomUid);
auto rywTransaction = makeReference<ReadYourWritesTransaction>(cx);
// clear old restore config associated with system keys
while (true) {
Error err;
try {
rywTransaction->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
rywTransaction->setOption(FDBTransactionOptions::LOCK_AWARE);
RestoreConfig oldRestore(randomUid);
oldRestore.clear(rywTransaction);
co_await rywTransaction->commit();
break;
} catch (Error& e) {
err = e;
}
co_await rywTransaction->onError(err);
}
}
// restore user data
Version ver = co_await restore(backupAgent,
cx,
cx,
tagName,
KeyRef(bc->getURL()),
bc->getProxy(),
restoreRange,
{},
WaitForComplete::True,
::invalidVersion,
Verbose::True,
addPrefix,
removePrefix,
LockDB::True,
UnlockDB::True,
OnlyApplyMutationLogs::False,
InconsistentSnapshotOnly::False,
{},
randomUid);
co_return ver;
}
};
const int FileBackupAgent::dataFooterSize = 20;
Future<Version> FileBackupAgent::restore(Database cx,
Optional<Database> cxOrig,
Key tagName,
Key url,
Optional<std::string> proxy,
Standalone<VectorRef<KeyRangeRef>> ranges,
Standalone<VectorRef<Version>> versions,
WaitForComplete waitForComplete,
Version targetVersion,
Verbose verbose,
Key addPrefix,
Key removePrefix,
LockDB lockDB,
UnlockDB unlockDB,
OnlyApplyMutationLogs onlyApplyMutationLogs,
InconsistentSnapshotOnly inconsistentSnapshotOnly,
Optional<std::string> const& encryptionKeyFileName,
Optional<UID> lockUID,
bool useRangeFileRestore) {
return FileBackupAgentImpl::restore(this,
cx,
cxOrig,
tagName,
url,
proxy,
ranges,
versions,
waitForComplete,
targetVersion,
verbose,
addPrefix,
removePrefix,
lockDB,
unlockDB,
onlyApplyMutationLogs,
inconsistentSnapshotOnly,
encryptionKeyFileName,
lockUID.present() ? lockUID.get() : deterministicRandom()->randomUniqueID(),
useRangeFileRestore);
}
Future<Version> FileBackupAgent::restore(Database cx,
Optional<Database> cxOrig,
Key tagName,
Key url,
Optional<std::string> proxy,
Standalone<VectorRef<KeyRangeRef>> ranges,
WaitForComplete waitForComplete,
Version targetVersion,
Verbose verbose,
Key addPrefix,
Key removePrefix,
LockDB lockDB,
UnlockDB unlockDB,
OnlyApplyMutationLogs onlyApplyMutationLogs,
InconsistentSnapshotOnly inconsistentSnapshotOnly,
Version beginVersion,
Optional<std::string> const& encryptionKeyFileName,
Optional<UID> lockUID,
bool useRangeFileRestore) {
Standalone<VectorRef<Version>> beginVersions;
for (auto i = 0; i < ranges.size(); ++i) {
beginVersions.push_back(beginVersions.arena(), beginVersion);
}
return restore(cx,
cxOrig,
tagName,
url,
proxy,
ranges,
beginVersions,
waitForComplete,
targetVersion,
verbose,
addPrefix,
removePrefix,
lockDB,
unlockDB,
onlyApplyMutationLogs,
inconsistentSnapshotOnly,
encryptionKeyFileName,
lockUID,
useRangeFileRestore);
}
Future<Version> FileBackupAgent::restore(Database cx,
Optional<Database> cxOrig,
Key tagName,
Key url,
Optional<std::string> proxy,
WaitForComplete waitForComplete,
Version targetVersion,
Verbose verbose,
KeyRange range,
Key addPrefix,
Key removePrefix,
LockDB lockDB,
OnlyApplyMutationLogs onlyApplyMutationLogs,
InconsistentSnapshotOnly inconsistentSnapshotOnly,
Version beginVersion,
Optional<std::string> const& encryptionKeyFileName,
bool useRangeFileRestore) {
Standalone<VectorRef<KeyRangeRef>> rangeRef;
if (range.begin.empty() && range.end.empty()) {
addDefaultBackupRanges(rangeRef);
} else {
rangeRef.push_back_deep(rangeRef.arena(), range);
}
Standalone<VectorRef<Version>> versionRef;
versionRef.push_back(versionRef.arena(), beginVersion);
return restore(cx,
cxOrig,
tagName,
url,
proxy,
rangeRef,
versionRef,
waitForComplete,
targetVersion,
verbose,
addPrefix,
removePrefix,
lockDB,
UnlockDB::True,
onlyApplyMutationLogs,
inconsistentSnapshotOnly,
encryptionKeyFileName,
{},
useRangeFileRestore);
}
Future<Version> FileBackupAgent::atomicRestore(Database cx,
Key tagName,
KeyRange range,
Key addPrefix,
Key removePrefix) {
Standalone<VectorRef<KeyRangeRef>> rangeRef;
if (range.begin.empty() && range.end.empty()) {
addDefaultBackupRanges(rangeRef);
} else {
rangeRef.push_back_deep(rangeRef.arena(), range);
}
return atomicRestore(cx, tagName, rangeRef, addPrefix, removePrefix);
}
Future<Version> FileBackupAgent::atomicRestore(Database cx,
Key tagName,
Standalone<VectorRef<KeyRangeRef>> ranges,
Key addPrefix,
Key removePrefix) {
return FileBackupAgentImpl::atomicRestore(this, cx, tagName, ranges, addPrefix, removePrefix);
}
Future<ERestoreState> FileBackupAgent::abortRestore(Reference<ReadYourWritesTransaction> tr, Key tagName) {
return fileBackup::abortRestore(tr, tagName);
}
Future<ERestoreState> FileBackupAgent::abortRestore(Database cx, Key tagName) {
return fileBackup::abortRestore(cx, tagName);
}
Future<std::string> FileBackupAgent::restoreStatus(Reference<ReadYourWritesTransaction> tr, Key tagName) {
return fileBackup::restoreStatus(tr, tagName);
}
Future<ERestoreState> FileBackupAgent::waitRestore(Database cx, Key tagName, Verbose verbose) {
return FileBackupAgentImpl::waitRestore(cx, tagName, verbose);
};
Future<Void> FileBackupAgent::submitBackup(Reference<ReadYourWritesTransaction> tr,
Key outContainer,
Optional<std::string> proxy,
int initialSnapshotIntervalSeconds,
int snapshotIntervalSeconds,
std::string const& tagName,
Standalone<VectorRef<KeyRangeRef>> backupRanges,
StopWhenDone stopWhenDone,
MutationLogType mutationLogType,
IncrementalBackupOnly incrementalBackupOnly,
Optional<std::string> const& encryptionKeyFileName,
int encryptionBlockSize,
int snapshotMode) {
return FileBackupAgentImpl::submitBackup(this,
tr,
outContainer,
proxy,
initialSnapshotIntervalSeconds,
snapshotIntervalSeconds,
tagName,
backupRanges,
stopWhenDone,
mutationLogType,
incrementalBackupOnly,
encryptionKeyFileName,
encryptionBlockSize,
snapshotMode);
}
Future<Void> FileBackupAgent::discontinueBackup(Reference<ReadYourWritesTransaction> tr, Key tagName) {
return FileBackupAgentImpl::discontinueBackup(this, tr, tagName);
}
Future<Void> FileBackupAgent::abortBackup(Reference<ReadYourWritesTransaction> tr, std::string tagName) {
return FileBackupAgentImpl::abortBackup(this, tr, tagName);
}
Future<Void> FileBackupAgent::checkAndDisableBackupWorkers(Database cx) {
return FileBackupAgentImpl::checkAndDisableBackupWorkers(cx);
}
Future<Void> FileBackupAgent::checkAndDisableRangePartitionedBackupWorkers(Database cx) {
return FileBackupAgentImpl::checkAndDisableRangePartitionedBackupWorkers(cx);
}
Future<std::string> FileBackupAgent::getStatus(Database cx, ShowErrors showErrors, std::string tagName) {
return FileBackupAgentImpl::getStatus(this, cx, showErrors, tagName);
}
Future<std::string> FileBackupAgent::getStatusJSON(Database cx, std::string tagName) {
return FileBackupAgentImpl::getStatusJSON(this, cx, tagName);
}
Future<Optional<Version>> FileBackupAgent::getLastRestorable(Reference<ReadYourWritesTransaction> tr,
Key tagName,
Snapshot snapshot) {
return FileBackupAgentImpl::getLastRestorable(this, tr, tagName, snapshot);
}
void FileBackupAgent::setLastRestorable(Reference<ReadYourWritesTransaction> tr, Key tagName, Version version) {
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
tr->set(lastRestorable.pack(tagName), BinaryWriter::toValue<Version>(version, Unversioned()));
}
Future<EBackupState> FileBackupAgent::waitBackup(Database cx,
std::string tagName,
StopWhenDone stopWhenDone,
Reference<IBackupContainer>* pContainer,
UID* pUID) {
return FileBackupAgentImpl::waitBackup(this, cx, tagName, stopWhenDone, pContainer, pUID);
}
Future<Void> FileBackupAgent::changePause(Database db, bool pause) {
return FileBackupAgentImpl::changePause(this, db, pause);
}