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/lib/client.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/sha2"
|
|
2
4
|
require "time"
|
|
3
5
|
|
|
4
6
|
module River
|
|
@@ -11,10 +13,7 @@ module River
|
|
|
11
13
|
# Default queue for a job.
|
|
12
14
|
QUEUE_DEFAULT = "default"
|
|
13
15
|
|
|
14
|
-
# Provides a client for
|
|
15
|
-
# River client, this one can insert jobs only. Jobs can only be worked from Go
|
|
16
|
-
# code, so job arg kinds and JSON encoding details must be shared between Ruby
|
|
17
|
-
# and Go code.
|
|
16
|
+
# Provides a River client for inserting and working jobs.
|
|
18
17
|
#
|
|
19
18
|
# Used in conjunction with a River driver like:
|
|
20
19
|
#
|
|
@@ -24,11 +23,38 @@ module River
|
|
|
24
23
|
# River drivers are found in separate gems like `riverqueue-sequel` to help
|
|
25
24
|
# minimize transient dependencies.
|
|
26
25
|
class Client
|
|
27
|
-
|
|
26
|
+
# Configuration used by this client.
|
|
27
|
+
attr_reader :config
|
|
28
|
+
|
|
29
|
+
# Database driver used by this client.
|
|
30
|
+
attr_reader :driver
|
|
31
|
+
|
|
32
|
+
# Creates a client backed by the given database driver. Pass a Config to
|
|
33
|
+
# customize workers, queues, plugins, and runtime behavior.
|
|
34
|
+
def initialize(driver, config: nil)
|
|
35
|
+
@config = config || Config.new
|
|
28
36
|
@driver = driver
|
|
37
|
+
@runtime = ClientRuntime.new(self, driver, @config)
|
|
29
38
|
@time_now_utc = -> { Time.now.utc } # for test time stubbing
|
|
30
39
|
end
|
|
31
40
|
|
|
41
|
+
# Internal extension point used by separately packaged batch workers after
|
|
42
|
+
# they atomically claim additional jobs alongside the batch leader.
|
|
43
|
+
def __finish_claimed_job(row, error = nil)
|
|
44
|
+
@runtime.finish_claimed(row, error)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def __perform_job(id, allow_scheduled: false)
|
|
48
|
+
@runtime.perform_job(id, allow_scheduled: allow_scheduled)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def __interrupt_workers = @runtime.interrupt_workers
|
|
52
|
+
|
|
53
|
+
def __runtime_healthy? = @runtime.healthy?
|
|
54
|
+
|
|
55
|
+
# Returns the client ID recorded on jobs while this client works them.
|
|
56
|
+
def id = config.id
|
|
57
|
+
|
|
32
58
|
# Inserts a new job for work given a job args implementation and insertion
|
|
33
59
|
# options (which may be omitted).
|
|
34
60
|
#
|
|
@@ -37,14 +63,14 @@ module River
|
|
|
37
63
|
# insert_res = client.insert(SimpleArgs.new(job_num: 1))
|
|
38
64
|
# insert_res.job # inserted job row
|
|
39
65
|
#
|
|
40
|
-
# With
|
|
66
|
+
# With keyword options:
|
|
41
67
|
#
|
|
42
|
-
# insert_res = client.insert(SimpleArgs.new(job_num: 1),
|
|
68
|
+
# insert_res = client.insert(SimpleArgs.new(job_num: 1), queue: :high_priority)
|
|
43
69
|
# insert_res.job # inserted job row
|
|
44
70
|
#
|
|
45
71
|
# Job arg implementations are expected to respond to:
|
|
46
72
|
#
|
|
47
|
-
# * `#kind`: A
|
|
73
|
+
# * `#kind`: A symbol or string identifying the job kind in the database.
|
|
48
74
|
# * `#to_json`: Encodes the args to JSON for persistence in the database.
|
|
49
75
|
# Must match encoding an args struct on the Go side to be workable.
|
|
50
76
|
#
|
|
@@ -64,15 +90,19 @@ module River
|
|
|
64
90
|
#
|
|
65
91
|
# def kind = "simple"
|
|
66
92
|
#
|
|
67
|
-
# def to_json = JSON.
|
|
93
|
+
# def to_json = JSON.generate({job_num: job_num})
|
|
68
94
|
# end
|
|
69
95
|
#
|
|
70
96
|
# See also JobArgsHash for an easy way to insert a job from a hash.
|
|
71
97
|
#
|
|
72
|
-
# Returns an instance of
|
|
73
|
-
|
|
98
|
+
# Returns an instance of JobInsertResult. A reusable InsertOpts may instead
|
|
99
|
+
# be supplied as +insert_opts:+; do not combine it with keyword options.
|
|
100
|
+
# @type method insert: (jobArgs, ?insert_opts: InsertOpts?, **untyped) -> JobInsertResult
|
|
101
|
+
def insert(args, insert_opts: nil, **options)
|
|
102
|
+
raise ArgumentError, "use insert_opts or keyword options, not both" if insert_opts && !options.empty?
|
|
103
|
+
insert_opts ||= options.empty? ? EMPTY_INSERT_OPTS : InsertOpts.new(**options)
|
|
74
104
|
insert_params = make_insert_params(args, insert_opts)
|
|
75
|
-
insert_and_check_unique_job(insert_params)
|
|
105
|
+
run_insert_plugins([insert_params]) { [insert_and_check_unique_job(insert_params)] }.first
|
|
76
106
|
end
|
|
77
107
|
|
|
78
108
|
# Inserts many new jobs as part of a single batch operation for improved
|
|
@@ -83,21 +113,21 @@ module River
|
|
|
83
113
|
#
|
|
84
114
|
# With job args:
|
|
85
115
|
#
|
|
86
|
-
#
|
|
116
|
+
# insert_results = client.insert_many([
|
|
87
117
|
# SimpleArgs.new(job_num: 1),
|
|
88
118
|
# SimpleArgs.new(job_num: 2)
|
|
89
119
|
# ])
|
|
90
120
|
#
|
|
91
121
|
# With InsertManyParams:
|
|
92
122
|
#
|
|
93
|
-
#
|
|
94
|
-
# River::InsertManyParams.new(SimpleArgs.new(job_num: 1),
|
|
95
|
-
# River::InsertManyParams.new(SimpleArgs.new(job_num: 2),
|
|
123
|
+
# insert_results = client.insert_many([
|
|
124
|
+
# River::InsertManyParams.new(SimpleArgs.new(job_num: 1), max_attempts: 5),
|
|
125
|
+
# River::InsertManyParams.new(SimpleArgs.new(job_num: 2), queue: :high_priority)
|
|
96
126
|
# ])
|
|
97
127
|
#
|
|
98
128
|
# Job arg implementations are expected to respond to:
|
|
99
129
|
#
|
|
100
|
-
# * `#kind`: A
|
|
130
|
+
# * `#kind`: A symbol or string identifying the job kind in the database.
|
|
101
131
|
# * `#to_json`: Encodes the args to JSON for persistence in the database.
|
|
102
132
|
# Must match encoding an args struct on the Go side to be workable.
|
|
103
133
|
#
|
|
@@ -112,13 +142,18 @@ module River
|
|
|
112
142
|
#
|
|
113
143
|
# def kind = "simple"
|
|
114
144
|
#
|
|
115
|
-
# def to_json = JSON.
|
|
145
|
+
# def to_json = JSON.generate({job_num: job_num})
|
|
116
146
|
# end
|
|
117
147
|
#
|
|
118
148
|
# See also JobArgsHash for an easy way to insert a job from a hash.
|
|
119
149
|
#
|
|
120
|
-
# Returns
|
|
150
|
+
# Returns one JobInsertResult for each argument, in input order.
|
|
151
|
+
# Raises ArgumentError if the batch is empty.
|
|
152
|
+
# A batch cannot contain repeated unique keys in their enforced states;
|
|
153
|
+
# the whole batch fails instead of partially inserting it.
|
|
121
154
|
def insert_many(args)
|
|
155
|
+
raise ArgumentError, "no jobs to insert" if args.empty?
|
|
156
|
+
|
|
122
157
|
all_params = args.map do |arg|
|
|
123
158
|
if arg.is_a?(InsertManyParams)
|
|
124
159
|
make_insert_params(arg.args, arg.insert_opts || EMPTY_INSERT_OPTS)
|
|
@@ -127,10 +162,179 @@ module River
|
|
|
127
162
|
end
|
|
128
163
|
end
|
|
129
164
|
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
|
|
165
|
+
run_insert_plugins(all_params) do
|
|
166
|
+
@driver.job_insert_many(all_params)
|
|
167
|
+
.map do |job, unique_skipped_as_duplicate|
|
|
168
|
+
JobInsertResult.new(job, unique_skipped_as_duplicated: unique_skipped_as_duplicate)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Cancels a job by ID and returns its updated JobRow.
|
|
174
|
+
#
|
|
175
|
+
# Raises NotFoundError if the job does not exist.
|
|
176
|
+
def job_cancel(id)
|
|
177
|
+
@driver.job_cancel(id) || raise(NotFoundError, "job not found: #{id}")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Deletes a job by ID and returns its former JobRow.
|
|
181
|
+
#
|
|
182
|
+
# Raises NotFoundError if the job does not exist and JobRunningError if it
|
|
183
|
+
# is currently running.
|
|
184
|
+
def job_delete(id)
|
|
185
|
+
job = @driver.job_delete(id) || raise(NotFoundError, "job not found: #{id}")
|
|
186
|
+
raise JobRunningError, "running jobs cannot be deleted" if job.state == JOB_STATE_RUNNING
|
|
187
|
+
|
|
188
|
+
job
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Deletes jobs matching the supplied filters and returns a
|
|
192
|
+
# JobDeleteManyResult. At least one filter is required. Accepts keyword
|
|
193
|
+
# filters or a reusable JobListParams, but not both.
|
|
194
|
+
# @type method job_delete_many: (?JobListParams?, **untyped) -> untyped
|
|
195
|
+
def job_delete_many(params = nil, **filters)
|
|
196
|
+
raise ArgumentError, "use params or keyword filters, not both" if params && !filters.empty?
|
|
197
|
+
params ||= JobListParams.new(**filters)
|
|
198
|
+
raise ArgumentError, "delete with no filters is not allowed" unless params.filters?
|
|
199
|
+
|
|
200
|
+
JobDeleteManyResult.new(@driver.job_delete_many(params))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Fetches a job by ID.
|
|
204
|
+
#
|
|
205
|
+
# Raises NotFoundError if the job does not exist.
|
|
206
|
+
def job_get(id)
|
|
207
|
+
@driver.job_get_by_id(id) || raise(NotFoundError, "job not found: #{id}")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Lists jobs matching the supplied filters and ordering.
|
|
211
|
+
#
|
|
212
|
+
# The returned JobListResult includes a cursor suitable for the next page.
|
|
213
|
+
# Accepts keyword filters or a reusable JobListParams, but not both.
|
|
214
|
+
# @type method job_list: (?JobListParams?, **untyped) -> untyped
|
|
215
|
+
def job_list(params = nil, **filters)
|
|
216
|
+
raise ArgumentError, "use params or keyword filters, not both" if params && !filters.empty?
|
|
217
|
+
params ||= JobListParams.new(**filters)
|
|
218
|
+
jobs = @driver.job_list(params)
|
|
219
|
+
last = jobs.last
|
|
220
|
+
cursor = last && JobListCursor.new(id: last.id, sort_by: params.sort_by, sort_order: params.sort_order, value: last.public_send(params.sort_by))
|
|
221
|
+
JobListResult.new(jobs, cursor)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Makes a non-running job immediately available for another attempt and
|
|
225
|
+
# returns its updated JobRow. Raises NotFoundError if it does not exist.
|
|
226
|
+
def job_retry(id)
|
|
227
|
+
@driver.job_retry(id) || raise(NotFoundError, "job not found: #{id}")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Applies keyword attributes or a JobUpdateParams to a job and returns its
|
|
231
|
+
# updated JobRow. Explicit nil clears nullable fields; omitted fields are
|
|
232
|
+
# unchanged. Raises NotFoundError if the job does not exist.
|
|
233
|
+
# @type method job_update: (Integer, ?JobUpdateParams?, **untyped) -> JobRow
|
|
234
|
+
def job_update(id, params = nil, **attributes)
|
|
235
|
+
raise ArgumentError, "use params or keyword attributes, not both" if params && !attributes.empty?
|
|
236
|
+
params ||= JobUpdateParams.new(**attributes)
|
|
237
|
+
@driver.job_update(id, params) || raise(NotFoundError, "job not found: #{id}")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Returns the live PeriodicJobBundle used to add and remove periodic jobs.
|
|
241
|
+
def periodic_jobs
|
|
242
|
+
raise ArgumentError, "cannot modify periodic jobs when leader_election_disabled is true" if @config.leader_election_disabled
|
|
243
|
+
|
|
244
|
+
@runtime.periodic_jobs
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Adds a queue to the client and returns self. If the client is running, it
|
|
248
|
+
# begins working the queue immediately.
|
|
249
|
+
def queue_add(name, queue_config)
|
|
250
|
+
@runtime.queue_add(name.to_s, queue_config)
|
|
251
|
+
self
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Fetches a queue by name.
|
|
255
|
+
#
|
|
256
|
+
# Raises NotFoundError if the queue does not exist.
|
|
257
|
+
def queue_get(name)
|
|
258
|
+
@driver.queue_get(name.to_s) || raise(NotFoundError, "queue not found: #{name}")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Lists up to +max+ known queues.
|
|
262
|
+
def queue_list(max: 100)
|
|
263
|
+
QueueListResult.new(@driver.queue_list(max: max))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Pauses a named queue, or all queues when +name+ is +"*"+.
|
|
267
|
+
def queue_pause(name)
|
|
268
|
+
name = name.to_s
|
|
269
|
+
@driver.queue_pause(name)
|
|
270
|
+
@runtime.wake
|
|
271
|
+
queues = (name == "*") ? @driver.queue_list : [@driver.queue_get(name)].compact
|
|
272
|
+
queues.each do |queue|
|
|
273
|
+
@runtime.publish_queue(EVENT_QUEUE_PAUSED, queue)
|
|
133
274
|
end
|
|
275
|
+
|
|
276
|
+
true
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Removes a configured queue, waits for active jobs in it to finish, and
|
|
280
|
+
# returns self.
|
|
281
|
+
def queue_remove(name)
|
|
282
|
+
@runtime.queue_remove(name.to_s)
|
|
283
|
+
self
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Resumes a named queue, or all queues when +name+ is +"*"+.
|
|
287
|
+
def queue_resume(name)
|
|
288
|
+
name = name.to_s
|
|
289
|
+
@driver.queue_resume(name)
|
|
290
|
+
@runtime.wake
|
|
291
|
+
queues = (name == "*") ? @driver.queue_list : [@driver.queue_get(name)].compact
|
|
292
|
+
queues.each do |queue|
|
|
293
|
+
@runtime.publish_queue(EVENT_QUEUE_RESUMED, queue)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
true
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Replaces a queue's metadata and returns the updated Queue.
|
|
300
|
+
def queue_update(name, metadata:)
|
|
301
|
+
@driver.queue_update(name.to_s, metadata: metadata) || raise(NotFoundError, "queue not found: #{name}")
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Starts polling configured queues and working jobs in background threads.
|
|
305
|
+
# Returns self.
|
|
306
|
+
def start
|
|
307
|
+
@runtime.start
|
|
308
|
+
self
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Returns true while the client runtime is started.
|
|
312
|
+
def started? = @runtime.started?
|
|
313
|
+
|
|
314
|
+
# Stops fetching and producing periodic jobs, waits for active jobs to
|
|
315
|
+
# finish, and returns self. Pass wait: false to request stop without
|
|
316
|
+
# waiting; call stop again to finish draining and release resources.
|
|
317
|
+
# In-flight fetches or maintenance operations may finish. Until a waiting
|
|
318
|
+
# stop completes, started? remains true and stopped? remains false.
|
|
319
|
+
def stop(wait: true)
|
|
320
|
+
@runtime.stop(wait: wait)
|
|
321
|
+
self
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Stops fetching new jobs, interrupts active work, and returns self.
|
|
325
|
+
def stop_and_cancel
|
|
326
|
+
@runtime.stop(cancel: true)
|
|
327
|
+
self
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Returns true when the client runtime is fully stopped.
|
|
331
|
+
def stopped? = @runtime.stopped?
|
|
332
|
+
|
|
333
|
+
# Subscribes to event kinds and returns a Subscription.
|
|
334
|
+
#
|
|
335
|
+
# Call Subscription#close when the subscription is no longer needed.
|
|
336
|
+
def subscribe(*kinds, buffer_size: 100)
|
|
337
|
+
@runtime.subscribe(kinds, buffer_size: buffer_size)
|
|
134
338
|
end
|
|
135
339
|
|
|
136
340
|
# Default states that are used during a unique insert. Can be overridden by
|
|
@@ -143,7 +347,8 @@ module River
|
|
|
143
347
|
JOB_STATE_RUNNING,
|
|
144
348
|
JOB_STATE_SCHEDULED
|
|
145
349
|
].freeze
|
|
146
|
-
|
|
350
|
+
|
|
351
|
+
EMPTY_INSERT_OPTS = InsertOpts.new.freeze
|
|
147
352
|
|
|
148
353
|
REQUIRED_UNIQUE_STATES = [
|
|
149
354
|
JOB_STATE_AVAILABLE,
|
|
@@ -151,22 +356,22 @@ module River
|
|
|
151
356
|
JOB_STATE_RUNNING,
|
|
152
357
|
JOB_STATE_SCHEDULED
|
|
153
358
|
].freeze
|
|
154
|
-
private_constant :REQUIRED_UNIQUE_STATES
|
|
155
359
|
|
|
156
|
-
|
|
157
|
-
|
|
360
|
+
TAG_RE = /\A\w[\w-]+\w\z/
|
|
361
|
+
|
|
362
|
+
private_constant :DEFAULT_UNIQUE_STATES, :EMPTY_INSERT_OPTS, :REQUIRED_UNIQUE_STATES, :TAG_RE
|
|
158
363
|
|
|
159
364
|
private def insert_and_check_unique_job(insert_params)
|
|
160
365
|
job, unique_skipped_as_duplicate = @driver.job_insert(insert_params)
|
|
161
|
-
|
|
366
|
+
JobInsertResult.new(job, unique_skipped_as_duplicated: unique_skipped_as_duplicate)
|
|
162
367
|
end
|
|
163
368
|
|
|
164
369
|
private def make_insert_params(args, insert_opts)
|
|
165
|
-
raise "args should respond to `#kind`" if !args.respond_to?(:kind)
|
|
370
|
+
raise ArgumentError, "args should respond to `#kind`" if !args.respond_to?(:kind)
|
|
166
371
|
|
|
167
372
|
# ~all objects in Ruby respond to `#to_json`, so check non-nil instead.
|
|
168
373
|
args_json = args.to_json
|
|
169
|
-
raise "args should return non-nil from `#to_json`" if !args_json
|
|
374
|
+
raise ArgumentError, "args should return non-nil from `#to_json`" if !args_json
|
|
170
375
|
|
|
171
376
|
args_insert_opts = if args.respond_to?(:insert_opts)
|
|
172
377
|
args_with_insert_opts = args #: _JobArgsWithInsertOpts # rubocop:disable Layout/LeadingCommentSpace
|
|
@@ -176,24 +381,29 @@ module River
|
|
|
176
381
|
end
|
|
177
382
|
|
|
178
383
|
scheduled_at = insert_opts.scheduled_at || args_insert_opts.scheduled_at
|
|
384
|
+
now = @time_now_utc.call
|
|
385
|
+
state = (insert_opts.state || args_insert_opts.state || ((scheduled_at && scheduled_at > now) ? JOB_STATE_SCHEDULED : JOB_STATE_AVAILABLE)).to_s #: jobStateAll # rubocop:disable Layout/LeadingCommentSpace
|
|
179
386
|
|
|
180
387
|
insert_params = Driver::JobInsertParams.new(
|
|
388
|
+
args: args,
|
|
181
389
|
encoded_args: args_json,
|
|
182
|
-
kind: args.kind,
|
|
390
|
+
kind: args.kind.to_s,
|
|
183
391
|
max_attempts: insert_opts.max_attempts || args_insert_opts.max_attempts || MAX_ATTEMPTS_DEFAULT,
|
|
392
|
+
metadata: (args_insert_opts.metadata || {}).merge(insert_opts.metadata || {}),
|
|
184
393
|
priority: insert_opts.priority || args_insert_opts.priority || PRIORITY_DEFAULT,
|
|
185
|
-
queue: insert_opts.queue || args_insert_opts.queue || QUEUE_DEFAULT,
|
|
186
|
-
scheduled_at: scheduled_at&.
|
|
187
|
-
state:
|
|
394
|
+
queue: (insert_opts.queue || args_insert_opts.queue || QUEUE_DEFAULT).to_s,
|
|
395
|
+
scheduled_at: scheduled_at&.getutc || now,
|
|
396
|
+
state: state,
|
|
188
397
|
tags: validate_tags(insert_opts.tags || args_insert_opts.tags || [])
|
|
189
398
|
)
|
|
190
399
|
|
|
191
400
|
unique_opts = insert_opts.unique_opts || args_insert_opts.unique_opts
|
|
192
|
-
if unique_opts
|
|
401
|
+
if unique_opts && !unique_opts.empty?
|
|
193
402
|
unique_key, unique_states = make_unique_key_and_bitmask(insert_params, unique_opts)
|
|
194
403
|
insert_params.unique_key = unique_key
|
|
195
404
|
insert_params.unique_states = unique_states
|
|
196
405
|
end
|
|
406
|
+
|
|
197
407
|
insert_params
|
|
198
408
|
end
|
|
199
409
|
|
|
@@ -208,18 +418,11 @@ module River
|
|
|
208
418
|
end
|
|
209
419
|
|
|
210
420
|
if unique_opts.by_args
|
|
211
|
-
|
|
212
|
-
filtered_args = if unique_opts.by_args.is_a?(Array)
|
|
213
|
-
parsed_args.slice(*unique_opts.by_args)
|
|
214
|
-
else
|
|
215
|
-
parsed_args
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
encoded_args = JSON.generate(filtered_args.sort.to_h)
|
|
421
|
+
encoded_args = UniqueArgs.encode(insert_params.encoded_args, unique_opts.by_args)
|
|
219
422
|
unique_key += "&args=#{encoded_args}"
|
|
220
423
|
end
|
|
221
424
|
|
|
222
|
-
if unique_opts.by_period
|
|
425
|
+
if unique_opts.by_period && unique_opts.by_period != 0
|
|
223
426
|
lower_period_bound = truncate_time(insert_params.scheduled_at || @time_now_utc.call, unique_opts.by_period).utc
|
|
224
427
|
|
|
225
428
|
unique_key += "&period=#{lower_period_bound.strftime("%FT%TZ")}"
|
|
@@ -229,18 +432,62 @@ module River
|
|
|
229
432
|
unique_key += "&queue=#{insert_params.queue}"
|
|
230
433
|
end
|
|
231
434
|
|
|
435
|
+
# Without kind or another dimension, every job would share the same key.
|
|
436
|
+
raise ArgumentError, "exclude_kind requires by_args, by_queue, or by_period" if unique_key.empty?
|
|
437
|
+
|
|
232
438
|
unique_key_hash = Digest::SHA256.digest(unique_key)
|
|
233
|
-
|
|
439
|
+
states = unique_opts.by_state
|
|
440
|
+
unique_states = validate_unique_states((states && !states.empty?) ? states : DEFAULT_UNIQUE_STATES)
|
|
234
441
|
|
|
235
442
|
[unique_key_hash, UniqueBitmask.from_states(unique_states)]
|
|
236
443
|
end
|
|
237
444
|
|
|
445
|
+
private def run_insert_plugins(all_params, &insert_operation)
|
|
446
|
+
unless config.plugins.any? { |plugin| plugin.respond_to?(:insert_begin) || plugin.respond_to?(:insert_end) || plugin.respond_to?(:insert_many) }
|
|
447
|
+
results = insert_operation.call
|
|
448
|
+
@runtime.wake
|
|
449
|
+
return results
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
operation = -> do
|
|
453
|
+
all_params.each do |insert_params|
|
|
454
|
+
config.plugins.each do |plugin|
|
|
455
|
+
plugin.insert_begin(insert_params) if plugin.respond_to?(:insert_begin)
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
results = insert_operation.call
|
|
460
|
+
results.each do |result|
|
|
461
|
+
config.plugins.reverse_each do |plugin|
|
|
462
|
+
plugin.insert_end(result) if plugin.respond_to?(:insert_end)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
results
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
config.plugins.reverse_each do |plugin|
|
|
470
|
+
next unless plugin.respond_to?(:insert_many)
|
|
471
|
+
|
|
472
|
+
next_operation = operation
|
|
473
|
+
operation = -> { plugin.insert_many(all_params, next_operation) }
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
results = @driver.transaction { operation.call }
|
|
477
|
+
@runtime.wake
|
|
478
|
+
results
|
|
479
|
+
end
|
|
480
|
+
|
|
238
481
|
# Truncates the given time down to the interval. For example:
|
|
239
482
|
#
|
|
240
483
|
# Thu Jan 15 21:26:36 UTC 2024 @ 15 minutes ->
|
|
241
484
|
# Thu Jan 15 21:15:00 UTC 2024
|
|
242
485
|
private def truncate_time(time, interval_seconds)
|
|
243
|
-
|
|
486
|
+
# Go truncates from year 1, not the Unix epoch. Rational arithmetic also
|
|
487
|
+
# avoids floating-point rounding across a period boundary.
|
|
488
|
+
epoch_offset = 62_135_596_800
|
|
489
|
+
interval = interval_seconds.to_r
|
|
490
|
+
Time.at(((time.to_r + epoch_offset) / interval).floor * interval - epoch_offset)
|
|
244
491
|
end
|
|
245
492
|
|
|
246
493
|
# Moves an integer that may occupy the entire uint64 space to one that's
|
|
@@ -249,9 +496,6 @@ module River
|
|
|
249
496
|
[int].pack("Q").unpack1("q") #: Integer # rubocop:disable Layout/LeadingCommentSpace
|
|
250
497
|
end
|
|
251
498
|
|
|
252
|
-
TAG_RE = /\A\w[\w-]+\w\z/
|
|
253
|
-
private_constant :TAG_RE
|
|
254
|
-
|
|
255
499
|
private def validate_tags(tags)
|
|
256
500
|
tags.each do |tag|
|
|
257
501
|
raise ArgumentError, "tags should be 255 characters or less" if tag.length > 255
|
|
@@ -260,9 +504,11 @@ module River
|
|
|
260
504
|
end
|
|
261
505
|
|
|
262
506
|
private def validate_unique_states(states)
|
|
507
|
+
states = states.map(&:to_s) #: Array[jobStateAll] # rubocop:disable Layout/LeadingCommentSpace
|
|
263
508
|
REQUIRED_UNIQUE_STATES.each do |required_state|
|
|
264
509
|
raise ArgumentError, "by_state should include required state #{required_state}" unless states.include?(required_state)
|
|
265
510
|
end
|
|
511
|
+
|
|
266
512
|
states
|
|
267
513
|
end
|
|
268
514
|
end
|
|
@@ -276,25 +522,39 @@ module River
|
|
|
276
522
|
# Insertion options to use with the insert.
|
|
277
523
|
attr_reader :insert_opts
|
|
278
524
|
|
|
279
|
-
|
|
525
|
+
# Pairs job arguments with per-job keyword options for Client#insert_many.
|
|
526
|
+
# A reusable InsertOpts may be supplied instead as +insert_opts:+.
|
|
527
|
+
# @type method initialize: (jobArgs, ?insert_opts: InsertOpts?, **untyped) -> void
|
|
528
|
+
def initialize(args, insert_opts: nil, **options)
|
|
529
|
+
raise ArgumentError, "use insert_opts or keyword options, not both" if insert_opts && !options.empty?
|
|
530
|
+
|
|
280
531
|
@args = args
|
|
281
|
-
@insert_opts = insert_opts
|
|
532
|
+
@insert_opts = insert_opts || (InsertOpts.new(**options) unless options.empty?)
|
|
282
533
|
end
|
|
283
534
|
end
|
|
284
535
|
|
|
285
536
|
# Result of a single insertion.
|
|
286
|
-
class
|
|
537
|
+
class JobInsertResult
|
|
287
538
|
# Inserted job row, or an existing job row if insert was skipped due to a
|
|
288
539
|
# previously existing unique job.
|
|
289
540
|
attr_reader :job
|
|
290
541
|
|
|
291
542
|
# True if for a unique job, the insertion was skipped due to an equivalent
|
|
292
543
|
# job matching unique property already being present.
|
|
293
|
-
|
|
544
|
+
def unique_skipped_as_duplicate? = @unique_skipped_as_duplicate
|
|
294
545
|
|
|
546
|
+
# Compatibility with the spelling used in earlier releases.
|
|
547
|
+
alias_method :unique_skipped_as_duplicated, :unique_skipped_as_duplicate?
|
|
548
|
+
|
|
549
|
+
# Creates an insertion result. Applications normally receive instances from
|
|
550
|
+
# Client#insert or Client#insert_many. The keyword retains its original
|
|
551
|
+
# spelling for compatibility with earlier releases.
|
|
295
552
|
def initialize(job, unique_skipped_as_duplicated:)
|
|
296
553
|
@job = job
|
|
297
|
-
@
|
|
554
|
+
@unique_skipped_as_duplicate = unique_skipped_as_duplicated
|
|
298
555
|
end
|
|
299
556
|
end
|
|
557
|
+
|
|
558
|
+
# Compatibility with the insertion result class name used in earlier releases.
|
|
559
|
+
InsertResult = JobInsertResult
|
|
300
560
|
end
|