Class: Wurk::RedisPool
- Inherits:
-
Object
- Object
- Wurk::RedisPool
- Defined in:
- lib/wurk/redis_pool.rb
Overview
Per-process pool over redis-client + connection_pool. Never share a socket across forks: the parent closes the pool before fork, each child opens a fresh one (see docs/idea/03-process-model.md, steps 3 and 5).
#with absorbs transient Redis failures (production incident #101):
* READONLY / NOREPLICAS / UNBLOCKED — a failover happened; close and retry
once immediately so redis-client redials the new primary (spec §26).
* ConnectionError (incl. CannotConnect / Read- / WriteTimeout) — a blip;
close and retry with exponential backoff up to CONN_MAX_ATTEMPTS, then
raise. At-least-once tolerates the rare duplicate and JobRetry re-runs
the job anyway, so retrying at the pool layer is strictly better.
* ConnectionPool::TimeoutError — checkout starved; retry once after a
short jittered pause, then raise (sizing is the fix, not queuing).
Every retry and final give-up is reported through the injected on_error
telemetry hook (Wurk::Configuration#on_redis_error).
Constant Summary collapse
- DEFAULT_URL =
ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
- DEFAULT_NAME =
'default'- DEFAULT_POOL_TIMEOUT =
ConnectionPool checkout wait — how long #with blocks for a free slot.
1.0- DEFAULT_CONNECT_TIMEOUT =
Socket-level timeouts handed to RedisClient, split apart from the checkout wait above. read/write are deliberately wider than connect so a briefly- slow-but-alive Redis (RDB fork pause, a large BLMOVE payload) doesn't spuriously ReadTimeout — the production incident (#101) the single dual-use timeout caused. reconnect_attempts re-dials a dropped socket once.
1.0- DEFAULT_READ_TIMEOUT =
2.5- DEFAULT_WRITE_TIMEOUT =
2.5- DEFAULT_RECONNECT_ATTEMPTS =
1- RETRYABLE_MSG =
Server-side messages where the connection is closed and the block retried exactly once. READONLY is itself a RedisClient::ConnectionError subclass, so this message match must be tested BEFORE the generic ConnectionError backoff below (otherwise a failover would sleep instead of redialing).
/\A(READONLY|NOREPLICAS|UNBLOCKED)/- CONN_MAX_ATTEMPTS =
ConnectionError backoff: CONN_MAX_ATTEMPTS total tries, sleeping (BASE * 2**attempt) + rand*JITTER before each retry. The 1.0s + 2.0s pair rides out a sub-4s blip; the jitter de-syncs a fleet reconnecting at once.
3- CONN_BACKOFF_BASE =
0.5- CONN_BACKOFF_JITTER =
0.25- POOL_RETRY_MIN =
Checkout-timeout retry: one retry after a jittered pause in [POOL_RETRY_MIN, POOL_RETRY_MIN + POOL_RETRY_SPREAD). No loop — sustained checkout starvation is a sizing bug, not something to queue behind.
0.1- POOL_RETRY_SPREAD =
0.2
Instance Attribute Summary collapse
-
#client_config ⇒ Object
readonly
Returns the value of attribute client_config.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#pool_timeout ⇒ Object
readonly
Returns the value of attribute pool_timeout.
-
#size ⇒ Object
readonly
Returns the value of attribute size.
-
#url ⇒ Object
readonly
Returns the value of attribute url.
Instance Method Summary collapse
-
#available ⇒ Object
Free (unchecked-out) slots right now.
- #disconnect! ⇒ Object
-
#info ⇒ Object
Redis INFO parsed to a Hash, with this pool's own
size/availableslot counts merged in — one call gives a heartbeat both Redis health and local pool saturation. -
#initialize(size:, name: DEFAULT_NAME, on_error: nil, **options) ⇒ RedisPool
constructor
Takes the standard Sidekiq
config.redishash:pool_timeouttunes the ConnectionPool checkout;connect_timeout/read_timeout/write_timeout/reconnect_attemptsplus any other key (driver, ssl_params, …) forward verbatim to RedisClient.config. -
#with(&block) ⇒ Object
Checkout a connection and run the block.
Constructor Details
#initialize(size:, name: DEFAULT_NAME, on_error: nil, **options) ⇒ RedisPool
Takes the standard Sidekiq config.redis hash: pool_timeout tunes the
ConnectionPool checkout; connect_timeout/read_timeout/write_timeout/
reconnect_attempts plus any other key (driver, ssl_params, …) forward
verbatim to RedisClient.config. on_error is an optional callable fired
per retry / final give-up with { error:, attempt:, retried:, pool: }.
66 67 68 69 70 71 72 73 74 |
# File 'lib/wurk/redis_pool.rb', line 66 def initialize(size:, name: DEFAULT_NAME, on_error: nil, **) @size = size @name = name @on_error = on_error @pool_timeout = .fetch(:pool_timeout, DEFAULT_POOL_TIMEOUT) @client_config = build_client_config() @url = @client_config[:url] @pool = ConnectionPool.new(size: size, timeout: @pool_timeout) { build_client } end |
Instance Attribute Details
#client_config ⇒ Object (readonly)
Returns the value of attribute client_config.
59 60 61 |
# File 'lib/wurk/redis_pool.rb', line 59 def client_config @client_config end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
59 60 61 |
# File 'lib/wurk/redis_pool.rb', line 59 def name @name end |
#pool_timeout ⇒ Object (readonly)
Returns the value of attribute pool_timeout.
59 60 61 |
# File 'lib/wurk/redis_pool.rb', line 59 def pool_timeout @pool_timeout end |
#size ⇒ Object (readonly)
Returns the value of attribute size.
59 60 61 |
# File 'lib/wurk/redis_pool.rb', line 59 def size @size end |
#url ⇒ Object (readonly)
Returns the value of attribute url.
59 60 61 |
# File 'lib/wurk/redis_pool.rb', line 59 def url @url end |
Instance Method Details
#available ⇒ Object
Free (unchecked-out) slots right now. Local and cheap — no Redis round trip — so a monitor can poll it without perturbing the pool.
109 110 111 |
# File 'lib/wurk/redis_pool.rb', line 109 def available @pool.available end |
#disconnect! ⇒ Object
95 96 97 |
# File 'lib/wurk/redis_pool.rb', line 95 def disconnect! @pool.shutdown { |conn| safe_close(conn) } end |
#info ⇒ Object
Redis INFO parsed to a Hash, with this pool's own size / available
slot counts merged in — one call gives a heartbeat both Redis health and
local pool saturation. (Real Redis INFO has no size/available field.)
102 103 104 105 |
# File 'lib/wurk/redis_pool.rb', line 102 def info with { |conn| parse_info(conn.call('INFO')) } .merge('size' => @size, 'available' => available) end |
#with(&block) ⇒ Object
Checkout a connection and run the block. ConnectionPool::TimeoutError is raised by @pool.with before the block runs, so it is caught out here (the in-block #run rescue never sees it) — one retry, then raise.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/wurk/redis_pool.rb', line 79 def with(&block) checkout_retried = false begin @pool.with { |conn| run(conn, &block) } rescue ConnectionPool::TimeoutError => e if checkout_retried notify_error(e, attempt: 2, retried: false) raise end checkout_retried = true notify_error(e, attempt: 1, retried: true) sleep(checkout_delay) retry end end |