Class: HttpConnectionPool::Registry

Inherits:
Object
  • Object
show all
Defined in:
lib/http_connection_pool/registry.rb

Overview

Global, thread-safe registry that holds one Pool per (origin, options) pair.

Pools are keyed by a SHA-256 digest of the canonical origin + options, so two callers targeting the same host with different credentials each get their own isolated pool — no credential confusion, no error. This makes subclassing safe: a subclass that overrides pool_options gets a distinct pool from its parent even when both share the same base_url.

The registry itself is a singleton (one instance per process). It is not implemented with the ‘Singleton` module so it can be replaced in tests. Storage is backed by `Concurrent::Map`, and the singleton slot by `Concurrent::AtomicReference`, so reads are lock-free under contention.

Usage:

registry = HttpConnectionPool::Registry.instance
registry.pool_for('https://api.example.com').with { |conn| conn.get('/status') }

Constant Summary collapse

PoolLimitError =

Backward-compatible alias — canonical class lives in errors.rb.

HttpConnectionPool::PoolLimitError
SUPPORTED_SCHEMES =
%w[http https].freeze
KEYABLE_SCALARS =

Option values that can be canonically serialized into a pool key. Anything else (an SSLContext, a PKey, a proc, an arbitrary object) is rejected by ensure_keyable! rather than risking a silent inspect-based collision. FUTURE (case C): a canonical serializer would let us key these safely —e.g. restore ssl_context: by digesting its real security material — and remove this rejection. See docs/superpowers/specs/2026-06-25-error-handling-design.md.

[String, Symbol, Integer, Float, TrueClass, FalseClass, NilClass].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_pools: nil) ⇒ Registry

Returns a new instance of Registry.

Parameters:

  • max_pools (Integer, nil) (defaults to: nil)

    Optional ceiling on the number of distinct origins held at once. nil (the default) means unlimited. Set this when origins can be influenced by untrusted input (webhook targets, redirect hosts, user-supplied URLs) to bound memory and file-descriptor use — without it, each new origin retains a pool and its sockets indefinitely.

Raises:

  • (ArgumentError)


78
79
80
81
82
83
# File 'lib/http_connection_pool/registry.rb', line 78

def initialize(max_pools: nil)
  @pools     = Concurrent::Map.new
  @max_pools = max_pools && Integer(max_pools)

  raise ArgumentError, 'max_pools must be >= 1' if @max_pools && @max_pools < 1
end

Instance Attribute Details

#max_poolsObject (readonly)

Returns the value of attribute max_pools.



85
86
87
# File 'lib/http_connection_pool/registry.rb', line 85

def max_pools
  @max_pools
end

Class Method Details

.configure(max_pools:) ⇒ Object

Configure the process-wide singleton’s pool ceiling. Must be called before the singleton is first used (e.g. in a Rails initializer); raises if the singleton already exists, since max_pools is fixed at construction.

Parameters:

  • max_pools (Integer, nil)


59
60
61
62
63
# File 'lib/http_connection_pool/registry.rb', line 59

def self.configure(max_pools:)
  raise 'Registry singleton already initialised; call configure earlier' if @instance_ref.get

  @configured_max_pools = max_pools
end

.instanceRegistry

Returns the process-wide singleton instance.

Returns:

  • (Registry)

    the process-wide singleton instance



46
47
48
49
50
51
52
# File 'lib/http_connection_pool/registry.rb', line 46

def self.instance
  existing = @instance_ref.get
  return existing if existing

  candidate = new(max_pools: @configured_max_pools)
  @instance_ref.compare_and_set(nil, candidate) ? candidate : @instance_ref.get
end

.reset!Object

Replace the singleton — primarily for testing.



66
67
68
69
70
# File 'lib/http_connection_pool/registry.rb', line 66

def self.reset!
  previous = @instance_ref.get_and_set(nil)
  @configured_max_pools = nil
  previous&.close_all
end

Instance Method Details

#close_allObject

Close every pool and clear the registry.



128
129
130
131
132
133
# File 'lib/http_connection_pool/registry.rb', line 128

def close_all
  @pools.each_pair do |key, pool|
    @pools.delete_pair(key, pool)
    pool.close
  end
end

#inspectObject Also known as: to_s

Safe inspect — shows pool count and cap without dumping internal keys or any pool state that might reference credential material.



158
159
160
161
# File 'lib/http_connection_pool/registry.rb', line 158

def inspect
  limit = @max_pools ? @max_pools.to_s : 'unlimited'
  "#<#{self.class.name} pools=#{@pools.size} max_pools=#{limit}>"
end

#pool_for(url, size: Pool::DEFAULT_SIZE, timeout: Pool::DEFAULT_TIMEOUT, **options) ⇒ Pool

Return (or lazily create) a Pool for the given URL’s origin + options.

Each unique (origin, options) combination gets its own isolated pool, so two callers sharing a host but using different credentials (Authorization headers, auth tokens, etc.) never share connections.

Parameters:

  • url (String)

    any URL whose scheme+host+port will be used as the key

  • size (Integer) (defaults to: Pool::DEFAULT_SIZE)

    pool size (ignored if an identical pool already exists)

  • timeout (Float) (defaults to: Pool::DEFAULT_TIMEOUT)

    checkout timeout in seconds (ignored if pool already exists)

  • options (Hash)

    HTTP client options forwarded to Pool (headers, auth, ssl, etc.)

Returns:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/http_connection_pool/registry.rb', line 98

def pool_for(url, size: Pool::DEFAULT_SIZE, timeout: Pool::DEFAULT_TIMEOUT, **options)
  origin = extract_origin(url)
  key    = pool_key(origin, options)

  loop do
    existing = @pools[key]
    return existing if existing && !existing.closed?

    # Only new keys count against the cap; reusing/replacing an existing
    # key is always allowed.
    ensure_within_limit!(key)

    candidate = Pool.new(origin: origin, size: size, timeout: timeout, **options)
    resolved  = insert_or_resolve(key, candidate)
    return resolved if resolved
  end
end

#release(url, **options) ⇒ Object

Remove and close the pool that exactly matches the given URL + options. Without options it closes the no-options pool for the origin.

Parameters:

  • url (String)
  • options (Hash)


121
122
123
124
125
# File 'lib/http_connection_pool/registry.rb', line 121

def release(url, **options)
  key  = pool_key(extract_origin(url), options)
  pool = @pools.delete(key)
  pool&.close
end

#statsArray<Hash>

Returns snapshot of stats for every registered pool.

Returns:

  • (Array<Hash>)

    snapshot of stats for every registered pool



150
151
152
153
154
# File 'lib/http_connection_pool/registry.rb', line 150

def stats
  result = []
  @pools.each_pair { |_key, pool| result << pool.stats }
  result
end

#sweep_closed!Object

Evict every pool that has already been closed out-of-band (e.g. via Pool#close rather than Registry#release). Dead pools are otherwise only reclaimed when their exact key is requested again, so a long-running process that closes pools directly should call this periodically to free the retained Pool objects (and their option material) and the cap slots they would otherwise hold. Returns the number of pools swept.



141
142
143
144
145
146
147
# File 'lib/http_connection_pool/registry.rb', line 141

def sweep_closed!
  swept = 0
  @pools.each_pair do |key, pool|
    swept += 1 if pool.closed? && @pools.delete_pair(key, pool)
  end
  swept
end