riverqueue-activerecord 0.10.0 → 0.11.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/lib/driver.rb +239 -23
- metadata +4 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8316b332b5bc9bb1eae57d81751a31ef42342da5ef9a1914b3bceccb222041ac
|
|
4
|
+
data.tar.gz: 49784779614e66d985af52516698d1766980ec43af019c62f20a8cd8459d1a69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3b77f7ad4752bad4401ccaad39bf66513ed84e9aa498bfe8f3f8552fd7f7c6a63a60ed4b2b8cdb3ac33ea8367698c61c35a7f790ed43d9f6e67e1729cd8ede0
|
|
7
|
+
data.tar.gz: 39992e6b530ed49a19deb622e2e689c0a221902fc23ef1ec8c7ca7bc54bbe9eb79e0bb2094aede12eafce8fe08497e248139260e2b9410e06eb43d85ae33bfbb
|
data/lib/driver.rb
CHANGED
|
@@ -1,13 +1,65 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
1
3
|
module River::Driver
|
|
2
|
-
# Provides
|
|
4
|
+
# Provides an ActiveRecord driver for River that supports both PostgreSQL
|
|
5
|
+
# and SQLite.
|
|
3
6
|
#
|
|
4
7
|
# Used in conjunction with a River client like:
|
|
5
8
|
#
|
|
6
|
-
#
|
|
7
|
-
# client = River::Client.new(River::Driver::ActiveRecord.new
|
|
9
|
+
# ActiveRecord::Base.establish_connection("postgres://...")
|
|
10
|
+
# client = River::Client.new(River::Driver::ActiveRecord.new)
|
|
8
11
|
#
|
|
9
12
|
class ActiveRecord
|
|
13
|
+
SQLITE_CONFLICT_WHERE = <<~SQL.chomp
|
|
14
|
+
unique_key IS NOT NULL
|
|
15
|
+
AND unique_states IS NOT NULL
|
|
16
|
+
AND CASE state
|
|
17
|
+
WHEN 'available' THEN unique_states & (1 << 0)
|
|
18
|
+
WHEN 'cancelled' THEN unique_states & (1 << 1)
|
|
19
|
+
WHEN 'completed' THEN unique_states & (1 << 2)
|
|
20
|
+
WHEN 'discarded' THEN unique_states & (1 << 3)
|
|
21
|
+
WHEN 'pending' THEN unique_states & (1 << 4)
|
|
22
|
+
WHEN 'retryable' THEN unique_states & (1 << 5)
|
|
23
|
+
WHEN 'running' THEN unique_states & (1 << 6)
|
|
24
|
+
WHEN 'scheduled' THEN unique_states & (1 << 7)
|
|
25
|
+
ELSE 0
|
|
26
|
+
END >= 1
|
|
27
|
+
SQL
|
|
28
|
+
private_constant :SQLITE_CONFLICT_WHERE
|
|
29
|
+
|
|
30
|
+
# SQLite 3.45+ may store JSON as binary JSONB. Always project JSON columns
|
|
31
|
+
# through json() so this driver can read both the current JSONB format and
|
|
32
|
+
# the text JSON used by River migrations through version 006. Cast times to
|
|
33
|
+
# text so ActiveRecord doesn't interpret timezone-less SQLite timestamps in
|
|
34
|
+
# the process timezone.
|
|
35
|
+
SQLITE_JOB_COLUMNS = <<~SQL.chomp
|
|
36
|
+
id,
|
|
37
|
+
json(args) AS args,
|
|
38
|
+
attempt,
|
|
39
|
+
CAST(attempted_at AS text) AS attempted_at,
|
|
40
|
+
json(attempted_by) AS attempted_by,
|
|
41
|
+
CAST(created_at AS text) AS created_at,
|
|
42
|
+
json(errors) AS errors,
|
|
43
|
+
CAST(finalized_at AS text) AS finalized_at,
|
|
44
|
+
kind,
|
|
45
|
+
max_attempts,
|
|
46
|
+
json(metadata) AS metadata,
|
|
47
|
+
priority,
|
|
48
|
+
queue,
|
|
49
|
+
state,
|
|
50
|
+
CAST(scheduled_at AS text) AS scheduled_at,
|
|
51
|
+
json(tags) AS tags,
|
|
52
|
+
unique_key,
|
|
53
|
+
unique_states
|
|
54
|
+
SQL
|
|
55
|
+
private_constant :SQLITE_JOB_COLUMNS
|
|
56
|
+
|
|
57
|
+
SQLITE_UNIQUE_NONCE_KEY = "river:unique_nonce"
|
|
58
|
+
private_constant :SQLITE_UNIQUE_NONCE_KEY
|
|
59
|
+
|
|
10
60
|
def initialize
|
|
61
|
+
@is_sqlite = ::ActiveRecord::Base.connection.adapter_name.downcase.include?("sqlite")
|
|
62
|
+
|
|
11
63
|
# It's Ruby, so we can only define a model after ActiveRecord's established a
|
|
12
64
|
# connection because it's all dynamic.
|
|
13
65
|
if !River::Driver::ActiveRecord.const_defined?(:RiverJob)
|
|
@@ -32,8 +84,13 @@ module River::Driver
|
|
|
32
84
|
end
|
|
33
85
|
|
|
34
86
|
def job_get_by_id(id)
|
|
35
|
-
|
|
36
|
-
|
|
87
|
+
if @is_sqlite
|
|
88
|
+
row = sqlite_job_rows("WHERE id = ? LIMIT 1", [id]).first
|
|
89
|
+
row ? sqlite_to_job_row_from_raw(row) : nil
|
|
90
|
+
else
|
|
91
|
+
data_set = RiverJob.where(id: id)
|
|
92
|
+
data_set.first ? to_job_row_from_model(data_set.first) : nil
|
|
93
|
+
end
|
|
37
94
|
end
|
|
38
95
|
|
|
39
96
|
def job_insert(insert_params)
|
|
@@ -41,8 +98,28 @@ module River::Driver
|
|
|
41
98
|
end
|
|
42
99
|
|
|
43
100
|
def job_insert_many(insert_params_many)
|
|
101
|
+
@is_sqlite ? sqlite_job_insert_many(insert_params_many) : postgres_job_insert_many(insert_params_many)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def job_list
|
|
105
|
+
if @is_sqlite
|
|
106
|
+
sqlite_job_rows("ORDER BY id").map { |row| sqlite_to_job_row_from_raw(row) }
|
|
107
|
+
else
|
|
108
|
+
RiverJob.order(:id).all.map { |job| to_job_row_from_model(job) }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def rollback_exception
|
|
113
|
+
::ActiveRecord::Rollback
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def transaction(&)
|
|
117
|
+
::ActiveRecord::Base.transaction(requires_new: true, &)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private def postgres_job_insert_many(insert_params_many)
|
|
44
121
|
res = RiverJob.upsert_all(
|
|
45
|
-
insert_params_many.map { |param|
|
|
122
|
+
insert_params_many.map { |param| postgres_insert_params_to_hash(param) },
|
|
46
123
|
on_duplicate: Arel.sql("kind = EXCLUDED.kind"),
|
|
47
124
|
returning: Arel.sql("*, (xmax != 0) AS unique_skipped_as_duplicate"),
|
|
48
125
|
|
|
@@ -53,25 +130,70 @@ module River::Driver
|
|
|
53
130
|
# clause. The workaround is to target the index name instead of columns.
|
|
54
131
|
unique_by: "river_job_unique_idx"
|
|
55
132
|
)
|
|
56
|
-
|
|
133
|
+
postgres_to_insert_results(res)
|
|
57
134
|
end
|
|
58
135
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
136
|
+
# River's current SQLite driver uses json_each to make a batch a single,
|
|
137
|
+
# atomic statement. The JSON columns are converted to SQLite JSONB here,
|
|
138
|
+
# matching migration 007 and newer River databases.
|
|
139
|
+
private def sqlite_job_insert_many(insert_params_many)
|
|
140
|
+
return [] if insert_params_many.empty?
|
|
63
141
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
142
|
+
::ActiveRecord::Base.transaction(requires_new: true) do
|
|
143
|
+
nonce = SecureRandom.hex(8)
|
|
144
|
+
jobs = insert_params_many.map { |param| sqlite_insert_params_to_hash(param, nonce) }
|
|
67
145
|
|
|
68
|
-
|
|
69
|
-
|
|
146
|
+
sql = <<~SQL
|
|
147
|
+
INSERT INTO river_job (
|
|
148
|
+
args,
|
|
149
|
+
created_at,
|
|
150
|
+
kind,
|
|
151
|
+
max_attempts,
|
|
152
|
+
metadata,
|
|
153
|
+
priority,
|
|
154
|
+
queue,
|
|
155
|
+
scheduled_at,
|
|
156
|
+
state,
|
|
157
|
+
tags,
|
|
158
|
+
unique_key,
|
|
159
|
+
unique_states
|
|
160
|
+
)
|
|
161
|
+
SELECT
|
|
162
|
+
jsonb(json_extract(value, '$.args')),
|
|
163
|
+
datetime('now', 'subsec'),
|
|
164
|
+
cast(json_extract(value, '$.kind') AS text),
|
|
165
|
+
cast(json_extract(value, '$.max_attempts') AS integer),
|
|
166
|
+
jsonb(json_extract(value, '$.metadata')),
|
|
167
|
+
cast(json_extract(value, '$.priority') AS integer),
|
|
168
|
+
cast(json_extract(value, '$.queue') AS text),
|
|
169
|
+
coalesce(cast(json_extract(value, '$.scheduled_at') AS text), datetime('now', 'subsec')),
|
|
170
|
+
cast(json_extract(value, '$.state') AS text),
|
|
171
|
+
jsonb(json_extract(value, '$.tags')),
|
|
172
|
+
CASE
|
|
173
|
+
WHEN length(cast(json_extract(value, '$.unique_key') AS text)) = 0 THEN NULL
|
|
174
|
+
ELSE unhex(cast(json_extract(value, '$.unique_key') AS text))
|
|
175
|
+
END,
|
|
176
|
+
nullif(cast(json_extract(value, '$.unique_states') AS integer), 0)
|
|
177
|
+
FROM json_each(cast(? AS blob))
|
|
178
|
+
WHERE true
|
|
179
|
+
ON CONFLICT (unique_key) WHERE #{SQLITE_CONFLICT_WHERE}
|
|
180
|
+
DO UPDATE SET kind = EXCLUDED.kind
|
|
181
|
+
RETURNING #{SQLITE_JOB_COLUMNS}
|
|
182
|
+
SQL
|
|
183
|
+
|
|
184
|
+
rows = ::ActiveRecord::Base.connection.raw_connection.execute(sql, [JSON.dump(jobs)])
|
|
185
|
+
sqlite_notify_insert(insert_params_many)
|
|
186
|
+
|
|
187
|
+
rows.map do |row|
|
|
188
|
+
metadata = JSON.parse(row["metadata"])
|
|
189
|
+
[sqlite_to_job_row_from_raw(row), metadata[SQLITE_UNIQUE_NONCE_KEY] != nonce]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
70
192
|
end
|
|
71
193
|
|
|
72
|
-
private def
|
|
194
|
+
private def postgres_insert_params_to_hash(insert_params)
|
|
73
195
|
{
|
|
74
|
-
args: insert_params.encoded_args,
|
|
196
|
+
args: JSON.parse(insert_params.encoded_args),
|
|
75
197
|
kind: insert_params.kind,
|
|
76
198
|
max_attempts: insert_params.max_attempts,
|
|
77
199
|
priority: insert_params.priority,
|
|
@@ -84,14 +206,34 @@ module River::Driver
|
|
|
84
206
|
}
|
|
85
207
|
end
|
|
86
208
|
|
|
209
|
+
private def sqlite_insert_params_to_hash(insert_params, nonce)
|
|
210
|
+
{
|
|
211
|
+
args: JSON.parse(insert_params.encoded_args),
|
|
212
|
+
kind: insert_params.kind,
|
|
213
|
+
max_attempts: insert_params.max_attempts,
|
|
214
|
+
metadata: {SQLITE_UNIQUE_NONCE_KEY => nonce},
|
|
215
|
+
priority: insert_params.priority,
|
|
216
|
+
queue: insert_params.queue,
|
|
217
|
+
scheduled_at: insert_params.scheduled_at ? format_time(insert_params.scheduled_at) : nil,
|
|
218
|
+
state: insert_params.state,
|
|
219
|
+
tags: insert_params.tags || [],
|
|
220
|
+
unique_key: insert_params.unique_key&.unpack1("H*"),
|
|
221
|
+
unique_states: insert_params.unique_states&.to_i(2)
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
|
|
87
225
|
private def to_job_row_from_model(river_job)
|
|
226
|
+
@is_sqlite ? sqlite_to_job_row_from_model(river_job) : postgres_to_job_row_from_model(river_job)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private def postgres_to_job_row_from_model(river_job)
|
|
88
230
|
# needs to be accessed through values because `errors` is shadowed by both
|
|
89
231
|
# ActiveRecord and the patch above
|
|
90
232
|
errors = river_job.attributes["errors"]
|
|
91
233
|
|
|
92
234
|
River::JobRow.new(
|
|
93
235
|
id: river_job.id,
|
|
94
|
-
args:
|
|
236
|
+
args: river_job.args,
|
|
95
237
|
attempt: river_job.attempt,
|
|
96
238
|
attempted_at: river_job.attempted_at&.getutc,
|
|
97
239
|
attempted_by: river_job.attempted_by,
|
|
@@ -120,9 +262,46 @@ module River::Driver
|
|
|
120
262
|
)
|
|
121
263
|
end
|
|
122
264
|
|
|
123
|
-
private def
|
|
265
|
+
private def sqlite_to_job_row_from_model(river_job)
|
|
266
|
+
row = sqlite_job_rows("WHERE id = ? LIMIT 1", [river_job.id]).first
|
|
267
|
+
sqlite_to_job_row_from_raw(row)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private def sqlite_to_job_row_from_raw(row)
|
|
271
|
+
errors = row["errors"] ? JSON.parse(row["errors"]) : []
|
|
272
|
+
|
|
273
|
+
River::JobRow.new(
|
|
274
|
+
id: row["id"],
|
|
275
|
+
args: JSON.parse(row["args"]),
|
|
276
|
+
attempt: row["attempt"],
|
|
277
|
+
attempted_at: parse_sqlite_time(row["attempted_at"]),
|
|
278
|
+
attempted_by: row["attempted_by"] ? JSON.parse(row["attempted_by"]) : nil,
|
|
279
|
+
created_at: parse_sqlite_time(row["created_at"]),
|
|
280
|
+
errors: errors.map { |e|
|
|
281
|
+
River::AttemptError.new(
|
|
282
|
+
at: Time.parse(e["at"]),
|
|
283
|
+
attempt: e["attempt"],
|
|
284
|
+
error: e["error"],
|
|
285
|
+
trace: e["trace"]
|
|
286
|
+
)
|
|
287
|
+
},
|
|
288
|
+
finalized_at: parse_sqlite_time(row["finalized_at"]),
|
|
289
|
+
kind: row["kind"],
|
|
290
|
+
max_attempts: row["max_attempts"],
|
|
291
|
+
metadata: JSON.parse(row["metadata"]),
|
|
292
|
+
priority: row["priority"],
|
|
293
|
+
queue: row["queue"],
|
|
294
|
+
scheduled_at: parse_sqlite_time(row["scheduled_at"]),
|
|
295
|
+
state: row["state"],
|
|
296
|
+
tags: JSON.parse(row["tags"]),
|
|
297
|
+
unique_key: row["unique_key"]&.to_s,
|
|
298
|
+
unique_states: row["unique_states"] ? ::River::UniqueBitmask.to_states(row["unique_states"]) : nil
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
private def postgres_to_insert_results(res)
|
|
124
303
|
res.rows.map do |row|
|
|
125
|
-
|
|
304
|
+
postgres_to_job_row_from_raw(row, res.columns, res.column_types)
|
|
126
305
|
end
|
|
127
306
|
end
|
|
128
307
|
|
|
@@ -132,7 +311,7 @@ module River::Driver
|
|
|
132
311
|
# searched long and hard for a way to have the former type of method return
|
|
133
312
|
# raw or the latter type of method return a model, but was unable to find
|
|
134
313
|
# anything.
|
|
135
|
-
private def
|
|
314
|
+
private def postgres_to_job_row_from_raw(row, columns, column_types)
|
|
136
315
|
river_job = {}
|
|
137
316
|
|
|
138
317
|
row.each_with_index do |val, i|
|
|
@@ -153,7 +332,7 @@ module River::Driver
|
|
|
153
332
|
[
|
|
154
333
|
River::JobRow.new(
|
|
155
334
|
id: river_job["id"],
|
|
156
|
-
args:
|
|
335
|
+
args: river_job["args"],
|
|
157
336
|
attempt: river_job["attempt"],
|
|
158
337
|
attempted_at: river_job["attempted_at"]&.getutc,
|
|
159
338
|
attempted_by: river_job["attempted_by"],
|
|
@@ -174,5 +353,42 @@ module River::Driver
|
|
|
174
353
|
river_job["unique_skipped_as_duplicate"]
|
|
175
354
|
]
|
|
176
355
|
end
|
|
356
|
+
|
|
357
|
+
private def format_time(time)
|
|
358
|
+
time.getutc.round(3).strftime("%Y-%m-%d %H:%M:%S.%3N")
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
private def parse_sqlite_time(value)
|
|
362
|
+
return nil unless value
|
|
363
|
+
|
|
364
|
+
value = value.to_s
|
|
365
|
+
value += " UTC" unless value.match?(/(?:Z|[+-]\d{2}:?\d{2})\z/)
|
|
366
|
+
Time.parse(value).utc
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private def sqlite_job_rows(suffix, binds = [])
|
|
370
|
+
sql = "SELECT #{SQLITE_JOB_COLUMNS} FROM river_job #{suffix}"
|
|
371
|
+
::ActiveRecord::Base.connection.raw_connection.execute(sql, binds)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
private def sqlite_notify_insert(insert_params_many)
|
|
375
|
+
queues = insert_params_many
|
|
376
|
+
.select { |param| param.state == ::River::JOB_STATE_AVAILABLE }
|
|
377
|
+
.map(&:queue)
|
|
378
|
+
.uniq
|
|
379
|
+
return if queues.empty?
|
|
380
|
+
|
|
381
|
+
notifications = queues.map do |queue|
|
|
382
|
+
{payload: JSON.dump({queue: queue}), topic: "insert"}
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
::ActiveRecord::Base.connection.raw_connection.execute(<<~SQL, [JSON.dump(notifications)])
|
|
386
|
+
INSERT INTO river_notification (payload, topic)
|
|
387
|
+
SELECT
|
|
388
|
+
json_extract(value, '$.payload'),
|
|
389
|
+
json_extract(value, '$.topic')
|
|
390
|
+
FROM json_each(cast(? AS blob))
|
|
391
|
+
SQL
|
|
392
|
+
end
|
|
177
393
|
end
|
|
178
394
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: riverqueue-activerecord
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Blake Gentry
|
|
@@ -50,28 +50,8 @@ dependencies:
|
|
|
50
50
|
- - "<"
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
52
|
version: '1000'
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
requirement: !ruby/object:Gem::Requirement
|
|
56
|
-
requirements:
|
|
57
|
-
- - ">"
|
|
58
|
-
- !ruby/object:Gem::Version
|
|
59
|
-
version: '0'
|
|
60
|
-
- - "<"
|
|
61
|
-
- !ruby/object:Gem::Version
|
|
62
|
-
version: '1000'
|
|
63
|
-
type: :runtime
|
|
64
|
-
prerelease: false
|
|
65
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
-
requirements:
|
|
67
|
-
- - ">"
|
|
68
|
-
- !ruby/object:Gem::Version
|
|
69
|
-
version: '0'
|
|
70
|
-
- - "<"
|
|
71
|
-
- !ruby/object:Gem::Version
|
|
72
|
-
version: '1000'
|
|
73
|
-
description: ActiveRecord driver for the River Ruby gem. Use in conjunction with the
|
|
74
|
-
riverqueue gem to insert jobs that are worked in Go.
|
|
53
|
+
description: ActiveRecord PostgreSQL and SQLite driver for the River Ruby gem. Use
|
|
54
|
+
in conjunction with the riverqueue gem to insert jobs that are worked in Go.
|
|
75
55
|
email: brandur@brandur.org
|
|
76
56
|
executables: []
|
|
77
57
|
extensions: []
|
|
@@ -99,5 +79,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
99
79
|
requirements: []
|
|
100
80
|
rubygems_version: 4.0.9
|
|
101
81
|
specification_version: 4
|
|
102
|
-
summary: ActiveRecord driver for the River Ruby gem.
|
|
82
|
+
summary: ActiveRecord PostgreSQL and SQLite driver for the River Ruby gem.
|
|
103
83
|
test_files: []
|