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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f95c0daf98dbfa77203eaa6d182f0b0a5ce4c22738a29f7d81b587c9f3abe3a
4
- data.tar.gz: bf5524aa572a45f439732ec30e5ad12d5ad25b37a98462381d39bd90b5dd7c91
3
+ metadata.gz: 7376e4285faba9c133e2d4b876f740e1c2a42f6ae571a3dad9a36a76bfc4fc37
4
+ data.tar.gz: 85c9e84ff0d75fb355ecfdd89df30d88ee2406d4807f2774d2baf19069839447
5
5
  SHA512:
6
- metadata.gz: cecbfb4efd3abe1598e66b1764ca07da25e4d103c5e59b66c14f84f5f8f3af6b45e91710d535b0b15ee69636a9396629a0e980451202f390a8aebb8a6b41091b
7
- data.tar.gz: cf40a09b495d1ea62e0789ea8688b530b26bfc7414e77cd2d4cc30f8c874c1a8f6c19b20f2eb2c493c8ca27bbeb2a319a62024dff639d6748d720b1ab13c04d6
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
+ ![SolidTrace dashboard](docs/images/solid-trace.jpg)
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.*
@@ -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 = @channels.reject do |key|
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: @processor_id.to_s,
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: @processor_id.to_s,
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.uuid
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, source.delete_prefix("#{Keyspace::PREFIX}:channel:"))
278
+ payload && record(payload, @channel_names.fetch(source))
276
279
  end
277
280
 
278
281
  def claim_now(source)
279
- claim_token = SecureRandom.uuid
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
- source.delete_prefix("#{Keyspace::PREFIX}:channel:"),
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
@@ -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
- raise InvalidJobError, "Task arguments must be an Array" unless envelope["arguments"].is_a?(Array)
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
- operation = -> { task.execute_task(*envelope.fetch("arguments")) }
29
- return operation.call if config.execute_interceptors.empty?
29
+ interceptors = config.execute_interceptors
30
+ return task.execute_task(*arguments) if interceptors.empty?
30
31
 
31
- config.execute_interceptors.call(Execution.new(task, envelope), &operation)
32
+ interceptors.call(Execution.new(task, envelope)) { task.execute_task(*arguments) }
32
33
  end
33
34
  end
34
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidJobs
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
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.0
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.11
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.11
73
+ version: 1.0.12
74
74
  - !ruby/object:Gem::Dependency
75
75
  name: minitest
76
76
  requirement: !ruby/object:Gem::Requirement