Opt-in HTTP keep-alive via keep_alive_connections#1
Closed
Conversation
lloydwatkin
approved these changes
Apr 27, 2026
Member
|
This is being contributed back to typesense? |
Currently `Typesense::ApiCall#perform_request` builds a fresh `Faraday.new(...)` (and therefore a new TCP and TLS handshake) on every request. On hot endpoints this can dominate the Typesense round-trip latency. This adds an opt-in `keep_alive_connections` configuration option (default `false`, so existing users see no behaviour change). When enabled: * Faraday connections are cached per `(thread, node)` rather than constructed per request. Net::HTTP is not thread-safe, so per-thread caching keeps concurrent callers isolated while still respecting the existing node round-robin. * Connections use the `:net_http_persistent` Faraday adapter with a 30s idle timeout, so reused sockets are dropped before most load balancers cull them. * On any rescued network error, the cached connection is dropped before the gem retries, so a half-closed keep-alive socket cannot fail the retry as well. Pair with `num_retries >= 1` for transparent recovery from server- or load-balancer-side idle timeouts. The `:net_http_persistent` adapter and its `net-http-persistent` runtime dependency are listed in the gemspec, and `require 'faraday/net_http_persistent'` is gated on the option being enabled, so loading the gem with the option off does not import the new dependency at runtime. New RSpec coverage: * connection reuse on the same thread * per-node cache keying * per-thread cache isolation * per-instance cache isolation * eviction on network error * timeouts propagate to the cached connection * the option defaults to false and the legacy per-request connection path is preserved
eafb23d to
86908f7
Compare
Member
Author
|
Replaced by upstream typesense#55. |
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.
Summary
keep_alive_connectionsconfig flag (defaultfalse).Typesense::ApiCallreuses Faraday connections per(thread, node)via the:net_http_persistentadapter instead of building a freshFaraday.newon every request.Motivation
Sentry profiling of a hot endpoint in our API showed that ~140 ms of a ~150 ms request was spent in the Typesense round-trip — most of it in
OpenSSL::X509::Store#set_default_pathsand the TLS handshake, repeated on every search call. The root cause isApiCall#perform_requestbuilding a brand-newFaraday.new(...)(and therefore a new TCP + TLS handshake) on every call. Aggregate metrics confirm ~50% of this endpoint's wall time is I/O.Design notes
Faraday.newpath is preserved whenkeep_alive_connectionsisfalse. Existing users (and our own production deploys) see zero behaviour change until they opt in.Net::HTTPis not thread-safe. Caching perThread.currentmeans each Puma/Sidekiq worker thread maintains its own keep-alive socket per node, with no cross-thread sharing.ApiCall#object_id, so multipleTypesense::Clientinstances in the same process do not share sockets.protocol://host:port, so the existing healthcheck/round-robin logic still selects nodes the same way.Faraday::ConnectionFailed/Errno::ECONNRESETetc. — the gem already rescues these inside the retry loop, and we now also evict the cached connection so the retry opens a fresh socket. Pair withnum_retries >= 1for transparent recovery.Usage
Test plan
bundle exec rspec spec/typesense/api_call_spec.rb— 50 examples, 0 failures (was 42; +8 new).bundle exec rspec(full suite) — 159 examples, 1 failure, 27 pending. The single failure iscollections_spec.rb:156(truncate_lenschema mismatch with the running Typesense container) — pre-existing onmaster, verified before this branch, unrelated.Faraday::ConnectionFailed/HTTPStatus0rate.New spec coverage
false, no thread-local cache populated.