Module: AtlasRb::Transport
- Defined in:
- lib/atlas_rb/transport.rb
Overview
Process-wide cache of Faraday connections, so Atlas calls reuse sockets instead of paying a TCP (and, under TLS, a full TLS) handshake per request.
Why a cache is needed at all
Keep-alive is what saves the handshake, and keep-alive needs two things
that a per-request Faraday.new cannot give it. The pool lives on the
adapter instance, and Faraday builds a new adapter for every connection —
so a connection built per request gets a fresh, empty pool and reuses
nothing. The socket has to outlive the request, which means the connection
holding it has to as well.
The cache is keyed on connection shape (:json, :multipart,
:system) and base URL, because those are the only things that vary at
build time now — per-request state (the signed assertion, the
Idempotency-Key, the query params) rides on Proxy instead.
Why not a connection per thread
A thread-local connection is the obvious cheap version and it is wrong for
this consumer: Cerberus fans out its page reads on short-lived threads, so
a thread-local pool dies with the thread that built it and the fan-out —
the calls that most want reuse — reuses nothing. net-http-persistent
keeps a shared ConnectionPool and uses Thread.current only to track
re-entrant checkouts, so a socket checked in by a dying thread goes back to
the shared stack.
Thread safety
A Faraday connection is safe for concurrent requests as long as nothing mutates it after it is built. Everything here respects that: the cached connection is built once under MUTEX (with its middleware stack forced so two threads cannot race to build two adapters, and therefore two pools), and per-request headers and params are set on the request, never assigned onto the shared connection.
Defined Under Namespace
Classes: Proxy
Constant Summary collapse
- DEFAULT_POOL_SIZE =
Sockets kept per host. Sized for the consumer's shape rather than left at the
net-http-persistentdefault (256, or a quarter of the open-file limit): Cerberus draws up to four concurrent reads per Puma thread, so the useful number is that fan-out times the thread count, plus headroom. 16
Class Method Summary collapse
-
.configure_persistent(http) ⇒ void
Apply the pool settings a host has configured to a
Net::HTTP::Persistent. -
.connection_for(key) ⇒ Faraday::Connection
Fetch the cached connection for
key, building it from the block on first use. -
.parsed_key(pem) ⇒ OpenSSL::PKey::PKey
Parse a PEM into an
OpenSSL::PKey, reusing the last parse. -
.pool_size ⇒ Integer
Pool size for a newly built adapter.
-
.reset_connections! ⇒ void
Close every pooled socket and drop the cached connections.
Class Method Details
.configure_persistent(http) ⇒ void
This method returns an undefined value.
Apply the pool settings a host has configured to a
Net::HTTP::Persistent. Called by the adapter's config block on every
request, so it must stay assignment-only and cheap.
max_retries is restored to 1 here because the adapter zeroes it, and
zero is the wrong default for a pooled socket: a server that closed an
idle connection produces an error on the next write, and one retry is
what turns that into a reconnect instead of a caller-visible failure.
Net::HTTP gates its retry on IDEMPOTENT_METHODS_, so the retry-safe
creates (POST /works, /file_sets, /files) are never replayed.
159 160 161 162 |
# File 'lib/atlas_rb/transport.rb', line 159 def configure_persistent(http) http.max_retries = 1 http.max_requests = AtlasRb.config.connection_max_requests end |
.connection_for(key) ⇒ Faraday::Connection
Fetch the cached connection for key, building it from the block on
first use.
132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/atlas_rb/transport.rb', line 132 def connection_for(key) MUTEX.synchronize do connections[key] ||= begin conn = yield # Force the middleware stack (and with it the adapter, and with it # the pool) while still holding the lock. Faraday memoizes the app # lazily on first request, and two threads racing that memoization # would each build an adapter — two pools, no reuse. conn.builder.app conn end end end |
.parsed_key(pem) ⇒ OpenSSL::PKey::PKey
Parse a PEM into an OpenSSL::PKey, reusing the last parse.
Parsing the key costs roughly five times the ES256 signature it exists to produce, and the signing key is configured as a callable returning a PEM, so it was being reparsed on every request. One entry is enough — the cache is a rotation check, not a store, so a rotated key parses once and the old one is dropped rather than accumulating.
181 182 183 184 185 186 187 188 189 |
# File 'lib/atlas_rb/transport.rb', line 181 def parsed_key(pem) KEY_MUTEX.synchronize do if @parsed_pem != pem @parsed_pem = pem @parsed_key = OpenSSL::PKey.read(pem) end @parsed_key end end |
.pool_size ⇒ Integer
Pool size for a newly built adapter.
167 168 169 |
# File 'lib/atlas_rb/transport.rb', line 167 def pool_size AtlasRb.config.connection_pool_size || DEFAULT_POOL_SIZE end |
.reset_connections! ⇒ void
This method returns an undefined value.
Close every pooled socket and drop the cached connections.
Pooled sockets outliving a test example are a new source of
cross-example coupling, so a suite that boots and tears down a real
server (Atlas's :atlas_rb_server layer) should call this in its
teardown. Shutting a net-http-persistent pool down makes it refuse
later checkouts, which is why the cached connections are dropped in the
same breath — the next call rebuilds both.
201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/atlas_rb/transport.rb', line 201 def reset_connections! MUTEX.synchronize do connections.each_value do |conn| conn.close rescue StandardError # A pool already shut down, or a socket already gone, has nothing # left to release. The connection is being discarded either way. nil end connections.clear end end |