1447 lines
54 KiB
C++
1447 lines
54 KiB
C++
/*
|
|
* RangePartitionedBackupWorker.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/BackupAgent.h"
|
|
#include "fdbclient/BackupFileFormat.h"
|
|
#include "fdbclient/BackupContainer.h"
|
|
#include "fdbclient/DatabaseContext.h"
|
|
#include "fdbclient/SystemData.h"
|
|
#include "fdbserver/core/BackupPartitionMap.h"
|
|
#include "fdbserver/core/BackupProgress.h"
|
|
#include "fdbserver/core/Knobs.h"
|
|
#include "PartitionMapMessage.h"
|
|
#include "fdbserver/core/WaitFailure.h"
|
|
#include "fdbserver/logsystem/LogSystem.h"
|
|
#include "fdbserver/logsystem/LogSystemConsumer.h"
|
|
#include "fdbserver/logsystem/LogSystemFactory.h"
|
|
#include "flow/CoroUtils.h"
|
|
|
|
#define SevDebugMemory SevVerbose
|
|
|
|
struct RangePartitionedVersionedMessage {
|
|
LogMessageVersion version;
|
|
StringRef message;
|
|
VectorRef<Tag> tags;
|
|
Arena arena;
|
|
|
|
RangePartitionedVersionedMessage(LogMessageVersion v, StringRef m, const VectorRef<Tag>& t, const Arena& a)
|
|
: version(v), message(m), tags(t), arena(a) {}
|
|
|
|
Version getVersion() const { return version.version; }
|
|
size_t getEstimatedSize() const { return message.size() + TagsAndMessage::getHeaderSize(6); }
|
|
|
|
// Returns true if the message is a mutation that could be backed up (normal keys, system key backup ranges, or the
|
|
// metadata version key).
|
|
bool isCandidateBackupMessage(MutationRef* m) {
|
|
// TODO akanksha: Implement this function to filter out messages that are not mutations or not relevant to
|
|
// backup. Need to figure out the what those message can be.
|
|
return true;
|
|
}
|
|
};
|
|
|
|
struct RangePartitionedLogFileInfo {
|
|
UID backupUid;
|
|
int32_t partitionId;
|
|
KeyRange fileKeyRange;
|
|
Version beginVersion;
|
|
Reference<IBackupFile> file;
|
|
int64_t blockEnd = 0;
|
|
};
|
|
|
|
struct RangePartitionedBackupData {
|
|
const UID myId;
|
|
const Tag tag; // tag for this backup worker
|
|
const int totalTags; // Total backup worker tags
|
|
const Version startVersion; // This worker's start version
|
|
const Optional<Version> endVersion; // old epoch's end version (inclusive), or empty for current epoch
|
|
const LogEpoch recruitedEpoch; // current epoch whose tLogs are receiving mutations
|
|
const LogEpoch backupEpoch; // the epoch workers should pull mutations
|
|
LogEpoch oldestBackupEpoch = 0; // oldest epoch that still has data on tLogs for backup to pull
|
|
// Minimumum known committed version in StorageServers.
|
|
Version minKnownCommittedVersion;
|
|
Version savedVersion; // Largest version saved to blob storage
|
|
NotifiedVersion pulledVersion;
|
|
Version logFolderBaseVersion;
|
|
AsyncVar<Reference<LogSystemConsumer>> logSystem;
|
|
AsyncVar<bool> paused; // Track if "backupPausedKey" is set.
|
|
Reference<FlowLock> lock;
|
|
AsyncTrigger doneTrigger;
|
|
AsyncTrigger changedTrigger;
|
|
// Set to true when the worker is shutting down (e.g., worker_removed). Used by uploadData to exit gracefully via
|
|
// allMessageSaved(), letting in-flight file writes and progress commits finish first.
|
|
bool stopped = false;
|
|
Database cx;
|
|
std::vector<RangePartitionedVersionedMessage> messages;
|
|
// Key range to partition ID map, used to determine which partition a mutation belongs to based on its key.
|
|
KeyRangeMap<int> keyRangeToPartitionId;
|
|
// Partition ID to key range map for easy lookup of partition's key range.
|
|
std::unordered_map<int, KeyRange> partitionToKeyRange;
|
|
// KeyRange to backup UID and partition id map needed to create log files for the right backup and partition.
|
|
KeyRangeMap<std::vector<std::pair<UID, int32_t>>> keyRangeToBackupAssignment;
|
|
|
|
struct PerBackupInfo {
|
|
PerBackupInfo() = default;
|
|
PerBackupInfo(RangePartitionedBackupData* data, UID uid, Version v) : self(data), startVersion(v) {
|
|
// Open the container and get the key ranges.
|
|
BackupConfig config(uid);
|
|
container = config.backupContainer().get(data->cx.getReference());
|
|
ranges = config.backupRanges().get(data->cx.getReference());
|
|
TraceEvent("RangePartitionedBWAddBackup", data->myId).detail("BackupID", uid).detail("Version", v);
|
|
}
|
|
|
|
RangePartitionedBackupData* self = nullptr;
|
|
Future<Optional<std::vector<KeyRange>>> ranges; // Key ranges of this backup
|
|
Future<Optional<Reference<IBackupContainer>>> container;
|
|
// Backup request's commit version. Mutations are logged at some version after this.
|
|
Version startVersion = invalidVersion;
|
|
// The next log's begin version.
|
|
Version nextFileBeginVersion = invalidVersion;
|
|
|
|
bool isBackupReady() const { return container.isReady() && ranges.isReady(); }
|
|
|
|
Future<Void> waitBackupReady() { co_await (success(container) && success(ranges)); }
|
|
};
|
|
|
|
// TODO akanksha: Add backups in this map when backup worker receives backup request.
|
|
std::unordered_map<UID, PerBackupInfo> backups; // Backup UID to infos
|
|
|
|
explicit RangePartitionedBackupData(UID id,
|
|
Reference<AsyncVar<ServerDBInfo> const> db,
|
|
const InitializeRangePartitionedBackupRequest& req)
|
|
: myId(id), tag(req.tag), totalTags(req.totalTags), startVersion(req.startVersion), endVersion(req.endVersion),
|
|
recruitedEpoch(req.recruitedEpoch), backupEpoch(req.backupEpoch), minKnownCommittedVersion(invalidVersion),
|
|
savedVersion(req.startVersion - 1), pulledVersion(0), logFolderBaseVersion(invalidVersion), paused(false),
|
|
lock(new FlowLock(SERVER_KNOBS->BACKUP_WORKER_LOCK_BYTES)) {
|
|
cx = openDBOnServer(db, TaskPriority::DefaultEndpoint, LockAware::True);
|
|
}
|
|
|
|
bool pullFinished() const { return endVersion.present() && pulledVersion.get() > endVersion.get(); }
|
|
|
|
Version maxPopVersion() const { return endVersion.present() ? endVersion.get() : minKnownCommittedVersion; }
|
|
|
|
bool allMessageSaved() const { return (endVersion.present() && savedVersion >= endVersion.get()) || stopped; }
|
|
|
|
// Tells uploadData to exit: sets stopped (read by allMessageSaved) and wakes it up via doneTrigger.
|
|
void stop() {
|
|
stopped = true;
|
|
doneTrigger.trigger();
|
|
}
|
|
|
|
// Erases messages and updates lock with memory released.
|
|
void eraseMessages(int num) {
|
|
ASSERT(num <= messages.size());
|
|
if (num == 0)
|
|
return;
|
|
|
|
// Accumulate erased message sizes
|
|
int64_t bytes = 0;
|
|
for (int i = 0; i < num; i++) {
|
|
bytes += messages[i].getEstimatedSize();
|
|
}
|
|
TraceEvent(SevDebugMemory, "RangePartitionedBWMemory", myId)
|
|
.detail("Release", bytes)
|
|
.detail("Total", lock->activePermits());
|
|
lock->release(bytes);
|
|
messages.erase(messages.begin(), messages.begin() + num);
|
|
}
|
|
|
|
void eraseMessagesAfterEndVersion() {
|
|
ASSERT(endVersion.present());
|
|
const Version ver = endVersion.get();
|
|
while (!messages.empty()) {
|
|
if (messages.back().getVersion() > ver) {
|
|
size_t bytes = messages.back().getEstimatedSize();
|
|
TraceEvent(SevDebugMemory, "RangePartitionedBWMemory", myId).detail("Release", bytes);
|
|
lock->release(bytes);
|
|
messages.pop_back();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Inserts a backup's single range into rangeMap.
|
|
template <class T>
|
|
void insertRange(KeyRangeMap<std::set<T>>& keyRangeMap, KeyRangeRef range, T value) {
|
|
for (auto& logRange : keyRangeMap.modify(range)) {
|
|
logRange->value().insert(value);
|
|
}
|
|
for (auto& logRange : keyRangeMap.modify(singleKeyRange(metadataVersionKey))) {
|
|
logRange->value().insert(value);
|
|
}
|
|
TraceEvent("BackupWorkerInsertRange", myId)
|
|
.detail("Value", value)
|
|
.detail("Begin", range.begin)
|
|
.detail("End", range.end);
|
|
}
|
|
|
|
// Finds the intersection between a vector of ranges and a target range.
|
|
// Returns the union of all intersecting portions as a single range.
|
|
// Returns empty Optional if there are no intersections.
|
|
Optional<KeyRange> getKeyRangeIntersection(const std::vector<KeyRange>& ranges, const KeyRange& target) {
|
|
Optional<KeyRange> result;
|
|
|
|
for (const auto& range : ranges) {
|
|
KeyRange intersection = range & target;
|
|
if (intersection.empty()) {
|
|
continue;
|
|
}
|
|
|
|
if (!result.present()) {
|
|
result = intersection;
|
|
} else {
|
|
KeyRef newBegin = std::min(result.get().begin, intersection.begin);
|
|
KeyRef newEnd = std::max(result.get().end, intersection.end);
|
|
result = KeyRange(KeyRangeRef(newBegin, newEnd));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void pop() {
|
|
if (!logSystem.get()) {
|
|
return;
|
|
}
|
|
// Defer the pop in two cases, both to avoid losing mutations a future worker might still need:
|
|
// 1. An older epoch still has work to finish (backupEpoch > oldestBackupEpoch). Wait for that
|
|
// older epoch to catch up before popping from this epoch.
|
|
// 2. We're shutting down (stopped) — our saved progress may not be visible to the next master
|
|
// in time, so let the next worker pop after it re-reads progress safely.
|
|
if (backupEpoch > oldestBackupEpoch || stopped) {
|
|
TraceEvent("RangePartitionedBWPopDeferred", myId)
|
|
.suppressFor(1.0)
|
|
.detail("BackupEpoch", backupEpoch)
|
|
.detail("OldestEpoch", oldestBackupEpoch)
|
|
.detail("Stopped", stopped)
|
|
.detail("Version", savedVersion);
|
|
return;
|
|
}
|
|
ASSERT_WE_THINK(backupEpoch == oldestBackupEpoch);
|
|
logSystem.get()->pop(savedVersion, tag);
|
|
}
|
|
|
|
Future<Void> waitAllBackupsReady() {
|
|
std::vector<Future<Void>> all;
|
|
for (auto& [uid, info] : backups) {
|
|
all.push_back(info.waitBackupReady());
|
|
}
|
|
co_await waitForAll(all);
|
|
}
|
|
|
|
bool isAllBackupsReady() const {
|
|
for (const auto& [uid, info] : backups) {
|
|
if (!info.isBackupReady())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static Future<Void> computeKeyRangeToBackupAssignment(RangePartitionedBackupData* self) {
|
|
self->keyRangeToBackupAssignment = KeyRangeMap<std::vector<std::pair<UID, int32_t>>>();
|
|
|
|
while (!self->isAllBackupsReady()) {
|
|
co_await self->waitAllBackupsReady();
|
|
}
|
|
|
|
for (auto& [uid, info] : self->backups) {
|
|
const auto& backupRanges = info.ranges.get().get();
|
|
|
|
for (auto iter : self->keyRangeToPartitionId.ranges()) {
|
|
int32_t partitionId = iter.value();
|
|
KeyRange partitionRange = iter.range();
|
|
|
|
Optional<KeyRange> intersection = self->getKeyRangeIntersection(backupRanges, partitionRange);
|
|
if (!intersection.present())
|
|
continue;
|
|
|
|
std::pair<UID, int32_t> bk{ uid, partitionId };
|
|
for (auto& range : self->keyRangeToBackupAssignment.modify(intersection.get())) {
|
|
range->value().push_back(bk);
|
|
}
|
|
}
|
|
}
|
|
self->keyRangeToBackupAssignment.coalesce(allKeys);
|
|
}
|
|
|
|
static Future<Void> onBackupChanges(RangePartitionedBackupData* self,
|
|
std::vector<std::pair<UID, Version>> uidVersions) {
|
|
std::unordered_set<UID> activeUids;
|
|
for (const auto& [uid, version] : uidVersions) {
|
|
activeUids.insert(uid);
|
|
}
|
|
|
|
bool modified = false;
|
|
bool hasNewBackup = false;
|
|
Version newBackupsMinVersion = std::numeric_limits<Version>::max();
|
|
|
|
// Add any new backups.
|
|
for (const auto& [uid, version] : uidVersions) {
|
|
if (!self->backups.contains(uid)) {
|
|
self->backups.emplace(uid, RangePartitionedBackupData::PerBackupInfo(self, uid, version));
|
|
modified = true;
|
|
newBackupsMinVersion = std::min(newBackupsMinVersion, version);
|
|
hasNewBackup = true;
|
|
}
|
|
}
|
|
|
|
// Remove backups that are no longer active.
|
|
for (auto it = self->backups.begin(); it != self->backups.end();) {
|
|
if (!activeUids.contains(it->first)) {
|
|
it = self->backups.erase(it);
|
|
modified = true;
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
if (hasNewBackup && self->backupEpoch < self->recruitedEpoch && self->savedVersion + 1 == self->startVersion) {
|
|
// Advance savedVersion to minimize version ranges in case backupEpoch's progress is not saved. Master may set a
|
|
// very low startVersion that is already popped. Advance the version is safe because these versions are not
|
|
// popped -- if they are popped, their progress should be already recorded and Master would use a higher version
|
|
// than minVersion.
|
|
self->savedVersion = std::max(newBackupsMinVersion, self->savedVersion);
|
|
}
|
|
if (modified) {
|
|
self->changedTrigger.trigger();
|
|
co_await computeKeyRangeToBackupAssignment(self);
|
|
}
|
|
}
|
|
|
|
Future<Void> checkRemoved(Reference<AsyncVar<ServerDBInfo> const> db,
|
|
LogEpoch recoveryCount,
|
|
RangePartitionedBackupData* self) {
|
|
while (true) {
|
|
bool isDisplaced =
|
|
db->get().recoveryCount > recoveryCount && db->get().recoveryState != RecoveryState::UNINITIALIZED;
|
|
if (isDisplaced) {
|
|
TraceEvent("RangePartitionedBWDisplaced", self->myId)
|
|
.detail("RecoveryCount", recoveryCount)
|
|
.detail("RecoveryState", (int)db->get().recoveryState);
|
|
throw worker_removed();
|
|
}
|
|
co_await db->onChange();
|
|
}
|
|
}
|
|
|
|
Future<Version> pullPartitionMapFromTLog(RangePartitionedBackupData* self, PartitionMap* outPartitionMap) {
|
|
Reference<IPeekCursor> cursor;
|
|
Version partitionMapVersion = invalidVersion;
|
|
Future<Void> logSystemChange = Void();
|
|
|
|
while (true) {
|
|
while (true) {
|
|
auto res = co_await race(cursor ? cursor->getMore() : Never(), logSystemChange);
|
|
if (res.index() == 0) {
|
|
break;
|
|
} else {
|
|
if (self->logSystem.get()) {
|
|
cursor = self->logSystem.get()->peekSingle(self->myId, self->startVersion, self->tag);
|
|
} else {
|
|
cursor = Reference<IPeekCursor>();
|
|
}
|
|
logSystemChange = self->logSystem.onChange();
|
|
}
|
|
}
|
|
if (!cursor->hasMessage()) {
|
|
continue;
|
|
}
|
|
for (; cursor->hasMessage(); cursor->nextMessage()) {
|
|
Version msgVersion = cursor->version().version;
|
|
StringRef message = cursor->getMessage();
|
|
Arena arena = cursor->arena();
|
|
ArenaReader reader(arena, message, AssumeVersion(g_network->protocolVersion()));
|
|
if (reader.protocolVersion().hasSpanContext() && SpanContextMessage::isNextIn(reader)) {
|
|
continue;
|
|
}
|
|
if (reader.protocolVersion().hasOTELSpanContext() && OTELSpanContextMessage::isNextIn(reader)) {
|
|
continue;
|
|
}
|
|
bool isPartitionMap = PartitionMapMessage::isNextIn(reader);
|
|
ASSERT(isPartitionMap);
|
|
|
|
PartitionMapMessage pmMsg;
|
|
reader >> pmMsg;
|
|
*outPartitionMap = std::move(pmMsg.partitionMap);
|
|
partitionMapVersion = msgVersion;
|
|
co_return partitionMapVersion;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Persist the (epoch, version) -> PartitionMap row to SS so older epoch backup workers can read it during
|
|
// recovery. Multiple workers may call this concurrently for the same (epoch, version) but only one succeed in writing
|
|
// to SS.
|
|
Future<Void> persistPartitionMapToSS(RangePartitionedBackupData* self,
|
|
Version partitionMapVersion,
|
|
PartitionMap const& partitionMap) {
|
|
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(self->cx));
|
|
Key key = backupPartitionMapHistoryKeyFor(self->backupEpoch, partitionMapVersion);
|
|
|
|
BinaryWriter valueWriter(IncludeVersion());
|
|
valueWriter << partitionMap;
|
|
Standalone<StringRef> serialized = valueWriter.toValue();
|
|
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr->setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
|
|
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
|
|
Optional<Value> existing = co_await tr->get(key);
|
|
if (existing.present()) {
|
|
co_return;
|
|
}
|
|
tr->set(key, serialized);
|
|
co_await tr->commit();
|
|
TraceEvent("RangePartitionedBWPMHistoryWritten", self->myId)
|
|
.detail("Epoch", self->backupEpoch)
|
|
.detail("Version", partitionMapVersion)
|
|
.detail("Size", serialized.size());
|
|
co_return;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr->onError(err);
|
|
}
|
|
}
|
|
|
|
// Reads the partition map active at `startVersion` for `epoch` from system keys. Any later re-partitions
|
|
// in this epoch arrive via the TLog cursor like a current-epoch worker, so we only need this one entry.
|
|
// Returns empty if no entry exists for this epoch (e.g., recovery happened before persistPartitionMapToSS).
|
|
Future<Optional<std::pair<Version, PartitionMap>>> loadActivePartitionMapFromSS(RangePartitionedBackupData* self,
|
|
LogEpoch epoch,
|
|
Version startVersion) {
|
|
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(self->cx));
|
|
KeyRange range = backupPartitionMapHistoryRangeFor(epoch);
|
|
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr->setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
|
|
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
|
|
// Goal: find the partition map that was active at this worker's startVersion within `epoch`.
|
|
//
|
|
// Example: epoch=5, startVersion=80. SS has entries at:
|
|
// [epoch=5, v=1], [epoch=5, v=50], [epoch=5, v=90], [epoch=5, v=95]
|
|
// We want the entry at v=50 (largest version <= 80 in epoch 5).
|
|
//
|
|
// getRange is used here so we get both the key and the value back in a single round trip
|
|
// - begin = lastLessOrEqual([epoch=5, v=80])
|
|
// "find the largest actual key in the DB <= this target".
|
|
// For our example, FDB resolves it to [epoch=5, v=50].
|
|
// - end = firstGreaterOrEqual(range.end)
|
|
// range.end = [epoch=6, v=0] (one past this epoch's keys), so the search
|
|
// never reads beyond this epoch.
|
|
// - limit = 1
|
|
// We only need that one entry.
|
|
RangeResult rows =
|
|
co_await tr->getRange(lastLessOrEqual(backupPartitionMapHistoryKeyFor(epoch, startVersion)),
|
|
firstGreaterOrEqual(range.end),
|
|
/*limit=*/1);
|
|
if (rows.empty()) {
|
|
TraceEvent("RangePartitionedBWActivePMNotFound", self->myId)
|
|
.detail("Epoch", epoch)
|
|
.detail("StartVersion", startVersion);
|
|
co_return Optional<std::pair<Version, PartitionMap>>();
|
|
}
|
|
|
|
// If no entry exists in our epoch at or before startVersion, lastLessOrEqual lands on a key from a previous
|
|
// epoch. We detect that by checking the decoded epoch on the result and treat it as "no entry". The caller
|
|
// falls back to pulling the partition map from TLog.
|
|
auto [decodedEpoch, version] = decodeBackupPartitionMapHistoryKey(rows[0].key);
|
|
if (decodedEpoch != epoch) {
|
|
TraceEvent("RangePartitionedBWActivePMNotFound", self->myId)
|
|
.detail("Epoch", epoch)
|
|
.detail("DecodedEpoch", decodedEpoch)
|
|
.detail("DecodedVersion", version)
|
|
.detail("StartVersion", startVersion);
|
|
co_return Optional<std::pair<Version, PartitionMap>>();
|
|
}
|
|
|
|
PartitionMap pm;
|
|
BinaryReader reader(rows[0].value, IncludeVersion());
|
|
reader >> pm;
|
|
|
|
TraceEvent("RangePartitionedBWActivePMRead", self->myId)
|
|
.detail("Epoch", epoch)
|
|
.detail("StartVersion", startVersion)
|
|
.detail("PMVersion", version);
|
|
co_return std::make_pair(version, std::move(pm));
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr->onError(err);
|
|
}
|
|
}
|
|
|
|
// TODO akanksha:
|
|
// 1. Test if concurrent uploads of identical content to the same path in blob storage is safe or not.
|
|
// 2. When folder is advanced to next version, do we upload the partition map again to that version.
|
|
Future<Void> uploadPartitionList(RangePartitionedBackupData* self, PartitionMap partitionMap) {
|
|
std::vector<Future<Void>> fileFutures;
|
|
auto it = self->backups.begin();
|
|
|
|
std::string jsonContent = serializePartitionListJSON(partitionMap);
|
|
|
|
for (; it != self->backups.end();) {
|
|
if (!it->second.container.get().present()) {
|
|
TraceEvent("RangePartitionedBWRemoveContainer", self->myId).detail("BackupId", it->first);
|
|
it = self->backups.erase(it);
|
|
continue;
|
|
}
|
|
Reference<IBackupContainer> container = it->second.container.get().get();
|
|
fileFutures.push_back(container->writePartitionListFile(self->logFolderBaseVersion, jsonContent));
|
|
it++;
|
|
}
|
|
if (fileFutures.empty()) {
|
|
TraceEvent("RangePartitionedBWNoContainers", self->myId);
|
|
co_return;
|
|
}
|
|
|
|
co_await waitForAll(fileFutures);
|
|
}
|
|
|
|
// Persists partitionMap to SS history (so that catch-up backup workers can find it during recovery) and writes the
|
|
// partitionId_keyRange_Map file for every active backup container.
|
|
Future<Void> persistAndUploadPartitionMap(RangePartitionedBackupData* self,
|
|
Version pmVersion,
|
|
PartitionMap const& partitionMap) {
|
|
co_await persistPartitionMapToSS(self, pmVersion, partitionMap);
|
|
co_await uploadPartitionList(self, partitionMap);
|
|
}
|
|
|
|
// Updates local routing state to use the new partition map.
|
|
Future<Void> setActivePartitionMap(RangePartitionedBackupData* self,
|
|
Version pmVersion,
|
|
PartitionMap const& partitionMap) {
|
|
self->logFolderBaseVersion = pmVersion;
|
|
ASSERT(partitionMap.contains(self->tag));
|
|
const auto& tagPartitions = partitionMap.at(self->tag);
|
|
ASSERT_GT(tagPartitions.size(), 0);
|
|
self->keyRangeToPartitionId.clear();
|
|
for (const auto& partition : tagPartitions) {
|
|
self->keyRangeToPartitionId.insert(partition.ranges, partition.partitionId);
|
|
}
|
|
co_await computeKeyRangeToBackupAssignment(self);
|
|
}
|
|
|
|
Future<Void> processPartitionMap(RangePartitionedBackupData* self) {
|
|
TraceEvent("RangePartitionedBWWaitingForPartitionMap", self->myId)
|
|
.detail("Tag", self->tag.toString())
|
|
.detail("StartVersion", self->startVersion)
|
|
.detail("BackupEpoch", self->backupEpoch)
|
|
.detail("RecruitedEpoch", self->recruitedEpoch);
|
|
|
|
PartitionMap partitionMap;
|
|
Version partitionMapVersion;
|
|
Optional<std::pair<Version, PartitionMap>> startPMFromHistory;
|
|
|
|
if (self->backupEpoch != self->recruitedEpoch) {
|
|
// Old epoch worker: the partition map active at our startVersion was persisted by the previous
|
|
// epoch's workers. Read just that one from system keys; any later re-partitions in this epoch
|
|
// will arrive via the TLog cursor like a current-epoch worker.
|
|
startPMFromHistory = co_await loadActivePartitionMapFromSS(self, self->backupEpoch, self->startVersion);
|
|
if (startPMFromHistory.present()) {
|
|
partitionMapVersion = startPMFromHistory.get().first;
|
|
partitionMap = std::move(startPMFromHistory.get().second);
|
|
auto it = partitionMap.find(self->tag);
|
|
ASSERT(it != partitionMap.end() && !it->second.empty());
|
|
TraceEvent("RangePartitionedBWLoadedPartitionMap", self->myId)
|
|
.detail("Epoch", self->backupEpoch)
|
|
.detail("Version", partitionMapVersion)
|
|
.detail("NumTags", partitionMap.size())
|
|
.detail("Tag", self->tag.toString())
|
|
.detail("NumPartitions", it->second.size());
|
|
}
|
|
}
|
|
|
|
if (self->backupEpoch == self->recruitedEpoch || !startPMFromHistory.present()) {
|
|
// Current-epoch worker, or old epoch worker with no SS history (recovery happened before the
|
|
// previous epoch's persistPartitionMapToSS). Receive the partition map via TLog as the first
|
|
// message, then persist it so the next old epoch worker doesn't hit this case.
|
|
partitionMapVersion = co_await pullPartitionMapFromTLog(self, &partitionMap);
|
|
auto it = partitionMap.find(self->tag);
|
|
ASSERT(it != partitionMap.end() && !it->second.empty());
|
|
TraceEvent("RangePartitionedBWPulledPartitionMap", self->myId)
|
|
.detail("Version", partitionMapVersion)
|
|
.detail("NumTags", partitionMap.size())
|
|
.detail("Tag", self->tag.toString())
|
|
.detail("NumPartitions", it->second.size());
|
|
|
|
// Persist the partition map to system key so that catch-up backup workers can read it during recovery.
|
|
// Every BW also writes the partitionId_keyRange_Map file. Content is deterministic across workers
|
|
// through serializePartitionListJSON, so concurrent PUTs of identical bytes are safe.
|
|
co_await persistAndUploadPartitionMap(self, partitionMapVersion, partitionMap);
|
|
TraceEvent("RangePartitionedBWPartitionMapUploaded", self->myId)
|
|
.detail("Version", partitionMapVersion)
|
|
.detail("NumBackups", self->backups.size());
|
|
|
|
self->pulledVersion.set(partitionMapVersion);
|
|
self->savedVersion = partitionMapVersion;
|
|
self->pop();
|
|
}
|
|
|
|
co_await setActivePartitionMap(self, partitionMapVersion, partitionMap);
|
|
}
|
|
|
|
// Pulls mutations from TLog servers.
|
|
Future<Void> pullAsyncData(RangePartitionedBackupData* self) {
|
|
Future<Void> logSystemChange = Void();
|
|
Reference<IPeekCursor> cursor;
|
|
|
|
Version tagAt = std::max({ self->pulledVersion.get(), self->startVersion, self->savedVersion });
|
|
TraceEvent("RangePartitionedBWPull", self->myId)
|
|
.detail("Tag", self->tag)
|
|
.detail("Version", tagAt)
|
|
.detail("StartVersion", self->startVersion)
|
|
.detail("SavedVersion", self->savedVersion);
|
|
|
|
while (true) {
|
|
while (self->paused.get()) {
|
|
co_await self->paused.onChange();
|
|
}
|
|
|
|
while (true) {
|
|
auto res = co_await race(cursor ? cursor->getMore(TaskPriority::TLogCommit) : Never(), logSystemChange);
|
|
if (res.index() == 0) {
|
|
DisabledTraceEvent("RangePartitionedBWGotMore", self->myId)
|
|
.detail("Tag", self->tag)
|
|
.detail("CursorVersion", cursor->version().version);
|
|
break;
|
|
} else {
|
|
if (self->logSystem.get()) {
|
|
cursor = self->logSystem.get()->peekSingle(self->myId, tagAt, self->tag);
|
|
} else {
|
|
cursor = Reference<IPeekCursor>();
|
|
}
|
|
logSystemChange = self->logSystem.onChange();
|
|
}
|
|
}
|
|
|
|
if (cursor->popped() > 0) {
|
|
TraceEvent(SevError, "RangePartitionedBWDataPopped", self->myId)
|
|
.detail("Popped", cursor->popped())
|
|
.detail("Expected", tagAt);
|
|
throw worker_removed();
|
|
}
|
|
|
|
self->minKnownCommittedVersion =
|
|
std::max(self->minKnownCommittedVersion, cursor->getMinKnownCommittedVersion());
|
|
int64_t peekedBytes = 0;
|
|
|
|
// Hold messages until we know how many we can take, self->messages always
|
|
// contains messages that we have reserved memory for. Therefore, lock->release()
|
|
// will always encounter message with reserved memory.
|
|
std::vector<RangePartitionedVersionedMessage> tmpMessages;
|
|
|
|
// Messages may be prefetched in peek here, but uncommitted messages should not be uploaded in uploadData().
|
|
while (cursor->hasMessage()) {
|
|
StringRef rawMessage = cursor->getMessage();
|
|
Arena msgArena = cursor->arena();
|
|
ArenaReader reader(msgArena, rawMessage, AssumeVersion(g_network->protocolVersion()));
|
|
|
|
// Skip metadata-only messages so they don't reach uploadData.
|
|
if (reader.protocolVersion().hasSpanContext() && SpanContextMessage::isNextIn(reader)) {
|
|
cursor->nextMessage();
|
|
continue;
|
|
}
|
|
if (reader.protocolVersion().hasOTELSpanContext() && OTELSpanContextMessage::isNextIn(reader)) {
|
|
cursor->nextMessage();
|
|
continue;
|
|
}
|
|
|
|
// Mid-stream PartitionMap update (re-partition). Persist + upload the new map immediately so
|
|
// future old-epoch workers can find it. The raw message bytes fall through to be buffered
|
|
// into self->messages like any other cursor message; uploadData detects it during its batch
|
|
// and calls setActivePartitionMap at the right version boundary.
|
|
// Do not `cursor->nextMessage()` or `continue` — fall through to buffer this message.
|
|
if (PartitionMapMessage::isNextIn(reader)) {
|
|
Version pmVersion = cursor->version().version;
|
|
PartitionMapMessage pmMsg;
|
|
reader >> pmMsg;
|
|
|
|
co_await persistAndUploadPartitionMap(self, pmVersion, pmMsg.partitionMap);
|
|
|
|
TraceEvent("RangePartitionedBWReceivedMidStreamPM", self->myId)
|
|
.detail("Version", pmVersion)
|
|
.detail("NumPartitions", pmMsg.partitionMap[self->tag].size());
|
|
}
|
|
|
|
auto msg = RangePartitionedVersionedMessage(cursor->version(), rawMessage, cursor->getTags(), msgArena);
|
|
tmpMessages.emplace_back(std::move(msg));
|
|
peekedBytes += tmpMessages.back().getEstimatedSize();
|
|
cursor->nextMessage();
|
|
}
|
|
|
|
if (peekedBytes > 0) {
|
|
TraceEvent(SevDebugMemory, "RangePartitionedBWMemory", self->myId)
|
|
.detail("Take", peekedBytes)
|
|
.detail("Current", self->lock->activePermits());
|
|
co_await self->lock->take(TaskPriority::DefaultYield, peekedBytes);
|
|
self->messages.insert(self->messages.end(),
|
|
std::make_move_iterator(tmpMessages.begin()),
|
|
std::make_move_iterator(tmpMessages.end()));
|
|
}
|
|
|
|
tagAt = cursor->version().version;
|
|
self->pulledVersion.set(tagAt);
|
|
TraceEvent("RangePartitionedBWGot", self->myId).suppressFor(1.0).detail("LatestPulledVersion", tagAt);
|
|
|
|
// For older epochs, we may have an end version to stop at.
|
|
if (self->pullFinished()) {
|
|
self->eraseMessagesAfterEndVersion();
|
|
self->doneTrigger.trigger();
|
|
TraceEvent("RangePartitionedBWFinishPull", self->myId)
|
|
.detail("Tag", self->tag.toString())
|
|
.detail("VersionGot", tagAt)
|
|
.detail("EndVersion", self->endVersion.get())
|
|
.detail("LogEpoch", self->recruitedEpoch)
|
|
.detail("BackupEpoch", self->backupEpoch);
|
|
co_return;
|
|
}
|
|
co_await yield();
|
|
}
|
|
}
|
|
|
|
Future<Void> writeFileHeader(Reference<IBackupFile> logFile, int32_t partitionId, KeyRange range) {
|
|
co_await logFile->append((uint8_t*)&RANGE_PARTITIONED_MLOG_VERSION, sizeof(RANGE_PARTITIONED_MLOG_VERSION));
|
|
|
|
BinaryWriter wr(Unversioned());
|
|
wr << partitionId << range.begin << range.end;
|
|
Standalone<StringRef> header = wr.toValue();
|
|
co_await logFile->append(header.begin(), header.size());
|
|
}
|
|
|
|
Future<Void> addMutation(Reference<IBackupFile> logFile,
|
|
RangePartitionedVersionedMessage message,
|
|
StringRef mutation,
|
|
int64_t* blockEnd,
|
|
int blockSize) {
|
|
// Format: version, subversion, messageSize, message
|
|
int bytes = sizeof(Version) + sizeof(uint32_t) + sizeof(int) + mutation.size();
|
|
|
|
// Convert to big Endianness for version.version, version.sub, and msgSize
|
|
// The decoder assumes 0xFF is the end, so little endian can easily be
|
|
// mistaken as the end. In contrast, big endian for version almost guarantee
|
|
// the first byte is not 0xFF (should always be 0x00).
|
|
BinaryWriter wr(Unversioned());
|
|
wr << bigEndian64(message.version.version) << bigEndian32(message.version.sub) << bigEndian32(mutation.size());
|
|
Standalone<StringRef> mutationHeader = wr.toValue();
|
|
|
|
// Start a new block if needed
|
|
if (logFile->size() + bytes > *blockEnd) {
|
|
const int bytesLeft = *blockEnd - logFile->size();
|
|
if (bytesLeft > 0) {
|
|
Value paddingFFs = fileBackup::makePadding(bytesLeft);
|
|
co_await logFile->append(paddingFFs.begin(), bytesLeft);
|
|
}
|
|
|
|
*blockEnd += blockSize;
|
|
// Block header.
|
|
co_await logFile->append((uint8_t*)&RANGE_PARTITIONED_MLOG_VERSION, sizeof(RANGE_PARTITIONED_MLOG_VERSION));
|
|
}
|
|
co_await logFile->append((void*)mutationHeader.begin(), mutationHeader.size());
|
|
co_await logFile->append(mutation.begin(), mutation.size());
|
|
}
|
|
|
|
static Future<Void> updateLogBytesWritten(RangePartitionedBackupData* self, std::map<UID, int64_t> bytesPerBackup) {
|
|
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(self->cx));
|
|
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr->setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
|
|
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
|
|
for (const auto& [uid, bytes] : bytesPerBackup) {
|
|
BackupConfig config(uid);
|
|
config.logBytesWritten().atomicOp(tr, bytes, MutationRef::AddValue);
|
|
}
|
|
co_await tr->commit();
|
|
co_return;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr->onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Void> saveMutationsToFile(RangePartitionedBackupData* self, Version lastVersionInFile, int numMsg) {
|
|
// Make sure all backups are ready, otherwise mutations will be lost.
|
|
while (!self->isAllBackupsReady()) {
|
|
co_await self->waitAllBackupsReady();
|
|
}
|
|
|
|
std::vector<RangePartitionedLogFileInfo> activeFiles;
|
|
// Map of (backupUid, partitionId) -> index into activeFiles.
|
|
std::map<std::pair<UID, int32_t>, int> fileIndexByBackupPartition;
|
|
int blockSize = SERVER_KNOBS->BACKUP_FILE_BLOCK_BYTES;
|
|
std::vector<Future<Reference<IBackupFile>>> fileFutures;
|
|
|
|
for (auto entry = self->keyRangeToBackupAssignment.ranges().begin();
|
|
entry != self->keyRangeToBackupAssignment.ranges().end();
|
|
++entry) {
|
|
for (const auto& [backupUid, partitionId] : entry->value()) {
|
|
|
|
auto it = self->backups.find(backupUid);
|
|
if (it == self->backups.end() || !it->second.container.get().present()) {
|
|
TraceEvent("RangePartitionedBWRemoveContainerInFileCreation", self->myId).detail("BackupId", backupUid);
|
|
continue;
|
|
}
|
|
|
|
std::pair<UID, int32_t> bpKey(backupUid, partitionId);
|
|
if (fileIndexByBackupPartition.contains(bpKey)) {
|
|
continue;
|
|
}
|
|
|
|
Version fileEndVersion = lastVersionInFile + 1;
|
|
if (it->second.nextFileBeginVersion == invalidVersion) {
|
|
it->second.nextFileBeginVersion = self->savedVersion + 1;
|
|
}
|
|
Version beginVersion = it->second.nextFileBeginVersion;
|
|
|
|
RangePartitionedLogFileInfo lf;
|
|
lf.backupUid = it->first;
|
|
lf.partitionId = partitionId;
|
|
lf.fileKeyRange = entry->range();
|
|
lf.beginVersion = beginVersion;
|
|
lf.blockEnd = 0;
|
|
|
|
activeFiles.push_back(lf);
|
|
fileIndexByBackupPartition[bpKey] = activeFiles.size() - 1;
|
|
fileFutures.push_back(it->second.container.get().get()->writeRangePartitionedLogFile(
|
|
beginVersion, fileEndVersion, self->logFolderBaseVersion, partitionId, blockSize));
|
|
}
|
|
}
|
|
|
|
if (fileFutures.empty()) {
|
|
co_return;
|
|
}
|
|
co_await waitForAll(fileFutures);
|
|
|
|
std::vector<Future<Void>> headerWrites;
|
|
int i;
|
|
for (i = 0; i < activeFiles.size(); i++) {
|
|
activeFiles[i].file = fileFutures[i].get();
|
|
headerWrites.push_back(
|
|
writeFileHeader(activeFiles[i].file, activeFiles[i].partitionId, activeFiles[i].fileKeyRange));
|
|
}
|
|
co_await waitForAll(headerWrites);
|
|
|
|
if (activeFiles.empty()) {
|
|
co_return;
|
|
}
|
|
|
|
// Process mutations
|
|
int idx;
|
|
for (idx = 0; idx < numMsg; idx++) {
|
|
auto& message = self->messages[idx];
|
|
MutationRef m;
|
|
if (!message.isCandidateBackupMessage(&m)) {
|
|
continue;
|
|
}
|
|
|
|
DEBUG_MUTATION("RangePartitionedBWAddMutation", message.version.version, m, self->myId)
|
|
.detail("KCV", self->minKnownCommittedVersion)
|
|
.detail("SavedVersion", self->savedVersion);
|
|
|
|
std::vector<Future<Void>> adds;
|
|
if (m.type != MutationRef::Type::ClearRange) {
|
|
for (const auto& entry : self->keyRangeToBackupAssignment[m.param1]) {
|
|
auto it = fileIndexByBackupPartition.find(entry);
|
|
ASSERT(it != fileIndexByBackupPartition.end());
|
|
|
|
int fileIdx = it->second;
|
|
auto& lf = activeFiles[fileIdx];
|
|
// Different backups may have different start version so need this check before writing.
|
|
if (message.getVersion() >= lf.beginVersion) {
|
|
adds.push_back(addMutation(lf.file, message, message.message, &lf.blockEnd, blockSize));
|
|
}
|
|
}
|
|
} else {
|
|
KeyRangeRef mutationRange(m.param1, m.param2);
|
|
std::unordered_set<int> writtenFiles;
|
|
for (auto range : self->keyRangeToBackupAssignment.intersectingRanges(mutationRange)) {
|
|
for (const auto& entry : range.value()) {
|
|
auto it = fileIndexByBackupPartition.find(entry);
|
|
ASSERT(it != fileIndexByBackupPartition.end());
|
|
|
|
int fileIdx = it->second;
|
|
// For ClearRange, we only need to write the full mutation once for each file.
|
|
if (writtenFiles.contains(fileIdx)) {
|
|
continue;
|
|
}
|
|
auto& lf = activeFiles[fileIdx];
|
|
if (message.getVersion() >= lf.beginVersion) {
|
|
adds.push_back(addMutation(lf.file, message, message.message, &lf.blockEnd, blockSize));
|
|
}
|
|
writtenFiles.insert(fileIdx);
|
|
}
|
|
}
|
|
}
|
|
if (!adds.empty()) {
|
|
co_await waitForAll(adds);
|
|
}
|
|
}
|
|
|
|
// Finish files
|
|
// TODO akanksha: Add FileLevel checksum.
|
|
std::vector<Future<Void>> finished;
|
|
for (auto& lf : activeFiles) {
|
|
finished.push_back(lf.file->finish());
|
|
}
|
|
co_await waitForAll(finished);
|
|
|
|
std::map<UID, int64_t> bytesPerBackup;
|
|
for (auto& lf : activeFiles) {
|
|
auto it = self->backups.find(lf.backupUid);
|
|
if (it != self->backups.end()) {
|
|
it->second.nextFileBeginVersion = lastVersionInFile + 1;
|
|
}
|
|
bytesPerBackup[lf.backupUid] += lf.file->size();
|
|
}
|
|
|
|
co_await updateLogBytesWritten(self, std::move(bytesPerBackup));
|
|
}
|
|
|
|
// It closes the race between getMinBackupVersion's snapshot at master-recruit time and the actual state of
|
|
// backupStartedKey when the old epoch backup worker comes up — specifically the case where backup configuration changed
|
|
// during that window so the backup worker is no longer needed.
|
|
static Future<bool> shouldBackupWorkerExitEarly(RangePartitionedBackupData* self) {
|
|
while (true) {
|
|
ReadYourWritesTransaction tr(self->cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
Optional<Value> value = co_await tr.get(backupStartedKey);
|
|
std::vector<std::pair<UID, Version>> uidVersions;
|
|
if (value.present()) {
|
|
bool shouldExit = self->endVersion.present();
|
|
uidVersions = decodeBackupStartedValue(value.get());
|
|
TraceEvent e("RangePartitionedBWGotStartKey", self->myId);
|
|
int i = 1;
|
|
for (auto [uid, version] : uidVersions) {
|
|
e.detail(format("BackupID%d", i), uid).detail(format("Version%d", i), version);
|
|
i++;
|
|
if (shouldExit && version < self->endVersion.get()) {
|
|
shouldExit = false;
|
|
}
|
|
}
|
|
co_await onBackupChanges(self, uidVersions);
|
|
co_return shouldExit;
|
|
}
|
|
|
|
TraceEvent("RangePartitionedBWEmptyStartKey", self->myId);
|
|
Future<Void> watchFuture = tr.watch(backupStartedKey);
|
|
co_await tr.commit();
|
|
co_await watchFuture;
|
|
break;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
static Future<Void> monitorBackupStartedKeyChanges(RangePartitionedBackupData* self) {
|
|
while (true) {
|
|
ReadYourWritesTransaction tr(self->cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
Optional<Value> value = co_await tr.get(backupStartedKey);
|
|
std::vector<std::pair<UID, Version>> uidVersions;
|
|
if (value.present()) {
|
|
uidVersions = decodeBackupStartedValue(value.get());
|
|
TraceEvent e("RangePartitionedBWGotStartKey", self->myId);
|
|
int i = 1;
|
|
for (auto [uid, version] : uidVersions) {
|
|
e.detail(format("BackupID%d", i), uid).detail(format("Version%d", i), version);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
co_await onBackupChanges(self, uidVersions);
|
|
Future<Void> watchFuture = tr.watch(backupStartedKey);
|
|
co_await tr.commit();
|
|
co_await watchFuture;
|
|
break;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
// This function is used to set backup worker's saved version latestBackupWorkerSavedVersion in BackupConfig.
|
|
Future<Void> setBackupKeys(RangePartitionedBackupData* self, std::map<UID, Version> savedLogVersions) {
|
|
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(self->cx));
|
|
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
tr->setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
|
|
|
|
std::vector<Future<Optional<Version>>> prevBackupWorkerSavedVersions;
|
|
std::vector<BackupConfig> versionConfigs;
|
|
for (const auto& [uid, version] : savedLogVersions) {
|
|
BackupConfig config(uid);
|
|
versionConfigs.emplace_back(config);
|
|
prevBackupWorkerSavedVersions.push_back(config.latestBackupWorkerSavedVersion().get(tr));
|
|
}
|
|
co_await waitForAll(prevBackupWorkerSavedVersions);
|
|
|
|
for (int i = 0; i < prevBackupWorkerSavedVersions.size(); i++) {
|
|
const Version current = savedLogVersions[versionConfigs[i].getUid()];
|
|
if (prevBackupWorkerSavedVersions[i].get().present()) {
|
|
const Version prev = prevBackupWorkerSavedVersions[i].get().get();
|
|
if (prev > current) {
|
|
TraceEvent(SevWarn, "RangePartitionedBWVersionInverse", self->myId)
|
|
.detail("Prev", prev)
|
|
.detail("Current", current);
|
|
}
|
|
}
|
|
if (self->backupEpoch == self->oldestBackupEpoch &&
|
|
(!prevBackupWorkerSavedVersions[i].get().present() ||
|
|
prevBackupWorkerSavedVersions[i].get().get() < current)) {
|
|
TraceEvent("RangePartitionedBWSetVersion", self->myId)
|
|
.detail("BackupID", versionConfigs[i].getUid())
|
|
.detail("Version", current);
|
|
versionConfigs[i].latestBackupWorkerSavedVersion().set(tr, current);
|
|
}
|
|
}
|
|
co_await tr->commit();
|
|
co_return;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr->onError(err);
|
|
}
|
|
}
|
|
|
|
static Future<Void> monitorWorkerPause(RangePartitionedBackupData* self) {
|
|
Reference<ReadYourWritesTransaction> tr(new ReadYourWritesTransaction(self->cx));
|
|
Future<Void> watch;
|
|
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr->setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
tr->setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
|
|
|
|
Optional<Value> value = co_await tr->get(backupPausedKey);
|
|
bool paused = value.present() && value.get() == "1"_sr;
|
|
if (self->paused.get() != paused) {
|
|
TraceEvent(paused ? "RangePartitionedBWPaused" : "RangePartitionedBWResumed", self->myId).log();
|
|
self->paused.set(paused);
|
|
}
|
|
|
|
watch = tr->watch(backupPausedKey);
|
|
co_await tr->commit();
|
|
co_await watch;
|
|
tr->reset();
|
|
continue;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr->onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Void> monitorRangePartitionedBackupProgress(RangePartitionedBackupData* self) {
|
|
Future<Void> interval;
|
|
|
|
while (true) {
|
|
interval = delay(SERVER_KNOBS->WORKER_LOGGING_INTERVAL / 2.0);
|
|
while (self->backups.empty() || !self->logSystem.get()) {
|
|
co_await (self->changedTrigger.onTrigger() || self->logSystem.onChange());
|
|
}
|
|
|
|
// Check all workers have started by checking their progress is larger than the backup's start version.
|
|
Reference<BackupProgress> progress(new BackupProgress(self->myId, {}));
|
|
co_await getBackupProgress(self->cx, self->myId, progress, SevDebug);
|
|
|
|
std::map<Tag, Version> tagVersions = progress->getEpochStatus(self->recruitedEpoch);
|
|
if (tagVersions.size() != self->totalTags) {
|
|
co_await interval;
|
|
continue;
|
|
}
|
|
|
|
std::map<UID, Version> savedLogVersions;
|
|
// update progress so far if previous epochs are done.
|
|
if (self->recruitedEpoch == self->oldestBackupEpoch) {
|
|
Version v = std::numeric_limits<Version>::max();
|
|
// Find the version we can gurantee is fully backed up for all backup workers.
|
|
for (const auto& [tag, version] : tagVersions) {
|
|
v = std::min(v, version);
|
|
}
|
|
|
|
for (auto& [uid, info] : self->backups) {
|
|
savedLogVersions.emplace(uid, v);
|
|
TraceEvent("RangePartitionedBWSavedBackupVersion", self->myId)
|
|
.detail("BackupID", uid)
|
|
.detail("Version", v);
|
|
}
|
|
}
|
|
|
|
Future<Void> setKeys = savedLogVersions.empty() ? Void() : setBackupKeys(self, savedLogVersions);
|
|
co_await (interval && setKeys);
|
|
}
|
|
}
|
|
|
|
Future<Void> saveProgress(RangePartitionedBackupData* self, Version backupVersion) {
|
|
Transaction tr(self->cx);
|
|
Key key = backupProgressKeyFor(self->myId);
|
|
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
// It's critical to save progress immediately so that after a master
|
|
// recovery, the new master can know the progress so far.
|
|
tr.setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr.setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
|
|
// CHECK: Don't save progress if backup workers are disabled
|
|
Optional<Value> backupWorkerEnabled = co_await tr.get(rangePartitionedBackupWorkerEnabledKey);
|
|
if (!backupWorkerEnabled.present() || backupWorkerEnabled.get() == "0"_sr) {
|
|
TraceEvent("RangePartitionedBWProgressSkipped", self->myId).detail("Reason", "BackupWorkersDisabled");
|
|
co_return;
|
|
}
|
|
|
|
WorkerBackupStatus status(self->backupEpoch, backupVersion, self->tag, self->totalTags);
|
|
tr.set(key, backupProgressValue(status));
|
|
tr.addReadConflictRange(singleKeyRange(key));
|
|
co_await tr.commit();
|
|
co_return;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
// Uploads self->messages to storage and updates savedVersion.
|
|
Future<Void> uploadData(RangePartitionedBackupData* self) {
|
|
// Version up to which messages will be popped from tlog.
|
|
Version popVersion = invalidVersion;
|
|
|
|
while (true) {
|
|
// Too large uploadDelay will delay popping tLog data for too long.
|
|
Future<Void> uploadDelay = delay(SERVER_KNOBS->BACKUP_UPLOAD_DELAY);
|
|
|
|
int numMsg = 0;
|
|
// Last Version that we popped from tlog.
|
|
Version lastPopVersion = popVersion;
|
|
// index of last version's end position in self->messages. i.e. the first index of next version???
|
|
int lastVersionIndex = 0;
|
|
// Version just before current popVersion - to find version boundaries.
|
|
Version lastVersion = invalidVersion;
|
|
|
|
for (auto& message : self->messages) {
|
|
// message may be prefetched in peek; uncommitted message should not be uploaded.
|
|
const Version msgVersion = message.getVersion();
|
|
if (msgVersion > self->maxPopVersion()) {
|
|
break;
|
|
}
|
|
if (msgVersion > popVersion) {
|
|
lastVersionIndex = numMsg;
|
|
lastVersion = popVersion;
|
|
popVersion = msgVersion;
|
|
}
|
|
numMsg++;
|
|
}
|
|
if (self->pullFinished()) {
|
|
popVersion = self->endVersion.get();
|
|
} else {
|
|
// make sure file is saved on version boundary
|
|
popVersion = lastVersion;
|
|
numMsg = lastVersionIndex;
|
|
|
|
// If we aren't able to process any messages and the lock is blocking us from
|
|
// queuing more, then we are stuck. This could suggest the lock capacity is too small.
|
|
ASSERT(numMsg > 0 || self->lock->waiters() == 0);
|
|
}
|
|
|
|
// TODO akanksha: Removed redundant check popVersion > lastPopVersion. Remove todo after testing completes.
|
|
if (numMsg > 0 || self->pullFinished()) {
|
|
TraceEvent("RangePartitionedBWSave", self->myId)
|
|
.detail("Version", popVersion)
|
|
.detail("LastPopVersion", lastPopVersion)
|
|
.detail("SavedVersion", self->savedVersion)
|
|
.detail("NumMsg", numMsg)
|
|
.detail("MsgQ", self->messages.size());
|
|
|
|
// Walk the batch and look for any PartitionMapMessage. Whenever we hit one, flush all the
|
|
// mutations before it under the current map, switch to the new map, and drop the message.
|
|
int idx = 0;
|
|
while (idx < numMsg) {
|
|
ArenaReader reader(self->messages[idx].arena,
|
|
self->messages[idx].message,
|
|
AssumeVersion(g_network->protocolVersion()));
|
|
if (!PartitionMapMessage::isNextIn(reader)) {
|
|
idx++;
|
|
continue;
|
|
}
|
|
|
|
Version pmV = self->messages[idx].getVersion();
|
|
if (idx > 0) {
|
|
co_await saveMutationsToFile(self, pmV - 1, idx);
|
|
self->eraseMessages(idx);
|
|
numMsg -= idx;
|
|
}
|
|
|
|
// PartitionMapMessage is now at index 0. Decode, apply, drop.
|
|
ArenaReader pmReader(
|
|
self->messages[0].arena, self->messages[0].message, AssumeVersion(g_network->protocolVersion()));
|
|
PartitionMapMessage pmMsg;
|
|
pmReader >> pmMsg;
|
|
co_await setActivePartitionMap(self, pmV, pmMsg.partitionMap);
|
|
self->eraseMessages(1);
|
|
numMsg -= 1;
|
|
|
|
TraceEvent("RangePartitionedBWAppliedMidStreamPM", self->myId)
|
|
.detail("Version", pmV)
|
|
.detail("NumPartitions", pmMsg.partitionMap[self->tag].size());
|
|
|
|
idx = 0;
|
|
}
|
|
|
|
// Even if numMsg is 0 (pullFinished with nothing left to save), write a final file at endVersion. Restore
|
|
// checks each tag's log files form a continuous range; without this marker, restore would stop at the last
|
|
// real file and miss the tail of this epoch. In-between empty ranges (gaps within [startVersion,
|
|
// endVersion)) are fine — restore only needs the worker's continuous range to extend through endVersion.
|
|
co_await saveMutationsToFile(self, popVersion, numMsg);
|
|
self->eraseMessages(numMsg);
|
|
}
|
|
|
|
if (popVersion > self->savedVersion) {
|
|
co_await saveProgress(self, popVersion);
|
|
TraceEvent("RangePartitionedBWSavedProgress", self->myId)
|
|
.detail("Tag", self->tag.toString())
|
|
.detail("Version", popVersion)
|
|
.detail("MsgQ", self->messages.size());
|
|
self->savedVersion = popVersion;
|
|
self->pop();
|
|
}
|
|
|
|
if (self->allMessageSaved()) {
|
|
co_return;
|
|
}
|
|
|
|
if (!self->pullFinished()) {
|
|
co_await (uploadDelay || self->doneTrigger.onTrigger());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keeps `self->logSystem` and `self->oldestBackupEpoch` in sync with the latest ServerDBInfo.
|
|
static Future<Void> monitorLogSystemFromDbInfo(Reference<AsyncVar<ServerDBInfo> const> db,
|
|
RangePartitionedBackupData* self) {
|
|
while (true) {
|
|
Reference<LogSystem> ls = makeLogSystemFromServerDBInfo(self->myId, db->get(), true);
|
|
if (ls.isValid()) {
|
|
self->logSystem.set(ls->makeConsumer());
|
|
self->oldestBackupEpoch = std::max(self->oldestBackupEpoch, ls->getOldestBackupEpoch());
|
|
TraceEvent("RangePartitionedBWLogSystemUpdate", self->myId)
|
|
.detail("Tag", self->tag.toString())
|
|
.detail("TagLocality", self->tag.locality)
|
|
.detail("OldestEpoch", self->oldestBackupEpoch);
|
|
} else {
|
|
TraceEvent("RangePartitionedBWNoLogSystem", self->myId);
|
|
}
|
|
co_await db->onChange();
|
|
}
|
|
}
|
|
|
|
Future<Void> rangePartitionedBackupWorker(BackupInterface interf,
|
|
InitializeRangePartitionedBackupRequest req,
|
|
Reference<AsyncVar<ServerDBInfo> const> db) {
|
|
RangePartitionedBackupData self(interf.id(), db, req);
|
|
PromiseStream<Future<Void>> addActor;
|
|
Future<Void> error = actorCollection(addActor.getFuture());
|
|
Future<Void> pull;
|
|
Future<Void> done;
|
|
Error err;
|
|
|
|
TraceEvent("RangePartitionedBWStart", self.myId)
|
|
.detail("Tag", req.tag.toString())
|
|
.detail("TotalTags", req.totalTags)
|
|
.detail("StartVersion", req.startVersion)
|
|
.detail("EndVersion", req.endVersion.present() ? req.endVersion.get() : -1)
|
|
.detail("LogEpoch", req.recruitedEpoch)
|
|
.detail("BackupEpoch", req.backupEpoch);
|
|
|
|
try {
|
|
addActor.send(checkRemoved(db, req.recruitedEpoch, &self));
|
|
addActor.send(waitFailureServer(interf.waitFailure.getFuture()));
|
|
|
|
if (req.recruitedEpoch == req.backupEpoch && req.tag.id == 0) {
|
|
addActor.send(monitorRangePartitionedBackupProgress(&self));
|
|
}
|
|
|
|
addActor.send(monitorWorkerPause(&self));
|
|
// Must be sent before processPartitionMap so logSystem is populated before the partition-map peek.
|
|
addActor.send(monitorLogSystemFromDbInfo(db, &self));
|
|
|
|
// First need to call processPartitionMap before starting to pull data, because we need to know the
|
|
// partition assignment.
|
|
co_await processPartitionMap(&self);
|
|
|
|
// If the worker is on an old epoch and all backups starts a version >= the endVersion
|
|
bool exitEarly = co_await shouldBackupWorkerExitEarly(&self);
|
|
TraceEvent("RangePartitionedBWExitEarly", self.myId).detail("ExitEarly", exitEarly);
|
|
if (!exitEarly) {
|
|
addActor.send(monitorBackupStartedKeyChanges(&self));
|
|
}
|
|
|
|
pull = exitEarly ? Void() : pullAsyncData(&self);
|
|
addActor.send(pull);
|
|
done = exitEarly ? Void() : uploadData(&self);
|
|
|
|
while (true) {
|
|
auto res = co_await race(done, error);
|
|
if (res.index() == 0) {
|
|
TraceEvent("RangePartitionedBWDone", self.myId).detail("BackupEpoch", self.backupEpoch);
|
|
// Notify master so that this worker can be removed from log system, then this
|
|
// worker (for an old epoch's unfinished work) can safely exit.
|
|
co_await brokenPromiseToNever(db->get().clusterInterface.notifyBackupWorkerDone.getReply(
|
|
BackupWorkerDoneRequest(self.myId, self.backupEpoch)));
|
|
break;
|
|
}
|
|
}
|
|
co_return;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
|
|
if (err.code() == error_code_worker_removed) {
|
|
pull = Void(); // cancels pulling
|
|
self.stop(); // lets uploadData finish its current upload and exit
|
|
try {
|
|
co_await done;
|
|
} catch (Error& shutdownErr) {
|
|
TraceEvent("RangePartitionedBWShutdownError", self.myId).errorUnsuppressed(shutdownErr);
|
|
}
|
|
}
|
|
TraceEvent("RangePartitionedBWTerminated", self.myId).errorUnsuppressed(err);
|
|
if (err.code() != error_code_actor_cancelled && err.code() != error_code_worker_removed) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
PartitionMap makeSamplePartitionMap() {
|
|
PartitionMap pm;
|
|
pm[Tag(tagLocalityRangePartitionedBackup, 0)] = {
|
|
Partition(0, KeyRangeRef("a"_sr, "c"_sr)),
|
|
Partition(1, KeyRangeRef("c"_sr, "f"_sr)),
|
|
};
|
|
pm[Tag(tagLocalityRangePartitionedBackup, 1)] = {
|
|
Partition(2, KeyRangeRef("f"_sr, "m"_sr)),
|
|
Partition(3, KeyRangeRef("m"_sr, "z"_sr)),
|
|
};
|
|
return pm;
|
|
}
|
|
|
|
void assertPartitionMapsEqual(PartitionMap const& a, PartitionMap const& b) {
|
|
ASSERT_EQ(a.size(), b.size());
|
|
for (auto const& [tag, list] : a) {
|
|
auto it = b.find(tag);
|
|
ASSERT(it != b.end());
|
|
ASSERT_EQ(list.size(), it->second.size());
|
|
for (size_t i = 0; i < list.size(); ++i) {
|
|
ASSERT_EQ(list[i].partitionId, it->second[i].partitionId);
|
|
ASSERT(list[i].ranges == it->second[i].ranges);
|
|
}
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("/RangePartitionedBackupWorker/PartitionMapMessage/RoundTrip") {
|
|
PartitionMap original = makeSamplePartitionMap();
|
|
PartitionMapMessage outgoing(original);
|
|
|
|
BinaryWriter wr(AssumeVersion(g_network->protocolVersion()));
|
|
wr << outgoing;
|
|
Standalone<StringRef> bytes = wr.toValue();
|
|
|
|
ArenaReader reader(bytes.arena(), bytes, AssumeVersion(g_network->protocolVersion()));
|
|
ASSERT(PartitionMapMessage::isNextIn(reader));
|
|
|
|
PartitionMapMessage incoming;
|
|
reader >> incoming;
|
|
assertPartitionMapsEqual(original, incoming.partitionMap);
|
|
return Void();
|
|
}
|
|
|
|
TEST_CASE("/RangePartitionedBackupWorker/PartitionMapMessage/RoundTripEmpty") {
|
|
PartitionMapMessage outgoing(PartitionMap{});
|
|
|
|
BinaryWriter wr(AssumeVersion(g_network->protocolVersion()));
|
|
wr << outgoing;
|
|
Standalone<StringRef> bytes = wr.toValue();
|
|
|
|
ArenaReader reader(bytes.arena(), bytes, AssumeVersion(g_network->protocolVersion()));
|
|
ASSERT(PartitionMapMessage::isNextIn(reader));
|
|
|
|
PartitionMapMessage incoming;
|
|
reader >> incoming;
|
|
ASSERT(incoming.partitionMap.empty());
|
|
return Void();
|
|
}
|
|
|
|
TEST_CASE("/RangePartitionedBackupWorker/PartitionMapMessage/IsNextInLeadingByte") {
|
|
BinaryWriter wr(AssumeVersion(g_network->protocolVersion()));
|
|
PartitionMapMessage outgoing(makeSamplePartitionMap());
|
|
wr << outgoing;
|
|
Standalone<StringRef> bytes = wr.toValue();
|
|
ASSERT(!bytes.empty());
|
|
ASSERT(PartitionMapMessage::startsPartitionMapMessage(bytes[0]));
|
|
|
|
ArenaReader pmmReader(bytes.arena(), bytes, AssumeVersion(g_network->protocolVersion()));
|
|
ASSERT(PartitionMapMessage::isNextIn(pmmReader));
|
|
|
|
uint8_t notPmm = MutationRef::SetValue;
|
|
StringRef otherBytes(¬Pmm, 1);
|
|
Arena arena;
|
|
ArenaReader otherReader(arena, otherBytes, AssumeVersion(g_network->protocolVersion()));
|
|
ASSERT(!PartitionMapMessage::isNextIn(otherReader));
|
|
return Void();
|
|
}
|
|
|
|
// TODO akanksha: Remove once a production caller of rangePartitionedBackupWorker() is wired up;
|
|
// this only exists to keep TEST_CASEs in this file from being dead-stripped from the static lib.
|
|
void forceLinkRangePartitionedBackupWorkerTests() {}
|