Skip to content

feat(service): supervised Python/Rust services with a non-blocking client - #1330

Merged
zackees merged 3 commits into
mainfrom
feat/python-broker-service
Oct 26, 2026
Merged

zackees merged 3 commits into
mainfrom
feat/python-broker-service

Conversation

@zackees

@zackees zackees commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Adds backend_sdk::service (ServiceServer, PollingFrameClient) and a light top-level Python package running_process_service (ServiceSpec / ServiceServer / serve / ServiceClient).

  • Single instance via bind_singleton on a per-user endpoint derived from the name; identity sidecar + BackendHandle probe for health; client-side respawn of command via spawn::spawn_daemon under a lock file, 3 per 30 s.
  • Client never blocks: send fails fast (ServiceUnavailable / QueueFull), try_recv polls, replies matched by request id (out of order OK).
  • Payload protocol is a caller parameter; no registry change.
  • Blessed API: PollingFrameClient ops listed with blocking = false; API snapshots, parity manifest, sync baseline updated. Docs: docs/SERVICE.md.

Deviation from the agreed design, stated: respawn uses spawn::spawn_daemon instead of launch_detached, because launch_detached is a running-process-daemon client and would make each service depend on that daemon.

Tests: tests/test_service.py (13), Rust unit tests in service.rs (4). Motivating consumer: kitty-worker in zackees' nixos config (kitty's main thread can only poll from timers).

@zackees zackees added the ci-full Run the complete CI matrix on this PR label Oct 26, 2026
…ient

The broker service API was Rust-only, and its frame client blocks. A
caller living on a UI thread whose loop only offers timers (kitty's
main thread, which draws every terminal) can use neither: one blocking
IPC call is a frozen screen.

backend_sdk::service packages the direct-endpoint recipe (INTEGRATE.md)
without the broker:

- ServiceServer binds a per-user endpoint derived from the service name
  through bind_singleton, so a second instance is refused and a crashed
  one's stale socket reclaimed; writes the identity sidecar; answers
  BackendHandle probes via the mux; queues requests for worker threads.
- PollingFrameClient: every method touches only a queue or an atomic.
  A supervisor thread probes the identity file on connect and every
  health_interval, and when the service is absent spawns the spec's
  command through spawn::spawn_daemon under an exclusive lock file,
  at most 3 per 30 s (the broker's own default budget). send() fails
  fast (Unavailable / QueueFull); replies come back through try_recv()
  matched by request id, out of order; a request in flight when the
  connection drops comes back as an error reply rather than vanishing.

Python gets it as a separate top-level package, running_process_service,
which loads running_process._native straight from the package directory
so importing it does not execute running_process/__init__.py.

Respawn uses spawn::spawn_daemon rather than launch_detached:
launch_detached is a running-process-daemon client and would make every
service depend on a second daemon being startable.

Payload protocol is the caller's (the binding registers none). The new
client is listed in blessed_api.toml (blocking = false) and the surfaces
in the API snapshots, parity manifest and sync baseline. docs/SERVICE.md
is the user guide.

Local-Gate: v1 tree=25c894fb332adf88e75564e154d2a9abb61cb925 secs=1491 lanes=linux-quick:run
Ci-Attestation: {"at":1792975619,"gate":"rust/all/fmt","host":"linux-x86_64","key":"bc94558ff21386c2012d00067c50572bc9fba0ec34da32a7caa2523f7fcb3e1b","lane":"linux-quick","parents":["dee1c2c6307739b6aed08927264a775bd6a2b727"],"secs":1491,"stamp":"d321ad644b5373401af5043fb21478cc","tree":"25c894fb332adf88e75564e154d2a9abb61cb925","v":1,"via":"run"}
Ci-Attestation: {"at":1792975619,"gate":"python/all/static","host":"linux-x86_64","key":"bc94558ff21386c2012d00067c50572bc9fba0ec34da32a7caa2523f7fcb3e1b","lane":"linux-quick","parents":["dee1c2c6307739b6aed08927264a775bd6a2b727"],"secs":1491,"stamp":"b82bbeaed748ae93b4690897f84c5adf","tree":"25c894fb332adf88e75564e154d2a9abb61cb925","v":1,"via":"run"}
Ci-Attestation: {"at":1792975619,"gate":"rust/x86_64-unknown-linux-gnu/check","host":"linux-x86_64","key":"bc94558ff21386c2012d00067c50572bc9fba0ec34da32a7caa2523f7fcb3e1b","lane":"linux-quick","parents":["dee1c2c6307739b6aed08927264a775bd6a2b727"],"secs":1491,"stamp":"e2b0c7e52aed54a448de89fa8fc211f1","tree":"25c894fb332adf88e75564e154d2a9abb61cb925","v":1,"via":"run"}
Ci-Attestation: {"at":1792975619,"gate":"rust/x86_64-unknown-linux-gnu/dylint","host":"linux-x86_64","key":"bc94558ff21386c2012d00067c50572bc9fba0ec34da32a7caa2523f7fcb3e1b","lane":"linux-quick","parents":["dee1c2c6307739b6aed08927264a775bd6a2b727"],"secs":1491,"stamp":"58dbf9aa3c67bc718dd9da7c1fb43ede","tree":"25c894fb332adf88e75564e154d2a9abb61cb925","v":1,"via":"run"}
Ci-Attestation: {"at":1792975619,"gate":"rust/x86_64-unknown-linux-gnu/test","host":"linux-x86_64","key":"bc94558ff21386c2012d00067c50572bc9fba0ec34da32a7caa2523f7fcb3e1b","lane":"linux-quick","parents":["dee1c2c6307739b6aed08927264a775bd6a2b727"],"secs":1491,"stamp":"b4b5db0afab650af4c26322013eab867","tree":"25c894fb332adf88e75564e154d2a9abb61cb925","v":1,"via":"run"}
Ci-Attestation: {"at":1792975619,"gate":"python/linux-x64/test","host":"linux-x86_64","key":"bc94558ff21386c2012d00067c50572bc9fba0ec34da32a7caa2523f7fcb3e1b","lane":"linux-quick","parents":["dee1c2c6307739b6aed08927264a775bd6a2b727"],"secs":1491,"stamp":"fb661aba610c8f34cc4b43bb5f168595","tree":"25c894fb332adf88e75564e154d2a9abb61cb925","v":1,"via":"run"}
Ci-Attestation: {"at":1792975619,"gate":"general/linux-x64/perf","host":"linux-x86_64","key":"bc94558ff21386c2012d00067c50572bc9fba0ec34da32a7caa2523f7fcb3e1b","lane":"linux-quick","parents":["dee1c2c6307739b6aed08927264a775bd6a2b727"],"secs":1491,"stamp":"de62abe40aea486d7033f86d216cb84c","tree":"25c894fb332adf88e75564e154d2a9abb61cb925","v":1,"via":"run"}
@zackees
zackees force-pushed the feat/python-broker-service branch from b3729db to b3a1cf2 Compare October 26, 2026 00:52
@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.27933% with 87 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../running-process/src/broker/backend_sdk/service.rs 90.33% 70 Missing ⚠️
crates/running-process-py/src/service.rs 91.25% 14 Missing ⚠️
...rocess-platform-internal/src/platform_linux/ipc.rs 57.14% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

A Windows named-pipe name stays taken while any connected server
instance is open, so ServiceServer::close released the endpoint only
once clients disconnected -- a restarted service could not rebind
(close_releases_endpoint_and_stops_client failed on windows-x86).
Connection threads now read with a poll timeout, drop their handle when
the server stops, and close waits (bounded, 2 s) for them.

Local-Gate: v1 tree=853931f3500afad19a454602ad61e69b757b4b61 secs=946 lanes=linux-quick:run
Ci-Attestation: {"at":1792978915,"gate":"rust/all/fmt","host":"linux-x86_64","key":"6d961a198cfb7f6b52e73cc8f04c590ffddde6ae459d1d673fce08ffffb067b5","lane":"linux-quick","parents":["b3a1cf2f05e7e39c9cd8c581ec48748c6aee2da2"],"secs":946,"stamp":"65fabf6de6ef8a33443118242f789240","tree":"853931f3500afad19a454602ad61e69b757b4b61","v":1,"via":"run"}
Ci-Attestation: {"at":1792978915,"gate":"python/all/static","host":"linux-x86_64","key":"6d961a198cfb7f6b52e73cc8f04c590ffddde6ae459d1d673fce08ffffb067b5","lane":"linux-quick","parents":["b3a1cf2f05e7e39c9cd8c581ec48748c6aee2da2"],"secs":946,"stamp":"8694bd8faedf64a67dd9365017dc5c01","tree":"853931f3500afad19a454602ad61e69b757b4b61","v":1,"via":"run"}
Ci-Attestation: {"at":1792978915,"gate":"rust/x86_64-unknown-linux-gnu/check","host":"linux-x86_64","key":"6d961a198cfb7f6b52e73cc8f04c590ffddde6ae459d1d673fce08ffffb067b5","lane":"linux-quick","parents":["b3a1cf2f05e7e39c9cd8c581ec48748c6aee2da2"],"secs":946,"stamp":"f31887a964f2b68bf839c0f434f9dcec","tree":"853931f3500afad19a454602ad61e69b757b4b61","v":1,"via":"run"}
Ci-Attestation: {"at":1792978915,"gate":"rust/x86_64-unknown-linux-gnu/dylint","host":"linux-x86_64","key":"6d961a198cfb7f6b52e73cc8f04c590ffddde6ae459d1d673fce08ffffb067b5","lane":"linux-quick","parents":["b3a1cf2f05e7e39c9cd8c581ec48748c6aee2da2"],"secs":946,"stamp":"d9d810daa542b262c2041b7f0b3be610","tree":"853931f3500afad19a454602ad61e69b757b4b61","v":1,"via":"run"}
Ci-Attestation: {"at":1792978915,"gate":"rust/x86_64-unknown-linux-gnu/test","host":"linux-x86_64","key":"6d961a198cfb7f6b52e73cc8f04c590ffddde6ae459d1d673fce08ffffb067b5","lane":"linux-quick","parents":["b3a1cf2f05e7e39c9cd8c581ec48748c6aee2da2"],"secs":946,"stamp":"05b9ce2a455f819a26883a8f6625d969","tree":"853931f3500afad19a454602ad61e69b757b4b61","v":1,"via":"run"}
Ci-Attestation: {"at":1792978915,"gate":"python/linux-x64/test","host":"linux-x86_64","key":"6d961a198cfb7f6b52e73cc8f04c590ffddde6ae459d1d673fce08ffffb067b5","lane":"linux-quick","parents":["b3a1cf2f05e7e39c9cd8c581ec48748c6aee2da2"],"secs":946,"stamp":"86b720be709e94b646f01fe754c62267","tree":"853931f3500afad19a454602ad61e69b757b4b61","v":1,"via":"run"}
Ci-Attestation: {"at":1792978915,"gate":"general/linux-x64/perf","host":"linux-x86_64","key":"6d961a198cfb7f6b52e73cc8f04c590ffddde6ae459d1d673fce08ffffb067b5","lane":"linux-quick","parents":["b3a1cf2f05e7e39c9cd8c581ec48748c6aee2da2"],"secs":946,"stamp":"9a596672524c1b6fd0d0ec60948b94da","tree":"853931f3500afad19a454602ad61e69b757b4b61","v":1,"via":"run"}
A Windows named-pipe name stays taken while any connected instance is
open, and a connection thread blocked in read holds one. Neither
set_recv_timeout (pipes ignore it) nor PIPE_NOWAIT (interprocess reports
ERROR_NO_DATA as end-of-stream) can wake it, which is how
close_releases_endpoint_and_stops_client and then round_trip failed on
windows-x86.

The platform choice lives behind the ipc facade, as the boundary gate
requires: Stream::set_interruptible_reads sets a receive timeout on Unix,
and on Windows says to check Stream::bytes_available (PeekNamedPipe)
before each read. The service polls that way with a 1-20 ms idle
backoff, so every connection thread notices close within a poll and
drops its handle; close waits for them (bounded, 2 s).

Local-Gate: v1 tree=249b6269cd5287c305e24ca2c17ef703fd4516ad secs=2368 lanes=linux-quick:run
Ci-Attestation: {"at":1792990969,"gate":"rust/all/fmt","host":"linux-x86_64","key":"6a939c78e0c85f571d6007eb717aef025b7f4e52acd374a42e7fec4220d5aa56","lane":"linux-quick","parents":["15f441429d074d4ac16dca29b2ceb02fe8eedcee"],"secs":2368,"stamp":"1d0b4f5d70b6e695438f236e9addb5ef","tree":"249b6269cd5287c305e24ca2c17ef703fd4516ad","v":1,"via":"run"}
Ci-Attestation: {"at":1792990969,"gate":"python/all/static","host":"linux-x86_64","key":"6a939c78e0c85f571d6007eb717aef025b7f4e52acd374a42e7fec4220d5aa56","lane":"linux-quick","parents":["15f441429d074d4ac16dca29b2ceb02fe8eedcee"],"secs":2368,"stamp":"47c5eb4ac7b5873ec6f1c025ecca0d39","tree":"249b6269cd5287c305e24ca2c17ef703fd4516ad","v":1,"via":"run"}
Ci-Attestation: {"at":1792990969,"gate":"rust/x86_64-unknown-linux-gnu/check","host":"linux-x86_64","key":"6a939c78e0c85f571d6007eb717aef025b7f4e52acd374a42e7fec4220d5aa56","lane":"linux-quick","parents":["15f441429d074d4ac16dca29b2ceb02fe8eedcee"],"secs":2368,"stamp":"c737f130234ce317198d256133303c8e","tree":"249b6269cd5287c305e24ca2c17ef703fd4516ad","v":1,"via":"run"}
Ci-Attestation: {"at":1792990969,"gate":"rust/x86_64-unknown-linux-gnu/dylint","host":"linux-x86_64","key":"6a939c78e0c85f571d6007eb717aef025b7f4e52acd374a42e7fec4220d5aa56","lane":"linux-quick","parents":["15f441429d074d4ac16dca29b2ceb02fe8eedcee"],"secs":2368,"stamp":"5ded3852f9760a359d413b654f0df027","tree":"249b6269cd5287c305e24ca2c17ef703fd4516ad","v":1,"via":"run"}
Ci-Attestation: {"at":1792990969,"gate":"rust/x86_64-unknown-linux-gnu/test","host":"linux-x86_64","key":"6a939c78e0c85f571d6007eb717aef025b7f4e52acd374a42e7fec4220d5aa56","lane":"linux-quick","parents":["15f441429d074d4ac16dca29b2ceb02fe8eedcee"],"secs":2368,"stamp":"713e85f3933b6e19210be366ae7ae346","tree":"249b6269cd5287c305e24ca2c17ef703fd4516ad","v":1,"via":"run"}
Ci-Attestation: {"at":1792990969,"gate":"python/linux-x64/test","host":"linux-x86_64","key":"6a939c78e0c85f571d6007eb717aef025b7f4e52acd374a42e7fec4220d5aa56","lane":"linux-quick","parents":["15f441429d074d4ac16dca29b2ceb02fe8eedcee"],"secs":2368,"stamp":"4a15e2f72e71cfe2cbdf39bff2726b79","tree":"249b6269cd5287c305e24ca2c17ef703fd4516ad","v":1,"via":"run"}
Ci-Attestation: {"at":1792990969,"gate":"general/linux-x64/perf","host":"linux-x86_64","key":"6a939c78e0c85f571d6007eb717aef025b7f4e52acd374a42e7fec4220d5aa56","lane":"linux-quick","parents":["15f441429d074d4ac16dca29b2ceb02fe8eedcee"],"secs":2368,"stamp":"61c11e1f6345660c131a448025ebe6fc","tree":"249b6269cd5287c305e24ca2c17ef703fd4516ad","v":1,"via":"run"}
@zackees
zackees force-pushed the feat/python-broker-service branch from 3284521 to 7429c9f Compare October 26, 2026 05:03
@zackees
zackees merged commit 3cac77a into main Oct 26, 2026
57 of 64 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-full Run the complete CI matrix on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant