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.
data/docs/migrations.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Schema migrations
2
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;
3
+ River Ruby includes a migration API and the `river` command. The gems bundle
4
+ byte-for-byte copies of River's canonical Postgres and SQLite migration SQL,
5
+ pinned to release v0.48.0 through migration 008;
6
6
  only the upstream schema template placeholders are substituted when executing
7
7
  them.
8
8
 
@@ -24,7 +24,7 @@ bundle exec river migrate-up --database-url sqlite://storage/river.sqlite3
24
24
 
25
25
  `DATABASE_URL` supplies the URL when `--database-url` is omitted. ActiveRecord
26
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
27
+ Postgres schema, pass `--schema jobs`; otherwise the connection's current
28
28
  schema is used. Schema names must be simple SQL identifiers. SQLite always
29
29
  uses its main schema.
30
30
 
@@ -52,22 +52,22 @@ migrator.migrate(dry_run: true, target: 8)
52
52
  migrator.migrate(direction: :down) # One version; no interactive confirmation.
53
53
  ```
54
54
 
55
- The API accepts either driver and optionally `schema:` for PostgreSQL. `migrate`
55
+ The API accepts either driver and optionally `schema:` for Postgres. `migrate`
56
56
  returns the migration records applied (or planned in dry-run mode), with their
57
57
  version, name, and original up/down SQL. Each version's SQL and history update
58
58
  commit in their own transaction. A failing version rolls back while earlier
59
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
60
+ in an application transaction: some Postgres changes require a real commit
61
61
  before the next version can run.
62
62
 
63
63
  An up migration targeting an already-applied version does nothing, even when
64
64
  newer migrations are pending.
65
65
 
66
- ## Go compatibility and Pro
66
+ ## Compatibility and Pro
67
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
68
+ The migrator reads and writes the `river_migration` history shared by all River
69
+ clients, including the legacy pre-version-5 format. Any River client can
70
+ continue from versions applied by another. Unknown newer versions and gaps in history cause an error
71
71
  rather than guessing what SQL to run. It does not baseline an existing schema
72
72
  that has no River migration history.
73
73
 
@@ -75,7 +75,7 @@ Pro SQL is distributed only inside `riverqueue-pro`, not the public core gem.
75
75
  SQLite main migration 008 rebuilds `river_job` with `AUTOINCREMENT` so deleted
76
76
  job IDs are not reused. Its upgrade and downgrade refuse to run while Pro's
77
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.
78
+ running it on an existing Pro database. Postgres migration 008 is a no-op.
79
79
 
80
80
  Migrate main first, then Pro:
81
81
 
@@ -92,14 +92,14 @@ River::Pro::Migrator.new(driver).migrate
92
92
  ```
93
93
 
94
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
95
+ process at a time across languages. Ruby migrators take a Postgres advisory
96
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.
97
+ history changes. These locks are not shared with other River clients' migration runners.
98
98
 
99
99
  ## Updating the bundled SQL
100
100
 
101
101
  `migration/manifest.json` records each canonical file's SHA-256. From `ruby/`,
102
- synchronize or verify against the Go migrations in this same checkout:
102
+ synchronize or verify against the canonical migrations in this same checkout:
103
103
 
104
104
  ```sh
105
105
  ruby scripts/sync_migrations.rb
@@ -111,8 +111,8 @@ CI uses the same working-tree comparison; no separate checkout is needed.
111
111
 
112
112
  Pro migrations have their own manifest and sync script in the private
113
113
  `riverqueue-ruby-pro` repository. Run that repository's `make verify` against
114
- the Go Pro checkout; the public sync script handles only the main migrations.
114
+ the River Pro checkout; the public sync script handles only the main migrations.
115
115
 
116
116
  Review the upstream changes and run the complete test matrix before publishing.
117
117
  Do not edit the copied SQL independently. Upstream changes are bundled in gem
118
- releases; users do not need the Go repositories or network access at runtime.
118
+ releases; users do not need the upstream repositories or network access at runtime.
data/docs/testing.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Testing River jobs
2
2
 
3
- Testing helpers ship in `riverqueue`. They use your real PostgreSQL or SQLite
3
+ Testing helpers ship in `riverqueue`. They use your real Postgres or SQLite
4
4
  database through either driver; there is no fake queue, global testing mode,
5
5
  implicit migration, or automatic cleanup. Require them explicitly:
6
6
 
data/lib/client.rb CHANGED
@@ -49,6 +49,13 @@ module River
49
49
  @runtime.finish_claimed(row, error)
50
50
  end
51
51
 
52
+ # Internal scheduler entry point. Prepare ordinary insertion options and
53
+ # uniqueness before applying an occurrence fallback to the stored schedule.
54
+ def __insert_with_default_scheduled_at(args, insert_opts, default_scheduled_at)
55
+ insert_params = make_insert_params(args, insert_opts, default_scheduled_at)
56
+ run_insert_plugins([insert_params]) { [insert_and_check_unique_job(insert_params)] }.first
57
+ end
58
+
52
59
  def __perform_job(id, allow_scheduled: false)
53
60
  @runtime.perform_job(id, allow_scheduled: allow_scheduled)
54
61
  end
@@ -77,7 +84,8 @@ module River
77
84
  #
78
85
  # * `#kind`: A symbol or string identifying the job kind in the database.
79
86
  # * `#to_json`: Encodes the args to JSON for persistence in the database.
80
- # Must match encoding an args struct on the Go side to be workable.
87
+ # Must match the JSON expected by whichever River client works the
88
+ # job.
81
89
  #
82
90
  # They may also respond to `#insert_opts` which is expected to return an
83
91
  # `InsertOpts` that contains options that will apply to all jobs of this
@@ -106,8 +114,7 @@ module River
106
114
  def insert(args, insert_opts: nil, **options)
107
115
  raise ArgumentError, "use insert_opts or keyword options, not both" if insert_opts && !options.empty?
108
116
  insert_opts ||= options.empty? ? EMPTY_INSERT_OPTS : InsertOpts.new(**options)
109
- insert_params = make_insert_params(args, insert_opts)
110
- run_insert_plugins([insert_params]) { [insert_and_check_unique_job(insert_params)] }.first
117
+ __insert_with_default_scheduled_at(args, insert_opts, nil)
111
118
  end
112
119
 
113
120
  # Inserts many new jobs as part of a single batch operation for improved
@@ -134,7 +141,8 @@ module River
134
141
  #
135
142
  # * `#kind`: A symbol or string identifying the job kind in the database.
136
143
  # * `#to_json`: Encodes the args to JSON for persistence in the database.
137
- # Must match encoding an args struct on the Go side to be workable.
144
+ # Must match the JSON expected by whichever River client works the
145
+ # job.
138
146
  #
139
147
  # For example:
140
148
  #
@@ -182,6 +190,44 @@ module River
182
190
  @driver.job_cancel(id) || raise(NotFoundError, "job not found: #{id}")
183
191
  end
184
192
 
193
+ # Completes a running Job and returns its updated JobRow, including pending
194
+ # metadata and output. The original JobRow is not modified.
195
+ #
196
+ # Requires and joins the caller's transaction on the same Active Record
197
+ # connection or Sequel database. Commit before returning from work;
198
+ # propagate rollback errors so the runtime can retry the job. A committed
199
+ # completion takes precedence over any subsequent worker error.
200
+ #
201
+ # Raises NotFoundError for a missing job, JobCancelError for a pending
202
+ # cancellation, and Error if there is no active transaction or the job is
203
+ # no longer running or completed.
204
+ def job_complete_tx(job)
205
+ raise ArgumentError, "job must be a River::Job" unless job.is_a?(Job)
206
+ raise ArgumentError, "job must belong to this client" unless job.client.equal?(self)
207
+ raise Error, "job must be running" unless job.row.state == JOB_STATE_RUNNING
208
+ raise Error, "job_complete_tx requires an active transaction on the driver's connection" unless @driver.in_transaction?
209
+
210
+ # Mark the attempt before the write: an interrupt can arrive after the
211
+ # database commits but before the driver returns. The runtime checks the
212
+ # persisted state because the caller may still roll back this transaction.
213
+ job.__completion_attempted = true
214
+ now = @time_now_utc.call
215
+ completed = @driver.job_complete(id: job.row.id, finalized_at: now, expected_job: job.row, metadata: job.metadata_updates, now: now)
216
+ raise JobCancelError if completed == :cancelled
217
+
218
+ completed ||= job_get(job.row.id)
219
+ unless completed.attempt == job.row.attempt && completed.attempted_at == job.row.attempted_at
220
+ raise Error, "job attempt is no longer current"
221
+ end
222
+ raise Error, "job must be running" unless completed.state == JOB_STATE_COMPLETED
223
+
224
+ # Retention or application code may delete the row before work returns.
225
+ # Only retain a snapshot after the caller's outer transaction commits;
226
+ # rolled-back transactions and savepoints must not report completion.
227
+ @driver.after_commit { job.__completion_snapshot = completed }
228
+ completed
229
+ end
230
+
185
231
  # Deletes a job by ID and returns its former JobRow.
186
232
  #
187
233
  # Raises NotFoundError if the job does not exist and JobRunningError if it
@@ -310,7 +356,13 @@ module River
310
356
  @driver.queue_update(name.to_s, metadata: metadata) || raise(NotFoundError, "queue not found: #{name}")
311
357
  end
312
358
 
313
- # Starts polling configured queues and working jobs in background threads.
359
+ # Requests that the current leader resign. Delivery waits for the caller's
360
+ # Active Record or Sequel transaction to commit, if one is open.
361
+ def request_resign
362
+ @driver.request_resign
363
+ end
364
+
365
+ # Starts notification receiving, polling, and working configured queues.
314
366
  # Returns self.
315
367
  def start
316
368
  @runtime.start
@@ -379,7 +431,7 @@ module River
379
431
  JobInsertResult.new(job, unique_skipped_as_duplicated: unique_skipped_as_duplicate)
380
432
  end
381
433
 
382
- private def make_insert_params(args, insert_opts)
434
+ private def make_insert_params(args, insert_opts, default_scheduled_at = nil)
383
435
  raise ArgumentError, "args should respond to `#kind`" if !args.respond_to?(:kind)
384
436
 
385
437
  # ~all objects in Ruby respond to `#to_json`, so check non-nil instead.
@@ -424,6 +476,9 @@ module River
424
476
  insert_params.unique_states = unique_states
425
477
  end
426
478
 
479
+ if scheduled_at.nil? && default_scheduled_at
480
+ insert_params.scheduled_at = default_scheduled_at.getutc
481
+ end
427
482
  insert_params
428
483
  end
429
484