Skip to content

Commit 8ce1cc8

Browse files
committed
fixup! fixup! feat(snowflake): Use string as type for snowflake ids
1 parent 79d326b commit 8ce1cc8

File tree

5 files changed

+76
-47
lines changed

5 files changed

+76
-47
lines changed

lib/private/DB/Snowflake/NextcloudSequenceResolver.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ public function isAvailable(): bool {
3030
return $this->localCache instanceof IMemcache;
3131
}
3232

33-
public function sequence(float $currentTime): int {
34-
if ($this->localCache->add((string)$currentTime, 1, 10)) {
33+
public function sequence(string $currentTime): int {
34+
if ($this->localCache->add($currentTime, 1, 10)) {
3535
return 0;
3636
}
3737

38-
return $this->localCache->inc((string)$currentTime, 1) | 0;
38+
return $this->localCache->inc($currentTime, 1) | 0;
3939
}
4040
}

lib/private/DB/Snowflake/Snowflake.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static function getGenerator(int $datacenter, int $workerId, bool $isCLI,
3030
$resolver,
3131
$isCLI,
3232
);
33-
$generator->setStartTimeStamp((float)strtotime('2025-01-01') * 1000.0);
33+
$generator->setStartTimeStamp(strtotime('2025-01-01'));
3434
self::$generators[$key] = $generator;
3535
}
3636
return self::$generators[$key];

lib/private/DB/Snowflake/SnowflakeGenerator.php

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ class SnowflakeGenerator {
3131
/**
3232
* The start timestamp.
3333
*/
34-
protected ?float $startTime = null;
34+
protected ?string $startTime = null;
3535

3636
/**
3737
* The last timestamp for the random generator.
3838
*/
39-
protected float $lastTimeStamp = -1;
39+
protected string $lastTimeStamp = '-1';
4040

4141
/**
4242
* The sequence number for the random generator.
@@ -64,7 +64,7 @@ public function __construct(
6464
* @internal For unit tests only.
6565
*/
6666
public function is32BitsSystem(): bool {
67-
return 2147483647 === PHP_INT_MAX;
67+
return PHP_INT_SIZE === 4;
6868
}
6969

7070
/**
@@ -86,7 +86,7 @@ public function nextId(): string {
8686
$timestampLeftMoveLength = self::MAX_DATACENTER_LENGTH + $datacenterLeftMoveLength;
8787

8888
if ($this->is32BitsSystem()) {
89-
// Slow version for 32-bits machine using string concatenation
89+
// Version using gmp for 32-bits machine
9090
if (!function_exists('gmp_init')
9191
|| !function_exists('gmp_sub')
9292
|| !function_exists('gmp_add')
@@ -97,8 +97,8 @@ public function nextId(): string {
9797
throw new \RuntimeException('gmp is a required extension on 32bits system.');
9898
}
9999

100-
$currentTimeGmp = gmp_init((string)round($currentTime));
101-
$timestampGmp = gmp_init((string)round($timestamp));
100+
$currentTimeGmp = gmp_init($currentTime);
101+
$timestampGmp = gmp_init($timestamp);
102102

103103
$gmpShiftLeft = static function (\GMP $num, int $bits): \GMP {
104104
return gmp_mul($num, gmp_pow(2, $bits));
@@ -114,8 +114,8 @@ public function nextId(): string {
114114

115115
return gmp_strval($id);
116116
} else {
117-
// Faster version for 64-bits machine using bits-shifts
118-
return (string)(((int)($currentTime - $timestamp) << $timestampLeftMoveLength)
117+
// Dependency less version for 64-bits machine using bits-shifts
118+
return (string)((((int)$currentTime - (int)$timestamp) << $timestampLeftMoveLength)
119119
| ($this->datacenter << $datacenterLeftMoveLength)
120120
| ($this->workerId << $workerLeftMoveLength)
121121
| ((int)$this->isCLI << $isCLILeftMoveLength)
@@ -171,50 +171,75 @@ public function parseId(string $id): array {
171171
* Get current millisecond time.
172172
* @internal For unit tests only.
173173
*/
174-
public function getCurrentMillisecond(): float {
175-
return floor(microtime(true) * 1000.0);
174+
public function getCurrentMillisecond(): string {
175+
if ($this->is32BitsSystem()) {
176+
return gmp_strval(gmp_mul(microtime(), 1000));
177+
} else {
178+
return (string)floor(microtime(true) * 1000);
179+
}
176180
}
177181

178182
/**
179-
* Set start time (millisecond).
183+
* Set start time (second).
180184
* @throw \InvalidArgumentException
181185
*/
182-
public function setStartTimeStamp(float $millisecond): self {
183-
$missTime = $this->getCurrentMillisecond() - $millisecond;
186+
public function setStartTimeStamp(int $second): self {
187+
if ($this->is32BitsSystem()) {
188+
$missTime = gmp_sub($this->getCurrentMillisecond(), gmp_mul($second, 1000));
184189

185-
if ($missTime < 0) {
186-
throw new \InvalidArgumentException('The start time cannot be greater than the current time');
187-
}
190+
if (gmp_cmp($missTime, 0) < 0) {
191+
throw new \InvalidArgumentException('The start time cannot be greater than the current time');
192+
}
193+
$mask = gmp_init(-1);
194+
$shifted = gmp_mul(gmp_init(-1), gmp_pow(2, self::MAX_TIMESTAMP_LENGTH)); // -1 << n
195+
$maxTimeDiff = gmp_xor($mask, $shifted);
196+
197+
if (gmp_cmp($missTime, $maxTimeDiff) > 0) {
198+
throw new \InvalidArgumentException(
199+
sprintf('The current microtime (%f) - starttime (%f) is not allowed to exceed -1 ^ (-1 << %d), You can reset the start time to fix this', $missTime, $maxTimeDiff, self::MAX_TIMESTAMP_LENGTH)
200+
);
201+
}
202+
$this->startTime = gmp_strval(gmp_mul($second, 1000));
203+
} else {
204+
$missTime = (int)$this->getCurrentMillisecond() - $second * 1000;
188205

189-
$maxTimeDiff = -1 ^ (-1 << self::MAX_TIMESTAMP_LENGTH);
206+
if ($missTime < 0) {
207+
throw new \InvalidArgumentException('The start time cannot be greater than the current time');
208+
}
190209

191-
if ($missTime > $maxTimeDiff && $maxTimeDiff === -1) {
192-
throw new \InvalidArgumentException(
193-
sprintf('The current microtime (%f) - starttime (%f) is not allowed to exceed -1 ^ (-1 << %d), You can reset the start time to fix this', $missTime, $maxTimeDiff, self::MAX_TIMESTAMP_LENGTH)
194-
);
195-
}
210+
$maxTimeDiff = -1 ^ (-1 << self::MAX_TIMESTAMP_LENGTH);
196211

197-
$this->startTime = $millisecond;
212+
if ($missTime > $maxTimeDiff) {
213+
throw new \InvalidArgumentException(
214+
sprintf('The current microtime (%f) - starttime (%f) is not allowed to exceed -1 ^ (-1 << %d), You can reset the start time to fix this', $missTime, $maxTimeDiff, self::MAX_TIMESTAMP_LENGTH)
215+
);
216+
}
217+
$this->startTime = (string)($second * 1000);
218+
}
198219

199220
return $this;
200221
}
201222

202223
/**
203224
* Get start timestamp (millisecond), If not set default to 2019-08-08 08:08:08.
204225
*/
205-
public function getStartTimeStamp(): float|int {
226+
public function getStartTimeStamp(): string {
206227
if (! is_null($this->startTime)) {
207228
return $this->startTime;
208229
}
209230

210231
// We set a default start time if you not set.
211-
return strtotime('2025-01-01') * 1000;
232+
if ($this->is32BitsSystem()) {
233+
return gmp_strval(gmp_mul(strtotime('2025-01-01'), 1000));
234+
} else {
235+
return (string)(strtotime('2025-01-01') * 1000);
236+
}
212237
}
213238

214239
/**
215240
* Call resolver.
216241
*/
217-
protected function callResolver(float $currentTime): int {
242+
protected function callResolver(string $currentTime): int {
218243
// Memcache based resolver
219244
if ($this->sequenceResolver->isAvailable()) {
220245
return $this->sequenceResolver->sequence($currentTime);

tests/lib/DB/SnowflakeTest.php

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,32 @@
1616
use Test\TestCase;
1717

1818
class SnowflakeTest extends TestCase {
19-
#[TestWith(data: [true, true, 42.0])] // 42ms
20-
#[TestWith(data: [true, false, 42.0])]
21-
#[TestWith(data: [false, true, 42.0])]
22-
#[TestWith(data: [true, false, 42.0])]
23-
#[TestWith(data: [false, true, 1000.0 * 60 * 60 * 24 * 365 * 10])] // ~10 years
24-
#[TestWith(data: [false, false, 1000.0 * 60 * 60 * 24 * 365 * 10])]
25-
public function testLayout(bool $isCLIExpected, bool $is32BitsSystem, float $timeDiff): void {
26-
$baseTimestamp = strtotime('2025-01-01') * 1000.0;
19+
#[TestWith(data: [true, true, 42])] // 42ms
20+
#[TestWith(data: [true, false, 42])]
21+
#[TestWith(data: [false, true, 42])]
22+
#[TestWith(data: [true, false, 42])]
23+
#[TestWith(data: [false, true, 60 * 60 * 24 * 365 * 10])] // ~10 years
24+
#[TestWith(data: [false, false, 60 * 60 * 24 * 365 * 10])]
25+
public function testLayout(bool $isCLIExpected, bool $is32BitsSystem, int $timeDiff): void {
26+
$baseTimestamp = strtotime('2025-01-01');
2727
$resolver = $this->createMock(NextcloudSequenceResolver::class);
2828
$resolver->method('isAvailable')->willReturn(true);
29-
$resolver->method('sequence')->willReturnCallback(function ($time) use ($baseTimestamp, $timeDiff) {
30-
$this->assertEqualsWithDelta($baseTimestamp + $timeDiff, $time, 0.01);
31-
return 42;
32-
});
29+
$resolver->method('sequence')->willReturn(42);
3330

3431
$snowFlake = $this->getMockBuilder(SnowflakeGenerator::class)
3532
->setConstructorArgs([21, 22, $resolver, $isCLIExpected])
3633
->onlyMethods(['getCurrentMillisecond', 'is32BitsSystem'])
3734
->getMock();
3835

39-
$snowFlake->method('getCurrentMillisecond')
40-
->willReturn($baseTimestamp + $timeDiff);
36+
if (PHP_INT_SIZE < 8) {
37+
$timeDiffString = gmp_strval(gmp_mul($timeDiff, 1000));
38+
$snowFlake->method('getCurrentMillisecond')
39+
->willReturn(gmp_strval(gmp_mul(gmp_add($baseTimestamp, $timeDiff), 1000)));
40+
} else {
41+
$timeDiffString = (string)($timeDiff * 1000);
42+
$snowFlake->method('getCurrentMillisecond')
43+
->willReturn((string)(($baseTimestamp + $timeDiff) * 1000));
44+
}
4145

4246
$snowFlake->method('is32BitsSystem')
4347
->willReturn($is32BitsSystem);
@@ -57,12 +61,12 @@ public function testLayout(bool $isCLIExpected, bool $is32BitsSystem, float $tim
5761
$this->assertEquals(22, $workerId);
5862
$this->assertEquals(21, $datacenter);
5963
$this->assertEquals($isCLIExpected, $isCLI);
60-
$this->assertEquals($timeDiff, $timestamp);
64+
$this->assertEquals($timeDiffString, $timestamp);
6165
$this->assertEquals(42, $sequence);
6266
}
6367

6468
public function testSetStartTimeStamp(): void {
6569
$generator = new SnowflakeGenerator(21, 22, $this->createMock(NextcloudSequenceResolver::class), true);
66-
$generator->setStartTimeStamp((float)strtotime('2025-01-01') * 1000);
70+
$generator->setStartTimeStamp(strtotime('2025-01-01'));
6771
}
6872
}

tests/lib/Preview/PreviewMapperTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private function createPreviewForFileId(int $fileId, ?int $bucket = null): void
5656
$locationId = null;
5757
if ($bucket) {
5858
$qb = $this->connection->getQueryBuilder();
59-
$locationId = $this->snowflake->id();
59+
$locationId = $this->snowflake->nextId();
6060
$qb->insert('preview_locations')
6161
->values([
6262
'id' => $locationId,

0 commit comments

Comments
 (0)