sidekiq-ratomic-pool
Sidekiq server middleware leveraging Ratomic::LocalPool for Ractor-local resource ownership. It includes automated connection health validation, exponential backoff retries, and an integrated Circuit Breaker to help prevent cascading resource failures.
Installation
Add to your Gemfile:
gem "sidekiq-ratomic-pool"
Sidekiq dependency
Despite its name, this gem does not declare sidekiq as a runtime dependency.
It provides a Sidekiq::Ratomic::Pool middleware implementation using the
standard call(job, payload, queue) { ... } middleware contract. Sidekiq is the
primary supported integration and the reason for the gem name, but other
Sidekiq-compatible job frameworks can use it if they support the same contract
and worker pool accessor pattern.
Features
- Ractor-Local Isolation: A pool runtime used inside a Ractor lazily owns
its resources through
Ratomic::LocalPool; threads within that Ractor share its Ractor-local pool. - Circuit Breaker Pattern: Trips open after a configurable threshold of checkout, health-check, or configured retryable I/O failures.
- Exponential Backoff: Applies increasing retry delays to transient checkout and retryable resource-operation failures.
- Automated Health Probes: Validates resources with
ping,active?, or a caller-supplied validator before use. - Configurable Failure Policy: Non-retryable worker exceptions propagate without changing circuit state, avoiding accidental duplicate work.
- Ratomic-Native Failure Accounting: Tracks circuit-breaker failures with the Ractor-shareable
Ratomic::Counterprimitive instead of addingconcurrent-ruby.
Usage
Factories are made Ractor-shareable because Ratomic::LocalPool creates resources
lazily inside each Ractor. A small frozen factory object is suitable for production use:
RedisFactory = Data.define(:url) do
def call
RedisClient.config(url:).new_client
end
end
Sidekiq.configure_server do |config|
config.server_middleware do |chain|
chain.add Sidekiq::Ratomic::Pool,
pool_name: :redis_pool, # Name of the worker accessor populated by the middleware
size: 10, # Maximum number of resources owned by each Ractor-local pool.
pool_timeout: 1, # Maximum time to wait for a resource checkout.
max_retries: 3, # Maximum number of retries for checkout and configured retryable failures.
retry_delay: 0.2, # Base delay in secs used for exponential retry backoff
cb_threshold: 5, # Number of recorded failures required to open the circuit
cb_timeout: 30, # Time an open circuit remains open before a half-open probe
factory: RedisFactory.new(ENV.fetch('REDIS_URL')).freeze
end
end
The middleware injects the pool into a worker accessor matching pool_name.
For example, a Redis-backed worker can use redis_pool.with inside perform:
class RedisWorker
include Sidekiq::Job
attr_accessor :redis_pool
def perform(key, value)
redis_pool.with do |redis|
redis.call('SET', key, value)
end
end
end
Resource checkout/health failures and configured retryable I/O errors use exponential backoff. Other exceptions raised by the worker block propagate without being retried, preventing accidental duplication of non-idempotent work.
Host-owned Ractor scheduling
This gem provides Ractor-safe, Ractor-local resource ownership; it does not create Ractors or dispatch Sidekiq jobs into them. The host framework or application owns Ractor creation, job routing, supervision, and shutdown.
Sidekiq::Ratomic::Pool contains mutable circuit-breaker coordination state and
is not itself required to be Ractor-shareable. A Ractor-aware host should pass
only shareable configuration and factory data into each Ractor, construct that
Ractor's pool runtime there, and execute the resource-backed work inside the
same Ractor:
PoolInput = Data.define(:pool_name, :size, :pool_timeout, :factory, :jobs)
ractor = Ractor.new(
Ractor.make_shareable(
PoolInput.new(:redis_pool, 20, 1, RedisFactory.new(ENV.fetch('REDIS_URL')), 100)
)
) do |input|
pool = Sidekiq::Ratomic::Pool.new(
pool_name: input.pool_name,
size: input.size,
pool_timeout: input.pool_timeout,
factory: input.factory
)
input.jobs.times do
pool.with { |resource| resource.call('PING') }
end
pool.close
:complete
end
ractor.value
Threads created by the host inside that Ractor use the same Ractor-local pool; resources must never be returned to, or used by, another Ractor. The integration contract is:
- The host owns Ractor creation, job routing, supervision, and shutdown. This gem provides pooling middleware, not a Ractor scheduler.
- Pool configuration and factories crossing a Ractor boundary must be shareable. Construct the mutable pool runtime and any stateful validator inside the destination Ractor.
Ratomic::LocalPoollazily creates resources in their owning Ractor. Threads in that Ractor share its resources, while other Ractors receive independent pools and resources.- Retry, health validation, circuit breaking, failure accounting, and explicit close/shutdown remain local to the runtime that owns the resources. A half-open circuit admits one recovery probe at a time.
The middleware runtime is ordinary mutable state and is not itself required to be Ractor-shareable. Hosts must supervise failures and coordinate cancellation across Ractors when their application topology requires it.
The opt-in host adapter example demonstrates bounded dispatch, real Redis connections, and per-Ractor activity metrics without making Ractor scheduling part of the gem:
cd smoke_test
docker compose up -d redis
REDIS_URL="${REDIS_URL:-redis://127.0.0.1:6380/0}" \
RACTOR_NATIVE_COUNT=4 \
RACTOR_NATIVE_THREADS=20 \
RATOMIC_POOL_SIZE=20 \
RATOMIC_POOL_TIMEOUT=1 \
RACTOR_QUEUE_CAPACITY=40 \
BENCHMARK_JOBS_PER_RACTOR=5000 \
BENCHMARK_WORK_SECONDS=0.05 \
bundle exec ruby ./benchmark/ractor_host_adapter.rb
The adapter defaults its Ractor count to Etc.nprocessors when the variable
is omitted. That CPU-based default belongs to the example only; production
applications remain responsible for choosing and supervising their topology.
The adapter performs real Redis PING, INCR, and HSET operations and
reports Ruby, Sidekiq, Ratomic, and Redis versions, elapsed time, throughput,
and per-Ractor results. With four Ractors, the command above processes 20,000
jobs and demonstrates four independent pools of 20 resources, shared by 20
threads inside each Ractor.
The circuit breaker uses Ratomic::Counter for its failure count. This keeps the
counter aligned with Ratomic's Ractor-safe primitive model and avoids a separate
concurrent-ruby production dependency; the pool mutex still protects the
failure-count and circuit-state transition as one operation.
Smoke test
The smoke_test/ harness runs Redis in Docker, starts a standalone
Sidekiq server, enqueues jobs from a separate client process, and verifies the
results through real Redis connections. It also prints ps -L snapshots showing
Sidekiq worker threads and the CPU core (PSR) on which they were recently scheduled:
cd smoke_test
./run.sh
For comparative throughput measurements, see the smoke_test/benchmark/
harness.