riverqueue-sequel 0.11.0 → 0.13.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 +197 -173
  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: bb0d975eb6921905735667d53aeba24da81b090f92af870935bd112cbc9a7b5e
4
+ data.tar.gz: 9f4b514ebc0ac579caeb7eacec5227a9e9e2b497cbfbd9d15366a3795e271ee9
5
5
  SHA512:
6
- metadata.gz: ae68c96561a0f54df6e181c4e4028f3f0ed1b5a929d2b12428ea4b8f3c53f8bc46f33ae2d77fd5b86e3c21ba8ba193bf21d6bd69433bb2c735152225f0810139
7
- data.tar.gz: 1027bd3dcddb61912915dd78214445be02e9ed2400123446a87c4aa364b4f4868811d60d227be878ee1900336692edea75e47899eb5174453998d97d8abd7369
6
+ metadata.gz: 9dc23c4d8a76b28a7d39e3422b011db46d648c3e5c7adfd652e6fa391643a003617362f5edc1e17e71722c1aa5f52d0db35fc95315242c28ab9d4f534c557620
7
+ data.tar.gz: 8fdce85797d7deefb7b82223ac81531bc61962b32a43b96bd29452a8f88e8352dedd2fac1da555e5e194991f281f9c3d5d4468bed82a2c6410404218b63adaf1
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,88 @@ 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
+ block_error = nil
86
+ @db.transaction(savepoint: true) do
87
+ yield
88
+ rescue ArgumentError => error
89
+ block_error = error
90
+ raise
91
+ end
92
+ rescue ::Sequel::DatabaseError => error
93
+ # SQLite treats ArgumentError as an adapter error, even when it came
94
+ # from an application callback. Preserve that error after rollback.
95
+ raise block_error if block_error && error.wrapped_exception.equal?(block_error)
96
+
97
+ raise
98
+ end
99
+
100
+ SQLITE_CONFLICT_WHERE = <<~SQL.chomp.freeze
18
101
  unique_key IS NOT NULL
19
102
  AND unique_states IS NOT NULL
20
103
  AND CASE state
@@ -29,105 +112,147 @@ module River::Driver
29
112
  ELSE 0
30
113
  END >= 1
31
114
  SQL
32
- private_constant :SQLITE_CONFLICT_WHERE
33
115
 
34
116
  # SQLite 3.45+ may store JSON as binary JSONB. Always project JSON columns
35
117
  # through json() so this driver can read both the current JSONB format and
36
118
  # the text JSON used by River migrations through version 006. Cast times to
37
119
  # text so Sequel doesn't interpret timezone-less SQLite timestamps in the
38
- # process timezone.
39
- SQLITE_JOB_COLUMNS = <<~SQL.chomp
120
+ # process timezone. Flag 10 validates JSONB contents, not just its header,
121
+ # so damaged blobs reach the row decoder without aborting the SQL query.
122
+ SQLITE_JOB_COLUMNS = <<~SQL.chomp.freeze
40
123
  id,
41
- json(args) AS args,
124
+ CASE WHEN json_valid(args, 10) THEN json(args) ELSE args END AS args,
42
125
  attempt,
43
126
  CAST(attempted_at AS text) AS attempted_at,
44
- json(attempted_by) AS attempted_by,
127
+ CASE WHEN json_valid(attempted_by, 10) THEN json(attempted_by) ELSE attempted_by END AS attempted_by,
45
128
  CAST(created_at AS text) AS created_at,
46
- json(errors) AS errors,
129
+ CASE WHEN json_valid(errors, 10) THEN json(errors) ELSE errors END AS errors,
47
130
  CAST(finalized_at AS text) AS finalized_at,
48
131
  kind,
49
132
  max_attempts,
50
- json(metadata) AS metadata,
133
+ CASE WHEN json_valid(metadata, 10) THEN json(metadata) ELSE metadata END AS metadata,
51
134
  priority,
52
135
  queue,
53
136
  state,
54
137
  CAST(scheduled_at AS text) AS scheduled_at,
55
- json(tags) AS tags,
138
+ CASE WHEN json_valid(tags, 10) THEN json(tags) ELSE tags END AS tags,
56
139
  unique_key,
57
140
  unique_states
58
141
  SQL
59
- private_constant :SQLITE_JOB_COLUMNS
60
142
 
61
- SQLITE_UNIQUE_NONCE_KEY = "river:unique_nonce"
62
- private_constant :SQLITE_UNIQUE_NONCE_KEY
143
+ UNIQUE_INSERT_METADATA_KEY = "river:unique_nonce"
63
144
 
64
- def initialize(db)
65
- @db = db
66
- @is_sqlite = (db.database_type == :sqlite)
145
+ private_constant :SQLITE_CONFLICT_WHERE, :SQLITE_JOB_COLUMNS, :UNIQUE_INSERT_METADATA_KEY
67
146
 
68
- unless @is_sqlite
69
- db.extension(:pg_array)
70
- db.extension(:pg_json)
71
- end
147
+ private def format_time(time)
148
+ time.getutc.round(3).strftime("%Y-%m-%d %H:%M:%S.%3N")
72
149
  end
73
150
 
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
151
+ private def postgres_insert_params_to_hash(insert_params, nonce)
152
+ metadata = insert_params.metadata || {}
153
+ metadata = metadata.merge(UNIQUE_INSERT_METADATA_KEY => nonce) if nonce
154
+ {
155
+ args: insert_params.encoded_args,
156
+ kind: insert_params.kind,
157
+ max_attempts: insert_params.max_attempts,
158
+ metadata: ::Sequel.pg_jsonb(metadata),
159
+ priority: insert_params.priority,
160
+ queue: insert_params.queue,
161
+ scheduled_at: insert_params.scheduled_at,
162
+ state: insert_params.state,
163
+ tags: ::Sequel.pg_array(insert_params.tags || [], :text),
164
+ unique_key: insert_params.unique_key ? ::Sequel.blob(insert_params.unique_key) : nil,
165
+ unique_states: insert_params.unique_states
166
+ }
82
167
  end
83
168
 
84
- def job_insert(insert_params)
85
- job_insert_many([insert_params]).first
169
+ private def postgres_job_insert_many(insert_params_array)
170
+ capabilities = postgres_capabilities
171
+ nonce = SecureRandom.hex(8) if capabilities.unique_insert_mode == :metadata_nonce
172
+ @db[:river_job]
173
+ .insert_conflict(
174
+ conflict_where: ::Sequel.lit(
175
+ "unique_key IS NOT NULL AND unique_states IS NOT NULL AND river_job_state_in_bitmask(unique_states, state)"
176
+ ),
177
+ target: [:unique_key],
178
+ update: {kind: ::Sequel[:river_job][:kind]}
179
+ )
180
+ .returning(::Sequel.lit("*, #{capabilities.unique_insert_sql} AS unique_skipped_as_duplicate"))
181
+ .multi_insert(insert_params_array.map { |p| postgres_insert_params_to_hash(p, nonce) })
182
+ .map do |row|
183
+ job = to_job_row(row)
184
+ [job, nonce ? job.metadata[UNIQUE_INSERT_METADATA_KEY] != nonce : row[:unique_skipped_as_duplicate]]
185
+ end
86
186
  end
87
187
 
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)
188
+ private def postgres_to_job_row(river_job)
189
+ runtime_decode_postgres_job(river_job)
190
+ end
191
+
192
+ private def runtime_connection_pool
193
+ @db
194
+ end
195
+
196
+ private def runtime_execute(sql)
197
+ @db.run(sql)
198
+ end
199
+
200
+ private def runtime_job_list_without_params
201
+ job_list(:all)
90
202
  end
91
203
 
92
- def job_list
204
+ private def runtime_job_rows(suffix)
93
205
  if @is_sqlite
94
- sqlite_job_rows("ORDER BY id").map { |row| sqlite_to_job_row_from_raw(row) }
206
+ sqlite_job_rows(suffix).map { |row| sqlite_to_job_row_from_raw(row) }
95
207
  else
96
- @db[:river_job].order_by(:id).all.map { |job| to_job_row(job) }
208
+ @db.fetch("SELECT * FROM river_job #{suffix}").map { |row| to_job_row(row) }
97
209
  end
98
210
  end
99
211
 
100
- def rollback_exception
101
- ::Sequel::Rollback
212
+ private def runtime_postgres?
213
+ !@is_sqlite
102
214
  end
103
215
 
104
- def transaction(&)
105
- @db.transaction(savepoint: true, &)
216
+ private def runtime_query_rows(sql)
217
+ @db.fetch(sql).all
106
218
  end
107
219
 
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]] }
220
+ private def runtime_quote(value)
221
+ @db.literal(value)
222
+ end
223
+
224
+ private def runtime_unique_violation_class
225
+ ::Sequel::UniqueConstraintViolation
226
+ end
227
+
228
+ private def runtime_value(row, key)
229
+ row[key] || row[key.to_s]
230
+ end
231
+
232
+ private def sqlite_insert_params_to_hash(insert_params, nonce)
233
+ {
234
+ args: JSON.parse(insert_params.encoded_args),
235
+ kind: insert_params.kind,
236
+ max_attempts: insert_params.max_attempts,
237
+ metadata: {UNIQUE_INSERT_METADATA_KEY => nonce},
238
+ priority: insert_params.priority,
239
+ queue: insert_params.queue,
240
+ scheduled_at: insert_params.scheduled_at ? format_time(insert_params.scheduled_at) : nil,
241
+ state: insert_params.state,
242
+ tags: insert_params.tags || [],
243
+ unique_key: insert_params.unique_key&.unpack1("H*"),
244
+ unique_states: insert_params.unique_states&.to_i(2)
245
+ }.tap { |values| values[:metadata] = (insert_params.metadata || {}).merge(values[:metadata]) }
120
246
  end
121
247
 
122
248
  # River's current SQLite driver uses json_each to make a batch a single,
123
249
  # atomic statement. The JSON columns are converted to SQLite JSONB here,
124
250
  # matching migration 007 and newer River databases.
125
251
  private def sqlite_job_insert_many(insert_params_array)
126
- return [] if insert_params_array.empty?
127
-
252
+ nonces = sqlite_insert_nonces(insert_params_array)
128
253
  @db.transaction(savepoint: true) do
129
- nonce = SecureRandom.hex(8)
130
- jobs = insert_params_array.map { |param| sqlite_insert_params_to_hash(param, nonce) }
254
+ jobs = insert_params_array.zip(nonces).map { |param, nonce| sqlite_insert_params_to_hash(param, nonce) }
255
+ inserted_nonces = nonces.to_h { |nonce| [nonce, true] }
131
256
 
132
257
  sql = <<~SQL
133
258
  INSERT INTO river_job (
@@ -163,134 +288,20 @@ module River::Driver
163
288
  FROM json_each(cast(? AS blob))
164
289
  WHERE true
165
290
  ON CONFLICT (unique_key) WHERE #{SQLITE_CONFLICT_WHERE}
166
- DO UPDATE SET kind = EXCLUDED.kind
291
+ DO UPDATE SET kind = river_job.kind
167
292
  RETURNING #{SQLITE_JOB_COLUMNS}
168
293
  SQL
169
294
 
170
- rows = @db.fetch(sql, JSON.dump(jobs)).all
295
+ rows = @db.fetch(sql, JSON.generate(jobs)).all
171
296
  sqlite_notify_insert(insert_params_array)
172
297
 
173
298
  rows.map do |row|
174
299
  metadata = JSON.parse(row[:metadata])
175
- [sqlite_to_job_row_from_raw(row), metadata[SQLITE_UNIQUE_NONCE_KEY] != nonce]
300
+ [sqlite_to_job_row_from_raw(row), !inserted_nonces.key?(metadata[UNIQUE_INSERT_METADATA_KEY])]
176
301
  end
177
302
  end
178
303
  end
179
304
 
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
305
  private def sqlite_job_rows(suffix, *binds)
295
306
  @db.fetch("SELECT #{SQLITE_JOB_COLUMNS} FROM river_job #{suffix}", *binds).all
296
307
  end
@@ -303,8 +314,21 @@ module River::Driver
303
314
  return if queues.empty?
304
315
 
305
316
  @db[:river_notification].multi_insert(queues.map do |queue|
306
- {payload: JSON.dump({queue: queue}), topic: "insert"}
317
+ {payload: JSON.generate({queue: queue}), topic: "river_insert"}
307
318
  end)
308
319
  end
320
+
321
+ private def sqlite_to_job_row_from_raw(row)
322
+ runtime_decode_sqlite_job(row)
323
+ end
324
+
325
+ private def to_job_row(river_job)
326
+ if @is_sqlite
327
+ row = sqlite_job_rows("WHERE id = ? LIMIT 1", river_job[:id]).first
328
+ sqlite_to_job_row_from_raw(row)
329
+ else
330
+ postgres_to_job_row(river_job)
331
+ end
332
+ end
309
333
  end
310
334
  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.13.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.13.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.13.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
  - - ">="