Skip to content

Commit a8c767a

Browse files
authored
Implement FNV hashing in the project (#14)
Implement FNV (Fowler–Noll–Vo) hashing in the project instead of pulling in a separate gem. This normally wouldn't really be worth it, but the algorithm is extremely simple to implement, and it is somewhat nice to keep the gem so that it has no dependencies.
1 parent f1bd9c2 commit a8c767a

File tree

9 files changed

+710
-10
lines changed

9 files changed

+710
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Implement the FNV (Fowler–Noll–Vo) hashing algorithm in the project and drop dependency on the `fnv-hash` gem. [PR #14](https://github.com/riverqueue/riverqueue-ruby/pull/14).
13+
1014
## [0.3.0] - 2024-04-27
1115

1216
### Added

Gemfile.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ PATH
22
remote: .
33
specs:
44
riverqueue (0.3.0)
5-
fnv-hash
65

76
GEM
87
remote: https://rubygems.org/
@@ -32,7 +31,6 @@ GEM
3231
drb (2.2.1)
3332
ffi (1.16.3)
3433
fileutils (1.7.2)
35-
fnv-hash (0.2.0)
3634
i18n (1.14.4)
3735
concurrent-ruby (~> 1.0)
3836
io-console (0.7.2)

lib/client.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,11 @@ def insert_many(args)
191191

192192
@driver.transaction do
193193
lock_key = if @advisory_lock_prefix.nil?
194-
Fnv::Hash.fnv_1(lock_str, size: 64)
194+
FNV.fnv1_hash(lock_str, size: 64)
195195
else
196196
# Steep should be able to tell that this is not nil, but it can't.
197197
prefix = @advisory_lock_prefix #: Integer # rubocop:disable Layout/LeadingCommentSpace
198-
prefix << 32 | Fnv::Hash.fnv_1(lock_str, size: 32)
198+
prefix << 32 | FNV.fnv1_hash(lock_str, size: 32)
199199
end
200200

201201
# Packs a uint64 then unpacks to int64, which we need to do to keep the

lib/fnv.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 = MASK.fetch(size)
12+
prime = PRIME.fetch(size)
13+
14+
str.each_byte do |byte|
15+
hash *= prime
16+
hash &= mask # take lower N bits of multiplication product
17+
hash ^= byte
18+
end
19+
20+
hash
21+
end
22+
23+
MASK = {
24+
32 => 0xffffffff, # mask 32 bits long
25+
64 => 0xffffffffffffffff # mask 64 bits long
26+
}.freeze
27+
private_constant :MASK
28+
29+
OFFSET_BASIS = {
30+
32 => 0x811c9dc5,
31+
64 => 0xcbf29ce484222325
32+
}.freeze
33+
private_constant :OFFSET_BASIS
34+
35+
PRIME = {
36+
32 => 0x01000193,
37+
64 => 0x00000100000001B3
38+
}.freeze
39+
private_constant :PRIME
40+
end
41+
end

lib/riverqueue.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require "json"
22

3+
require_relative "fnv"
34
require_relative "insert_opts"
45
require_relative "job"
56

riverqueue.gemspec

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,4 @@ Gem::Specification.new do |s|
1414
"rubygems_mfa_required" => "true",
1515
"source_code_uri" => "https://github.com/riverqueue/riverqueue-ruby"
1616
}
17-
18-
s.add_dependency "fnv-hash"
1917
end

sig/fnv.rbs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module River
2+
module FNV
3+
def self.fnv1_hash: (String, size: 32 | 64) -> Integer
4+
5+
MASK: Hash[Integer, Integer]
6+
OFFSET_BASIS: Hash[Integer, Integer]
7+
PRIME: Hash[Integer, Integer]
8+
end
9+
end

spec/client_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def check_bigint_bounds(int)
222222
lock_str = "unique_keykind=#{job_args.kind}" \
223223
"&queue=#{River::QUEUE_DEFAULT}" \
224224
"&state=#{River::Client.const_get(:DEFAULT_UNIQUE_STATES).join(",")}"
225-
expect(mock_driver.advisory_lock_calls).to eq([check_bigint_bounds(client.send(:uint64_to_int64, Fnv::Hash.fnv_1(lock_str, size: 64)))])
225+
expect(mock_driver.advisory_lock_calls).to eq([check_bigint_bounds(client.send(:uint64_to_int64, River::FNV.fnv1_hash(lock_str, size: 64)))])
226226
end
227227

228228
it "inserts a new unique job with all options" do
@@ -245,7 +245,7 @@ def check_bigint_bounds(int)
245245
"&period=#{client.send(:truncate_time, now, 15 * 60).utc.strftime("%FT%TZ")}" \
246246
"&queue=#{River::QUEUE_DEFAULT}" \
247247
"&state=#{[River::JOB_STATE_AVAILABLE].join(",")}"
248-
expect(mock_driver.advisory_lock_calls).to eq([check_bigint_bounds(client.send(:uint64_to_int64, Fnv::Hash.fnv_1(lock_str, size: 64)))])
248+
expect(mock_driver.advisory_lock_calls).to eq([check_bigint_bounds(client.send(:uint64_to_int64, River::FNV.fnv1_hash(lock_str, size: 64)))])
249249
end
250250

251251
it "inserts a new unique job with advisory lock prefix" do
@@ -265,7 +265,7 @@ def check_bigint_bounds(int)
265265
lock_str = "unique_keykind=#{job_args.kind}" \
266266
"&queue=#{River::QUEUE_DEFAULT}" \
267267
"&state=#{River::Client.const_get(:DEFAULT_UNIQUE_STATES).join(",")}"
268-
expect(mock_driver.advisory_lock_calls).to eq([check_bigint_bounds(client.send(:uint64_to_int64, 123456 << 32 | Fnv::Hash.fnv_1(lock_str, size: 32)))])
268+
expect(mock_driver.advisory_lock_calls).to eq([check_bigint_bounds(client.send(:uint64_to_int64, 123456 << 32 | River::FNV.fnv1_hash(lock_str, size: 32)))])
269269

270270
lock_key = mock_driver.advisory_lock_calls[0]
271271
expect(lock_key >> 32).to eq(123456)
@@ -300,7 +300,7 @@ def job_args_to_row(job_args, insert_opts: River::InsertOpts.new)
300300
"&period=#{client.send(:truncate_time, now, 15 * 60).utc.strftime("%FT%TZ")}" \
301301
"&queue=#{River::QUEUE_DEFAULT}" \
302302
"&state=#{[River::JOB_STATE_AVAILABLE].join(",")}"
303-
expect(mock_driver.advisory_lock_calls).to eq([check_bigint_bounds(client.send(:uint64_to_int64, Fnv::Hash.fnv_1(lock_str, size: 64)))])
303+
expect(mock_driver.advisory_lock_calls).to eq([check_bigint_bounds(client.send(:uint64_to_int64, River::FNV.fnv1_hash(lock_str, size: 64)))])
304304
end
305305

306306
it "skips unique check if unique opts empty" do

0 commit comments

Comments
 (0)