riverqueue-sequel 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 +228 -19
- 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: c7ade67ed4f568f7dbc60f31ea72eb6e5b6c71b1274869e67bd9d3dbf120a562
|
|
4
|
+
data.tar.gz: af7100c24950d9a32458928690b0e4a61a1375d1dfdbd1571bddb43efa717619
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae68c96561a0f54df6e181c4e4028f3f0ed1b5a929d2b12428ea4b8f3c53f8bc46f33ae2d77fd5b86e3c21ba8ba193bf21d6bd69433bb2c735152225f0810139
|
|
7
|
+
data.tar.gz: 1027bd3dcddb61912915dd78214445be02e9ed2400123446a87c4aa364b4f4868811d60d227be878ee1900336692edea75e47899eb5174453998d97d8abd7369
|
data/lib/driver.rb
CHANGED
|
@@ -1,21 +1,84 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
1
3
|
module River::Driver
|
|
2
|
-
# Provides a Sequel driver for River.
|
|
4
|
+
# Provides a Sequel driver for River that supports both PostgreSQL and SQLite.
|
|
3
5
|
#
|
|
4
6
|
# Used in conjunction with a River client like:
|
|
5
7
|
#
|
|
6
8
|
# DB = Sequel.connect("postgres://...")
|
|
7
9
|
# client = River::Client.new(River::Driver::Sequel.new(DB))
|
|
8
10
|
#
|
|
11
|
+
# Or with SQLite:
|
|
12
|
+
#
|
|
13
|
+
# DB = Sequel.connect("sqlite://path/to/river.db")
|
|
14
|
+
# client = River::Client.new(River::Driver::Sequel.new(DB))
|
|
15
|
+
#
|
|
9
16
|
class Sequel
|
|
17
|
+
SQLITE_CONFLICT_WHERE = <<~SQL.chomp
|
|
18
|
+
unique_key IS NOT NULL
|
|
19
|
+
AND unique_states IS NOT NULL
|
|
20
|
+
AND CASE state
|
|
21
|
+
WHEN 'available' THEN unique_states & (1 << 0)
|
|
22
|
+
WHEN 'cancelled' THEN unique_states & (1 << 1)
|
|
23
|
+
WHEN 'completed' THEN unique_states & (1 << 2)
|
|
24
|
+
WHEN 'discarded' THEN unique_states & (1 << 3)
|
|
25
|
+
WHEN 'pending' THEN unique_states & (1 << 4)
|
|
26
|
+
WHEN 'retryable' THEN unique_states & (1 << 5)
|
|
27
|
+
WHEN 'running' THEN unique_states & (1 << 6)
|
|
28
|
+
WHEN 'scheduled' THEN unique_states & (1 << 7)
|
|
29
|
+
ELSE 0
|
|
30
|
+
END >= 1
|
|
31
|
+
SQL
|
|
32
|
+
private_constant :SQLITE_CONFLICT_WHERE
|
|
33
|
+
|
|
34
|
+
# SQLite 3.45+ may store JSON as binary JSONB. Always project JSON columns
|
|
35
|
+
# through json() so this driver can read both the current JSONB format and
|
|
36
|
+
# the text JSON used by River migrations through version 006. Cast times to
|
|
37
|
+
# text so Sequel doesn't interpret timezone-less SQLite timestamps in the
|
|
38
|
+
# process timezone.
|
|
39
|
+
SQLITE_JOB_COLUMNS = <<~SQL.chomp
|
|
40
|
+
id,
|
|
41
|
+
json(args) AS args,
|
|
42
|
+
attempt,
|
|
43
|
+
CAST(attempted_at AS text) AS attempted_at,
|
|
44
|
+
json(attempted_by) AS attempted_by,
|
|
45
|
+
CAST(created_at AS text) AS created_at,
|
|
46
|
+
json(errors) AS errors,
|
|
47
|
+
CAST(finalized_at AS text) AS finalized_at,
|
|
48
|
+
kind,
|
|
49
|
+
max_attempts,
|
|
50
|
+
json(metadata) AS metadata,
|
|
51
|
+
priority,
|
|
52
|
+
queue,
|
|
53
|
+
state,
|
|
54
|
+
CAST(scheduled_at AS text) AS scheduled_at,
|
|
55
|
+
json(tags) AS tags,
|
|
56
|
+
unique_key,
|
|
57
|
+
unique_states
|
|
58
|
+
SQL
|
|
59
|
+
private_constant :SQLITE_JOB_COLUMNS
|
|
60
|
+
|
|
61
|
+
SQLITE_UNIQUE_NONCE_KEY = "river:unique_nonce"
|
|
62
|
+
private_constant :SQLITE_UNIQUE_NONCE_KEY
|
|
63
|
+
|
|
10
64
|
def initialize(db)
|
|
11
65
|
@db = db
|
|
12
|
-
@db.
|
|
13
|
-
|
|
66
|
+
@is_sqlite = (db.database_type == :sqlite)
|
|
67
|
+
|
|
68
|
+
unless @is_sqlite
|
|
69
|
+
db.extension(:pg_array)
|
|
70
|
+
db.extension(:pg_json)
|
|
71
|
+
end
|
|
14
72
|
end
|
|
15
73
|
|
|
16
74
|
def job_get_by_id(id)
|
|
17
|
-
|
|
18
|
-
|
|
75
|
+
if @is_sqlite
|
|
76
|
+
row = sqlite_job_rows("WHERE id = ? LIMIT 1", id).first
|
|
77
|
+
row ? sqlite_to_job_row_from_raw(row) : nil
|
|
78
|
+
else
|
|
79
|
+
data_set = @db[:river_job].where(id: id)
|
|
80
|
+
data_set.first ? to_job_row(data_set.first) : nil
|
|
81
|
+
end
|
|
19
82
|
end
|
|
20
83
|
|
|
21
84
|
def job_insert(insert_params)
|
|
@@ -23,6 +86,26 @@ module River::Driver
|
|
|
23
86
|
end
|
|
24
87
|
|
|
25
88
|
def job_insert_many(insert_params_array)
|
|
89
|
+
@is_sqlite ? sqlite_job_insert_many(insert_params_array) : postgres_job_insert_many(insert_params_array)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def job_list
|
|
93
|
+
if @is_sqlite
|
|
94
|
+
sqlite_job_rows("ORDER BY id").map { |row| sqlite_to_job_row_from_raw(row) }
|
|
95
|
+
else
|
|
96
|
+
@db[:river_job].order_by(:id).all.map { |job| to_job_row(job) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def rollback_exception
|
|
101
|
+
::Sequel::Rollback
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def transaction(&)
|
|
105
|
+
@db.transaction(savepoint: true, &)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private def postgres_job_insert_many(insert_params_array)
|
|
26
109
|
@db[:river_job]
|
|
27
110
|
.insert_conflict(
|
|
28
111
|
target: [:unique_key],
|
|
@@ -32,24 +115,69 @@ module River::Driver
|
|
|
32
115
|
update: {kind: ::Sequel[:excluded][:kind]}
|
|
33
116
|
)
|
|
34
117
|
.returning(::Sequel.lit("*, (xmax != 0) AS unique_skipped_as_duplicate"))
|
|
35
|
-
.multi_insert(insert_params_array.map { |p|
|
|
36
|
-
.map { |row|
|
|
118
|
+
.multi_insert(insert_params_array.map { |p| postgres_insert_params_to_hash(p) })
|
|
119
|
+
.map { |row| [to_job_row(row), row[:unique_skipped_as_duplicate]] }
|
|
37
120
|
end
|
|
38
121
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
122
|
+
# River's current SQLite driver uses json_each to make a batch a single,
|
|
123
|
+
# atomic statement. The JSON columns are converted to SQLite JSONB here,
|
|
124
|
+
# matching migration 007 and newer River databases.
|
|
125
|
+
private def sqlite_job_insert_many(insert_params_array)
|
|
126
|
+
return [] if insert_params_array.empty?
|
|
43
127
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
128
|
+
@db.transaction(savepoint: true) do
|
|
129
|
+
nonce = SecureRandom.hex(8)
|
|
130
|
+
jobs = insert_params_array.map { |param| sqlite_insert_params_to_hash(param, nonce) }
|
|
47
131
|
|
|
48
|
-
|
|
49
|
-
|
|
132
|
+
sql = <<~SQL
|
|
133
|
+
INSERT INTO river_job (
|
|
134
|
+
args,
|
|
135
|
+
created_at,
|
|
136
|
+
kind,
|
|
137
|
+
max_attempts,
|
|
138
|
+
metadata,
|
|
139
|
+
priority,
|
|
140
|
+
queue,
|
|
141
|
+
scheduled_at,
|
|
142
|
+
state,
|
|
143
|
+
tags,
|
|
144
|
+
unique_key,
|
|
145
|
+
unique_states
|
|
146
|
+
)
|
|
147
|
+
SELECT
|
|
148
|
+
jsonb(json_extract(value, '$.args')),
|
|
149
|
+
datetime('now', 'subsec'),
|
|
150
|
+
cast(json_extract(value, '$.kind') AS text),
|
|
151
|
+
cast(json_extract(value, '$.max_attempts') AS integer),
|
|
152
|
+
jsonb(json_extract(value, '$.metadata')),
|
|
153
|
+
cast(json_extract(value, '$.priority') AS integer),
|
|
154
|
+
cast(json_extract(value, '$.queue') AS text),
|
|
155
|
+
coalesce(cast(json_extract(value, '$.scheduled_at') AS text), datetime('now', 'subsec')),
|
|
156
|
+
cast(json_extract(value, '$.state') AS text),
|
|
157
|
+
jsonb(json_extract(value, '$.tags')),
|
|
158
|
+
CASE
|
|
159
|
+
WHEN length(cast(json_extract(value, '$.unique_key') AS text)) = 0 THEN NULL
|
|
160
|
+
ELSE unhex(cast(json_extract(value, '$.unique_key') AS text))
|
|
161
|
+
END,
|
|
162
|
+
nullif(cast(json_extract(value, '$.unique_states') AS integer), 0)
|
|
163
|
+
FROM json_each(cast(? AS blob))
|
|
164
|
+
WHERE true
|
|
165
|
+
ON CONFLICT (unique_key) WHERE #{SQLITE_CONFLICT_WHERE}
|
|
166
|
+
DO UPDATE SET kind = EXCLUDED.kind
|
|
167
|
+
RETURNING #{SQLITE_JOB_COLUMNS}
|
|
168
|
+
SQL
|
|
169
|
+
|
|
170
|
+
rows = @db.fetch(sql, JSON.dump(jobs)).all
|
|
171
|
+
sqlite_notify_insert(insert_params_array)
|
|
172
|
+
|
|
173
|
+
rows.map do |row|
|
|
174
|
+
metadata = JSON.parse(row[:metadata])
|
|
175
|
+
[sqlite_to_job_row_from_raw(row), metadata[SQLITE_UNIQUE_NONCE_KEY] != nonce]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
50
178
|
end
|
|
51
179
|
|
|
52
|
-
private def
|
|
180
|
+
private def postgres_insert_params_to_hash(insert_params)
|
|
53
181
|
{
|
|
54
182
|
args: insert_params.encoded_args,
|
|
55
183
|
kind: insert_params.kind,
|
|
@@ -64,11 +192,32 @@ module River::Driver
|
|
|
64
192
|
}
|
|
65
193
|
end
|
|
66
194
|
|
|
67
|
-
private def
|
|
68
|
-
|
|
195
|
+
private def sqlite_insert_params_to_hash(insert_params, nonce)
|
|
196
|
+
{
|
|
197
|
+
args: JSON.parse(insert_params.encoded_args),
|
|
198
|
+
kind: insert_params.kind,
|
|
199
|
+
max_attempts: insert_params.max_attempts,
|
|
200
|
+
metadata: {SQLITE_UNIQUE_NONCE_KEY => nonce},
|
|
201
|
+
priority: insert_params.priority,
|
|
202
|
+
queue: insert_params.queue,
|
|
203
|
+
scheduled_at: insert_params.scheduled_at ? format_time(insert_params.scheduled_at) : nil,
|
|
204
|
+
state: insert_params.state,
|
|
205
|
+
tags: insert_params.tags || [],
|
|
206
|
+
unique_key: insert_params.unique_key&.unpack1("H*"),
|
|
207
|
+
unique_states: insert_params.unique_states&.to_i(2)
|
|
208
|
+
}
|
|
69
209
|
end
|
|
70
210
|
|
|
71
211
|
private def to_job_row(river_job)
|
|
212
|
+
if @is_sqlite
|
|
213
|
+
row = sqlite_job_rows("WHERE id = ? LIMIT 1", river_job[:id]).first
|
|
214
|
+
sqlite_to_job_row_from_raw(row)
|
|
215
|
+
else
|
|
216
|
+
postgres_to_job_row(river_job)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private def postgres_to_job_row(river_job)
|
|
72
221
|
River::JobRow.new(
|
|
73
222
|
id: river_job[:id],
|
|
74
223
|
args: river_job[:args].to_h,
|
|
@@ -97,5 +246,65 @@ module River::Driver
|
|
|
97
246
|
unique_states: ::River::UniqueBitmask.to_states(river_job[:unique_states]&.to_i(2))
|
|
98
247
|
)
|
|
99
248
|
end
|
|
249
|
+
|
|
250
|
+
private def sqlite_to_job_row_from_raw(river_job)
|
|
251
|
+
errors = river_job[:errors] ? JSON.parse(river_job[:errors]) : []
|
|
252
|
+
|
|
253
|
+
River::JobRow.new(
|
|
254
|
+
id: river_job[:id],
|
|
255
|
+
args: JSON.parse(river_job[:args]),
|
|
256
|
+
attempt: river_job[:attempt],
|
|
257
|
+
attempted_at: parse_sqlite_time(river_job[:attempted_at]),
|
|
258
|
+
attempted_by: river_job[:attempted_by] ? JSON.parse(river_job[:attempted_by]) : nil,
|
|
259
|
+
created_at: parse_sqlite_time(river_job[:created_at]),
|
|
260
|
+
errors: errors.map { |deserialized_error|
|
|
261
|
+
River::AttemptError.new(
|
|
262
|
+
at: Time.parse(deserialized_error["at"]),
|
|
263
|
+
attempt: deserialized_error["attempt"],
|
|
264
|
+
error: deserialized_error["error"],
|
|
265
|
+
trace: deserialized_error["trace"]
|
|
266
|
+
)
|
|
267
|
+
},
|
|
268
|
+
finalized_at: parse_sqlite_time(river_job[:finalized_at]),
|
|
269
|
+
kind: river_job[:kind],
|
|
270
|
+
max_attempts: river_job[:max_attempts],
|
|
271
|
+
metadata: JSON.parse(river_job[:metadata]),
|
|
272
|
+
priority: river_job[:priority],
|
|
273
|
+
queue: river_job[:queue],
|
|
274
|
+
scheduled_at: parse_sqlite_time(river_job[:scheduled_at]),
|
|
275
|
+
state: river_job[:state],
|
|
276
|
+
tags: JSON.parse(river_job[:tags]),
|
|
277
|
+
unique_key: river_job[:unique_key]&.to_s,
|
|
278
|
+
unique_states: river_job[:unique_states] ? ::River::UniqueBitmask.to_states(river_job[:unique_states]) : nil
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
private def format_time(time)
|
|
283
|
+
time.getutc.round(3).strftime("%Y-%m-%d %H:%M:%S.%3N")
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
private def parse_sqlite_time(value)
|
|
287
|
+
return nil unless value
|
|
288
|
+
|
|
289
|
+
value = value.to_s
|
|
290
|
+
value += " UTC" unless value.match?(/(?:Z|[+-]\d{2}:?\d{2})\z/)
|
|
291
|
+
Time.parse(value).utc
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
private def sqlite_job_rows(suffix, *binds)
|
|
295
|
+
@db.fetch("SELECT #{SQLITE_JOB_COLUMNS} FROM river_job #{suffix}", *binds).all
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
private def sqlite_notify_insert(insert_params_array)
|
|
299
|
+
queues = insert_params_array
|
|
300
|
+
.select { |param| param.state == ::River::JOB_STATE_AVAILABLE }
|
|
301
|
+
.map(&:queue)
|
|
302
|
+
.uniq
|
|
303
|
+
return if queues.empty?
|
|
304
|
+
|
|
305
|
+
@db[:river_notification].multi_insert(queues.map do |queue|
|
|
306
|
+
{payload: JSON.dump({queue: queue}), topic: "insert"}
|
|
307
|
+
end)
|
|
308
|
+
end
|
|
100
309
|
end
|
|
101
310
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: riverqueue-sequel
|
|
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
|
|
@@ -10,26 +10,6 @@ bindir: bin
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: pg
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - ">"
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
20
|
-
- - "<"
|
|
21
|
-
- !ruby/object:Gem::Version
|
|
22
|
-
version: '1000'
|
|
23
|
-
type: :runtime
|
|
24
|
-
prerelease: false
|
|
25
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
-
requirements:
|
|
27
|
-
- - ">"
|
|
28
|
-
- !ruby/object:Gem::Version
|
|
29
|
-
version: '0'
|
|
30
|
-
- - "<"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1000'
|
|
33
13
|
- !ruby/object:Gem::Dependency
|
|
34
14
|
name: sequel
|
|
35
15
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -50,8 +30,8 @@ dependencies:
|
|
|
50
30
|
- - "<"
|
|
51
31
|
- !ruby/object:Gem::Version
|
|
52
32
|
version: '1000'
|
|
53
|
-
description: Sequel driver for the River Ruby gem. Use in conjunction
|
|
54
|
-
gem to insert jobs that are worked in Go.
|
|
33
|
+
description: Sequel PostgreSQL and SQLite driver for the River Ruby gem. Use in conjunction
|
|
34
|
+
with the riverqueue gem to insert jobs that are worked in Go.
|
|
55
35
|
email: brandur@brandur.org
|
|
56
36
|
executables: []
|
|
57
37
|
extensions: []
|
|
@@ -79,5 +59,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
79
59
|
requirements: []
|
|
80
60
|
rubygems_version: 4.0.9
|
|
81
61
|
specification_version: 4
|
|
82
|
-
summary: Sequel driver for the River Ruby gem.
|
|
62
|
+
summary: Sequel PostgreSQL and SQLite driver for the River Ruby gem.
|
|
83
63
|
test_files: []
|