Skip to content

Commit b01b1a3

Browse files
committed
Added comprehensive historical query tests
1 parent 5957cda commit b01b1a3

10 files changed

Lines changed: 394 additions & 146 deletions

src/ledger/ImmutableLedgerView.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,8 @@ ImmutableLedgerData::createAndMaybeLoadConfig(
279279
ImmutableLedgerView tempView(tempState, metrics);
280280
sorobanConfig = SorobanNetworkConfig::loadFromLedger(tempView);
281281
}
282-
return std::make_shared<ImmutableLedgerData>(
283-
liveBL, hotArchiveBL, lcl, has, std::move(sorobanConfig));
282+
return std::make_shared<ImmutableLedgerData>(liveBL, hotArchiveBL, lcl, has,
283+
std::move(sorobanConfig));
284284
}
285285

286286
ImmutableLedgerView::ImmutableLedgerView(ImmutableLedgerDataPtr state,

src/ledger/LedgerManagerImpl.cpp

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

2221-
JITTER_INJECT_DELAY();
2222-
SharedLockExclusive lock(mLastClosedLedgerStateMutex);
2223-
JITTER_INJECT_DELAY();
2221+
ImmutableLedgerDataPtr state;
2222+
{
2223+
JITTER_INJECT_DELAY();
2224+
SharedLockExclusive lock(mLastClosedLedgerStateMutex);
2225+
JITTER_INJECT_DELAY();
22242226

2225-
auto state = buildLedgerState(header, has, /*sorobanConfig=*/std::nullopt);
2227+
state = buildLedgerState(header, has, /*sorobanConfig=*/std::nullopt);
22262228

2227-
mApplyState.setLedgerStateForTesting(state);
2229+
mApplyState.setLedgerStateForTesting(state);
2230+
2231+
mLastClosedLedgerState = state;
2232+
}
22282233

2229-
mLastClosedLedgerState = state;
2234+
mApp.getCommandHandler().addSnapshot(state);
22302235
}
22312236
#endif
22322237
}

src/ledger/test/ImmutableLedgerViewTests.cpp

Lines changed: 105 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include "ledger/ImmutableLedgerView.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(
@@ -295,7 +323,7 @@ class SnapshotStressTest
295323
{
296324
public:
297325
SnapshotStressTest(int numThreads, unsigned seed, Application& app,
298-
PregenData const& pregen);
326+
PregenData const& pregen, QueryServer& queryServer);
299327
~SnapshotStressTest() = default;
300328

301329
void run();
@@ -322,13 +350,15 @@ class SnapshotStressTest
322350
int const mNumThreads;
323351
unsigned const mSeed;
324352
Application& mApp;
353+
QueryServer& mQueryServer;
325354
uint32_t const mProtocolVersion;
326355
uint32_t const mNumHistorical;
327356
PregenData const& mPregen;
328357

329358
// --- Shared state ---
330359
std::atomic<bool> mDone{false};
331360
std::atomic<bool> mError{false};
361+
std::atomic<int> mHistoricalVerifications{0};
332362
std::vector<std::unique_ptr<SnapshotThread>> mThreads;
333363

334364
bool
@@ -369,10 +399,12 @@ class SnapshotStressTest
369399

370400
SnapshotStressTest::SnapshotStressTest(int numThreads, unsigned seed,
371401
Application& app,
372-
PregenData const& pregen)
402+
PregenData const& pregen,
403+
QueryServer& queryServer)
373404
: mNumThreads(numThreads)
374405
, mSeed(seed)
375406
, mApp(app)
407+
, mQueryServer(queryServer)
376408
, mProtocolVersion(getAppLedgerVersion(app))
377409
, mNumHistorical(app.getConfig().QUERY_SNAPSHOT_LEDGERS)
378410
, mPregen(pregen)
@@ -391,21 +423,39 @@ SnapshotStressTest::SnapshotStressTest(int numThreads, unsigned seed,
391423
void
392424
SnapshotStressTest::run()
393425
{
426+
std::atomic<int> numRegistered{0};
427+
394428
ThreadGroup tg;
395429
for (int t = 0; t < mNumThreads; ++t)
396430
{
397-
tg.launch(1, [this, t]() { workerLoop(t); });
431+
tg.launch(1, [this, t, &numRegistered]() {
432+
mQueryServer.registerThread();
433+
++numRegistered;
434+
435+
// Wait until all threads are registered before proceeding.
436+
while (numRegistered.load(std::memory_order_acquire) < mNumThreads)
437+
{
438+
std::this_thread::yield();
439+
}
440+
workerLoop(t);
441+
});
398442
}
399443
tg.start();
400444
closeLedgers();
401445

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

407451
REQUIRE(!mError.load());
408452

453+
// Ensure historical queries were actually exercised and verified
454+
if (mNumHistorical > 0)
455+
{
456+
REQUIRE(mHistoricalVerifications.load() > 0);
457+
}
458+
409459
// Liveness check: after all ledgers are closed, a fresh snapshot must
410460
// reflect the final ledger sequence.
411461
auto finalLedgerView = mApp.getLedgerManager().copyImmutableLedgerView();
@@ -631,7 +681,7 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
631681
auto const& histState = histStateIt->second;
632682

633683
// Build query: positive keys (exist at histSeq) + negative keys
634-
// (exist at currentSeq but not at histSeq). We only need to track
684+
// (exist at a later ledger but not at histSeq). We only need to track
635685
// negativeKeys separately; any queried key not in negativeKeys is
636686
// positive.
637687
std::set<LedgerKey, LedgerEntryIdCmp> queryKeys;
@@ -642,15 +692,17 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
642692
}
643693

644694
std::set<LedgerKey, LedgerEntryIdCmp> negativeKeys;
645-
if (histSeq < currentSeq)
695+
696+
// Add negative keys from nearby ledgers (histSeq+1, histSeq+2, etc.)
697+
// to catch off-by-one bugs in snapshot selection.
698+
for (uint32_t futureSeq = histSeq + 1;
699+
futureSeq <= std::min(histSeq + 3, currentSeq); ++futureSeq)
646700
{
647-
auto curStateIt = stateMap.find(currentSeq);
648-
if (curStateIt != stateMap.end())
701+
auto futureStateIt = stateMap.find(futureSeq);
702+
if (futureStateIt != stateMap.end())
649703
{
650-
auto const& curState = curStateIt->second;
651-
for (int c = 0; c < 5; c++)
704+
for (auto const& [key, _] : futureStateIt->second)
652705
{
653-
auto const& [key, _] = randMapEntry(curState, rng);
654706
if (histState.find(key) == histState.end())
655707
{
656708
queryKeys.insert(key);
@@ -660,33 +712,40 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
660712
}
661713
}
662714

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

668745
if (archive)
669746
{
670-
auto result =
671-
sthread.ledgerView().loadArchiveKeysFromLedger(queryKeys, histSeq);
672-
if (!retained)
673-
{
674-
if (result.has_value())
675-
{
676-
fail(fmt::format("{} expected nullopt histSeq={} "
677-
"currentSeq={} seed={}",
678-
opName, histSeq, currentSeq, mSeed));
679-
}
680-
return;
681-
}
682-
if (!result.has_value())
683-
{
684-
fail(fmt::format("{} unexpected nullopt histSeq={} "
685-
"currentSeq={} seed={}",
686-
opName, histSeq, currentSeq, mSeed));
687-
return;
688-
}
689-
for (auto const& habe : *result)
747+
auto result = histSnapshot->loadArchiveKeys(queryKeys);
748+
for (auto const& habe : result)
690749
{
691750
if (habe.type() != HOT_ARCHIVE_ARCHIVED)
692751
{
@@ -700,26 +759,8 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
700759
}
701760
else
702761
{
703-
auto result =
704-
sthread.ledgerView().loadLiveKeysFromLedger(queryKeys, histSeq);
705-
if (!retained)
706-
{
707-
if (result.has_value())
708-
{
709-
fail(fmt::format("{} expected nullopt histSeq={} "
710-
"currentSeq={} seed={}",
711-
opName, histSeq, currentSeq, mSeed));
712-
}
713-
return;
714-
}
715-
if (!result.has_value())
716-
{
717-
fail(fmt::format("{} unexpected nullopt histSeq={} "
718-
"currentSeq={} seed={}",
719-
opName, histSeq, currentSeq, mSeed));
720-
return;
721-
}
722-
for (auto const& entry : *result)
762+
auto result = histSnapshot->loadLiveKeys(queryKeys, "hist-query");
763+
for (auto const& entry : result)
723764
{
724765
resultMap[LedgerEntryKey(entry)] = entry;
725766
}
@@ -759,6 +800,8 @@ SnapshotStressTest::readHistoricalQuery(SnapshotThread& sthread, bool archive,
759800
}
760801
}
761802
}
803+
804+
++mHistoricalVerifications;
762805
}
763806

764807
// Copy a snapshot from a random peer thread. The peer's copySnapshot()
@@ -820,9 +863,9 @@ SnapshotStressTest::closeLedgers()
820863
// Add both live and archive batches to their bucket lists, then
821864
// update the canonical state once so the snapshot atomically
822865
// includes both.
823-
bm.getLiveBucketList().addBatch(mApp, header.ledgerSeq,
824-
header.ledgerVersion,
825-
mPregen.liveEntriesToWrite[i], {}, {});
866+
bm.getLiveBucketList().addBatch(
867+
mApp, header.ledgerSeq, header.ledgerVersion,
868+
mPregen.liveEntriesToWrite[i], mPregen.liveUpdatesToWrite[i], {});
826869
if (i < mPregen.archiveEntriesToWrite.size())
827870
{
828871
bm.getHotArchiveBucketList().addBatch(
@@ -966,11 +1009,14 @@ TEST_CASE("snapshot concurrent stress test", "[snapshot][acceptance]")
9661009
VirtualClock clock;
9671010
auto cfg = getTestConfig();
9681011
cfg.QUERY_SNAPSHOT_LEDGERS = numHistorical;
1012+
cfg.QUERY_SERVER_FOR_TESTING = true;
9691013
auto app = createTestApplication<BucketTestApplication>(clock, cfg);
9701014
auto startSeq = app->getLedgerManager().getLastClosedLedgerNum();
9711015
auto pregen = pregenEntries(startSeq, NUM_LEDGERS, ENTRIES_PER_LEDGER);
9721016

973-
SnapshotStressTest test(NUM_THREADS, seed, *app, pregen);
1017+
auto& qServer = app->getCommandHandler().getQueryServer();
1018+
1019+
SnapshotStressTest test(NUM_THREADS, seed, *app, pregen, qServer);
9741020
test.run();
9751021
}
9761022

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.

src/main/CommandHandler.cpp

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ CommandHandler::CommandHandler(Application& app) : mApp(app)
8080
mApp.getConfig().QUERY_THREAD_POOL_SIZE,
8181
mApp.getAppConnector());
8282
}
83+
#ifdef BUILD_TESTS
84+
else if (mApp.getConfig().QUERY_SERVER_FOR_TESTING)
85+
{
86+
mQueryServer = std::make_unique<QueryServer>(
87+
"127.0.0.1", 0, 1, 1, mApp.getAppConnector(), true);
88+
mQueryServer->setReady();
89+
}
90+
#endif
8391
}
8492

8593
if (!mApp.getConfig().HTTP_PORT)
@@ -169,17 +177,6 @@ CommandHandler::addSnapshot(ImmutableLedgerDataPtr state)
169177
}
170178
}
171179

172-
#ifdef BUILD_TESTS
173-
void
174-
CommandHandler::initQueryServerForTesting()
175-
{
176-
releaseAssert(!mQueryServer);
177-
mQueryServer = std::make_unique<QueryServer>("127.0.0.1", 0, 1, 1,
178-
mApp.getAppConnector(), true);
179-
mQueryServer->setReady();
180-
}
181-
#endif
182-
183180
void
184181
CommandHandler::addRoute(std::string const& name, HandlerRoute route)
185182
{

0 commit comments

Comments
 (0)