riverqueue 0.12.0 → 0.13.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 +50 -0
- data/README.md +22 -0
- data/docs/README.md +60 -13
- data/docs/conformance.md +72 -0
- data/docs/migrations.md +5 -8
- data/lib/client.rb +42 -9
- data/lib/client_runtime.rb +167 -58
- data/lib/driver/runtime.rb +215 -104
- data/lib/event.rb +8 -1
- data/lib/insert_opts.rb +7 -2
- data/lib/job.rb +1 -1
- data/lib/periodic_cron.rb +41 -6
- data/lib/periodic_job.rb +9 -1
- data/migration/README.md +4 -2
- data/migration/manifest.json +1 -2
- data/sig/client.rbs +5 -2
- data/sig/driver.rbs +6 -4
- data/sig/insert_opts.rbs +2 -2
- data/sig/job.rbs +2 -0
- data/sig/runtime.rbs +24 -4
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c22dfdcd40a71eaf7e3f9f74351b795957b2a23c7a51d5e302c7dd6d046ce3e8
|
|
4
|
+
data.tar.gz: 495748ff5de7b982d2d18c8ed6670865e5452c5e8198a457428e53c2af5ecf76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1707b4db9e48540d168af6cdd89dc98e78db8101e4cfaa1a95ede93a1e4d1bd5ab28b13e94f6ab3c106f5673dc30f4092b83d299b98a257c23384ffeb2ba170d
|
|
7
|
+
data.tar.gz: cffa8f11fda5a7b003413ed3dab643ce44168987dc2afe2fb0b6d5dccafcfbfe0e6a88264262502018e53a38f6f3c2b1a460fef8ccdfb7ce0c9c37999bfe3514
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.13.0] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Expanded checks against Go-generated conformance fixtures to cover cron schedules, snooze counters, all shared metadata keys, and queue-control and leadership notification emission through both SQL drivers. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
15
|
+
- Added `CRON_TZ=` / `TZ=` prefixes, `?` wildcards, and Go-style `@every` durations to `PeriodicCron`. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Ruby is now developed in the main [River repository](https://github.com/riverqueue/river/tree/master/ruby). Gem names and require paths are unchanged. The four public gems (`riverqueue`, `riverqueue-activerecord`, `riverqueue-sequel`, and `riverqueue-rails`) are released together. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
20
|
+
- **Breaking:** Job insertion accepts only `available`, `pending`, and `scheduled` states. `running`, `retryable`, and terminal states are rejected, including when set by insertion hooks. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
21
|
+
- **Breaking:** Insertion rejects queue names containing characters other than ASCII letters, digits, underscores, hyphens, colons, or periods. Nonzero `UniqueOpts#by_period` values must be at least one second. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
22
|
+
- **Breaking:** Attempt counts must be between 0 and 32,767, and `max_attempts` must be between 1 and 32,767. These bounds also apply on SQLite. `job_retry` raises `ArgumentError` when another attempt would exceed the limit. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
23
|
+
- **Breaking:** Job and queue metadata updates require a Hash; JSON-encoded strings are no longer accepted. Pass an empty Hash to clear metadata. Updates to `attempted_by` require string entries. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Externally claimed jobs honor worker retry hooks, including Active Job's retry policy, and use their claim timestamps for queue-wait statistics and attempt errors. Worker initialization failures are logged without losing the reported work error. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
28
|
+
- Externally reported cancellation, snooze, and interruption signals follow normal worker finalization instead of being recorded as ordinary failures. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
29
|
+
- Graceful shutdown and queue removal continue observing remote cancellation until active jobs finish, including when worker timeouts are disabled or cancellation polling temporarily fails. Draining waits respect polling intervals and wake when the last attempt finishes. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
30
|
+
- Blocking lifecycle calls from the client's own runtime threads fail immediately instead of waiting for themselves. Workers can still request nonblocking stop. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
31
|
+
- Inserts reject states other than `available`, `pending`, and `scheduled`, preventing stranded `running` jobs without attempt timestamps. Insertion hooks are checked after all callbacks run, and invalid batches roll back hook writes. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
32
|
+
- The Sequel driver preserves application `ArgumentError` exceptions raised inside SQLite transactions after rolling back, including validation errors from insertion hooks. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
33
|
+
- Job inserts and updates reject attempt limits above 32,767 consistently across databases. Attempt updates and manual retries enforce the portable counter range, and claims tolerate counters already at the limit. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
34
|
+
- Wildcard queue pause and resume publish events for every affected queue, including beyond 100 queues. Events use the update's snapshots so concurrent changes cannot replace their contents or introduce unrelated queues. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
35
|
+
- Manually retrying a cancelled job clears the old cancellation request so its next attempt can complete. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
36
|
+
- Cancellation wins atomically over discarded attempts and other state transitions, including the last attempt or a worker that disables retries. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
37
|
+
- Stuck-job rescue falls back to the default retry policy per job when an application policy fails or returns an invalid time, allowing recovery and cleanup to continue. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
38
|
+
- Clients without consumer queues run configured periodic jobs and maintenance services. Adding periodic jobs starts maintenance when needed; registration is rejected when leader election is disabled. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
39
|
+
- Bulk deletion excludes running jobs before applying its limit and locks eligible PostgreSQL rows so concurrent retries cannot cause deletion of jobs that no longer match the filters. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
40
|
+
- Stuck-job rescue respects longer or disabled client and worker timeouts and scans past protected attempts without consuming the rescue limit. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
41
|
+
- Snoozing and rescue tolerate JSON counters that overflow SQLite's floating-point representation. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
42
|
+
- Snoozing tolerates nonnumeric metadata counters instead of failing on booleans or collections. Ruby-specific recovery cases are tested independently of the shared Go fixtures. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
43
|
+
- Periodic jobs now carry `periodic: true` and, when named, `river:periodic_job_id`, while preserving constructor options and application metadata. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
44
|
+
- Queue pause, resume, metadata changes, and leader resignation now broadcast notifications for clients in other languages. Notifications commit and roll back with the corresponding database change. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
45
|
+
- Cron schedules include both occurrences of repeated daylight-saving times when given a local reference time. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
46
|
+
- Job insertion rejects invalid queue names, nonpositive attempt limits, and nonzero uniqueness periods shorter than one second before writing any jobs. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
47
|
+
- Custom maintenance service failures are logged without blocking other services, stuck-job rescue, or cleanup. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
48
|
+
- Job updates reject non-object metadata before writing to the database, preserving the existing job when invalid metadata is supplied. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
49
|
+
- Job completion atomically checks for cancellation so a concurrent cancellation cannot be overwritten by completion, including when finalization hooks are configured. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
50
|
+
- Job updates reject nonpositive attempt limits consistently on PostgreSQL and SQLite. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
51
|
+
- Job lifecycle events report worker execution and completion durations separately using a monotonic clock. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
52
|
+
- Finalization hooks that delete jobs honor concurrent cancellation and record the cancelled attempt instead of deleting it. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
53
|
+
- Queue metadata writes and job metadata merges reject non-object values before changing persisted data. Job updates also reject non-string worker IDs in attempt histories. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
54
|
+
- Unique inserts with `exclude_kind` preserve the existing job's kind when a different kind conflicts, on both databases and through both drivers. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
55
|
+
- Retrying, snoozing, and interrupting jobs preserve the original queue-wait duration in lifecycle events. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
56
|
+
- Invalid rescue counters no longer prevent other stuck jobs in the batch from being rescued. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
57
|
+
- SQLite retains only the newest 100 worker IDs when claiming a job, matching PostgreSQL. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
58
|
+
- Attempt-error serialization supports frozen timestamps and preserves the caller's timezone. [PR #1463](https://github.com/riverqueue/river/pull/1463).
|
|
59
|
+
|
|
10
60
|
## [0.12.0] - 2026-10-01
|
|
11
61
|
|
|
12
62
|
### Added
|
data/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# River for Ruby
|
|
2
|
+
|
|
3
|
+
A PostgreSQL and SQLite job queue that shares River's schema with the Go, Rust,
|
|
4
|
+
and JavaScript clients. Includes Active Record and Sequel drivers, plus Rails
|
|
5
|
+
and Active Job integration.
|
|
6
|
+
|
|
7
|
+
- [Usage and configuration](docs/README.md)
|
|
8
|
+
- [Development and releases](docs/development.md)
|
|
9
|
+
- [Conformance coverage](docs/conformance.md)
|
|
10
|
+
- [Migrations](docs/migrations.md)
|
|
11
|
+
|
|
12
|
+
From the repository root:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
make -C ruby install
|
|
16
|
+
createdb river_test
|
|
17
|
+
RIVER_REQUIRE_DATABASES=1 make test/ruby
|
|
18
|
+
make lint/ruby typecheck/ruby
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Ruby 3.2 or later is required. `make test/ruby/conformance` runs only the
|
|
22
|
+
Go-generated fixture checks and needs no PostgreSQL server.
|
data/docs/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# River client for Ruby [](https://github.com/riverqueue/river/actions/workflows/ruby.yaml) [](https://badge.fury.io/rb/riverqueue)
|
|
2
2
|
|
|
3
3
|
A Ruby client for [River](https://github.com/riverqueue/river), packaged in the [`riverqueue` gem](https://rubygems.org/gems/riverqueue). It inserts and works jobs using River's canonical database schema and state machine, so Ruby and Go clients can safely share a River database. Separate queues are recommended when each language recognizes different job kinds.
|
|
4
4
|
|
|
@@ -133,6 +133,12 @@ end
|
|
|
133
133
|
|
|
134
134
|
Insertion keywords control the queue, priority, maximum attempts, schedule,
|
|
135
135
|
tags, metadata, and uniqueness of a job. Priority `1` is highest.
|
|
136
|
+
`max_attempts` must be between 1 and 32,767 on every database.
|
|
137
|
+
An explicit `state` must be `:available`, `:pending`, or `:scheduled` (strings
|
|
138
|
+
are also accepted). Other states raise `ArgumentError` before any jobs are
|
|
139
|
+
inserted, including when an invalid state appears in a batch. Without an
|
|
140
|
+
explicit state, jobs are available immediately or scheduled when `scheduled_at`
|
|
141
|
+
is in the future.
|
|
136
142
|
|
|
137
143
|
```ruby
|
|
138
144
|
result = client.insert(args,
|
|
@@ -224,7 +230,7 @@ order affect the hash even when the decoded JSON is equivalent.
|
|
|
224
230
|
|
|
225
231
|
### [Reliable execution and stuck jobs](https://riverqueue.com/docs/reliable-workers)
|
|
226
232
|
|
|
227
|
-
Claims and state transitions are atomic in the database. If a process disappears while working, the elected maintenance client
|
|
233
|
+
Claims and state transitions are atomic in the database. If a process disappears while working, the elected maintenance client checks for stale running jobs after an hour, retrying or discarding them according to their attempt count. Rescue respects longer client and worker timeouts; disabling a job's timeout also disables automatic timeout-based rescue for it. Cancellation requests still take precedence. Keep worker registrations and timeout configuration consistent across maintenance clients. `attempted_by`, attempt errors, and final state remain in the canonical River row for inspection by Ruby, Go, or River UI.
|
|
228
234
|
|
|
229
235
|
### [Job retries](https://riverqueue.com/docs/job-retries)
|
|
230
236
|
|
|
@@ -243,12 +249,18 @@ config = River::Config.new(
|
|
|
243
249
|
```
|
|
244
250
|
|
|
245
251
|
Use `client.job_retry(job_id)` to make a non-running job available immediately.
|
|
252
|
+
This clears its previous cancellation request. Once a job has reached 32,767
|
|
253
|
+
attempts, requesting another retry raises `ArgumentError` without changing it.
|
|
246
254
|
|
|
247
255
|
A worker may implement `retry?(job, error)` and return false to discard a
|
|
248
256
|
reported error immediately, without reducing the attempt budget used for crash
|
|
249
257
|
recovery. The Rails integration uses this to let Active Job own application retries.
|
|
258
|
+
Jobs claimed and finished by batch extensions honor the registered worker's
|
|
259
|
+
`retry?` and `next_retry` hooks too, including workers registered as classes.
|
|
250
260
|
If a retry hook or policy raises, River logs the callback error and falls back to
|
|
251
261
|
retrying with the default backoff. The original work error is still recorded.
|
|
262
|
+
Stuck-job rescue also falls back per job when a retry policy raises or returns
|
|
263
|
+
an invalid time, allowing other rescues and cleanup to continue.
|
|
252
264
|
|
|
253
265
|
### [Error handling and timeouts](https://riverqueue.com/docs/error-handling)
|
|
254
266
|
|
|
@@ -310,6 +322,9 @@ config = River::Config.new(queues: {
|
|
|
310
322
|
```
|
|
311
323
|
|
|
312
324
|
Queues may also be added and removed at runtime with `client.queue_add(name, config)` and `client.queue_remove(name)`.
|
|
325
|
+
Queue removal stops fetching and keeps observing cancellation requests until its
|
|
326
|
+
active jobs finish. Removing a queue from one of its own workers raises
|
|
327
|
+
`ThreadError`, because that worker cannot wait for itself to finish.
|
|
313
328
|
|
|
314
329
|
### [Pausing queues](https://riverqueue.com/docs/pausing-queues)
|
|
315
330
|
|
|
@@ -324,6 +339,9 @@ client.queue_resume "*"
|
|
|
324
339
|
```
|
|
325
340
|
|
|
326
341
|
Use `queue_get`, `queue_list`, and `queue_update` to inspect queues and attach metadata.
|
|
342
|
+
Pause and resume events contain a snapshot of each affected queue. Wildcard
|
|
343
|
+
operations publish events for every affected queue; size subscription buffers
|
|
344
|
+
to accommodate the number of events you expect to receive.
|
|
327
345
|
|
|
328
346
|
### Rails and Active Job
|
|
329
347
|
|
|
@@ -340,6 +358,11 @@ the job is due, not during registration. A reusable callable can be passed as
|
|
|
340
358
|
`constructor:` instead; don't supply both. Core periodic schedules live in the
|
|
341
359
|
client process; River Pro adds durable schedules.
|
|
342
360
|
|
|
361
|
+
A started client can produce periodic jobs with `queues: {}` while other clients
|
|
362
|
+
consume them. Jobs added through `client.periodic_jobs` also start maintenance
|
|
363
|
+
when needed. Periodic registration requires leader election to be enabled,
|
|
364
|
+
including jobs registered after startup.
|
|
365
|
+
|
|
343
366
|
```ruby
|
|
344
367
|
cleanup = River::PeriodicJob.new(
|
|
345
368
|
id: :cleanup,
|
|
@@ -360,6 +383,10 @@ registrations and returns the bundle.
|
|
|
360
383
|
`add_many` registers a batch atomically: duplicate IDs or invalid schedules leave
|
|
361
384
|
the registry unchanged.
|
|
362
385
|
|
|
386
|
+
Inserted jobs carry `periodic: true` in metadata and, when the registration has
|
|
387
|
+
an ID, `river:periodic_job_id`. Constructor options and application metadata are
|
|
388
|
+
preserved.
|
|
389
|
+
|
|
363
390
|
Schedules can be callbacks (`schedule: ->(now) { now + 300 }`) or objects
|
|
364
391
|
implementing `next(time)`. They must return a `Time` strictly after the supplied
|
|
365
392
|
time; they do not execute the job themselves. A schedule that raises at runtime
|
|
@@ -376,12 +403,15 @@ cleanup = River::PeriodicJob.new(
|
|
|
376
403
|
|
|
377
404
|
`PeriodicCron` parses once and loads Fugit only when constructed. Fugit is not
|
|
378
405
|
a runtime dependency of the River gem. The timezone defaults explicitly to UTC;
|
|
379
|
-
|
|
380
|
-
optional seconds, and aliases such as `@daily` use Fugit's
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
406
|
+
set `timezone:` or use a `CRON_TZ=` / `TZ=` prefix, which takes precedence.
|
|
407
|
+
Five-field cron, optional seconds, and aliases such as `@daily` use Fugit's
|
|
408
|
+
syntax; `?` is also accepted as a wildcard. `@every 1h30m` uses Go duration
|
|
409
|
+
syntax, rounded down to whole seconds with a one-second minimum. Unlike
|
|
410
|
+
`PeriodicInterval`, these intervals align to whole-second boundaries.
|
|
411
|
+
Results are UTC `Time` objects. Nonexistent spring-forward times are skipped;
|
|
412
|
+
both occurrences of a repeated fall-back time are scheduled. See
|
|
413
|
+
[conformance differences](conformance.md) before sharing schedules across
|
|
414
|
+
languages. Cron does not replay missed occurrences after downtime.
|
|
385
415
|
|
|
386
416
|
For a one-time date, insert a job with
|
|
387
417
|
`client.insert(args, scheduled_at: Time.utc(2026, 9, 20, 9))` instead of registering
|
|
@@ -534,6 +564,9 @@ A plugin may implement any combination of these methods:
|
|
|
534
564
|
|
|
535
565
|
A single plugin can provide both styles. For example, it might use
|
|
536
566
|
`insert_begin` to add metadata and `work` to time the complete work operation.
|
|
567
|
+
After insertion hooks run, each job must still have an initial state of
|
|
568
|
+
`available`, `pending`, or `scheduled`. Invalid states roll back the batch
|
|
569
|
+
and any database writes made by its hooks.
|
|
537
570
|
|
|
538
571
|
```ruby
|
|
539
572
|
class TimingPlugin
|
|
@@ -604,7 +637,11 @@ client.job_delete_many states: [:cancelled]
|
|
|
604
637
|
|
|
605
638
|
Reusable `JobListParams` and `JobUpdateParams` objects are also accepted as
|
|
606
639
|
positional arguments, instead of keywords. For updates, omitted fields remain
|
|
607
|
-
unchanged; an explicit `nil` clears a nullable field.
|
|
640
|
+
unchanged; an explicit `nil` clears a nullable field. Metadata must be a Hash;
|
|
641
|
+
use `metadata: {}` to clear it. Values inside the Hash may be `nil`.
|
|
642
|
+
Updated `max_attempts` must be between 1 and 32,767; `attempt` must be between
|
|
643
|
+
0 and 32,767. Claims cap the attempt counter at 32,767 so an administratively
|
|
644
|
+
requeued job cannot prevent other jobs from being claimed.
|
|
608
645
|
|
|
609
646
|
Bulk deletion requires at least one filter and never deletes running jobs.
|
|
610
647
|
Metadata filters compare complete JSON values at each supplied top-level key,
|
|
@@ -647,6 +684,8 @@ config = River::Config.new(
|
|
|
647
684
|
```
|
|
648
685
|
|
|
649
686
|
A custom service implements `run(client, driver, now)` and runs only while this client holds leadership.
|
|
687
|
+
Service errors are logged without interrupting other services, stuck-job rescue,
|
|
688
|
+
or cleanup. A failed service is retried on the next scheduled maintenance pass.
|
|
650
689
|
|
|
651
690
|
On SQLite, the leader also removes notification outbox entries older than five
|
|
652
691
|
minutes in bounded batches. Cancellation requests write Go-compatible control
|
|
@@ -682,6 +721,13 @@ The Ruby configuration file must return an unstarted client. The following clien
|
|
|
682
721
|
methods are for applications managing their own runtime lifecycle:
|
|
683
722
|
|
|
684
723
|
`client.stop` stops fetching and waits for active jobs to finish. `client.stop_and_cancel` interrupts active worker threads and returns their jobs to `available` without consuming the interrupted attempt.
|
|
724
|
+
While draining, the client continues polling for cancellation requests from
|
|
725
|
+
other clients, including for workers with no timeout. Transient polling errors
|
|
726
|
+
are retried until the active attempts finish.
|
|
727
|
+
|
|
728
|
+
Blocking stop calls from the client's own workers or other runtime threads
|
|
729
|
+
raise `ThreadError`. A worker can request shutdown with `client.stop(wait: false)`
|
|
730
|
+
and then finish its work.
|
|
685
731
|
|
|
686
732
|
Use `client.stop(wait: false)` to request stop and return immediately without
|
|
687
733
|
interrupting active workers. Call `client.stop` later to wait for draining and
|
|
@@ -714,7 +760,7 @@ limitations are resolved.
|
|
|
714
760
|
|
|
715
761
|
## RBS and type checking
|
|
716
762
|
|
|
717
|
-
The gem bundles [RBS files](https://github.com/riverqueue/
|
|
763
|
+
The gem bundles [RBS files](https://github.com/riverqueue/river/tree/master/ruby/sig) for tools such as [Steep](https://github.com/soutaro/steep) and other RBS-compatible type checkers.
|
|
718
764
|
|
|
719
765
|
## Drivers
|
|
720
766
|
|
|
@@ -750,9 +796,10 @@ River Pro is kept in the separate, privately distributed `riverqueue-pro` gem, s
|
|
|
750
796
|
|
|
751
797
|
## Current differences from the Go client
|
|
752
798
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
799
|
+
Ruby checks Go-generated fixtures for unique keys, cron, snooze counters, and
|
|
800
|
+
shared protocol values.
|
|
801
|
+
See [conformance coverage and differences](conformance.md); this is not yet a
|
|
802
|
+
full mixed-language runtime suite.
|
|
756
803
|
|
|
757
804
|
The Ruby client does not currently provide dedicated OpenTelemetry/metrics
|
|
758
805
|
integrations or
|
data/docs/conformance.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Ruby conformance
|
|
2
|
+
|
|
3
|
+
Ruby reads fixtures generated from this checkout's Go implementation, alongside
|
|
4
|
+
Rust and JavaScript. From the repository root:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
make -C ruby install
|
|
8
|
+
make test/ruby/conformance
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This generates `conformance/testdata/` and runs `ruby/spec/conformance_spec.rb`.
|
|
12
|
+
No PostgreSQL server, pinned upstream checkout, protocol adapter, or separately
|
|
13
|
+
maintained golden files are needed. Missing fixtures fail with instructions to
|
|
14
|
+
regenerate them. The regular Ruby suite includes these checks too.
|
|
15
|
+
|
|
16
|
+
The fixture checks cover:
|
|
17
|
+
|
|
18
|
+
- All unique-key cases, including typed-only cases, raw JSON tokens, selected
|
|
19
|
+
fields, periods, queues, and state masks.
|
|
20
|
+
- Job state names and unique bits, attempt-error encoding/decoding, retry bounds,
|
|
21
|
+
and all six shared metadata keys, including actual output, rescue, periodic,
|
|
22
|
+
unique insertion, and resumable-step persistence.
|
|
23
|
+
- All snooze-counter fixtures through real worker attempts and both SQL drivers.
|
|
24
|
+
- Cron occurrences, named zones, daylight-saving transitions, interval schedules,
|
|
25
|
+
and invalid expressions, with the Ruby API differences below checked explicitly.
|
|
26
|
+
- Insertion, cancellation, queue pause/resume/metadata, and leadership resignation
|
|
27
|
+
notification encoding through both SQL drivers, using in-memory SQLite and
|
|
28
|
+
the bundled canonical migrations.
|
|
29
|
+
|
|
30
|
+
The Ruby workflow runs on Ruby changes. The shared Conformance workflow also
|
|
31
|
+
runs these tests when Go code changes. The ordinary driver suites retain
|
|
32
|
+
PostgreSQL notification delivery/commit-ordering tests and shared PostgreSQL /
|
|
33
|
+
SQLite insertion, transaction, uniqueness, and worker tests.
|
|
34
|
+
|
|
35
|
+
This replaces the old `insert-only-v1` adapter and its pinned external harness.
|
|
36
|
+
It checks shared data formats, not live mixed-language worker execution. Adapter
|
|
37
|
+
handshake and request-schema tests were specific to that removed harness; their
|
|
38
|
+
protocol is not part of the Ruby API.
|
|
39
|
+
|
|
40
|
+
## Coverage gaps and API differences
|
|
41
|
+
|
|
42
|
+
Ruby's runtime still polls job and queue state rather than consuming PostgreSQL
|
|
43
|
+
LISTEN events or the SQLite outbox. The fixture target checks notification
|
|
44
|
+
emission, not dispatch. Ruby does not emit or handle `request_resign`; other
|
|
45
|
+
clients' requests therefore cannot force a Ruby leader to resign. Completing
|
|
46
|
+
that coverage would require a notification listener and runtime dispatch path.
|
|
47
|
+
The ordinary driver suites verify PostgreSQL delivery, commit ordering, and
|
|
48
|
+
rollback for queue controls and resignations as well as insertion/cancellation.
|
|
49
|
+
|
|
50
|
+
Shared snooze-counter fixtures cover non-negative integers and absent counters.
|
|
51
|
+
Recovery from other JSON values is implementation-specific. Ruby's shared driver
|
|
52
|
+
tests cover its lenient conversion rules separately on PostgreSQL and SQLite.
|
|
53
|
+
|
|
54
|
+
Cron retains Ruby's existing Fugit extensions: six fields with seconds, Sunday
|
|
55
|
+
as 7, hour 24, and descending ranges. These four expressions are accepted in
|
|
56
|
+
Ruby and rejected in Go; the fixture suite explicitly asserts the difference.
|
|
57
|
+
Fugit rejects impossible calendar dates at construction instead of returning
|
|
58
|
+
Go's zero time. Ruby's default calendar zone remains UTC; tests explicitly pass
|
|
59
|
+
the reference time's fixed offset to model Go's time-location API. `CRON_TZ=` /
|
|
60
|
+
`TZ=` prefixes, `?`, and `@every` duration rounding now follow Go for the shared
|
|
61
|
+
fixtures. No new dependencies were added.
|
|
62
|
+
|
|
63
|
+
Selected unique fields use `by_args: [:field, [:nested, :field]]`, rather than Go
|
|
64
|
+
struct tags. Dotted string keys are literal. Cross-language unique keys require
|
|
65
|
+
the same encoded argument values, including escaping, numeric tokens, and
|
|
66
|
+
nested ordering; Ruby does not override the application's JSON serialization.
|
|
67
|
+
|
|
68
|
+
Transactions are connection-scoped blocks. Workers use exceptions and thread
|
|
69
|
+
interruption. List cursors encode the selected field explicitly. Historical
|
|
70
|
+
error timestamps accept Ruby's `Time.parse` formats, and metadata must decode
|
|
71
|
+
to an object. Live interoperability across these boundaries is not covered by
|
|
72
|
+
the fixture target.
|
data/docs/migrations.md
CHANGED
|
@@ -98,19 +98,16 @@ history changes. These are not shared locks with Go's migration runner.
|
|
|
98
98
|
|
|
99
99
|
## Updating the bundled SQL
|
|
100
100
|
|
|
101
|
-
`migration/manifest.json` records
|
|
102
|
-
|
|
101
|
+
`migration/manifest.json` records each canonical file's SHA-256. From `ruby/`,
|
|
102
|
+
synchronize or verify against the Go migrations in this same checkout:
|
|
103
103
|
|
|
104
104
|
```sh
|
|
105
|
-
ruby scripts/sync_migrations.rb
|
|
105
|
+
ruby scripts/sync_migrations.rb
|
|
106
106
|
make verify
|
|
107
|
-
make verify RIVER_PATH=/path/to/river
|
|
108
107
|
```
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
River repository at that recorded revision and runs the same check; it does not
|
|
113
|
-
require a sibling checkout or access to the private Pro repository.
|
|
109
|
+
Verification checks SQL contents, filenames, license, and manifest checksums.
|
|
110
|
+
CI uses the same working-tree comparison; no separate checkout is needed.
|
|
114
111
|
|
|
115
112
|
Pro migrations have their own manifest and sync script in the private
|
|
116
113
|
`riverqueue-ruby-pro` repository. Run that repository's `make verify` against
|
data/lib/client.rb
CHANGED
|
@@ -7,6 +7,9 @@ module River
|
|
|
7
7
|
# Default number of maximum attempts for a job.
|
|
8
8
|
MAX_ATTEMPTS_DEFAULT = 25
|
|
9
9
|
|
|
10
|
+
# Largest attempt limit supported by all River databases.
|
|
11
|
+
MAX_ATTEMPTS_LIMIT = 32_767
|
|
12
|
+
|
|
10
13
|
# Default priority for a job.
|
|
11
14
|
PRIORITY_DEFAULT = 1
|
|
12
15
|
|
|
@@ -40,6 +43,8 @@ module River
|
|
|
40
43
|
|
|
41
44
|
# Internal extension point used by separately packaged batch workers after
|
|
42
45
|
# they atomically claim additional jobs alongside the batch leader.
|
|
46
|
+
# Errors honor the registered worker's retry hooks and River's cancellation,
|
|
47
|
+
# snooze, and interruption signals. Timings use the row's claim timestamp.
|
|
43
48
|
def __finish_claimed_job(row, error = nil)
|
|
44
49
|
@runtime.finish_claimed(row, error)
|
|
45
50
|
end
|
|
@@ -223,13 +228,18 @@ module River
|
|
|
223
228
|
|
|
224
229
|
# Makes a non-running job immediately available for another attempt and
|
|
225
230
|
# returns its updated JobRow. Raises NotFoundError if it does not exist.
|
|
231
|
+
# Clears any cancellation request from its previous attempt. Raises
|
|
232
|
+
# ArgumentError if another attempt would exceed MAX_ATTEMPTS_LIMIT.
|
|
226
233
|
def job_retry(id)
|
|
227
234
|
@driver.job_retry(id) || raise(NotFoundError, "job not found: #{id}")
|
|
228
235
|
end
|
|
229
236
|
|
|
230
237
|
# Applies keyword attributes or a JobUpdateParams to a job and returns its
|
|
231
238
|
# updated JobRow. Explicit nil clears nullable fields; omitted fields are
|
|
232
|
-
# unchanged.
|
|
239
|
+
# unchanged. Metadata must be a Hash; use an empty Hash to clear it.
|
|
240
|
+
# attempt must be between 0 and MAX_ATTEMPTS_LIMIT (32,767);
|
|
241
|
+
# max_attempts must be between 1 and MAX_ATTEMPTS_LIMIT.
|
|
242
|
+
# Raises NotFoundError if the job does not exist.
|
|
233
243
|
# @type method job_update: (Integer, ?JobUpdateParams?, **untyped) -> JobRow
|
|
234
244
|
def job_update(id, params = nil, **attributes)
|
|
235
245
|
raise ArgumentError, "use params or keyword attributes, not both" if params && !attributes.empty?
|
|
@@ -266,9 +276,8 @@ module River
|
|
|
266
276
|
# Pauses a named queue, or all queues when +name+ is +"*"+.
|
|
267
277
|
def queue_pause(name)
|
|
268
278
|
name = name.to_s
|
|
269
|
-
@driver.queue_pause(name)
|
|
279
|
+
queues = @driver.queue_pause(name)
|
|
270
280
|
@runtime.wake
|
|
271
|
-
queues = (name == "*") ? @driver.queue_list : [@driver.queue_get(name)].compact
|
|
272
281
|
queues.each do |queue|
|
|
273
282
|
@runtime.publish_queue(EVENT_QUEUE_PAUSED, queue)
|
|
274
283
|
end
|
|
@@ -278,6 +287,7 @@ module River
|
|
|
278
287
|
|
|
279
288
|
# Removes a configured queue, waits for active jobs in it to finish, and
|
|
280
289
|
# returns self.
|
|
290
|
+
# Raises ThreadError if called from that queue's own worker or producer.
|
|
281
291
|
def queue_remove(name)
|
|
282
292
|
@runtime.queue_remove(name.to_s)
|
|
283
293
|
self
|
|
@@ -286,9 +296,8 @@ module River
|
|
|
286
296
|
# Resumes a named queue, or all queues when +name+ is +"*"+.
|
|
287
297
|
def queue_resume(name)
|
|
288
298
|
name = name.to_s
|
|
289
|
-
@driver.queue_resume(name)
|
|
299
|
+
queues = @driver.queue_resume(name)
|
|
290
300
|
@runtime.wake
|
|
291
|
-
queues = (name == "*") ? @driver.queue_list : [@driver.queue_get(name)].compact
|
|
292
301
|
queues.each do |queue|
|
|
293
302
|
@runtime.publish_queue(EVENT_QUEUE_RESUMED, queue)
|
|
294
303
|
end
|
|
@@ -316,12 +325,14 @@ module River
|
|
|
316
325
|
# waiting; call stop again to finish draining and release resources.
|
|
317
326
|
# In-flight fetches or maintenance operations may finish. Until a waiting
|
|
318
327
|
# stop completes, started? remains true and stopped? remains false.
|
|
328
|
+
# Raises ThreadError for a waiting call from this client's runtime threads.
|
|
319
329
|
def stop(wait: true)
|
|
320
330
|
@runtime.stop(wait: wait)
|
|
321
331
|
self
|
|
322
332
|
end
|
|
323
333
|
|
|
324
334
|
# Stops fetching new jobs, interrupts active work, and returns self.
|
|
335
|
+
# Raises ThreadError if called from this client's own runtime threads.
|
|
325
336
|
def stop_and_cancel
|
|
326
337
|
@runtime.stop(cancel: true)
|
|
327
338
|
self
|
|
@@ -350,6 +361,8 @@ module River
|
|
|
350
361
|
|
|
351
362
|
EMPTY_INSERT_OPTS = InsertOpts.new.freeze
|
|
352
363
|
|
|
364
|
+
INITIAL_STATES = [JOB_STATE_AVAILABLE, JOB_STATE_PENDING, JOB_STATE_SCHEDULED].freeze
|
|
365
|
+
|
|
353
366
|
REQUIRED_UNIQUE_STATES = [
|
|
354
367
|
JOB_STATE_AVAILABLE,
|
|
355
368
|
JOB_STATE_PENDING,
|
|
@@ -359,7 +372,7 @@ module River
|
|
|
359
372
|
|
|
360
373
|
TAG_RE = /\A\w[\w-]+\w\z/
|
|
361
374
|
|
|
362
|
-
private_constant :DEFAULT_UNIQUE_STATES, :EMPTY_INSERT_OPTS, :REQUIRED_UNIQUE_STATES, :TAG_RE
|
|
375
|
+
private_constant :DEFAULT_UNIQUE_STATES, :EMPTY_INSERT_OPTS, :INITIAL_STATES, :REQUIRED_UNIQUE_STATES, :TAG_RE
|
|
363
376
|
|
|
364
377
|
private def insert_and_check_unique_job(insert_params)
|
|
365
378
|
job, unique_skipped_as_duplicate = @driver.job_insert(insert_params)
|
|
@@ -380,18 +393,25 @@ module River
|
|
|
380
393
|
EMPTY_INSERT_OPTS
|
|
381
394
|
end
|
|
382
395
|
|
|
396
|
+
max_attempts = insert_opts.max_attempts || args_insert_opts.max_attempts || MAX_ATTEMPTS_DEFAULT
|
|
397
|
+
raise ArgumentError, "max_attempts must be greater than zero" unless max_attempts > 0
|
|
398
|
+
raise ArgumentError, "max_attempts must not exceed #{MAX_ATTEMPTS_LIMIT}" if max_attempts > MAX_ATTEMPTS_LIMIT
|
|
399
|
+
|
|
400
|
+
queue = (insert_opts.queue || args_insert_opts.queue || QUEUE_DEFAULT).to_s
|
|
401
|
+
raise ArgumentError, "invalid queue name: #{queue.inspect}" unless queue.match?(QUEUE_NAME_REGEX) && queue.length < 128
|
|
402
|
+
|
|
383
403
|
scheduled_at = insert_opts.scheduled_at || args_insert_opts.scheduled_at
|
|
384
404
|
now = @time_now_utc.call
|
|
385
|
-
state = (insert_opts.state || args_insert_opts.state || ((scheduled_at && scheduled_at > now) ? JOB_STATE_SCHEDULED : JOB_STATE_AVAILABLE))
|
|
405
|
+
state = validate_insert_state(insert_opts.state || args_insert_opts.state || ((scheduled_at && scheduled_at > now) ? JOB_STATE_SCHEDULED : JOB_STATE_AVAILABLE))
|
|
386
406
|
|
|
387
407
|
insert_params = Driver::JobInsertParams.new(
|
|
388
408
|
args: args,
|
|
389
409
|
encoded_args: args_json,
|
|
390
410
|
kind: args.kind.to_s,
|
|
391
|
-
max_attempts:
|
|
411
|
+
max_attempts: max_attempts,
|
|
392
412
|
metadata: (args_insert_opts.metadata || {}).merge(insert_opts.metadata || {}),
|
|
393
413
|
priority: insert_opts.priority || args_insert_opts.priority || PRIORITY_DEFAULT,
|
|
394
|
-
queue:
|
|
414
|
+
queue: queue,
|
|
395
415
|
scheduled_at: scheduled_at&.getutc || now,
|
|
396
416
|
state: state,
|
|
397
417
|
tags: validate_tags(insert_opts.tags || args_insert_opts.tags || [])
|
|
@@ -423,6 +443,8 @@ module River
|
|
|
423
443
|
end
|
|
424
444
|
|
|
425
445
|
if unique_opts.by_period && unique_opts.by_period != 0
|
|
446
|
+
raise ArgumentError, "by_period should not be less than 1 second" if unique_opts.by_period < 1
|
|
447
|
+
|
|
426
448
|
lower_period_bound = truncate_time(insert_params.scheduled_at || @time_now_utc.call, unique_opts.by_period).utc
|
|
427
449
|
|
|
428
450
|
unique_key += "&period=#{lower_period_bound.strftime("%FT%TZ")}"
|
|
@@ -456,6 +478,10 @@ module River
|
|
|
456
478
|
end
|
|
457
479
|
end
|
|
458
480
|
|
|
481
|
+
# Hooks may change any job in the batch, including an earlier entry.
|
|
482
|
+
# Validate the final states only after all insertion hooks have run.
|
|
483
|
+
all_params.each { |params| params.state = validate_insert_state(params.state) }
|
|
484
|
+
|
|
459
485
|
results = insert_operation.call
|
|
460
486
|
results.each do |result|
|
|
461
487
|
config.plugins.reverse_each do |plugin|
|
|
@@ -496,6 +522,13 @@ module River
|
|
|
496
522
|
[int].pack("Q").unpack1("q") #: Integer # rubocop:disable Layout/LeadingCommentSpace
|
|
497
523
|
end
|
|
498
524
|
|
|
525
|
+
private def validate_insert_state(state)
|
|
526
|
+
state = state.to_s #: jobStateInitial # rubocop:disable Layout/LeadingCommentSpace
|
|
527
|
+
raise ArgumentError, "invalid insertion state: #{state.inspect}; must be available, pending, or scheduled" unless INITIAL_STATES.include?(state)
|
|
528
|
+
|
|
529
|
+
state
|
|
530
|
+
end
|
|
531
|
+
|
|
499
532
|
private def validate_tags(tags)
|
|
500
533
|
tags.each do |tag|
|
|
501
534
|
raise ArgumentError, "tags should be 255 characters or less" if tag.length > 255
|