riverqueue 0.13.0 → 0.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c22dfdcd40a71eaf7e3f9f74351b795957b2a23c7a51d5e302c7dd6d046ce3e8
4
- data.tar.gz: 495748ff5de7b982d2d18c8ed6670865e5452c5e8198a457428e53c2af5ecf76
3
+ metadata.gz: 1ce8622d0f151131d632d673e694e8ea90ded0502ba912274c7d044076063ee9
4
+ data.tar.gz: 1df7e4f2f10a3cfe36d3d08793a38b5400bf12dd32c3db3d3fd248184523d961
5
5
  SHA512:
6
- metadata.gz: 1707b4db9e48540d168af6cdd89dc98e78db8101e4cfaa1a95ede93a1e4d1bd5ab28b13e94f6ab3c106f5673dc30f4092b83d299b98a257c23384ffeb2ba170d
7
- data.tar.gz: cffa8f11fda5a7b003413ed3dab643ce44168987dc2afe2fb0b6d5dccafcfbfe0e6a88264262502018e53a38f6f3c2b1a460fef8ccdfb7ce0c9c37999bfe3514
6
+ metadata.gz: c09ee3fefb3b0d9bdb4bd969761117470a84b7bcfa1278819446471c53348356ddffea3fa424d0cf086f57b1134ec49c1d470d03314286d21d50c9d1670568a4
7
+ data.tar.gz: a79a913f6f455f8a6a14e989ae72c415adb39248d97c1457b2cfd077b74ce41676e1f3bc79239d4a33d2653485074de7a8a6fd414699fec2e30b97c1fedb3d11
data/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.14.0] - 2026-10-26
11
+
12
+ ### Added
13
+
14
+ - Receive Postgres and SQLite notifications for insertions, queue controls, cancellation, and leadership handoffs, with reconnect recovery and a `poll_only` option. [PR #1496](https://github.com/riverqueue/river/pull/1496).
15
+ - Add `Client#request_resign` to request that the maintenance leader relinquish leadership after the caller's transaction commits. [PR #1496](https://github.com/riverqueue/river/pull/1496).
16
+ - Allow `River::Workers#add` to register a kind and a work block, using the client's default retry and timeout policies. [PR #1486](https://github.com/riverqueue/river/pull/1486).
17
+ - Added `Client#job_complete_tx(job)` to complete a running job in the caller's Active Record or Sequel transaction, including pending metadata and output. An active transaction on the driver's connection is required. Completion rolls back with the transaction; once committed, it takes precedence over subsequent worker errors and emits a completion event when the worker finishes. [PR #1490](https://github.com/riverqueue/river/pull/1490).
18
+
19
+ ### Changed
20
+
21
+ - River operations called inside an open Active Record or Sequel transaction now run in that transaction and open no savepoint of their own. If an operation raises, roll back the transaction, or wrap the call in your own savepoint to recover and continue. A `driver.rollback_exception` raised inside `Driver#transaction` now reaches the enclosing transaction block, as a rollback raised in your own code would. Before, it rolled back only River's savepoint. Set `RIVER_USE_LEGACY_SUBTRANSACTIONS=1` (or `true`) to restore the savepoints. This temporary fallback will be removed in a future release. [PR #1544](https://github.com/riverqueue/river/pull/1544).
22
+ - `work_end(job, error)` hooks now return the attempt's error, matching Go. Return the incoming error to preserve it, a replacement exception to change it, or `nil` to clear a failure. The return value feeds the next hook in registration order. Existing observing hooks must explicitly return `error`. End hooks are skipped if `work_begin` raises or the worker raises an exception outside `StandardError`. [PR #1520](https://github.com/riverqueue/river/pull/1520).
23
+
24
+ ### Fixed
25
+
26
+ - The scheduler no longer opens a savepoint for each due job to catch unique key conflicts. A pass that made more than 64 jobs available overflowed the Postgres subtransaction cache, which can slow queries across the server. The scheduler now finds conflicts with one statement on Postgres and checks before each write on SQLite. Conflicting jobs are still discarded with `unique_key_conflict: scheduler_discarded` metadata. On Postgres, the scheduler now waits for a due job that another transaction has locked, instead of skipping it. [PR #1544](https://github.com/riverqueue/river/pull/1544).
27
+ - Default periodic jobs to their occurrence time, preserving constructor and argument-level schedules. [PR #1537](https://github.com/riverqueue/river/pull/1537).
28
+ - Fail retries that omit a saved cursor-only step, while preserving their checkpoints for recovery. Jobs with an existing step marker retain their behavior, including legacy stale cursor entries. [PR #1534](https://github.com/riverqueue/river/pull/1534).
29
+ - Support large SQLite metadata updates without reaching the expression-depth limit, preserving top-level replacement and JSON null values. [PR #1533](https://github.com/riverqueue/river/pull/1533).
30
+ - A `River::Config` without an explicit `id:` generates its default client ID on first use in each process, so clients built before a fork (as with preloading Puma, Unicorn, or Pitchfork servers) no longer share one ID across forked workers. Explicit IDs are unchanged, and `Config#with` copies share the original's ID. [PR #1498](https://github.com/riverqueue/river/pull/1498).
31
+ - Inserting a job stores its encoded args as given instead of decoding and re-encoding them in Ruby, which rewrote numbers like `1.10` as `1.1` and `1e3` as `1000.0` and rounded digits beyond `Float` precision. Postgres applies its usual `jsonb` normalization, and SQLite keeps the encoded bytes, matching River's Go drivers. Affects Active Record on Postgres and SQLite, and Sequel on SQLite. [PR #1503](https://github.com/riverqueue/river/pull/1503).
32
+ - Wake workers when scheduled jobs become available or jobs are manually retried, including workers in other clients. [PR #1496](https://github.com/riverqueue/river/pull/1496).
33
+ - Honor cancellations that arrive between claiming a job and starting its worker. [PR #1496](https://github.com/riverqueue/river/pull/1496).
34
+ - Preserve worker wakeups that arrive between a fetch and the producer going to sleep. [PR #1496](https://github.com/riverqueue/river/pull/1496).
35
+
10
36
  ## [0.13.0] - 2026-10-08
11
37
 
12
38
  ### Added
@@ -17,10 +43,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
43
  ### Changed
18
44
 
19
45
  - 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).
46
+ - 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).
47
+ - 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).
48
+ - 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).
49
+ - 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
50
 
25
51
  ### Fixed
26
52
 
@@ -36,7 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
36
62
  - 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
63
  - 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
64
  - 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).
65
+ - Bulk deletion excludes running jobs before applying its limit and locks eligible Postgres 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
66
  - 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
67
  - Snoozing and rescue tolerate JSON counters that overflow SQLite's floating-point representation. [PR #1463](https://github.com/riverqueue/river/pull/1463).
42
68
  - 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).
@@ -47,21 +73,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
47
73
  - 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
74
  - 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
75
  - 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).
76
+ - Job updates reject nonpositive attempt limits consistently on Postgres and SQLite. [PR #1463](https://github.com/riverqueue/river/pull/1463).
51
77
  - Job lifecycle events report worker execution and completion durations separately using a monotonic clock. [PR #1463](https://github.com/riverqueue/river/pull/1463).
52
78
  - 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
79
  - 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
80
  - 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
81
  - Retrying, snoozing, and interrupting jobs preserve the original queue-wait duration in lifecycle events. [PR #1463](https://github.com/riverqueue/river/pull/1463).
56
82
  - 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).
83
+ - SQLite retains only the newest 100 worker IDs when claiming a job, matching Postgres. [PR #1463](https://github.com/riverqueue/river/pull/1463).
58
84
  - Attempt-error serialization supports frozen timestamps and preserves the caller's timezone. [PR #1463](https://github.com/riverqueue/river/pull/1463).
59
85
 
60
86
  ## [0.12.0] - 2026-10-01
61
87
 
62
88
  ### Added
63
89
 
64
- - Add a full Ruby client for River with Go-compatible job insertion and execution on PostgreSQL, SQLite, and YugabyteDB through ActiveRecord or Sequel. Includes workers, retries, cancellation, periodic and resumable jobs, job-persisted logging, job administration, migration and worker CLIs, and testing helpers. Rails and Active Job integration is available through `riverqueue-rails`, with workflows, batches, sequences, concurrency controls, and other advanced features in the separately distributed `riverqueue-pro` gem. [PR #70](https://github.com/riverqueue/riverqueue-ruby/pull/70).
90
+ - Add a full Ruby client for River with Go-compatible job insertion and execution on Postgres, SQLite, and YugabyteDB through ActiveRecord or Sequel. Includes workers, retries, cancellation, periodic and resumable jobs, job-persisted logging, job administration, migration and worker CLIs, and testing helpers. Rails and Active Job integration is available through `riverqueue-rails`, with workflows, batches, sequences, concurrency controls, and other advanced features in the separately distributed `riverqueue-pro` gem. [PR #70](https://github.com/riverqueue/riverqueue-ruby/pull/70).
65
91
 
66
92
  ## [0.11.0] - 2026-09-02
67
93
 
@@ -71,7 +97,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
71
97
 
72
98
  ### Changed
73
99
 
74
- - Stop pulling in `pg` as a hard dependency of either driver. Applications now select their database adapter by including `pg` for PostgreSQL or `sqlite3` for SQLite. [PR #67](https://github.com/riverqueue/riverqueue-ruby/pull/67).
100
+ - Stop pulling in `pg` as a hard dependency of either driver. Applications now select their database adapter by including `pg` for Postgres or `sqlite3` for SQLite. [PR #67](https://github.com/riverqueue/riverqueue-ruby/pull/67).
75
101
 
76
102
  ## [0.10.1] - 2026-04-09
77
103
 
@@ -130,7 +156,7 @@ Users should upgrade backends to River v0.12.0 before upgrading this library in
130
156
 
131
157
  This updated implementation is significantly faster due to the removal of advisory locks in favor of an index-backed uniqueness system, while allowing some flexibility in which job states are considered. However, not all states may be removed from consideration when using the `by_state` option; pending, scheduled, available, and running states are required whenever customizing this list. [PR #32](https://github.com/riverqueue/riverqueue-ruby/pull/32).
132
158
 
133
- - Update REXML dependency. [PR #28](https://github.com/riverqueue/riverqueue-ruby/pull/36).
159
+ - Update REXML dependency. [PR #36](https://github.com/riverqueue/riverqueue-ruby/pull/36).
134
160
 
135
161
  ## [0.7.0] - 2024-08-30
136
162
 
@@ -155,7 +181,7 @@ Users should upgrade backends to River v0.12.0 before upgrading this library in
155
181
  ### Changed
156
182
 
157
183
  - Tag format is now checked on insert. Tags should be no more than 255 characters and match the regex `/\A[\w][\w\-]+[\w]\z/`. [PR #22](https://github.com/riverqueue/riverqueue-ruby/pull/22).
158
- - Returned jobs now have a `metadata` property. [PR #21](https://github.com/riverqueue/riverqueue-ruby/pull/22).
184
+ - Returned jobs now have a `metadata` property. [PR #22](https://github.com/riverqueue/riverqueue-ruby/pull/22).
159
185
 
160
186
  ## [0.4.0] - 2024-04-28
161
187
 
data/README.md CHANGED
@@ -1,22 +1,91 @@
1
1
  # River for Ruby
2
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.
3
+ River is a fast, reliable background job system backed by Postgres or SQLite. The Ruby client inserts and works jobs using the same database schema and job protocol as River's other languages. It includes Active Record and Sequel drivers, plus Rails and Active Job integration.
6
4
 
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)
5
+ ## Installation
11
6
 
12
- From the repository root:
7
+ Ruby 3.2 or newer is required. Add a River driver and its database adapter to your application's `Gemfile`; the driver brings in the core `riverqueue` gem:
8
+
9
+ ```ruby
10
+ gem "riverqueue-sequel"
11
+ gem "pg"
12
+ ```
13
+
14
+ Use `riverqueue-activerecord` for Active Record, or `sqlite3` instead of `pg` for SQLite. Install the gems with `bundle install`. Rails applications can use [`riverqueue-rails`](rails/riverqueue-rails/README.md) to configure River as their Active Job backend.
15
+
16
+ ## Quick start
17
+
18
+ Set `DATABASE_URL` to an existing application database and apply River's migrations before inserting or working jobs:
13
19
 
14
20
  ```sh
15
- make -C ruby install
16
- createdb river_test
17
- RIVER_REQUIRE_DATABASES=1 make test/ruby
18
- make lint/ruby typecheck/ruby
21
+ export DATABASE_URL=postgres://localhost/my_app
22
+ bundle exec river migrate-up
23
+ ```
24
+
25
+ Define job arguments and a worker with the same stable kind, register the worker, and insert a job. Save this as `worker.rb`:
26
+
27
+ ```ruby
28
+ require "riverqueue-sequel"
29
+
30
+ class SendEmailArgs
31
+ def initialize(address:)
32
+ @address = address
33
+ end
34
+
35
+ def kind = "send_email"
36
+ def to_json = JSON.generate(address: @address)
37
+ end
38
+
39
+ class SendEmailWorker
40
+ def self.kind = "send_email"
41
+
42
+ def work(job)
43
+ puts "Sending email to #{job.args.fetch("address")}"
44
+ job.output = {delivered: true}
45
+ end
46
+ end
47
+
48
+ db = Sequel.connect(ENV.fetch("DATABASE_URL"), max_connections: 12)
49
+ client = River::Client.new(
50
+ River::Driver::Sequel.new(db),
51
+ config: River::Config.new(
52
+ queues: {default: 10},
53
+ workers: River::Workers.new.add(SendEmailWorker)
54
+ )
55
+ )
56
+
57
+ begin
58
+ client.start
59
+ result = client.insert(SendEmailArgs.new(address: "person@example.com"))
60
+ puts "Inserted job #{result.job.id}; press Ctrl-C to stop"
61
+ sleep
62
+ rescue Interrupt
63
+ # Stop fetching and wait for active jobs before closing the database pool.
64
+ ensure
65
+ client.stop
66
+ db.disconnect
67
+ end
19
68
  ```
20
69
 
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.
70
+ Run `bundle exec ruby worker.rb`. Replace the worker body with your mail service. Job arguments implement `kind` and `to_json`; workers read decoded JSON keys as strings. Services sharing a job must agree on its kind and JSON fields. For SQLite and alternate Postgres schemas, see [migrations](docs/migrations.md).
71
+
72
+ A client without configured queues can insert jobs without starting worker threads. Applications own their database pools and should close them only after stopping River.
73
+
74
+ ## Transactions and other features
75
+
76
+ Inserts join a transaction opened on the same Sequel database or Active Record connection, so jobs commit or roll back with the application writes that caused them. See [transactional enqueueing](docs/README.md#transactional-enqueueing) for examples.
77
+
78
+ - [Bulk insertion](docs/README.md#bulk-insertion), [scheduled jobs](docs/README.md#scheduled-jobs), and [unique jobs](docs/README.md#unique-jobs).
79
+ - [Retries](docs/README.md#job-retries), [cancellation](docs/README.md#cancelling-jobs), and [snoozing](docs/README.md#snoozing-jobs).
80
+ - [Multiple queues](docs/README.md#multiple-queues), [periodic jobs](docs/README.md#periodic-jobs), and [resumable jobs](docs/README.md#resumable-jobs).
81
+ - [Event subscriptions](docs/README.md#subscriptions) and [job administration](docs/README.md#job-administration).
82
+ - [Graceful stopping](docs/README.md#stopping-gracefully) and a foreground [worker command](docs/workers.md).
83
+ - [Testing helpers](docs/testing.md) for application jobs and workers.
84
+
85
+ ## Documentation
86
+
87
+ See the [usage and configuration guide](docs/README.md), [migration guide](docs/migrations.md), and [Rails and Active Job integration](rails/riverqueue-rails/README.md). The [Sidekiq migration guide](docs/migrating_from_sidekiq.md) covers moving an existing application.
88
+
89
+ ## Development
90
+
91
+ See [developing River for Ruby](docs/development.md).
data/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # River client for Ruby [![Build Status](https://github.com/riverqueue/river/actions/workflows/ruby.yaml/badge.svg)](https://github.com/riverqueue/river/actions/workflows/ruby.yaml) [![Gem Version](https://badge.fury.io/rb/riverqueue.svg)](https://badge.fury.io/rb/riverqueue)
2
2
 
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.
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 River clients in other languages can safely share a River database. Separate queues are recommended when each language recognizes different job kinds.
4
4
 
5
5
  ## Installation
6
6
 
@@ -26,9 +26,9 @@ its uniqueness and notification capabilities.
26
26
  ## Schema and migrations
27
27
 
28
28
  Run migrations before inserting jobs or starting workers. The bundled `river`
29
- command uses Go's canonical PostgreSQL and SQLite migrations; no Go installation
30
- is needed. After installing a River driver and its database adapter, create your
31
- application's PostgreSQL database and run:
29
+ command uses River's canonical Postgres and SQLite migrations, bundled with the
30
+ gem. After installing a River driver and its database adapter, create your
31
+ application's Postgres database and run:
32
32
 
33
33
  ```sh
34
34
  export DATABASE_URL=postgres://localhost/my_app
@@ -39,8 +39,8 @@ bundle exec river migrate-up
39
39
  ```
40
40
 
41
41
  `migrate-up` applies all pending migrations and is safe to rerun: already-applied
42
- versions are skipped, including migrations applied by Go. `--dry-run` previews
43
- the plan without changing the database. Pass `--database-url URL` to override
42
+ versions are skipped, including migrations applied by other River clients.
43
+ `--dry-run` previews the plan without changing the database. Pass `--database-url URL` to override
44
44
  `DATABASE_URL`.
45
45
 
46
46
  For SQLite, ensure the parent directory exists, then use a SQLite URL:
@@ -57,7 +57,7 @@ both are in your bundle.
57
57
  For River Pro, install `riverqueue-pro`, apply the main migrations above, then
58
58
  run `bundle exec river migrate-up --line pro` against the same database.
59
59
 
60
- See the [migration guide](migrations.md) for the Ruby API, PostgreSQL schemas,
60
+ See the [migration guide](migrations.md) for the Ruby API, Postgres schemas,
61
61
  target versions, downgrade precautions, and existing SQLite Pro installations.
62
62
  Test schema snapshots under `spec/support` are test-only and must not be used
63
63
  to provision production databases.
@@ -110,12 +110,37 @@ Job arguments must respond to `#kind` and `#to_json`. They may also return defau
110
110
  Use strings for `#kind` definitions, and symbols for identifiers such as queues,
111
111
  states, periodic job IDs, and resumable step names. Both forms are accepted.
112
112
  Persisted job attributes and JSON object keys remain strings, preserving
113
- compatibility with Go clients.
113
+ compatibility with River clients in other languages.
114
114
  Examples omit parentheses on simple calls and `do...end` blocks, retaining them
115
115
  for nested expressions and `{ ... }` blocks where they make binding clear.
116
116
 
117
117
  ## Core features
118
118
 
119
+ ### Block workers
120
+
121
+ Pass a kind and a block to `Workers#add` to define a worker inline:
122
+
123
+ ```ruby
124
+ workers = River::Workers.new
125
+ workers.add(:message) do |job|
126
+ puts "Message: #{job.args.fetch("message")}"
127
+ end
128
+
129
+ # With client configured to use this registry and the ruby queue:
130
+ client.insert River::JobArgsHash.new(:message, message: "Hello!"), queue: :ruby
131
+ ```
132
+
133
+ Pass this registry as `workers:` in `River::Config`. The kind accepts a string
134
+ or symbol and must match the inserted job's kind. The block receives a
135
+ `River::Job` and uses the client's retry and timeout policies. Returning normally
136
+ completes the job; the block's return value is ignored. Raise exceptions to fail,
137
+ cancel, or snooze work as with a worker class. Optionally use `job.output =` to
138
+ persist a result. Aliases are supported through `aliases:` as for other workers.
139
+ Supply a worker or a block, not both. Existing procs or methods can be passed as
140
+ the block with `&`.
141
+ The same block may run concurrently for multiple jobs, so any captured mutable
142
+ state must be safe to share across threads.
143
+
119
144
  ### [Accessing the client from workers](https://riverqueue.com/docs/context-client)
120
145
 
121
146
  Every running `River::Job` exposes the client that claimed it. Workers can use
@@ -159,8 +184,8 @@ For simple jobs, `River::JobArgsHash.new(:kind, hash)` avoids defining an argume
159
184
 
160
185
  ### [Transactional enqueueing](https://riverqueue.com/docs/transactional-enqueueing)
161
186
 
162
- Insertion hooks and middleware execute inside the insertion transaction. If
163
- they raise, their database writes and the enqueue roll back together. PostgreSQL
187
+ Insertion hooks and middleware execute inside the insertion transaction, so
188
+ their database writes and the enqueue commit or roll back together. Postgres
164
189
  insert notifications are delivered only when the surrounding transaction commits.
165
190
 
166
191
  Inserts automatically join a transaction opened through the same Active Record connection or Sequel database object. A rollback also rolls back the job:
@@ -181,6 +206,62 @@ end
181
206
 
182
207
  The equivalent works inside `ActiveRecord::Base.transaction` with the Active Record driver.
183
208
 
209
+ River calls inside an open transaction run in that transaction and open no
210
+ savepoint of their own. If a River call raises, it may leave partial changes,
211
+ so roll back the transaction. To recover from the error and continue the
212
+ transaction, wrap the call in your own savepoint, such as
213
+ `DB.transaction(savepoint: true)` or
214
+ `ApplicationRecord.transaction(requires_new: true)`.
215
+
216
+ ### Transactional completion
217
+
218
+ Call `job.client.job_complete_tx(job)` inside a transaction to commit application
219
+ writes and job completion together (the equivalent of Go's `JobCompleteTx`):
220
+
221
+ ```ruby
222
+ class FulfillOrderWorker
223
+ def work(job)
224
+ DB.transaction do
225
+ DB[:orders].where(id: job.args.fetch("order_id")).update(fulfilled: true)
226
+ job.output = {fulfilled: true}
227
+ job.client.job_complete_tx(job)
228
+ end
229
+ end
230
+ end
231
+ ```
232
+
233
+ With Active Record 7.2 or later, use it inside an `ApplicationRecord.transaction` block. As
234
+ with insertion, the transaction must use the driver's connection on the same
235
+ thread. No explicit transaction argument is needed. Calling `job_complete_tx`
236
+ without an active transaction on that connection raises `River::Error` without
237
+ changing the job.
238
+
239
+ Sequel 5.20 or later is required so transaction callbacks respect savepoints.
240
+
241
+ `job_complete_tx` accepts the running `River::Job` passed to the worker and returns
242
+ an updated `River::JobRow`, leaving the original job unchanged. Set output and
243
+ metadata before calling it so they're included in the transaction. A missing
244
+ job raises `River::NotFoundError`; a pending cancellation raises
245
+ `River::JobCancelError`, allowing the transaction to roll back.
246
+ The supplied job must still belong to the current attempt; a stale worker cannot
247
+ complete a replacement attempt.
248
+ Metadata added afterward, including persisted logs, is saved separately when
249
+ the worker finishes, provided the completed attempt has not been retried.
250
+
251
+ Make completion the last operation in the transaction and let the transaction
252
+ commit before returning from `work`. Propagate errors that roll back the
253
+ transaction so River retries the job. If you swallow a rollback and return
254
+ successfully, River completes the job normally. Once completion commits, later
255
+ worker errors cannot retry or cancel the job. River emits `:job_completed` when
256
+ the worker finishes, and skips its usual finalization hooks for the already
257
+ completed job.
258
+ If the completed row is deleted before the worker finishes, River emits the
259
+ completion event using the snapshot retained when the transaction committed.
260
+
261
+ If the job is manually retried before the original worker finishes, that worker
262
+ leaves the replacement attempt untouched and does not emit a completion event
263
+ for it.
264
+
184
265
  ### [Bulk insertion](https://riverqueue.com/docs/inserting-many-jobs)
185
266
 
186
267
  `#insert_many` inserts a batch atomically and returns one `River::JobInsertResult` per input. Use `River::InsertManyParams` when jobs need different options:
@@ -230,7 +311,7 @@ order affect the hash even when the decoded JSON is equivalent.
230
311
 
231
312
  ### [Reliable execution and stuck jobs](https://riverqueue.com/docs/reliable-workers)
232
313
 
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.
314
+ 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 any River client or River UI.
234
315
 
235
316
  ### [Job retries](https://riverqueue.com/docs/job-retries)
236
317
 
@@ -405,9 +486,9 @@ cleanup = River::PeriodicJob.new(
405
486
  a runtime dependency of the River gem. The timezone defaults explicitly to UTC;
406
487
  set `timezone:` or use a `CRON_TZ=` / `TZ=` prefix, which takes precedence.
407
488
  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.
489
+ syntax; `?` is also accepted as a wildcard. `@every` accepts durations such
490
+ as `1h30m`, built from `ns`, `us`, `ms`, `s`, `m`, and `h` units, rounded down
491
+ to whole seconds with a one-second minimum. Unlike `PeriodicInterval`, these intervals align to whole-second boundaries.
411
492
  Results are UTC `Time` objects. Nonexistent spring-forward times are skipped;
412
493
  both occurrences of a repeated fall-back time are scheduled. See
413
494
  [conformance differences](conformance.md) before sharing schedules across
@@ -419,7 +500,7 @@ a periodic job. This stores the scheduled job immediately in the database.
419
500
 
420
501
  ### [Resumable jobs](https://riverqueue.com/docs/resumable-jobs)
421
502
 
422
- Long jobs can checkpoint idempotent steps and cursor progress. On retry, River skips completed steps and resumes a cursor step from its last recorded value using the same metadata format as Go.
503
+ Long jobs can checkpoint idempotent steps and cursor progress. On retry, River skips completed steps and resumes a cursor step from its last recorded value using River's shared metadata format.
423
504
 
424
505
  ```ruby
425
506
  def work(job)
@@ -481,7 +562,7 @@ end
481
562
  ### Job-persisted logging
482
563
 
483
564
  Add `River::JobPersistedLogging::Plugin` to save worker logs with each job attempt,
484
- using the same `metadata["river:log"]` format as Go's `riverlog` and River UI. This
565
+ using the shared `metadata["river:log"]` format that River UI displays. This
485
566
  is a core feature; it does not require Pro or additional migrations.
486
567
 
487
568
  ```ruby
@@ -557,7 +638,7 @@ A plugin may implement any combination of these methods:
557
638
  | Hook | `insert_begin(params)` | Before each job is inserted; `params` may be modified. |
558
639
  | Hook | `insert_end(result)` | After each job is inserted. |
559
640
  | Hook | `work_begin(job)` | After a job is claimed, immediately before its worker runs. |
560
- | Hook | `work_end(job, error)` | After the worker returns or raises; `error` is `nil` on success. |
641
+ | Hook | `work_end(job, error)` | After the worker returns or raises a `StandardError`; returns the attempt's error or `nil`. |
561
642
  | Hook | `job_finalize(job, state)` | Before successful finalization; returning `:delete` deletes the job instead of retaining it. |
562
643
  | Middleware | `insert_many(params, operation)` | Around one insertion call; `params` is an array even for `Client#insert`. |
563
644
  | Middleware | `work(job, operation)` | Around the work hooks and worker for one claimed job. |
@@ -584,10 +665,33 @@ end
584
665
  config = River::Config.new(plugins: [AuditPlugin.new, TimingPlugin.new])
585
666
  ```
586
667
 
587
- Plugins earlier in the list are the outermost wrappers. Begin callbacks run in
588
- configuration order, while `insert_end` callbacks run in reverse order. In
589
- effect, work execution is nested as: middleware before, `work_begin`, worker,
590
- `work_end`, middleware after.
668
+ Plugins earlier in the list are the outermost wrappers. Begin callbacks and
669
+ `work_end` callbacks run in configuration order, while `insert_end` callbacks
670
+ run in reverse order. In effect, work execution is nested as: middleware before,
671
+ `work_begin`, worker, `work_end`, middleware after.
672
+
673
+ Each `work_end(job, error)` receives `nil` on success or the worker's exception
674
+ on failure. Return `error` to preserve that outcome, a different exception to
675
+ replace it, or `nil` to clear a failure. Each subsequent end hook receives the
676
+ previous hook's return value. After all end hooks run, River raises the final
677
+ error through the middleware, or completes the operation if it is `nil`.
678
+ Returning `River.job_cancel(...)` or `River.job_snooze(...)` selects the
679
+ corresponding cancellation or snooze behavior.
680
+
681
+ An observing hook must explicitly return its incoming error:
682
+
683
+ ```ruby
684
+ def work_end(job, error)
685
+ Metrics.record_attempt(job.kind, error: error)
686
+ error
687
+ end
688
+ ```
689
+
690
+ If a `work_begin` hook raises, River skips the worker and all end hooks. If an
691
+ end hook raises instead of returning an error, it stops the end-hook chain and
692
+ propagates that exception through the middleware. Fatal exceptions outside
693
+ `StandardError` also bypass end hooks. Use middleware with `ensure` for cleanup
694
+ that must run when hooks or work raise.
591
695
 
592
696
  ### [Subscriptions](https://riverqueue.com/docs/subscriptions)
593
697
 
@@ -687,10 +791,32 @@ A custom service implements `run(client, driver, now)` and runs only while this
687
791
  Service errors are logged without interrupting other services, stuck-job rescue,
688
792
  or cleanup. A failed service is retried on the next scheduled maintenance pass.
689
793
 
794
+ Call `client.request_resign` to ask the current maintenance leader to resign.
795
+ The leader releases its lease and briefly delays reelection so another client
796
+ can take over. The request joins the caller's transaction and is delivered only
797
+ on commit.
798
+
690
799
  On SQLite, the leader also removes notification outbox entries older than five
691
- minutes in bounded batches. Cancellation requests write Go-compatible control
692
- notifications in the same transaction as the job update; Ruby workers continue
693
- to observe cancellation through polling.
800
+ minutes in bounded batches.
801
+
802
+ ### Notifications
803
+
804
+ Started clients receive job insertion, queue control, cancellation, and
805
+ leadership notifications. Postgres uses one dedicated LISTEN connection per
806
+ started client, separate from the application's connection pool. SQLite polls
807
+ the notification outbox with an independent cursor for each client. Inserts,
808
+ scheduled jobs becoming available, and manual retries wake workers promptly.
809
+
810
+ The receiver starts before workers and stays active during graceful shutdown.
811
+ Startup fails if the initial subscription fails; an established receiver retries
812
+ after connection errors and refreshes persisted state on reconnect. Normal job
813
+ and queue polling also remains active.
814
+
815
+ Set `poll_only: true` in `River::Config` to disable receiving notifications, for
816
+ example when connecting through a proxy that cannot support LISTEN. Job, queue,
817
+ and cancellation changes are then observed at the polling interval, and
818
+ `request_resign` is not received. Backends without native notification support,
819
+ such as Yugabyte with LISTEN/NOTIFY disabled, use polling automatically.
694
820
 
695
821
  ### [Renaming job kinds](https://riverqueue.com/docs/renaming-jobs)
696
822
 
@@ -721,7 +847,7 @@ The Ruby configuration file must return an unstarted client. The following clien
721
847
  methods are for applications managing their own runtime lifecycle:
722
848
 
723
849
  `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
850
+ While draining, the client continues receiving and polling for cancellation requests from
725
851
  other clients, including for workers with no timeout. Transient polling errors
726
852
  are retried until the active attempts finish.
727
853
 
@@ -751,6 +877,11 @@ Load River and worker definitions in the main Ractor before spawning others. Eac
751
877
 
752
878
  Database drivers, Active Record, Sequel, and optional dependencies such as Fugit still need their own Ractor compatibility. Job timeouts also depend on Ruby and the `timeout` gem: the test suite exercises them on Ruby 4 with `timeout` 0.6.1; tests on older Rubies disable job timeouts. `WorkerRunner` handles process-wide signals and must run on the main thread of the main Ractor.
753
879
 
880
+ Ruby 3.3 and 3.4 can deadlock while scheduling threads inside Ractors, even
881
+ without River ([Ruby bug #20905](https://bugs.ruby-lang.org/issues/20905), fixed
882
+ in Ruby 4). The test suite skips the producer/maintenance lifecycle test on
883
+ those versions; insertion and synchronous worker checks still run.
884
+
754
885
  Ruby 4.0.2 with `timeout` 0.6.1 can intermittently deadlock during VM shutdown
755
886
  while terminating Ractors and their timeout helper threads, even after work
756
887
  finishes successfully. This reproduces without River. Tests bypass that shutdown
@@ -794,18 +925,17 @@ Minitest integrations are optional and explicitly loaded.
794
925
 
795
926
  River Pro is kept in the separate, privately distributed `riverqueue-pro` gem, so possession of that package is the access boundary. It is not included in the MPL-2.0 core gem. Its implementation and documentation live in the private `riverqueue-ruby-pro` repository; see that repository's README for configuration and feature examples.
796
927
 
797
- ## Current differences from the Go client
928
+ ## Current limitations
798
929
 
799
- Ruby checks Go-generated fixtures for unique keys, cron, snooze counters, and
800
- shared protocol values.
930
+ Ruby checks shared fixtures for unique keys, cron, snooze counters, and
931
+ protocol values.
801
932
  See [conformance coverage and differences](conformance.md); this is not yet a
802
933
  full mixed-language runtime suite.
803
934
 
804
935
  The Ruby client does not currently provide dedicated OpenTelemetry/metrics
805
936
  integrations or
806
937
  transactional job completion alongside application writes. Plugins and
807
- subscriptions provide integration points for telemetry. Job execution uses Ruby
808
- worker objects rather than Go's work-function API.
938
+ subscriptions provide integration points for telemetry.
809
939
 
810
940
  ## Development
811
941
 
data/docs/conformance.md CHANGED
@@ -9,7 +9,7 @@ make test/ruby/conformance
9
9
  ```
10
10
 
11
11
  This generates `conformance/testdata/` and runs `ruby/spec/conformance_spec.rb`.
12
- No PostgreSQL server, pinned upstream checkout, protocol adapter, or separately
12
+ No Postgres server, pinned upstream checkout, protocol adapter, or separately
13
13
  maintained golden files are needed. Missing fixtures fail with instructions to
14
14
  regenerate them. The regular Ruby suite includes these checks too.
15
15
 
@@ -23,13 +23,13 @@ The fixture checks cover:
23
23
  - All snooze-counter fixtures through real worker attempts and both SQL drivers.
24
24
  - Cron occurrences, named zones, daylight-saving transitions, interval schedules,
25
25
  and invalid expressions, with the Ruby API differences below checked explicitly.
26
- - Insertion, cancellation, queue pause/resume/metadata, and leadership resignation
26
+ - Insertion, cancellation, queue pause/resume/metadata, and leadership resignation/request
27
27
  notification encoding through both SQL drivers, using in-memory SQLite and
28
28
  the bundled canonical migrations.
29
29
 
30
30
  The Ruby workflow runs on Ruby changes. The shared Conformance workflow also
31
31
  runs these tests when Go code changes. The ordinary driver suites retain
32
- PostgreSQL notification delivery/commit-ordering tests and shared PostgreSQL /
32
+ Postgres notification delivery/commit-ordering tests and shared Postgres /
33
33
  SQLite insertion, transaction, uniqueness, and worker tests.
34
34
 
35
35
  This replaces the old `insert-only-v1` adapter and its pinned external harness.
@@ -39,17 +39,17 @@ protocol is not part of the Ruby API.
39
39
 
40
40
  ## Coverage gaps and API differences
41
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.
42
+ Ruby consumes Postgres LISTEN events and the SQLite notification outbox. The
43
+ ordinary driver suites exercise insert wakeups, cancellation while draining,
44
+ queue pause/resume, and leadership handoff on `request_resign` through both
45
+ backends and both SQL drivers. They also check commit ordering, rollback,
46
+ independent listeners, startup failures, and recovery after listener failures.
47
+ The fixture target checks wire formats; live mixed-language worker execution
48
+ remains outside its scope.
49
49
 
50
50
  Shared snooze-counter fixtures cover non-negative integers and absent counters.
51
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.
52
+ tests cover its lenient conversion rules separately on Postgres and SQLite.
53
53
 
54
54
  Cron retains Ruby's existing Fugit extensions: six fields with seconds, Sunday
55
55
  as 7, hour 24, and descending ranges. These four expressions are accepted in