feat(ipc): control socket stage 3 - a state stream replaces polled cache keys - #735
Conversation
…che keys state.get returns a versioned snapshot of the display's state (mode, on-demand, brightness, plugin runtime snapshot, render-loop heartbeat age), served from memory. state.subscribe pushes the latest version on every change plus keepalive ticks, with its own subscriber bound; publishing never waits for a reader and a reader that stops reading is dropped. The web interface holds one subscription and reads current-status, on-demand status, the plugin runtime view and the health display_loop check from it, falling back to the cache keys and heartbeat file. #726's stale and stalled rules apply to both. While the socket serves readers, display_current_state and plugin_runtime_snapshot are written less often (5 -> 1.5 cache writes/min for a rotation of 15 s screens). 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 (16)
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 pull request adds versioned state snapshots and subscriptions to the control socket. Display and plugin publishers send state to an in-memory hub. Web-interface readers prefer socket data and retain cache and heartbeat-file fallbacks. ChangesControl Socket State Stream
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant WebRoute
participant DisplayState
participant StateSubscription
participant ControlServer
participant StateHub
participant Cache
WebRoute->>DisplayState: Request display or runtime state
DisplayState->>StateSubscription: Read latest snapshot
StateSubscription->>ControlServer: Subscribe to state stream
ControlServer->>StateHub: Read snapshot and wait for changes
StateHub-->>ControlServer: Return state and keepalive events
ControlServer-->>StateSubscription: Send snapshot and events
DisplayState-->>WebRoute: Return socket state when available
DisplayState->>Cache: Read fallback when socket state is unavailable
Merge Risk: ⚪ Minimal · up to No identified issue blocks merging after normal checks. Validation on ledpi remains outstanding. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The state stream is bounded, uses connection-level access controls, and retains file-based fallbacks. No introduced security issue was established in the inspected paths. Actual deployment permissions and the complete pre-change exposure comparison remain unverified. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 35.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 184 functions across 13 files. (3 skipped: 3 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 | 197 |
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.
Codacy flagged the bare try/except/pass (Bandit B110). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 3 of the display control socket (#706, #720, #723): a state stream, so the web interface reads what the display is doing from the socket and stops polling cache files. The design follows the stage plan in
docs/IPC_CONTROL_SOCKET.md.Protocol (still version 1, two new commands)
state.get {since?, epoch?}returns a versioned snapshot:{schema, version, epoch, pid, served_at, changed, loop, state}.statehas five sections:display: whatdisplay_current_stateholds;on_demand: whatdisplay_on_demand_stateholds;brightness:{brightness, panel_brightness, dimmed};plugins: theplugin_runtime_snapshot;loop: the render loop's heartbeat age, measured in memory when the display answers.versiongoes up only when a section really changes. Timestamps such aslast_updated,remainingandpublished_atdon't count.epochchanges with each run of the display process.sinceandepochstill current, the answer is the short{changed: false, ...}form with no state in it.state.subscribeanswers with the same snapshot, then pushes events on that connection:{"event": "state"}, a full snapshot of the latest version, whenever the state changes. A reader that falls behind skips versions; nothing is queued up for it.{"event": "tick"}, carryingloop, at least every 5 s.eventwhere responses haveok.unknown_command, and the web falls back.plugins(truncated: ["plugins"]), and readers use the cache for that section only.Display side.
StateHub(insrc/ipc/server.py) holds the state in memory, with no disk writes._publish_current_mode_state[_if_changed]and_publish_on_demand_state. The plugin runtime publisher publishes on its 5 s tick.display_controller.pyedits are limited to the state-publish helpers,_start_control_server(plus_start_state_stream) and cleanup. I didn't touchrun()or the Source helpers (coordination with run-loop stage 2).Readers that switched
web_interface/display_state.pyholds one subscription per web process and answers from memory. Before the subscription has a snapshot, it makes onestate.get(0.5 s timeout). When neither works, readers use the cache keys and heartbeat file as before.GET /display/current-statusstate.displaydisplay_current_stateGET /display/on-demand/statusstate.on_demand(remainingrecomputed)display_on_demand_state/plugins/installedruntime,/plugins/state,/plugins/state/reconcile, startup reconciliationstate.plugins+state.loopplugin_runtime_snapshot+ heartbeat file/healthchecks.display_loopstate.loopsource(socket/cache/heartbeat_file).stalledwhen the loop heartbeat is at least 60 s old;stalewhen the snapshot is paststale_after;displaysection unrefreshed for 120 s reads as unknown, like the cache key'smax_age.SD writes
The cache keys are still written for one release. While the socket is serving readers (a subscriber is connected, or a
state.getcame in the last 60 s), the display writes two of them less often:display_current_state: written every 60 s and on a flag change, instead of on every mode change.plugin_runtime_snapshot: refreshed every 120 s instead of 60 s. The snapshot advertisesstale_after360.Why it is safe:
max_age.Measured with a fake-clock harness (
test_cache_writes_per_minute_with_and_without_socket_readers, rotation of 15 s screens):display_current_stateplugin_runtime_snapshotdisplay_on_demand_stateis event-only and unchanged. The heartbeat is on tmpfs, so it was never an SD write; it stays, becauseauto_update_verifyreads it.Tests
test/test_ipc_state_stream.py(44) andtest/test_state_stream_readers.py(35) cover:since/epochshort form;off), the routes use the cache and heartbeat file. In the end-to-end test a real server serves the routes; after it closes they reportsource: cache.displaysection.busy.pingstill gets through withmax_clients=2.displaysection is trusted;chuckand as root. The wider IPC, runtime, health and socket-route set passes 396 (2 skipped) both ways.root:chuck 0660socket.chuckgotstate.getand a subscription that saw a pushed change.nobodywas refused at connect.test/test_run_loop_golden.py): 16 passed, byte-identical (the hub isNonewithout a socket, so the traced cache writes are unchanged).origin/mainbaseline worktree, same Pillow (12.3.0 before and after): baseline 61 failed / 6 errors / 8003 passed. Branch 62 failed / 6 errors / 8072 passed (+69 new tests).test_display_watchdog.py::TestCheckInPoints::test_the_dwell_sleep_checks_in. It is a real-time sleep test of_sleep_with_plugin_updates, which this PR does not touch.scripts/check_types.pywith mypy 1.20.2 (pure-Python build): clean, 93 modules.Not done here
src/module (the hub is insrc/ipc/server.py, the subscription insrc/ipc/client.py), so no follow-up is needed for the monorepo's core-module table. The one new module isweb_interface/display_state.py.Control socket listeningin the journal. Confirmls -l /run/ledmatrix/control.sockis stillsrw-rw---- root ledmatrix.curl -s localhost:5000/api/v3/display/current-statusshould show"source": "socket"and follow the panel through a rotation./api/v3/healthshould showdisplay_loop.source: socketwith an age under ~5 s./api/v3/plugins/installedshould showruntime.source: socketandstatus: live.on-demand/statusshould show the outcome at once, withremainingcounting down.sudo systemctl stop ledmatrixshould switch the routes tosource: cache(with nulls once the 120 smax_agepasses). Start it again and they should return tosocketwithin about 30 s (reconnect backoff).sudo systemctl restart ledmatrix-web.serviceshould make the subscription come back, with no errors in the display journal.display_current_stateandplugin_runtime_snapshotfiles in/var/cache/ledmatrixonce a second and count the changes. Expect about 1 + 0.5 per minute versus about 4 + 1 per minute for 15 s screens.python3 scripts/frame_soak.py --previewagainst alternatedmainarms (the default gate already fails on plain main). This checks that the per-pass in-memory publish adds no late frames.display()sleeps 90 s should makeruntime.statusstalledanddisplay_loopstalledover the socket, as fix(status): runtime status agrees with the heartbeat; current-status republishes on wake #726 does over the files.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements