Skip to content

fix(nomadnet): bound cache transient-stall so a failing SD seam cannot freeze the UI - #97

Merged
torlando-tech merged 3 commits into
mainfrom
fix/nomadnet-cache-transient-budget
Sep 15, 2026
Merged

torlando-tech merged 3 commits into
mainfrom
fix/nomadnet-cache-transient-budget

Conversation

@torlando-tech

Copy link
Copy Markdown
Owner

Summary

Fixes the NomadNet UI freeze where opening any page hung forever on "Checking SD page cache...".

The SD page cache (16af3b5, Aug 17) made the SD card a soft dependency of page
loads for the first time. NomadNetCache retries transient SD storage results
(BUSY = SPI-mutex timeout, UNAVAILABLE = card not mounted) with no budget or
deadline
: every step treats them as "retry next tick" forever. Because
NomadNetCacheFlow::service() early-returns while cache_.busy() and the UI's
NomadState::CACHE has no timeout, a persistently-unhealthy SD seam pins the UI
in "Checking SD page cache..." indefinitely.

Fix

NomadNetCache::service() now detects no-progress ticks across consecutive
calls: if this call's entry state (op, offset, scan/cleanup index, scan count,
open-flags) is identical to the previous call's, a counter increments; any real
advance resets it. After MAX_TRANSIENT_STALL_TICKS (500 — far beyond any real
SPI contention or mount window, and safe for legitimate multi-tick steps like
64 KB chunked commits and 96-record recovery scans) it bails:

  • clears the in-flight operation (operation_ = NONE), and
  • marks the namespace non-authoritative for the session (recovery_complete_ = false), so subsequent beginLookup() calls return BYPASS.

The flow then drops to NEED_LIVE and does a live fetch — the pre-cache
behavior. No data is wiped; a healthy SD later in the session simply means no
cache benefit for that boot. The cache becomes a no-op rather than a freeze.

Verification

  • RED→GREEN: new regression tests fail on origin/main and pass with the fix.
    • test_nomadnet_cache.cpp: persistent UNAVAILABLE at boot and persistent
      BUSY at boot both bail in bounded ticks and mark the namespace
      non-authoritative; a mid-transfer commit stall bails and reports.
    • test_nomadnet_cache_flow.cpp: drives the real NomadNetCacheFlow with a
      BusyListStorage mock — asserts the flow reaches NEED_LIVE (not stuck in
      LOOKUP) in bounded service ticks (the exact UI symptom).
  • tdeck firmware: build SUCCESS (23.2% RAM, 92.9% flash).
  • tests/native: 121 passed; tests/build_scripts: 184 passed. (3 pre-existing
    env failures — nomadnet_x86_flow, 2 lvgl_dependency — fail identically on
    the origin/main baseline, not regressions.)
  • Physical (T-Deck): app-only flash of 0.3.8-39-gfeb7f05, all partitions
    preserved (NVS/LittleFS/identity/otadata/coredump), app0 read-back verified
    sha256. Device boots clean; a NomadNet page loads (first request needed a
    second attempt — live-fetch fallback is working as designed).

Test Plan

  • pio run -e tdeck builds clean
  • pytest tests/native/test_nomadnet_cache.py test_nomadnet_cache_flow.py test_nomadnet_storage.py pass (RED on main, GREEN with fix)
  • Physical: NomadNet page loads on T-Deck; no permanent "Checking SD page cache..." freeze

Closes the NomadNet page-cache UI freeze.

…t freeze the UI

The SD page cache (16af3b5) made the SD card a soft dependency of
NomadNet page loads, but its step machine retries a transient storage
result (BUSY = SPI-mutex timeout, UNAVAILABLE = card not mounted)
forever with no budget. On a persistently unhealthy seam the boot-time
recovery pins operation_ != NONE, the flow stays LOOKUP, and the UI
freezes at 'Checking SD page cache...' (the CACHE state has no
deadline, unlike every other NomadNet state).

NomadNetCache::service() now compares each call's entry state to the
previous call's. Any advance (op, offset, scan/cleanup index, scan
count, open-flags) resets a stall counter, so slow-but-progressing
steps (chunked 64 KB transfers, up to 96-record directory scans) never
false-trip; a no-progress tick is a transient stall. Past 500
consecutive no-progress ticks (far beyond any real SPI contention or
SD mount window) the cache bails: mark the namespace non-authoritative
for the session (lookups/commits bypass) and clear the op, so the flow
falls through to a live fetch -- the pre-cache page-load behavior.

Regression tests: cache-level (persistent UNAVAILABLE recovery bails;
lookup bypasses after bail) and flow-level (a permanently BUSY
beginList no longer parks the flow in LOOKUP; it reaches NEED_LIVE in
bounded ticks). Both fail on the pre-fix code and pass with it.

Verified: tdeck firmware build SUCCESS; tests/native 121 passed (3
pre-existing env failures, fail identically on origin/main baseline);
tests/build_scripts 184 passed.
@greptile-apps

greptile-apps Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the previous findings are resolved and no new actionable failure was identified.

Summary

The PR prevents persistent transient SD-card failures from indefinitely blocking NomadNet page loading.

  • Adds tick- and elapsed-time-based detection for cache operations that make no progress.
  • Abandons stalled cache operations, releases in-flight storage resources on a best-effort basis, and falls back to live page retrieval.
  • Handles Arduino millis() rollover with wrap-safe elapsed-time arithmetic.
  • Adds cache- and flow-level regression coverage for persistent stalls, recovery, reload invalidation, resource cleanup, and clock rollover.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[NomadNet cache operation] --> B{State progressed?}
    B -- Yes --> C[Reset stall counter]
    B -- No --> D[Increment stall counter]
    D --> E{Tick floor and 10 seconds elapsed?}
    E -- No --> A
    E -- Yes --> F[Best-effort resource cleanup]
    F --> G[Mark namespace non-authoritative]
    G --> H[Clear cache operation with MISS]
    H --> I[Flow transitions to NEED_LIVE]
    I --> J[Fetch page live]
Loading

Reviews (3) · Last reviewed commit: "fix(nomadnet): make the transient-stall ..."

Comment thread lib/tdeck_ui/UI/LXMF/NomadNetCache.cpp Outdated
Comment thread lib/tdeck_ui/UI/LXMF/NomadNetCache.cpp Outdated
Comment thread lib/tdeck_ui/UI/LXMF/NomadNetCache.h
…guard

Three findings on the transient-stall bail, all valid:

- Reload invalidation: a bail recorded BYPASS, but NomadNetCacheFlow::service()
  accepts only MISS as a successful invalidation, so a bail during an admitted
  reload reported 'Page cache invalidation failed' instead of falling through
  to a live fetch. The bail now records MISS.

- Open-resource leak: the bail cleared read_open_/write_open_ (and abandoned an
  open directory enumeration) without calling endRead()/abortWrite()/endList(),
  leaking SD handles. The bail now releases each in-flight resource via the
  seam's own teardown (bounded best-effort; a still-transient close is
  accepted rather than re-pinning the op).

- Tick-vs-time: the not-ready UNAVAILABLE path returns immediately (no bus
  wait), so a pure 500-tick budget could expire during a legitimate SD mount
  window and disable caching for the whole session. The bail is now gated on
  BOTH the tick floor AND a 10s wall-time window (service() takes a monotonic
  ms clock; production passes millis(), 0 is a safe default for tests).

Regression tests: reload-invalidation bail -> NEED_LIVE (flow), flat-clock does
not bail, healable transient keeps authority, list-open (RECOVERY_END) stall
bails and releases the handle (cache).
Comment thread lib/tdeck_ui/UI/LXMF/NomadNetCache.cpp Outdated
…millis()

A stall spanning the 32-bit millis() rollover zero-extends to a value smaller
than the pre-wrap start, so the now_ms >= start_ms guard blocked the bail until
the counter lapped the ~49.7-day start value. Use 32-bit unsigned subtraction,
which measures true elapsed time across the wrap. Regression: a stall starting
near the counter max and wrapping bails in bounded ticks.
@torlando-tech
torlando-tech merged commit 7ae47d5 into main Sep 15, 2026
5 checks passed
@torlando-tech
torlando-tech deleted the fix/nomadnet-cache-transient-budget branch September 15, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant