Skip to content

Commit 3cc6f16

Browse files
committed
Added comprehensive historical query tests
1 parent bb7e0d6 commit 3cc6f16

10 files changed

Lines changed: 398 additions & 149 deletions

src/ledger/LedgerManagerImpl.cpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2210,15 +2210,20 @@ LedgerManagerImpl::updateCanonicalStateForTesting(LedgerHeader const& header)
22102210
HistoryArchiveState has;
22112211
has.currentLedger = header.ledgerSeq;
22122212

2213-
JITTER_INJECT_DELAY();
2214-
SharedLockExclusive lock(mLedgerStateSnapshotMutex);
2215-
JITTER_INJECT_DELAY();
2213+
CompleteConstLedgerStatePtr state;
2214+
{
2215+
JITTER_INJECT_DELAY();
2216+
SharedLockExclusive lock(mLedgerStateSnapshotMutex);
2217+
JITTER_INJECT_DELAY();
22162218

2217-
auto state = buildLedgerState(header, has, /*sorobanConfig=*/std::nullopt);
2219+
state = buildLedgerState(header, has, /*sorobanConfig=*/std::nullopt);
22182220

2219-
mApplyState.setLedgerStateForTesting(state);
2221+
mApplyState.setLedgerStateForTesting(state);
2222+
2223+
mLastClosedLedgerState = state;
2224+
}
22202225

2221-
mLastClosedLedgerState = state;
2226+
mApp.getCommandHandler().addSnapshot(state);
22222227
}
22232228
#endif
22242229
}

src/ledger/test/LedgerStateSnapshotTests.cpp

Lines changed: 110 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include "ledger/LedgerStateSnapshot.h"
1515
#include "ledger/test/LedgerTestUtils.h"
1616
#include "main/Application.h"
17+
#include "main/CommandHandler.h"
18+
#include "main/QueryServer.h"
1719
#include "test/TestUtils.h"
1820
#include "test/test.h"
1921
#include "util/Logging.h"
@@ -75,9 +77,11 @@ makeHeader(uint32_t seq, uint32_t protocolVersion)
7577
// ---------------------------------------------------------------------------
7678
struct PregenData
7779
{
78-
// Each element is the set of entries to write to the live BucketList
80+
// Each element is the set of new entries to write to the live BucketList
7981
// for a given ledger, in order. Index 0 = first ledger closed.
8082
std::vector<std::vector<LedgerEntry>> liveEntriesToWrite;
83+
// Updates to existing live entries for each ledger.
84+
std::vector<std::vector<LedgerEntry>> liveUpdatesToWrite;
8185
// Same for the hot archive BucketList.
8286
std::vector<std::vector<LedgerEntry>> archiveEntriesToWrite;
8387

@@ -107,6 +111,7 @@ pregenEntries(uint32_t startSeq, int numLedgers, int entriesPerLedger)
107111
auto seq = startSeq + 1 + i;
108112

109113
// --- Live entries ---
114+
// Generate new unique entries for this ledger.
110115
auto entries =
111116
LedgerTestUtils::generateValidUniqueLedgerEntriesWithExclusions(
112117
SOROBAN_TYPES, entriesPerLedger, seenKeys);
@@ -115,8 +120,31 @@ pregenEntries(uint32_t startSeq, int numLedgers, int entriesPerLedger)
115120
e.lastModifiedLedgerSeq = seq;
116121
runningLiveState[LedgerEntryKey(e)] = e;
117122
}
123+
124+
// Modify some existing entries so that adjacent ledgers have
125+
// distinguishable data for the same keys. This ensures that loading
126+
// from the wrong snapshot is detected by the data comparison.
127+
std::vector<LedgerEntry> updates;
128+
if (i > 0)
129+
{
130+
int updated = 0;
131+
for (auto& [key, entry] : runningLiveState)
132+
{
133+
if (entry.lastModifiedLedgerSeq < seq)
134+
{
135+
entry.lastModifiedLedgerSeq = seq;
136+
updates.push_back(entry);
137+
if (++updated >= entriesPerLedger / 2)
138+
{
139+
break;
140+
}
141+
}
142+
}
143+
}
144+
118145
data.stateAtLedger[seq] = runningLiveState;
119146
data.liveEntriesToWrite.push_back(std::move(entries));
147+
data.liveUpdatesToWrite.push_back(std::move(updates));
120148

121149
auto archiveEntries =
122150
LedgerTestUtils::generateValidUniqueLedgerEntriesWithTypes(
@@ -219,7 +247,7 @@ class SnapshotThread
219247
bool
220248
headerMatchesExpected() const NO_THREAD_SAFETY_ANALYSIS
221249
{
222-
return mSnapshot.getLedgerHeader().ledgerSeq == mExpectedSeq;
250+
return mSnapshot.getLedgerHeader().current().ledgerSeq == mExpectedSeq;
223251
}
224252

225253
// --- Mutation operations (take exclusive lock, update mExpectedSeq) ---
@@ -293,7 +321,7 @@ class SnapshotStressTest
293321
{
294322
public:
295323
SnapshotStressTest(int numThreads, unsigned seed, Application& app,
296-
PregenData const& pregen);
324+
PregenData const& pregen, QueryServer& queryServer);
297325
~SnapshotStressTest() = default;
298326

299327
void run();
@@ -320,13 +348,15 @@ class SnapshotStressTest
320348
int const mNumThreads;
321349
unsigned const mSeed;
322350
Application& mApp;
351+
QueryServer& mQueryServer;
323352
uint32_t const mProtocolVersion;
324353
uint32_t const mNumHistorical;
325354
PregenData const& mPregen;
326355

327356
// --- Shared state ---
328357
std::atomic<bool> mDone{false};
329358
std::atomic<bool> mError{false};
359+
std::atomic<int> mHistoricalVerifications{0};
330360
std::vector<std::unique_ptr<SnapshotThread>> mThreads;
331361

332362
bool
@@ -367,10 +397,12 @@ class SnapshotStressTest
367397

368398
SnapshotStressTest::SnapshotStressTest(int numThreads, unsigned seed,
369399
Application& app,
370-
PregenData const& pregen)
400+
PregenData const& pregen,
401+
QueryServer& queryServer)
371402
: mNumThreads(numThreads)
372403
, mSeed(seed)
373404
, mApp(app)
405+
, mQueryServer(queryServer)
374406
, mProtocolVersion(getAppLedgerVersion(app))
375407
, mNumHistorical(app.getConfig().QUERY_SNAPSHOT_LEDGERS)
376408
, mPregen(pregen)
@@ -389,21 +421,39 @@ SnapshotStressTest::SnapshotStressTest(int numThreads, unsigned seed,
389421
void
390422
SnapshotStressTest::run()
391423
{
424+
std::atomic<int> numRegistered{0};
425+
392426
ThreadGroup tg;
393427
for (int t = 0; t < mNumThreads; ++t)
394428
{
395-
tg.launch(1, [this, t]() { workerLoop(t); });
429+
tg.launch(1, [this, t, &numRegistered]() {
430+
mQueryServer.registerThread();
431+
++numRegistered;
432+
433+
// Wait until all threads are registered before proceeding.
434+
while (numRegistered.load(std::memory_order_acquire) < mNumThreads)
435+
{
436+
std::this_thread::yield();
437+
}
438+
workerLoop(t);
439+
});
396440
}
397441
tg.start();
398442
closeLedgers();
399443

400444
// Give workers a brief window to exercise the final state.
401-
std::this_thread::sleep_for(std::chrono::milliseconds{10});
445+
std::this_thread::sleep_for(std::chrono::milliseconds{100});
402446
mDone.store(true, std::memory_order_release);
403447
tg.join();
404448

405449
REQUIRE(!mError.load());
406450

451+
// Ensure historical queries were actually exercised and verified
452+
if (mNumHistorical > 0)
453+
{
454+
REQUIRE(mHistoricalVerifications.load() > 0);
455+
}
456+
407457
// Liveness check: after all ledgers are closed, a fresh snapshot must
408458
// reflect the final ledger sequence.
409459
auto finalSnapshot = mApp.getLedgerManager().copyLedgerStateSnapshot();
@@ -629,7 +679,7 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
629679
auto const& histState = histStateIt->second;
630680

631681
// Build query: positive keys (exist at histSeq) + negative keys
632-
// (exist at currentSeq but not at histSeq). We only need to track
682+
// (exist at a later ledger but not at histSeq). We only need to track
633683
// negativeKeys separately; any queried key not in negativeKeys is
634684
// positive.
635685
std::set<LedgerKey, LedgerEntryIdCmp> queryKeys;
@@ -640,15 +690,17 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
640690
}
641691

642692
std::set<LedgerKey, LedgerEntryIdCmp> negativeKeys;
643-
if (histSeq < currentSeq)
693+
694+
// Add negative keys from nearby ledgers (histSeq+1, histSeq+2, etc.)
695+
// to catch off-by-one bugs in snapshot selection.
696+
for (uint32_t futureSeq = histSeq + 1;
697+
futureSeq <= std::min(histSeq + 3, currentSeq); ++futureSeq)
644698
{
645-
auto curStateIt = stateMap.find(currentSeq);
646-
if (curStateIt != stateMap.end())
699+
auto futureStateIt = stateMap.find(futureSeq);
700+
if (futureStateIt != stateMap.end())
647701
{
648-
auto const& curState = curStateIt->second;
649-
for (int c = 0; c < 5; c++)
702+
for (auto const& [key, _] : futureStateIt->second)
650703
{
651-
auto const& [key, _] = randMapEntry(curState, rng);
652704
if (histState.find(key) == histState.end())
653705
{
654706
queryKeys.insert(key);
@@ -658,33 +710,40 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
658710
}
659711
}
660712

661-
// Call the appropriate bulk historical load and extract LedgerEntries
662-
// from the result into a uniform map for verification.
663-
bool retained = shouldHistoricalExist(currentSeq, histSeq);
713+
// Look up the historical snapshot from the QueryServer. Use the QS's
714+
// latest seq to determine the expected window: there is a brief window
715+
// where the LedgerManager has advanced to seq N but addSnapshot(N) hasn't
716+
// been called yet, so the worker's currentSeq may be ahead of the QS.
717+
auto* latestSnapshot =
718+
mQueryServer.getSnapshotForLedgerForTesting(std::nullopt);
719+
releaseAssert(latestSnapshot);
720+
auto qsCurrentSeq = latestSnapshot->getLedgerSeq();
721+
722+
bool retained = shouldHistoricalExist(qsCurrentSeq, histSeq);
723+
auto* histSnapshot = mQueryServer.getSnapshotForLedgerForTesting(histSeq);
724+
725+
// We use lazy GC for the per-thread cache, so it's possible we retain
726+
// something outside the window.
727+
if (!retained && !histSnapshot)
728+
{
729+
return;
730+
}
731+
if (!histSnapshot)
732+
{
733+
fail(fmt::format("{} unexpected nullptr histSeq={} "
734+
"currentSeq={} seed={}",
735+
opName, histSeq, currentSeq, mSeed));
736+
return;
737+
}
738+
739+
// Load from the historical snapshot and extract LedgerEntries
740+
// into a uniform map for verification.
664741
UnorderedMap<LedgerKey, LedgerEntry> resultMap;
665742

666743
if (archive)
667744
{
668-
auto result =
669-
sthread.snapshot().loadArchiveKeysFromLedger(queryKeys, histSeq);
670-
if (!retained)
671-
{
672-
if (result.has_value())
673-
{
674-
fail(fmt::format("{} expected nullopt histSeq={} "
675-
"currentSeq={} seed={}",
676-
opName, histSeq, currentSeq, mSeed));
677-
}
678-
return;
679-
}
680-
if (!result.has_value())
681-
{
682-
fail(fmt::format("{} unexpected nullopt histSeq={} "
683-
"currentSeq={} seed={}",
684-
opName, histSeq, currentSeq, mSeed));
685-
return;
686-
}
687-
for (auto const& habe : *result)
745+
auto result = histSnapshot->loadArchiveKeys(queryKeys);
746+
for (auto const& habe : result)
688747
{
689748
if (habe.type() != HOT_ARCHIVE_ARCHIVED)
690749
{
@@ -698,26 +757,8 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
698757
}
699758
else
700759
{
701-
auto result =
702-
sthread.snapshot().loadLiveKeysFromLedger(queryKeys, histSeq);
703-
if (!retained)
704-
{
705-
if (result.has_value())
706-
{
707-
fail(fmt::format("{} expected nullopt histSeq={} "
708-
"currentSeq={} seed={}",
709-
opName, histSeq, currentSeq, mSeed));
710-
}
711-
return;
712-
}
713-
if (!result.has_value())
714-
{
715-
fail(fmt::format("{} unexpected nullopt histSeq={} "
716-
"currentSeq={} seed={}",
717-
opName, histSeq, currentSeq, mSeed));
718-
return;
719-
}
720-
for (auto const& entry : *result)
760+
auto result = histSnapshot->loadLiveKeys(queryKeys, "hist-query");
761+
for (auto const& entry : result)
721762
{
722763
resultMap[LedgerEntryKey(entry)] = entry;
723764
}
@@ -757,6 +798,8 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
757798
}
758799
}
759800
}
801+
802+
++mHistoricalVerifications;
760803
}
761804

762805
// Copy a snapshot from a random peer thread. The peer's copySnapshot()
@@ -794,9 +837,10 @@ SnapshotStressTest::checkSelfConsistency(SnapshotThread const& sthread)
794837
}
795838
if (!sthread.headerMatchesExpected())
796839
{
797-
fail(fmt::format("header/seq mismatch {}/{} seed={}",
798-
sthread.snapshot().getLedgerHeader().ledgerSeq,
799-
sthread.expectedSeq(), mSeed));
840+
fail(fmt::format(
841+
"header/seq mismatch {}/{} seed={}",
842+
sthread.snapshot().getLedgerHeader().current().ledgerSeq,
843+
sthread.expectedSeq(), mSeed));
800844
}
801845
}
802846

@@ -817,9 +861,9 @@ SnapshotStressTest::closeLedgers()
817861
// Add both live and archive batches to their bucket lists, then
818862
// update the canonical state once so the snapshot atomically
819863
// includes both.
820-
bm.getLiveBucketList().addBatch(mApp, header.ledgerSeq,
821-
header.ledgerVersion,
822-
mPregen.liveEntriesToWrite[i], {}, {});
864+
bm.getLiveBucketList().addBatch(
865+
mApp, header.ledgerSeq, header.ledgerVersion,
866+
mPregen.liveEntriesToWrite[i], mPregen.liveUpdatesToWrite[i], {});
823867
if (i < mPregen.archiveEntriesToWrite.size())
824868
{
825869
bm.getHotArchiveBucketList().addBatch(
@@ -963,11 +1007,14 @@ TEST_CASE("snapshot concurrent stress test", "[snapshot][acceptance]")
9631007
VirtualClock clock;
9641008
auto cfg = getTestConfig();
9651009
cfg.QUERY_SNAPSHOT_LEDGERS = numHistorical;
1010+
cfg.QUERY_SERVER_FOR_TESTING = true;
9661011
auto app = createTestApplication<BucketTestApplication>(clock, cfg);
9671012
auto startSeq = app->getLedgerManager().getLastClosedLedgerNum();
9681013
auto pregen = pregenEntries(startSeq, NUM_LEDGERS, ENTRIES_PER_LEDGER);
9691014

970-
SnapshotStressTest test(NUM_THREADS, seed, *app, pregen);
1015+
auto& qServer = app->getCommandHandler().getQueryServer();
1016+
1017+
SnapshotStressTest test(NUM_THREADS, seed, *app, pregen, qServer);
9711018
test.run();
9721019
}
9731020

src/main/ApplicationImpl.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,17 @@ ApplicationImpl::start()
809809
CLOG_INFO(Ledger, "Starting up application");
810810
mStarted = true;
811811

812+
#ifdef BUILD_TESTS
813+
// In tests, newDB() and loadLastKnownLedger() both run in the same
814+
// process, causing advanceLastClosedLedgerState to push the same LCL
815+
// seq to the QueryServer twice. Clear the QS state so
816+
// loadLastKnownLedger starts fresh.
817+
if (getConfig().QUERY_SERVER_FOR_TESTING)
818+
{
819+
mCommandHandler->getQueryServer().resetForTesting();
820+
}
821+
#endif
822+
812823
mLedgerManager->loadLastKnownLedger();
813824

814825
// LCL is now loaded; unblock HTTP endpoints that were gated during boot.

0 commit comments

Comments
 (0)