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,
size: 10,
pool_timeout: 1,
max_retries: 3,
retry_delay: 0.2,
cb_threshold: 5,
cb_timeout: 30,
factory: RedisFactory.new(ENV.fetch('REDIS_URL')).freeze
end
end
Workers use the injected pool with with:
redis_pool.with { |redis| redis.call('PING') }
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 not be returned to, or used by, another Ractor. See the
Ractor-local pooling implementation plan
for the integration contract and rollout criteria.
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.