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// ---------------------------------------------------------------------------
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 (
@@ -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
370400SnapshotStressTest::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,
391423void
392424SnapshotStressTest::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
0 commit comments