hyperuuid 1.0.0-aarch64-linux-gnu → 1.1.0-aarch64-linux-gnu
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.
- checksums.yaml +4 -4
- data/README.md +71 -40
- data/lib/hyperuuid/3.4/hyperuuid_native.so +0 -0
- data/lib/hyperuuid/4.0/hyperuuid_native.so +0 -0
- data/lib/hyperuuid/errors.rb +9 -3
- data/lib/hyperuuid/runtime.rb +9 -1
- data/lib/hyperuuid/uuid.rb +2 -1
- data/lib/hyperuuid.rb +28 -28
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b9608e59f7992c81542452c00896d1fe2e9b923378578add4657a3ca29f3b3ac
|
|
4
|
+
data.tar.gz: 65c6b2f516f5e324c55039b0631b5893becc8aa15d7f0a02c75aeb164afcfb2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be03eddff6ee0b0f2d2e9cd0526d1dccb1cde03d5d9192c67a14cbf090f6f07ea7bb64fa98dcf9ce51601257354ef6aaf4f6efc8cdb5363030495cf5a5b133b3
|
|
7
|
+
data.tar.gz: 64b5c3aee73b60cd60b3f8056b28541956ceb6817a68811838d16f9256b4de6bfc1db2ffa5daef06894f868571b5ead93a0f348e3e52e1dd0585662150175642
|
data/README.md
CHANGED
|
@@ -23,15 +23,24 @@ require "hyperuuid"
|
|
|
23
23
|
id = HyperUuid.new_v4
|
|
24
24
|
id2 = HyperUuid.new_v5(HyperUuid::Namespaces::DNS, "example.com")
|
|
25
25
|
id3 = HyperUuid.new_v7
|
|
26
|
+
puts "v4: #{id}"
|
|
27
|
+
puts "v5: #{id2}"
|
|
28
|
+
puts "v7: #{id3}"
|
|
29
|
+
|
|
30
|
+
puts "timestamp: #{id3.timestamp}" # recover the embedded UTC Time
|
|
31
|
+
# nil instead of raising if id3 isn't an RFC 9562 v7:
|
|
32
|
+
puts "checked timestamp: #{id3.timestamp(raise_on_mismatch: false)}"
|
|
33
|
+
# the RFC 9562 variant and version 7, the guard before trusting its fields:
|
|
34
|
+
puts "is v7: #{id3.rfc?(7)}"
|
|
26
35
|
|
|
27
|
-
id3.timestamp # recover the embedded UTC Time
|
|
28
|
-
id3.timestamp(raise_on_mismatch: false) # nil instead of raising if id3 isn't an RFC 9562 v7
|
|
29
|
-
id3.rfc?(7) # the RFC 9562 variant and version 7, the guard before trusting its fields
|
|
30
36
|
sql = id3.to_sql_order # a version 8 laid out so SQL Server's uniqueidentifier sorts by creation order
|
|
31
|
-
sql
|
|
37
|
+
puts "sql order: #{sql}"
|
|
38
|
+
puts "sql is v7: #{sql.rfc?(7, layout: :sql_server)}"
|
|
39
|
+
puts "sql timestamp: #{sql.timestamp(layout: :sql_server)}" # read straight from SQL order, no round trip
|
|
32
40
|
|
|
33
41
|
# One native call, one random-bytes fetch, one counter reservation for the whole batch:
|
|
34
42
|
batch = HyperUuid.new_v7_batch(1000)
|
|
43
|
+
puts "batch: #{batch.length}"
|
|
35
44
|
```
|
|
36
45
|
|
|
37
46
|
## The doors
|
|
@@ -107,41 +116,58 @@ one time in 16, and is then converted a second time rather than refused.
|
|
|
107
116
|
Send `#to_sql_order`'s result as its raw `#bytes`, not as UUID text. SQL Server stores a UUID
|
|
108
117
|
string's first three groups byte-reversed, which loses the creation order
|
|
109
118
|
([details](https://github.com/SkunkWerkx/HyperUuid#sql-server-ordering)); tiny_tds 3.4 and an ActiveRecord `:uuid` attribute both send text. A binary
|
|
110
|
-
literal lands byte for byte:
|
|
119
|
+
literal lands byte for byte (`client` is a connected `TinyTds::Client`):
|
|
111
120
|
|
|
112
121
|
```ruby
|
|
113
122
|
sql = HyperUuid.new_v7.to_sql_order
|
|
114
123
|
client.execute("INSERT INTO orders (id) VALUES (0x#{sql.bytes.unpack1('H*')})").do
|
|
115
124
|
|
|
116
125
|
row = client.execute("SELECT CAST(id AS binary(16)) AS id FROM orders").first
|
|
117
|
-
sql = HyperUuid::Uuid.new(row["id"])
|
|
126
|
+
sql = HyperUuid::Uuid.new(row["id"]) # read with layout: :sql_server
|
|
118
127
|
```
|
|
119
128
|
|
|
120
129
|
In ActiveRecord, map the column as `:binary`, which quotes as `0x…`. Reading needs the `CAST`
|
|
121
130
|
too: tiny_tds returns a `uniqueidentifier` as text with those groups reversed.
|
|
122
131
|
|
|
123
|
-
###
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
### Version 7 ordering and the batch limit
|
|
133
|
+
|
|
134
|
+
Every v7 call, single or batch, reserves from one process-wide stream that is re-seeded at
|
|
135
|
+
random whenever a timestamp moves it forward. A single call consumes one of a millisecond's 2²⁰
|
|
136
|
+
steps and a batch of n consumes ⌈n/64⌉. A batch is always in strictly increasing order. For
|
|
137
|
+
timestamps up to 2⁴⁴ − 2 (the year 2527):
|
|
138
|
+
|
|
139
|
+
- **Ordering.** A call whose timestamp is within 10 s of the newest stream timestamp either
|
|
140
|
+
raises `HyperUuid::CounterExhaustedError` (below) or reserves from the stream, and its UUIDs
|
|
141
|
+
then sort strictly after every UUID from a reservation that completed before it began, on any
|
|
142
|
+
thread. Overlapping reservations are ordered by reservation, which need not be completion
|
|
143
|
+
order.
|
|
144
|
+
- **Embedded timestamp.** At most 11 s ahead of the one supplied: a timestamp up to 11 s behind
|
|
145
|
+
the stream is clamped onto the stream's millisecond, and a millisecond whose steps run out
|
|
146
|
+
splits into the next, so a split's tail carries the millisecond after it (at most 1000 ms
|
|
147
|
+
ahead of the supplied timestamp). An out-of-stream call carries its own timestamp exactly.
|
|
148
|
+
- **Backfill and future-dated calls.** A timestamp more than 11 s behind the stream, or past
|
|
149
|
+
2⁴⁴ − 2, is minted at itself, outside the stream: it succeeds, leaves the stream alone and is
|
|
150
|
+
ordered against nothing. One call far in the future therefore advances the stream, and until
|
|
151
|
+
the clock catches up, real-time calls are minted out of stream, unordered among themselves.
|
|
152
|
+
- **Exhaustion.** A split that would take the stream more than 1000 ms ahead of the supplied
|
|
153
|
+
timestamp, or past 2⁴⁴ − 2, raises `HyperUuid::CounterExhaustedError`, before anything is
|
|
154
|
+
reserved. A call whose timestamp is past the stream's millisecond succeeds.
|
|
155
|
+
- **Batch size.** `HyperUuid::MAX_V7_BATCH` is 67,108,864 (2²⁶), the most one `new_v7_batch` or
|
|
156
|
+
`new_v7_batch_bytes` call mints. A larger count is an `ArgumentError` naming the limit,
|
|
157
|
+
raised before anything is allocated.
|
|
138
158
|
|
|
139
159
|
### Errors
|
|
140
160
|
|
|
141
|
-
|
|
161
|
+
Three exceptions describe a failed call, and each carries the same message on every backend:
|
|
142
162
|
|
|
143
|
-
- `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded in a version 7:
|
|
144
|
-
|
|
163
|
+
- `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded in a version 7: after
|
|
164
|
+
9999-12-31T23:59:59.999Z (253,402,300,799,999 ms, the generation cap; the 48-bit field itself
|
|
165
|
+
reaches 10889, and `#timestamp` reads a version 7 another generator dated past the cap as it
|
|
166
|
+
reads a version 6: no timestamp), or negative (a `Time` before the Unix epoch included).
|
|
167
|
+
- `HyperUuid::CounterExhaustedError` — the version 7 stream cannot advance without running
|
|
168
|
+
more than 1000 ms ahead of the supplied time or past its last millisecond (see
|
|
169
|
+
[Version 7 ordering and the batch limit](#version-7-ordering-and-the-batch-limit)). Nothing is
|
|
170
|
+
reserved.
|
|
145
171
|
- `HyperUuid::RandomSourceError` — the operating system's random source failed.
|
|
146
172
|
|
|
147
173
|
Everything else is a caller bug and raises Ruby's own error for it, checked once in the shared
|
|
@@ -188,10 +214,10 @@ The honest trade-off: this gem is native code, not pure Ruby — a precompiled e
|
|
|
188
214
|
|
|
189
215
|
```ruby
|
|
190
216
|
bytes = HyperUuid.new_v7_batch_bytes(1000)
|
|
191
|
-
first = bytes[0, 16]
|
|
217
|
+
first = bytes[0, 16] # ready for a BINARY(16) bind parameter
|
|
192
218
|
```
|
|
193
219
|
|
|
194
|
-
**About 41x faster than `new_v7_batch`** for a 1000-UUID batch (4.9 µs versus 202 µs). The native call is identical — the difference is that `new_v7_batch` then allocates a `Uuid` object and its byte Strings for every item on top of it. This hands back the bytes the native core already produced, untouched.
|
|
220
|
+
**About 41x faster than `new_v7_batch`** for a 1000-UUID batch (4.9 µs versus 202 µs). The native call is identical — the difference is that `new_v7_batch` then allocates a `Uuid` object and its byte Strings for every item on top of it. This hands back the bytes the native core already produced, untouched. (Measured before the per-millisecond re-seed, core change, 2026-10-27.)
|
|
195
221
|
|
|
196
222
|
The catch, and it inverts the advice: **if you need `Uuid` objects, keep using `new_v7_batch`.** Slicing these bytes into objects yourself just relocates the identical allocations into your own code, and measures no better — sometimes worse. Reach for the byte form only when bytes are the destination: a bind parameter, a wire format, a bulk load.
|
|
197
223
|
|
|
@@ -209,24 +235,28 @@ Real numbers, `benchmark-ips` on Ruby 4.0.7, linux-x64 on an Intel Core i9-11900
|
|
|
209
235
|
|---|---:|---:|
|
|
210
236
|
| `SecureRandom.uuid` | 943,917 | baseline |
|
|
211
237
|
| `HyperUuid.new_v4` | 5,236,000 | **5.5x faster** |
|
|
212
|
-
| `HyperUuid.new_v7` (explicit ms) | 3,785,000 | **4.0x faster** |
|
|
213
|
-
| `HyperUuid.new_v7` (current time) | 3,574,000 | **3.8x faster** |
|
|
238
|
+
| `HyperUuid.new_v7` (explicit ms) † | 3,785,000 | **4.0x faster** |
|
|
239
|
+
| `HyperUuid.new_v7` (current time) † | 3,574,000 | **3.8x faster** |
|
|
214
240
|
| `HyperUuid.new_v5` | 1,942,000 | 2.1x faster |
|
|
215
241
|
|
|
242
|
+
† Measured before the per-millisecond re-seed (core change, 2026-10-27).
|
|
243
|
+
|
|
216
244
|
A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by the shared Rust core — costs less than a fifth of what `SecureRandom.uuid` does, because the Magnus extension is an ordinary native method call with nothing marshalled around it.
|
|
217
245
|
|
|
218
|
-
The "current time" rows deserve a footnote, because they will not look like this everywhere. Here they land beside the explicit-ms rows: the only difference between the two is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read, and on this machine that read is too cheap to see. On a machine with a slow clock it is the whole story — where a virtualized clock defeats the vDSO fast path the same read costs ~1µs, which puts both current-time rows at parity with `SecureRandom.uuid` while the explicit-ms rows stay well ahead of it. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does. If your clock is slow and you are minting many, read it once and pass the timestamp, or use the batch doors.
|
|
246
|
+
The "current time" rows deserve a footnote, because they will not look like this everywhere. Here they land beside the explicit-ms rows: the only difference between the two is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read, and on this machine that read is too cheap to see. On a machine with a slow clock it is the whole story — where a virtualized clock defeats the vDSO fast path the same read costs ~1µs, which puts both current-time rows at parity with `SecureRandom.uuid` while the explicit-ms rows stay well ahead of it. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does. If your clock is slow and you are minting many, read it once and pass the timestamp, or use the batch doors. (Measured before the per-millisecond re-seed, core change, 2026-10-27.)
|
|
219
247
|
|
|
220
|
-
The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.16x slower than `SecureRandom.uuid` for v4 (1.24 µs against 1.07 µs in its own run) and about 1.3x slower for v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it.
|
|
248
|
+
The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.16x slower than `SecureRandom.uuid` for v4 (1.24 µs against 1.07 µs in its own run) and about 1.3x slower for v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it. (Measured before the per-millisecond re-seed, core change, 2026-10-27.)
|
|
221
249
|
|
|
222
250
|
Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
|
|
223
251
|
|
|
224
252
|
| Call | i/s (Magnus backend) |
|
|
225
253
|
|---|---:|
|
|
226
|
-
| `new_v7` × 1000 (individual) | 3,742 |
|
|
227
|
-
| `new_v7_batch(1000)` | 4,850 (**1.3x**) |
|
|
254
|
+
| `new_v7` × 1000 (individual) † | 3,742 |
|
|
255
|
+
| `new_v7_batch(1000)` † | 4,850 (**1.3x**) |
|
|
256
|
+
|
|
257
|
+
† Measured before the per-millisecond re-seed (core change, 2026-10-27).
|
|
228
258
|
|
|
229
|
-
The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 206 µs on (in this table's run; 202 µs in the bytes-versus-objects run above) is building a thousand `Uuid` objects — the byte form above does the same native work in 4.9 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v7, need many at once, or need this Ruby service's IDs to agree byte-for-byte with a Go or Python service's, that's what this gem is for — and now it's the fast option too, not just the capable one.
|
|
259
|
+
The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 206 µs on (in this table's run; 202 µs in the bytes-versus-objects run above) is building a thousand `Uuid` objects — the byte form above does the same native work in 4.9 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v7, need many at once, or need this Ruby service's IDs to agree byte-for-byte with a Go or Python service's, that's what this gem is for — and now it's the fast option too, not just the capable one. (Measured before the per-millisecond re-seed, core change, 2026-10-27.)
|
|
230
260
|
|
|
231
261
|
## Backends
|
|
232
262
|
|
|
@@ -280,7 +310,7 @@ gh attestation verify hyperuuid-<version>-<platform>.gem --repo SkunkWerkx/Hyper
|
|
|
280
310
|
That is the release's second layer of checking, not the only one: before any gem is built, the
|
|
281
311
|
same job verifies every native binary it packs — the Fiddle libraries, the Magnus extensions
|
|
282
312
|
(one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own* build
|
|
283
|
-
provenance, signed from `SkunkWerkx/.github` by `hyper-
|
|
313
|
+
provenance, signed from `SkunkWerkx/.github` by `hyper-core.yml`, `hyper-test-ruby.yml` and
|
|
284
314
|
`hyper-build-wasm.yml` (so that check needs `--signer-repo SkunkWerkx/.github` added), and
|
|
285
315
|
refuses to proceed on an unverified one. RubyGems.org has no unpublish and no
|
|
286
316
|
duplicate-version overwrite, so all of this happens while a bad artifact is still reversible,
|
|
@@ -320,7 +350,7 @@ one at `require` time:
|
|
|
320
350
|
| 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | platform |
|
|
321
351
|
| 3.3 (the floor, until its EOL 2027-03-31) | Fiddle | universal |
|
|
322
352
|
|
|
323
|
-
What stands behind each cell: CI runs the whole suite through each Magnus extension — Ruby 4.0 on every pull request and Ruby 3.4 in the weekly and release builds, on all seven platform-gem platforms, the two musl ones inside each
|
|
353
|
+
What stands behind each cell: CI runs the whole suite through each Magnus extension — Ruby 4.0 on every pull request that touches the gem, the core or the workflows (after the gem's lint passed once, on Linux arm64), and Ruby 3.4 in the weekly and release builds, which run everything — on all seven platform-gem platforms, the two musl ones inside each
|
|
324
354
|
Ruby's own Alpine image — and the Fiddle suite on Ruby 4.0 on every one of them, plus on Ruby
|
|
325
355
|
3.3 inside Alpine. Intel macOS has no CI leg: its library is cross-built and tested at the
|
|
326
356
|
core, and Ruby there runs the universal gem's Fiddle backend over it. Anywhere else — a platform with no native build at
|
|
@@ -363,7 +393,7 @@ Fiddle fallback costs the most: measured on win-x64, Ruby 3.4, the Magnus backen
|
|
|
363
393
|
`new_v4` in 406ns against Fiddle's 2407ns (**5.9x**) and `new_v7` in 595ns against 2759ns
|
|
364
394
|
(**4.6x**) — a far wider gap than any Linux or macOS leg shows — and on real Windows-on-ARM
|
|
365
395
|
hardware, Ruby 4.0.6, `new_v4` in 416ns against Fiddle's 2299ns (**5.5x**) and `new_v7` in
|
|
366
|
-
621ns against 2474ns (**4.0x**), the same shape. Both extensions are built for the `gnullvm`
|
|
396
|
+
621ns against 2474ns (**4.0x**), the same shape (the `new_v7` figures measured before the per-millisecond re-seed, core change, 2026-10-27). Both extensions are built for the `gnullvm`
|
|
367
397
|
Rust targets rather than `gnu` — the same mingw-w64/UCRT ABI RubyInstaller's Ruby uses,
|
|
368
398
|
linked with LLVM and compiler-rt instead of GCC and a statically-linked libgcc, which is what
|
|
369
399
|
keeps the shipped extension small. The build script, and the two flags that are load-bearing
|
|
@@ -422,10 +452,11 @@ which read the clock through WASI.
|
|
|
422
452
|
rename them to names of their own when CI builds the archive, which also fails if a newer
|
|
423
453
|
Rust starts exporting another.
|
|
424
454
|
|
|
425
|
-
What stands behind it:
|
|
426
|
-
it ships, links it into a fresh
|
|
427
|
-
|
|
428
|
-
packed around those attested
|
|
455
|
+
What stands behind it: in the weekly and release builds (not on pull requests), CI builds each
|
|
456
|
+
minor's archive from the commit, packs the gem the way it ships, links it into a fresh
|
|
457
|
+
interpreter and runs [`wasm-smoke/test.rb`](wasm-smoke/test.rb) under Node and in headless
|
|
458
|
+
Chrome (the forge's `hyper-build-wasm.yml`). The published gem is packed around those attested
|
|
459
|
+
archives.
|
|
429
460
|
|
|
430
461
|
## Development
|
|
431
462
|
|
|
Binary file
|
|
Binary file
|
data/lib/hyperuuid/errors.rb
CHANGED
|
@@ -4,8 +4,14 @@ module HyperUuid
|
|
|
4
4
|
# arguments are valid. Carries the same message on every backend.
|
|
5
5
|
class RandomSourceError < StandardError; end
|
|
6
6
|
|
|
7
|
-
# Raised when a timestamp cannot be embedded in the version 7 UUID being minted: one
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Raised when a timestamp cannot be embedded in the version 7 UUID being minted: one after
|
|
8
|
+
# 9999-12-31T23:59:59.999Z (253402300799999 ms, the generation cap; the 48-bit field itself
|
|
9
|
+
# reaches 10889), or a negative one (a `Time` before the Unix epoch included). Carries the
|
|
10
|
+
# same message on every backend.
|
|
10
11
|
class TimestampOutOfRangeError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Raised when the version 7 stream cannot advance without running more than 1000 ms ahead of
|
|
14
|
+
# the supplied timestamp or past its last millisecond (2**44 - 2). Nothing is reserved.
|
|
15
|
+
# Carries the same message on every backend.
|
|
16
|
+
class CounterExhaustedError < StandardError; end
|
|
11
17
|
end
|
data/lib/hyperuuid/runtime.rb
CHANGED
|
@@ -18,7 +18,7 @@ module HyperUuid
|
|
|
18
18
|
# One copy of the out-of-range message for the Fiddle backend and for the doors' own
|
|
19
19
|
# range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs) carries the
|
|
20
20
|
# same text, and spec/native_backend_spec.rb pins that the two agree.
|
|
21
|
-
V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must
|
|
21
|
+
V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must be between 0 and 253402300799999 (9999-12-31T23:59:59.999Z)"
|
|
22
22
|
|
|
23
23
|
# The two batch-size refusals, shared the same way: a version 7 batch past the 26-bit
|
|
24
24
|
# counter space (the core's return code 4, which the doors in hyperuuid.rb refuse before
|
|
@@ -28,6 +28,12 @@ module HyperUuid
|
|
|
28
28
|
V7_BATCH_TOO_LARGE = "a single version 7 batch takes at most 67108864 UUIDs (the 26-bit counter space)"
|
|
29
29
|
BATCH_TOO_LARGE_TO_ADDRESS = "the batch is too large to address on this platform"
|
|
30
30
|
|
|
31
|
+
# The version 7 stream's refusal (the core's return code 6): it cannot advance without
|
|
32
|
+
# running more than 1000 ms ahead of the supplied timestamp or past its last millisecond.
|
|
33
|
+
# The Magnus extension carries the same text, and spec/counter_exhaustion_spec.rb pins it.
|
|
34
|
+
V7_COUNTER_EXHAUSTED = "version 7 counter exhausted: the stream cannot advance without running more " \
|
|
35
|
+
"than 1000 ms ahead of the supplied timestamp or past its last millisecond"
|
|
36
|
+
|
|
31
37
|
# The SQL Server order conversions' one refusal, in the core's own words (the Magnus
|
|
32
38
|
# extension raises its SqlOrderError's text): a value that isn't a UUID of the
|
|
33
39
|
# conversion's version (code 5), an ArgumentError. Every version 7 converts.
|
|
@@ -68,6 +74,7 @@ module HyperUuid
|
|
|
68
74
|
case rc
|
|
69
75
|
when 0 then out[0, 16]
|
|
70
76
|
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
77
|
+
when 6 then raise CounterExhaustedError, V7_COUNTER_EXHAUSTED
|
|
71
78
|
else raise random_source_failure("uuid_new_v7")
|
|
72
79
|
end
|
|
73
80
|
end
|
|
@@ -89,6 +96,7 @@ module HyperUuid
|
|
|
89
96
|
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
90
97
|
when 3 then raise ArgumentError, BATCH_TOO_LARGE_TO_ADDRESS
|
|
91
98
|
when 4 then raise ArgumentError, V7_BATCH_TOO_LARGE
|
|
99
|
+
when 6 then raise CounterExhaustedError, V7_COUNTER_EXHAUSTED
|
|
92
100
|
else raise random_source_failure("uuid_new_v7_batch")
|
|
93
101
|
end
|
|
94
102
|
end
|
data/lib/hyperuuid/uuid.rb
CHANGED
|
@@ -128,7 +128,8 @@ module HyperUuid
|
|
|
128
128
|
# variant as well as the version: a 7 in the version nibble under any variant but
|
|
129
129
|
# +:rfc9562+ is not a version 7 UUID, so it has no timestamp (#rfc? is the same check on
|
|
130
130
|
# its own). Every other version has none either, a version 6 or 1 from another generator
|
|
131
|
-
# included.
|
|
131
|
+
# included, and nor has a version 7 dated past 9999-12-31T23:59:59.999Z, the generation
|
|
132
|
+
# cap: the 48-bit field reaches 10889, and only Runtime.v7_unix_millis reads it that far.
|
|
132
133
|
#
|
|
133
134
|
# Raises by default for anything that isn't an RFC 9562 version 7 UUID; pass
|
|
134
135
|
# `raise_on_mismatch: false` to get `nil` back instead — for a caller that doesn't already
|
data/lib/hyperuuid.rb
CHANGED
|
@@ -19,7 +19,7 @@ require_relative "hyperuuid/runtime"
|
|
|
19
19
|
module HyperUuid
|
|
20
20
|
# This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v7) the rest
|
|
21
21
|
# of this module generates.
|
|
22
|
-
VERSION = "1.
|
|
22
|
+
VERSION = "1.1.0"
|
|
23
23
|
|
|
24
24
|
# The widest batch count, and the longest v5 name in bytes, the native ABI carries (a u32).
|
|
25
25
|
# (The millisecond count is a u64; unix_millis_from refuses anything wider.)
|
|
@@ -27,17 +27,8 @@ module HyperUuid
|
|
|
27
27
|
private_constant :U32_MAX
|
|
28
28
|
|
|
29
29
|
# The most UUIDs one version 7 batch (#new_v7_batch, #new_v7_batch_bytes) mints:
|
|
30
|
-
# 67,108,864
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# Every batch up to this size is in strictly increasing order: the counter is one
|
|
34
|
-
# process-wide sequence, so a batch can straddle the point where it wraps back to 0, and the
|
|
35
|
-
# UUIDs from there on carry a timestamp one millisecond later than the one supplied.
|
|
36
|
-
# The roll-forward orders one batch, not the stream: the next batch or single #new_v7 in
|
|
37
|
-
# the same real millisecond starts its counter just past the wrap and carries the supplied
|
|
38
|
-
# timestamp, so it sorts before this batch's tail, which was stamped a millisecond later.
|
|
39
|
-
# Two single #new_v7 calls either side of the wrap in one millisecond sort in reverse the
|
|
40
|
-
# same way. It happens at most once per 2**26 UUIDs the process mints.
|
|
30
|
+
# 67,108,864. A larger count is refused with ArgumentError before anything is allocated.
|
|
31
|
+
# See #new_v7_batch for the ordering a batch carries.
|
|
41
32
|
MAX_V7_BATCH = Runtime::MAX_V7_BATCH
|
|
42
33
|
|
|
43
34
|
# Creates a random UUID version 4 (RFC 9562 §5.4).
|
|
@@ -103,33 +94,40 @@ module HyperUuid
|
|
|
103
94
|
end
|
|
104
95
|
|
|
105
96
|
# Creates a time-sortable UUID version 7 (RFC 9562 §6.2). Defaults to the current time; pass
|
|
106
|
-
# an explicit `Time` or Unix-epoch millisecond integer (non-negative,
|
|
107
|
-
# embed a specific time instead.
|
|
97
|
+
# an explicit `Time` or Unix-epoch millisecond integer (non-negative, at most 253402300799999:
|
|
98
|
+
# 9999-12-31T23:59:59.999Z, the generation cap) to embed a specific time instead.
|
|
99
|
+
#
|
|
100
|
+
# Every call reserves the next step of one process-wide stream: see #new_v7_batch for the
|
|
101
|
+
# ordering and the embedded timestamp.
|
|
108
102
|
#
|
|
109
|
-
# @raise [TimestampOutOfRangeError] if the time is
|
|
103
|
+
# @raise [TimestampOutOfRangeError] if the time is before 1970 or after 9999-12-31T23:59:59.999Z.
|
|
104
|
+
# @raise [CounterExhaustedError] if the stream cannot advance without running more than
|
|
105
|
+
# 1000 ms ahead of the supplied time or past its last millisecond; nothing is reserved.
|
|
110
106
|
# @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
|
|
111
107
|
def self.new_v7(unix_millis = nil)
|
|
112
108
|
Uuid.new(Runtime.new_v7(unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE)), true)
|
|
113
109
|
end
|
|
114
110
|
|
|
115
111
|
# Creates `count` time-sortable version 7 UUIDs sharing one timestamp capture and one
|
|
116
|
-
# contiguous block of the
|
|
112
|
+
# contiguous block of the v7 stream — one FFI call and one random-bytes fetch
|
|
117
113
|
# instead of `count` of each. Defaults to the current time; pass an explicit `Time` or
|
|
118
114
|
# Unix-epoch millisecond integer to embed a specific time instead.
|
|
119
115
|
#
|
|
120
|
-
#
|
|
121
|
-
# a batch can straddle the point where it wraps back to 0; the UUIDs from there on carry a
|
|
122
|
-
# timestamp one millisecond later than the one supplied rather than sorting before the ones
|
|
123
|
-
# ahead of them. That is also why one batch takes at most MAX_V7_BATCH.
|
|
116
|
+
# A batch is always in strictly increasing order, and takes at most MAX_V7_BATCH.
|
|
124
117
|
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
118
|
+
# Every call reserves from one process-wide stream, re-seeded at random whenever a timestamp
|
|
119
|
+
# moves it forward: a single call consumes one of a millisecond's 2**20 steps and a batch of
|
|
120
|
+
# n consumes ceil(n / 64). For timestamps up to 2**44 - 2 (the year 2527), a call whose
|
|
121
|
+
# timestamp is within 10 s of the newest stream timestamp either raises CounterExhaustedError
|
|
122
|
+
# or sorts strictly after every UUID from a reservation that completed before it began. The
|
|
123
|
+
# embedded timestamp is at most 11 s ahead of the one supplied, and a split's tail carries the
|
|
124
|
+
# millisecond after it (at most 1000 ms ahead). A timestamp more than 11 s behind the stream,
|
|
125
|
+
# or past 2**44 - 2, is minted at itself, outside the stream and ordered against nothing.
|
|
130
126
|
#
|
|
131
127
|
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and MAX_V7_BATCH.
|
|
132
|
-
# @raise [TimestampOutOfRangeError] if the time is
|
|
128
|
+
# @raise [TimestampOutOfRangeError] if the time is before 1970 or after 9999-12-31T23:59:59.999Z.
|
|
129
|
+
# @raise [CounterExhaustedError] if the stream cannot advance without running more than
|
|
130
|
+
# 1000 ms ahead of the supplied time or past its last millisecond; nothing is reserved.
|
|
133
131
|
def self.new_v7_batch(count, unix_millis = nil)
|
|
134
132
|
bytes = new_v7_batch_bytes(count, unix_millis)
|
|
135
133
|
Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
|
|
@@ -149,10 +147,12 @@ module HyperUuid
|
|
|
149
147
|
# String into them yourself just moves the same allocations into your own code.
|
|
150
148
|
#
|
|
151
149
|
# Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally. The
|
|
152
|
-
#
|
|
150
|
+
# ordering, embedded timestamp and the MAX_V7_BATCH limit are #new_v7_batch's.
|
|
153
151
|
#
|
|
154
152
|
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and MAX_V7_BATCH.
|
|
155
|
-
# @raise [TimestampOutOfRangeError] if the time is
|
|
153
|
+
# @raise [TimestampOutOfRangeError] if the time is before 1970 or after 9999-12-31T23:59:59.999Z.
|
|
154
|
+
# @raise [CounterExhaustedError] if the stream cannot advance without running more than
|
|
155
|
+
# 1000 ms ahead of the supplied time or past its last millisecond; nothing is reserved.
|
|
156
156
|
def self.new_v7_batch_bytes(count, unix_millis = nil)
|
|
157
157
|
# First, so any count past the limit (2**32 and beyond included) names the limit itself.
|
|
158
158
|
raise ArgumentError, Runtime::V7_BATCH_TOO_LARGE if count.is_a?(Integer) && count > MAX_V7_BATCH
|