solid-jobs 0.4.2 → 0.6.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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +540 -10
- data/docs/reliability.md +5 -0
- data/lib/solid_jobs/backends/base.rb +58 -0
- data/lib/solid_jobs/backends/redis.rb +80 -0
- data/lib/solid_jobs/backends/solid_engine.rb +288 -0
- data/lib/solid_jobs/backends/worker.rb +166 -0
- data/lib/solid_jobs/blueprint.rb +126 -3
- data/lib/solid_jobs/claim.rb +74 -25
- data/lib/solid_jobs/conductor.rb +145 -6
- data/lib/solid_jobs/engine.rb +63 -7
- data/lib/solid_jobs/failure_policy.rb +30 -2
- data/lib/solid_jobs/heartbeat.rb +74 -20
- data/lib/solid_jobs/instrumentation.rb +75 -0
- data/lib/solid_jobs/keyspace.rb +5 -0
- data/lib/solid_jobs/observations.rb +177 -0
- data/lib/solid_jobs/publication_barrier.rb +46 -0
- data/lib/solid_jobs/publisher.rb +53 -8
- data/lib/solid_jobs/recovery.rb +31 -2
- data/lib/solid_jobs/task.rb +35 -0
- data/lib/solid_jobs/version.rb +1 -1
- data/lib/solid_jobs.rb +15 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 926751135fceec06e8dc722a4e69fc2397b5abdbccc2b555ec9ffddd064e30ca
|
|
4
|
+
data.tar.gz: a5e48fa286ed78aa8d568de6946ae139c3d1a1f125e5090b17062cca61f46d28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4290b8368db3b6ae96393dd9437fdf39b98b996b7363a9e9929cffd75cb9539e393ddbcdfa6d37d01dc4a36eaa1439054792ecaa032d41341735c5cb76fcff25
|
|
7
|
+
data.tar.gz: 706dc9eae83cfee673b202bfe7ce098837f5726ef65206c62aaba5bc3c1a6cdd15cd9461a4061a07c525397c6da41a23d9f512064ff9170a31f8ba75039cccc1
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.6.0] - 2026-10-19
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Real-process reliability tests for SolidEngine (worker SIGKILL, lost ACK, server
|
|
12
|
+
restart, clean stop); `Backends::Worker` now survives connection errors while
|
|
13
|
+
settling a job and lets the lease expire for redelivery.
|
|
14
|
+
- Backend adapters: `Backends::Base` contract, `Backends::Redis` and
|
|
15
|
+
`Backends::SolidEngine`; `config.backends`, `config.channel_backends` and
|
|
16
|
+
per-channel routing in `Publisher` and `Conductor`.
|
|
17
|
+
- `config.execution_mode` (`:ractor` default, or `:thread`): non-Redis backend
|
|
18
|
+
workers run in Ractors, each with its own client, so the backend no longer
|
|
19
|
+
determines CPU parallelism. Ruby 4 is recommended: on 3.4.4 Ractor CPU cost per
|
|
20
|
+
job rises sharply beyond 4 workers.
|
|
21
|
+
|
|
22
|
+
## [0.5.0] - 2026-10-12
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Warm worker routing: `enqueue` reroutes tasks holding non Ractor-safe state
|
|
27
|
+
(`Mutex`, `Thread`, `Fiber`, pools) to persistent Unix-socket workers
|
|
28
|
+
(`SolidJobsGenericExecutor`, `script/warm_worker.rb`).
|
|
29
|
+
|
|
5
30
|
## [0.4.2] - 2026-10-05
|
|
6
31
|
|
|
7
32
|
### Changed
|
data/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://github.com/nicolasva/solid-jobs/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/solid-jobs)
|
|
5
5
|
[](https://rubygems.org/gems/solid-jobs)
|
|
6
|
+
[](https://www.rubydoc.info/gems/solid-jobs)
|
|
6
7
|
|
|
7
8
|
SolidJobs is a Ractor-oriented Redis task runner for Ruby. It provides its own
|
|
8
9
|
task API, Redis envelope, keyspace, interceptor model, failure policy, and
|
|
@@ -12,6 +13,39 @@ SolidJobs is **not compatible with Sidekiq**. It does not read Sidekiq queues,
|
|
|
12
13
|
does not write Sidekiq payloads, and does not expose Sidekiq's job API. Existing
|
|
13
14
|
applications and queued work must be migrated explicitly.
|
|
14
15
|
|
|
16
|
+
## SolidTrace observability
|
|
17
|
+
|
|
18
|
+
SolidJobs and SolidTrace have separate, complementary roles:
|
|
19
|
+
|
|
20
|
+
- **SolidJobs** is the job runner. It enqueues and executes background tasks,
|
|
21
|
+
manages channels, retries, recovery, and worker Ractors.
|
|
22
|
+
- **SolidTrace** is the observability dashboard. It consumes structured
|
|
23
|
+
lifecycle and operational telemetry emitted by SolidJobs and presents a
|
|
24
|
+
read-only view of job activity and system health.
|
|
25
|
+
|
|
26
|
+
With SolidTrace, operators can inspect recent job executions and their
|
|
27
|
+
observed lifecycle, monitor queues and worker Ractors, and review process
|
|
28
|
+
resource usage, Redis health, telemetry freshness, and gaps that require
|
|
29
|
+
resynchronization. SolidTrace does not enqueue, execute, retry, or control
|
|
30
|
+
SolidJobs tasks; SolidJobs remains responsible for processing them. The
|
|
31
|
+
dashboard is a separate Sinatra/Rack component that can be mounted in a Rails
|
|
32
|
+
application, with host authentication and access protection supplied by that
|
|
33
|
+
application.
|
|
34
|
+
|
|
35
|
+
SolidTrace uses structured telemetry rather than job arguments or business
|
|
36
|
+
payloads. Its trace views show retained lifecycle events and calculable
|
|
37
|
+
durations, and identify missing or unavailable information instead of
|
|
38
|
+
guessing. See the [SolidTrace project](https://github.com/nicolasva/SolidTrace)
|
|
39
|
+
for more information.
|
|
40
|
+
|
|
41
|
+
### SolidTrace dashboard
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
For access to SolidTrace, contact me at [nicolas.vandenbogaerde@gmail.com](mailto:nicolas.vandenbogaerde@gmail.com).
|
|
48
|
+
|
|
15
49
|
## Installation
|
|
16
50
|
|
|
17
51
|
```ruby
|
|
@@ -143,6 +177,139 @@ application-level deduplication when duplicate effects are unsafe.
|
|
|
143
177
|
See [docs/reliability.md](docs/reliability.md) for the state machine, claim
|
|
144
178
|
fencing, recovery rules, and Ruby Ractor caveats.
|
|
145
179
|
|
|
180
|
+
## SolidEngine backend (SolidRedis or SolidEngine)
|
|
181
|
+
|
|
182
|
+
SolidJobs is the job API; the storage and execution engine sits behind a
|
|
183
|
+
backend adapter. Two adapters ship with the gem and your tasks do not change:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
Application → SolidJobs → Backend adapter ─┬─ Backends::Redis → SolidRedis → Redis
|
|
187
|
+
└─ Backends::SolidEngine → Rust server (Unix socket / TCP)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
SolidJobs.configure do |config|
|
|
192
|
+
config.backend = :redis # default for every channel
|
|
193
|
+
|
|
194
|
+
config.backends = {
|
|
195
|
+
redis: {adapter: :solid_redis, url: "redis://localhost:6379"},
|
|
196
|
+
engine: {adapter: :solid_engine, url: "unix:///var/run/solidengine.sock"},
|
|
197
|
+
# TCP also works: "tcp://127.0.0.1:7777"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Per-channel routing: run both engines side by side and migrate gradually.
|
|
201
|
+
config.channel_backends = {"critical" => :redis, "default" => :engine, "bulk" => :engine}
|
|
202
|
+
config.channels = %w[critical default bulk]
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Setting only `config.backend = :solid_engine` and `config.url = "unix://..."`
|
|
207
|
+
routes every channel to SolidEngine. Without these options nothing changes:
|
|
208
|
+
the Redis path is the unchanged default.
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
class SendEmail
|
|
212
|
+
include SolidJobs::Task
|
|
213
|
+
task_options channel: "critical"
|
|
214
|
+
def execute_task(user_id) = # ...
|
|
215
|
+
end
|
|
216
|
+
SendEmail.enqueue(42) # Redis or SolidEngine, decided by the channel
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`Conductor` starts Ractor processors for channels served by Redis and, in
|
|
220
|
+
the same process, `concurrency` workers per non-Redis backend (one connection
|
|
221
|
+
each, lease keep-alive while a task runs). If every channel is
|
|
222
|
+
routed to SolidEngine, no Ractor, heartbeat or Redis scheduler is started and
|
|
223
|
+
Redis is not required.
|
|
224
|
+
|
|
225
|
+
### Backend contract
|
|
226
|
+
|
|
227
|
+
`SolidJobs::Backends::Base` defines `enqueue`, `schedule(at:)`, `claim(channel)`,
|
|
228
|
+
`ack(claim)`, `retry_job(claim, delay:)`, `fail_job`, `get_job`, `recover`,
|
|
229
|
+
`capabilities` and `supports?`. A *claim* is the object returned by `claim`; it
|
|
230
|
+
carries the reservation and fencing tokens, so `ack` and `retry_job` take the
|
|
231
|
+
claim rather than a bare job id. Delivery is at-least-once on both adapters.
|
|
232
|
+
|
|
233
|
+
Guarantees are not hidden. Operations a backend cannot honor raise
|
|
234
|
+
`SolidJobs::Backends::NotSupported`; check `backend.supports?(:capability)`:
|
|
235
|
+
|
|
236
|
+
| Capability | SolidRedis | SolidEngine |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| At-least-once, claim/ack, retries, scheduling | yes | yes |
|
|
239
|
+
| Crash recovery | yes (node heartbeat scan) | yes (server-side lease expiry) |
|
|
240
|
+
| Deduplication by job id | no | yes (`push` of an existing id is idempotent) |
|
|
241
|
+
| Fencing tokens, lease extension | claim-token check only | yes (stale `ack`/`nack` is rejected) |
|
|
242
|
+
| `get_job` by id, `cancel` | no | yes |
|
|
243
|
+
| Durability | Redis persistence settings | `memory`, `balanced` or `strict` (server config) |
|
|
244
|
+
|
|
245
|
+
Job ids must be UUIDs on SolidEngine (the default). Channel names map to
|
|
246
|
+
engine channel ids by CRC32 unless `channel_ids:` is set in the backend spec:
|
|
247
|
+
pin the mapping in production. Retry/discard follows the task options
|
|
248
|
+
(`max_failures`, `retry_within`, `retry_delay`, `after_final_failure`);
|
|
249
|
+
`discarded` archives stay Redis-only, SolidEngine keeps dead jobs itself.
|
|
250
|
+
|
|
251
|
+
### Switching a channel
|
|
252
|
+
|
|
253
|
+
Changing a channel's backend **does not migrate jobs already queued** in the
|
|
254
|
+
old engine. Drain the old queue first (keep a worker on it until empty), then
|
|
255
|
+
switch producers, then workers. To roll back, keep a worker on the SolidEngine
|
|
256
|
+
channel until `stats` shows it is empty. Jobs must be idempotent in both
|
|
257
|
+
directions.
|
|
258
|
+
|
|
259
|
+
### Execution mode (independent of the backend)
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
SolidJobs.configure do |config|
|
|
263
|
+
config.backend = :solid_engine
|
|
264
|
+
config.execution_mode = :ractor # default; or :thread
|
|
265
|
+
config.concurrency = 4
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- `:ractor` (default): each worker is a Ractor that builds **its own**
|
|
270
|
+
SolidEngine client (own socket, no shared state, no central relay), so CPU
|
|
271
|
+
tasks run in parallel exactly as with Redis.
|
|
272
|
+
- `:thread`: workers are threads of the main Ractor, bound by the GVL. Use it as
|
|
273
|
+
an explicit fallback when a task depends on code that is not Ractor-compatible
|
|
274
|
+
(non-shareable globals, unsupported native extensions, closures as callbacks).
|
|
275
|
+
|
|
276
|
+
Redis channels always use Ractor processors. The mode is never changed silently
|
|
277
|
+
by the backend choice.
|
|
278
|
+
|
|
279
|
+
### Reliability of the SolidEngine backend
|
|
280
|
+
|
|
281
|
+
Guarantees (verified by `test/solid_engine_reliability_test.rb`, which uses real
|
|
282
|
+
`solid-engine-server` and worker processes):
|
|
283
|
+
|
|
284
|
+
- **At-least-once.** A task is acknowledged only after it succeeds. A worker that
|
|
285
|
+
dies (SIGKILL) mid-job loses its lease; the job is claimed again after
|
|
286
|
+
`lease_ms` and may run more than once. Exactly-once is **not** provided.
|
|
287
|
+
- **Fencing.** A late ACK from a reservation that was reclaimed is rejected
|
|
288
|
+
(`stale_reservation`) and ignored by the worker.
|
|
289
|
+
- **Connection errors.** An error while claiming or settling does not stop the
|
|
290
|
+
worker; an unsettled job is redelivered once its lease expires.
|
|
291
|
+
- **Clean stop.** `stop` lets the active job finish and claims nothing new.
|
|
292
|
+
- **Durability is a server setting.** With `durability = "memory"` (default) a
|
|
293
|
+
server restart loses every job. With `"strict"` or `"balanced"` and a
|
|
294
|
+
`data_dir`, confirmed jobs survive a SIGKILL of the server. The 60 s
|
|
295
|
+
retention of completed jobs is not a persistence guarantee.
|
|
296
|
+
- **Recovery delay** is roughly `lease_ms` (default 30 s) after a worker crash.
|
|
297
|
+
|
|
298
|
+
### Limits of this release
|
|
299
|
+
|
|
300
|
+
The Python (`solid-jobs-py`) and Node.js (`solid-jobs-node`) packages contain
|
|
301
|
+
the SolidEngine client but not yet this routing layer. `Catalog`, `Observations`,
|
|
302
|
+
`Console` and `IntegrityCheck` still read Redis only.
|
|
303
|
+
|
|
304
|
+
### SolidEngine benchmark
|
|
305
|
+
|
|
306
|
+
Run `bundle exec rake comparison` in `benchmark_sidekiq_solid-jobs` with
|
|
307
|
+
`SOLID_ENGINE_BIN` pointing at `solid-engine-server`; the `process-*`
|
|
308
|
+
scenarios then add a `solid-jobs-engine` client (jobs through SolidEngine in
|
|
309
|
+
`memory` durability, matching Redis without persistence; Redis only holds the
|
|
310
|
+
benchmark counters). Results: see [Performance](#performance-solidjobs-vs-sidekiq)
|
|
311
|
+
and [SolidEngine A/B on CPU tasks](#solidengine-ab-on-cpu-tasks-process-cpu).
|
|
312
|
+
|
|
146
313
|
## Node control
|
|
147
314
|
|
|
148
315
|
```ruby
|
|
@@ -195,18 +362,208 @@ The Active Job adapter remains available as
|
|
|
195
362
|
`ActiveJob::QueueAdapters::SolidJobsAdapter`, but it writes only SolidJobs
|
|
196
363
|
envelopes and keys.
|
|
197
364
|
|
|
198
|
-
##
|
|
365
|
+
## Instrumentation
|
|
366
|
+
|
|
367
|
+
SolidJobs exposes an optional Ractor-local instrumenter and defaults to a
|
|
368
|
+
no-op. Any configured instrumenter must respond to `instrument(name, payload)`
|
|
369
|
+
and be Ractor-shareable before a conductor starts. Ordinary instrumentation
|
|
370
|
+
failures are logged when possible and never change publication, execution,
|
|
371
|
+
retry, acknowledgement, recovery, or shutdown behavior. Process-control
|
|
372
|
+
interruptions still propagate so shutdown can requeue in-flight work.
|
|
373
|
+
|
|
374
|
+
SolidJobs emits the v1 lifecycle events `job.enqueued`, `job.journaled`,
|
|
375
|
+
`job.reserved`, `job.started`, `job.completed`, `job.failed`,
|
|
376
|
+
`job.retry_scheduled`, `job.dead`, `job.acknowledged`, and `job.recovered`.
|
|
377
|
+
Ready jobs are not reservable until `job.enqueued` and `job.journaled` have
|
|
378
|
+
been emitted; an expiring Redis publication barrier preserves this order
|
|
379
|
+
without stranding work if a publisher exits.
|
|
380
|
+
They carry correlation identifiers and structured errors, but never task
|
|
381
|
+
arguments, business payloads, secrets, or textual logs. `node_id` is shared by
|
|
382
|
+
all Ractors in a conductor; `ractor_id` and `worker_id` are integer processor
|
|
383
|
+
identities. `reservation_id` is the reliable claim token.
|
|
384
|
+
|
|
385
|
+
The heartbeat Ractor also samples and emits `process.observed`, one
|
|
386
|
+
`ractor.observed` per logical processor, and one `redis.observed` per processor
|
|
387
|
+
every five seconds. Process observations are limited to process CPU time, RSS,
|
|
388
|
+
GC count/time, and total Ruby allocations. Sources are local Ruby/OS reads made
|
|
389
|
+
outside worker Ractors. A source that has never succeeded is `unavailable`; a
|
|
390
|
+
later failure preserves the last value as `stale` with its increasing age.
|
|
391
|
+
SolidJobs never estimates CPU, memory, GC, or allocations per Ractor.
|
|
392
|
+
|
|
393
|
+
Ractor observations expose only state, activity, integer
|
|
394
|
+
`ractor_id`/`worker_id`/`executor_id`, and current job IDs. Quiet workers report
|
|
395
|
+
`waiting`; shutdown reports `stopping` then `stopped` without inventing work.
|
|
396
|
+
Redis connection state comes from worker operations already being performed:
|
|
397
|
+
successful operations report `connected` and `SolidRedis::ConnectionError`
|
|
398
|
+
reports `disconnected`. No monitoring-only Redis command is added. Redis
|
|
399
|
+
latency, command, memory, and connection-count metrics remain explicitly
|
|
400
|
+
`unavailable` until a reliable source exists.
|
|
401
|
+
|
|
402
|
+
## Transparent Ractor-Safe Hybrid Execution Architecture
|
|
403
|
+
|
|
404
|
+
Ractors enforce a strict shared-nothing model. Referencing mutable global
|
|
405
|
+
state, legacy connection pools, or thread-based dependencies (**ActiveRecord**,
|
|
406
|
+
**ActionMailer**, or gems built on `Mutex`, `Thread` and `Fiber`) inside a
|
|
407
|
+
Ractor raises `ArgumentError: can't share mutable object`.
|
|
408
|
+
|
|
409
|
+
SolidJobs handles this with an **adaptive router** in `enqueue`: tasks that are
|
|
410
|
+
safe stay Ractor-native, and tasks that hold non-shareable state are
|
|
411
|
+
transparently forwarded to a persistent **warm worker** process. You do not
|
|
412
|
+
have to rewrite your gem stack.
|
|
413
|
+
|
|
414
|
+
### How it works
|
|
415
|
+
|
|
416
|
+

|
|
417
|
+
|
|
418
|
+
```text
|
|
419
|
+
Task.enqueue(*args)
|
|
420
|
+
│
|
|
421
|
+
├─ requires_warm_pool? = false ─► Ractor-native path (Redis → worker Ractors)
|
|
422
|
+
│
|
|
423
|
+
└─ requires_warm_pool? = true ──► JSON payload ─► SolidJobsGenericExecutor
|
|
424
|
+
─► UNIXSocket ─► script/warm_worker.rb
|
|
425
|
+
(booted Rails process, runs the task)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
| Context | Path | Typical tasks |
|
|
429
|
+
|---|---|---|
|
|
430
|
+
| Pure / stateless | Ractor-native, runs on all cores | CPU work, stateless I/O |
|
|
431
|
+
| Holds a `Mutex`, `Thread`, `Fiber` or pool in class-level state | Warm worker over a Unix socket | Legacy gems, shared connection pools |
|
|
432
|
+
|
|
433
|
+
The warm worker boots your application once and stays resident, so a routed job
|
|
434
|
+
pays no per-job boot cost, only a local socket round trip. The latency has not
|
|
435
|
+
been benchmarked.
|
|
436
|
+
|
|
437
|
+
### How routing is decided
|
|
438
|
+
|
|
439
|
+
`requires_warm_pool?(arguments)` returns `true` only when **all** of these hold:
|
|
440
|
+
|
|
441
|
+
1. `defined?(Rails)` is truthy.
|
|
442
|
+
2. At least one argument is not `Ractor.shareable?`.
|
|
443
|
+
3. The task class holds, in a class-level instance variable, a `Mutex`,
|
|
444
|
+
`Thread` or `Fiber`, or an object whose class name contains `Pool`.
|
|
445
|
+
|
|
446
|
+
Otherwise `enqueue` runs the normal Ractor-native path. When routed, the
|
|
447
|
+
arguments are converted to plain JSON data (deep-copied and frozen) and enqueued
|
|
448
|
+
as a `SolidJobsGenericExecutor` task, which forwards them over a Unix socket.
|
|
449
|
+
|
|
450
|
+
> Detection is heuristic and inspects only class-level instance variables. It
|
|
451
|
+
> does not detect constants such as `ActiveRecord::Base` referenced inside
|
|
452
|
+
> `execute_task`. Arguments must be JSON-serializable, and a task with
|
|
453
|
+
> shareable arguments always stays Ractor-native.
|
|
454
|
+
|
|
455
|
+
### Components
|
|
456
|
+
|
|
457
|
+
- `app/jobs/solid_jobs_generic_executor.rb` — `SolidJobsGenericExecutor`
|
|
458
|
+
connects to `/tmp/solid_jobs_rails_<worker_id>.sock`, sends
|
|
459
|
+
`{"target_class": ..., "arguments": [...]}` as one JSON line, and returns the
|
|
460
|
+
result or raises `SolidJobsGenericExecutor::RemoteError`. The worker is chosen
|
|
461
|
+
as `crc32(task name) % SOLID_JOBS_WARM_WORKERS` (default `1`, stable across processes). Require this file
|
|
462
|
+
explicitly (it is loaded automatically on first routing).
|
|
463
|
+
- `script/warm_worker.rb` — a standalone process, run outside any Ractor, that
|
|
464
|
+
loads `config/environment.rb` from its working directory once, listens on the
|
|
465
|
+
socket, resolves the class with `Object.const_get`, runs `execute_task` (or
|
|
466
|
+
`perform`), and replies with `{"status":"ok","result":...}` or
|
|
467
|
+
`{"status":"error","error_class":...,"message":...}`. Requests are handled one
|
|
468
|
+
at a time per worker. The socket is created with mode `0600` and removed on
|
|
469
|
+
`INT`/`TERM`.
|
|
470
|
+
|
|
471
|
+
### Running warm workers
|
|
472
|
+
|
|
473
|
+
Warm workers are **not** spawned by `solid-jobs`; start and supervise them
|
|
474
|
+
yourself (systemd, foreman, Procfile, ...), one per worker id, from the Rails
|
|
475
|
+
root:
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
SOLID_JOBS_WARM_WORKERS=2 bundle exec ruby script/warm_worker.rb 0 &
|
|
479
|
+
SOLID_JOBS_WARM_WORKERS=2 bundle exec ruby script/warm_worker.rb 1 &
|
|
480
|
+
bundle exec solid-jobs --require ./config/environment --concurrency 8 --channel critical
|
|
481
|
+
```
|
|
199
482
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
483
|
+
The worker count used by the runner must match the number of workers started.
|
|
484
|
+
The boot cost is paid once per worker; the process then stays warm. Memory is
|
|
485
|
+
reclaimed only when a worker is restarted, so supervise and recycle them as you
|
|
486
|
+
would any long-lived Rails process.
|
|
204
487
|
|
|
205
|
-
|
|
488
|
+
### Usage
|
|
206
489
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
490
|
+
Include the task module as usual; the only requirement is that the class-level
|
|
491
|
+
state that makes the task unsafe is visible to the detector, and that your
|
|
492
|
+
arguments are JSON-serializable:
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
class SyncAccounts
|
|
496
|
+
include SolidJobs::Task
|
|
497
|
+
@lock = Mutex.new # class-level non-shareable state => warm worker routing
|
|
498
|
+
|
|
499
|
+
def execute_task(account_ids)
|
|
500
|
+
Account.where(id: account_ids).find_each(&:sync!)
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
SyncAccounts.enqueue([1, 2, 3]) # routed to the warm worker
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Tests live in `test/warm_pool_test.rb` and spawn a real worker process.
|
|
508
|
+
|
|
509
|
+
### Hybrid architecture benchmark: Ractor vs Unix-socket IPC
|
|
510
|
+
|
|
511
|
+
Run with `bundle exec rake hybrid` in `benchmark_sidekiq_solid-jobs`
|
|
512
|
+
(Ruby 4.0.7, 4 Conductor Ractors, 4 warm workers, 10,000 mutable tasks per
|
|
513
|
+
mode, all routed to the warm pool). Each latency series is stored as its own
|
|
514
|
+
microsecond array and histogram, in JSON and Markdown under `results/`.
|
|
515
|
+
|
|
516
|
+
- **Ractor pure:** time inside a Conductor Ractor between the `job.reserved`
|
|
517
|
+
event (payload returned by Redis) and `job.started` (just before the task
|
|
518
|
+
runs). SolidJobs reserves with an atomic Lua `RPOP`, not `BLPOP`, so this is
|
|
519
|
+
the closest equivalent.
|
|
520
|
+
- **IPC roundtrip:** `UNIXSocket` open → status line read, reported by the
|
|
521
|
+
`warm_pool.roundtrip` instrumentation event of `SolidJobsGenericExecutor`.
|
|
522
|
+
- `transport` uses zero-work warm tasks to isolate the pure socket cost;
|
|
523
|
+
`sustained` paces the 10,000 tasks linearly over 30 s; `burst` enqueues them
|
|
524
|
+
at once. In `sustained` and `burst`, each warm task sleeps 5 ms to simulate
|
|
525
|
+
service time, so their IPC roundtrip is dominated by that work (macOS timer
|
|
526
|
+
granularity makes it ≈6.4 ms).
|
|
527
|
+
|
|
528
|
+
| Mode | Series | p50 (µs) | p95 (µs) | p99 (µs) |
|
|
529
|
+
|---|---|---:|---:|---:|
|
|
530
|
+
| Transport | Ractor pure | 9 | 21 | 39 |
|
|
531
|
+
| Transport | IPC roundtrip (no work) | 43 | 77 | 192 |
|
|
532
|
+
| Sustained 30 s (≈333 jobs/s) | Ractor pure | 25 | 51 | 119 |
|
|
533
|
+
| Sustained 30 s (≈333 jobs/s) | IPC roundtrip (5 ms work) | 6,355 | 11,713 | 12,680 |
|
|
534
|
+
| Burst | Ractor pure | 23 | 58 | 249 |
|
|
535
|
+
| Burst | IPC roundtrip (5 ms work) | 6,426 | 12,522 | 13,183 |
|
|
536
|
+
|
|
537
|
+
All 30,000 tasks completed with zero failures and **zero warm pool exhaustion
|
|
538
|
+
events**: the 4 Conductor Ractors never have more requests in flight than there
|
|
539
|
+
are warm workers. The pure socket overhead is tens of microseconds (p50 43 µs,
|
|
540
|
+
p99 192 µs); the rest of the IPC latency is the task's own work in the warm
|
|
541
|
+
worker.
|
|
542
|
+
|
|
543
|
+
**Saturation (oversubscribed pool).** With 8 Ractors on a single warm worker, a
|
|
544
|
+
30 ms timeout and 1,000 burst tasks, the pool is exhausted: 432 timeouts and 563
|
|
545
|
+
refused connections (the worker's Unix socket listen backlog is full), only 5
|
|
546
|
+
tasks completed on the first attempt, and the rest failed and went through
|
|
547
|
+
SolidJobs retries. Exhaustion is counted as `timeout` + `refused` + `ok`
|
|
548
|
+
roundtrips above `HYBRID_EXHAUSTION_MS`. Size the number of warm workers at
|
|
549
|
+
least equal to the Conductor concurrency, and set `SOLID_JOBS_WARM_TIMEOUT`
|
|
550
|
+
(default 30 s) accordingly. A timed-out request is not cancelled in the worker,
|
|
551
|
+
so make routed tasks idempotent.
|
|
552
|
+
|
|
553
|
+
*Environment: Ruby 4.0.7 (arm64-darwin27); SolidJobs 0.5.0; local loopback,
|
|
554
|
+
synthetic work, single run per mode.*
|
|
555
|
+
|
|
556
|
+
## Commercial licensing & observability
|
|
557
|
+
|
|
558
|
+
`SolidJobs` is open source under the **LGPL-3.0-or-later** license.
|
|
559
|
+
|
|
560
|
+
Running multi-core clusters blind in staging or production makes monitoring
|
|
561
|
+
hard. For visual insight into Ractor memory allocation, CPU thresholds, and live
|
|
562
|
+
queue states, pair this gem with **SolidTrace** (commercial licenses available
|
|
563
|
+
from $50 to $200 / month).
|
|
564
|
+
|
|
565
|
+
For commercial keys, custom architecture audits, or enterprise support, contact
|
|
566
|
+
**licensing@solidtrace.io**.
|
|
210
567
|
|
|
211
568
|
## Performance: SolidJobs vs Sidekiq
|
|
212
569
|
|
|
@@ -229,7 +586,151 @@ scales linearly across cores:
|
|
|
229
586
|
| 4 | 303 | 1,094 | 96.9% | 100.3% | 378.2% |
|
|
230
587
|
| 8 | 302 | **2,170** | **96.2%** | 100.1% | **761.0%** |
|
|
231
588
|
|
|
232
|
-
*At 8 concurrency, SolidJobs delivers **7.2× the throughput** of Sidekiq on pure CPU workloads
|
|
589
|
+
*At 8 concurrency, SolidJobs delivers **7.2× the throughput** of a single Sidekiq process on pure CPU workloads
|
|
590
|
+
(one process, N threads; see the multi-process comparison below).
|
|
591
|
+
This table comes from the Ruby 4.x campaign (see the environment line at the end
|
|
592
|
+
of this section); it predates the A/B campaign below and is not directly comparable with it.*
|
|
593
|
+
|
|
594
|
+
### SolidEngine A/B on CPU tasks (`process-cpu`)
|
|
595
|
+
|
|
596
|
+
Same tasks and repetitions for every client; median of 6 runs (min–max), jobs/s.
|
|
597
|
+
Sidekiq is one process with N threads. Machine: 12 cores (8 performance).
|
|
598
|
+
|
|
599
|
+
**Ruby 4.0.7** (separate campaign: `ruby-4.0.7-process-cpu-procs.md`)
|
|
600
|
+
|
|
601
|
+
"Sidekiq (N procs)" is N Sidekiq processes with one thread each, i.e. the same
|
|
602
|
+
number of cores as SolidJobs' N Ractors.
|
|
603
|
+
|
|
604
|
+
| Concurrency | Sidekiq (1 proc, N threads) | Sidekiq (N procs) | SolidJobs + Redis (Ractors) | SolidJobs + SolidEngine (Ractors) |
|
|
605
|
+
|---:|---:|---:|---:|---:|
|
|
606
|
+
| 1 | 287 (283–288) | 286 (278–288) | 269 (260–271) | 286 (284–289) |
|
|
607
|
+
| 2 | 297 (296–298) | 563 (561–564) | 516 (510–517) | **568** (566–570) |
|
|
608
|
+
| 4 | 298 (295–300) | **1,107** (1,104–1,127) | 953 (947–964) | 1,103 (1,100–1,117) |
|
|
609
|
+
| 8 | 299 (298–301) | **2,156** (2,145–2,161) | 1,667 (1,644–1,691) | 2,058 (2,049–2,086) |
|
|
610
|
+
|
|
611
|
+
Ruby-worker CPU-seconds per 1,000 jobs: 3.4–3.6 for every client at every concurrency.
|
|
612
|
+
|
|
613
|
+
Whole-infrastructure memory and CPU (separate run of the same workload,
|
|
614
|
+
`ruby-4.0.7-process-cpu-memory.md`; peak physical footprint measured with macOS
|
|
615
|
+
`footprint` during the run, workers + Redis + SolidEngine server):
|
|
616
|
+
|
|
617
|
+
| Concurrency | Client | jobs/s | Workers MiB | Redis MiB | SolidEngine MiB | Total MiB | MiB per 1,000 jobs/s | Total CPU-s/1,000 jobs |
|
|
618
|
+
|---:|---|---:|---:|---:|---:|---:|---:|---:|
|
|
619
|
+
| 4 | Sidekiq (N procs) | 1,090 | 138.0 | 5.8 | – | **143.9** | 132 | 3.55 |
|
|
620
|
+
| 4 | SolidJobs + Redis | 954 | 32.3 | 5.8 | – | **38.1** | 40 | 3.55 |
|
|
621
|
+
| 4 | SolidJobs + SolidEngine | 1,101 | 54.7 | 5.8 | 6.0 | **66.6** | 61 | 3.51 |
|
|
622
|
+
| 8 | Sidekiq (N procs) | 2,025 | 243.7 | 6.1 | – | **249.8** | 123 | 3.83 |
|
|
623
|
+
| 8 | SolidJobs + Redis | 1,663 | 32.4 | 6.1 | – | **38.6** | 23 | 3.65 |
|
|
624
|
+
| 8 | SolidJobs + SolidEngine | 1,968 | 55.0 | 6.1 | 6.4 | **67.6** | 34 | 3.71 |
|
|
625
|
+
|
|
626
|
+
At 8 workers SolidJobs + SolidEngine uses about 73% less physical memory than 8
|
|
627
|
+
Sidekiq processes (68 vs 250 MiB) for 3% less throughput in this run, with a
|
|
628
|
+
similar total CPU per job. Redis holds only the benchmark counters here (about
|
|
629
|
+
2,000 queued jobs), so its footprint is small; a real backlog would grow it.
|
|
630
|
+
|
|
631
|
+
**Ruby 3.4.4**
|
|
632
|
+
|
|
633
|
+
| Concurrency | Sidekiq (threads) | SolidJobs + Redis (Ractors) | SolidJobs + SolidEngine (Ractors) | SolidJobs + SolidEngine (threads) |
|
|
634
|
+
|---:|---:|---:|---:|---:|
|
|
635
|
+
| 1 | 202 (200–207) | 196 (192–199) | 209 (196–210) | 206 (205–208) |
|
|
636
|
+
| 2 | 211 (209–213) | 296 (277–319) | **327** (310–337) | 205 (202–209) |
|
|
637
|
+
| 4 | 210 (204–213) | **464** (281–497) | 401 (376–446) | 208 (203–209) |
|
|
638
|
+
| 8 | 214 (211–216) | **313** (290–317) | 303 (299–329) | 209 (207–211) |
|
|
639
|
+
|
|
640
|
+
CPU-seconds per 1,000 jobs on Ruby 3.4.4: 4.7–4.8 at concurrency 1; at concurrency 8
|
|
641
|
+
Redis Ractors 24.3, Engine Ractors 25.8, Engine threads 4.8, Sidekiq 4.7.
|
|
642
|
+
|
|
643
|
+
CPU covers the Ruby worker process only, not Redis or the SolidEngine server.
|
|
644
|
+
|
|
645
|
+
Separate conclusions:
|
|
646
|
+
|
|
647
|
+
1. **Execution model** (same SolidEngine backend): Ractors reach 2,058 jobs/s at
|
|
648
|
+
concurrency 8 on Ruby 4.0.7 against about 290 for threads (about 7×). The earlier
|
|
649
|
+
plateau was the GVL, not the server.
|
|
650
|
+
2. **Backend** (both in Ractors, Ruby 4.0.7): SolidEngine leads Redis at every
|
|
651
|
+
concurrency with non-overlapping ranges (+10% at 2, +16% at 4, +23% at 8). On
|
|
652
|
+
Ruby 3.4.4 the two are comparable and ranges overlap.
|
|
653
|
+
3. **SolidJobs vs Sidekiq:** against one Sidekiq process with N threads SolidJobs is
|
|
654
|
+
about 6.9× faster at concurrency 8, but that comparison uses one core for Sidekiq.
|
|
655
|
+
Against N Sidekiq processes (same cores) throughput is **on par**: Sidekiq is
|
|
656
|
+
within ±5% of SolidJobs + SolidEngine and ahead of SolidJobs + Redis at
|
|
657
|
+
concurrency 4–8. SolidJobs' measured advantage on this workload is memory
|
|
658
|
+
(one process with Ractors instead of N processes), not throughput.
|
|
659
|
+
4. **Ruby 3.4.4:** throughput drops and CPU per job rises sharply at concurrency 8 for
|
|
660
|
+
both backends (Ractor coordination cost in the runtime); use Ruby 4.
|
|
661
|
+
|
|
662
|
+
Not covered: Linux PSS (macOS `footprint` is used instead), memory under large
|
|
663
|
+
backlogs, more than 8 performance cores, Ruby 3.4.4 with multi-process Sidekiq.
|
|
664
|
+
Reports: `benchmark_sidekiq_solid-jobs/results/ruby-3.4.4-process-cpu-ab.md` and
|
|
665
|
+
`ruby-4.0.7-process-cpu-procs.md`.
|
|
666
|
+
|
|
667
|
+
### SolidEngine re-run (Ruby 4.0.7, all processing scenarios)
|
|
668
|
+
|
|
669
|
+
Re-run with the Rust `solid-engine-server` (release build, `memory` durability):
|
|
670
|
+
`process-cpu`, `process-io` and `process-mixed`, median of 6 runs, jobs/s, 0 errors
|
|
671
|
+
everywhere. Sidekiq is one process with N threads; "Engine (threads)" is SolidJobs +
|
|
672
|
+
SolidEngine with threads instead of Ractors.
|
|
673
|
+
|
|
674
|
+
| Scenario | Concurrency | Sidekiq | SolidJobs + Redis | SolidJobs + SolidEngine | Engine (threads) |
|
|
675
|
+
|---|---:|---:|---:|---:|---:|
|
|
676
|
+
| `process-cpu` | 1 | 280 | 268 | **285** | 283 |
|
|
677
|
+
| `process-cpu` | 2 | 292 | 516 | **565** | 288 |
|
|
678
|
+
| `process-cpu` | 4 | 296 | 956 | **1,109** | 291 |
|
|
679
|
+
| `process-cpu` | 8 | 292 | 1,635 | **2,021** | 286 |
|
|
680
|
+
| `process-io` | 1 | 156 | 160 | **166** | 153 |
|
|
681
|
+
| `process-io` | 2 | 308 | 300 | **322** | 300 |
|
|
682
|
+
| `process-io` | 4 | 625 | 594 | **653** | 616 |
|
|
683
|
+
| `process-io` | 8 | 1,221 | 1,076 | **1,242** | 1,200 |
|
|
684
|
+
| `process-mixed` | 1 | 189 | 194 | **204** | 192 |
|
|
685
|
+
| `process-mixed` | 2 | 402 | 396 | **426** | 398 |
|
|
686
|
+
| `process-mixed` | 4 | 568 | 752 | **874** | 554 |
|
|
687
|
+
| `process-mixed` | 8 | 572 | 1,471 | **1,763** | 552 |
|
|
688
|
+
|
|
689
|
+
- CPU and mixed work: Ractors scale with SolidEngine (about 6.9× Sidekiq on
|
|
690
|
+
`process-cpu` and 3.1× on `process-mixed` at concurrency 8), while threads stay
|
|
691
|
+
GVL-bound at the Sidekiq level. SolidEngine is +20% to +24% ahead of Redis at
|
|
692
|
+
concurrency 8.
|
|
693
|
+
- I/O work: all clients are within about 15% of each other because the wait
|
|
694
|
+
dominates; SolidEngine is on par with or slightly ahead of Sidekiq.
|
|
695
|
+
- Footprint at `process-cpu`, concurrency 8 (workers / Redis / SolidEngine MiB):
|
|
696
|
+
Sidekiq 28.8 / 6.0 / –; SolidJobs + Redis 32.4 / 6.0 / –; SolidJobs + SolidEngine
|
|
697
|
+
54.8 / 6.0 / 7.3. Single process, N Ractors: the Sidekiq column is one process, not N.
|
|
698
|
+
- Reports: `benchmark_sidekiq_solid-jobs/results/ruby-4.0.7-solid-engine.md` and
|
|
699
|
+
`.csv`. Command: `RBENV_VERSION=4.0.7 BENCHMARK_SCENARIOS=process-cpu,process-io,process-mixed bundle exec rake comparison`.
|
|
700
|
+
|
|
701
|
+
### SolidRedis backend re-run (solid-redis 1.0.13, Ruby 4.0.7)
|
|
702
|
+
|
|
703
|
+
Second run with the local `solid-redis` 1.0.13 checkout wired into the benchmark
|
|
704
|
+
bundle (`SOLID_REDIS_PATH`, default `../solid-redis`; the previous run resolved
|
|
705
|
+
1.0.12 from rubygems). Only Sidekiq and SolidJobs + SolidRedis (Ractors) were run
|
|
706
|
+
(144 cases, median of 6, 0 errors). The SolidEngine column is the run above, for
|
|
707
|
+
comparison.
|
|
708
|
+
|
|
709
|
+
| Scenario | Concurrency | Sidekiq | SolidJobs + SolidRedis | SolidJobs + SolidEngine |
|
|
710
|
+
|---|---:|---:|---:|---:|
|
|
711
|
+
| `process-cpu` | 1 | 282 | 266 | **285** |
|
|
712
|
+
| `process-cpu` | 2 | 281 | 490 | **565** |
|
|
713
|
+
| `process-cpu` | 4 | 292 | 946 | **1,109** |
|
|
714
|
+
| `process-cpu` | 8 | 291 | 1,603 | **2,021** |
|
|
715
|
+
| `process-io` | 1 | 154 | 160 | **166** |
|
|
716
|
+
| `process-io` | 2 | 307 | 302 | **322** |
|
|
717
|
+
| `process-io` | 4 | 604 | 566 | **653** |
|
|
718
|
+
| `process-io` | 8 | 1,172 | 998 | **1,242** |
|
|
719
|
+
| `process-mixed` | 1 | 185 | 192 | **204** |
|
|
720
|
+
| `process-mixed` | 2 | 400 | 397 | **426** |
|
|
721
|
+
| `process-mixed` | 4 | 566 | 746 | **874** |
|
|
722
|
+
| `process-mixed` | 8 | 573 | 1,468 | **1,763** |
|
|
723
|
+
|
|
724
|
+
- SolidRedis scales like SolidEngine but stays 10–20% below it on CPU and mixed work
|
|
725
|
+
(concurrency 8: 1,603 vs 2,021 on `process-cpu`, 1,468 vs 1,763 on `process-mixed`),
|
|
726
|
+
and is still 5.5× Sidekiq (threads) on `process-cpu` and 2.6× on `process-mixed`.
|
|
727
|
+
- On `process-io` SolidRedis is within about 15% of Sidekiq and below it at
|
|
728
|
+
concurrency 4–8 (998 vs 1,172 at 8); SolidEngine matches or beats Sidekiq.
|
|
729
|
+
- The SolidEngine column comes from a separate run on the same machine, so
|
|
730
|
+
differences of a few percent are within run-to-run noise.
|
|
731
|
+
- Reports: `benchmark_sidekiq_solid-jobs/results/ruby-4.0.7-solid-redis-1.0.13.md`
|
|
732
|
+
and `.csv`. Command:
|
|
733
|
+
`RBENV_VERSION=4.0.7 BENCHMARK_CLIENTS=sidekiq,solid-jobs BENCHMARK_SCENARIOS=process-cpu,process-io,process-mixed bundle exec rake comparison`.
|
|
233
734
|
|
|
234
735
|
### Mixed Workload (`process-mixed` — CPU + I/O)
|
|
235
736
|
|
|
@@ -251,6 +752,35 @@ scales linearly across cores:
|
|
|
251
752
|
| 4 | 593 | **604** | 50 |
|
|
252
753
|
| 8 | 1,121 | **1,133** | 52 |
|
|
253
754
|
|
|
755
|
+
### Stress test: concurrency 4, 8 and 16
|
|
756
|
+
|
|
757
|
+
Run with `bundle exec rake stress` (5,000 pre-seeded jobs per case, medians of
|
|
758
|
+
3 runs; `STRESS_CONCURRENCY`, `STRESS_REPETITIONS` and `STRESS_JOBS` override
|
|
759
|
+
the defaults). It writes a Markdown report and a CSV for plotting curves.
|
|
760
|
+
|
|
761
|
+
| Workload | Concurrency | Sidekiq (jobs/s) | SolidJobs (jobs/s) | Ratio | Sidekiq CPU | SolidJobs CPU |
|
|
762
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
763
|
+
| `process-cpu` | 4 | 295 | 1,021 | 3.5× | 100% | 358% |
|
|
764
|
+
| `process-cpu` | 8 | 288 | 1,916 | 6.7× | 100% | 683% |
|
|
765
|
+
| `process-cpu` | 16 | 293 | 2,117 | 7.2× | 100% | 875% |
|
|
766
|
+
| `process-mixed` | 4 | 557 | 792 | 1.4× | 99% | 144% |
|
|
767
|
+
| `process-mixed` | 8 | 567 | 1,656 | 2.9× | 101% | 305% |
|
|
768
|
+
| `process-mixed` | 16 | 568 | 3,073 | 5.4× | 101% | 590% |
|
|
769
|
+
| `process-io` | 4 | 595 | 574 | 0.96× | 21% | 28% |
|
|
770
|
+
| `process-io` | 8 | 1,177 | 1,053 | 0.89× | 31% | 43% |
|
|
771
|
+
| `process-io` | 16 | 2,295 | 1,932 | 0.84× | 48% | 75% |
|
|
772
|
+
|
|
773
|
+
- Sidekiq stays flat near one core on CPU and mixed work at every level, while
|
|
774
|
+
SolidJobs keeps scaling; CPU gains flatten past 8 because the machine runs out
|
|
775
|
+
of cores.
|
|
776
|
+
- On pure I/O (sleep) work, Sidekiq's threads are slightly faster at these
|
|
777
|
+
levels and use less CPU. SolidJobs is not faster on I/O-bound jobs.
|
|
778
|
+
- Latencies in the report measure the drain of the pre-seeded backlog, not
|
|
779
|
+
steady-state latency. Per-job execution p99 stayed under 10 ms for both.
|
|
780
|
+
- Zero errors in all 54 measurements.
|
|
781
|
+
|
|
782
|
+
*Environment: Ruby 4.0.7 (arm64-darwin27); Sidekiq 8.1.7; SolidJobs 0.5.0.*
|
|
783
|
+
|
|
254
784
|
### Client Enqueue Throughput
|
|
255
785
|
|
|
256
786
|
| Concurrency | Sidekiq Enqueue (jobs/s) | SolidJobs Enqueue (jobs/s) | Sidekiq Bulk (jobs/s) | SolidJobs Bulk (jobs/s) |
|
data/docs/reliability.md
CHANGED
|
@@ -29,6 +29,11 @@ attempt monotonically increasing execution count
|
|
|
29
29
|
claimed_at wall-clock claim time
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
A ready payload has a short-lived publication barrier while its
|
|
33
|
+
`job.enqueued` and `job.journaled` events are emitted. Executors leave marked
|
|
34
|
+
payloads in READY until the publisher removes the marker. Markers expire after
|
|
35
|
+
30 seconds, so a publisher crash can delay a task but cannot strand it.
|
|
36
|
+
|
|
32
37
|
Completion and requeue are fenced by `claim_token`. Their Lua scripts verify
|
|
33
38
|
that the executor still owns the current journal generation before changing
|
|
34
39
|
state. A delayed or revived executor cannot complete or requeue a newer claim.
|