fix(ipc): ticks carry the volatile timestamps, so current-status stays known over the socket - #737
Conversation
…s known
/api/v3/display/current-status answered mode: null over the control socket
once the same mode had been on screen for more than two minutes (a live
game, Vegas, a single plugin). On ledpi it returned nulls in 1059/1059
samples over 90 minutes while a direct state.get showed the mode with
last_updated 0.15 s old.
The hub's version ignores the keys a publisher names as volatile, so
display.last_updated moving alone never sent a state event, and the
subscriber's keepalive tick carried only loop and served_at. The web's copy
kept the last_updated of the last real change, and current_status's 120 s
rule called it unknown. The plugins section had the same problem with
published_at: /plugins/state read stale after 180 s with nothing changing.
The short changed:false answer (what a tick is) now carries volatile:
{section: {key: value}}, the current values of those keys, and
StateSubscription merges them into its copy. Freshness still follows the
writers, so the verdicts from #726 are unchanged: a render thread that stops
publishing reads stalled at 60 s and unknown at 120 s, a runtime publisher
that stops goes stale, and a quiet subscription falls back to the cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (7)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe control socket now includes current volatile values in unchanged-version responses and ticks. Subscriptions merge matching values into cached state without changing the version. Tests cover timestamp freshness, status verdicts, and cached-status fallback. ChangesVolatile State Stream
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant StateHub
participant StateSubscription
participant WebReader
StateHub->>StateSubscription: Send unchanged-version tick with volatile values
StateSubscription->>StateSubscription: Merge matching values into cached state
WebReader->>StateSubscription: Read cached state
StateSubscription-->>WebReader: Return refreshed state
Merge Risk: ⚪ Minimal · up to No actionable merge-blocking issue is identified; the change is ready for normal merge checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change refreshes reported status without granting new access. Its effects appear narrowly scoped, but mixed-version operation and recovery behavior are not fully established. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 26.47% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 34 functions across 5 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 18 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
…connection (#740) StateSubscription._run reset its backoff only when _follow() returned normally, which happens only on stop(). Every real disconnect raises ControlError, so the wait kept doubling across connections: after successive display restarts the web resubscribed 1, 2, 4, 8, 16 and then 30 s later for good, answering from one-shot state.get connections in the meantime. The docs promise "1 s up to 30 s" per outage. The wait now goes back to the minimum once a connection got as far as storing a snapshot, whatever ended it. A display without the stream (unknown_command) is still retried at the slow interval. The frozen-timestamp bug found in the same review is fixed by #737. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fixes a bug from #735 (control socket stage 3, the state stream) found on the ledpi rig.
Bug
GET /api/v3/display/current-statusreturned{"mode": null, "plugin_id": null, "last_updated": null, "source": "socket"}once the same mode had been on screen for more than about 2 minutes, while the display was live. This hit a live game under live priority, Vegas, and a single plugin. On ledpi it returned nulls in 1059 of 1059 samples over 90 minutes. A directstate.getat the same time showedncaa_fb_livewithlast_updated0.15 s old.Root cause
StateHubleaves out of its version the keys a publisher marks as volatile. The display publishesdisplay.last_updatedthat way, so a change tolast_updatedalone never sends astateevent.loopandserved_at. SoStateSubscription._storenever refresheddisplay.last_updated, and the web's copy kept the timestamp of the last real change.display_state.current_status()treatslast_updatedolder than 120 s (CURRENT_STATE_MAX_AGE_SECONDS) as unknown.Other socket-fed readers:
plugins.published_atis volatile, so with no plugin changing state,/plugins/state,/plugins/state/reconcileand theruntimein/plugins/installedreadstaleafter itsstale_after(180 s)./display/on-demand/statusnever nulls out: it recomputesremainingand doesn't judge age. Itslast_updatedwas stale in the same way, and is now current too./healthreads onlyloop, which ticks already refreshed.Fix
The server tells the reader the current timestamps. Readers keep their freshness rules as they are.
changed: falseanswer fromStateHub.snapshot()(the result a tick carries, and astate.getwithsince) now includesvolatile: {section: {key: value}}. These are the current values of each section's volatile keys:display.last_updated,on_demand.last_updatedandremaining, andplugins.published_at. They are read under the same lock as the version, so they always belong to the version the reader has.StateSubscription._storemerges them into its copy on every tick with the same epoch and version. It takes only keys the section already has, so a tick never brings back a section that was dropped from a truncated snapshot.display_stateorplugin_runtime. The cache fallback's 120 s rule is unchanged.Freshness still follows the writers, so #726's verdicts hold:
stalledonce its heartbeat is 60 s old, and current-status reads unknown after 120 s;stale;I chose this over judging freshness by
served_atplus the loop heartbeat incurrent_status. It fixes every socket reader in one place, and each section keeps its own writer as its proof of life. A display without this change sends novolatile, and the client then behaves as before.Protocol: still version 1.
volatileis a new optional field in the short answer.Docs:
docs/IPC_CONTROL_SOCKET.mdand the contract docstrings are updated.Tests
test/test_state_stream_readers.py::TestFreshnessOverTicksruns the realDisplayControllerpublish point, the runtime publisher, the hub and aStateSubscriptionon fake clocks, with ticks every 5 s:last_updatedat most 5 s old. The plugins view stayslive, and all of this comes from ticks: one snapshot in total;source: socketafter 10 minutes;stalledand the loop age is at least 60 s at +65 s, while current-status still shows the mode. At +185 s current-status is unknown;stale;latest()returns None and the route falls back to the cache.test/test_ipc_state_stream.pycovers:last_updatedand a liveStateSubscriptionstays current without a new snapshot.client.pychange reverted, 5 of the new tests fail (4 of them regression tests). The ticks-stop fallback test passes either way.test_ipc_*files,test_state_stream_readers.pyand the golden traces (test_run_loop_golden.py) give 315 passed, 2 skipped. The 2 skips are "Windows only" and "root may always connect" (as root).origin/mainbaseline worktree: the sorted FAILED/ERROR lists are identical (62 failed, 6 errors in both; these failures were already there). On the branch, 9 more pass and 2 more skip; the new socket tests skip on Windows.scripts/check_types.pywith mypy 1.20.2: no issues in 94 modules.I didn't touch the Pi rigs.
🤖 Generated with Claude Code
Summary by CodeRabbit