Skip to content

dashboard: a raise in the data loop freezes iteration_count, turning a one-in-ten step into every poll #1637

Description

@VijitSingh97

What

_run_data_loop's iteration_count is incremented as the last statement of the try body, so
any exception inside that body skips the increment. The counter gates the periodic steps, so a
persistent failure does not merely fail — it changes the cadence of everything gated on it.

Measured at source by AST, at c437f7fa:

fact value
try statement service/data_service.py:940
its body lines 942..1448
last statement of the body iteration_count += 1, line 1448
handler except Exception, line 1449
iteration_count = 0 line 928

The steps gated on the counter: if ENABLE_XVB and iteration_count % 10 == 0 (1399),
if self.wallet_client is not None and iteration_count % 10 == 0 (1419, calling _sync_payouts at
1420), and if self.tari_wallet_client is not None and iteration_count % 10 == 0 (1424, calling
_sync_tari_payouts at 1425). _sync_prices at 1446 is ungated but sits below all of them.

Why the shape matters

A raise at, say, line 1420 does three things, and only the first is intended:

  1. the handler logs and the loop continues — the designed degradation;
  2. lines 1425 and 1446 are skipped, so a payout failure silently suppresses the Tari payout sync
    and the price sync, which have nothing to do with it; and
  3. line 1448 is skipped, so iteration_count stays on the same value.

Point 3 is the one worth the issue. The counter is frozen at a multiple of 10, which is exactly the
value that satisfies % 10 == 0. So the next poll runs the failing step again, and the one after
that, and so on: a step designed to run one poll in ten now runs on every poll, retrying a
persistent failure at ten times its intended rate and never backing off. The failure mode inverts
the throttle it was given.

This is triggered by any exception in that body, not by any particular caller. It is latent
rather than active — I am not claiming a live path reaches it today.

Relationship to #1592

#1592 is where I found it: a malformed monerod/wallet body raised out of _sync_payouts at 1420
and hit exactly this. That path is fixed — the PR for #1592 makes those clients return None
and [] instead of raising, so the specific trigger is gone. The counter's placement is not,
and it is the general case, so it is filed on its own rather than in passing. The #1592 fix does
not depend on this one and this one does not depend on it.

Not claimed / not verified

Shapes worth considering — none of them ruled

  • finally. Moving the increment to a finally makes the cadence monotonic regardless of the
    outcome. Simplest, and it makes the counter mean "polls attempted" rather than "polls completed".
  • Increment first. Same effect, and it reads more plainly, but it changes which poll number the
    first iteration gets — the gates are % 10 == 0, so the phase shifts, which is visible in
    behaviour and needs the tests read before it is chosen.
  • Per-step guards. Wrapping each gated step in its own try stops one step's failure from
    skipping its unrelated siblings (point 2 above). This is orthogonal to the counter and could be
    taken separately.

A note on the third: the codebase already has an opinion here. data_service.py:116 records that
the interval gates should be phrased as now - last >= N rather than iteration_count % k, because
the modulo form "silently changes cadence if UPDATE_INTERVAL" moves. This defect is that same
sentence arriving by a different route — the modulo form silently changes cadence when the counter
stops advancing. Whoever takes this should read that comment first; it may be that the honest fix is
the one it already recommends.

Metadata

Metadata

Assignees

No one assigned

    Labels

    dashboardMining dashboard web UI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions