foundationdb/fdbserver/tlog/TestTLogServer.cpp

546 lines
22 KiB
C++

/*
* TestTLogServer.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 "TestTLogServer.h"
#include <filesystem>
#include <vector>
#include "fdbrpc/Locality.h"
#include "fdbrpc/ReplicationPolicy.h"
#include "fdbserver/core/ServerDBInfo.h"
#include "fdbserver/kvstore/IDiskQueue.h"
#include "fdbserver/core/Knobs.h"
#include "fdbserver/core/TLogInterface.h"
#include "fdbserver/tlog/TLogServer.h"
#include "fdbserver/core/WorkerInterface.actor.h"
#include "fdbserver/logsystem/LogSystem.h"
#include "fdbserver/logsystem/LogSystemFactory.h"
#include "flow/IRandom.h"
#include "flow/DebugTrace.h"
#include "flow/CoroUtils.h"
namespace {
OldTLogConf buildOldTLogConf(const TLogTestContext& oldCtx, int numLogServers, int8_t primaryLocality) {
OldTLogConf oldTLogConf;
oldTLogConf.tLogs = oldCtx.dbInfo.logSystemConfig.tLogs;
for (int i = 0; i < numLogServers && i < oldTLogConf.tLogs.size(); ++i) {
oldTLogConf.tLogs[i].locality = primaryLocality;
oldTLogConf.tLogs[i].isLocal = true;
}
oldTLogConf.epochBegin = oldCtx.initVersion;
oldTLogConf.epochEnd = oldCtx.numCommits;
oldTLogConf.logRouterTags = 0;
oldTLogConf.recoverAt = oldCtx.initVersion;
oldTLogConf.epoch = oldCtx.epoch;
return oldTLogConf;
}
std::string makeDiskQueueBasename(const TLogTestContext& ctx, UID tLogId) {
return ctx.diskQueueBasename + "." + tLogId.toString() + "." + std::to_string(ctx.epoch) + ".";
}
std::string makeKVStoreBasename(const TestTLogOptions& options, const TLogTestContext& ctx, UID tLogId) {
return options.kvStoreFilename + "." + tLogId.toString() + "." + std::to_string(ctx.epoch) + ".";
}
InitializeTLogRequest buildInitializeRequest(const TLogTestContext& ctx) {
InitializeTLogRequest req;
std::vector<Tag> tags;
tags.reserve(ctx.numTags);
for (uint32_t tagID = 0; tagID < ctx.numTags; tagID++) {
tags.emplace_back(ctx.tagLocality, tagID);
}
req.epoch = 1;
req.allTags = tags;
req.isPrimary = true;
req.locality = ctx.primaryLocality;
req.recoveryTransactionVersion = ctx.initVersion;
return req;
}
struct TempStorageFiles {
std::string queueBase;
std::string queueExt;
std::string kvBase;
std::string kvExt;
bool active{ false };
TempStorageFiles() = default;
TempStorageFiles(std::string queueBase, std::string queueExt, std::string kvBase, std::string kvExt)
: queueBase(std::move(queueBase)), queueExt(std::move(queueExt)), kvBase(std::move(kvBase)),
kvExt(std::move(kvExt)), active(true) {}
~TempStorageFiles() { cleanup(); }
void cleanup() {
if (!active)
return;
if (!queueBase.empty()) {
deleteFile(queueBase + "0." + queueExt);
deleteFile(queueBase + "1." + queueExt);
}
if (!kvBase.empty()) {
deleteFile(kvBase + "0." + kvExt);
deleteFile(kvBase + "1." + kvExt);
}
active = false;
}
};
struct StorageResources {
std::string diskQueueFilename;
std::string kvStoreFilename;
TempStorageFiles tempFiles;
StorageResources() = default;
StorageResources(std::string dq, std::string kv, TempStorageFiles files)
: diskQueueFilename(std::move(dq)), kvStoreFilename(std::move(kv)), tempFiles(files) {}
};
StorageResources setupPersistentStorage(Reference<TLogContext> tLogContext,
const TLogTestContext& testContext,
const TestTLogOptions& options) {
std::string diskQueueFilename = options.dataFolder + "/" + makeDiskQueueBasename(testContext, tLogContext->tLogID);
tLogContext->persistentQueue =
openDiskQueue(diskQueueFilename, options.diskQueueExtension, tLogContext->tLogID, DiskQueueVersion::V2);
std::string kvStoreFilename =
options.dataFolder + "/" + makeKVStoreBasename(options, testContext, tLogContext->tLogID);
tLogContext->persistentData = keyValueStoreMemory(kvStoreFilename,
tLogContext->tLogID,
options.kvMemoryLimit,
options.kvStoreExtension,
KeyValueStoreType::MEMORY_RADIXTREE);
TempStorageFiles tempFiles(
diskQueueFilename, options.diskQueueExtension, kvStoreFilename, options.kvStoreExtension);
return StorageResources(diskQueueFilename, kvStoreFilename, tempFiles);
}
Reference<TLogTestContext> initTLogTestContext(TestTLogOptions tLogOptions,
Optional<Reference<TLogTestContext>> oldTLogTestContext) {
Reference<TLogTestContext> context(new TLogTestContext(tLogOptions));
context->logID = deterministicRandom()->randomUniqueID();
context->workerID = deterministicRandom()->randomUniqueID();
context->diskQueueBasename = tLogOptions.diskQueueBasename;
context->numCommits = tLogOptions.numCommits;
context->numTags = tLogOptions.numTags;
context->numLogServers = tLogOptions.numLogServers;
context->dcID = "test"_sr;
context->tagLocality = context->primaryLocality;
context->dbInfo = ServerDBInfo();
if (oldTLogTestContext.present()) {
const TLogTestContext& oldCtx = *oldTLogTestContext.get();
context->dbInfo.logSystemConfig.oldTLogs.push_back(
buildOldTLogConf(oldCtx, context->numLogServers, oldCtx.primaryLocality));
context->tagLocality = oldCtx.primaryLocality;
context->epoch = oldCtx.epoch + 1;
context->commitHistory = oldCtx.commitHistory;
}
context->dbInfo.logSystemConfig.logSystemType = LogSystemType::tagPartitioned;
context->dbInfo.logSystemConfig.recruitmentID = deterministicRandom()->randomUniqueID();
context->initVersion = tLogOptions.initVersion;
context->recover = tLogOptions.recover;
context->dbInfoRef = makeReference<AsyncVar<ServerDBInfo>>(context->dbInfo);
return context;
}
} // namespace
// Create and start a tLog. If optional parmeters are set, the tLog is a new generation of "tLogID"
// as described by initReq. Otherwise, it is a newborn generation 0 tLog.
Future<Void> getTLogCreateActor(Reference<TLogTestContext> pTLogTestContext,
TestTLogOptions tLogOptions,
uint16_t processID,
InitializeTLogRequest* initReq = nullptr,
UID tLogID = UID()) {
// build per-tLog state.
Reference<TLogContext> pTLogContext = pTLogTestContext->pTLogContextList[processID];
pTLogContext->tagProcessID = processID;
pTLogContext->tLogID = tLogID != UID(0, 0) ? tLogID : deterministicRandom()->randomUniqueID();
TraceEvent("TestTLogServerEnterGetTLogCreateActor", pTLogContext->tLogID).detail("Epoch", pTLogTestContext->epoch);
std::filesystem::create_directory(tLogOptions.dataFolder);
// create persistent storage
StorageResources storage = setupPersistentStorage(pTLogContext, *pTLogTestContext, tLogOptions);
// prepare tLog construction.
Standalone<StringRef> machineID = "machine"_sr;
LocalityData localities(
Optional<Standalone<StringRef>>(), pTLogTestContext->zoneID, machineID, pTLogTestContext->dcID);
localities.set(StringRef("datacenter"_sr), pTLogTestContext->dcID);
Reference<AsyncVar<bool>> isDegraded = FlowTransport::transport().getDegraded();
Reference<AsyncVar<bool>> lowDiskTLogExclusion(new AsyncVar<bool>(false));
Reference<AsyncVar<UID>> activeSharedTLog(new AsyncVar<UID>(pTLogContext->tLogID));
Reference<AsyncVar<bool>> enablePrimaryTxnSystemHealthCheck(new AsyncVar<bool>(false));
PromiseStream<InitializeTLogRequest> promiseStream;
Promise<Void> oldLog;
Promise<Void> recovery;
// construct tLog.
Future<Void> tl = ::tLog(pTLogContext->persistentData,
pTLogContext->persistentQueue,
pTLogTestContext->dbInfoRef,
localities,
promiseStream,
pTLogContext->tLogID,
pTLogTestContext->workerID,
false, /* restoreFromDisk */
oldLog,
recovery,
pTLogTestContext->diskQueueBasename,
isDegraded,
lowDiskTLogExclusion,
activeSharedTLog,
enablePrimaryTxnSystemHealthCheck);
InitializeTLogRequest initTLogReq = initReq != nullptr ? *initReq : buildInitializeRequest(*pTLogTestContext);
TLogInterface interface = co_await promiseStream.getReply(initTLogReq);
pTLogContext->TestTLogInterface = interface;
pTLogContext->init = promiseStream;
// inform other actors tLog is ready.
pTLogContext->TLogCreated.send(true);
TraceEvent("TestTLogServerInitializedTLog", pTLogContext->tLogID);
// wait for either test completion or tLog failure.
auto choice = co_await race(tl, pTLogContext->TestTLogServerCompleted.getFuture());
if (choice.index() == 1) {
bool testCompleted = std::get<1>(choice);
ASSERT_EQ(testCompleted, true);
}
co_await delay(1.0);
storage.tempFiles.cleanup();
}
Future<Void> TLogTestContext::sendPushMessages(TLogTestContext* pTLogTestContext) {
TraceEvent("TestTLogServerEnterPush", pTLogTestContext->workerID);
for (uint16_t logID = 0; logID < pTLogTestContext->numLogServers; ++logID) {
Reference<TLogContext> pTLogContext = pTLogTestContext->pTLogContextList[logID];
bool tLogReady = co_await pTLogContext->TLogStarted.getFuture();
ASSERT_EQ(tLogReady, true);
}
Version prev = pTLogTestContext->initVersion - 1;
Version next = pTLogTestContext->initVersion;
for (uint32_t i = 0; i < pTLogTestContext->numCommits; ++i) {
Standalone<StringRef> key = StringRef(format("key %d", i));
Standalone<StringRef> val = StringRef(format("value %d", i));
MutationRef m(MutationRef::Type::SetValue, key, val);
// build commit request
LogPushData toCommit(pTLogTestContext->ls, pTLogTestContext->numLogServers);
// UID spanID = deterministicRandom()->randomUniqueID();
toCommit.addTransactionInfo(SpanContext());
std::unordered_map<uint16_t, Version> tpcvMap;
uint32_t replicaCount = pTLogTestContext->tLogOptions.replicaCount;
uint32_t startLogServer = 0;
uint32_t teamSize = pTLogTestContext->numLogServers / replicaCount;
for (uint32_t tagID = 0; tagID < pTLogTestContext->numTags; tagID++, startLogServer++) {
for (uint32_t logServer = startLogServer; logServer < pTLogTestContext->numLogServers;
logServer += teamSize) {
// test tLogs advancing versions at different rates
// if (!tagID && i > 5 && i > pTLogTestContext->numCommits / 2) {
// continue;
// }
Tag tag(pTLogTestContext->tagLocality, tagID);
std::vector<Tag> tags = { tag };
toCommit.addTags(tags);
toCommit.writeTypedMessage(m, false, true);
tpcvMap[logServer] = prev;
pTLogTestContext->commitHistory[std::tuple(tagID, logServer)].push_back(next);
TraceEvent("AddTags").detail("C", i).detail("T", tag).detail("L", logServer).detail("N", next);
}
}
if (toCommit.getMutationCount()) {
const auto versionSet = LogPushVersionSet{ prev, next, prev, prev };
Future<Version> loggingComplete =
pTLogTestContext->ls->push(versionSet, toCommit, SpanContext(), UID(), tpcvMap);
Version ver = co_await loggingComplete;
ASSERT_LE(ver, next);
}
prev = next;
pTLogTestContext->tLogOptions.versions.push_back(next);
next += SERVER_KNOBS->VERSIONS_PER_SECOND + deterministicRandom()->randomInt(0, 20);
}
TraceEvent("TestTLogServerExitPush", pTLogTestContext->workerID)
.detail("NumVersions", pTLogTestContext->tLogOptions.versions.size());
}
// send peek/pop through a given TLog interface and tag
Future<Void> TLogTestContext::peekCommitMessages(TLogTestContext* pTLogTestContext, uint16_t tagID, uint32_t logID) {
Reference<TLogContext> pTLogContext = pTLogTestContext->pTLogContextList[logID];
bool tLogReady = co_await pTLogContext->TLogStarted.getFuture();
ASSERT_EQ(tLogReady, true);
// peek from the same tag
Tag tag(pTLogTestContext->tagLocality, tagID);
TraceEvent("TestTLogServerEnterPeek", pTLogTestContext->workerID).detail("LogID", logID).detail("Tag", tag);
uint32_t i = 0;
std::vector<Version> versions = pTLogTestContext->commitHistory[std::tuple(tagID, logID)];
for (Version v : versions) {
Version begin = v;
// wait for next message commit
::TLogPeekRequest request(begin, tag, false, false);
::TLogPeekReply reply = co_await pTLogContext->TestTLogInterface.peekMessages.getReply(request);
TraceEvent("TestTLogServerTryValidateDataOnPeek", pTLogTestContext->workerID)
.detail("R", begin)
.detail("B", reply.begin.present() ? reply.begin.get() : -1)
.detail("P", reply.popped)
.detail("To", pTLogContext->TestTLogInterface.toString());
if (reply.popped.present() && reply.popped.get() > begin) {
break;
}
// validate versions
ASSERT_GE(reply.maxKnownVersion, i);
// deserialize package, first the version header
ArenaReader rd = ArenaReader(reply.arena, reply.messages, AssumeVersion(g_network->protocolVersion()));
ASSERT_EQ(*(int32_t*)rd.peekBytes(4), VERSION_HEADER);
int32_t dummy; // skip past VERSION_HEADER
Version ver;
rd >> dummy >> ver;
// deserialize transaction header
int32_t messageLength;
uint16_t tagCount;
uint32_t sub = 1;
if (FLOW_KNOBS->WRITE_TRACING_ENABLED) {
rd >> messageLength >> sub >> tagCount;
rd.readBytes(tagCount * sizeof(Tag));
// deserialize span id
if (sub == 1) {
SpanContextMessage contextMessage;
rd >> contextMessage;
}
}
// deserialize mutation header
if (sub == 1) {
rd >> messageLength >> sub >> tagCount;
rd.readBytes(tagCount * sizeof(Tag));
}
// deserialize mutation
MutationRef m;
rd >> m;
// validate data
Standalone<StringRef> expectedKey = StringRef(format("key %d", i));
Standalone<StringRef> expectedVal = StringRef(format("value %d", i));
ASSERT_WE_THINK(m.param1 == expectedKey);
ASSERT_WE_THINK(m.param2 == expectedVal);
TraceEvent("TestTLogServerValidatedDataOnPeek", pTLogTestContext->workerID)
.detail("CommitCount", i)
.detail("LogID", logID)
.detail("TagID", tag);
// go directly to pop as there is no SS.
::TLogPopRequest requestPop(begin, begin, tag);
co_await pTLogContext->TestTLogInterface.popMessages.getReply(requestPop);
i++;
}
TraceEvent("TestTLogServerExitPeek", pTLogTestContext->workerID).detail("LogID", logID).detail("TagID", tag);
}
Future<Void> buildTLogSet(Reference<TLogTestContext> pTLogTestContext) {
TLogSet tLogSet;
tLogSet.tLogLocalities.push_back(LocalityData());
tLogSet.tLogPolicy = makeReference<PolicyOne>();
tLogSet.locality = pTLogTestContext->primaryLocality;
tLogSet.isLocal = true;
tLogSet.tLogVersion = TLogVersion::V6;
tLogSet.tLogReplicationFactor = 1;
for (uint16_t processID = 0; processID < pTLogTestContext->numLogServers; ++processID) {
Reference<TLogContext> pTLogContext = pTLogTestContext->pTLogContextList[processID];
bool isCreated = co_await pTLogContext->TLogCreated.getFuture();
ASSERT_EQ(isCreated, true);
tLogSet.tLogs.push_back(OptionalInterface<TLogInterface>(pTLogContext->TestTLogInterface));
}
pTLogTestContext->dbInfo.logSystemConfig.tLogs.push_back(tLogSet);
for (uint16_t processID = 0; processID < pTLogTestContext->numLogServers; ++processID) {
Reference<TLogContext> pTLogContext = pTLogTestContext->pTLogContextList[processID];
// start transactions
pTLogContext->TLogStarted.send(true);
}
}
// This test creates a tLog and pushes data to it. the If the recovery
// test switch is on, a new "generation" of tLogs is then created. These enter recover mode
// and pull data from the old generation. The data is peeked from either the old or new generation
// depending on the recovery switch, validated, and popped.
Future<Void> startTestsTLogRecoveryActors(TestTLogOptions params) {
std::vector<Future<Void>> tLogActors;
Reference<TLogTestContext> pTLogTestContextEpochOne =
initTLogTestContext(params, Optional<Reference<TLogTestContext>>());
FlowTransport::createInstance(false, 1, WLTOKEN_RESERVED_COUNT);
uint16_t tLogIdx = 0;
TraceEvent("TestTLogServerEnterRecoveryTest");
// Create the first "old" generation of tLogs
for (tLogIdx = 0; tLogIdx < pTLogTestContextEpochOne->numLogServers; tLogIdx++) {
Reference<TLogContext> pTLogContext(new TLogContext(tLogIdx));
pTLogTestContextEpochOne->pTLogContextList.push_back(pTLogContext);
tLogActors.emplace_back(
getTLogCreateActor(pTLogTestContextEpochOne, pTLogTestContextEpochOne->tLogOptions, tLogIdx));
}
// wait for tLogs to be created, and signal pushes can start
co_await buildTLogSet(pTLogTestContextEpochOne);
PromiseStream<Future<Void>> promises;
pTLogTestContextEpochOne->ls = makeLogSystemFromServerDBInfo(
pTLogTestContextEpochOne->logID, pTLogTestContextEpochOne->dbInfo, false, promises);
co_await pTLogTestContextEpochOne->sendPushMessages();
uint32_t tagID = 0;
std::map<std::tuple<uint32_t, uint32_t>, std::vector<Version>> commitHistory;
if (!pTLogTestContextEpochOne->recover) {
commitHistory = pTLogTestContextEpochOne->commitHistory;
for (const auto& [loc, ver] : commitHistory) {
co_await pTLogTestContextEpochOne->peekCommitMessages(std::get<0>(loc), std::get<1>(loc));
}
} else {
// Done with old generation. Lock the old generations of tLogs.
Reference<TLogTestContext> pTLogTestContextEpochTwo =
initTLogTestContext(TestTLogOptions(params), pTLogTestContextEpochOne);
// for validation: the expected versions in EpochTwo that were written in EpochOne
pTLogTestContextEpochTwo->tLogOptions.versions = pTLogTestContextEpochOne->tLogOptions.versions;
commitHistory = pTLogTestContextEpochTwo->commitHistory;
pTLogTestContextEpochTwo->pTLogContextList.resize(commitHistory.size());
for (const auto& [loc, ver] : commitHistory) {
tLogIdx = std::get<1>(loc);
tagID = std::get<0>(loc);
TLogLockResult data = co_await pTLogTestContextEpochOne->pTLogContextList[tLogIdx]
->TestTLogInterface.lock.template getReply<TLogLockResult>();
TraceEvent("TestTLogServerLockResult")
.detail("KCV", data.knownCommittedVersion)
.detail("T", tagID)
.detail("L", tLogIdx);
Reference<TLogContext> pNewTLogContext(new TLogContext(tLogIdx));
pTLogTestContextEpochTwo->pTLogContextList[tLogIdx] = pNewTLogContext;
InitializeTLogRequest req;
req.recruitmentID = pTLogTestContextEpochTwo->dbInfo.logSystemConfig.recruitmentID;
req.recoverAt = pTLogTestContextEpochOne->tLogOptions.versions.back();
// for testing + 10;
// wait(pTLogTestContextEpochOne->pTLogContextList[tLogIdx]
// ->TestTLogInterface.setClusterRecoveryVersion.getReply(
// setClusterRecoveryVersionRequest(req.recoverAt + 1)));
req.startVersion = pTLogTestContextEpochOne->initVersion + 1;
req.recoveryTransactionVersion = pTLogTestContextEpochOne->initVersion;
req.knownCommittedVersion = 0;
req.epoch = pTLogTestContextEpochTwo->epoch;
req.logVersion = TLogVersion::V6;
req.locality = pTLogTestContextEpochTwo->primaryLocality;
req.isPrimary = true;
req.logRouterTags = 0;
req.recoverTags = { Tag(pTLogTestContextEpochTwo->primaryLocality, tagID) };
req.recoverFrom = pTLogTestContextEpochOne->dbInfo.logSystemConfig;
req.recoverFrom.logRouterTags = 0;
const TestTLogOptions& tLogOptions = pTLogTestContextEpochTwo->tLogOptions;
tLogActors.emplace_back(getTLogCreateActor(pTLogTestContextEpochTwo,
tLogOptions,
tLogIdx,
&req,
pTLogTestContextEpochOne->pTLogContextList[tLogIdx]->tLogID));
Reference<TLogContext> pTLogContext = pTLogTestContextEpochTwo->pTLogContextList[tLogIdx];
bool isCreated = co_await pTLogContext->TLogCreated.getFuture();
ASSERT_EQ(isCreated, true);
pTLogContext->TLogStarted.send(true);
}
for (const auto& [loc, ver] : commitHistory) {
co_await pTLogTestContextEpochTwo->peekCommitMessages(std::get<0>(loc), std::get<1>(loc));
}
// signal that tLogs can be destroyed
for (tLogIdx = 0; tLogIdx < pTLogTestContextEpochOne->numLogServers; tLogIdx++) {
pTLogTestContextEpochTwo->pTLogContextList[tLogIdx]->TestTLogServerCompleted.send(true);
}
}
for (tLogIdx = 0; tLogIdx < pTLogTestContextEpochOne->numLogServers; tLogIdx++) {
pTLogTestContextEpochOne->pTLogContextList[tLogIdx]->TestTLogServerCompleted.send(true);
}
// wait for tLogs to destruct
co_await waitForAll(tLogActors);
TraceEvent("TestTLogServerExitRecoveryTest");
}
// TEST_CASE("/fdbserver/test/TestTLogCommits") {
// params.set("recover", static_cast<int64_t>(0));
// TestTLogOptions testTLogOptions(params);
// co_await startTestsTLogRecoveryActors(testTLogOptions);
// }
// // Two tlogs, one tag. Write 10 commits up to version V. Start recovery with RV set to V+10.
// TEST_CASE("/fdbserver/test/TestTLogRecovery1") {
// TestTLogOptions testTLogOptions(params);
// co_await startTestsTLogRecoveryActors(testTLogOptions);
// }
// // Four tlogs, two tags.
// TEST_CASE("/fdbserver/test/TestTLogRecovery2") {
// params.set("numLogServers", static_cast<int64_t>(4));
// params.set("numTags", static_cast<int64_t>(2));
// TestTLogOptions testTLogOptions(params);
// co_await startTestsTLogRecoveryActors(testTLogOptions);
// }