Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UUID v7 support #15

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,20 @@ SecureRandom.alphanumeric(10) #=> "S8baxMJnPl"
SecureRandom.alphanumeric(10) #=> "aOxAg8BAJe"
```

Generate UUIDs:
Generate UUIDs v4 (random):

```ruby
SecureRandom.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
SecureRandom.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
```

Generate UUIDs v7 (unix timestamp + random):

```ruby
SecureRandom.uuid_v7 #=> "01843a55-736e-785c-9f2e-0f74f11d6145"
SecureRandom.uuid_v7 #=> "01843a55-7370-799e-8498-669a5b1fbc19"
```

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
22 changes: 22 additions & 0 deletions lib/random/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,28 @@ def uuid
"%08x-%04x-%04x-%04x-%04x%08x" % ary
end

# Random::Formatter#uuid generates a random v7 UUID (Universally Unique IDentifier).
#
# require 'random/formatter'
#
# prng.uuid_v7 #=> "01843a54-f268-7f51-934c-216e9ca4fe05"
# prng.uuid_v7 #=> "01843a54-f26b-7edf-862b-d986fb0f421b"
# prng.uuid_v7 #=> "01843a54-f26f-7664-b455-85fa8a5e6a4d"
#
# The version 7 UUID is random, but contains a time-based component for ordering.
#
# The result contains 74 random bits (9 random bytes).
#
# See RFC 4122 for details of UUID.
#
def uuid_v7
ts = [Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)].pack('Q>').unpack('nNn').drop(1)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC is fundamentally flawed >> and will not work at scale if monotonic total ordering is required <<. CLOCK_REALTIME skips forwards and backwards on many events, just to name a few: hibernation, NTP adjustments, daylight savings time, and leapseconds. And CLOCK_MONOTOMIC_RAW is not suitable for use between systems. If there will only ever be a single system generating UUIDs, then CLOCK_MONOTONIC_RAW fallback on CLOCK_MONOTONIC is appropriate. If multiple systems expect canonical total monotonic ordering, then deploy PTP and use TAI ( CLOCK_TAI on Linux ). CLOCK_REALTIME with a timezone of UTC can never be monotonic due leapseconds. UTC(t) = TAI(t) - leap_seconds_for_year_and_month(t(m, y)) data here. TAI is the primary reliable, global monotonic time standard and essential to providing lock-free, unique, total ordering across multiple systems. The fallback method to global ordering is to have a single (possible SPoF risk) UUID master issuer. TL;DR In any case, this type of UUID won't be useful for anything important.

Copy link
Author

@khasinski khasinski Jan 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comment! Is monotonic total ordering required though? From my perspective there are a lot of use cases where a certain instability is accepted while an approximate monotonic ordering will help.

Consider for example a batching mechanism for backfills in a typical RoR application:

Model.in_batches do |batch| # Loads records by 1000 keeping the latest id
   batch.update_all(something: :something)
   # batch operation that would normally lock the table, but it's now locking only selected rows
end

In the above-mentioned example having an UUIDv4 as a primary key means that the records don't have a stable order. The occasional inconsistency of UUIDv7 is usually covered by the batch size.

However I'd be open to rewrite this to use TAI (perhaps as an option) if necessary.

ary = random_bytes(10).unpack("nnnN")
ary[0] = (ary[0] & 0x0fff) | 0x7000
ary[1] = (ary[1] & 0x3fff) | 0x8000
"%08x-%04x-%04x-%04x-%04x%08x" % (ts + ary)
end

private def gen_random(n)
self.bytes(n)
end
Expand Down
11 changes: 11 additions & 0 deletions test/ruby/test_random_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ def test_uuid
assert_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/, uuid)
end

def test_uuid_v7
uuid = @it.uuid_v7
assert_equal(36, uuid.size)

# Check time_hi_and_version and clock_seq_hi_res bits (RFC 4122 4.4)
assert_equal('7', uuid[14])
assert_include(%w'8 9 a b', uuid[19])

assert_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/, uuid)
end

def test_alphanumeric
65.times do |n|
an = @it.alphanumeric(n)
Expand Down