solid-jobs 0.1.1 → 0.1.3
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 +4 -4
- data/CHANGELOG.md +28 -2
- data/README.md +20 -15
- data/docs/reliability.md +27 -0
- data/lib/solid_jobs/server.rb +55 -4
- data/lib/solid_jobs/version.rb +1 -2
- data/lib/solid_jobs.rb +0 -1
- metadata +3 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52c2c82a111ab574881c9efe6f5c692c145b50ad85542844e34de74a44860a8e
|
|
4
|
+
data.tar.gz: 7a23ba9ea19c901c7c888680351b018b8f554cac0192a17195682aaa3725bd82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63447e81affab81cac25ed04aed7fa8cc8b27d89e5fd6425db087ed0feadc4d950266c35c16df6e91f27f5a5562844610516048ccbc4120f5132f9ab83456970
|
|
7
|
+
data.tar.gz: 3da40a61ca01cd8f868ebc0dd734b925ad0d46852517f34ea73b23cb8e9762c05b09db55caf218729a734975c579cb97d27253666142dbc694e0c5a018df427a
|
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.1.3] - 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Require `solid-redis` 1.0.11 and rely on its `solid-resp-ractor` dependency
|
|
10
|
+
instead of declaring and loading the RESP codec directly.
|
|
11
|
+
|
|
12
|
+
## [0.1.2] - 2026-10-01
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `Server#start` logs a warning on Ruby < 4 when `concurrency > 1`, pointing
|
|
17
|
+
to the Ruby 3.4 Ractor GC-barrier deadlock and the recommended setups.
|
|
18
|
+
- `Server#stop` is bounded: components get `shutdown_timeout + STOP_GRACE`
|
|
19
|
+
to return, `:stop` is re-sent up to `STOP_RESENDS` times, then the
|
|
20
|
+
component is abandoned with an error log instead of hanging shutdown.
|
|
21
|
+
- `test/support/ractor_barrier_repro.rb`: standalone reproducer of the Ruby
|
|
22
|
+
3.4 Ractor GC-barrier deadlock (no SolidJobs, no Redis).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Server-based stress tests are skipped on Ruby < 4: Ruby 3.4's Ractor
|
|
27
|
+
scheduler deadlocks the VM on a GC barrier under cross-Ractor `move:`
|
|
28
|
+
traffic. Multi-Ractor servers are recommended on Ruby ≥ 4.0 (see
|
|
29
|
+
`docs/reliability.md`). CI jobs now time out after 20 minutes.
|
|
6
30
|
|
|
7
31
|
## [0.1.1] - 2026-10-01
|
|
8
32
|
|
|
@@ -37,5 +61,7 @@ All notable changes to this project will be documented in this file.
|
|
|
37
61
|
- Reliable-hot-path and CPU-scaling profilers plus the independent Sidekiq
|
|
38
62
|
comparison benchmark.
|
|
39
63
|
|
|
40
|
-
[
|
|
64
|
+
[0.1.3]: https://github.com/nicolasva/solid-jobs/compare/v0.1.2...v0.1.3
|
|
65
|
+
[0.1.2]: https://github.com/nicolasva/solid-jobs/compare/v0.1.1...v0.1.2
|
|
66
|
+
[0.1.1]: https://github.com/nicolasva/solid-jobs/compare/v0.1.0...v0.1.1
|
|
41
67
|
[0.1.0]: https://github.com/nicolasva/solid-jobs/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -131,29 +131,34 @@ stability. Every measurement runs in a fresh Ruby process; client order
|
|
|
131
131
|
alternates and the default report uses the median of six repetitions.
|
|
132
132
|
|
|
133
133
|
The current homogeneous CPU-processing reference uses Ruby 4.0.1,
|
|
134
|
-
Sidekiq 8.1.7, a
|
|
134
|
+
Sidekiq 8.1.7, SolidJobs 0.1.2, a 100,000-iteration integer workload, and
|
|
135
|
+
2,000 jobs per case:
|
|
135
136
|
|
|
136
137
|
| Concurrency | Sidekiq jobs/s | SolidJobs jobs/s | SolidJobs scaling | Sidekiq CPU-s/1k | SolidJobs CPU-s/1k | Sidekiq RSS | SolidJobs RSS | Sidekiq alloc/job | SolidJobs alloc/job |
|
|
137
138
|
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
138
|
-
| 1 |
|
|
139
|
-
| 2 |
|
|
140
|
-
| 4 |
|
|
141
|
-
| 8 |
|
|
139
|
+
| 1 | 289 | 281 | 100.0% | 3.35 | 3.40 | 41.4 MiB | 81.8 MiB | 117.2 | 84.0 |
|
|
140
|
+
| 2 | 296 | 549 | 97.7% | 3.38 | 3.44 | 41.7 MiB | 83.1 MiB | 116.8 | 79.7 |
|
|
141
|
+
| 4 | 298 | 1,090 | 97.0% | 3.37 | 3.43 | 41.8 MiB | 79.5 MiB | 111.1 | 77.7 |
|
|
142
|
+
| 8 | 298 | 2,053 | 91.4% | 3.36 | 3.67 | 42.4 MiB | 74.1 MiB | 108.1 | 74.3 |
|
|
142
143
|
|
|
143
|
-
At eight concurrency units, SolidJobs reaches
|
|
144
|
-
for
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
also uses more RSS, but reaches
|
|
144
|
+
At eight concurrency units, SolidJobs reaches 2,053 jobs/s versus 298 jobs/s
|
|
145
|
+
for Sidekiq. This is Ractor parallelism rather than equal CPU efficiency:
|
|
146
|
+
SolidJobs consumes 752.6% CPU and 3.67 CPU-seconds per 1,000 jobs, while
|
|
147
|
+
Sidekiq consumes 100.2% CPU and 3.36 CPU-seconds per 1,000 jobs. SolidJobs
|
|
148
|
+
also uses more RSS, but reaches 27.71 jobs/s/MiB versus 7.03 for Sidekiq.
|
|
148
149
|
|
|
149
150
|
These are local synthetic measurements, not application-capacity claims.
|
|
150
151
|
Queue p95/p99 values in this run use sparse sampling and are excluded from the
|
|
151
152
|
summary until the final latency campaign increases the sample count. Ruby
|
|
152
|
-
3.4.4 eight-Ractor results are also excluded:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
3.4.4 eight-Ractor results are also excluded: Ruby 3.4's Ractor scheduler
|
|
154
|
+
crashed (concurrent TCP/RESP initialization, reproduced on macOS arm64 and
|
|
155
|
+
Linux x86_64) or deadlocked VM-wide on a GC barrier under cross-Ractor
|
|
156
|
+
message traffic (`test/support/ractor_barrier_repro.rb` reproduces it without
|
|
157
|
+
SolidJobs).
|
|
158
|
+
Ruby 4.0.1 passed the equivalent reproducers, and `StartupBarrier` serializes
|
|
159
|
+
component initialization before releasing normal parallel processing.
|
|
160
|
+
**Multi-Ractor servers are recommended on Ruby ≥ 4.0**; see
|
|
161
|
+
`docs/reliability.md`.
|
|
157
162
|
|
|
158
163
|
The project is under active development. The Web UI and commercial Sidekiq
|
|
159
164
|
features are not part of the initial scope.
|
data/docs/reliability.md
CHANGED
|
@@ -71,6 +71,33 @@ initialization paths known to crash Ruby 3.4 (reproduced on 3.4.4 macOS arm64
|
|
|
71
71
|
and 3.4.11 Linux x86_64; `rake startup_torture` is the reproducer); normal
|
|
72
72
|
processing remains parallel after `RUNNING`.
|
|
73
73
|
|
|
74
|
+
### Ruby 3.4 Ractor caveat
|
|
75
|
+
|
|
76
|
+
Ruby 3.4's Ractor scheduler can deadlock the whole VM (main thread included)
|
|
77
|
+
when a GC-triggered `rb_ractor_sched_barrier_start` runs while several
|
|
78
|
+
Ractors exchange `move: true` messages: every thread parks in
|
|
79
|
+
`ractor_sched_barrier_join_wait_locked` and the barrier never completes.
|
|
80
|
+
`test/support/ractor_barrier_repro.rb` reproduces it **without SolidJobs or
|
|
81
|
+
Redis** (one receiver, four senders, 24k moved messages per iteration):
|
|
82
|
+
Ruby 3.4.4 freezes within the first iterations, Ruby 4.0.1 completes 30/30.
|
|
83
|
+
Inside SolidJobs the same traffic pattern is the Processor → Heartbeat
|
|
84
|
+
`:work`/`:done`/`:stats` channel, so any multi-Processor server on Ruby 3.4
|
|
85
|
+
is exposed; once frozen, neither `Timeout` nor process exit
|
|
86
|
+
(`rb_ractor_terminate_all`) can recover.
|
|
87
|
+
|
|
88
|
+
Recommendation: **run multi-Ractor SolidJobs servers on Ruby ≥ 4.0**. On
|
|
89
|
+
Ruby 3.4 use the client/API side freely, and prefer one process per
|
|
90
|
+
Processor (`concurrency: 1`) for the server. Server-based stress tests are
|
|
91
|
+
skipped on Ruby < 4 for this reason, and `Server#start` logs a warning when
|
|
92
|
+
it detects `concurrency > 1` on Ruby < 4.
|
|
93
|
+
|
|
94
|
+
### Bounded shutdown
|
|
95
|
+
|
|
96
|
+
`Server#stop` never waits forever for a component. Each Ractor gets
|
|
97
|
+
`shutdown_timeout + Server::STOP_GRACE` to return after `:stop`; the signal
|
|
98
|
+
is re-sent up to `STOP_RESENDS` times, then the component is abandoned with
|
|
99
|
+
an error log so the process can proceed with shutdown.
|
|
100
|
+
|
|
74
101
|
## Configuration scope
|
|
75
102
|
|
|
76
103
|
`SolidJobs.config` and the testing mode are Ractor-local, not thread-local.
|
data/lib/solid_jobs/server.rb
CHANGED
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "socket"
|
|
5
|
+
require "timeout"
|
|
5
6
|
|
|
6
7
|
module SolidJobs
|
|
7
8
|
class Server
|
|
9
|
+
# Extra time granted beyond shutdown_timeout before a component is
|
|
10
|
+
# considered stuck, and how many times :stop is re-sent before giving up.
|
|
11
|
+
STOP_GRACE = 5.0
|
|
12
|
+
STOP_RESENDS = 3
|
|
13
|
+
|
|
8
14
|
attr_reader :config, :identity, :ractors
|
|
9
15
|
|
|
10
|
-
def initialize(config: SolidJobs.config)
|
|
16
|
+
def initialize(config: SolidJobs.config, stop_grace: STOP_GRACE)
|
|
11
17
|
@config = config
|
|
18
|
+
@stop_grace = stop_grace
|
|
12
19
|
@ractors = []
|
|
13
20
|
@scheduler = nil
|
|
14
21
|
@heartbeat = nil
|
|
@@ -20,6 +27,7 @@ module SolidJobs
|
|
|
20
27
|
def start
|
|
21
28
|
raise Error, "Server is already running" if @started
|
|
22
29
|
|
|
30
|
+
warn_ruby34_multi_ractor
|
|
23
31
|
snapshot = Utilities.shareable_copy(
|
|
24
32
|
config.ractor_snapshot.merge(identity: identity, started_at: @started_at),
|
|
25
33
|
)
|
|
@@ -197,10 +205,12 @@ module SolidJobs
|
|
|
197
205
|
|
|
198
206
|
@ractors.each { |ractor| ractor.send(:stop) }
|
|
199
207
|
@scheduler&.send(:stop)
|
|
200
|
-
results = @ractors.map
|
|
201
|
-
|
|
208
|
+
results = @ractors.each_with_index.map do |ractor, index|
|
|
209
|
+
await_termination(ractor, "processor #{index}")
|
|
210
|
+
end
|
|
211
|
+
await_termination(@scheduler, "scheduler") if @scheduler
|
|
202
212
|
@heartbeat&.send(:stop)
|
|
203
|
-
|
|
213
|
+
await_termination(@heartbeat, "heartbeat") if @heartbeat
|
|
204
214
|
@ractors = []
|
|
205
215
|
@scheduler = nil
|
|
206
216
|
@heartbeat = nil
|
|
@@ -213,6 +223,47 @@ module SolidJobs
|
|
|
213
223
|
@started
|
|
214
224
|
end
|
|
215
225
|
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
RUBY34_MULTI_RACTOR_WARNING =
|
|
229
|
+
"SolidJobs: Ruby %s can deadlock the whole VM on a Ractor GC barrier " \
|
|
230
|
+
"under multi-Processor load (see docs/reliability.md). Run multi-Ractor " \
|
|
231
|
+
"servers on Ruby >= 4.0, or use concurrency: 1 (one process per Processor)."
|
|
232
|
+
|
|
233
|
+
def warn_ruby34_multi_ractor
|
|
234
|
+
return if RUBY_VERSION >= "4" || config.concurrency <= 1
|
|
235
|
+
|
|
236
|
+
config.logger.warn(format(RUBY34_MULTI_RACTOR_WARNING, RUBY_VERSION))
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Waits for a component Ractor to return after :stop. Ruby 3.4 can lose
|
|
240
|
+
# the wakeup of a Ractor.receive running in a secondary thread while the
|
|
241
|
+
# inbox is busy; a fresh :stop message re-triggers it. If the component
|
|
242
|
+
# still does not return, it is abandoned so shutdown never hangs forever.
|
|
243
|
+
def await_termination(ractor, name)
|
|
244
|
+
deadline = config.shutdown_timeout + @stop_grace
|
|
245
|
+
resends = 0
|
|
246
|
+
begin
|
|
247
|
+
Timeout.timeout(deadline) { RactorSupport.value(ractor) }
|
|
248
|
+
rescue Timeout::Error
|
|
249
|
+
if resends < STOP_RESENDS
|
|
250
|
+
resends += 1
|
|
251
|
+
config.logger.warn(
|
|
252
|
+
"SolidJobs #{name} did not stop within #{deadline}s, " \
|
|
253
|
+
"re-sending :stop (#{resends}/#{STOP_RESENDS})",
|
|
254
|
+
)
|
|
255
|
+
begin
|
|
256
|
+
ractor.send(:stop)
|
|
257
|
+
rescue Ractor::ClosedError
|
|
258
|
+
nil
|
|
259
|
+
end
|
|
260
|
+
retry
|
|
261
|
+
end
|
|
262
|
+
config.logger.error("SolidJobs #{name} did not stop; abandoning it")
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
216
267
|
def remote_signal
|
|
217
268
|
config.redis_pool.call("RPOP", "#{identity}-signals")
|
|
218
269
|
end
|
data/lib/solid_jobs/version.rb
CHANGED
data/lib/solid_jobs.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid-jobs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolas Vandenbogaerde
|
|
@@ -60,7 +60,7 @@ dependencies:
|
|
|
60
60
|
version: '1.0'
|
|
61
61
|
- - ">="
|
|
62
62
|
- !ruby/object:Gem::Version
|
|
63
|
-
version: 1.0.
|
|
63
|
+
version: 1.0.11
|
|
64
64
|
type: :runtime
|
|
65
65
|
prerelease: false
|
|
66
66
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -70,27 +70,7 @@ dependencies:
|
|
|
70
70
|
version: '1.0'
|
|
71
71
|
- - ">="
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: 1.0.
|
|
74
|
-
- !ruby/object:Gem::Dependency
|
|
75
|
-
name: solid-resp-ractor
|
|
76
|
-
requirement: !ruby/object:Gem::Requirement
|
|
77
|
-
requirements:
|
|
78
|
-
- - "~>"
|
|
79
|
-
- !ruby/object:Gem::Version
|
|
80
|
-
version: '0.1'
|
|
81
|
-
- - ">="
|
|
82
|
-
- !ruby/object:Gem::Version
|
|
83
|
-
version: 0.1.3
|
|
84
|
-
type: :runtime
|
|
85
|
-
prerelease: false
|
|
86
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
87
|
-
requirements:
|
|
88
|
-
- - "~>"
|
|
89
|
-
- !ruby/object:Gem::Version
|
|
90
|
-
version: '0.1'
|
|
91
|
-
- - ">="
|
|
92
|
-
- !ruby/object:Gem::Version
|
|
93
|
-
version: 0.1.3
|
|
73
|
+
version: 1.0.11
|
|
94
74
|
- !ruby/object:Gem::Dependency
|
|
95
75
|
name: minitest
|
|
96
76
|
requirement: !ruby/object:Gem::Requirement
|