solid-jobs 0.4.0 → 0.4.1
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 +7 -0
- data/README.md +63 -0
- data/lib/solid_jobs/claim.rb +17 -9
- data/lib/solid_jobs/executor.rb +5 -4
- data/lib/solid_jobs/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7376e4285faba9c133e2d4b876f740e1c2a42f6ae571a3dad9a36a76bfc4fc37
|
|
4
|
+
data.tar.gz: 85c9e84ff0d75fb355ecfdd89df30d88ee2406d4807f2774d2baf19069839447
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 187226b5954de8767a72a826ac2804fdd58206a4c1bbf3c1decbb0dab938832f9fe88ecae89a49367d379c08e364a4266281639e508f4dead3f5213ede087cb3
|
|
7
|
+
data.tar.gz: bca45848cd5bd02011fe77816bec257dd870e5a5451ab92c4c7a3a78f0e275b443ecaf0e2699e9e87acecb78ce012fe121dcf037d53deaa6ab12539ac9842ef5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.1] - 2026-10-04
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Reduce allocations in reliable claims and task dispatch.
|
|
10
|
+
- Require SolidRedis 1.0.12 or newer.
|
|
11
|
+
|
|
5
12
|
## [0.4.0] - 2026-10-03
|
|
6
13
|
|
|
7
14
|
### Changed
|
data/README.md
CHANGED
|
@@ -193,3 +193,66 @@ absent:
|
|
|
193
193
|
The Active Job adapter remains available as
|
|
194
194
|
`ActiveJob::QueueAdapters::SolidJobsAdapter`, but it writes only SolidJobs
|
|
195
195
|
envelopes and keys.
|
|
196
|
+
|
|
197
|
+
## Monitoring with SolidTrace
|
|
198
|
+
|
|
199
|
+
**SolidTrace** is a statistics and observability tool for SolidJobs. It shows
|
|
200
|
+
real-time SolidJobs metrics while your application runs: jobs per second, CPU
|
|
201
|
+
and memory usage per Ractor, queue depth, Redis latency, job flow, and recent
|
|
202
|
+
job executions.
|
|
203
|
+
|
|
204
|
+

|
|
205
|
+
|
|
206
|
+
SolidTrace is a commercial product. If you would like to acquire it, please
|
|
207
|
+
contact me at [nicolas.vandenbogaerde@gmail.com](mailto:nicolas.vandenbogaerde@gmail.com)
|
|
208
|
+
or via GitHub ([@nicolasva](https://github.com/nicolasva)).
|
|
209
|
+
|
|
210
|
+
## Performance: SolidJobs vs Sidekiq
|
|
211
|
+
|
|
212
|
+
SolidJobs uses Ruby Ractors for parallel execution across real CPU cores, while
|
|
213
|
+
Sidekiq uses threads within a single Ruby process subject to the Global VM
|
|
214
|
+
Lock (GVL).
|
|
215
|
+
|
|
216
|
+
The benchmark suite (`benchmark_sidekiq_solid-jobs`) measures throughput and
|
|
217
|
+
resource usage under identical workloads against an isolated Redis server.
|
|
218
|
+
|
|
219
|
+
### CPU Workload (`process-cpu`)
|
|
220
|
+
|
|
221
|
+
Sidekiq remains capped by Ruby's GVL near 1 core (~100% CPU), whereas SolidJobs
|
|
222
|
+
scales linearly across cores:
|
|
223
|
+
|
|
224
|
+
| Concurrency | Sidekiq (jobs/s) | SolidJobs (jobs/s) | SolidJobs Scaling | Sidekiq CPU | SolidJobs CPU |
|
|
225
|
+
|---:|---:|---:|---:|---:|---:|
|
|
226
|
+
| 1 | 290 | 285 | 100.0% | 96.4% | 94.8% |
|
|
227
|
+
| 2 | 299 | 559 | 98.2% | 100.1% | 189.4% |
|
|
228
|
+
| 4 | 301 | 1,098 | 96.5% | 100.4% | 375.5% |
|
|
229
|
+
| 8 | 302 | **2,141** | **94.0%** | 100.2% | **749.7%** |
|
|
230
|
+
|
|
231
|
+
*At 8 concurrency, SolidJobs delivers **7.1× the throughput** of Sidekiq on pure CPU workloads.*
|
|
232
|
+
|
|
233
|
+
### Mixed Workload (`process-mixed` — CPU + I/O)
|
|
234
|
+
|
|
235
|
+
| Concurrency | Sidekiq (jobs/s) | SolidJobs (jobs/s) | SolidJobs Scaling |
|
|
236
|
+
|---:|---:|---:|---:|
|
|
237
|
+
| 1 | 183 | 194 | 100.0% |
|
|
238
|
+
| 2 | 372 | 420 | 108.2% |
|
|
239
|
+
| 4 | 555 | 778 | 100.2% |
|
|
240
|
+
| 8 | 585 | **1,827** | **117.6%** |
|
|
241
|
+
|
|
242
|
+
*At 8 concurrency, SolidJobs delivers **3.1× higher throughput** on mixed workloads.*
|
|
243
|
+
|
|
244
|
+
### Client Enqueue Throughput
|
|
245
|
+
|
|
246
|
+
| Concurrency | Sidekiq Enqueue (jobs/s) | SolidJobs Enqueue (jobs/s) | Sidekiq Bulk (jobs/s) | SolidJobs Bulk (jobs/s) |
|
|
247
|
+
|---:|---:|---:|---:|---:|
|
|
248
|
+
| 1 | 22,560 | 23,540 | 256,996 | 203,910 |
|
|
249
|
+
| 2 | 24,747 | 41,416 | 292,047 | 358,227 |
|
|
250
|
+
| 4 | 23,989 | 65,291 | 293,154 | 604,933 |
|
|
251
|
+
| 8 | 24,092 | **88,865** | 291,347 | **824,067** |
|
|
252
|
+
|
|
253
|
+
### Memory & Allocations
|
|
254
|
+
|
|
255
|
+
- **Peak RSS:** Comparable memory footprint (~42–44 MiB for both at 8 concurrency).
|
|
256
|
+
- **Allocations:** SolidJobs produces fewer object allocations per task (75 vs 108 allocations/job on CPU tasks, 47 vs 60 on individual enqueue), reducing GC pressure.
|
|
257
|
+
|
|
258
|
+
*Environment: Ruby 4.0.1 (arm64-darwin25); Sidekiq 8.1.7; SolidJobs 0.4.0.*
|
data/lib/solid_jobs/claim.rb
CHANGED
|
@@ -137,10 +137,15 @@ module SolidJobs
|
|
|
137
137
|
@channels = config.channel_entries.flat_map do |name, weight|
|
|
138
138
|
Array.new(weight, Keyspace.channel(name))
|
|
139
139
|
end.freeze
|
|
140
|
+
@channel_names = @channels.to_h do |key|
|
|
141
|
+
[key, key.delete_prefix("#{Keyspace::PREFIX}:channel:")]
|
|
142
|
+
end.freeze
|
|
143
|
+
@available_channels = @channels
|
|
140
144
|
@claimed_key = Keyspace.claimed(identity, processor_id) if @reliable && identity
|
|
141
145
|
@claims_key = Keyspace.claims(identity) if @claimed_key
|
|
142
146
|
@identity = identity
|
|
143
147
|
@processor_id = processor_id
|
|
148
|
+
@executor_field = processor_id.to_s
|
|
144
149
|
@paused = []
|
|
145
150
|
@paused_refresh_at = 0.0
|
|
146
151
|
@channel_index = 0
|
|
@@ -156,9 +161,7 @@ module SolidJobs
|
|
|
156
161
|
end
|
|
157
162
|
|
|
158
163
|
refresh_paused
|
|
159
|
-
channels = @
|
|
160
|
-
@paused.include?(key.delete_prefix("#{Keyspace::PREFIX}:channel:"))
|
|
161
|
-
end
|
|
164
|
+
channels = @available_channels
|
|
162
165
|
return sleep(0.05) if channels.empty?
|
|
163
166
|
|
|
164
167
|
if @claimed_key
|
|
@@ -190,7 +193,7 @@ module SolidJobs
|
|
|
190
193
|
redis_pool: @redis_pool,
|
|
191
194
|
claimed_key: @claimed_key,
|
|
192
195
|
claims_key: @claims_key,
|
|
193
|
-
executor_field: @
|
|
196
|
+
executor_field: @executor_field,
|
|
194
197
|
claim_token: claim_token,
|
|
195
198
|
attempt: attempt,
|
|
196
199
|
)
|
|
@@ -209,7 +212,7 @@ module SolidJobs
|
|
|
209
212
|
redis_pool: @redis_pool,
|
|
210
213
|
claimed_key: @claimed_key,
|
|
211
214
|
claims_key: @claims_key,
|
|
212
|
-
executor_field: @
|
|
215
|
+
executor_field: @executor_field,
|
|
213
216
|
claim_token: metadata.fetch("claim_token"),
|
|
214
217
|
attempt: Integer(metadata.fetch("attempt")),
|
|
215
218
|
)
|
|
@@ -221,7 +224,7 @@ module SolidJobs
|
|
|
221
224
|
def register(envelope, channel)
|
|
222
225
|
return [nil, nil] unless @claimed_key
|
|
223
226
|
|
|
224
|
-
claim_token = SecureRandom.
|
|
227
|
+
claim_token = SecureRandom.hex(16)
|
|
225
228
|
metadata = JSON.generate(
|
|
226
229
|
"task_id" => envelope["id"],
|
|
227
230
|
"claim_token" => claim_token,
|
|
@@ -272,11 +275,11 @@ module SolidJobs
|
|
|
272
275
|
TIMEOUT,
|
|
273
276
|
"BLMOVE", source, @claimed_key, "RIGHT", "LEFT", TIMEOUT,
|
|
274
277
|
)
|
|
275
|
-
payload && record(payload,
|
|
278
|
+
payload && record(payload, @channel_names.fetch(source))
|
|
276
279
|
end
|
|
277
280
|
|
|
278
281
|
def claim_now(source)
|
|
279
|
-
claim_token = SecureRandom.
|
|
282
|
+
claim_token = SecureRandom.hex(16)
|
|
280
283
|
claimed_at = Time.now.to_f
|
|
281
284
|
result = begin
|
|
282
285
|
@redis_pool.call(
|
|
@@ -298,7 +301,7 @@ module SolidJobs
|
|
|
298
301
|
payload, attempt = result
|
|
299
302
|
record(
|
|
300
303
|
payload,
|
|
301
|
-
|
|
304
|
+
@channel_names.fetch(source),
|
|
302
305
|
claim_token: claim_token,
|
|
303
306
|
attempt: Integer(attempt),
|
|
304
307
|
)
|
|
@@ -309,6 +312,11 @@ module SolidJobs
|
|
|
309
312
|
return if now < @paused_refresh_at
|
|
310
313
|
|
|
311
314
|
@paused = Array(@redis_pool.call("SMEMBERS", Keyspace::PAUSED_CHANNELS))
|
|
315
|
+
@available_channels = if @paused.empty?
|
|
316
|
+
@channels
|
|
317
|
+
else
|
|
318
|
+
@channels.reject { |key| @paused.include?(@channel_names.fetch(key)) }
|
|
319
|
+
end
|
|
312
320
|
@paused_refresh_at = now + 5
|
|
313
321
|
end
|
|
314
322
|
end
|
data/lib/solid_jobs/executor.rb
CHANGED
|
@@ -18,17 +18,18 @@ module SolidJobs
|
|
|
18
18
|
|
|
19
19
|
task_name = envelope["task"]
|
|
20
20
|
raise InvalidJobError, "Task must be present" unless task_name.is_a?(String) && !task_name.empty?
|
|
21
|
-
|
|
21
|
+
arguments = envelope["arguments"]
|
|
22
|
+
raise InvalidJobError, "Task arguments must be an Array" unless arguments.is_a?(Array)
|
|
22
23
|
unless envelope["channel"].is_a?(String) && !envelope["channel"].empty?
|
|
23
24
|
raise InvalidJobError, "Task channel must be present"
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
task = Utilities.constantize(task_name).new
|
|
27
28
|
task.task_id = envelope["id"] if task.respond_to?(:task_id=)
|
|
28
|
-
|
|
29
|
-
return
|
|
29
|
+
interceptors = config.execute_interceptors
|
|
30
|
+
return task.execute_task(*arguments) if interceptors.empty?
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
interceptors.call(Execution.new(task, envelope)) { task.execute_task(*arguments) }
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
end
|
data/lib/solid_jobs/version.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.4.
|
|
4
|
+
version: 0.4.1
|
|
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.12
|
|
64
64
|
type: :runtime
|
|
65
65
|
prerelease: false
|
|
66
66
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -70,7 +70,7 @@ dependencies:
|
|
|
70
70
|
version: '1.0'
|
|
71
71
|
- - ">="
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: 1.0.
|
|
73
|
+
version: 1.0.12
|
|
74
74
|
- !ruby/object:Gem::Dependency
|
|
75
75
|
name: minitest
|
|
76
76
|
requirement: !ruby/object:Gem::Requirement
|