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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/driver.rb +228 -19
  3. metadata +4 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af0f2602b8323031224709b01961d93e73865df77caffb8099959f411541677d
4
- data.tar.gz: b1b720a495fc3a5eaf8413fdc8732eda6a1de2bb7e033a0ec6531337a3d53f3f
3
+ metadata.gz: c7ade67ed4f568f7dbc60f31ea72eb6e5b6c71b1274869e67bd9d3dbf120a562
4
+ data.tar.gz: af7100c24950d9a32458928690b0e4a61a1375d1dfdbd1571bddb43efa717619
5
5
  SHA512:
6
- metadata.gz: 7215adbf69d421de6d65fceb09be674b777d399b14d75ac47e847c79cf6b29eab17b7267392e99813af29d33fc3543d234c2c19ef2a57d3898f4d6a50624e9c0
7
- data.tar.gz: 9dbd8f19c8ce23281bf6c78fde501e7dbb5f8969562ef25fc0945bd58ec2879948d7cdd49671e9dee93480f703deb9e417e86fb4cbda089797246c22e23823d0
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.extension(:pg_array)
13
- @db.extension(:pg_json)
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
- data_set = @db[:river_job].where(id: id)
18
- data_set.first ? to_job_row(data_set.first) : nil
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| insert_params_to_hash(p) })
36
- .map { |row| to_insert_result(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
- def job_list
40
- data_set = @db[:river_job].order_by(:id)
41
- data_set.all.map { |job| to_job_row(job) }
42
- end
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
- def rollback_exception
45
- ::Sequel::Rollback
46
- end
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
- def transaction(&)
49
- @db.transaction(savepoint: true, &)
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 insert_params_to_hash(insert_params)
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 to_insert_result(result)
68
- [to_job_row(result), result[:unique_skipped_as_duplicate]]
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.10.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 with the riverqueue
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: []