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.
One consequence is worth knowing: the pool size, the timeouts and the
retry count are read when a connection is first built, so a host must set
them before its first Atlas call. Changing them later takes a
Transport.reset_connections! to bite. A single call that needs a different
deadline sets it on the request instead — the block Proxy forwards runs
last, so req.options.timeout there wins over the connection's.
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- DEFAULT_OPEN_TIMEOUT =
Seconds to wait for a socket to open. Atlas is a container on the same host in development and a service on the same overlay network in staging, so a healthy connect is sub-millisecond and two seconds is already generous. A refused connect fails immediately either way; this bounds the connect that hangs.
2- DEFAULT_READ_TIMEOUT =
Seconds to wait for a response. Sized off the measured read path — the slowest single call on a Cerberus Work page was ~200ms and the whole eight-call page ~450ms — so this is roughly fifty times the observed per-call cost. That leaves room for a cold cache or a slow Solr commit while still failing inside a user's patience rather than parking the request thread for minutes.
10- DEFAULT_READ_RETRIES =
Extra attempts an idempotent read gets after a transport failure, so three attempts in all. Three is the ceiling the SRE guidance settles on and the retry middleware's own default: enough to hide a restart, few enough that a struggling Atlas is not handed 3x the load it is already failing to serve.
2
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. -
.open_timeout ⇒ Numeric?
Socket-open deadline for a newly built connection.
-
.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.
-
.read_retries ⇒ Integer
Extra attempts an idempotent read gets after a transport failure.
-
.read_timeout ⇒ Numeric?
Response deadline for a newly built JSON or system connection.
-
.reset_connections! ⇒ void
Close every pooled socket and drop the cached connections.
-
.upload_read_timeout ⇒ Numeric?
Response deadline for a newly built multipart connection.
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 pinned to 0 so the retry policy lives in exactly one
layer. Net::HTTP replays an idempotent request itself on a read
timeout, which doubles every timeout wait, and it does so with no
backoff, no jitter and no instrumentation. The stale-pooled-socket case
that wants a replay — a server that closed an idle connection, seen as
EOFError / ECONNRESET / Faraday::ConnectionFailed on the next
write — is covered by the retry middleware
(FaradayHelper#retry_reads), which is visible and bounded.
Stacking both multiplies the attempts.
191 192 193 194 |
# File 'lib/atlas_rb/transport.rb', line 191 def configure_persistent(http) http.max_retries = 0 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.
161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/atlas_rb/transport.rb', line 161 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 |
.open_timeout ⇒ Numeric?
Socket-open deadline for a newly built connection.
206 207 208 |
# File 'lib/atlas_rb/transport.rb', line 206 def open_timeout resolve_timeout(AtlasRb.config.open_timeout, DEFAULT_OPEN_TIMEOUT) 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.
242 243 244 245 246 247 248 249 250 |
# File 'lib/atlas_rb/transport.rb', line 242 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.
199 200 201 |
# File 'lib/atlas_rb/transport.rb', line 199 def pool_size AtlasRb.config.connection_pool_size || DEFAULT_POOL_SIZE end |
.read_retries ⇒ Integer
Extra attempts an idempotent read gets after a transport failure.
228 229 230 |
# File 'lib/atlas_rb/transport.rb', line 228 def read_retries AtlasRb.config.read_retries || DEFAULT_READ_RETRIES end |
.read_timeout ⇒ Numeric?
Response deadline for a newly built JSON or system connection.
213 214 215 |
# File 'lib/atlas_rb/transport.rb', line 213 def read_timeout resolve_timeout(AtlasRb.config.read_timeout, DEFAULT_READ_TIMEOUT) 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.
262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/atlas_rb/transport.rb', line 262 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 |
.upload_read_timeout ⇒ Numeric?
Response deadline for a newly built multipart connection. Uncapped by default — a binary upload legitimately outlives any page-sized budget.
221 222 223 |
# File 'lib/atlas_rb/transport.rb', line 221 def upload_read_timeout AtlasRb.config.upload_read_timeout end |