ULID Generation#
Everytime you call nextULID
, a new ULID is generated. It get's current millisecond time using ZIO's Clock
and 10 random bytes using ZIO's Random
. It then encodes the time and random bytes into a ULID. However, this simple implementation can cause random collision if you generate ULIDs in the same millisecond.
How is random collision avoided?#
To avoid it, ULIDGen
is a stateful layer. It stores the last millisecond when ULID was generated and the last random bytes used. When nextULID
is called, it compares current milliseconds with last milliseconds and if they are same, it does not generate new random bytes. Instead, it increments the last random bytes by 1, combines it with current milliseconds and encodes it into a ULID. It also stores the current milliseconds and random bytes in the state. To access and modify this state in a threadsafe manner, it uses ZIO's Ref
.
This behaviour is called monotonical increase. And because of this behaviour, ULIDs generated by ULIDGen
are lexicographically sortable.
How is overflow avoided?#
Maximum value of random bytes, as per spec encoding is ZZZZZZZZZZZZZZZZ
. What if we have a huge compute power and we are able to generate lots of (more than 2^80 - 1) ULIDs in the same millisecond? In that case, we will run out of random bytes.
ZIO-ULID does not return error in such case. Instead, within same millisecond ULIDGen
checks if the last generated random bytes is ZZZZZZZZZZZZZZZZ
. If it is, it waits for 1 millisecond and then generates a new ULID.
Note
Monotonical increase to avoid random collision and waiting for 1 millisecond to avoid overflow both are scoped to a millisecond. ULIDs generated in different milliseconds are not affected by these behaviours.
Note2
Both these concepts are taken from airframe-ulid implementation. Implementation is different since ZIO-ULID is implemented in ZIO.