feat(ledger): attribute deferrals and timeouts to ledger acquisition

Both counters are recorded in TimeoutCounter, a base shared by five
subclasses with different job limits, so they pooled every lane
together. The documented fingerprint for a stalled acquisition is
deferrals rising while timeouts stay flat -- but a saturated replay lane
reproduces that shape while ledger acquisition is healthy, and the two
counters being compared were not drawn from the same population.

Adds ledger-acquisition-scoped counts alongside the totals, discriminated
by the job name already held at both call sites, so no new state and no
signature change. The all-lane counters stay for callers that want them.
Their exporters went in with the preceding commit, which shared a file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pratik Mankawde
2026-07-28 18:25:22 +01:00
parent 61b101e147
commit 56325f56c2
3 changed files with 61 additions and 4 deletions

View File

@@ -93,9 +93,11 @@ public:
* advance the retry count toward give-up.
*/
void
recordDeferral()
recordDeferral(bool ledgerAcquisition = false)
{
deferrals_.fetch_add(1, std::memory_order_relaxed);
if (ledgerAcquisition)
ledgerDeferrals_.fetch_add(1, std::memory_order_relaxed);
}
/**
@@ -105,9 +107,11 @@ public:
* separate from deferrals.
*/
void
recordTimeout()
recordTimeout(bool ledgerAcquisition = false)
{
timeouts_.fetch_add(1, std::memory_order_relaxed);
if (ledgerAcquisition)
ledgerTimeouts_.fetch_add(1, std::memory_order_relaxed);
}
/**
@@ -171,6 +175,32 @@ public:
return timeouts_.load(std::memory_order_relaxed);
}
/**
* Deferrals that belong to ledger acquisition only.
*
* @ref getDeferrals covers every TimeoutCounter subclass, so a busy
* replay or transaction-set lane inflates it. Compare this against
* @ref getLedgerTimeouts to judge ledger acquisition on its own.
*/
[[nodiscard]] std::uint64_t
getLedgerDeferrals() const
{
return ledgerDeferrals_.load(std::memory_order_relaxed);
}
/**
* Timeouts that belong to ledger acquisition only.
*
* The partner of @ref getLedgerDeferrals: rising deferrals with flat
* timeouts here means ledger acquisition's give-up path is disarmed,
* which the all-lane counters cannot show.
*/
[[nodiscard]] std::uint64_t
getLedgerTimeouts() const
{
return ledgerTimeouts_.load(std::memory_order_relaxed);
}
/**
* Return the number of acquisitions that exhausted their retry budget.
*/
@@ -224,6 +254,16 @@ private:
*/
std::atomic<std::uint64_t> deferrals_{0};
/**
* Deferrals attributable to ledger acquisition alone.
*/
std::atomic<std::uint64_t> ledgerDeferrals_{0};
/**
* Timeouts attributable to ledger acquisition alone.
*/
std::atomic<std::uint64_t> ledgerTimeouts_{0};
/**
* Timer bodies that ran and advanced the retry count.
*/

View File

@@ -67,7 +67,7 @@ TimeoutCounter::queueJob(ScopedLockType& sl)
// Counted separately from timeouts: this path re-arms the timer
// without running invokeOnTimer, so timeouts_ does not advance and the
// give-up test that reads it cannot fire while the lane stays full.
app_.getAcquireStats().recordDeferral();
app_.getAcquireStats().recordDeferral(isLedgerAcquisition());
JLOG(journal_.debug()) << "Deferring " << queueJobParameter_.jobName
<< " timer due to load";
setTimer(sl);
@@ -92,7 +92,7 @@ TimeoutCounter::invokeOnTimer()
if (!progress_)
{
++timeouts_;
app_.getAcquireStats().recordTimeout();
app_.getAcquireStats().recordTimeout(isLedgerAcquisition());
JLOG(journal_.debug()) << "Timeout(" << timeouts_ << ") "
<< " acquiring " << hash_;
onTimer(false, sl);

View File

@@ -139,6 +139,23 @@ protected:
QueueJobParameter queueJobParameter_;
/**
* Whether this counter belongs to ledger acquisition.
*
* Deferrals and timeouts are recorded in this base class, so they would
* otherwise pool every subclass together: a saturated replay lane would
* look exactly like a stalled ledger acquisition. The job name already
* identifies the subclass, so it is the cheapest discriminator available
* and needs no extra state.
*
* @return True for the InboundLedger lane, false for every other.
*/
[[nodiscard]] bool
isLedgerAcquisition() const
{
return queueJobParameter_.jobName == "InboundLedger";
}
private:
/**
* Calls onTimer() if in the right state.