riverqueue 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8aa1b2a14e085df2b2a7e79fa6c6cd5b19aeb2c893a4fd369b99e469dd5916b9
4
- data.tar.gz: 3cebd975dcadb223ecc163918b2e8bb0ce67bb671be99c6f706d3d9ec2da6507
3
+ metadata.gz: 46f45a18aa1b8884c93295ba1b023751c826d8feeac212949e18f388f3a6b6cc
4
+ data.tar.gz: 679dde3151af410e5e8f8949d80750d68f247e300af1e6f93b43270d81dc5d40
5
5
  SHA512:
6
- metadata.gz: 868459a39a19fe9aacec7552b03148d97228ff58bdfb6b79a6e724c800cc97ef10017097a46f5132684d5d4e97ea9c788b83c813dcb6dc812835e9c0c9ec9645
7
- data.tar.gz: '05538541ee6466adc722af9d6ab4cbf4c1a081a59c9eb887089b8abea8474415d61dfa9fdf827e4863ee393874258f09e6c54b06ffac87ddc90931f346f12f0b'
6
+ metadata.gz: a7507d5e78f99ef0bb6cda3a91479da386bb5e66c18c40a26c98ed5aec42bb3b5425b3753e797759e4ca4117f9a01636d9784fd1b27d160af215e5e5216ac998
7
+ data.tar.gz: a65b3a772b4d85b823256fe2bebb656a816cff1ba87777c77d9393670dbdfe3be383095bb49f3446d8110a143cb1ba68b9c7530d64da44a9e9093265be9aa8ba
data/lib/client.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "digest"
2
- require "fnv"
3
2
  require "time"
4
3
 
5
4
  module River
@@ -25,9 +24,8 @@ module River
25
24
  # River drivers are found in separate gems like `riverqueue-sequel` to help
26
25
  # minimize transient dependencies.
27
26
  class Client
28
- def initialize(driver, advisory_lock_prefix: nil)
27
+ def initialize(driver)
29
28
  @driver = driver
30
- @advisory_lock_prefix = check_advisory_lock_prefix_bounds(advisory_lock_prefix)
31
29
  @time_now_utc = -> { Time.now.utc } # for test time stubbing
32
30
  end
33
31
 
@@ -73,11 +71,8 @@ module River
73
71
  #
74
72
  # Returns an instance of InsertResult.
75
73
  def insert(args, insert_opts: EMPTY_INSERT_OPTS)
76
- insert_params, unique_opts = make_insert_params(args, insert_opts)
77
- check_unique_job(insert_params, unique_opts) do
78
- job = @driver.job_insert(insert_params)
79
- InsertResult.new(job)
80
- end
74
+ insert_params = make_insert_params(args, insert_opts)
75
+ insert_and_check_unique_job(insert_params)
81
76
  end
82
77
 
83
78
  # Inserts many new jobs as part of a single batch operation for improved
@@ -122,30 +117,20 @@ module River
122
117
  #
123
118
  # See also JobArgsHash for an easy way to insert a job from a hash.
124
119
  #
125
- # Unique job insertion isn't supported with bulk insertion because it'd run
126
- # the risk of major lock contention.
127
- #
128
120
  # Returns the number of jobs inserted.
129
121
  def insert_many(args)
130
122
  all_params = args.map do |arg|
131
123
  if arg.is_a?(InsertManyParams)
132
- make_insert_params(arg.args, arg.insert_opts || EMPTY_INSERT_OPTS, is_insert_many: true).first # unique opts ignored on batch insert
124
+ make_insert_params(arg.args, arg.insert_opts || EMPTY_INSERT_OPTS)
133
125
  else # jobArgs
134
- make_insert_params(arg, EMPTY_INSERT_OPTS, is_insert_many: true).first # unique opts ignored on batch insert
126
+ make_insert_params(arg, EMPTY_INSERT_OPTS)
135
127
  end
136
128
  end
137
129
 
138
130
  @driver.job_insert_many(all_params)
139
- end
140
-
141
- private def check_advisory_lock_prefix_bounds(advisory_lock_prefix)
142
- return nil if advisory_lock_prefix.nil?
143
-
144
- # 2**32-1 is 0xffffffff (the largest number that's four bytes)
145
- if advisory_lock_prefix < 0 || advisory_lock_prefix > 2**32 - 1
146
- raise ArgumentError, "advisory lock prefix must fit inside four bytes"
131
+ .map do |job, unique_skipped_as_duplicate|
132
+ InsertResult.new(job, unique_skipped_as_duplicated: unique_skipped_as_duplicate)
147
133
  end
148
- advisory_lock_prefix
149
134
  end
150
135
 
151
136
  # Default states that are used during a unique insert. Can be overridden by
@@ -153,123 +138,101 @@ module River
153
138
  DEFAULT_UNIQUE_STATES = [
154
139
  JOB_STATE_AVAILABLE,
155
140
  JOB_STATE_COMPLETED,
141
+ JOB_STATE_PENDING,
156
142
  JOB_STATE_RETRYABLE,
157
143
  JOB_STATE_RUNNING,
158
144
  JOB_STATE_SCHEDULED
159
145
  ].freeze
160
146
  private_constant :DEFAULT_UNIQUE_STATES
161
147
 
148
+ REQUIRED_UNIQUE_STATES = [
149
+ JOB_STATE_AVAILABLE,
150
+ JOB_STATE_PENDING,
151
+ JOB_STATE_RUNNING,
152
+ JOB_STATE_SCHEDULED
153
+ ].freeze
154
+ private_constant :REQUIRED_UNIQUE_STATES
155
+
162
156
  EMPTY_INSERT_OPTS = InsertOpts.new.freeze
163
157
  private_constant :EMPTY_INSERT_OPTS
164
158
 
165
- private def check_unique_job(insert_params, unique_opts, &block)
166
- return block.call if unique_opts.nil?
167
-
168
- any_unique_opts = false
169
- get_params = Driver::JobGetByKindAndUniquePropertiesParam.new(kind: insert_params.kind)
170
- unique_key = ""
159
+ private def insert_and_check_unique_job(insert_params)
160
+ job, unique_skipped_as_duplicate = @driver.job_insert(insert_params)
161
+ InsertResult.new(job, unique_skipped_as_duplicated: unique_skipped_as_duplicate)
162
+ end
171
163
 
172
- # It's extremely important here that this lock string format and algorithm
173
- # match the one in the main River library _exactly_. Don't change them
174
- # unless they're updated everywhere.
175
- if unique_opts.by_args
176
- any_unique_opts = true
177
- get_params.encoded_args = insert_params.encoded_args
178
- unique_key += "&args=#{insert_params.encoded_args}"
179
- end
164
+ private def make_insert_params(args, insert_opts)
165
+ raise "args should respond to `#kind`" if !args.respond_to?(:kind)
180
166
 
181
- if unique_opts.by_period
182
- lower_period_bound = truncate_time(@time_now_utc.call, unique_opts.by_period).utc
167
+ # ~all objects in Ruby respond to `#to_json`, so check non-nil instead.
168
+ args_json = args.to_json
169
+ raise "args should return non-nil from `#to_json`" if !args_json
183
170
 
184
- any_unique_opts = true
185
- get_params.created_at = [lower_period_bound, lower_period_bound + unique_opts.by_period]
186
- unique_key += "&period=#{lower_period_bound.strftime("%FT%TZ")}"
171
+ args_insert_opts = if args.respond_to?(:insert_opts)
172
+ args_with_insert_opts = args #: _JobArgsWithInsertOpts # rubocop:disable Layout/LeadingCommentSpace
173
+ args_with_insert_opts.insert_opts || EMPTY_INSERT_OPTS
174
+ else
175
+ EMPTY_INSERT_OPTS
187
176
  end
188
177
 
189
- if unique_opts.by_queue
190
- any_unique_opts = true
191
- get_params.queue = insert_params.queue
192
- unique_key += "&queue=#{insert_params.queue}"
193
- end
178
+ scheduled_at = insert_opts.scheduled_at || args_insert_opts.scheduled_at
194
179
 
195
- if unique_opts.by_state
196
- any_unique_opts = true
197
- get_params.state = unique_opts.by_state
198
- unique_key += "&state=#{unique_opts.by_state.join(",")}"
199
- else
200
- get_params.state = DEFAULT_UNIQUE_STATES
201
- unique_key += "&state=#{DEFAULT_UNIQUE_STATES.join(",")}"
180
+ insert_params = Driver::JobInsertParams.new(
181
+ encoded_args: args_json,
182
+ kind: args.kind,
183
+ max_attempts: insert_opts.max_attempts || args_insert_opts.max_attempts || MAX_ATTEMPTS_DEFAULT,
184
+ priority: insert_opts.priority || args_insert_opts.priority || PRIORITY_DEFAULT,
185
+ queue: insert_opts.queue || args_insert_opts.queue || QUEUE_DEFAULT,
186
+ scheduled_at: scheduled_at&.utc || Time.now,
187
+ state: scheduled_at ? JOB_STATE_SCHEDULED : JOB_STATE_AVAILABLE,
188
+ tags: validate_tags(insert_opts.tags || args_insert_opts.tags || [])
189
+ )
190
+
191
+ unique_opts = insert_opts.unique_opts || args_insert_opts.unique_opts
192
+ if unique_opts
193
+ unique_key, unique_states = make_unique_key_and_bitmask(insert_params, unique_opts)
194
+ insert_params.unique_key = unique_key
195
+ insert_params.unique_states = unique_states
202
196
  end
197
+ insert_params
198
+ end
203
199
 
204
- return block.call unless any_unique_opts
200
+ private def make_unique_key_and_bitmask(insert_params, unique_opts)
201
+ unique_key = ""
205
202
 
206
- # fast path
207
- if !unique_opts.by_state || unique_opts.by_state.sort == DEFAULT_UNIQUE_STATES
208
- unique_key_hash = Digest::SHA256.digest(unique_key)
209
- job, unique_skipped_as_duplicate = @driver.job_insert_unique(insert_params, unique_key_hash)
210
- return InsertResult.new(job, unique_skipped_as_duplicated: unique_skipped_as_duplicate)
203
+ # It's extremely important here that this unique key format and algorithm
204
+ # match the one in the main River library _exactly_. Don't change them
205
+ # unless they're updated everywhere.
206
+ unless unique_opts.exclude_kind
207
+ unique_key += "&kind=#{insert_params.kind}"
211
208
  end
212
209
 
213
- @driver.transaction do
214
- lock_str = "unique_key"
215
- lock_str += "kind=#{insert_params.kind}"
216
- lock_str += unique_key
217
-
218
- lock_key = if @advisory_lock_prefix.nil?
219
- FNV.fnv1_hash(lock_str, size: 64)
210
+ if unique_opts.by_args
211
+ parsed_args = JSON.parse(insert_params.encoded_args)
212
+ filtered_args = if unique_opts.by_args.is_a?(Array)
213
+ parsed_args.select { |k, _| unique_opts.by_args.include?(k) }
220
214
  else
221
- # Steep should be able to tell that this is not nil, but it can't.
222
- prefix = @advisory_lock_prefix #: Integer # rubocop:disable Layout/LeadingCommentSpace
223
- prefix << 32 | FNV.fnv1_hash(lock_str, size: 32)
215
+ parsed_args
224
216
  end
225
217
 
226
- # Packs a uint64 then unpacks to int64, which we need to do to keep the
227
- # value within the bounds of Postgres' bigint. Overflow is okay because
228
- # we can use the full bigint space (including negative numbers) for the
229
- # advisory lock.
230
- lock_key = uint64_to_int64(lock_key)
231
-
232
- @driver.advisory_lock(lock_key)
233
-
234
- existing_job = @driver.job_get_by_kind_and_unique_properties(get_params)
235
- return InsertResult.new(existing_job, unique_skipped_as_duplicated: true) if existing_job
236
-
237
- block.call
218
+ encoded_args = JSON.generate(filtered_args.sort.to_h)
219
+ unique_key += "&args=#{encoded_args}"
238
220
  end
239
- end
240
221
 
241
- private def make_insert_params(args, insert_opts, is_insert_many: false)
242
- raise "args should respond to `#kind`" if !args.respond_to?(:kind)
222
+ if unique_opts.by_period
223
+ lower_period_bound = truncate_time(@time_now_utc.call, unique_opts.by_period).utc
243
224
 
244
- # ~all objects in Ruby respond to `#to_json`, so check non-nil instead.
245
- args_json = args.to_json
246
- raise "args should return non-nil from `#to_json`" if !args_json
225
+ unique_key += "&period=#{lower_period_bound.strftime("%FT%TZ")}"
226
+ end
247
227
 
248
- args_insert_opts = if args.respond_to?(:insert_opts)
249
- args_with_insert_opts = args #: _JobArgsWithInsertOpts # rubocop:disable Layout/LeadingCommentSpace
250
- args_with_insert_opts.insert_opts || EMPTY_INSERT_OPTS
251
- else
252
- EMPTY_INSERT_OPTS
228
+ if unique_opts.by_queue
229
+ unique_key += "&queue=#{insert_params.queue}"
253
230
  end
254
231
 
255
- scheduled_at = insert_opts.scheduled_at || args_insert_opts.scheduled_at
256
- unique_opts = insert_opts.unique_opts || args_insert_opts.unique_opts
232
+ unique_key_hash = Digest::SHA256.digest(unique_key)
233
+ unique_states = validate_unique_states(unique_opts.by_state || DEFAULT_UNIQUE_STATES)
257
234
 
258
- raise ArgumentError, "unique opts can't be used with `#insert_many`" if is_insert_many && unique_opts
259
-
260
- [
261
- Driver::JobInsertParams.new(
262
- encoded_args: args_json,
263
- kind: args.kind,
264
- max_attempts: insert_opts.max_attempts || args_insert_opts.max_attempts || MAX_ATTEMPTS_DEFAULT,
265
- priority: insert_opts.priority || args_insert_opts.priority || PRIORITY_DEFAULT,
266
- queue: insert_opts.queue || args_insert_opts.queue || QUEUE_DEFAULT,
267
- scheduled_at: scheduled_at&.utc, # database defaults to now
268
- state: scheduled_at ? JOB_STATE_SCHEDULED : JOB_STATE_AVAILABLE,
269
- tags: validate_tags(insert_opts.tags || args_insert_opts.tags)
270
- ),
271
- unique_opts
272
- ]
235
+ [unique_key_hash, UniqueBitmask.from_states(unique_states)]
273
236
  end
274
237
 
275
238
  # Truncates the given time down to the interval. For example:
@@ -290,11 +253,18 @@ module River
290
253
  private_constant :TAG_RE
291
254
 
292
255
  private def validate_tags(tags)
293
- tags&.each do |tag|
256
+ tags.each do |tag|
294
257
  raise ArgumentError, "tags should be 255 characters or less" if tag.length > 255
295
258
  raise ArgumentError, "tag should match regex #{TAG_RE.inspect}" unless TAG_RE.match(tag)
296
259
  end
297
260
  end
261
+
262
+ private def validate_unique_states(states)
263
+ REQUIRED_UNIQUE_STATES.each do |required_state|
264
+ raise ArgumentError, "by_state should include required state #{required_state}" unless states.include?(required_state)
265
+ end
266
+ states
267
+ end
298
268
  end
299
269
 
300
270
  # A single job to insert that's part of an #insert_many batch insert. Unlike
@@ -322,7 +292,7 @@ module River
322
292
  # job matching unique property already being present.
323
293
  attr_reader :unique_skipped_as_duplicated
324
294
 
325
- def initialize(job, unique_skipped_as_duplicated: false)
295
+ def initialize(job, unique_skipped_as_duplicated:)
326
296
  @job = job
327
297
  @unique_skipped_as_duplicated = unique_skipped_as_duplicated
328
298
  end
data/lib/driver.rb CHANGED
@@ -4,29 +4,6 @@ module River
4
4
  # considered to be for internal use only and subject to change. API stability
5
5
  # is not guaranteed.
6
6
  module Driver
7
- # Parameters for looking up a job by kind and unique properties.
8
- class JobGetByKindAndUniquePropertiesParam
9
- attr_accessor :created_at
10
- attr_accessor :encoded_args
11
- attr_accessor :kind
12
- attr_accessor :queue
13
- attr_accessor :state
14
-
15
- def initialize(
16
- kind:,
17
- created_at: nil,
18
- encoded_args: nil,
19
- queue: nil,
20
- state: nil
21
- )
22
- self.kind = kind
23
- self.created_at = created_at
24
- self.encoded_args = encoded_args
25
- self.queue = queue
26
- self.state = state
27
- end
28
- end
29
-
30
7
  # Insert parameters for a job. This is sent to underlying drivers and is meant
31
8
  # for internal use only. Its interface is subject to change.
32
9
  class JobInsertParams
@@ -38,6 +15,8 @@ module River
38
15
  attr_accessor :scheduled_at
39
16
  attr_accessor :state
40
17
  attr_accessor :tags
18
+ attr_accessor :unique_key
19
+ attr_accessor :unique_states
41
20
 
42
21
  def initialize(
43
22
  encoded_args:,
@@ -47,7 +26,9 @@ module River
47
26
  queue:,
48
27
  scheduled_at:,
49
28
  state:,
50
- tags:
29
+ tags:,
30
+ unique_key: nil,
31
+ unique_states: nil
51
32
  )
52
33
  self.encoded_args = encoded_args
53
34
  self.kind = kind
@@ -57,6 +38,8 @@ module River
57
38
  self.scheduled_at = scheduled_at
58
39
  self.state = state
59
40
  self.tags = tags
41
+ self.unique_key = unique_key
42
+ self.unique_states = unique_states
60
43
  end
61
44
  end
62
45
  end
data/lib/insert_opts.rb CHANGED
@@ -114,23 +114,33 @@ module River
114
114
  # Unlike other unique options, ByState gets a default when it's not set for
115
115
  # user convenience. The default is equivalent to:
116
116
  #
117
- # by_state: [River::JOB_STATE_AVAILABLE, River::JOB_STATE_COMPLETED, River::JOB_STATE_RUNNING, River::JOB_STATE_RETRYABLE, River::JOB_STATE_SCHEDULED]
117
+ # by_state: [River::JOB_STATE_AVAILABLE, River::JOB_STATE_COMPLETED, River::JOB_STATE_PENDING, River::JOB_STATE_RUNNING, River::JOB_STATE_RETRYABLE, River::JOB_STATE_SCHEDULED]
118
118
  #
119
119
  # With this setting, any jobs of the same kind that have been completed or
120
120
  # discarded, but not yet cleaned out by the system, won't count towards the
121
121
  # uniqueness of a new insert.
122
+ #
123
+ # The pending, scheduled, available, and running states are required when
124
+ # customizing this list.
122
125
  attr_accessor :by_state
123
126
 
127
+ # Indicates that the job kind should not be considered for uniqueness. This
128
+ # is useful when you want to enforce uniqueness based on other properties
129
+ # across multiple worker types.
130
+ attr_accessor :exclude_kind
131
+
124
132
  def initialize(
125
133
  by_args: nil,
126
134
  by_period: nil,
127
135
  by_queue: nil,
128
- by_state: nil
136
+ by_state: nil,
137
+ exclude_kind: nil
129
138
  )
130
139
  self.by_args = by_args
131
140
  self.by_period = by_period
132
141
  self.by_queue = by_queue
133
142
  self.by_state = by_state
143
+ self.exclude_kind = exclude_kind
134
144
  end
135
145
  end
136
146
  end
data/lib/job.rb CHANGED
@@ -3,6 +3,7 @@ module River
3
3
  JOB_STATE_CANCELLED = "cancelled"
4
4
  JOB_STATE_COMPLETED = "completed"
5
5
  JOB_STATE_DISCARDED = "discarded"
6
+ JOB_STATE_PENDING = "pending"
6
7
  JOB_STATE_RETRYABLE = "retryable"
7
8
  JOB_STATE_RUNNING = "running"
8
9
  JOB_STATE_SCHEDULED = "scheduled"
@@ -113,6 +114,9 @@ module River
113
114
  # configuration.
114
115
  attr_accessor :unique_key
115
116
 
117
+ # A list of states that the job must be in to be considered for uniqueness.
118
+ attr_accessor :unique_states
119
+
116
120
  def initialize(
117
121
  id:,
118
122
  args:,
@@ -132,7 +136,8 @@ module River
132
136
  errors: nil,
133
137
  finalized_at: nil,
134
138
  tags: nil,
135
- unique_key: nil
139
+ unique_key: nil,
140
+ unique_states: nil
136
141
  )
137
142
  self.id = id
138
143
  self.args = args
@@ -151,6 +156,7 @@ module River
151
156
  self.state = state
152
157
  self.tags = tags
153
158
  self.unique_key = unique_key
159
+ self.unique_states = unique_states
154
160
  end
155
161
  end
156
162
 
data/lib/riverqueue.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  require "json"
2
2
 
3
- require_relative "fnv"
4
3
  require_relative "insert_opts"
5
4
  require_relative "job"
6
5
 
7
6
  require_relative "client"
8
7
  require_relative "driver"
8
+ require_relative "unique_bitmask"
9
9
 
10
10
  module River
11
11
  end
@@ -0,0 +1,41 @@
1
+ module River
2
+ class UniqueBitmask
3
+ JOB_STATE_BIT_POSITIONS = {
4
+ ::River::JOB_STATE_AVAILABLE => 7,
5
+ ::River::JOB_STATE_CANCELLED => 6,
6
+ ::River::JOB_STATE_COMPLETED => 5,
7
+ ::River::JOB_STATE_DISCARDED => 4,
8
+ ::River::JOB_STATE_PENDING => 3,
9
+ ::River::JOB_STATE_RETRYABLE => 2,
10
+ ::River::JOB_STATE_RUNNING => 1,
11
+ ::River::JOB_STATE_SCHEDULED => 0
12
+ }.freeze
13
+ private_constant :JOB_STATE_BIT_POSITIONS
14
+
15
+ def self.from_states(states)
16
+ val = 0
17
+
18
+ states.each do |state|
19
+ bit_index = JOB_STATE_BIT_POSITIONS[state]
20
+
21
+ bit_position = 7 - (bit_index % 8)
22
+ val |= 1 << bit_position
23
+ end
24
+
25
+ format("%08b", val)
26
+ end
27
+
28
+ def self.to_states(mask)
29
+ states = [] #: Array[jobStateAll] # rubocop:disable Layout/LeadingCommentSpace
30
+
31
+ JOB_STATE_BIT_POSITIONS.each do |state, bit_index|
32
+ bit_position = 7 - (bit_index % 8)
33
+ if (mask & (1 << bit_position)) != 0
34
+ states << state
35
+ end
36
+ end
37
+
38
+ states.sort
39
+ end
40
+ end
41
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riverqueue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Gentry
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-08-31 00:00:00.000000000 Z
12
+ date: 2024-12-20 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: River is a fast job queue for Go. Use this gem in conjunction with gems
15
15
  riverqueue-activerecord or riverqueue-sequel to insert jobs in Ruby which will be
@@ -21,10 +21,10 @@ extra_rdoc_files: []
21
21
  files:
22
22
  - lib/client.rb
23
23
  - lib/driver.rb
24
- - lib/fnv.rb
25
24
  - lib/insert_opts.rb
26
25
  - lib/job.rb
27
26
  - lib/riverqueue.rb
27
+ - lib/unique_bitmask.rb
28
28
  homepage: https://riverqueue.com
29
29
  licenses:
30
30
  - LGPL-3.0-or-later
@@ -48,7 +48,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
48
48
  - !ruby/object:Gem::Version
49
49
  version: '0'
50
50
  requirements: []
51
- rubygems_version: 3.4.20
51
+ rubygems_version: 3.5.16
52
52
  signing_key:
53
53
  specification_version: 4
54
54
  summary: River is a fast job queue for Go.
data/lib/fnv.rb DELETED
@@ -1,35 +0,0 @@
1
- module River
2
- # FNV is the Fowler–Noll–Vo hash function, a simple hash that's very easy to
3
- # implement, and hash the perfect characteristics for use with the 64 bits of
4
- # available space in a PG advisory lock.
5
- #
6
- # I'm implemented it myself so that the River gem can stay dependency free
7
- # (and because it's quite easy to do).
8
- module FNV
9
- def self.fnv1_hash(str, size:)
10
- hash = OFFSET_BASIS.fetch(size)
11
- mask = (2**size - 1).to_int # creates a mask of 1s of `size` bits long like 0xffffffff
12
- prime = PRIME.fetch(size)
13
-
14
- str.each_byte do |byte|
15
- hash *= prime
16
- hash &= mask
17
- hash ^= byte
18
- end
19
-
20
- hash
21
- end
22
-
23
- OFFSET_BASIS = {
24
- 32 => 0x811c9dc5,
25
- 64 => 0xcbf29ce484222325
26
- }.freeze
27
- private_constant :OFFSET_BASIS
28
-
29
- PRIME = {
30
- 32 => 0x01000193,
31
- 64 => 0x00000100000001B3
32
- }.freeze
33
- private_constant :PRIME
34
- end
35
- end