fix: dispatch retired-ledger destruction off advance thread unconditionally

Previously, when shouldRetire was false (pre-TRACKING, before the
sticky caught-up flag flipped), retiredLedgers fell out of scope at
the end of setFullLedger and destructed synchronously on the advance
thread. Each publish past ledger_history cascaded through a
million-leaf destruction before doAdvance could loop to the next
publishable ledger, producing a stall-then-flurry pattern during
catch-up.

Always move retiredLedgers into the async job. Inside the job, the
shouldRetire capture gates only the bookkeeping side effects
(mCompleteLedgers / relational / LedgerHistory pruning). Destruction
of the captured shared_ptrs happens on the worker regardless, so the
advance thread stays on the publish hot path.
This commit is contained in:
Nicholas Dudfield
2026-04-14 17:00:49 +07:00
parent 01361d8b67
commit 47da8cccd6

View File

@@ -984,13 +984,27 @@ LedgerMaster::setFullLedger(
// Ledgers' SHAMap spines). The retired Ledgers stay alive in the
// captured vector until the job runs; destruction happens on the
// worker thread, off doAdvance's critical path.
if (shouldRetire && !retiredLedgers.empty())
//
// Dispatch unconditionally whenever we have retired Ledgers — even
// pre-TRACKING, where shouldRetire is false and we skip the
// mCompleteLedgers / relational / LedgerHistory pruning. The job
// still owns the shared_ptrs, so their destruction cascade runs on
// the worker, not on the advance thread. Without this, retired
// Ledgers fall out of scope synchronously in setFullLedger and the
// advance thread blocks on a million-leaf destruction per publish,
// producing the sync-stall-then-flurry pattern during catch-up.
if (!retiredLedgers.empty())
{
app_.getJobQueue().addJob(
jtLEDGER_DATA,
"retireLedgers",
[&app = app_, retired = std::move(retiredLedgers)]() {
app.getSHAMapStore().retireLedgers(retired);
[&app = app_, shouldRetire, retired = std::move(retiredLedgers)]() {
if (shouldRetire)
app.getSHAMapStore().retireLedgers(retired);
// Otherwise `retired` just destructs here on this
// worker thread as the lambda exits — bookkeeping
// side effects skipped, destruction cascade kept off
// the advance thread either way.
});
}