feat(ipc)!: remove the cache-key mailboxes, control socket stage 5 (for the release after 3.8.1) - #773
Draft
ChuckBuilds wants to merge 10 commits into
Draft
feat(ipc)!: remove the cache-key mailboxes, control socket stage 5 (for the release after 3.8.1)#773ChuckBuilds wants to merge 10 commits into
ChuckBuilds wants to merge 10 commits into
Conversation
The control socket is now the only way the web interface sends the display a command. The display stops reading display_on_demand_request and plugin_error_clear_request, and the web interface stops writing them. - Display: no mailbox poll (MailboxWatch, the 1 s / 0.25 s cadence, _consume_on_demand_request, the deprecation log) and no persisted display_on_demand_processed_id guard; the error publisher reads no clear request. CacheManager.file_signature and MailboxWatch are removed. - A write to either retired key is dropped by CacheManager.save_cache and logged once per writer, naming the plugin from the call stack (or the request's plugin_id), with the API to move to. - Web: on-demand start with no display listening starts the service (when start_service) and sends the request again once the socket answers (45 s, 10 s for a running service without a socket yet); every other failure is a 503 (400 for invalid_args). Stop answers 503 when no display listens, unless stop_service. errors/clear answers 503 with a reason-specific message instead of writing a request; clear_pending is always false. src.ipc.client.should_fall_back is replaced by display_not_listening. - Kept: display_current_state, display_on_demand_state, plugin_runtime_snapshot and the heartbeat (read whenever the socket cannot answer), and display_on_demand_config (the display's resume record). Tests: mailbox-only tests removed (test_on_demand_mailbox.py, the mailbox cadence, file_signature and MailboxWatch tests); tests that injected requests through the mailbox now use the socket queue or a plugin's in-process request. The run-loop harness sends on-demand requests over its fake control socket, so four golden traces change: on-demand starts and stops land at the request instant instead of the next 0.25 s mailbox look (one frame fewer on the screen they end), and in vegas.json within one frame instead of 263 ms, which shifts the later 1 s-throttled WiFi-notice check by under a second. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e mailbox test_api_v3_bool_coercion, test_api_v3_lazy_plugin_discovery and test_web_api's stop test checked the mailbox write; they now get an ack from a mocked control socket. The stop route touches no manager any more, so it leaves the removed-catch-all sample. A request that was sent is never 'not listening' (pins the sent check the mutation run let through). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
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 | 20 |
| Duplication | 0 |
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.
# Conflicts: # CHANGELOG.md
…vered in the background The start route held a request open for up to 45 s while a cold-started display loaded its plugins; the MQTT bridge (15 s timeout) and browsers reported a failure for a request that was then delivered. Now, when no display is listening, the route starts the service if asked and answers 202 with status "starting" at once. A single worker in the web process (web_interface/on_demand_dispatch.py) sends the request until the display acknowledges it or the wait runs out (45 s cold start, 10 s for a running service without a socket yet). A newer start supersedes the pending one; a stop cancels it (and succeeds, with cancelled_request_id, even with no display listening). The outcome is reported by /display/on-demand/status (source "web": starting, or error with start-timeout or the socket's reason, until the display publishes something newer) and by /display/current-status as on_demand_pending. Callers: the web UI's on-demand modal and "Preview on display" treat "starting" as taken (an info toast); the MQTT bridge already treats any non-error 2xx as success (now pinned by a test). Tests: the dispatcher (ack, retry then ack, start-timeout, other failures, superseded, an in-flight ack for a superseded start, stop while pending, a per-start wait, outcome lifetime); the routes (202, status routes while pending and after a timeout, a later display state replacing the failure, stop while pending, a new start superseding); a JS suite for app.js. Mutation check: 20 mutants on the worker, the routes and app.js, 20 killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines
+366
to
+382
| return jsonify({ | ||
| 'status': 'starting', | ||
| 'message': ('The display service is starting; the request is sent to it as soon ' | ||
| 'as it is listening. Check the on-demand status for the outcome.'), | ||
| 'data': { | ||
| 'request_id': request_id, | ||
| 'plugin_id': resolved_plugin, | ||
| 'mode': resolved_mode, | ||
| 'duration': duration, | ||
| 'pinned': pinned, | ||
| 'service': service_result, | ||
| 'transport': 'socket', | ||
| 'socket_error': reason, | ||
| 'pending': True, | ||
| 'wait_seconds': wait, | ||
| }, | ||
| }), 202 |
…boxes # Conflicts: # CHANGELOG.md
… its replacement CI caught a race in test_a_new_start_supersedes_the_pending_one: the dispatcher's worker could read the old start, the route cancel it and send the new one, and the worker's send of the old one land after it. cancel() now waits out a send already in flight (the worker holds a send lock while it reads the pending start and sends it), so whatever the caller sends next lands after it. Pinned by test_a_cancel_waits_for_a_send_in_flight, which fails without the wait. The Linux-only TestRealSocket test for a display that went away now expects the 202 and the start-timeout that follows, as the route answers since the background dispatcher. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…boxes # Conflicts: # CHANGELOG.md
…play acts on it On ledpi (three cold starts) the display acknowledged the start as its socket opened, then took ~5 s to act on it while Vegas built its first strip; the status routes meanwhile showed the display's own idle state, so a UI polling every 700 ms flashed idle. The display's on-demand state now names the request it answers (request_id). A delivered start keeps reading as status "starting" with delivered: true, in /display/on-demand/status and as on_demand_pending in /display/current-status, until the display publishes state for that request id (a display without the field: any state newer than the delivery), for at most DELIVERED_SHOWN_SECONDS (30 s). The display's startup state, which can be published after the acknowledgement, names no request and does not end it. Tests: stays starting against the startup idle state (no id, an older id); the matching active state and the matching error take over; an older display's newer state takes over; the 30 s cap; the display's state names its request. Mutation check: 11 mutants, 11 killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…boxes # Conflicts: # CHANGELOG.md
…s built without __init__ _on_demand_state now publishes it, and test_state_stream_readers builds controllers with __new__. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge condition: do not merge yet
This is stage 5 of the control-socket plan in
docs/IPC_CONTROL_SOCKET.md. It is a draft on purpose. Merge it only when one of these is true:BasePlugin.request_on_demand()/end_on_demand()) has been tagged, and one more release cycle has passed after it. Both ship in 3.8.1 (chore: prepare the 3.8.1 release #756, merged), so this waits for 3.8.1's tag plus one more release cycle;That gives older and third-party plugins a full release to move off
display_on_demand_request. The branch is merged with main through 3.8.2 (#776). Its CHANGELOG section sits under## Unreleased, above## 3.8.2.What is removed
Display
MailboxWatch,MAILBOX_POLL_INTERVAL_WITH_SOCKET/ON_DEMAND_POLL_INTERVAL,_last_on_demand_poll,_mailbox_poll_interval,_consume_on_demand_requestand_note_mailbox_requestare all gone._poll_on_demand_requests()now only drains the socket queue and the plugin queue, so it touches no cache key.display_on_demand_processed_idguard is gone. It existed only to stop a mailbox from being replayed after a restart. The in-memoryon_demand_request_idcheck still drops a start that is sent twice.plugin_error_clear_request. Clears arrive only througherrors.clear, whichclear_nowapplies.CacheManager.file_signature,src.cache_manager.MailboxWatch,src.ipc.client.should_fall_back(replaced bydisplay_not_listening),src.error_aggregator.ERROR_CLEAR_REQUEST_KEY, and the clear-request plumbing inread_error_report/error_summary_from_report/plugin_health_from_report/request_error_clear(_pending_cutoff,_is_after,applied_clear_cutoff).Web
POST /display/on-demand/start:no_socket/refused, never sent):start_servicefalse:400, as before;202withstatus: "starting"at once. It never waits for the display.web_interface/on_demand_dispatch.py) then sends the request every 0.5 s until the display acknowledges it, for up to 45 s after a cold start or 10 s for a running service without a socket yet. Any other failure ends it at once.cancelled_request_id, even with no display listening.cancel()waits out a send of the old start that is already in flight (at most the 1 s client timeout), so a superseded start never reaches the display after its replacement. CI caught that race;test_a_cancel_waits_for_a_send_in_flightpins it.GET /display/on-demand/status:stateis{status: "starting", source: "web", request_id, plugin_id, ...}while pending, the display's own state once delivered, or{status: "error", error: "start-timeout"}(or the socket's reason) until the display publishes something newer;GET /display/current-status: addson_demand_pending.startingwithdelivered: trueuntil the display publishes the state that answers it, for at most 30 s (DELIVERED_SHOWN_SECONDS). On ledpi the display acknowledged a start as its socket opened but acted on it ~5 s later, while Vegas built its first strip, so a UI polling every 700 ms flashed idle. The display's on-demand state now names itsrequest_id, and the status routes wait for a match. A display without the field counts any state published after the delivery. Its startup state names no request, so it does not end the wait._callraises only on>= 400orstatus: "error"); a new test pins that for202.app.js) treated onlystatus: "success"as taken. They now treat"starting"as taken too: an info toast, and the floating preview opens.request_on_demand().503forunknown_commandfrom an older display,disabled/unsupported,busy, timeouts andforbidden;400forinvalid_args. Every error carriessocket_error.POST /display/on-demand/stop:503when no display is listening and no pending start was cancelled. The message says whether the service is stopped or may still be starting. Withstop_service, the route still stops the service and succeeds.POST /errors/clear: without the socket it answers503withcontext.socket_error. The message depends on the cause:transportis always"socket", andclear_pendingis alwaysfalse. Both fields stay for API compatibility.Tests removed (mailbox only):
test_on_demand_mailbox.py, plus the mailbox cadence,file_signature,MailboxWatch, deprecation-log and mailbox-fallback classes intest_ipc_stage4.py,test_api_v3_on_demand_socket.py,test_api_v3_on_demand_restart.pyandtest_error_snapshot_cross_process.py.What stays, and why
display_current_stateGET /display/current-statuswhen the socket cannot answerdisplay_on_demand_stateGET /display/on-demand/statuswhen the socket cannot answerplugin_runtime_snapshot/plugins/installed(runtime),/plugins/state, the reconciliations, when the socket cannot answerdisplay-heartbeat.json(tmpfs)/healthdisplay_loop, the update health checkdisplay_on_demand_configplugin_error_snapshot/errors/*routes (their only source)"When the socket cannot answer" covers several cases: the display is stopped or still starting, the web user is not yet in the socket's group, the platform has no Unix sockets (Windows), or
LEDMATRIX_CONTROL_SOCKET=off. A stopped display in particular still shows its last state through these keys. Retiring them is left for a later change.A plugin that still writes
display_on_demand_requestThe display process has no mailbox reader any more, so
CacheManager.save_cache(and soset) refuses both retired keys (RETIRED_MAILBOX_KEYS):Ignored a write to the retired 'display_on_demand_request' cache key by plugin 'on-air': the display no longer reads this file mailbox. Update it to call self.request_on_demand() / self.end_on_demand() instead ...selfhas a stringplugin_idand acache_manager(any BasePlugin). Failing that, the request's ownplugin_id, marked(named in the request). Otherwiseunknown.Writers found:
plugins/birdnet-go/manager.py:455request_on_demandis missing, returnsNone, or raisesplugins/mqtt-notifications/manager.py:346plugins/on-air/manager.py:553plugins/pomodoro-timer/manager.py:1383No plugin anywhere writes or reads
plugin_error_clear_request, and none uses the removed helpers. That includesMailboxWatch,file_signature,should_fall_back, the error-report functions anddisplay_on_demand_processed_id.Third-party repos in
plugins.json(the 8 with their own repo URL) were all cloned and searched, with no hits: BigC07/ledmatrix-f1-live, ant456/ledmatrix-gif-player, sarjent/ledmatrix-golf, ant456/ledmatrix-plex-marquee, ryug0/ledmatrix-dresden-departures, trsadler/MLB-Scoreboard-LEDMatrix, ant456/ledmatrix-sleeper-fantasy, crazzybrad/LEDMATRIX-NASCAR-PLUGIN.GitHub-wide
gh search codehits outside these repos are copies of core, not plugins:ant456/ledmatrix-fixes-repopatches/display_controller.py: a patched display controller that reads the mailbox. It will diverge from this change.darthdavidthesith/DavidLedMatrixCode: a copy of core.After this merges, each of the four monorepo plugins can drop its mailbox fallback once its
ledmatrix_min_versionreaches the release with #768. Until then the fallback only runs where it should do nothing anyway (no display in the process, or a full queue), and on a stage-5 core it costs one warning line.Behaviour notes for review
202at once, and the web process delivers it in the background (see above). A client follows the outcome through the on-demand status route. The pending start lives in the web process's memory, so restarting the web service drops it.LEDMATRIX_CONTROL_SOCKET=off. The web UI can no longer start or stop on-demand sessions or clear errors there. The mailbox used to carry those requests.on_demand,on_demand_named_live,on_demand_pinned: starts and stops land at the request instant instead of the next 0.25 s mailbox look, one frame fewer on the screen they end.vegas: the request lands within one frame instead of 263 ms. That moves the later 1 s-throttled WiFi-notice check by under a second.Tests
unknown;display_not_listeningfor every reason, andsent=True;test/test_on_demand_dispatch.py): acknowledgement, retry then ack,start-timeout, a per-start wait, other failures reported at once, superseded (including an in-flight ack for the superseded start), stop while pending, cancel with nothing pending, outcome lifetime;202answered at once for a cold start and for a running service without a socket, the status routes while pending and after a timeout, a later display state replacing the failure, a stop while pending, a new start superseding a pending one;202startingas success, and a JS suite (test/js/unit/test_on_demand_starting.js) forapp.js;stop_service;errors/clearfailure message;client.display_not_listening, the start/stop/clear routes, the retired-key guard and its writer naming, the display's duplicate and ownership guards): 19 killed. The one that first survived (dropping thesentcheck) is now pinned bytest_a_request_that_was_sent_had_a_display.app.js(no retry, retry everything, no timeout, reporting a superseded result, delivered left pending, no supersede, ignoring a per-start wait, cancel that keeps sending, outcomes that never expire,200instead of202, never submitting, the wrong wait, no supersede and no cancel in the routes, the status routes not showing or always showing the pending start,app.jstreatingstartingas failure): 20 killed. One survived at first (ignoring a per-start wait) and is now pinned bytest_a_start_can_carry_its_own_wait. Removing the in-flight wait incancel()is killed bytest_a_cancel_waits_for_a_send_in_flight.deliveredflag, always shadowing, id match ignored or inverted, timestamp rule inverted,current-statusnot gated, the display omittingrequest_id): 11 killed.test_config_flows.py::test_multiple_config_changes: it failed once on the branch and fails 2 runs in 6 on the baseline alone (flaky on main). The existing Windows failures are starlark ownership, install_lowmem, pixlet_download and similar. The Linux-only socket tests (TestOverTheSocket,TestRealSocket) skip on Windows and run in CI.scripts/check_types.py): same result asorigin/main. The one existing error (fetch_service.py:676, unused ignore on Windows) is also there on the baseline.cache_manager/error_aggregator/ipc.clientdrop from 16 to 15 errors (not on the ratchet).~/hwbatch.lockwas held by another session (a malloc-trim soak) every time it was checked.🤖 Generated with Claude Code