-
Notifications
You must be signed in to change notification settings - Fork 11
Discussion: Monotonicity and Counters #60
Comments
Please include this in version 7: A counter MAY be placed instead of the left side of the random section, immediately after the timestamp. The counter is designed to keep UUIDs monotonic within a timestamp step, by default within a millisecond. The default counter length is 16 bits for the millisecond timestamp. The counter MUST be reset every timestamp step. The counter MUST be frozen just before overflowing up to the next timestep step. For each UUID instance, the random section MUST be updated despite the use of the counter. |
Is there any benefit to this over simply treating the entire The advantage of ULID approach is that counter rollover so statistically unlikely as to not warrant mention. Whereas placement at the left side of the field requires reasoning about creation rates, counter sizes, and rollover error cases. |
|
Isn't this true of any monotonic counter scheme? Unless you randomize all bits with every uuid, there will be some degree of predictability.
Again, true of any counter scheme. Treating the whole 70-bit random value as a counter means the odds of actually encountering a rollover case are very small. Even if the lowest 20 bits turn over (1M uuids / msec), the top-most 50 bits would have to all be ones. There's only a ~1 in 1015 chance of that occurring. For the truly cautious, simply checking for that case and generating a different random value, or just forcing any of those high-order bits to be zero, eliminates that possibility. |
l beleive that all random bits of every uuid MUST be randomized despite using counter. Such monotonic counter scheme is secure.
No, if we have 16 empty bits of separate counter for every millisecond.
You are correct. |
I agree that it should generally be encouraged to entirely randomize some (say, 16 or 32 bits) trailing bits at every generation to ensure some levels of unpredictability, rather than use the whole |
oklog/ulid.Monotonic provides monotonicity parameterized by a user-provided IMO — specs should not take any stance on any property of random portions of IDs. Specs should define them as random, and if implementations want to provide monotonicity within some bounds, that's above-and-beyond spec guarantees. |
@peterbourgon If something is not in the specification, then this is a 99.99% guarantee that this will not be in the implementation either. Therefore, the specification should contain at least a more or less satisfactory solution as an option. |
@LiosK I like your solution very much. I would add freezing just before overflowing the counter, although this is unlikely. |
I don't think I agree, but I also don't think this is a bad thing. In fact, I think it's correct! The spec defines the random portion of the ID to be random. If those bytes happen to be monotonic, that's a detail of the implementation. |
@peterbourgon @sergeyprokhorenko |
|
UUID generation is necessarily a possible source of runtime errors, because entropy is exhaustible. |
@peterbourgon This is not true. It is entirely possible to prevent errors during the generation of UUIDs. It is better to prevent errors if possible. If you mean collisions of UUIDs, then such errors do not occur during generation, but when writing to the table. |
If you run out of entropy, as far as I'm aware your options are (a) fail, or (b) block until entropy is replenished. |
I assume you are referencing I simply say you SHOULD use a monotonic random and for fixed-length counters you SHOULD use UUIDv8 because of the heated debates around counter length, rollover handling, and initialization of the counter have so many variations based on user input that it is better fit for UUIDv8. If you would like I can put some text in the "Fixed-Length Dedicated Counter Bits (Method 1):" bullet that stated UUIDv7
The text let's the application implementers decide and provides some guidance. Both usages in this section are SHOULD not MUST. Implementations are fit to do what they want. If they desire to let some items go out of order then they may. |
@peterbourgon It sounds like "run out of numbers" :)) |
@kyzer-davis No, I mean #60 (comment) I like it very much |
@kyzer-davis Yes please. That would be great! |
@sergeyprokhorenko Running out of entropy is much more common than running out of numbers :)
|
@kyzer-davis It seems to me that the specification should suggest to developers a safer option, and not an unexpected pause in the generation of UUIDs when the counter overflows. Overflows always remind me of the Ariane flight V88 disaster |
This is about the low quality of pseudo-random numbers, but not about their deficit. Poor quality generators generate a repeating sequence of pseudo-random numbers, which leads to collisions. |
Let me put together a Change Proposal template for this and get it in Draft 03 Phase 3 PR.
I apologize, somehow I missed this comment entirely. It does seem like a valid approach but seems pretty similar to I could update Fixed-Length Dedicated Counter Seeding:
|
I deleted my comment |
As did I :) Let me know what you think about that proposed "Fixed-Length Dedicated Counter Seeding:" text. |
I believe you're mistaken. Both are issues, but I'm pointing out the fact that entropy — crypto-grade and pseudorandom both — is a resource which can be depleted. If something consumes that resource, then it necessarily must deal with the possibility that there is no entropy available in a given period of time. |
@LiosK Great writeup.
This statement hits the nail on the head for the best practices section. I am working on the counter changes. Another update to come. |
@LiosK, I merged the changes which are live now via https://uuid6.github.io/uuid6-ietf-draft/#name-monotonicity-and-counters The text should now describe:
I removed the harsh verbiage against fixed-length counters. Both methods solve the problems. The bits being tallied are just in two different positions within the UUID layout. We are describing the solutions and their possible upsides/downfalls/challenges. Implementations pick the one that seems correct for them. I also made the changes to the fixed-length seeding, length, rollover parts as per your comment #60 (comment) Lastly, I abstracted the "when to increment" sub-section to point to Counter Methods and Increment Types. @sergeyprokhorenko, the text reads:
This should be sufficient to cover the topic. Edit: Personal note, I have always been a proponent of the fixed-length counter method. Drafts 01 and 02 never had monotonic random because of this personal bias. Any harsh verbiage against fixed-length counters in the previous stages of Draft 03 was not intended. As we have iterated via this thread, both methods have proven sufficient at solving this problem. As a result in this latest revision both should be presented as equal solutions. If this is not the case please suggest an edit. |
@kyzer-davis, Now the text of section I'm convinced that both methods 1 and 2 should be dropped in favor of paragraph We also need to drop |
The new draft looks good! Thank you, @kyzer-davis. If I might add something, Counter Rollover Handling is also relevant to the monotonic random approach particularly when it's combined with the random increment strategy, and thus it should be placed under "The following sub-topics cover methods behind incrementing either type of counter method:". |
@LiosK, good callout. I split into two sections for rollover handling and rollover guarding in the latest PR. |
Completely agree. @kyzer-davis, IMO current standard version is too loose, it is unclear and confusing.
|
IMO MAY is fine here. With a 24-bit counter, on average 8 million IDs can be generated per millisecond and with a 99.9% chance at least 10,000 IDs can be generated. BTW, I am an advocate of 42-bit counters and in this case 2.2 trillion on average and with a 99.999999% chance at least 10,000 can be generated. These numbers are more than enough for most applications and only few extremely high-load applications and those that prefer short counters are adversely affected by the initialize-full-counter approach. On the other hand, if the spec recommends the initialize-portion-counter approach, the generic libraries might force every application to pay one bit entropy for the overflow guard not really relevant to most applications. The initialize-portion-counter approach definitely solves some problems but is not a clearly superior approach that should be recommended to everyone.
Your approaches are actually very interesting and creative if combined together. The timestamp state is simply incremented at a counter overflow and such an advanced timestamp state is accepted so long as the difference from the real clock is small enough. The generator will stall, reset state, or raise error only when the real clock is rewound so much or when the generator is "overheated" (i.e. when it experiences too many counter overflows). Very interesting! I would try this in my prototype. I'm not sure if many people think this approach is "right", but I'm kind of sure this works well in common scenarios. |
So you're advocating not using "N-1 rollover protection" as the default approach? |
N-1 approach shouldn't be default. Overflow protection isn't worth one bit entropy for most applications. Plus, counter overflow isn't a serious disaster if handled properly e.g. by your smart handling technique. |
I agree. One note: in my proposal, the timestamp resolution was 100ns, which is overkill in most cases. So timestamp incrementing shouldn't hurt. |
1 ms is likely overkill in a scenario where distributed nodes use their own clocks, while even 100 ns increment can be problematic if a clock is shared by distributed nodes. I'm now trying to figure out in what conditions the timestamp increment can hurt and in what conditions not. Anyway, using the timestamp field as a reserve counter is an eye opening but counterintuitive idea that not many implementers will come up with. I think it's worth discussing this here and in the RFC. |
@edo1, great feedback on the section. On the topics:
Plus, remember, everything in this section also pertains to UUIDv8 where somebody may want to do it their own way. Rather than spend a ton of cycles on R&D; we have already vetted most problems/solutions and documented the pros/cons. They can implement what they need and be on their way. Edit: Saw your other comment on timestamp length thread:
And MAY layer in a counter of parts:
The folks using those libraries will take what they get and not need to think about all the other "best practices" defined in that section. |
I agree to trade the N-1 approach for a timestamp increment. Complex problems require complex but ultimate tools. It's not a choice between green and purple UUIDs with an A/B test among Instagrammers |
It would be clearer if all options had short names instead of numbers, letters and counting sticks. It seems to me that the merits and demerits of the options are less described in the RFC than in the GitHub issues. Therefore, the implementers will turn into Buridan donkeys. The RFC should indicate the preferred option, of course with justification. It seems to me that in the course of the discussion, the positions of the participants here quickly converge. Therefore, it should not be a problem to choose the preferred option. If something causes controversy, then it has not yet been sufficiently thought out. It would be desirable to show an exact and unambiguous reference layout of UUID for example, for a marketplace or an international bank. |
@kyzer-davis , I just wanted to suggest this change below as the Plus One increment type is a special case of Plus N (random increment).
I gave up on the idea of creating a Change Proposal issue because the deadline for submitting the draft was running out. We all want to see our collective effort specification published as a standard as soon as possible. However, it is important that we correct small problems as they are discovered. So I want to take the opportunity to suggest further changes. Forgive me for saying this just now. I was looking forward to the submission of the new draft. I really liked the two counter methods and the two increment types. The combinations produced by them seemed to resolve all the differences of opinion that there were. I mean if an implementer wanted to use method 1 with type 2 they would be free to do so with full approval of the specification. However, while implementing a GitHub Gist to test the four combinations of counter methods and increment types, I realized that maybe it's not necessary to have 4 combinations. I'll explain.
So I would like to propose that there are only 2 combinations: Method 1 with Type A and Method 2 with Type B. With that I hope to remove the cons and just keep the pros. Of course you are all free to disagree. The changes I want to propose in the text are the ones below. I just copy-pasted and changed some modal verbs. Fixed-Length Dedicated Counter Bits (Method 1): This references the practice of allocating a specific number of With this increment logic the counter method is incremented by one Monotonic Random (Method 2): With this method the random data is extended to also double as a With this increment the actual increment of the counter The increment of the countar MAY be 1. However when this increment |
I'd rather suggest to subsume the monotonic random under the fixed-length counter because the monotonic random is just a 74-bit variation of fixed-length counters, but I don't have a strong opinion here as long as unsafe choices are properly quarantined.
It's a very good point. I've been observing that UUIDv7 with counter is much faster than that without counter because even the blazing fast |
I guess it will exactly the opposite in real life. Version with counter needs mutex or another lock. |
Yes, monotonic random can be thought of as a 74-bit variation of fixed-length counters. I just thought "hey, maybe we can reduce the cognitive load if we omit the fixed-length counter that increments by a random number" (Method 1 with Type B). I don't have a strong opinion either.
I think it might be true if the pseudo-random generator is not cryptographically secure. I just tested this hypothesis in a project of mine using |
I think it depends. Nowadays, ChaCha20-based fast cryptographic PRNGs are widely available, and thus CSRNG can be regarded non-blocking, but taking secure random bits used to be considered (or in some environments it actually is) a slow I/O operation. Also, many RNGs use some sync mechanisms to protect their own states. |
@fabiolimace, perhaps you're right! The random increment approach makes sense with 74-but counters only and the plus-one approach makes sense with 42-bit or less counters only. In this sense, the two types of counters might have different natures, so your approach will make the discussion easier to understand! |
To facilitate the discussion, I propose Layout grid for UUIDv7 with Crockford's Base32 encoding (for a new RFC Draft) |
@fabiolimace, The old deadline for Draft 03 submission was just so the existing draft didn't expire! My new deadline for Draft 04 is a month before IETF 114. So June 23 since I plan to attend in person and have a live working session/discussion for this draft. ( As for your summary of Counter Method, Increment Type combos:
I agree: I was creating the test vectors in the appendix for this reason. I wanted to show these with real data and ultimately a real UUIDv6/7/8. I think it would be beneficial to have a version of UUIDv7 with counters as a reference point. I will open a Also just tracking for my own sanity: |
Hi! I've just created a change proposal #107 to insert a single line discussing the "timestamp as reserve counter" approach discussed above. It's a very powerful approach while being counter-intuitive at first sight; it's worth noting in the standard to help implementers come up with it. I've tested this approach on my prototypes in JS and Rust for a while and found that:
|
All things Monotonicity and Counters to assist with sort ordering, db index locality, and same Timestamp-tick collision avoidance during batch UUID creation.
Sub-Topics: Counter position, length, rollover handling and seeding.
Extension of the thread: #58 (comment)
Required Reading: #41 (comment) and Research efforts
The text was updated successfully, but these errors were encountered: