Skip to content

Commit

Permalink
[Uid] Ensure ULIDs are monotonic even when the time goes backward
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Sep 9, 2022
1 parent d35d402 commit 7e727b7
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 20 deletions.
5 changes: 5 additions & 0 deletions Tests/UlidTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@ public function testGenerate()
{
$a = new Ulid();
$b = new Ulid();
usleep(-10000);
$c = new Ulid();

$this->assertSame(0, strncmp($a, $b, 20));
$this->assertSame(0, strncmp($a, $c, 20));
$a = base_convert(strtr(substr($a, -6), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'), 32, 10);
$b = base_convert(strtr(substr($b, -6), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'), 32, 10);
$c = base_convert(strtr(substr($c, -6), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'), 32, 10);
$this->assertSame(1, $b - $a);
$this->assertSame(1, $c - $b);
}

public function testWithInvalidUlid()
Expand Down
33 changes: 13 additions & 20 deletions Ulid.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,33 +137,23 @@ public function getDateTime(): \DateTimeImmutable
}

if (4 > \strlen($time)) {
$time = str_pad($time, 4, '0', \STR_PAD_LEFT);
$time = '000'.$time;
}

return \DateTimeImmutable::createFromFormat('U.u', substr_replace($time, '.', -3, 0));
}

public static function generate(\DateTimeInterface $time = null): string
{
if (null === $time) {
return self::doGenerate();
}

if (0 > $time = substr($time->format('Uu'), 0, -3)) {
throw new \InvalidArgumentException('The timestamp must be positive.');
}

return self::doGenerate($time);
}

private static function doGenerate(string $mtime = null): string
{
if (null === $time = $mtime) {
if (null === $mtime = $time) {
$time = microtime(false);
$time = substr($time, 11).substr($time, 2, 3);
} elseif (0 > $time = $time->format('Uv')) {
throw new \InvalidArgumentException('The timestamp must be positive.');
}

if ($time !== self::$time) {
if ($time > self::$time || (null !== $mtime && $time !== self::$time)) {
randomize:
$r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10));
$r['r1'] |= ($r['r'] <<= 4) & 0xF0000;
$r['r2'] |= ($r['r'] <<= 4) & 0xF0000;
Expand All @@ -173,19 +163,22 @@ private static function doGenerate(string $mtime = null): string
self::$rand = array_values($r);
self::$time = $time;
} elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
if (null === $mtime) {
usleep(100);
if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) {
$time = (string) (1 + $time);
} elseif ('999999999' === $mtime = substr($time, -9)) {
$time = (1 + substr($time, 0, -9)).'000000000';
} else {
self::$rand = [0, 0, 0, 0];
$time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9);
}

return self::doGenerate($mtime);
goto randomize;
} else {
for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) {
self::$rand[$i] = 0;
}

++self::$rand[$i];
$time = self::$time;
}

if (\PHP_INT_SIZE >= 8) {
Expand Down

0 comments on commit 7e727b7

Please sign in to comment.