riverqueue-sequel 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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/driver.rb +184 -172
  3. data/lib/riverqueue-sequel.rb +3 -0
  4. metadata +19 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7ade67ed4f568f7dbc60f31ea72eb6e5b6c71b1274869e67bd9d3dbf120a562
4
- data.tar.gz: af7100c24950d9a32458928690b0e4a61a1375d1dfdbd1571bddb43efa717619
3
+ metadata.gz: 8672d5a5d5c01059a17a307704e5512496aecc5e62f57afe2dfcd5ddd8569a34
4
+ data.tar.gz: abca68b736d841506449226a45e7b3d049884b40accdfb2c0e5fecf8cbbf896a
5
5
  SHA512:
6
- metadata.gz: ae68c96561a0f54df6e181c4e4028f3f0ed1b5a929d2b12428ea4b8f3c53f8bc46f33ae2d77fd5b86e3c21ba8ba193bf21d6bd69433bb2c735152225f0810139
7
- data.tar.gz: 1027bd3dcddb61912915dd78214445be02e9ed2400123446a87c4aa364b4f4868811d60d227be878ee1900336692edea75e47899eb5174453998d97d8abd7369
6
+ metadata.gz: 8af058f89ffef579beb2cc91f428a4e004a7d844c61fd2ae588c8214e6fd54e7dc3b22acf7a01feca0dd2474945c5a6ec408378b42918e4f7bd29359c861c8fe
7
+ data.tar.gz: 1d16f622b3df74b166ffccabee0c2374d810b43d026d344529df4f7d9eb2fd66a7f0bc2448a4e12619d00632c99e119a312f487635074dc14f35c47a44fbffd3
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 a Sequel driver for River that supports both PostgreSQL and SQLite.
6
+ # Provides a Sequel driver for River supporting PostgreSQL, YugabyteDB, and SQLite.
5
7
  #
6
8
  # Used in conjunction with a River client like:
7
9
  #
@@ -14,7 +16,76 @@ module River::Driver
14
16
  # client = River::Client.new(River::Driver::Sequel.new(DB))
15
17
  #
16
18
  class Sequel
17
- SQLITE_CONFLICT_WHERE = <<~SQL.chomp
19
+ include River::Driver::Runtime
20
+
21
+ # Creates a driver backed by a connected Sequel::Database for PostgreSQL or
22
+ # SQLite.
23
+ def initialize(db)
24
+ @db = db
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
32
+ end
33
+
34
+ def job_get_by_id(id)
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
42
+ end
43
+
44
+ def job_insert(insert_params)
45
+ job_insert_many([insert_params]).first
46
+ end
47
+
48
+ def job_insert_many(insert_params_array)
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
58
+ end
59
+
60
+ def job_list(params = :all)
61
+ return super unless params == :all
62
+
63
+ runtime_job_rows("ORDER BY id")
64
+ end
65
+
66
+ def rollback_exception
67
+ ::Sequel::Rollback
68
+ end
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.
84
+ def transaction(&)
85
+ @db.transaction(savepoint: true, &)
86
+ end
87
+
88
+ SQLITE_CONFLICT_WHERE = <<~SQL.chomp.freeze
18
89
  unique_key IS NOT NULL
19
90
  AND unique_states IS NOT NULL
20
91
  AND CASE state
@@ -29,105 +100,147 @@ module River::Driver
29
100
  ELSE 0
30
101
  END >= 1
31
102
  SQL
32
- private_constant :SQLITE_CONFLICT_WHERE
33
103
 
34
104
  # SQLite 3.45+ may store JSON as binary JSONB. Always project JSON columns
35
105
  # through json() so this driver can read both the current JSONB format and
36
106
  # the text JSON used by River migrations through version 006. Cast times to
37
107
  # text so Sequel doesn't interpret timezone-less SQLite timestamps in the
38
- # process timezone.
39
- SQLITE_JOB_COLUMNS = <<~SQL.chomp
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
40
111
  id,
41
- json(args) AS args,
112
+ CASE WHEN json_valid(args, 10) THEN json(args) ELSE args END AS args,
42
113
  attempt,
43
114
  CAST(attempted_at AS text) AS attempted_at,
44
- json(attempted_by) AS attempted_by,
115
+ CASE WHEN json_valid(attempted_by, 10) THEN json(attempted_by) ELSE attempted_by END AS attempted_by,
45
116
  CAST(created_at AS text) AS created_at,
46
- json(errors) AS errors,
117
+ CASE WHEN json_valid(errors, 10) THEN json(errors) ELSE errors END AS errors,
47
118
  CAST(finalized_at AS text) AS finalized_at,
48
119
  kind,
49
120
  max_attempts,
50
- json(metadata) AS metadata,
121
+ CASE WHEN json_valid(metadata, 10) THEN json(metadata) ELSE metadata END AS metadata,
51
122
  priority,
52
123
  queue,
53
124
  state,
54
125
  CAST(scheduled_at AS text) AS scheduled_at,
55
- json(tags) AS tags,
126
+ CASE WHEN json_valid(tags, 10) THEN json(tags) ELSE tags END AS tags,
56
127
  unique_key,
57
128
  unique_states
58
129
  SQL
59
- private_constant :SQLITE_JOB_COLUMNS
60
130
 
61
- SQLITE_UNIQUE_NONCE_KEY = "river:unique_nonce"
62
- private_constant :SQLITE_UNIQUE_NONCE_KEY
131
+ UNIQUE_INSERT_METADATA_KEY = "river:unique_nonce"
63
132
 
64
- def initialize(db)
65
- @db = db
66
- @is_sqlite = (db.database_type == :sqlite)
133
+ private_constant :SQLITE_CONFLICT_WHERE, :SQLITE_JOB_COLUMNS, :UNIQUE_INSERT_METADATA_KEY
67
134
 
68
- unless @is_sqlite
69
- db.extension(:pg_array)
70
- db.extension(:pg_json)
71
- end
135
+ private def format_time(time)
136
+ time.getutc.round(3).strftime("%Y-%m-%d %H:%M:%S.%3N")
72
137
  end
73
138
 
74
- def job_get_by_id(id)
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
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
142
+ {
143
+ args: insert_params.encoded_args,
144
+ kind: insert_params.kind,
145
+ max_attempts: insert_params.max_attempts,
146
+ metadata: ::Sequel.pg_jsonb(metadata),
147
+ priority: insert_params.priority,
148
+ queue: insert_params.queue,
149
+ scheduled_at: insert_params.scheduled_at,
150
+ state: insert_params.state,
151
+ tags: ::Sequel.pg_array(insert_params.tags || [], :text),
152
+ unique_key: insert_params.unique_key ? ::Sequel.blob(insert_params.unique_key) : nil,
153
+ unique_states: insert_params.unique_states
154
+ }
82
155
  end
83
156
 
84
- def job_insert(insert_params)
85
- job_insert_many([insert_params]).first
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
86
174
  end
87
175
 
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)
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
90
182
  end
91
183
 
92
- def job_list
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)
93
193
  if @is_sqlite
94
- sqlite_job_rows("ORDER BY id").map { |row| sqlite_to_job_row_from_raw(row) }
194
+ sqlite_job_rows(suffix).map { |row| sqlite_to_job_row_from_raw(row) }
95
195
  else
96
- @db[:river_job].order_by(:id).all.map { |job| to_job_row(job) }
196
+ @db.fetch("SELECT * FROM river_job #{suffix}").map { |row| to_job_row(row) }
97
197
  end
98
198
  end
99
199
 
100
- def rollback_exception
101
- ::Sequel::Rollback
200
+ private def runtime_postgres?
201
+ !@is_sqlite
102
202
  end
103
203
 
104
- def transaction(&)
105
- @db.transaction(savepoint: true, &)
204
+ private def runtime_query_rows(sql)
205
+ @db.fetch(sql).all
106
206
  end
107
207
 
108
- private def postgres_job_insert_many(insert_params_array)
109
- @db[:river_job]
110
- .insert_conflict(
111
- target: [:unique_key],
112
- conflict_where: ::Sequel.lit(
113
- "unique_key IS NOT NULL AND unique_states IS NOT NULL AND river_job_state_in_bitmask(unique_states, state)"
114
- ),
115
- update: {kind: ::Sequel[:excluded][:kind]}
116
- )
117
- .returning(::Sequel.lit("*, (xmax != 0) AS unique_skipped_as_duplicate"))
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]] }
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]) }
120
234
  end
121
235
 
122
236
  # River's current SQLite driver uses json_each to make a batch a single,
123
237
  # atomic statement. The JSON columns are converted to SQLite JSONB here,
124
238
  # matching migration 007 and newer River databases.
125
239
  private def sqlite_job_insert_many(insert_params_array)
126
- return [] if insert_params_array.empty?
127
-
240
+ nonces = sqlite_insert_nonces(insert_params_array)
128
241
  @db.transaction(savepoint: true) do
129
- nonce = SecureRandom.hex(8)
130
- jobs = insert_params_array.map { |param| sqlite_insert_params_to_hash(param, nonce) }
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] }
131
244
 
132
245
  sql = <<~SQL
133
246
  INSERT INTO river_job (
@@ -167,130 +280,16 @@ module River::Driver
167
280
  RETURNING #{SQLITE_JOB_COLUMNS}
168
281
  SQL
169
282
 
170
- rows = @db.fetch(sql, JSON.dump(jobs)).all
283
+ rows = @db.fetch(sql, JSON.generate(jobs)).all
171
284
  sqlite_notify_insert(insert_params_array)
172
285
 
173
286
  rows.map do |row|
174
287
  metadata = JSON.parse(row[:metadata])
175
- [sqlite_to_job_row_from_raw(row), metadata[SQLITE_UNIQUE_NONCE_KEY] != nonce]
288
+ [sqlite_to_job_row_from_raw(row), !inserted_nonces.key?(metadata[UNIQUE_INSERT_METADATA_KEY])]
176
289
  end
177
290
  end
178
291
  end
179
292
 
180
- private def postgres_insert_params_to_hash(insert_params)
181
- {
182
- args: insert_params.encoded_args,
183
- kind: insert_params.kind,
184
- max_attempts: insert_params.max_attempts,
185
- priority: insert_params.priority,
186
- queue: insert_params.queue,
187
- state: insert_params.state,
188
- scheduled_at: insert_params.scheduled_at,
189
- tags: ::Sequel.pg_array(insert_params.tags || [], :text),
190
- unique_key: insert_params.unique_key ? ::Sequel.blob(insert_params.unique_key) : nil,
191
- unique_states: insert_params.unique_states
192
- }
193
- end
194
-
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
- }
209
- end
210
-
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)
221
- River::JobRow.new(
222
- id: river_job[:id],
223
- args: river_job[:args].to_h,
224
- attempt: river_job[:attempt],
225
- attempted_at: river_job[:attempted_at]&.getutc,
226
- attempted_by: river_job[:attempted_by],
227
- created_at: river_job[:created_at].getutc,
228
- errors: river_job[:errors]&.map { |deserialized_error|
229
- River::AttemptError.new(
230
- at: Time.parse(deserialized_error["at"]),
231
- attempt: deserialized_error["attempt"],
232
- error: deserialized_error["error"],
233
- trace: deserialized_error["trace"]
234
- )
235
- },
236
- finalized_at: river_job[:finalized_at]&.getutc,
237
- kind: river_job[:kind],
238
- max_attempts: river_job[:max_attempts],
239
- metadata: river_job[:metadata],
240
- priority: river_job[:priority],
241
- queue: river_job[:queue],
242
- scheduled_at: river_job[:scheduled_at].getutc,
243
- state: river_job[:state],
244
- tags: river_job[:tags].to_a,
245
- unique_key: river_job[:unique_key]&.to_s,
246
- unique_states: ::River::UniqueBitmask.to_states(river_job[:unique_states]&.to_i(2))
247
- )
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
293
  private def sqlite_job_rows(suffix, *binds)
295
294
  @db.fetch("SELECT #{SQLITE_JOB_COLUMNS} FROM river_job #{suffix}", *binds).all
296
295
  end
@@ -303,8 +302,21 @@ module River::Driver
303
302
  return if queues.empty?
304
303
 
305
304
  @db[:river_notification].multi_insert(queues.map do |queue|
306
- {payload: JSON.dump({queue: queue}), topic: "insert"}
305
+ {payload: JSON.generate({queue: queue}), topic: "river_insert"}
307
306
  end)
308
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
320
+ end
309
321
  end
310
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.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Gentry
@@ -30,8 +30,22 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '1000'
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.
33
+ - !ruby/object:Gem::Dependency
34
+ name: riverqueue
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 0.12.0
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 0.12.0
47
+ description: Sequel PostgreSQL and SQLite driver for inserting and working River jobs
48
+ in Ruby.
35
49
  email: brandur@brandur.org
36
50
  executables: []
37
51
  extensions: []
@@ -41,7 +55,7 @@ files:
41
55
  - lib/riverqueue-sequel.rb
42
56
  homepage: https://riverqueue.com
43
57
  licenses:
44
- - LGPL-3.0-or-later
58
+ - MPL-2.0
45
59
  metadata: {}
46
60
  rdoc_options: []
47
61
  require_paths:
@@ -50,7 +64,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
50
64
  requirements:
51
65
  - - ">="
52
66
  - !ruby/object:Gem::Version
53
- version: '0'
67
+ version: '3.2'
54
68
  required_rubygems_version: !ruby/object:Gem::Requirement
55
69
  requirements:
56
70
  - - ">="