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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce7c73e437bf4a78efbf9422e4a5cb7cf05635b5f3c9cbca4cd44a9baa0ee2d9
4
- data.tar.gz: f0b4513d2c4d75bc9f871ba2c64c7848789747cf797e32044d56eefdab961647
3
+ metadata.gz: 52c2c82a111ab574881c9efe6f5c692c145b50ad85542844e34de74a44860a8e
4
+ data.tar.gz: 7a23ba9ea19c901c7c888680351b018b8f554cac0192a17195682aaa3725bd82
5
5
  SHA512:
6
- metadata.gz: e6d245f323c4cdf117f2bb53584eed2e2c697a72fdc170792954d25feccb15b32d2cae7f3d8a337e7b29c3fedb071ab4ad9b773ac414fe893e389754532ca5af
7
- data.tar.gz: 9abce08d1c4fb800a5f39e56fcebc266a7cc1dd52acd891fdaccf9fd1f2840ead4a3c4ce0a5649acc6c27a8f26ad5554d0599a5eb55f8e742afc568a307ddc09
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
- ## [Unreleased]
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
- [Unreleased]: https://github.com/nicolasva/solid-jobs/compare/v0.1.0...HEAD
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 210,000-iteration integer workload, and 1,000 jobs per case:
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 | 142 | 138 | 100.0% | 6.95 | 7.11 | 41.5 MiB | 72.0 MiB | 129.8 | 89.9 |
139
- | 2 | 144 | 272 | 98.7% | 6.94 | 7.16 | 41.8 MiB | 77.6 MiB | 117.5 | 81.3 |
140
- | 4 | 143 | 539 | 97.8% | 6.99 | 7.18 | 41.9 MiB | 71.4 MiB | 111.9 | 77.2 |
141
- | 8 | 144 | 957 | 86.7% | 6.94 | 8.01 | 42.2 MiB | 69.5 MiB | 108.7 | 75.4 |
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 957 jobs/s versus 144 jobs/s
144
- for one Sidekiq process. This is Ractor parallelism rather than equal CPU
145
- efficiency: SolidJobs consumes 766% CPU and 8.01 CPU-seconds per 1,000 jobs,
146
- while Sidekiq consumes 100% CPU and 6.94 CPU-seconds per 1,000 jobs. SolidJobs
147
- also uses more RSS, but reaches 13.77 jobs/s/MiB versus 3.41 for Sidekiq.
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: concurrent TCP/RESP
153
- initialization triggered a reproducible native crash on the tested Apple
154
- Silicon environment. Ruby 4.0.1 passed the equivalent reproducer 100/100
155
- times, and `StartupBarrier` serializes component initialization before
156
- releasing normal parallel processing.
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.
@@ -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 { |ractor| RactorSupport.value(ractor) }
201
- RactorSupport.value(@scheduler) if @scheduler
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
- RactorSupport.value(@heartbeat) if @heartbeat
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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidJobs
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
6
-
data/lib/solid_jobs.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  require "base_service"
4
4
  require "callback_collection"
5
5
  require "solid_redis"
6
- require "solid_resp_ractor"
7
6
 
8
7
  require_relative "solid_jobs/version"
9
8
  require_relative "solid_jobs/errors"
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.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.10
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.10
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