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-persistent default (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

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.

Parameters:

  • http (Net::HTTP::Persistent)


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.

Parameters:

  • key (Array)

    connection shape and base URL.

Yield Returns:

  • (Faraday::Connection)

    a freshly built connection.

Returns:

  • (Faraday::Connection)

    the shared connection for key.



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_timeoutNumeric?

Socket-open deadline for a newly built connection.

Returns:

  • (Numeric, nil)

    seconds, or nil for no deadline.



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.

Parameters:

  • pem (String)

    the PEM-encoded key.

Returns:

  • (OpenSSL::PKey::PKey)


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_sizeInteger

Pool size for a newly built adapter.

Returns:

  • (Integer)


199
200
201
# File 'lib/atlas_rb/transport.rb', line 199

def pool_size
  AtlasRb.config.connection_pool_size || DEFAULT_POOL_SIZE
end

.read_retriesInteger

Extra attempts an idempotent read gets after a transport failure.

Returns:

  • (Integer)


228
229
230
# File 'lib/atlas_rb/transport.rb', line 228

def read_retries
  AtlasRb.config.read_retries || DEFAULT_READ_RETRIES
end

.read_timeoutNumeric?

Response deadline for a newly built JSON or system connection.

Returns:

  • (Numeric, nil)

    seconds, or nil for no deadline.



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_timeoutNumeric?

Response deadline for a newly built multipart connection. Uncapped by default — a binary upload legitimately outlives any page-sized budget.

Returns:

  • (Numeric, nil)

    seconds, or nil for no deadline.



221
222
223
# File 'lib/atlas_rb/transport.rb', line 221

def upload_read_timeout
  AtlasRb.config.upload_read_timeout
end