riverqueue-activerecord 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/lib/driver.rb +234 -254
- data/lib/riverqueue-activerecord.rb +3 -0
- metadata +19 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe1889a91869f37c4e65eb7d68f94ca9413e1c27c35b6388bfc032cdc7042efd
|
|
4
|
+
data.tar.gz: 538826dffc04cf44bddb4a45deec552f99f3c71d3059d42453929a05383c6995
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea37012be253b41ee43e17f43837ec35809171c6668bd367faf9716c930e1321521206757636013e135dbfd3f13e677b8ec0076f245be2327ddded4bfa9c09c8
|
|
7
|
+
data.tar.gz: dcf1f5c99e8ebef5f2900f702b3714b26d1de04ad63a7b4e938c9e499ecd1ac8f51ad8a6d43b2dd942d58b34ca223dc4714ed3901aba27ada01c586b5e75e20c
|
data/lib/driver.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "securerandom"
|
|
2
4
|
|
|
3
5
|
module River::Driver
|
|
4
|
-
# Provides an ActiveRecord driver for River
|
|
6
|
+
# Provides an ActiveRecord driver for River supporting PostgreSQL, YugabyteDB,
|
|
5
7
|
# and SQLite.
|
|
6
8
|
#
|
|
7
9
|
# Used in conjunction with a River client like:
|
|
@@ -10,7 +12,101 @@ module River::Driver
|
|
|
10
12
|
# client = River::Client.new(River::Driver::ActiveRecord.new)
|
|
11
13
|
#
|
|
12
14
|
class ActiveRecord
|
|
13
|
-
|
|
15
|
+
include River::Driver::Runtime
|
|
16
|
+
|
|
17
|
+
# Connection class whose pool and transaction context this driver uses.
|
|
18
|
+
attr_reader :connection_class
|
|
19
|
+
|
|
20
|
+
# Uses an established PostgreSQL or SQLite connection. The class must be
|
|
21
|
+
# ActiveRecord::Base or an abstract Active Record class. Routing follows its
|
|
22
|
+
# current role/shard; configure consumers explicitly for each database.
|
|
23
|
+
def initialize(connection_class: ::ActiveRecord::Base)
|
|
24
|
+
unless connection_class.is_a?(Class) && connection_class <= ::ActiveRecord::Base
|
|
25
|
+
raise ArgumentError, "connection_class must be an Active Record class"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
unless connection_class == ::ActiveRecord::Base || connection_class.abstract_class?
|
|
29
|
+
raise ArgumentError, "connection_class must be abstract"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@connection_class = connection_class
|
|
33
|
+
@postgres_capabilities_cache = PostgresCapabilities::Cache.new
|
|
34
|
+
@is_sqlite = connection_class.connection.adapter_name.downcase.include?("sqlite")
|
|
35
|
+
# Do not inherit application scopes, callbacks, or schema caches. Each
|
|
36
|
+
# driver owns its model, but all operations use the selected class's pool.
|
|
37
|
+
@job_model = Class.new(::ActiveRecord::Base) do
|
|
38
|
+
self.table_name = "river_job"
|
|
39
|
+
define_singleton_method(:connection_pool) { connection_class.connection_pool }
|
|
40
|
+
|
|
41
|
+
def self.dangerous_attribute_method?(method_name)
|
|
42
|
+
return false if method_name == "errors"
|
|
43
|
+
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def errors = {}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def job_get_by_id(id)
|
|
52
|
+
if @is_sqlite
|
|
53
|
+
row = sqlite_job_rows("WHERE id = ? LIMIT 1", [id]).first
|
|
54
|
+
row ? sqlite_to_job_row_from_raw(row) : nil
|
|
55
|
+
else
|
|
56
|
+
row = @job_model.find_by(id: id)
|
|
57
|
+
row ? to_job_row_from_model(row) : nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def job_insert(insert_params)
|
|
62
|
+
job_insert_many([insert_params]).first
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def job_insert_many(insert_params_many)
|
|
66
|
+
return [] if insert_params_many.empty?
|
|
67
|
+
|
|
68
|
+
return sqlite_job_insert_many(insert_params_many) if @is_sqlite
|
|
69
|
+
|
|
70
|
+
transaction do
|
|
71
|
+
results = postgres_job_insert_many(insert_params_many)
|
|
72
|
+
postgres_notify_insert(insert_params_many)
|
|
73
|
+
results
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def job_list(params = :all)
|
|
78
|
+
return super unless params == :all
|
|
79
|
+
|
|
80
|
+
runtime_job_rows("ORDER BY id")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def rollback_exception
|
|
84
|
+
::ActiveRecord::Rollback
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Backend used by River::Migrator.
|
|
88
|
+
def migration_backend = @is_sqlite ? :sqlite : :postgresql
|
|
89
|
+
|
|
90
|
+
# Pins a raw connection for migration SQL and refreshes cached model columns.
|
|
91
|
+
def migration_connection
|
|
92
|
+
@connection_class.connection_pool.with_connection do |connection|
|
|
93
|
+
raise River::Error, "migrations cannot run inside an application transaction" if connection.transaction_open?
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
yield connection.raw_connection
|
|
97
|
+
ensure
|
|
98
|
+
@job_model.reset_column_information
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Runs the block in a new Active Record transaction or savepoint and returns
|
|
104
|
+
# the block's result.
|
|
105
|
+
def transaction(&)
|
|
106
|
+
@connection_class.transaction(requires_new: true, &)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
SQLITE_CONFLICT_WHERE = <<~SQL.chomp.freeze
|
|
14
110
|
unique_key IS NOT NULL
|
|
15
111
|
AND unique_states IS NOT NULL
|
|
16
112
|
AND CASE state
|
|
@@ -25,123 +121,169 @@ module River::Driver
|
|
|
25
121
|
ELSE 0
|
|
26
122
|
END >= 1
|
|
27
123
|
SQL
|
|
28
|
-
private_constant :SQLITE_CONFLICT_WHERE
|
|
29
124
|
|
|
30
125
|
# SQLite 3.45+ may store JSON as binary JSONB. Always project JSON columns
|
|
31
126
|
# through json() so this driver can read both the current JSONB format and
|
|
32
127
|
# the text JSON used by River migrations through version 006. Cast times to
|
|
33
128
|
# text so ActiveRecord doesn't interpret timezone-less SQLite timestamps in
|
|
34
|
-
# the process timezone.
|
|
35
|
-
|
|
129
|
+
# the process timezone. Flag 10 validates JSONB contents, not just its header,
|
|
130
|
+
# so damaged blobs reach the row decoder without aborting the SQL query.
|
|
131
|
+
SQLITE_JOB_COLUMNS = <<~SQL.chomp.freeze
|
|
36
132
|
id,
|
|
37
|
-
json(args) AS args,
|
|
133
|
+
CASE WHEN json_valid(args, 10) THEN json(args) ELSE args END AS args,
|
|
38
134
|
attempt,
|
|
39
135
|
CAST(attempted_at AS text) AS attempted_at,
|
|
40
|
-
json(attempted_by) AS attempted_by,
|
|
136
|
+
CASE WHEN json_valid(attempted_by, 10) THEN json(attempted_by) ELSE attempted_by END AS attempted_by,
|
|
41
137
|
CAST(created_at AS text) AS created_at,
|
|
42
|
-
json(errors) AS errors,
|
|
138
|
+
CASE WHEN json_valid(errors, 10) THEN json(errors) ELSE errors END AS errors,
|
|
43
139
|
CAST(finalized_at AS text) AS finalized_at,
|
|
44
140
|
kind,
|
|
45
141
|
max_attempts,
|
|
46
|
-
json(metadata) AS metadata,
|
|
142
|
+
CASE WHEN json_valid(metadata, 10) THEN json(metadata) ELSE metadata END AS metadata,
|
|
47
143
|
priority,
|
|
48
144
|
queue,
|
|
49
145
|
state,
|
|
50
146
|
CAST(scheduled_at AS text) AS scheduled_at,
|
|
51
|
-
json(tags) AS tags,
|
|
147
|
+
CASE WHEN json_valid(tags, 10) THEN json(tags) ELSE tags END AS tags,
|
|
52
148
|
unique_key,
|
|
53
149
|
unique_states
|
|
54
150
|
SQL
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
private_constant :
|
|
59
|
-
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
151
|
+
|
|
152
|
+
UNIQUE_INSERT_METADATA_KEY = "river:unique_nonce"
|
|
153
|
+
|
|
154
|
+
private_constant :SQLITE_CONFLICT_WHERE, :SQLITE_JOB_COLUMNS, :UNIQUE_INSERT_METADATA_KEY
|
|
155
|
+
|
|
156
|
+
private def format_time(time)
|
|
157
|
+
time.getutc.round(3).strftime("%Y-%m-%d %H:%M:%S.%3N")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private def postgres_insert_params_to_hash(insert_params, nonce)
|
|
161
|
+
metadata = insert_params.metadata || {}
|
|
162
|
+
metadata = metadata.merge(UNIQUE_INSERT_METADATA_KEY => nonce) if nonce
|
|
163
|
+
{
|
|
164
|
+
args: JSON.parse(insert_params.encoded_args),
|
|
165
|
+
kind: insert_params.kind,
|
|
166
|
+
max_attempts: insert_params.max_attempts,
|
|
167
|
+
metadata: metadata,
|
|
168
|
+
priority: insert_params.priority,
|
|
169
|
+
queue: insert_params.queue,
|
|
170
|
+
scheduled_at: insert_params.scheduled_at,
|
|
171
|
+
state: insert_params.state,
|
|
172
|
+
tags: insert_params.tags || [],
|
|
173
|
+
unique_key: insert_params.unique_key,
|
|
174
|
+
unique_states: insert_params.unique_states
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private def postgres_job_insert_many(insert_params_many)
|
|
179
|
+
capabilities = postgres_capabilities
|
|
180
|
+
nonce = SecureRandom.hex(8) if capabilities.unique_insert_mode == :metadata_nonce
|
|
181
|
+
res = @job_model.upsert_all(
|
|
182
|
+
insert_params_many.map { |param| postgres_insert_params_to_hash(param, nonce) },
|
|
183
|
+
on_duplicate: Arel.sql("kind = EXCLUDED.kind"),
|
|
184
|
+
returning: Arel.sql("*, #{capabilities.unique_insert_sql} AS unique_skipped_as_duplicate"),
|
|
185
|
+
|
|
186
|
+
# It'd be nice to specify this as `(kind, unique_key) WHERE unique_key
|
|
187
|
+
# IS NOT NULL` like we do elsewhere, but in its pure ingenuity, fucking
|
|
188
|
+
# ActiveRecord tries to look up a unique index instead of letting
|
|
189
|
+
# Postgres handle that, and of course it doesn't support a `WHERE`
|
|
190
|
+
# clause. The workaround is to target the index name instead of columns.
|
|
191
|
+
unique_by: "river_job_unique_idx"
|
|
192
|
+
)
|
|
193
|
+
postgres_to_insert_results(res, nonce)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private def postgres_to_insert_results(res, nonce)
|
|
197
|
+
res.rows.map do |row|
|
|
198
|
+
job, duplicate = postgres_to_job_row_from_raw(row, res.columns, res.column_types)
|
|
199
|
+
[job, nonce ? job.metadata[UNIQUE_INSERT_METADATA_KEY] != nonce : duplicate]
|
|
83
200
|
end
|
|
84
201
|
end
|
|
85
202
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
203
|
+
private def postgres_to_job_row_from_model(river_job)
|
|
204
|
+
# Read attributes directly because Active Record shadows the errors column.
|
|
205
|
+
runtime_decode_postgres_job(river_job.attributes)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Upserts bypass model casting; normalize their values before shared decoding.
|
|
209
|
+
private def postgres_to_job_row_from_raw(row, columns, column_types)
|
|
210
|
+
river_job = {}
|
|
211
|
+
|
|
212
|
+
row.each_with_index do |val, i|
|
|
213
|
+
river_job[columns[i]] = column_types[i].deserialize(val)
|
|
93
214
|
end
|
|
215
|
+
|
|
216
|
+
[runtime_decode_postgres_job(river_job), river_job["unique_skipped_as_duplicate"]]
|
|
94
217
|
end
|
|
95
218
|
|
|
96
|
-
def
|
|
97
|
-
|
|
219
|
+
private def runtime_connection_pool
|
|
220
|
+
@connection_class.connection_pool
|
|
98
221
|
end
|
|
99
222
|
|
|
100
|
-
def
|
|
101
|
-
@
|
|
223
|
+
private def runtime_execute(sql)
|
|
224
|
+
@connection_class.connection.execute(sql)
|
|
102
225
|
end
|
|
103
226
|
|
|
104
|
-
def
|
|
227
|
+
private def runtime_job_list_without_params
|
|
228
|
+
job_list(:all)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private def runtime_job_rows(suffix)
|
|
105
232
|
if @is_sqlite
|
|
106
|
-
sqlite_job_rows(
|
|
233
|
+
sqlite_job_rows(suffix).map { |row| sqlite_to_job_row_from_raw(row) }
|
|
107
234
|
else
|
|
108
|
-
|
|
235
|
+
@job_model.find_by_sql("SELECT * FROM river_job #{suffix}").map { |row| to_job_row_from_model(row) }
|
|
109
236
|
end
|
|
110
237
|
end
|
|
111
238
|
|
|
112
|
-
def
|
|
113
|
-
|
|
239
|
+
private def runtime_postgres?
|
|
240
|
+
!@is_sqlite
|
|
114
241
|
end
|
|
115
242
|
|
|
116
|
-
def
|
|
117
|
-
|
|
243
|
+
private def runtime_query_rows(sql)
|
|
244
|
+
if @is_sqlite
|
|
245
|
+
@connection_class.connection.raw_connection.execute(sql).to_a
|
|
246
|
+
else
|
|
247
|
+
@connection_class.connection.select_all(sql).to_a
|
|
248
|
+
end
|
|
118
249
|
end
|
|
119
250
|
|
|
120
|
-
private def
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
on_duplicate: Arel.sql("kind = EXCLUDED.kind"),
|
|
124
|
-
returning: Arel.sql("*, (xmax != 0) AS unique_skipped_as_duplicate"),
|
|
251
|
+
private def runtime_quote(value)
|
|
252
|
+
@connection_class.connection.quote(value)
|
|
253
|
+
end
|
|
125
254
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
255
|
+
private def runtime_unique_violation_class
|
|
256
|
+
::ActiveRecord::RecordNotUnique
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
private def runtime_value(row, key)
|
|
260
|
+
row[key.to_s] || row[key]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
private def sqlite_insert_params_to_hash(insert_params, nonce)
|
|
264
|
+
{
|
|
265
|
+
args: JSON.parse(insert_params.encoded_args),
|
|
266
|
+
kind: insert_params.kind,
|
|
267
|
+
max_attempts: insert_params.max_attempts,
|
|
268
|
+
metadata: {UNIQUE_INSERT_METADATA_KEY => nonce},
|
|
269
|
+
priority: insert_params.priority,
|
|
270
|
+
queue: insert_params.queue,
|
|
271
|
+
scheduled_at: insert_params.scheduled_at ? format_time(insert_params.scheduled_at) : nil,
|
|
272
|
+
state: insert_params.state,
|
|
273
|
+
tags: insert_params.tags || [],
|
|
274
|
+
unique_key: insert_params.unique_key&.unpack1("H*"),
|
|
275
|
+
unique_states: insert_params.unique_states&.to_i(2)
|
|
276
|
+
}.tap { |values| values[:metadata] = (insert_params.metadata || {}).merge(values[:metadata]) }
|
|
134
277
|
end
|
|
135
278
|
|
|
136
279
|
# River's current SQLite driver uses json_each to make a batch a single,
|
|
137
280
|
# atomic statement. The JSON columns are converted to SQLite JSONB here,
|
|
138
281
|
# matching migration 007 and newer River databases.
|
|
139
282
|
private def sqlite_job_insert_many(insert_params_many)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
jobs = insert_params_many.map { |param| sqlite_insert_params_to_hash(param, nonce) }
|
|
283
|
+
nonces = sqlite_insert_nonces(insert_params_many)
|
|
284
|
+
@connection_class.transaction(requires_new: true) do
|
|
285
|
+
jobs = insert_params_many.zip(nonces).map { |param, nonce| sqlite_insert_params_to_hash(param, nonce) }
|
|
286
|
+
inserted_nonces = nonces.to_h { |nonce| [nonce, true] }
|
|
145
287
|
|
|
146
288
|
sql = <<~SQL
|
|
147
289
|
INSERT INTO river_job (
|
|
@@ -181,194 +323,19 @@ module River::Driver
|
|
|
181
323
|
RETURNING #{SQLITE_JOB_COLUMNS}
|
|
182
324
|
SQL
|
|
183
325
|
|
|
184
|
-
rows =
|
|
326
|
+
rows = @connection_class.connection.raw_connection.execute(sql, [JSON.generate(jobs)])
|
|
185
327
|
sqlite_notify_insert(insert_params_many)
|
|
186
328
|
|
|
187
329
|
rows.map do |row|
|
|
188
330
|
metadata = JSON.parse(row["metadata"])
|
|
189
|
-
[sqlite_to_job_row_from_raw(row), metadata[
|
|
331
|
+
[sqlite_to_job_row_from_raw(row), !inserted_nonces.key?(metadata[UNIQUE_INSERT_METADATA_KEY])]
|
|
190
332
|
end
|
|
191
333
|
end
|
|
192
334
|
end
|
|
193
335
|
|
|
194
|
-
private def postgres_insert_params_to_hash(insert_params)
|
|
195
|
-
{
|
|
196
|
-
args: JSON.parse(insert_params.encoded_args),
|
|
197
|
-
kind: insert_params.kind,
|
|
198
|
-
max_attempts: insert_params.max_attempts,
|
|
199
|
-
priority: insert_params.priority,
|
|
200
|
-
queue: insert_params.queue,
|
|
201
|
-
state: insert_params.state,
|
|
202
|
-
scheduled_at: insert_params.scheduled_at,
|
|
203
|
-
tags: insert_params.tags || [],
|
|
204
|
-
unique_key: insert_params.unique_key,
|
|
205
|
-
unique_states: insert_params.unique_states
|
|
206
|
-
}
|
|
207
|
-
end
|
|
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
|
-
|
|
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)
|
|
230
|
-
# needs to be accessed through values because `errors` is shadowed by both
|
|
231
|
-
# ActiveRecord and the patch above
|
|
232
|
-
errors = river_job.attributes["errors"]
|
|
233
|
-
|
|
234
|
-
River::JobRow.new(
|
|
235
|
-
id: river_job.id,
|
|
236
|
-
args: river_job.args,
|
|
237
|
-
attempt: river_job.attempt,
|
|
238
|
-
attempted_at: river_job.attempted_at&.getutc,
|
|
239
|
-
attempted_by: river_job.attempted_by,
|
|
240
|
-
created_at: river_job.created_at.getutc,
|
|
241
|
-
errors: errors&.map { |e|
|
|
242
|
-
deserialized_error = JSON.parse(e, symbolize_names: true)
|
|
243
|
-
|
|
244
|
-
River::AttemptError.new(
|
|
245
|
-
at: Time.parse(deserialized_error[:at]),
|
|
246
|
-
attempt: deserialized_error[:attempt],
|
|
247
|
-
error: deserialized_error[:error],
|
|
248
|
-
trace: deserialized_error[:trace]
|
|
249
|
-
)
|
|
250
|
-
},
|
|
251
|
-
finalized_at: river_job.finalized_at&.getutc,
|
|
252
|
-
kind: river_job.kind,
|
|
253
|
-
max_attempts: river_job.max_attempts,
|
|
254
|
-
metadata: river_job.metadata,
|
|
255
|
-
priority: river_job.priority,
|
|
256
|
-
queue: river_job.queue,
|
|
257
|
-
scheduled_at: river_job.scheduled_at.getutc,
|
|
258
|
-
state: river_job.state,
|
|
259
|
-
tags: river_job.tags,
|
|
260
|
-
unique_key: river_job.unique_key,
|
|
261
|
-
unique_states: river_job.unique_states
|
|
262
|
-
)
|
|
263
|
-
end
|
|
264
|
-
|
|
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)
|
|
303
|
-
res.rows.map do |row|
|
|
304
|
-
postgres_to_job_row_from_raw(row, res.columns, res.column_types)
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
# This is really awful, but some of ActiveRecord's methods (e.g. `.create`)
|
|
309
|
-
# return a model, and others (e.g. `.upsert`) return raw values, and
|
|
310
|
-
# therefore this second version from unmarshaling a job row exists. I
|
|
311
|
-
# searched long and hard for a way to have the former type of method return
|
|
312
|
-
# raw or the latter type of method return a model, but was unable to find
|
|
313
|
-
# anything.
|
|
314
|
-
private def postgres_to_job_row_from_raw(row, columns, column_types)
|
|
315
|
-
river_job = {}
|
|
316
|
-
|
|
317
|
-
row.each_with_index do |val, i|
|
|
318
|
-
river_job[columns[i]] = column_types[i].deserialize(val)
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
errors = river_job["errors"]&.map do |e|
|
|
322
|
-
deserialized_error = JSON.parse(e)
|
|
323
|
-
|
|
324
|
-
River::AttemptError.new(
|
|
325
|
-
at: Time.parse(deserialized_error["at"]),
|
|
326
|
-
attempt: deserialized_error["attempt"],
|
|
327
|
-
error: deserialized_error["error"],
|
|
328
|
-
trace: deserialized_error["trace"]
|
|
329
|
-
)
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
[
|
|
333
|
-
River::JobRow.new(
|
|
334
|
-
id: river_job["id"],
|
|
335
|
-
args: river_job["args"],
|
|
336
|
-
attempt: river_job["attempt"],
|
|
337
|
-
attempted_at: river_job["attempted_at"]&.getutc,
|
|
338
|
-
attempted_by: river_job["attempted_by"],
|
|
339
|
-
created_at: river_job["created_at"].getutc,
|
|
340
|
-
errors: errors,
|
|
341
|
-
finalized_at: river_job["finalized_at"]&.getutc,
|
|
342
|
-
kind: river_job["kind"],
|
|
343
|
-
max_attempts: river_job["max_attempts"],
|
|
344
|
-
metadata: river_job["metadata"],
|
|
345
|
-
priority: river_job["priority"],
|
|
346
|
-
queue: river_job["queue"],
|
|
347
|
-
scheduled_at: river_job["scheduled_at"].getutc,
|
|
348
|
-
state: river_job["state"],
|
|
349
|
-
tags: river_job["tags"],
|
|
350
|
-
unique_key: river_job["unique_key"],
|
|
351
|
-
unique_states: ::River::UniqueBitmask.to_states(river_job["unique_states"]&.to_i(2))
|
|
352
|
-
),
|
|
353
|
-
river_job["unique_skipped_as_duplicate"]
|
|
354
|
-
]
|
|
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
336
|
private def sqlite_job_rows(suffix, binds = [])
|
|
370
337
|
sql = "SELECT #{SQLITE_JOB_COLUMNS} FROM river_job #{suffix}"
|
|
371
|
-
|
|
338
|
+
@connection_class.connection.raw_connection.execute(sql, binds)
|
|
372
339
|
end
|
|
373
340
|
|
|
374
341
|
private def sqlite_notify_insert(insert_params_many)
|
|
@@ -379,10 +346,10 @@ module River::Driver
|
|
|
379
346
|
return if queues.empty?
|
|
380
347
|
|
|
381
348
|
notifications = queues.map do |queue|
|
|
382
|
-
{payload: JSON.
|
|
349
|
+
{payload: JSON.generate({queue: queue}), topic: "river_insert"}
|
|
383
350
|
end
|
|
384
351
|
|
|
385
|
-
|
|
352
|
+
@connection_class.connection.raw_connection.execute(<<~SQL, [JSON.generate(notifications)])
|
|
386
353
|
INSERT INTO river_notification (payload, topic)
|
|
387
354
|
SELECT
|
|
388
355
|
json_extract(value, '$.payload'),
|
|
@@ -390,5 +357,18 @@ module River::Driver
|
|
|
390
357
|
FROM json_each(cast(? AS blob))
|
|
391
358
|
SQL
|
|
392
359
|
end
|
|
360
|
+
|
|
361
|
+
private def sqlite_to_job_row_from_model(river_job)
|
|
362
|
+
row = sqlite_job_rows("WHERE id = ? LIMIT 1", [river_job.id]).first
|
|
363
|
+
sqlite_to_job_row_from_raw(row)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
private def sqlite_to_job_row_from_raw(row)
|
|
367
|
+
runtime_decode_sqlite_job(row)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
private def to_job_row_from_model(river_job)
|
|
371
|
+
@is_sqlite ? sqlite_to_job_row_from_model(river_job) : postgres_to_job_row_from_model(river_job)
|
|
372
|
+
end
|
|
393
373
|
end
|
|
394
374
|
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.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Blake Gentry
|
|
@@ -50,8 +50,22 @@ dependencies:
|
|
|
50
50
|
- - "<"
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
52
|
version: '1000'
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: riverqueue
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - '='
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 0.12.0
|
|
60
|
+
type: :runtime
|
|
61
|
+
prerelease: false
|
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - '='
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: 0.12.0
|
|
67
|
+
description: ActiveRecord PostgreSQL and SQLite driver for inserting and working River
|
|
68
|
+
jobs in Ruby.
|
|
55
69
|
email: brandur@brandur.org
|
|
56
70
|
executables: []
|
|
57
71
|
extensions: []
|
|
@@ -61,7 +75,7 @@ files:
|
|
|
61
75
|
- lib/riverqueue-activerecord.rb
|
|
62
76
|
homepage: https://riverqueue.com
|
|
63
77
|
licenses:
|
|
64
|
-
-
|
|
78
|
+
- MPL-2.0
|
|
65
79
|
metadata: {}
|
|
66
80
|
rdoc_options: []
|
|
67
81
|
require_paths:
|
|
@@ -70,7 +84,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
70
84
|
requirements:
|
|
71
85
|
- - ">="
|
|
72
86
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '
|
|
87
|
+
version: '3.2'
|
|
74
88
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
89
|
requirements:
|
|
76
90
|
- - ">="
|