Repository navigation
Replace the IPC server's 10ms poll with a blocking receive - #65
Closed
Magniquick wants to merge 1 commit into
Closed
Magniquick wants to merge 1 commit into
Magniquick wants to merge 1 commit into
Conversation
IpcSocketServer::run looped on recv_timeout(10ms). The timeout was not for the channel; it existed so the loop fell through to a non-blocking accept() and to prune_clients() a hundred times a second, whether or not anything was connected. That was the whole idle cost of the ipc-server thread. The channel now carries a ServerMsg: Event, Client or Shutdown. The listener moves to an ipc-accept thread that blocks in accept() and forwards each connection as Client, so the server loop can do a plain blocking recv(), drain with try_recv(), then prune. Both threads are 0 wakeups per second when idle. IpcServer::shutdown() sends Shutdown before it joins. The accept thread holds a sender, so the channel never disconnects by itself and the explicit message is what ends the loop. The loop no longer watches the running flag: a thread blocked in recv() cannot observe an atomic, so checking one there would be illusory safety. IpcServer::start now creates the channel and returns the notifier with it, which drops the sender accessor and shrinks the call site to one line. Accept errors are retried after 100ms instead of every 10ms. std retries EINTR internally, so what reaches that arm means out of resources: EMFILE, ENFILE, ENOMEM, ENOBUFS. Returning there would close the listener for the rest of the process lifetime while the socket file stayed on disk, so every later client would get ECONNREFUSED. The kernel allocates the descriptor before dequeuing from the backlog, so a connection that hit EMFILE is still queued and is served on the next retry. Two consequences worth naming. The socket now stays up until Core returns, so sunsetr status during the smooth shutdown transition answers with the last applied state instead of failing; nothing waits on the socket file, as stop and restart wait on the lock file. And a subscriber that closes while the loop is blocked keeps its descriptors until the next message, so the stale count is bounded by peak concurrent subscribers, since every new connection is itself a message and prunes the ones before it. Two tests cover the shutdown path, with a bounded join so a hung loop fails the test instead of wedging the run: Shutdown with no client ever connected, which is the case a blocking receive could have deadlocked, and Shutdown with a subscriber attached that has already read its registration snapshot. Both assert the socket file is removed.
There was a problem hiding this comment.
🟢 Approval recommended
The change achieves the stated idle-wakeup goal with a coherent design and adds targeted shutdown tests; only a minor documentation accuracy tweak was noted.
Pull request overview
This PR removes the IPC server thread’s periodic 10ms wakeups by restructuring the IPC server to block on a single channel receive, while moving socket accept() into a dedicated blocking accept thread. This aligns the IPC subsystem with the repo’s broader idle-wakeup reductions (following #64) by keeping both IPC threads idle when no clients or events exist.
Changes:
- Introduce a
ServerMsgenum so the server loop can block onrecv()and handle events, new clients, and shutdown through one channel. - Move
UnixListener::accept()to anipc-acceptthread that forwards new connections to the server loop asServerMsg::Client. - Update the IPC startup API to return
(IpcNotifier, IpcServer)fromIpcServer::start(), and add tests that ensure shutdown unblocks a blockingrecv()loop and removes the socket file.
File summaries
| File | Description |
|---|---|
| src/sunsetr.rs | Simplifies the IPC startup call site to use the new IpcServer::start(debug_enabled) API and preserves shutdown behavior. |
| src/state/ipc/mod.rs | Refactors IPC wiring to use ServerMsg, adds explicit Shutdown signaling, and adjusts IpcNotifier to wrap events. |
| src/state/ipc/server.rs | Implements blocking recv() server loop, adds accept thread forwarding connections via ServerMsg, and adds shutdown-focused tests. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+129
to
+133
| /// Take the listener off the main loop's hands. The thread blocks in | ||
| /// `accept()` and is never joined. Once the server loop drops the receiver | ||
| /// the next accepted connection fails to send and the thread returns, so | ||
| /// this leaks nothing per process. It would leak a thread and a descriptor | ||
| /// per cycle if an in-process restart were ever added; today |
Owner
Owner
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.
Written with an LLM. Tested on real hardware, not just compiled.
This is the second half of the idle-wakeup work after #64, which left the IPC
server thread as the process's remaining idle cost. It touches only
src/state/ipc/and one call site insrc/sunsetr.rs, and it does not dependon #64, so it applies to main on its own.
Problem
IpcSocketServer::runloops onrecv_timeout(10ms). The timeout is not forthe channel; it exists so the loop falls through to a non-blocking
accept()and to
prune_clients()a hundred times a second, whether or not anything isconnected. Nothing on my machine subscribes to the socket, so that was about
100 wakeups per second for nothing.
Fix
The channel now carries a
ServerMsgenum:Event(IpcEvent),Client(UnixStream), orShutdown. The listener moves to a newipc-acceptthread that blocks in
accept()and forwards each connection asClient, sothe server loop can do a plain blocking
recv(), drain withtry_recv(), thenprune. Both threads are 0 wakeups/s when idle.
IpcServer::shutdown()sendsShutdownbefore it joins. The accept threadholds a sender, so the channel never disconnects by itself; the explicit
message is what ends the loop. The loop no longer watches the
runningflag,because a thread blocked in
recv()cannot observe an atomic and checking onethere would be illusory safety.
IpcServer::startnow creates the channel andreturns the notifier with it, so the call site shrinks to one line.
Behaviour changes worth knowing about:
sunsetr statusduring the smoothshutdown transition now answers with the last applied state instead of
failing. Nothing waits on the socket file;
stopandrestartwait on thelock file.
EINTR internally, so what reaches that arm is EMFILE/ENFILE/ENOMEM/ENOBUFS.
The kernel allocates the descriptor before dequeuing from the backlog, so a
connection that hit EMFILE is still queued and is served on the next retry.
the next message. The stale count is bounded by peak concurrent subscribers,
since every new connection is itself a message and prunes the ones before it.
The accept thread is never joined. Once the loop drops the receiver, the next
accepted connection fails to send and the thread returns. It would leak a thread
and a descriptor per cycle if an in-process restart were added; today
ensure_single_instanceruns before the server starts, so there is one perprocess.
Two tests cover the shutdown path, with a bounded join so a hung loop fails the
test instead of wedging the run:
Shutdownwith no client ever connected, whichis the case a blocking receive could have deadlocked, and
Shutdownwith asubscriber attached that has already read its registration snapshot. Both assert
the socket file is removed.
Tested
Arch, Hyprland, Intel Xe.
with Replace hotplug polling with a registry watcher thread #64 alone and about 290/s on unpatched main.
ipc-serverandipc-acceptboth 0.sunsetr statusone-shot served in about 20ms, and the debug log shows theclient pruned back to 0 on the next message.
config_changedandstate_appliedevents across two config reloads.file removed, join did not hang.
Not exercised: a real EMFILE on the listener.