riverqueue-sequel 0.10.1 → 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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/driver.rb +271 -50
  3. data/lib/riverqueue-sequel.rb +3 -0
  4. metadata +12 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 863659587eec09b4b452469224566c7ca5ba81e34b743326f7ba0e8e7dcd8935
4
- data.tar.gz: b1b720a495fc3a5eaf8413fdc8732eda6a1de2bb7e033a0ec6531337a3d53f3f
3
+ metadata.gz: 8672d5a5d5c01059a17a307704e5512496aecc5e62f57afe2dfcd5ddd8569a34
4
+ data.tar.gz: abca68b736d841506449226a45e7b3d049884b40accdfb2c0e5fecf8cbbf896a
5
5
  SHA512:
6
- metadata.gz: 6d47a5886765f27631e57ed847a4f84814bf4814b10f3dc971776d883d781dd4d7bf2610d841b8495af55bc9d4bd5f91424722e2a29a0a8ce84fc254a407a5f0
7
- data.tar.gz: 9dbd8f19c8ce23281bf6c78fde501e7dbb5f8969562ef25fc0945bd58ec2879948d7cdd49671e9dee93480f703deb9e417e86fb4cbda089797246c22e23823d0
6
+ metadata.gz: 8af058f89ffef579beb2cc91f428a4e004a7d844c61fd2ae588c8214e6fd54e7dc3b22acf7a01feca0dd2474945c5a6ec408378b42918e4f7bd29359c861c8fe
7
+ data.tar.gz: 1d16f622b3df74b166ffccabee0c2374d810b43d026d344529df4f7d9eb2fd66a7f0bc2448a4e12619d00632c99e119a312f487635074dc14f35c47a44fbffd3
data/lib/driver.rb CHANGED
@@ -1,21 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
1
5
  module River::Driver
2
- # Provides a Sequel driver for River.
6
+ # Provides a Sequel driver for River supporting PostgreSQL, YugabyteDB, and SQLite.
3
7
  #
4
8
  # Used in conjunction with a River client like:
5
9
  #
6
10
  # DB = Sequel.connect("postgres://...")
7
11
  # client = River::Client.new(River::Driver::Sequel.new(DB))
8
12
  #
13
+ # Or with SQLite:
14
+ #
15
+ # DB = Sequel.connect("sqlite://path/to/river.db")
16
+ # client = River::Client.new(River::Driver::Sequel.new(DB))
17
+ #
9
18
  class Sequel
19
+ include River::Driver::Runtime
20
+
21
+ # Creates a driver backed by a connected Sequel::Database for PostgreSQL or
22
+ # SQLite.
10
23
  def initialize(db)
11
24
  @db = db
12
- @db.extension(:pg_array)
13
- @db.extension(:pg_json)
25
+ @postgres_capabilities_cache = PostgresCapabilities::Cache.new
26
+ @is_sqlite = (db.database_type == :sqlite)
27
+
28
+ unless @is_sqlite
29
+ db.extension(:pg_array)
30
+ db.extension(:pg_json)
31
+ end
14
32
  end
15
33
 
16
34
  def job_get_by_id(id)
17
- data_set = @db[:river_job].where(id: id)
18
- data_set.first ? to_job_row(data_set.first) : nil
35
+ if @is_sqlite
36
+ row = sqlite_job_rows("WHERE id = ? LIMIT 1", id).first
37
+ row ? sqlite_to_job_row_from_raw(row) : nil
38
+ else
39
+ row = @db[:river_job].where(id: id).first
40
+ row ? to_job_row(row) : nil
41
+ end
19
42
  end
20
43
 
21
44
  def job_insert(insert_params)
@@ -23,79 +46,277 @@ module River::Driver
23
46
  end
24
47
 
25
48
  def job_insert_many(insert_params_array)
26
- @db[:river_job]
27
- .insert_conflict(
28
- target: [:unique_key],
29
- conflict_where: ::Sequel.lit(
30
- "unique_key IS NOT NULL AND unique_states IS NOT NULL AND river_job_state_in_bitmask(unique_states, state)"
31
- ),
32
- update: {kind: ::Sequel[:excluded][:kind]}
33
- )
34
- .returning(::Sequel.lit("*, (xmax != 0) AS unique_skipped_as_duplicate"))
35
- .multi_insert(insert_params_array.map { |p| insert_params_to_hash(p) })
36
- .map { |row| to_insert_result(row) }
49
+ return [] if insert_params_array.empty?
50
+
51
+ return sqlite_job_insert_many(insert_params_array) if @is_sqlite
52
+
53
+ transaction do
54
+ results = postgres_job_insert_many(insert_params_array)
55
+ postgres_notify_insert(insert_params_array)
56
+ results
57
+ end
37
58
  end
38
59
 
39
- def job_list
40
- data_set = @db[:river_job].order_by(:id)
41
- data_set.all.map { |job| to_job_row(job) }
60
+ def job_list(params = :all)
61
+ return super unless params == :all
62
+
63
+ runtime_job_rows("ORDER BY id")
42
64
  end
43
65
 
44
66
  def rollback_exception
45
67
  ::Sequel::Rollback
46
68
  end
47
69
 
70
+ # Backend used by River::Migrator.
71
+ def migration_backend = @is_sqlite ? :sqlite : :postgresql
72
+
73
+ # Pins a raw connection for the complete migration operation.
74
+ def migration_connection
75
+ @db.synchronize do |connection|
76
+ raise River::Error, "migrations cannot run inside an application transaction" if @db.in_transaction?
77
+
78
+ yield connection
79
+ end
80
+ end
81
+
82
+ # Runs the block in a Sequel transaction or savepoint and returns the
83
+ # block's result.
48
84
  def transaction(&)
49
85
  @db.transaction(savepoint: true, &)
50
86
  end
51
87
 
52
- private def insert_params_to_hash(insert_params)
88
+ SQLITE_CONFLICT_WHERE = <<~SQL.chomp.freeze
89
+ unique_key IS NOT NULL
90
+ AND unique_states IS NOT NULL
91
+ AND CASE state
92
+ WHEN 'available' THEN unique_states & (1 << 0)
93
+ WHEN 'cancelled' THEN unique_states & (1 << 1)
94
+ WHEN 'completed' THEN unique_states & (1 << 2)
95
+ WHEN 'discarded' THEN unique_states & (1 << 3)
96
+ WHEN 'pending' THEN unique_states & (1 << 4)
97
+ WHEN 'retryable' THEN unique_states & (1 << 5)
98
+ WHEN 'running' THEN unique_states & (1 << 6)
99
+ WHEN 'scheduled' THEN unique_states & (1 << 7)
100
+ ELSE 0
101
+ END >= 1
102
+ SQL
103
+
104
+ # SQLite 3.45+ may store JSON as binary JSONB. Always project JSON columns
105
+ # through json() so this driver can read both the current JSONB format and
106
+ # the text JSON used by River migrations through version 006. Cast times to
107
+ # text so Sequel doesn't interpret timezone-less SQLite timestamps in the
108
+ # process timezone. Flag 10 validates JSONB contents, not just its header,
109
+ # so damaged blobs reach the row decoder without aborting the SQL query.
110
+ SQLITE_JOB_COLUMNS = <<~SQL.chomp.freeze
111
+ id,
112
+ CASE WHEN json_valid(args, 10) THEN json(args) ELSE args END AS args,
113
+ attempt,
114
+ CAST(attempted_at AS text) AS attempted_at,
115
+ CASE WHEN json_valid(attempted_by, 10) THEN json(attempted_by) ELSE attempted_by END AS attempted_by,
116
+ CAST(created_at AS text) AS created_at,
117
+ CASE WHEN json_valid(errors, 10) THEN json(errors) ELSE errors END AS errors,
118
+ CAST(finalized_at AS text) AS finalized_at,
119
+ kind,
120
+ max_attempts,
121
+ CASE WHEN json_valid(metadata, 10) THEN json(metadata) ELSE metadata END AS metadata,
122
+ priority,
123
+ queue,
124
+ state,
125
+ CAST(scheduled_at AS text) AS scheduled_at,
126
+ CASE WHEN json_valid(tags, 10) THEN json(tags) ELSE tags END AS tags,
127
+ unique_key,
128
+ unique_states
129
+ SQL
130
+
131
+ UNIQUE_INSERT_METADATA_KEY = "river:unique_nonce"
132
+
133
+ private_constant :SQLITE_CONFLICT_WHERE, :SQLITE_JOB_COLUMNS, :UNIQUE_INSERT_METADATA_KEY
134
+
135
+ private def format_time(time)
136
+ time.getutc.round(3).strftime("%Y-%m-%d %H:%M:%S.%3N")
137
+ end
138
+
139
+ private def postgres_insert_params_to_hash(insert_params, nonce)
140
+ metadata = insert_params.metadata || {}
141
+ metadata = metadata.merge(UNIQUE_INSERT_METADATA_KEY => nonce) if nonce
53
142
  {
54
143
  args: insert_params.encoded_args,
55
144
  kind: insert_params.kind,
56
145
  max_attempts: insert_params.max_attempts,
146
+ metadata: ::Sequel.pg_jsonb(metadata),
57
147
  priority: insert_params.priority,
58
148
  queue: insert_params.queue,
59
- state: insert_params.state,
60
149
  scheduled_at: insert_params.scheduled_at,
150
+ state: insert_params.state,
61
151
  tags: ::Sequel.pg_array(insert_params.tags || [], :text),
62
152
  unique_key: insert_params.unique_key ? ::Sequel.blob(insert_params.unique_key) : nil,
63
153
  unique_states: insert_params.unique_states
64
154
  }
65
155
  end
66
156
 
67
- private def to_insert_result(result)
68
- [to_job_row(result), result[:unique_skipped_as_duplicate]]
157
+ private def postgres_job_insert_many(insert_params_array)
158
+ capabilities = postgres_capabilities
159
+ nonce = SecureRandom.hex(8) if capabilities.unique_insert_mode == :metadata_nonce
160
+ @db[:river_job]
161
+ .insert_conflict(
162
+ conflict_where: ::Sequel.lit(
163
+ "unique_key IS NOT NULL AND unique_states IS NOT NULL AND river_job_state_in_bitmask(unique_states, state)"
164
+ ),
165
+ target: [:unique_key],
166
+ update: {kind: ::Sequel[:excluded][:kind]}
167
+ )
168
+ .returning(::Sequel.lit("*, #{capabilities.unique_insert_sql} AS unique_skipped_as_duplicate"))
169
+ .multi_insert(insert_params_array.map { |p| postgres_insert_params_to_hash(p, nonce) })
170
+ .map do |row|
171
+ job = to_job_row(row)
172
+ [job, nonce ? job.metadata[UNIQUE_INSERT_METADATA_KEY] != nonce : row[:unique_skipped_as_duplicate]]
173
+ end
69
174
  end
70
175
 
71
- private def to_job_row(river_job)
72
- River::JobRow.new(
73
- id: river_job[:id],
74
- args: river_job[:args].to_h,
75
- attempt: river_job[:attempt],
76
- attempted_at: river_job[:attempted_at]&.getutc,
77
- attempted_by: river_job[:attempted_by],
78
- created_at: river_job[:created_at].getutc,
79
- errors: river_job[:errors]&.map { |deserialized_error|
80
- River::AttemptError.new(
81
- at: Time.parse(deserialized_error["at"]),
82
- attempt: deserialized_error["attempt"],
83
- error: deserialized_error["error"],
84
- trace: deserialized_error["trace"]
176
+ private def postgres_to_job_row(river_job)
177
+ runtime_decode_postgres_job(river_job)
178
+ end
179
+
180
+ private def runtime_connection_pool
181
+ @db
182
+ end
183
+
184
+ private def runtime_execute(sql)
185
+ @db.run(sql)
186
+ end
187
+
188
+ private def runtime_job_list_without_params
189
+ job_list(:all)
190
+ end
191
+
192
+ private def runtime_job_rows(suffix)
193
+ if @is_sqlite
194
+ sqlite_job_rows(suffix).map { |row| sqlite_to_job_row_from_raw(row) }
195
+ else
196
+ @db.fetch("SELECT * FROM river_job #{suffix}").map { |row| to_job_row(row) }
197
+ end
198
+ end
199
+
200
+ private def runtime_postgres?
201
+ !@is_sqlite
202
+ end
203
+
204
+ private def runtime_query_rows(sql)
205
+ @db.fetch(sql).all
206
+ end
207
+
208
+ private def runtime_quote(value)
209
+ @db.literal(value)
210
+ end
211
+
212
+ private def runtime_unique_violation_class
213
+ ::Sequel::UniqueConstraintViolation
214
+ end
215
+
216
+ private def runtime_value(row, key)
217
+ row[key] || row[key.to_s]
218
+ end
219
+
220
+ private def sqlite_insert_params_to_hash(insert_params, nonce)
221
+ {
222
+ args: JSON.parse(insert_params.encoded_args),
223
+ kind: insert_params.kind,
224
+ max_attempts: insert_params.max_attempts,
225
+ metadata: {UNIQUE_INSERT_METADATA_KEY => nonce},
226
+ priority: insert_params.priority,
227
+ queue: insert_params.queue,
228
+ scheduled_at: insert_params.scheduled_at ? format_time(insert_params.scheduled_at) : nil,
229
+ state: insert_params.state,
230
+ tags: insert_params.tags || [],
231
+ unique_key: insert_params.unique_key&.unpack1("H*"),
232
+ unique_states: insert_params.unique_states&.to_i(2)
233
+ }.tap { |values| values[:metadata] = (insert_params.metadata || {}).merge(values[:metadata]) }
234
+ end
235
+
236
+ # River's current SQLite driver uses json_each to make a batch a single,
237
+ # atomic statement. The JSON columns are converted to SQLite JSONB here,
238
+ # matching migration 007 and newer River databases.
239
+ private def sqlite_job_insert_many(insert_params_array)
240
+ nonces = sqlite_insert_nonces(insert_params_array)
241
+ @db.transaction(savepoint: true) do
242
+ jobs = insert_params_array.zip(nonces).map { |param, nonce| sqlite_insert_params_to_hash(param, nonce) }
243
+ inserted_nonces = nonces.to_h { |nonce| [nonce, true] }
244
+
245
+ sql = <<~SQL
246
+ INSERT INTO river_job (
247
+ args,
248
+ created_at,
249
+ kind,
250
+ max_attempts,
251
+ metadata,
252
+ priority,
253
+ queue,
254
+ scheduled_at,
255
+ state,
256
+ tags,
257
+ unique_key,
258
+ unique_states
85
259
  )
86
- },
87
- finalized_at: river_job[:finalized_at]&.getutc,
88
- kind: river_job[:kind],
89
- max_attempts: river_job[:max_attempts],
90
- metadata: river_job[:metadata],
91
- priority: river_job[:priority],
92
- queue: river_job[:queue],
93
- scheduled_at: river_job[:scheduled_at].getutc,
94
- state: river_job[:state],
95
- tags: river_job[:tags].to_a,
96
- unique_key: river_job[:unique_key]&.to_s,
97
- unique_states: ::River::UniqueBitmask.to_states(river_job[:unique_states]&.to_i(2))
98
- )
260
+ SELECT
261
+ jsonb(json_extract(value, '$.args')),
262
+ datetime('now', 'subsec'),
263
+ cast(json_extract(value, '$.kind') AS text),
264
+ cast(json_extract(value, '$.max_attempts') AS integer),
265
+ jsonb(json_extract(value, '$.metadata')),
266
+ cast(json_extract(value, '$.priority') AS integer),
267
+ cast(json_extract(value, '$.queue') AS text),
268
+ coalesce(cast(json_extract(value, '$.scheduled_at') AS text), datetime('now', 'subsec')),
269
+ cast(json_extract(value, '$.state') AS text),
270
+ jsonb(json_extract(value, '$.tags')),
271
+ CASE
272
+ WHEN length(cast(json_extract(value, '$.unique_key') AS text)) = 0 THEN NULL
273
+ ELSE unhex(cast(json_extract(value, '$.unique_key') AS text))
274
+ END,
275
+ nullif(cast(json_extract(value, '$.unique_states') AS integer), 0)
276
+ FROM json_each(cast(? AS blob))
277
+ WHERE true
278
+ ON CONFLICT (unique_key) WHERE #{SQLITE_CONFLICT_WHERE}
279
+ DO UPDATE SET kind = EXCLUDED.kind
280
+ RETURNING #{SQLITE_JOB_COLUMNS}
281
+ SQL
282
+
283
+ rows = @db.fetch(sql, JSON.generate(jobs)).all
284
+ sqlite_notify_insert(insert_params_array)
285
+
286
+ rows.map do |row|
287
+ metadata = JSON.parse(row[:metadata])
288
+ [sqlite_to_job_row_from_raw(row), !inserted_nonces.key?(metadata[UNIQUE_INSERT_METADATA_KEY])]
289
+ end
290
+ end
291
+ end
292
+
293
+ private def sqlite_job_rows(suffix, *binds)
294
+ @db.fetch("SELECT #{SQLITE_JOB_COLUMNS} FROM river_job #{suffix}", *binds).all
295
+ end
296
+
297
+ private def sqlite_notify_insert(insert_params_array)
298
+ queues = insert_params_array
299
+ .select { |param| param.state == ::River::JOB_STATE_AVAILABLE }
300
+ .map(&:queue)
301
+ .uniq
302
+ return if queues.empty?
303
+
304
+ @db[:river_notification].multi_insert(queues.map do |queue|
305
+ {payload: JSON.generate({queue: queue}), topic: "river_insert"}
306
+ end)
307
+ end
308
+
309
+ private def sqlite_to_job_row_from_raw(row)
310
+ runtime_decode_sqlite_job(row)
311
+ end
312
+
313
+ private def to_job_row(river_job)
314
+ if @is_sqlite
315
+ row = sqlite_job_rows("WHERE id = ? LIMIT 1", river_job[:id]).first
316
+ sqlite_to_job_row_from_raw(row)
317
+ else
318
+ postgres_to_job_row(river_job)
319
+ end
99
320
  end
100
321
  end
101
322
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sequel"
4
+ require "riverqueue"
2
5
 
3
6
  require_relative "driver"
4
7
 
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.10.1
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Gentry
@@ -11,7 +11,7 @@ cert_chain: []
11
11
  date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: pg
14
+ name: sequel
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">"
@@ -31,27 +31,21 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: '1000'
33
33
  - !ruby/object:Gem::Dependency
34
- name: sequel
34
+ name: riverqueue
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">"
37
+ - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
40
- - - "<"
41
- - !ruby/object:Gem::Version
42
- version: '1000'
39
+ version: 0.12.0
43
40
  type: :runtime
44
41
  prerelease: false
45
42
  version_requirements: !ruby/object:Gem::Requirement
46
43
  requirements:
47
- - - ">"
48
- - !ruby/object:Gem::Version
49
- version: '0'
50
- - - "<"
44
+ - - '='
51
45
  - !ruby/object:Gem::Version
52
- version: '1000'
53
- description: Sequel driver for the River Ruby gem. Use in conjunction with the riverqueue
54
- gem to insert jobs that are worked in Go.
46
+ version: 0.12.0
47
+ description: Sequel PostgreSQL and SQLite driver for inserting and working River jobs
48
+ in Ruby.
55
49
  email: brandur@brandur.org
56
50
  executables: []
57
51
  extensions: []
@@ -61,7 +55,7 @@ files:
61
55
  - lib/riverqueue-sequel.rb
62
56
  homepage: https://riverqueue.com
63
57
  licenses:
64
- - LGPL-3.0-or-later
58
+ - MPL-2.0
65
59
  metadata: {}
66
60
  rdoc_options: []
67
61
  require_paths:
@@ -70,7 +64,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
70
64
  requirements:
71
65
  - - ">="
72
66
  - !ruby/object:Gem::Version
73
- version: '0'
67
+ version: '3.2'
74
68
  required_rubygems_version: !ruby/object:Gem::Requirement
75
69
  requirements:
76
70
  - - ">="
@@ -79,5 +73,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
73
  requirements: []
80
74
  rubygems_version: 4.0.9
81
75
  specification_version: 4
82
- summary: Sequel driver for the River Ruby gem.
76
+ summary: Sequel PostgreSQL and SQLite driver for the River Ruby gem.
83
77
  test_files: []