feat(keycardai-oauth): reuse Net::HTTP sessions in the default transport - #35
Conversation
NetHTTPClient keeps Net::HTTP sessions open and reuses them per instance, per thread, per (host, port, scheme). The request path takes no lock; a small mutex guards only the registry of per-thread session maps, which is swept of dead threads whenever a new thread registers or close runs. Timeouts are now assigned per request on the cached session and fall back to Net::HTTP's defaults when a request carries none. NetHTTPClient#close finishes every session and clears the registry; the client stays usable afterwards. TLS selection and the NetworkError rescue list are unchanged. Behavior note: a keepalive connection the server dropped while idle now surfaces as NetworkError on the first attempt after idle, where a fresh-connection design could not fail that way. There is no transparent retry; the Ruby retryability classification (ECO-360) is the consumer-side answer when it lands. Throwaway clients built by module-level function defaults keep their sessions until garbage collection. Co-Authored-By: Larry Osakwe <larry@keycard.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Larry-Osakwe
left a comment
There was a problem hiding this comment.
The design landed as decided: per-instance, per-thread, per-(host, port, scheme) sessions with the mutex confined to registry registration and close, dead-thread sweep pinned by a spec, and no transparent retry. The timeout handling corrects my prompt: restoring nil would have disabled timeouts, so capturing and restoring Net::HTTP's own defaults (60s) is what actually reproduces the old fresh-session behavior. Session self-healing after a dead keepalive rides stdlib begin_transport reconnect, verified against net/http's error path.
Summary
Closes ECO-383 (Ruby twin of python-sdk #291).
NetHTTPClient#performbuiltNet::HTTP.new(host, port)per request and never kept the session, so every verify and token exchange through the mcp gem's long-livedNetHTTPClientpaid a fresh TCP and TLS handshake. Sessions are now pooled per instance, per thread, per(host, port, scheme):The mutex is taken only when a thread registers for the first time or on
close, and each time it is taken the registry is swept of dead threads (their sessions are finished). No transparent retry: a keepalive the server dropped while idle raisesNetworkErrorexactly as any other failure, matching the Python decision; the commit body carries the changelog note about first-attempt-after-idle failures and points at ECO-360 for consumer-side retryability.NetHTTPClient#closefinishes every registered session and clears the registry; the client stays usable and the next request opens fresh sessions. Documented as "call when no requests are in flight". The 16 module-level function defaults that build a throwawayNetHTTPClientkeep that shape; their sessions now live until GC, stated in the class docs.One deliberate deviation from the prompt, flagged: a request with no
timeout:resets the cached session toNet::HTTP's construction defaults (60s open/read, captured once at load asDEFAULT_OPEN_TIMEOUT/DEFAULT_READ_TIMEOUT) rather than to literalnil.nilonNet::HTTPmeans no timeout at all, which would have silently turned a timeout-less request into an unbounded one; today's fresh-session behavior is the 60s default, and that is what is preserved. The spec pins the reset value.No new dependencies; stdlib only. No mcp or a2a changes; no version or changelog hand-edits.
Specs (
oauth/spec/keycardai/oauth/http_spec.rb,Net::HTTP.newstubbed with a fake session, no network): same-thread same-host reuse, distinct sessions per host/port/scheme, distinct sessions per thread, per-request timeout applied then reset,closefinishes and the next request opens a fresh session, dead-thread sweep, and the unchangedNetworkErrormessage onEOFError.rake spec(oauth 161, mcp 30, a2a 16) andrake rubocopclean on Ruby 3.4.Link to Devin session: https://app.devin.ai/sessions/bbc062cf32b548ef9c4b8b444d2ec67f
Open in Devin Desktop: https://app.devin.ai/desktop/session/bbc062cf32b548ef9c4b8b444d2ec67f?variant=devin
Requested by: @Larry-Osakwe