Skip to content

chore(services): three hand-rolled exponential backoffs, and three fixed-2s reconnect loops with no ceiling (#645 follow-up) #646

Description

@vibechoom

Follow-up from PR #645 (#621). That PR added a third hand-rolled exponential backoff to the workspace, which is the right call for its own lane — sharing the type would have meant editing wifi/mod.rs, outside the issue's file lane — but it makes the duplication worth filing rather than absorbing silently.

Filing as code-climate, not a bug. Nothing here is user-visible today.

Three independent exponential-backoff implementations

Site Shape Schedule
crates/hytte-bus/src/connection.rs:63-81 Backoff { next_ms }, mutable cursor with reset()/next() 250 ms, doubling, 30 s cap
crates/hytte-services/src/wifi/mod.rs:213-260 RetryPolicy { max_attempts, initial, max_backoff } + pure step() 500 ms, doubling, 8 s cap
crates/hytte-services/src/networkd.rs (new in #645) RefreshRetry { max_attempts, initial, max_backoff } + pure step() 500 ms, doubling, 8 s cap

The latter two are near-identical: same three fields with the same meanings, and a backoff() body that is character-for-character the same checked_shl / saturating_mul / min sequence. They differ only in the type their step() weighs — wifi's is typed to Result<BackendChoice, ProbeError> because an inconclusive probe is a distinct outcome from a failed one, while networkd's is generic over &Result<T, E> because a refresh either read the links or did not.

That difference is real, and it is the reason #645 did not just call the existing type. But it is also small: the generic version in networkd.rs subsumes the wifi one if wifi keeps its verdict-weighing in the caller rather than inside step.

hytte-bus's Backoff is a different enough shape (a stateful cursor, no attempt budget, no give-up) that folding all three into one type is probably over-unification. Two is the interesting number here.

The doc comments on the two hytte-services copies currently cross-reference each other by hand, which is what keeps them from drifting. That works until someone edits one and not the other.

Three fixed-2s reconnect loops with no backoff and no log ceiling

Same family, different symptom — a listen()-style stream that ends or errors, retried at a flat 2 s forever, logging a warn! every time:

  • crates/hytte-services/src/networkd.rs:346-353Ok(()) => warn!("networkd stream ended, retrying in 2s") / Err(e) => warn!("networkd error, retrying in 2s"), then sleep(2s). Deliberately left untouched by fix(networkd): retry a transient startup refresh failure instead of going inert (#621) #645, which was scoped to the startup seed path.
  • crates/hytte-services/src/mpris/mod.rs:172-182 — identical shape, "mpris watcher stream closed/error, reconnecting in 2s".
  • crates/hytte-services/src/bluetooth/mod.rs:~190-201 — resets the adapter-path store and sleeps 2 s inside its watch loop.

If any of these daemons is down for an extended period, the journal gets a line every 2 s indefinitely. This is the same concern the STARTUP_REFRESH_RETRY docs in #645 reason about explicitly, and the same one hytte-bus already solved for connections with its 30 s cap.

Ruled out, so nobody re-adds it: crates/hytte-services/src/wifi/watcher.rs:131 matches the sleep(Duration::from_secs(2)) grep but is not a retry loop — it is a one-shot debounce before return on the iwd station-removed path, where the caller re-establishes the watch. Leave it alone.

Severity

Low. No wrong behaviour and no user-facing symptom. The cost is maintenance: a policy change (or a bug fix) in one copy silently fails to reach the other, and the two hytte-services copies are similar enough that a reader can easily believe they have already read the second one. The journal-cadence half is a mild operational annoyance on a host with a persistently dead daemon, not a defect.

Recommendation

Two independent pieces of work; the first is worth doing, the second is worth doing only if someone is in the file anyway.

  1. Promote one retry-policy type within hytte-services. Keep networkd.rs's generic step<T, E>(&Result<T, E>, attempt) as the surviving shape, move it somewhere shared in the crate, and have wifi weigh its inconclusive-vs-failed verdict in the caller before handing a Result to step. This keeps both call sites' current behaviour byte-identical — the constants already match — so it is a pure refactor with the existing tests on both sides as the gate. Do not pull hytte-bus's Backoff into it.

  2. Give the three listen-loop retries a capped backoff. The reconnect cadence matters much less than the startup one, so the simple move is to reuse whatever type falls out of (1) with a longer ceiling, and drop the "retrying in 2s" wording from the log lines, which becomes false the moment a backoff exists.

Neither is urgent. If only one gets done, do (1) — the drift risk is the actual cost here.

Refs #645, #621, #634, #613.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions