Skip to content

Commit 220b50c

Browse files
authored
Avoid generating UUIDs within the same timestamp (#25)
* Avoid generating UUIDs within the same timestamp
1 parent de0aeae commit 220b50c

File tree

3 files changed

+107
-25
lines changed

3 files changed

+107
-25
lines changed

src/UUID.php

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,32 @@ class UUID
6060
/** @internal */
6161
private const REPLACE_ARR = array('urn:', 'uuid:', '-', '{', '}');
6262

63+
/** @internal */
64+
private static $unixts = 0;
65+
66+
/** @internal */
67+
private static $subsec = 0;
68+
69+
private static function getUnixTime()
70+
{
71+
$timestamp = microtime(false);
72+
$unixts = intval(substr($timestamp, 11), 10);
73+
$subsec = intval(substr($timestamp, 2, 7), 10);
74+
if (self::$unixts > $unixts || self::$unixts === $unixts && self::$subsec >= $subsec) {
75+
$unixts = self::$unixts;
76+
$subsec = self::$subsec;
77+
if ($subsec === 9999999) {
78+
$subsec = 0;
79+
$unixts++;
80+
} else {
81+
$subsec++;
82+
}
83+
}
84+
self::$unixts = $unixts;
85+
self::$subsec = $subsec;
86+
return [$unixts, $subsec];
87+
}
88+
6389
/** @internal */
6490
private static function stripExtras($uuid)
6591
{
@@ -86,23 +112,23 @@ private static function getBytes($uuid)
86112
}
87113

88114
/** @internal */
89-
private static function uuidFromHash($hash, $version)
115+
private static function uuidFromHex($uhex, $version)
90116
{
91117
return sprintf(
92118
'%08s-%04s-%04x-%04x-%12s',
93119
// 32 bits for "time_low"
94-
substr($hash, 0, 8),
120+
substr($uhex, 0, 8),
95121
// 16 bits for "time_mid"
96-
substr($hash, 8, 4),
122+
substr($uhex, 8, 4),
97123
// 16 bits for "time_hi_and_version",
98124
// four most significant bits holds version number
99-
(hexdec(substr($hash, 12, 4)) & 0x0fff) | $version << 12,
125+
(hexdec(substr($uhex, 12, 4)) & 0x0fff) | $version << 12,
100126
// 16 bits, 8 bits for "clk_seq_hi_res",
101127
// 8 bits for "clk_seq_low",
102128
// two most significant bits holds zero and one for variant DCE1.1
103-
(hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000,
129+
(hexdec(substr($uhex, 16, 4)) & 0x3fff) | 0x8000,
104130
// 48 bits for "node"
105-
substr($hash, 20, 12)
131+
substr($uhex, 20, 12)
106132
);
107133
}
108134

@@ -119,9 +145,9 @@ public static function uuid3($namespace, $name)
119145
$nbytes = self::getBytes($namespace);
120146

121147
// Calculate hash value
122-
$hash = md5($nbytes . $name);
148+
$uhex = md5($nbytes . $name);
123149

124-
return self::uuidFromHash($hash, 3);
150+
return self::uuidFromHex($uhex, 3);
125151
}
126152

127153
/**
@@ -132,8 +158,8 @@ public static function uuid3($namespace, $name)
132158
public static function uuid4()
133159
{
134160
$bytes = random_bytes(16);
135-
$hash = bin2hex($bytes);
136-
return self::uuidFromHash($hash, 4);
161+
$uhex = bin2hex($bytes);
162+
return self::uuidFromHex($uhex, 4);
137163
}
138164

139165
/**
@@ -149,9 +175,9 @@ public static function uuid5($namespace, $name)
149175
$nbytes = self::getBytes($namespace);
150176

151177
// Calculate hash value
152-
$hash = sha1($nbytes . $name);
178+
$uhex = sha1($nbytes . $name);
153179

154-
return self::uuidFromHash($hash, 5);
180+
return self::uuidFromHex($uhex, 5);
155181
}
156182

157183
/**
@@ -163,17 +189,17 @@ public static function uuid5($namespace, $name)
163189
*/
164190
public static function uuid6()
165191
{
166-
$time = microtime(false);
167-
$time = substr($time, 11) . substr($time, 2, 7);
192+
[$unixts, $subsec] = self::getUnixTime();
193+
$time = $unixts * 10 ** 7 + $subsec;
168194
$time = str_pad(dechex($time + self::TIME_OFFSET_INT), 16, '0', \STR_PAD_LEFT);
169195
$time = sprintf(
170196
'%012s6%03s',
171197
substr($time, -15, 12),
172198
substr($time, -3)
173199
);
174200
$bytes = random_bytes(8);
175-
$hash = $time . bin2hex($bytes);
176-
return self::uuidFromHash($hash, 6);
201+
$uhex = $time . bin2hex($bytes);
202+
return self::uuidFromHex($uhex, 6);
177203
}
178204

179205
/**
@@ -184,20 +210,18 @@ public static function uuid6()
184210
*/
185211
public static function uuid7()
186212
{
187-
$time = microtime(false);
188-
$unixts = substr($time, 11);
189-
$subsec = substr($time, 2, 7);
190-
$unixts = str_pad(dechex(intval($unixts, 10)), 9, '0', \STR_PAD_LEFT);
191-
$subsec = str_pad(dechex(intval($subsec, 10)), 6, '0', \STR_PAD_LEFT);
213+
[$unixts, $subsec] = self::getUnixTime();
214+
$unixts = str_pad(dechex($unixts), 9, '0', \STR_PAD_LEFT);
215+
$subsec = str_pad(dechex($subsec), 6, '0', \STR_PAD_LEFT);
192216
$time = sprintf(
193217
'%09s%03s7%03s',
194218
$unixts,
195219
substr($subsec, 0, 3),
196220
substr($subsec, -3)
197221
);
198222
$bytes = random_bytes(8);
199-
$hash = $time . bin2hex($bytes);
200-
return self::uuidFromHash($hash, 7);
223+
$uhex = $time . bin2hex($bytes);
224+
return self::uuidFromHex($uhex, 7);
201225
}
202226

203227
/**

tests/FutureTimeTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace UUID\Test;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use UUID\UUID;
9+
10+
/**
11+
* @covers \UUID\UUID
12+
*/
13+
final class FutureTimeTest extends TestCase
14+
{
15+
protected function setUp(): void
16+
{
17+
$a = new UUID();
18+
$reflection = new \ReflectionClass($a);
19+
$property = $reflection->getProperty('unixts');
20+
$property->setAccessible(true);
21+
$property->setValue($a, 9000000000);
22+
$property = $reflection->getProperty('subsec');
23+
$property->setAccessible(true);
24+
$property->setValue($a, 9999990);
25+
}
26+
27+
public function testFutureTimeVersion6()
28+
{
29+
$uuid1 = UUID::uuid6();
30+
for ($x = 0; $x < 1000; $x++) {
31+
$this->assertMatchesRegularExpression(
32+
'/^[0-9a-f]{8}\-[0-9a-f]{4}\-6[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/',
33+
$uuid1
34+
);
35+
$uuid2 = UUID::uuid6();
36+
$this->assertGreaterThan(
37+
$uuid1,
38+
$uuid2
39+
);
40+
$uuid1 = $uuid2;
41+
}
42+
}
43+
44+
public function testFutureTimeVersion7()
45+
{
46+
$uuid1 = UUID::uuid7();
47+
for ($x = 0; $x < 1000; $x++) {
48+
$this->assertMatchesRegularExpression(
49+
'/^[0-9a-f]{8}\-[0-9a-f]{4}\-7[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/',
50+
$uuid1
51+
);
52+
$uuid2 = UUID::uuid7();
53+
$this->assertGreaterThan(
54+
$uuid1,
55+
$uuid2
56+
);
57+
$uuid1 = $uuid2;
58+
}
59+
}
60+
}

tests/UuidTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ public function testCanGenerateValidVersion6()
5353
'/^[0-9a-f]{8}\-[0-9a-f]{4}\-6[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/',
5454
$uuid1
5555
);
56-
usleep(1);
5756
$uuid2 = UUID::uuid6();
5857
$this->assertGreaterThan(
5958
$uuid1,
@@ -75,7 +74,6 @@ public function testCanGenerateValidVersion7()
7574
'/^[0-9a-f]{8}\-[0-9a-f]{4}\-7[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/',
7675
$uuid1
7776
);
78-
usleep(1);
7977
$uuid2 = UUID::uuid7();
8078
$this->assertGreaterThan(
8179
$uuid1,

0 commit comments

Comments
 (0)