riverqueue 0.11.0 → 0.12.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 +132 -0
- data/LICENSE +374 -0
- data/docs/README.md +765 -0
- data/docs/migrations.md +121 -0
- data/docs/testing.md +173 -0
- data/docs/workers.md +126 -0
- data/exe/river +7 -0
- data/lib/cli.rb +55 -0
- data/lib/client.rb +314 -54
- data/lib/client_runtime.rb +612 -0
- data/lib/config.rb +154 -0
- data/lib/driver/job_row_decoder.rb +85 -0
- data/lib/driver/postgres_capabilities.rb +41 -0
- data/lib/driver/runtime.rb +661 -0
- data/lib/driver.rb +13 -6
- data/lib/errors.rb +83 -0
- data/lib/event.rb +78 -0
- data/lib/insert_opts.rb +40 -11
- data/lib/job.rb +28 -13
- data/lib/job_persisted_logging/plugin.rb +108 -0
- data/lib/migration_cli.rb +90 -0
- data/lib/migrator.rb +189 -0
- data/lib/params.rb +96 -0
- data/lib/periodic_cron.rb +24 -0
- data/lib/periodic_job.rb +135 -0
- data/lib/resumable.rb +148 -0
- data/lib/riverqueue/testing/minitest.rb +18 -0
- data/lib/riverqueue/testing/rspec.rb +109 -0
- data/lib/riverqueue/testing.rb +134 -0
- data/lib/riverqueue.rb +16 -0
- data/lib/unique_args.rb +97 -0
- data/lib/unique_bitmask.rb +14 -12
- data/lib/worker.rb +152 -0
- data/lib/worker_runner.rb +106 -0
- data/migration/LICENSE +374 -0
- data/migration/README.md +11 -0
- data/migration/manifest.json +38 -0
- data/migration/postgresql/main/001_create_river_migration.down.sql +1 -0
- data/migration/postgresql/main/001_create_river_migration.up.sql +8 -0
- data/migration/postgresql/main/002_initial_schema.down.sql +5 -0
- data/migration/postgresql/main/002_initial_schema.up.sql +96 -0
- data/migration/postgresql/main/003_river_job_tags_non_null.down.sql +3 -0
- data/migration/postgresql/main/003_river_job_tags_non_null.up.sql +3 -0
- data/migration/postgresql/main/004_pending_and_more.down.sql +42 -0
- data/migration/postgresql/main/004_pending_and_more.up.sql +45 -0
- data/migration/postgresql/main/005_migration_unique_client.down.sql +57 -0
- data/migration/postgresql/main/005_migration_unique_client.up.sql +79 -0
- data/migration/postgresql/main/006_bulk_unique.down.sql +16 -0
- data/migration/postgresql/main/006_bulk_unique.up.sql +40 -0
- data/migration/postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.down.sql +56 -0
- data/migration/postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.up.sql +44 -0
- data/migration/postgresql/main/008_job_id_autoincrement.down.sql +3 -0
- data/migration/postgresql/main/008_job_id_autoincrement.up.sql +3 -0
- data/migration/sqlite/main/001_create_river_migration.down.sql +1 -0
- data/migration/sqlite/main/001_create_river_migration.up.sql +8 -0
- data/migration/sqlite/main/002_initial_schema.down.sql +8 -0
- data/migration/sqlite/main/002_initial_schema.up.sql +19 -0
- data/migration/sqlite/main/003_river_job_tags_non_null.down.sql +6 -0
- data/migration/sqlite/main/003_river_job_tags_non_null.up.sql +6 -0
- data/migration/sqlite/main/004_pending_and_more.down.sql +26 -0
- data/migration/sqlite/main/004_pending_and_more.up.sql +33 -0
- data/migration/sqlite/main/005_migration_unique_client.down.sql +37 -0
- data/migration/sqlite/main/005_migration_unique_client.up.sql +64 -0
- data/migration/sqlite/main/006_bulk_unique.down.sql +7 -0
- data/migration/sqlite/main/006_bulk_unique.up.sql +63 -0
- data/migration/sqlite/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.down.sql +255 -0
- data/migration/sqlite/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.up.sql +261 -0
- data/migration/sqlite/main/008_job_id_autoincrement.down.sql +121 -0
- data/migration/sqlite/main/008_job_id_autoincrement.up.sql +123 -0
- data/sig/client.rbs +95 -0
- data/sig/driver.rbs +170 -0
- data/sig/insert_opts.rbs +25 -0
- data/sig/job.rbs +72 -0
- data/sig/job_persisted_logging/plugin.rbs +29 -0
- data/sig/migrator.rbs +44 -0
- data/sig/riverqueue.rbs +2 -0
- data/sig/runtime.rbs +382 -0
- data/sig/testing.rbs +95 -0
- data/sig/unique_args.rbs +8 -0
- data/sig/unique_bitmask.rbs +8 -0
- data/sig/worker_runner.rbs +22 -0
- metadata +165 -10
data/docs/migrations.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Schema migrations
|
|
2
|
+
|
|
3
|
+
River Ruby includes a migration API and the `river` command. Neither needs
|
|
4
|
+
Go installed. The gems bundle byte-for-byte copies of the PostgreSQL and SQLite
|
|
5
|
+
SQL from Go's River drivers, pinned to release v0.48.0 through migration 008;
|
|
6
|
+
only the upstream schema template placeholders are substituted when executing
|
|
7
|
+
them.
|
|
8
|
+
|
|
9
|
+
## Command line
|
|
10
|
+
|
|
11
|
+
Install `riverqueue-sequel` or `riverqueue-activerecord` and the application's
|
|
12
|
+
database gem (`pg` or `sqlite3`). The command auto-detects the River driver gem
|
|
13
|
+
available in your bundle, preferring Sequel when both are available. This choice
|
|
14
|
+
does not need to match your application's driver: both run the same migrations.
|
|
15
|
+
Create the database first, then run:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
bundle exec river migrate-status --database-url postgres://localhost/my_app
|
|
19
|
+
bundle exec river migrate-up --database-url postgres://localhost/my_app
|
|
20
|
+
|
|
21
|
+
# SQLite, using Sequel:
|
|
22
|
+
bundle exec river migrate-up --database-url sqlite://storage/river.sqlite3
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`DATABASE_URL` supplies the URL when `--database-url` is omitted. ActiveRecord
|
|
26
|
+
uses `sqlite3:` URLs instead of Sequel's `sqlite:` URLs. To target an existing
|
|
27
|
+
PostgreSQL schema, pass `--schema jobs`; otherwise the connection's current
|
|
28
|
+
schema is used. Schema names must be simple SQL identifiers. SQLite always
|
|
29
|
+
uses its main schema.
|
|
30
|
+
|
|
31
|
+
Up applies all pending versions. `--target N` stops at version N; `--steps N`
|
|
32
|
+
limits the number applied. `--dry-run` lists the plan without executing SQL.
|
|
33
|
+
Down defaults to one version and requires explicit confirmation:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
bundle exec river migrate-down --database-url postgres://localhost/my_app --dry-run
|
|
37
|
+
bundle exec river migrate-down --database-url postgres://localhost/my_app --yes
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Down migrations can delete jobs and other data. Back up the database and stop
|
|
41
|
+
workers first. `--target 0 --yes` removes the complete selected migration line.
|
|
42
|
+
|
|
43
|
+
## Ruby API
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
driver = River::Driver::Sequel.new(DB)
|
|
47
|
+
migrator = River::Migrator.new(driver)
|
|
48
|
+
|
|
49
|
+
migrator.status # Version/name records with an applied boolean.
|
|
50
|
+
migrator.migrate # Up to the newest bundled version.
|
|
51
|
+
migrator.migrate(dry_run: true, target: 8)
|
|
52
|
+
migrator.migrate(direction: :down) # One version; no interactive confirmation.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The API accepts either driver and optionally `schema:` for PostgreSQL. `migrate`
|
|
56
|
+
returns the migration records applied (or planned in dry-run mode), with their
|
|
57
|
+
version, name, and original up/down SQL. Each version's SQL and history update
|
|
58
|
+
commit in their own transaction. A failing version rolls back while earlier
|
|
59
|
+
versions remain committed, so rerunning resumes safely. Do not wrap the migrator
|
|
60
|
+
in an application transaction: some PostgreSQL changes require a real commit
|
|
61
|
+
before the next version can run.
|
|
62
|
+
|
|
63
|
+
An up migration targeting an already-applied version does nothing, even when
|
|
64
|
+
newer migrations are pending.
|
|
65
|
+
|
|
66
|
+
## Go compatibility and Pro
|
|
67
|
+
|
|
68
|
+
The migrator reads and writes Go's `river_migration` history, including the
|
|
69
|
+
legacy pre-version-5 format. Either language can continue from the other's
|
|
70
|
+
applied versions. Unknown newer versions and gaps in history cause an error
|
|
71
|
+
rather than guessing what SQL to run. It does not baseline an existing schema
|
|
72
|
+
that has no River migration history.
|
|
73
|
+
|
|
74
|
+
Pro SQL is distributed only inside `riverqueue-pro`, not the public core gem.
|
|
75
|
+
SQLite main migration 008 rebuilds `river_job` with `AUTOINCREMENT` so deleted
|
|
76
|
+
job IDs are not reused. Its upgrade and downgrade refuse to run while Pro's
|
|
77
|
+
workflow/sequence schema is installed; coordinate that upgrade with Pro before
|
|
78
|
+
running it on an existing Pro database. PostgreSQL migration 008 is a no-op.
|
|
79
|
+
|
|
80
|
+
Migrate main first, then Pro:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
bundle exec river migrate-up --database-url postgres://localhost/my_app
|
|
84
|
+
bundle exec river migrate-up --database-url postgres://localhost/my_app --line pro
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
require "riverqueue-pro"
|
|
89
|
+
|
|
90
|
+
River::Migrator.new(driver).migrate
|
|
91
|
+
River::Pro::Migrator.new(driver).migrate
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Remove other migration lines before downgrading main. Run only one migration
|
|
95
|
+
process at a time across languages. Ruby migrators take a PostgreSQL advisory
|
|
96
|
+
lock per schema; SQLite takes a write lock per version and checks for concurrent
|
|
97
|
+
history changes. These are not shared locks with Go's migration runner.
|
|
98
|
+
|
|
99
|
+
## Updating the bundled SQL
|
|
100
|
+
|
|
101
|
+
`migration/manifest.json` records the upstream commit and each file's SHA-256.
|
|
102
|
+
Synchronize or verify the public migrations against a local Go checkout:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
ruby scripts/sync_migrations.rb ../river
|
|
106
|
+
make verify
|
|
107
|
+
make verify RIVER_PATH=/path/to/river
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`make verify` defaults to `../river` and checks SQL contents, filenames, the
|
|
111
|
+
license, and the manifest's checksums and source revision. CI fetches the public
|
|
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.
|
|
114
|
+
|
|
115
|
+
Pro migrations have their own manifest and sync script in the private
|
|
116
|
+
`riverqueue-ruby-pro` repository. Run that repository's `make verify` against
|
|
117
|
+
the Go Pro checkout; the public sync script handles only the main migrations.
|
|
118
|
+
|
|
119
|
+
Review the upstream changes and run the complete test matrix before publishing.
|
|
120
|
+
Do not edit the copied SQL independently. Upstream changes are bundled in gem
|
|
121
|
+
releases; users do not need the Go repositories or network access at runtime.
|
data/docs/testing.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Testing River jobs
|
|
2
|
+
|
|
3
|
+
Testing helpers ship in `riverqueue`. They use your real PostgreSQL or SQLite
|
|
4
|
+
database through either driver; there is no fake queue, global testing mode,
|
|
5
|
+
implicit migration, or automatic cleanup. Require them explicitly:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require "riverqueue/testing"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Use an isolated test database/schema and a stopped client with registered workers.
|
|
12
|
+
Do not run background consumers or concurrent tests against the same job rows.
|
|
13
|
+
Your test suite owns migrations and cleanup. Transactional tests work when the
|
|
14
|
+
client uses the same database connection as the test transaction: synchronous
|
|
15
|
+
execution stays on the calling thread. Sharing a database URL is not sufficient.
|
|
16
|
+
|
|
17
|
+
## RSpec
|
|
18
|
+
|
|
19
|
+
Add `rspec-expectations` (or `rspec`) to your test bundle, then:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "riverqueue/testing/rspec"
|
|
23
|
+
|
|
24
|
+
RSpec.configure do |config|
|
|
25
|
+
config.include River::Testing::RSpec
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
expect { enqueue_order(42) }.to insert_job(
|
|
29
|
+
client,
|
|
30
|
+
args: {"order_id" => 42},
|
|
31
|
+
kind: :fulfill_order,
|
|
32
|
+
queue: :orders
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect { enqueue_orders }.to insert_jobs(client, count: 3, kind: :fulfill_order)
|
|
36
|
+
expect { ignore_duplicate_order }.not_to insert_job(client)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Attributes support composable RSpec expectations, including nested hash/array
|
|
40
|
+
matchers and time tolerances. Literal hashes still require all their keys to
|
|
41
|
+
match; use `a_hash_including` for a subset. JSON object keys are strings.
|
|
42
|
+
Identifier filters (`kind:`, `queue:`, and `state:`) accept symbols or strings;
|
|
43
|
+
literal JSON values and nested matchers are not coerced.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
expect { enqueue_order(42) }.to insert_job(
|
|
47
|
+
client,
|
|
48
|
+
args: a_hash_including("order_id" => 42),
|
|
49
|
+
scheduled_at: be_within(1).of(expected_time)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
expect { enqueue_orders }.to insert_jobs(client, kind: :fulfill_order).at_least(2)
|
|
53
|
+
expect { enqueue_orders }.to insert_jobs(client).at_most(5)
|
|
54
|
+
expect { enqueue_orders }.to insert_jobs(client).exactly(3)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`insert_job` and `insert_jobs` default to exactly one matching new row. `count:`
|
|
58
|
+
on `insert_jobs` remains shorthand for an exact count. Count chains accept
|
|
59
|
+
nonnegative integers; the last chain determines the count requirement. Other,
|
|
60
|
+
nonmatching jobs do not affect the count. Negation means **zero** matching rows,
|
|
61
|
+
not merely failure of the positive count requirement—even with `at_most` or
|
|
62
|
+
`at_least`. Use a positive `.exactly(0)` to assert a zero count explicitly.
|
|
63
|
+
|
|
64
|
+
Use `have_job` to inspect existing persisted jobs rather than insertions inside
|
|
65
|
+
a block. It defaults to at least one match and supports the same count chains:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
expect(client).to have_job(kind: :fulfill_order, queue: :orders)
|
|
69
|
+
expect(client).to have_job(args: a_hash_including("order_id" => 42)).exactly(1)
|
|
70
|
+
expect(client).not_to have_job(state: :discarded)
|
|
71
|
+
|
|
72
|
+
expect { enqueue_order_and_notification }.to(
|
|
73
|
+
insert_job(client, kind: :fulfill_order)
|
|
74
|
+
.and(insert_job(client, kind: :notify_order))
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`have_job` includes every job state unless filtered with `state:`; it does not
|
|
79
|
+
mean only waiting jobs. Insertion matchers support block expectations and
|
|
80
|
+
`have_job` supports value expectations. Compound insertion expectations execute
|
|
81
|
+
the application block once. Matchers only read persisted rows; they do not run
|
|
82
|
+
workers or clean up data. `River::Testing.jobs(client)` also returns all rows
|
|
83
|
+
through paginated reads for custom assertions.
|
|
84
|
+
|
|
85
|
+
These richer comparisons are RSpec-only. Framework-neutral and Minitest
|
|
86
|
+
insertion assertions use the same identifier normalization, but otherwise retain
|
|
87
|
+
exact attribute equality and exact counts.
|
|
88
|
+
|
|
89
|
+
## Minitest and other frameworks
|
|
90
|
+
|
|
91
|
+
Add `minitest` to your test bundle, then explicitly include its integration:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
require "minitest/autorun"
|
|
95
|
+
require "riverqueue/testing/minitest"
|
|
96
|
+
|
|
97
|
+
class OrderTest < Minitest::Test
|
|
98
|
+
include River::Testing::Minitest
|
|
99
|
+
|
|
100
|
+
def test_enqueue
|
|
101
|
+
row = assert_job_inserted client, args: {"order_id" => 42}, kind: :fulfill_order do
|
|
102
|
+
enqueue_order(42)
|
|
103
|
+
end
|
|
104
|
+
assert_equal "orders", row.queue
|
|
105
|
+
|
|
106
|
+
assert_jobs_inserted(client, count: 3) { enqueue_orders }
|
|
107
|
+
assert_no_jobs_inserted(client) { ignore_duplicate_order }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`client`, `enqueue_order`, and `enqueue_orders` above are application/test fixture
|
|
113
|
+
methods. The integration uses Minitest's assertion counts and failure reporting;
|
|
114
|
+
it does not enable autorun itself. Other frameworks can include
|
|
115
|
+
`River::Testing::Assertions` (failures raise `River::Testing::AssertionError`) or
|
|
116
|
+
use `River::Testing.inserted_jobs(client, **attributes) { ... }` directly.
|
|
117
|
+
|
|
118
|
+
Insertion checks compare row IDs before and after the block. A uniqueness conflict
|
|
119
|
+
returning an existing row does not count as an insertion. Rows deleted or rolled
|
|
120
|
+
back before the block ends cannot be observed. Exceptions from your block propagate.
|
|
121
|
+
|
|
122
|
+
## Execute one real attempt
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
result = River::Testing.perform_job(client, row.id)
|
|
126
|
+
|
|
127
|
+
expect(result).to have_attributes(error: nil, outcome: :completed)
|
|
128
|
+
expect(result.job).to have_attributes(attempt: 1, state: "completed")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This claims the specified job atomically, then uses the real runtime, including
|
|
132
|
+
plugins, timeout, retries, error handling, snoozes, cancellation, resumable steps,
|
|
133
|
+
metadata/output persistence, and finalization. It does not start consumer threads,
|
|
134
|
+
maintenance services, or periodic producers. The client cannot be started or used
|
|
135
|
+
for another synchronous attempt until execution finishes.
|
|
136
|
+
|
|
137
|
+
`ExecutionResult` contains `id`, `error` (the original exception, or `nil`), `job`
|
|
138
|
+
(the row after execution, or `nil` if deleted), and `outcome`: `:completed`,
|
|
139
|
+
`:retried`, `:discarded`, `:cancelled`, `:snoozed`, `:interrupted`, or `:deleted`.
|
|
140
|
+
Worker errors are returned, not re-raised; assert the outcome so a failed worker
|
|
141
|
+
cannot accidentally pass your test. Database/finalization failures can still raise.
|
|
142
|
+
|
|
143
|
+
Minitest and framework-neutral assertion modules also provide
|
|
144
|
+
`assert_job_completed(result)`, `assert_job_cancelled(result)`, and
|
|
145
|
+
`assert_job_discarded(result)`; each returns the result on success.
|
|
146
|
+
|
|
147
|
+
Only available, scheduled, or retryable jobs are eligible. Future jobs require an
|
|
148
|
+
explicit override; this does not change their original `scheduled_at`:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
result = River::Testing.perform_job(client, row.id, allow_scheduled: true)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Direct execution bypasses queue pause, worker capacity, and Pro concurrency claim
|
|
155
|
+
controls. It is a worker test helper, not a scheduler simulator. Pro plugins may
|
|
156
|
+
have additional effects (such as a batch worker claiming additional jobs).
|
|
157
|
+
Use regular threaded integration tests for these behaviors and maintenance-driven
|
|
158
|
+
workflow progression. With Rails, continue to use `ActiveJob::TestHelper` for
|
|
159
|
+
adapter-independent tests; use these helpers with a real River adapter/client to
|
|
160
|
+
test persistence and execution.
|
|
161
|
+
|
|
162
|
+
## Drain a queue
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
results = River::Testing.drain(client, max_jobs: 20, queue: :orders)
|
|
166
|
+
expect(results.map(&:outcome)).to all(eq(:completed))
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Draining runs due jobs sequentially in priority, scheduled-time, then ID order,
|
|
170
|
+
including jobs inserted by workers. It does not sleep for future jobs or run
|
|
171
|
+
maintenance. Due scheduled/retryable jobs can be claimed directly. The default
|
|
172
|
+
limit is 100 attempts; if runnable work remains at the limit, it raises
|
|
173
|
+
`River::Testing::DrainLimitError`. Attempts already performed remain persisted.
|
data/docs/workers.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Dedicated worker processes
|
|
2
|
+
|
|
3
|
+
Run River consumers separately from web processes with `river worker`. The
|
|
4
|
+
command owns one foreground process: it boots your application, starts a client,
|
|
5
|
+
handles stop signals, and waits for workers. It does not daemonize, fork
|
|
6
|
+
children, run migrations, or supervise replacement processes.
|
|
7
|
+
|
|
8
|
+
## Plain Ruby
|
|
9
|
+
|
|
10
|
+
Install `riverqueue`, your driver gem, and the database gem that driver needs.
|
|
11
|
+
Apply [migrations](./migrations.md) before starting consumers. Create a Ruby
|
|
12
|
+
configuration file whose **last expression returns an unstarted client**:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# config/river.rb
|
|
16
|
+
require_relative "../app" # Boots your application and defines SortWorker.
|
|
17
|
+
require "riverqueue-sequel"
|
|
18
|
+
|
|
19
|
+
River::Client.new(
|
|
20
|
+
River::Driver::Sequel.new(
|
|
21
|
+
Sequel.connect(ENV.fetch("DATABASE_URL"), max_connections: 20)
|
|
22
|
+
),
|
|
23
|
+
config: River::Config.new(
|
|
24
|
+
queues: {ruby: 10},
|
|
25
|
+
workers: River::Workers.new.add(SortWorker)
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Run from your application's root directory:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
bundle exec river worker --config config/river.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Do not call `.start` in the configuration file or install your own signal
|
|
37
|
+
handlers. Configure at least one queue and register its workers. Producers must
|
|
38
|
+
insert into the same database/schema and queue names. Ten workers means up to
|
|
39
|
+
ten concurrent job threads in this process, not ten OS processes.
|
|
40
|
+
|
|
41
|
+
The configuration file is evaluated as Ruby, not parsed as data; use only trusted
|
|
42
|
+
application code. It can return an ActiveRecord-backed client or a
|
|
43
|
+
`River::Pro::Client` instead. Pro must be installed and required by the application;
|
|
44
|
+
the command does not load it automatically. Pool sizing is application-specific:
|
|
45
|
+
allow connections for queue producers, maintenance, and application database work
|
|
46
|
+
as well as worker threads. The example's pool size is illustrative.
|
|
47
|
+
|
|
48
|
+
## Rails
|
|
49
|
+
|
|
50
|
+
Install and configure [riverqueue-rails](../rails/riverqueue-rails/README.md), then
|
|
51
|
+
run from the Rails application root:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
RAILS_ENV=production bundle exec river worker --rails
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This boots `config/environment.rb` and builds a consumer from
|
|
58
|
+
`Rails.application.config.river`, including the configured connection class,
|
|
59
|
+
Active Job worker, and Rails execution wrapping. Configure native workers through
|
|
60
|
+
the same integration when needed. Keep web processes insertion-only; do not start
|
|
61
|
+
a consumer in a web initializer.
|
|
62
|
+
|
|
63
|
+
The generated `bin/jobs start` also invokes the shared runner. Use the `river`
|
|
64
|
+
command for the CLI options shown here. Do not combine `--rails` with `--config`.
|
|
65
|
+
|
|
66
|
+
## Stopping
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
bundle exec river worker --config config/river.rb --stop-timeout 60
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The graceful stop timeout defaults to 30 seconds for plain Ruby. Rails uses
|
|
73
|
+
`config.river.stop_timeout`; `--stop-timeout` overrides it. The value must
|
|
74
|
+
be finite and nonnegative; zero skips the grace period. It is separate from the
|
|
75
|
+
per-job execution timeout.
|
|
76
|
+
|
|
77
|
+
| Signal / event | Behavior |
|
|
78
|
+
|-----------------------|------------------------------------------------------------------------------------------|
|
|
79
|
+
| `SIGTSTP` | Calls `client.stop(wait: false)`: stops accepting work but keeps the process alive. |
|
|
80
|
+
| `SIGTERM` / `SIGINT` | Requests a stop, waits for active attempts, then exits. |
|
|
81
|
+
| Another stop signal | Skips the remaining grace period and interrupts active attempts. |
|
|
82
|
+
| Grace period expires | Interrupts active attempts and allows five seconds for finalization. |
|
|
83
|
+
| Finalization expires | Forces process exit with status 1, bypassing `at_exit` handlers. |
|
|
84
|
+
|
|
85
|
+
After interruption, a further stop signal can force exit before the finalization
|
|
86
|
+
window ends. In-flight fetches or maintenance operations may finish after stop
|
|
87
|
+
is requested. `SIGTSTP` does not pause queues in the database, does not start the
|
|
88
|
+
stop deadline, and does not exit automatically when work drains. Send
|
|
89
|
+
`SIGTERM` afterward to complete stop; there is no signal to resume fetching.
|
|
90
|
+
|
|
91
|
+
Attempts successfully finalized as interrupted become available again without
|
|
92
|
+
consuming the attempt. If the process dies before finalization, running jobs are
|
|
93
|
+
left for normal stuck-job recovery. Workers must tolerate retries; stop cannot
|
|
94
|
+
roll back external side effects. Keep an external supervisor's kill deadline as
|
|
95
|
+
a backstop for uninterruptible native calls or blocked application code.
|
|
96
|
+
|
|
97
|
+
For `river worker`, a clean drain exits with status 0. Boot failures, detected
|
|
98
|
+
runtime-thread failures, and interrupted stops return status 1; forced
|
|
99
|
+
termination also exits with status 1. Lifecycle messages go to standard output;
|
|
100
|
+
worker logging uses the client's configured logger.
|
|
101
|
+
|
|
102
|
+
## Deployment
|
|
103
|
+
|
|
104
|
+
Configure your deployment platform's worker command as one of the commands above.
|
|
105
|
+
Run multiple independent instances for multiple processes; queue concurrency is
|
|
106
|
+
per process. A shell wrapper should use `exec` so signals reach the worker:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
exec bundle exec river worker --config config/river.rb --stop-timeout 30
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Give the supervisor more than the graceful timeout plus the five-second
|
|
113
|
+
finalization window before it forcibly kills the process, with additional margin
|
|
114
|
+
for scheduling and cleanup. Migrate once as a deployment step, not on each worker
|
|
115
|
+
boot. Configure restart policy in the supervisor, accounting for status 1 after
|
|
116
|
+
an interrupted stop.
|
|
117
|
+
|
|
118
|
+
For a rolling replacement, start new instances, confirm startup and database
|
|
119
|
+
connectivity, then send `SIGTERM` to old instances and allow them to drain. An
|
|
120
|
+
optional earlier `SIGTSTP` stops old instances from accepting more work. The
|
|
121
|
+
`River worker: ready pid=...` log means the initial database check and client start
|
|
122
|
+
have completed; it is not a health endpoint or a guarantee of ongoing queue health.
|
|
123
|
+
|
|
124
|
+
River does not provide a multiprocess supervisor, rolling-restart coordinator,
|
|
125
|
+
PID-file management, or a readiness probe server. Those remain deployment-platform
|
|
126
|
+
responsibilities.
|
data/exe/river
ADDED
data/lib/cli.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "migration_cli"
|
|
5
|
+
require_relative "worker_runner"
|
|
6
|
+
|
|
7
|
+
module River
|
|
8
|
+
# Shared command dispatch for migrations and dedicated worker processes.
|
|
9
|
+
module CLI
|
|
10
|
+
# Runs a River command and returns its process exit status.
|
|
11
|
+
def self.run(argv, err: $stderr, out: $stdout)
|
|
12
|
+
return worker(argv.drop(1), out: out) if argv.first == "worker"
|
|
13
|
+
|
|
14
|
+
if argv.empty? || %w[-h --help].include?(argv.first)
|
|
15
|
+
out.puts("Usage: river worker|migrate-up|migrate-down|migrate-status [options]")
|
|
16
|
+
return 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
MigrationCLI.run(argv, err: err, out: out)
|
|
20
|
+
rescue StandardError, LoadError, SyntaxError => error
|
|
21
|
+
err.puts("Worker failed: #{error.class}: #{error.message}")
|
|
22
|
+
1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.worker(argv, out:)
|
|
26
|
+
options = {} #: Hash[Symbol, untyped]
|
|
27
|
+
parser = OptionParser.new do |args|
|
|
28
|
+
args.banner = "Usage: river worker --config FILE | --rails [options]"
|
|
29
|
+
args.on("--config FILE", "Ruby file returning an unstarted River client") { |value| options[:config] = value }
|
|
30
|
+
args.on("--rails", "Boot config/environment.rb in the current directory") { options[:rails] = true }
|
|
31
|
+
args.on("--stop-timeout SECONDS", Float) { |value| options[:stop_timeout] = value }
|
|
32
|
+
args.on("-h", "--help") do
|
|
33
|
+
out.puts(args)
|
|
34
|
+
return 0
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
args = argv.dup
|
|
39
|
+
parser.parse!(args)
|
|
40
|
+
raise ArgumentError, parser.banner unless args.empty? && (!!options[:config] ^ !!options[:rails])
|
|
41
|
+
|
|
42
|
+
if options[:rails]
|
|
43
|
+
require File.expand_path("config/environment.rb")
|
|
44
|
+
require "riverqueue-rails"
|
|
45
|
+
Object.const_get("River::Rails::Runner").start(out: out, stop_timeout: options[:stop_timeout])
|
|
46
|
+
else
|
|
47
|
+
path = File.expand_path(options[:config])
|
|
48
|
+
client = TOPLEVEL_BINDING.eval(File.read(path), path)
|
|
49
|
+
raise ArgumentError, "configuration must return a River::Client" unless client.is_a?(River::Client)
|
|
50
|
+
|
|
51
|
+
WorkerRunner.new(client, out: out, stop_timeout: options.fetch(:stop_timeout, 30)).run
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|