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// ---------------------------------------------------------------------------
7678struct 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
368398SnapshotStressTest::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,
389421void
390422SnapshotStressTest::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
0 commit comments