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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -0
  3. data/LICENSE +374 -0
  4. data/docs/README.md +765 -0
  5. data/docs/migrations.md +121 -0
  6. data/docs/testing.md +173 -0
  7. data/docs/workers.md +126 -0
  8. data/exe/river +7 -0
  9. data/lib/cli.rb +55 -0
  10. data/lib/client.rb +314 -54
  11. data/lib/client_runtime.rb +612 -0
  12. data/lib/config.rb +154 -0
  13. data/lib/driver/job_row_decoder.rb +85 -0
  14. data/lib/driver/postgres_capabilities.rb +41 -0
  15. data/lib/driver/runtime.rb +661 -0
  16. data/lib/driver.rb +13 -6
  17. data/lib/errors.rb +83 -0
  18. data/lib/event.rb +78 -0
  19. data/lib/insert_opts.rb +40 -11
  20. data/lib/job.rb +28 -13
  21. data/lib/job_persisted_logging/plugin.rb +108 -0
  22. data/lib/migration_cli.rb +90 -0
  23. data/lib/migrator.rb +189 -0
  24. data/lib/params.rb +96 -0
  25. data/lib/periodic_cron.rb +24 -0
  26. data/lib/periodic_job.rb +135 -0
  27. data/lib/resumable.rb +148 -0
  28. data/lib/riverqueue/testing/minitest.rb +18 -0
  29. data/lib/riverqueue/testing/rspec.rb +109 -0
  30. data/lib/riverqueue/testing.rb +134 -0
  31. data/lib/riverqueue.rb +16 -0
  32. data/lib/unique_args.rb +97 -0
  33. data/lib/unique_bitmask.rb +14 -12
  34. data/lib/worker.rb +152 -0
  35. data/lib/worker_runner.rb +106 -0
  36. data/migration/LICENSE +374 -0
  37. data/migration/README.md +11 -0
  38. data/migration/manifest.json +38 -0
  39. data/migration/postgresql/main/001_create_river_migration.down.sql +1 -0
  40. data/migration/postgresql/main/001_create_river_migration.up.sql +8 -0
  41. data/migration/postgresql/main/002_initial_schema.down.sql +5 -0
  42. data/migration/postgresql/main/002_initial_schema.up.sql +96 -0
  43. data/migration/postgresql/main/003_river_job_tags_non_null.down.sql +3 -0
  44. data/migration/postgresql/main/003_river_job_tags_non_null.up.sql +3 -0
  45. data/migration/postgresql/main/004_pending_and_more.down.sql +42 -0
  46. data/migration/postgresql/main/004_pending_and_more.up.sql +45 -0
  47. data/migration/postgresql/main/005_migration_unique_client.down.sql +57 -0
  48. data/migration/postgresql/main/005_migration_unique_client.up.sql +79 -0
  49. data/migration/postgresql/main/006_bulk_unique.down.sql +16 -0
  50. data/migration/postgresql/main/006_bulk_unique.up.sql +40 -0
  51. data/migration/postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.down.sql +56 -0
  52. data/migration/postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.up.sql +44 -0
  53. data/migration/postgresql/main/008_job_id_autoincrement.down.sql +3 -0
  54. data/migration/postgresql/main/008_job_id_autoincrement.up.sql +3 -0
  55. data/migration/sqlite/main/001_create_river_migration.down.sql +1 -0
  56. data/migration/sqlite/main/001_create_river_migration.up.sql +8 -0
  57. data/migration/sqlite/main/002_initial_schema.down.sql +8 -0
  58. data/migration/sqlite/main/002_initial_schema.up.sql +19 -0
  59. data/migration/sqlite/main/003_river_job_tags_non_null.down.sql +6 -0
  60. data/migration/sqlite/main/003_river_job_tags_non_null.up.sql +6 -0
  61. data/migration/sqlite/main/004_pending_and_more.down.sql +26 -0
  62. data/migration/sqlite/main/004_pending_and_more.up.sql +33 -0
  63. data/migration/sqlite/main/005_migration_unique_client.down.sql +37 -0
  64. data/migration/sqlite/main/005_migration_unique_client.up.sql +64 -0
  65. data/migration/sqlite/main/006_bulk_unique.down.sql +7 -0
  66. data/migration/sqlite/main/006_bulk_unique.up.sql +63 -0
  67. data/migration/sqlite/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.down.sql +255 -0
  68. data/migration/sqlite/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.up.sql +261 -0
  69. data/migration/sqlite/main/008_job_id_autoincrement.down.sql +121 -0
  70. data/migration/sqlite/main/008_job_id_autoincrement.up.sql +123 -0
  71. data/sig/client.rbs +95 -0
  72. data/sig/driver.rbs +170 -0
  73. data/sig/insert_opts.rbs +25 -0
  74. data/sig/job.rbs +72 -0
  75. data/sig/job_persisted_logging/plugin.rbs +29 -0
  76. data/sig/migrator.rbs +44 -0
  77. data/sig/riverqueue.rbs +2 -0
  78. data/sig/runtime.rbs +382 -0
  79. data/sig/testing.rbs +95 -0
  80. data/sig/unique_args.rbs +8 -0
  81. data/sig/unique_bitmask.rbs +8 -0
  82. data/sig/worker_runner.rbs +22 -0
  83. metadata +165 -10
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "riverqueue"
5
+ require_relative "../lib/cli"
6
+
7
+ exit River::CLI.run(ARGV)
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