feat(ipc): the control socket carries every web command; mailboxes are a fallback (stage 4) - #765
Conversation
…e a fallback (stage 4) - client: ControlError.sent says whether the display had the request; should_fall_back() allows a mailbox write only when it did not, or when the display is too old to know the command (upgrade case) - on-demand start/stop: a display that had the request and failed it is answered 503 (400 for invalid_args), no mailbox copy - errors.clear: new socket command, answered on the connection thread by a handler the display registers; applied and republished before the answer; plugin_error_clear_request only on fallback - display: on-demand mailbox looked at once a second while the socket is up (0.25 s without), read only when its file changed (one stat via CacheManager.file_signature / MailboxWatch); socket commands no longer touch the mailbox; a processed duplicate is consumed; writers logged once - error publisher: mailbox read only when changed; snapshot carries applied_clear_cutoff so an older mailbox request is not shown pending - docs and CHANGELOG (mailboxes kept for one release) 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 (11)
🚧 Files skipped from review as they are similar to previous changes (1)
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 display control socket is the primary transport for on-demand requests and error clearing. Mailboxes remain fallbacks when the socket cannot carry a request. The display polls mailboxes less often while the socket is active and reads them only when their files change. ChangesControl-socket requests
Priority: ⬇️ Low Estimated code review effort: 4 (Complex) | ~45 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant WebAPI
participant IPCClient
participant ControlServer
participant DisplayController
participant RequestMailbox
WebAPI->>IPCClient: Send on-demand request
IPCClient->>ControlServer: Send socket command
ControlServer->>DisplayController: Queue socket request
alt Socket cannot carry request
WebAPI->>RequestMailbox: Write fallback request
DisplayController->>RequestMailbox: Read changed mailbox
else Display received but rejected request
ControlServer-->>WebAPI: Return socket error without mailbox write
end
sequenceDiagram
participant WebAPI
participant IPCClient
participant ControlServer
participant ErrorSnapshotPublisher
participant ClearMailbox
WebAPI->>IPCClient: Send error-clear request
IPCClient->>ControlServer: Send errors.clear
ControlServer->>ErrorSnapshotPublisher: Apply clear and publish snapshot
ErrorSnapshotPublisher-->>ControlServer: Return cleared count
ControlServer-->>WebAPI: Return applied response
alt Socket cannot carry request
WebAPI->>ClearMailbox: Write clear request
ErrorSnapshotPublisher->>ClearMailbox: Read changed mailbox
ErrorSnapshotPublisher->>ErrorSnapshotPublisher: Apply clear and publish snapshot
end
Merge Risk: ⚪ Minimal · up to No actionable issue remains from these findings. The change is mergeable after normal checks; hardware validation remains pending. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to The new delivery rules reduce duplicate commands, and access checks remain in place. However, storage failures can produce a successful error-clear response without updating the shared report, or prevent a fallback command from being retried after storage recovers. These risks are limited to the affected display and its diagnostic state. 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 27.81% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 151 functions across 13 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 | 65 |
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.
…-primary # Conflicts: # CHANGELOG.md
Stage 4 of the web → display control socket (
docs/IPC_CONTROL_SOCKET.md): the socket is the primary channel for every web command, and the file mailboxes are written only as the fallback when the socket cannot carry a request.What still went through a mailbox
busy, reply timeout,invalid_args)unknown_command,unsupported_version); otherwise503(400forinvalid_args) and no copyPOST /api/v3/errors/clear)plugin_error_clear_requestmailbox, applied on the display's next 5 s tickerrors.clearsocket command, applied and republished before the answer (applied: true, the display's owncleared_count); mailbox only on the same fallback rulestat()once a second while the socket is up (0.25 s without), read only when the file changed; socket commands never touch itstat()every 5 s, read only when the file changedBrightness (
brightness.set) and plugin reload (plugin.reload) never had a mailbox; config saves (schedule, dim schedule, plugin settings) stay onconfig.json+ its watcher, the setting itself rather than a message (as decided in stage 2). Plugin health/metrics resets write the published record and are not mailboxes; left as they are.How
src/ipc/client.py:ControlError.sent(the whole request was written to a connected display; a door refusal with no request id leaves it False) andshould_fall_back(error), the one rule every route uses. Newerrors_clear().src/ipc/contract.py/server.py:errors.clear {cutoff}andDIRECT_COMMANDS, answered on the connection thread by a handler the display registers (ControlServer(handlers=...)). No handler →unknown_command, so the client falls back as from an older display.src/cache_manager.py:CacheManager.file_signature(key)→(inode, mtime_ns, size)or None, andMailboxWatch, which says whether a mailbox changed since the last look. Every DiskCache write renames a new file into place, so a new write always has a new inode.src/display_controller.py:MAILBOX_POLL_INTERVAL_WITH_SOCKET = 1.0; socket commands (source: 'socket') skip the mailbox consume; a processed duplicate is consumed instead of re-read for an hour; a mailbox request while the socket is up is logged once per writer (the four plugins that write it directly: birdnet-go, mqtt-notifications, on-air, pomodoro-timer).src/error_aggregator.py:ErrorSnapshotPublisher.clear_now(),apply_error_clear()(the socket handler);request_error_clear(..., send=); the snapshot carriesapplied_clear_cutoffso an older mailbox request is not shown as pending after a wider socket clear.Backward compatibility (one release)
The display still reads both mailboxes and still writes
display_current_state,display_on_demand_stateandplugin_runtime_snapshot. Upgrade windows:errors.cleargetsunknown_commandand falls back to the mailbox, which the old display applies.CHANGELOG has the entry under Unreleased. No new
src/module (so no plugins-repo guard-table entry is needed).Tests
test/test_ipc_stage4.py:sent/should_fall_backmatrix through_exchange(platform-independent fake socket),errors.clearon the server, mailbox cadence with and without a socket, stat-gating, socket commands leaving the mailbox alone, the duplicate case, the deprecation log,file_signature/MailboxWatchon a real cache, and real-socket round trips (Linux).test_api_v3_on_demand_socket.py: socket up; socket down (sent=False reasons → mailbox); the display had it (busy/internal/timeout/closed/bad_response → 503, invalid_args → 400, no mailbox); upgrade (unknown_command/unsupported_version→ mailbox); stop withstop_service; a real full queue → 503 (Linux).test_error_snapshot_cross_process.py: errors.clear over the socket (applied before the answer, no mailbox file), socket down → mailbox, older display → mailbox, display failed → 503, an older mailbox request not shown pending, publisher reads the mailbox only on change.sentbookkeeping, poll interval, stat-gating, consume rules, deprecation log, applied cutoff, socket result handling, handler-less answer, route branches), all killed.Hardware (ledpi)
Deployed this branch (597653e, merged with main 26cae3e) on ledpi (Pi 4), exercised through the web API on localhost:5000, then restored ledpi to origin/main 26cae3e and restarted both services. config.json was identical to its pre-test backup afterwards.
Upgrade window: new web, old display (web restarted on the branch, display still on main 294bd52):
POST /errors/clear→transport: mailbox(web log:Control socket did not take errors.clear (unknown_command ...)); the old display applied it from the mailbox within 1 s (Cleared 0 plugin error record(s) as requested).transport: socket; current-status showed clock-simple,source: socket.Both on the branch:
/health:display_looprunning(source: socket); current-statussource: socket.transport: socket, on screen after 0.29 s, no mailbox file written; stop → socket, statusidle.POST /config/schedule, end_time changed) → 200, display's config watcher reloaded within 2 s; restored → 200.POST /config/main) →brightness_transport: socket; display loggedBrightness set to 70% over the control socket, then back to 80.POST /errors/clear→transport: socket,applied: true, messageCleared all errors;/errors/summaryclear_pending: false.CacheManager().set('display_on_demand_request', ...)) while the socket is up → active after 0.52 s, logged once:... came through the file mailbox although the control socket is up. The mailbox is deprecated ...; the mailbox file was consumed.Display stopped:
/health→degraded,display_loop: stopped(sourceservice); current-status →source: cache, all null.start_service: false→ 400, mailbox withdrawn (no file left).transport: mailbox,socket_error: no_socket./errors/clear→transport: mailbox,applied: false, request file written.start_service: true→transport: mailbox, service started; the new display took the request from the mailbox on its first poll (on screen ~12 s after the start, i.e. as soon as the display was up), then served the socket.🤖 Generated with Claude Code