sdk/python: keep a handle's relay connection across calls - #199
Merged
Merged
Conversation
Every data-plane call dialed the owner node, upgraded and, behind an edge, shook hands with TLS, then paid a vsock connect on the node (#195). With silkd serving RPCs back to back (#196) a handle now parks its connection after a call and the next call sends on it: a Sandbox carries a ConnPool (8 connections, 30 s idle; Client(keep_alive=...) tunes or disables it) and every RPC helper runs under _park_after, which parks the connection after a terminal frame, error frames included, and drops it on anything else. Streams (watch, pty, port_forward, lsp) still own their connection; run joins its stdin thread before the connection goes back, so the pump's frames never land on the next RPC. close and hibernate drain the pool first. Before a parked connection is reused, a zero-timeout select tells a peer that hung up (hibernate, release, the idle sweep) or spoke unprompted from a quiet one, so the next call redials and wakes the guest as before. A handle's first dial asks the daemon's proto with one info round trip: a daemon before proto 2 closes after answering, so the handle redials and dials per call from then on. logs' trailing done is consumed so the connection stays in frame. Hot path: a pooled call costs one lock pair and one select(2) instead of a TCP dial, an HTTP upgrade, a TLS handshake and a vsock connect; the first call on a handle pays one info round trip.
Every park armed a threading.Timer, a real OS thread, which measured at 42.6 µs, over half the cost of a kept-connection call, and left eight sleeping threads per handle; the pool now records a deadline per parked connection and one self-rearming sweeper closes them as they expire, so park and take touch no thread at all. run() sends stdin_close inline when there is no stdin instead of spawning the pump thread for one frame. The daemon's proto lives on the Sandbox, a zero keep_alive window skips the probe, _park_after and _lease fold into one context manager, _Parked goes, and logs' trailing done is a parameter rather than a string compare. The tests share sandbox_at, accept_upgrade and wait_until through conftest and pin the old one-shot behaviour with keep_alive=0 instead of a private field.
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.
Fourth step of #195: the Python SDK side of a persistent data-plane connection, the same shape as #198 for Go. Needs #196 in the guest image to take effect; against an older silkd it behaves exactly as before.
What changes
Sandboxhandle keeps its relay connection. After a call the connection is parked in aConnPool(up to 8 per handle, 30 s idle;Client(..., keep_alive=...)tunes the window, 0 dials per call) and the next call sends its request on it. Every RPC helper runs under_park_after: a terminal frame, error frames included, parks the connection; a dropped connection, a timeout cut or a local error drops it.closeandhibernatedrain the pool first.watch,open_pty,dial_portand an LSP session own their connection as today.runjoins its stdin thread before the connection goes back, so the pump's frames never land on the next RPC (silkd drains what an exited command left unread).select: a peer that hung up (hibernate, release, the idle sweep) or spoke unprompted fails it and the call redials, which wakes the guest exactly as before. It works through TLS sockets too, since it never consumes bytes.inforound trip. A daemon before proto 2 closes after answering, so the handle redials and dials per call from then on. One extra round trip once per handle.logswritesexitthendone; the trailingdoneis now consumed so the connection stays in frame.docs/sdk-python.mddocuments the window and its interplay withidle_hibernate_seconds.Hot path
A pooled call: one lock pair and one
select(2), replacing a TCP dial, an HTTP upgrade, a TLS handshake behind an edge and a vsock connect on the node. The first call on a handle pays oneinforound trip. Frames on the wire are unchanged.Evidence
Run on Python 3.9.6, the declared floor. The OpenAI adapter suite could not be collected locally (no
agentspackage in this environment); CI runs it on 3.12.New tests in
tests/test_keepalive.pyagainst an in-process fake agent that answersinfo,fs_statandexec: four calls share one upgrade; an old daemon costs the probe dial plus one per call;keep_alive=0dials per call; an idle connection closes after the window; a peer that hung up is noticed before reuse;closedrains the parked connection. The fake half-closes and drains like the relay does, since a plain close with an unreadstdin_closeresets the reply. Scripted one-shot fakes in the existing tests run with the proto pinned to 1, the fallback path; thelogsscript now endsexit,doneas silkd does.