Class: HttpConnectionPool::Registry
- Inherits:
-
Object
- Object
- HttpConnectionPool::Registry
- 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
-
#max_pools ⇒ Object
readonly
Returns the value of attribute max_pools.
Class Method Summary collapse
-
.configure(max_pools:) ⇒ Object
Configure the process-wide singleton’s pool ceiling.
-
.instance ⇒ Registry
The process-wide singleton instance.
-
.reset! ⇒ Object
Replace the singleton — primarily for testing.
Instance Method Summary collapse
-
#close_all ⇒ Object
Close every pool and clear the registry.
-
#initialize(max_pools: nil) ⇒ Registry
constructor
A new instance of Registry.
-
#inspect ⇒ Object
(also: #to_s)
Safe inspect — shows pool count and cap without dumping internal keys or any pool state that might reference credential material.
-
#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.
-
#release(url, **options) ⇒ Object
Remove and close the pool that exactly matches the given URL + options.
-
#stats ⇒ Array<Hash>
Snapshot of stats for every registered pool.
-
#sweep_closed! ⇒ Object
Evict every pool that has already been closed out-of-band (e.g. via Pool#close rather than Registry#release).
Constructor Details
#initialize(max_pools: nil) ⇒ Registry
Returns a new instance of Registry.
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_pools ⇒ Object (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.
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 |
.instance ⇒ Registry
Returns 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_all ⇒ Object
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 |
#inspect ⇒ Object 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.
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, **) origin = extract_origin(url) key = pool_key(origin, ) 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, **) 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.
121 122 123 124 125 |
# File 'lib/http_connection_pool/registry.rb', line 121 def release(url, **) key = pool_key(extract_origin(url), ) pool = @pools.delete(key) pool&.close end |
#stats ⇒ Array<Hash>
Returns 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 |