sidekiq-ratomic-pool 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +82 -0
- data/lib/sidekiq/ratomic/pool/errors.rb +18 -0
- data/lib/sidekiq/ratomic/pool/version.rb +12 -0
- data/lib/sidekiq/ratomic/pool.rb +169 -0
- data/lib/sidekiq_ratomic_pool.rb +3 -0
- data/sig/sidekiq/ratomic/pool.rbs +62 -0
- data/smoke_test/client.rb +37 -0
- data/smoke_test/server.rb +32 -0
- data/smoke_test/worker.rb +24 -0
- metadata +72 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 73e5869196bd322f91cea20c1332e39b2c2e3c9d81e99717ac74210fb766be36
|
|
4
|
+
data.tar.gz: 4b830bdc58727ba25cc653011bf52d1394d2344ad5de996a7f20965b9b404e57
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6272ccc17b365724a851f3d6b39cb82d4ce109068cc854a640ab0b649f2c062bf4ee6afdeccf0c6e442789dfe03be411eb3bf8abd8dab2da36d8f9b5f8d09e00
|
|
7
|
+
data.tar.gz: 71f001b3aca75e43792bf760795d037a6f7a220eb15de33da7b1802eab89e976186ebce147f6eeb21f95e88c702ffd05c8309b06a410831609b58ff613133417
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
- Added Sidekiq middleware with Ratomic `LocalPool` resource injection,
|
|
4
|
+
health validation, exponential retries, and circuit-breaker fast failure.
|
|
5
|
+
- Added thread-boundary pool behavior tests and synchronized RBS signatures for the
|
|
6
|
+
Ractor-local implementation.
|
|
7
|
+
- Added the `quality` Rake task covering tests, RuboCop, Steep, and YARD validation.
|
|
8
|
+
- Enforced at least 99% line and branch coverage through SimpleCov.
|
|
9
|
+
- Used Ratomic's native `Counter` primitive.
|
|
10
|
+
- Added YARD validation and documented the public pool API.
|
|
11
|
+
- Added argument validation and current-Ractor pool shutdown.
|
|
12
|
+
- Added configurable retryable I/O errors and real exponential backoff delays.
|
|
13
|
+
- Prevented non-retryable worker exceptions from affecting the circuit breaker.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kenneth C. Demanawa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# sidekiq-ratomic-pool
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/sidekiq-ratomic-pool)
|
|
4
|
+
[](https://github.com/kanutocd/sidekiq-ratomic-pool/actions)
|
|
5
|
+
[](https://www.ruby-lang.org/en/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add to your `Gemfile`:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "sidekiq-ratomic-pool"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Sidekiq dependency
|
|
20
|
+
|
|
21
|
+
Despite its name, this gem does not declare `sidekiq` as a runtime dependency.
|
|
22
|
+
It provides a `Sidekiq::Ratomic::Pool` middleware implementation using the
|
|
23
|
+
standard `call(job, payload, queue) { ... }` middleware contract. Sidekiq is the
|
|
24
|
+
primary supported integration and the reason for the gem name, but other
|
|
25
|
+
Sidekiq-compatible job frameworks can use it if they support the same contract
|
|
26
|
+
and worker pool accessor pattern.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **Ractor-Local Isolation**: Each Ractor lazily owns its resources through `Ratomic::LocalPool`; threads within the same Ractor share that Ractor-local pool.
|
|
31
|
+
- **Circuit Breaker Pattern**: Trips open after a configurable threshold of checkout, health-check, or configured retryable I/O failures.
|
|
32
|
+
- **Exponential Backoff**: Applies increasing retry delays to transient checkout and retryable resource-operation failures.
|
|
33
|
+
- **Automated Health Probes**: Validates resources with `ping`, `active?`, or a caller-supplied validator before use.
|
|
34
|
+
- **Configurable Failure Policy**: Non-retryable worker exceptions propagate without changing circuit state, avoiding accidental duplicate work.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Factories must be Ractor-shareable because `Ratomic::LocalPool` creates resources lazily
|
|
39
|
+
inside each Ractor. A small frozen factory object is suitable for production use:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
RedisFactory = Data.define(:url) do
|
|
43
|
+
def call
|
|
44
|
+
RedisClient.new(url:)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Sidekiq.configure_server do |config|
|
|
49
|
+
config.server_middleware do |chain|
|
|
50
|
+
chain.add Sidekiq::Ratomic::Pool,
|
|
51
|
+
pool_name: :redis_pool,
|
|
52
|
+
size: 10,
|
|
53
|
+
max_retries: 3,
|
|
54
|
+
retry_delay: 0.2,
|
|
55
|
+
cb_threshold: 5,
|
|
56
|
+
cb_timeout: 30,
|
|
57
|
+
factory: RedisFactory.new(ENV.fetch('REDIS_URL')).freeze
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Workers use the injected pool with `with`:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
redis_pool.with { |redis| redis.call('PING') }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Resource checkout/health failures and configured retryable I/O errors use exponential backoff.
|
|
69
|
+
Other exceptions raised by the worker block propagate without being retried, preventing
|
|
70
|
+
accidental duplication of non-idempotent work.
|
|
71
|
+
|
|
72
|
+
## Smoke test
|
|
73
|
+
|
|
74
|
+
The [`smoke_test/`](smoke_test/) harness runs Redis in Docker, starts a standalone
|
|
75
|
+
Sidekiq server, enqueues jobs from a separate client process, and verifies the
|
|
76
|
+
results through real Redis connections. It also prints `ps -L` snapshots showing
|
|
77
|
+
Sidekiq worker threads and the CPU core (`PSR`) on which they were recently scheduled:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cd smoke_test
|
|
81
|
+
./run.sh
|
|
82
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Sidekiq integration namespace.
|
|
4
|
+
module Sidekiq
|
|
5
|
+
# Ratomic-backed Sidekiq middleware namespace.
|
|
6
|
+
module Ratomic
|
|
7
|
+
class Pool
|
|
8
|
+
# Base error for pool failures.
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Raised when a checked-out resource fails validation.
|
|
12
|
+
class CheckoutError < Error; end
|
|
13
|
+
|
|
14
|
+
# Raised when the circuit breaker is open.
|
|
15
|
+
class CircuitOpenError < Error; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ratomic'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
require_relative 'pool/errors'
|
|
6
|
+
require_relative 'pool/version'
|
|
7
|
+
|
|
8
|
+
# Sidekiq integration namespace.
|
|
9
|
+
module Sidekiq
|
|
10
|
+
# Ratomic-backed Sidekiq middleware namespace.
|
|
11
|
+
module Ratomic
|
|
12
|
+
# Sidekiq server middleware that exposes a Ractor-local resource pool.
|
|
13
|
+
#
|
|
14
|
+
# Resources are validated before checkout and transient failures are retried
|
|
15
|
+
# with exponential backoff. Persistent failures open the circuit breaker.
|
|
16
|
+
class Pool
|
|
17
|
+
attr_reader :pool_name, :size, :max_retries, :retry_delay, :validator, :cb_threshold, :cb_timeout,
|
|
18
|
+
:retryable_errors
|
|
19
|
+
|
|
20
|
+
def initialize(pool_name:, size: 10, max_retries: 3, retry_delay: 0.2,
|
|
21
|
+
cb_threshold: 5, cb_timeout: 30, validator: nil,
|
|
22
|
+
retryable_errors: [IOError, SystemCallError, Timeout::Error], factory: nil, &block)
|
|
23
|
+
factory ||= block
|
|
24
|
+
raise ArgumentError, 'A resource factory must be provided' unless factory
|
|
25
|
+
|
|
26
|
+
validate_options!(size, max_retries, retry_delay, cb_threshold, cb_timeout)
|
|
27
|
+
|
|
28
|
+
@pool_name = pool_name.to_sym
|
|
29
|
+
@size = size
|
|
30
|
+
@max_retries = max_retries
|
|
31
|
+
@retry_delay = retry_delay
|
|
32
|
+
@cb_threshold = cb_threshold
|
|
33
|
+
@cb_timeout = cb_timeout
|
|
34
|
+
@validator = validator || method(:default_validator)
|
|
35
|
+
@retryable_errors = retryable_errors.freeze
|
|
36
|
+
@state_mutex = Mutex.new
|
|
37
|
+
@failure_count = ::Ratomic::Counter.new
|
|
38
|
+
@state = :closed
|
|
39
|
+
@last_state_change = monotonic_time
|
|
40
|
+
|
|
41
|
+
shareable_factory = Ractor.make_shareable(factory)
|
|
42
|
+
@local_pool = ::Ratomic::LocalPool.new(size: @size, factory: shareable_factory)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Inject this pool into a worker's configured pool accessor.
|
|
46
|
+
def call(job_instance, _job_payload, _queue)
|
|
47
|
+
setter = "#{@pool_name}="
|
|
48
|
+
job_instance.public_send(setter, self) if job_instance.respond_to?(setter)
|
|
49
|
+
yield
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Close resources owned by the current Ractor.
|
|
53
|
+
def close
|
|
54
|
+
@local_pool.close
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
alias shutdown close
|
|
58
|
+
|
|
59
|
+
# Check out a healthy resource and yield it to the caller.
|
|
60
|
+
def with
|
|
61
|
+
check_circuit_state!
|
|
62
|
+
attempts = 0
|
|
63
|
+
work_failed = false
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
attempts += 1
|
|
67
|
+
@local_pool.with do |resource|
|
|
68
|
+
raise Pool::CheckoutError, 'Resource connection health check failed' unless verify_health(resource)
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
result = yield resource
|
|
72
|
+
rescue StandardError
|
|
73
|
+
work_failed = true
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
record_success
|
|
77
|
+
result
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
raise unless !work_failed || retryable_error?(e)
|
|
81
|
+
|
|
82
|
+
record_failure
|
|
83
|
+
if attempts <= @max_retries && state != :open
|
|
84
|
+
delay = @retry_delay * (2**(attempts - 1))
|
|
85
|
+
sleep(delay) if delay.positive?
|
|
86
|
+
retry
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
raise
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Return the current circuit-breaker state.
|
|
94
|
+
def state
|
|
95
|
+
@state_mutex.synchronize do
|
|
96
|
+
if @state == :open && monotonic_time - @last_state_change > @cb_timeout
|
|
97
|
+
@state = :half_open
|
|
98
|
+
@last_state_change = monotonic_time
|
|
99
|
+
end
|
|
100
|
+
@state
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def check_circuit_state!
|
|
107
|
+
return unless state == :open
|
|
108
|
+
|
|
109
|
+
raise Pool::CircuitOpenError, 'Circuit breaker is open'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def verify_health(resource)
|
|
113
|
+
@validator.call(resource)
|
|
114
|
+
rescue StandardError
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def retryable_error?(error)
|
|
119
|
+
!work_error?(error) || @retryable_errors.any? { |error_class| error.is_a?(error_class) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def work_error?(error)
|
|
123
|
+
error.is_a?(StandardError) && !error.is_a?(Pool::CheckoutError)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def validate_options!(size, max_retries, retry_delay, cb_threshold, cb_timeout)
|
|
127
|
+
validations = [
|
|
128
|
+
[size.is_a?(Integer) && size.positive?, 'size must be a positive Integer'],
|
|
129
|
+
[max_retries.is_a?(Integer) && max_retries >= 0, 'max_retries must be a non-negative Integer'],
|
|
130
|
+
[retry_delay.is_a?(Numeric) && retry_delay >= 0, 'retry_delay must be non-negative'],
|
|
131
|
+
[cb_threshold.is_a?(Integer) && cb_threshold.positive?, 'cb_threshold must be a positive Integer'],
|
|
132
|
+
[cb_timeout.is_a?(Numeric) && cb_timeout >= 0, 'cb_timeout must be non-negative']
|
|
133
|
+
]
|
|
134
|
+
validations.each do |valid, message|
|
|
135
|
+
raise ArgumentError, message unless valid
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def default_validator(resource)
|
|
140
|
+
return resource.ping if resource.respond_to?(:ping)
|
|
141
|
+
return resource.active? if resource.respond_to?(:active?)
|
|
142
|
+
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def record_success
|
|
147
|
+
@state_mutex.synchronize do
|
|
148
|
+
failure_count = @failure_count.value
|
|
149
|
+
@failure_count.decrement(failure_count) unless failure_count.zero?
|
|
150
|
+
@state = :closed if @state == :half_open
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def record_failure
|
|
155
|
+
@state_mutex.synchronize do
|
|
156
|
+
@failure_count.increment(1)
|
|
157
|
+
if @failure_count.value >= @cb_threshold || @state == :half_open
|
|
158
|
+
@state = :open
|
|
159
|
+
@last_state_change = monotonic_time
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def monotonic_time
|
|
165
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
module Ratomic
|
|
3
|
+
class Pool
|
|
4
|
+
VERSION: String
|
|
5
|
+
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class CheckoutError < Error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class CircuitOpenError < Error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader pool_name: Symbol
|
|
16
|
+
attr_reader size: Integer
|
|
17
|
+
attr_reader max_retries: Integer
|
|
18
|
+
attr_reader retry_delay: untyped
|
|
19
|
+
attr_reader validator: ^(untyped) -> bool
|
|
20
|
+
attr_reader cb_threshold: Integer
|
|
21
|
+
attr_reader cb_timeout: Numeric
|
|
22
|
+
attr_reader retryable_errors: Array[Class]
|
|
23
|
+
|
|
24
|
+
def initialize: (
|
|
25
|
+
pool_name: Symbol | String,
|
|
26
|
+
?size: Integer,
|
|
27
|
+
?max_retries: Integer,
|
|
28
|
+
?retry_delay: Numeric,
|
|
29
|
+
?cb_threshold: Integer,
|
|
30
|
+
?cb_timeout: Numeric,
|
|
31
|
+
?validator: ^(untyped) -> bool,
|
|
32
|
+
?retryable_errors: Array[Class],
|
|
33
|
+
?factory: _SidekiqRatomicFactory
|
|
34
|
+
) { () -> untyped } -> void
|
|
35
|
+
|
|
36
|
+
def call: (untyped, untyped, untyped) { () -> untyped } -> untyped
|
|
37
|
+
def close: () -> nil
|
|
38
|
+
def shutdown: () -> nil
|
|
39
|
+
def with: () { (untyped) -> untyped } -> untyped
|
|
40
|
+
def state: () -> Symbol
|
|
41
|
+
private
|
|
42
|
+
def validate_options!: (untyped, untyped, untyped, untyped, untyped) -> void
|
|
43
|
+
def check_circuit_state!: () -> void
|
|
44
|
+
def verify_health: (untyped) -> bool
|
|
45
|
+
def retryable_error?: (StandardError) -> bool
|
|
46
|
+
def work_error?: (StandardError) -> bool
|
|
47
|
+
def default_validator: (untyped) -> bool
|
|
48
|
+
def record_success: () -> void
|
|
49
|
+
def record_failure: () -> void
|
|
50
|
+
def monotonic_time: () -> Float
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
interface _SidekiqRatomicFactory
|
|
56
|
+
def call: () -> untyped
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module Timeout
|
|
60
|
+
class Error < StandardError
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sidekiq'
|
|
4
|
+
require 'redis-client'
|
|
5
|
+
require_relative 'worker'
|
|
6
|
+
|
|
7
|
+
redis_url = ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379/0').freeze
|
|
8
|
+
job_count = Integer(ENV.fetch('SMOKE_JOB_COUNT', '20'))
|
|
9
|
+
timeout = Integer(ENV.fetch('SMOKE_TIMEOUT', '30'))
|
|
10
|
+
run_id = ENV.fetch('SMOKE_RUN_ID', Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond).to_s)
|
|
11
|
+
redis = RedisClient.config(url: redis_url).new_client
|
|
12
|
+
Sidekiq.configure_client { |config| config.redis = { url: redis_url } }
|
|
13
|
+
|
|
14
|
+
redis.call('DEL', SmokeTest::COUNT_KEY, SmokeTest::RESULTS_KEY)
|
|
15
|
+
job_ids = job_count.times.map do |index|
|
|
16
|
+
job_id = "#{run_id}-#{index}"
|
|
17
|
+
Sidekiq::Client.push('class' => SmokeTest::RedisSmokeWorker, 'args' => [job_id], 'queue' => 'smoke')
|
|
18
|
+
job_id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
22
|
+
loop do
|
|
23
|
+
processed = Integer(redis.call('GET', SmokeTest::COUNT_KEY) || 0)
|
|
24
|
+
results = Integer(redis.call('HLEN', SmokeTest::RESULTS_KEY) || 0)
|
|
25
|
+
break if processed >= job_count && results >= job_count
|
|
26
|
+
|
|
27
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
28
|
+
abort "Timed out waiting for #{job_count} jobs (processed=#{processed}, results=#{results})"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sleep 0.1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
missing = job_ids.reject { |job_id| redis.call('HGET', SmokeTest::RESULTS_KEY, job_id) == 'processed' }
|
|
35
|
+
abort "Missing processed jobs: #{missing.join(', ')}" unless missing.empty?
|
|
36
|
+
|
|
37
|
+
puts "Smoke test passed: #{job_count} jobs processed by standalone Sidekiq through real Redis connections"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sidekiq'
|
|
4
|
+
require 'redis-client'
|
|
5
|
+
require 'sidekiq_ratomic_pool'
|
|
6
|
+
require_relative 'worker'
|
|
7
|
+
|
|
8
|
+
RedisFactory = Data.define(:url) do
|
|
9
|
+
def call
|
|
10
|
+
RedisClient.config(url:).new_client
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
redis_port = ENV.fetch('REDIS_PORT', '6379')
|
|
15
|
+
redis_url = ENV.fetch('REDIS_URL', "redis://127.0.0.1:#{redis_port}/0").freeze
|
|
16
|
+
|
|
17
|
+
Sidekiq.configure_server do |config|
|
|
18
|
+
config.redis = { url: redis_url }
|
|
19
|
+
config.server_middleware do |chain|
|
|
20
|
+
chain.add(
|
|
21
|
+
Sidekiq::Ratomic::Pool,
|
|
22
|
+
pool_name: :redis_pool,
|
|
23
|
+
size: Integer(ENV.fetch('RATOMIC_POOL_SIZE', '4')),
|
|
24
|
+
max_retries: 3,
|
|
25
|
+
retry_delay: 0.05,
|
|
26
|
+
cb_threshold: 5,
|
|
27
|
+
cb_timeout: 5,
|
|
28
|
+
validator: ->(redis) { redis.call('PING') == 'PONG' },
|
|
29
|
+
factory: RedisFactory.new(redis_url).freeze
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SmokeTest
|
|
4
|
+
COUNT_KEY = 'sidekiq-ratomic-pool:smoke:processed'
|
|
5
|
+
RESULTS_KEY = 'sidekiq-ratomic-pool:smoke:results'
|
|
6
|
+
|
|
7
|
+
# Sidekiq job that performs real Redis work through the injected pool.
|
|
8
|
+
class RedisSmokeWorker
|
|
9
|
+
include Sidekiq::Job
|
|
10
|
+
|
|
11
|
+
sidekiq_options queue: 'smoke', retry: 2
|
|
12
|
+
|
|
13
|
+
attr_accessor :redis_pool
|
|
14
|
+
|
|
15
|
+
def perform(job_id)
|
|
16
|
+
redis_pool.with do |redis|
|
|
17
|
+
redis.call('INCR', SmokeTest::COUNT_KEY)
|
|
18
|
+
redis.call('HSET', SmokeTest::RESULTS_KEY, job_id, 'processed')
|
|
19
|
+
work_seconds = Float(ENV.fetch('SMOKE_WORK_SECONDS', '0.05'))
|
|
20
|
+
sleep work_seconds if work_seconds.positive?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sidekiq-ratomic-pool
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ken C. Demanawa
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ratomic
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.4.3
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.4.3
|
|
26
|
+
description: |
|
|
27
|
+
Thread and Ractor safe connection pooling utilizing Ratomic's LocalPool for Sidekiq middleware
|
|
28
|
+
with health validation, exponential retries, and circuit breakers.
|
|
29
|
+
email:
|
|
30
|
+
- kenneth.c.demanawa@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- lib/sidekiq/ratomic/pool.rb
|
|
39
|
+
- lib/sidekiq/ratomic/pool/errors.rb
|
|
40
|
+
- lib/sidekiq/ratomic/pool/version.rb
|
|
41
|
+
- lib/sidekiq_ratomic_pool.rb
|
|
42
|
+
- sig/sidekiq/ratomic/pool.rbs
|
|
43
|
+
- smoke_test/client.rb
|
|
44
|
+
- smoke_test/server.rb
|
|
45
|
+
- smoke_test/worker.rb
|
|
46
|
+
homepage: https://kanutocd.github.io/sidekiq-ratomic-pool
|
|
47
|
+
licenses:
|
|
48
|
+
- MIT
|
|
49
|
+
metadata:
|
|
50
|
+
homepage_uri: https://kanutocd.github.io/sidekiq-ratomic-pool
|
|
51
|
+
source_code_uri: https://github.com/kanutocd/sidekiq-ratomic-pool
|
|
52
|
+
changelog_uri: https://github.com/kanutocd/sidekiq-ratomic-pool/blob/main/CHANGELOG.md
|
|
53
|
+
documentation_uri: https://kanutocd.github.io/sidekiq-ratomic-pool
|
|
54
|
+
rubygems_mfa_required: 'true'
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '4.0'
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
requirements: []
|
|
69
|
+
rubygems_version: 4.0.18
|
|
70
|
+
specification_version: 4
|
|
71
|
+
summary: Ractor-safe connection pooling for Sidekiq utilizing Ratomic's LocalPool.
|
|
72
|
+
test_files: []
|