foundationdb/fdbserver/TagPartitionedLogSystemReco...

460 lines
18 KiB
C++

/*
* TagPartitionedLogSystemRecoveryTests.actor.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 "fdbserver/TagPartitionedLogSystem.actor.h"
#include "flow/UnitTest.h"
#include "flow/actorcompiler.h" // This must be the last #include.
namespace {
Reference<LogSet> makeSingleLogSet(const std::vector<TLogInterface>& tlogs, bool isLocal = true) {
Reference<LogSet> logSet = makeReference<LogSet>();
logSet->isLocal = isLocal;
for (const auto& tlog : tlogs) {
logSet->logServers.push_back(
makeReference<AsyncVar<OptionalInterface<TLogInterface>>>(OptionalInterface<TLogInterface>(tlog)));
}
return logSet;
}
std::tuple<int, std::vector<TLogLockResult>, bool> makeLogGroupResults(
int replicationFactor,
const std::vector<std::vector<UnknownCommittedVersions>>& perTLogUCV,
const std::vector<TLogInterface>& tlogs,
bool nonAvailableTLogsCompletePolicy = true,
const std::vector<Version>& knownCommitted = {}) {
std::vector<TLogLockResult> lockResults;
lockResults.reserve(tlogs.size());
for (int i = 0; i < tlogs.size(); ++i) {
TLogLockResult result;
result.logId = tlogs[i].id();
result.knownCommittedVersion = (i < knownCommitted.size()) ? knownCommitted[i] : 0;
for (const auto& ucv : perTLogUCV[i]) {
result.unknownCommittedVersions.push_back(ucv);
}
lockResults.push_back(result);
}
return std::make_tuple(replicationFactor, std::move(lockResults), nonAvailableTLogsCompletePolicy);
}
} // namespace
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/Simple") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// Construct two local tLogs backed by a single LogSet.
// Both tLogs have known committed version 100 and report a higher durable
// version 110 that was sent to both log servers.
LocalityData locality;
TLogInterface tlogA(locality);
TLogInterface tlogB(locality);
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ tlogA, tlogB }) };
UnknownCommittedVersions ucv(110, 100, std::vector<uint16_t>{ 0, 1 });
auto logGroupResults = makeLogGroupResults(2, { { ucv }, { ucv } }, { tlogA, tlogB }, true, { 100, 100 });
Version minDV = 90;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 100) {
TraceEvent(SevError, "SimpleTestMaxKCVFailed").detail("Expected", 100).detail("Got", maxKCV);
}
ASSERT(maxKCV == 100);
if (recoverVersion != 110) {
TraceEvent(SevError, "SimpleTestRecoverVersionFailed").detail("Expected", 110).detail("Got", recoverVersion);
}
ASSERT(recoverVersion == 110);
return Void();
}
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/FallbackToMaxKCV") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// When no unknown committed versions are reported, should fall back to maxKCV
LocalityData locality;
TLogInterface tlogA(locality);
TLogInterface tlogB(locality);
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ tlogA, tlogB }) };
auto logGroupResults = makeLogGroupResults(2, { {}, {} }, { tlogA, tlogB }, true, { 80, 90 });
Version minDV = 70;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 90) {
TraceEvent(SevError, "FallbackTestMaxKCVFailed").detail("Expected", 90).detail("Got", maxKCV);
}
ASSERT(maxKCV == 90);
if (recoverVersion != 90) {
TraceEvent(SevError, "FallbackTestRecoverVersionFailed").detail("Expected", 90).detail("Got", recoverVersion);
}
ASSERT(recoverVersion == 90);
return Void();
}
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/HaltOnMissingDelivery") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// When an available tLog didn't receive a version, recovery should halt at the previous version
LocalityData locality;
TLogInterface tlogA(locality);
TLogInterface tlogB(locality);
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ tlogA, tlogB }) };
UnknownCommittedVersions ucv(110, 100, std::vector<uint16_t>{ 0, 1 });
UnknownCommittedVersions ucvLate(120, 110, std::vector<uint16_t>{ 0, 1 });
// Only tlogA reports the 120 version (tlogB missed it).
auto logGroupResults = makeLogGroupResults(2, { { ucv, ucvLate }, { ucv } }, { tlogA, tlogB }, true, { 100, 100 });
Version minDV = 90;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 100) {
TraceEvent(SevError, "MissingDeliveryTestMaxKCVFailed").detail("Expected", 100).detail("Got", maxKCV);
}
ASSERT(maxKCV == 100);
// Because not all available tLogs received 120, the recovery version should stay at 110.
if (recoverVersion != 110) {
TraceEvent(SevError, "MissingDeliveryTestRecoverVersionFailed")
.detail("Expected", 110)
.detail("Got", recoverVersion)
.detail("Reason", "tlogB did not receive version 120");
}
ASSERT(recoverVersion == 110);
return Void();
}
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/PolicyNotSatisfied") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// When a version was sent to both tLogs but only received by one (insufficient for RF=2)
LocalityData locality;
TLogInterface tlogA(locality);
TLogInterface tlogB(locality);
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ tlogA, tlogB }) };
UnknownCommittedVersions ucv(110, 100, std::vector<uint16_t>{ 0, 1 });
UnknownCommittedVersions ucv2(120, 110, std::vector<uint16_t>{ 0, 1 });
// Version 120 was sent to BOTH tLogs but only tlogA received it.
// With replication factor 2, we need both to receive it.
auto logGroupResults = makeLogGroupResults(2, { { ucv, ucv2 }, { ucv } }, { tlogA, tlogB }, true, { 100, 100 });
Version minDV = 90;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 100) {
TraceEvent(SevError, "PolicyNotSatisfiedTestMaxKCVFailed").detail("Expected", 100).detail("Got", maxKCV);
}
ASSERT(maxKCV == 100);
if (recoverVersion != 110) {
TraceEvent(SevError, "PolicyNotSatisfiedTestRecoverVersionFailed")
.detail("Expected", 110)
.detail("Got", recoverVersion)
.detail("Reason", "Version 120 sent to both tLogs but only received by 1 (RF=2)");
}
ASSERT(recoverVersion == 110);
return Void();
}
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/MinDVRespected") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// Tests that recovery version respects maxKCV when minDV < maxKCV
LocalityData locality;
TLogInterface tlogA(locality);
TLogInterface tlogB(locality);
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ tlogA, tlogB }) };
UnknownCommittedVersions ucv(95, 90, std::vector<uint16_t>{ 0, 1 });
auto logGroupResults = makeLogGroupResults(2, { { ucv }, { ucv } }, { tlogA, tlogB }, true, { 90, 90 });
Version minDV = 80;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 90) {
TraceEvent(SevError, "MinDVRespectedTestMaxKCVFailed").detail("Expected", 90).detail("Got", maxKCV);
}
ASSERT(maxKCV == 90);
if (recoverVersion != 95) {
TraceEvent(SevError, "MinDVRespectedTestRecoverVersionFailed")
.detail("Expected", 95)
.detail("Got", recoverVersion)
.detail("MinDV", minDV)
.detail("MaxKCV", maxKCV);
}
ASSERT(recoverVersion == 95);
return Void();
}
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/BrokenChain") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// Tests that recovery halts when prevVersion chain is broken
LocalityData locality;
TLogInterface tlogA(locality);
TLogInterface tlogB(locality);
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ tlogA, tlogB }) };
// Version 110 and 120 sent to both, but 120's prevVersion != 110 (broken chain)
UnknownCommittedVersions ucv110(110, 100, std::vector<uint16_t>{ 0, 1 });
UnknownCommittedVersions ucv120(120, 115, std::vector<uint16_t>{ 0, 1 }); // prevVersion=115, not 110!
auto logGroupResults =
makeLogGroupResults(2, { { ucv110, ucv120 }, { ucv110, ucv120 } }, { tlogA, tlogB }, true, { 100, 100 });
Version minDV = 90;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 100) {
TraceEvent(SevError, "BrokenChainTestMaxKCVFailed").detail("Expected", 100).detail("Got", maxKCV);
}
ASSERT(maxKCV == 100);
// Should stop at 110 because prevVersion chain breaks at 120
if (recoverVersion != 110) {
TraceEvent(SevError, "BrokenChainTestRecoverVersionFailed")
.detail("Expected", 110)
.detail("Got", recoverVersion)
.detail("Reason", "Version 120 has prevVersion=115, expected 110");
}
ASSERT(recoverVersion == 110);
return Void();
}
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/MultipleLogSets") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// Tests recovery with multiple LogSets (primary + satellite)
LocalityData locality;
TLogInterface primary1(locality), primary2(locality);
TLogInterface satellite1(locality), satellite2(locality);
// Two LogSets: primary (local) + satellite (non-local)
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ primary1, primary2 }, true),
makeSingleLogSet({ satellite1, satellite2 }, false) };
// Only the 2 primary tLogs report version 110 (satellite LogSet is non-local and not in logGroupResults)
UnknownCommittedVersions ucv(110, 100, std::vector<uint16_t>{ 0, 1 });
auto logGroupResults = makeLogGroupResults(2, { { ucv }, { ucv } }, { primary1, primary2 }, true, { 100, 100 });
Version minDV = 90;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 100) {
TraceEvent(SevError, "MultipleLogSetsTestMaxKCVFailed").detail("Expected", 100).detail("Got", maxKCV);
}
ASSERT(maxKCV == 100);
if (recoverVersion != 110) {
TraceEvent(SevError, "MultipleLogSetsTestRecoverVersionFailed")
.detail("Expected", 110)
.detail("Got", recoverVersion)
.detail("NumLogSets", logServers.size());
}
ASSERT(recoverVersion == 110);
return Void();
}
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/PartialAvailabilityPolicyFail") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// Tests that available tLogs must satisfy replication policy
LocalityData locality;
TLogInterface tlogA(locality), tlogB(locality), tlogC(locality);
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ tlogA, tlogB, tlogC }) };
// Version 120 sent to all 3, but only 2 of 3 received it (not enough for RF=3)
UnknownCommittedVersions ucv110(110, 100, std::vector<uint16_t>{ 0, 1, 2 });
UnknownCommittedVersions ucv120(120, 110, std::vector<uint16_t>{ 0, 1, 2 });
// Only tlogA and tlogB report receiving 120
auto logGroupResults = makeLogGroupResults(
3, { { ucv110, ucv120 }, { ucv110, ucv120 }, { ucv110 } }, { tlogA, tlogB, tlogC }, false, { 100, 100, 100 });
Version minDV = 90;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 100) {
TraceEvent(SevError, "PartialAvailabilityTestMaxKCVFailed").detail("Expected", 100).detail("Got", maxKCV);
}
ASSERT(maxKCV == 100);
// Should stay at 110 because 120 doesn't satisfy RF=3
if (recoverVersion != 110) {
TraceEvent(SevError, "PartialAvailabilityTestRecoverVersionFailed")
.detail("Expected", 110)
.detail("Got", recoverVersion)
.detail("Reason", "Only 2 of 3 tLogs received version 120 (RF=3)");
}
ASSERT(recoverVersion == 110);
return Void();
}
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/VersionsBelowMaxKCV") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// Tests that versions <= maxKCV are filtered out
LocalityData locality;
TLogInterface tlogA(locality);
TLogInterface tlogB(locality);
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ tlogA, tlogB }) };
// Report version 80 (below maxKCV=100), should be ignored
UnknownCommittedVersions ucv80(80, 70, std::vector<uint16_t>{ 0, 1 });
auto logGroupResults = makeLogGroupResults(2, { { ucv80 }, { ucv80 } }, { tlogA, tlogB }, true, { 100, 100 });
Version minDV = 90;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 100) {
TraceEvent(SevError, "VersionsBelowMaxKCVTestMaxKCVFailed").detail("Expected", 100).detail("Got", maxKCV);
}
ASSERT(maxKCV == 100);
// Should fall back to maxKCV since all UCVs are <= maxKCV
if (recoverVersion != 100) {
TraceEvent(SevError, "VersionsBelowMaxKCVTestRecoverVersionFailed")
.detail("Expected", 100)
.detail("Got", recoverVersion)
.detail("Reason", "All UCVs below maxKCV should be filtered");
}
ASSERT(recoverVersion == 100);
return Void();
}
TEST_CASE("/TagPartitionedLogSystem/GetRecoverVersionUnicast/RandomVersionsPartialDelivery") {
if (!SERVER_KNOBS->ENABLE_VERSION_VECTOR_TLOG_UNICAST) {
return Void();
}
// Tests that recovery handles random versions in range (maxKCV, highest_version) that:
// 1. Are not reported at all (missing from UCVs)
// 2. Are only received by a subset of logs (partial delivery)
LocalityData locality;
TLogInterface tlogA(locality);
TLogInterface tlogB(locality);
TLogInterface tlogC(locality);
TLogInterface tlogD(locality);
std::vector<Reference<LogSet>> logServers{ makeSingleLogSet({ tlogA, tlogB, tlogC, tlogD }) };
// Setup: maxKCV=100, potential versions 110, 112, 115, 118, 120, 125
// Version 110: received by tlogA and tlogB (full delivery)
// Version 112: sent to tlogC and tlogD, but NOT REPORTED at all (missing from UCVs)
// Version 115: only received by tlogA (partial delivery - indicated by tLogLocIds={0})
// Version 118: sent to tlogC and tlogD, but NOT REPORTED at all (missing from UCVs)
// Version 120: received by tlogA and tlogB (full delivery)
// Version 125: only received by tlogA (partial delivery - indicated by tLogLocIds={0})
UnknownCommittedVersions ucv110(110, 100, std::vector<uint16_t>{ 0, 1 });
// Version 112 is missing - not in any UCV list
UnknownCommittedVersions ucv115(115, 112, std::vector<uint16_t>{ 0 }); // Only tlogA (loc 0)
// Version 118 is missing - not in any UCV list
UnknownCommittedVersions ucv120(120, 118, std::vector<uint16_t>{ 0, 1 });
UnknownCommittedVersions ucv125(125, 120, std::vector<uint16_t>{ 0 }); // Only tlogA (loc 0)
// tlogA reports versions 110, 115, 120, 125
// tlogB only reports versions 110, 120 (missing 115, 125)
// tlogC doesn't report any versions
// tlogD doesn't report any versions
auto logGroupResults = makeLogGroupResults(2,
{ { ucv110, ucv115, ucv120, ucv125 }, { ucv110, ucv120 }, {}, {} },
{ tlogA, tlogB, tlogC, tlogD },
true,
{ 100, 100, 100, 100 });
Version minDV = 90;
Optional<std::tuple<Version, Version>> result = getRecoverVersionUnicast(logServers, logGroupResults, minDV);
ASSERT(result.present());
Version maxKCV = std::get<0>(result.get());
Version recoverVersion = std::get<1>(result.get());
if (maxKCV != 100) {
TraceEvent(SevError, "RandomVersionsPartialDeliveryTestMaxKCVFailed")
.detail("Expected", 100)
.detail("Got", maxKCV);
}
ASSERT(maxKCV == 100);
// Recovery should stop at 110 because:
// - 110 was received by tlogA and tlogB (satisfies RF=2)
// - 112 was not received by tlogC and tlogD
// - 115 was only received by tlogA (tLogLocIds={0}, doesn't satisfy RF=2)
// Even though 120 was received by both tlogA and tlogB, the prevVersion chain requires 112, 115, and 118
if (recoverVersion != 110) {
TraceEvent(SevError, "RandomVersionsPartialDeliveryTestRecoverVersionFailed")
.detail("Expected", 110)
.detail("Got", recoverVersion)
.detail("Reason",
"Missing versions 112 and 118, break recovery before 120. "
"Version 115 received by only tlogA (subset), also breaks recovery before 120. ");
}
ASSERT(recoverVersion == 110);
return Void();
}