Skip to content

Commit c8c92ef

Browse files
committed
fixup! feat(database): introduce Snowflake IDs generator
Signed-off-by: Benjamin Gaussorgues <[email protected]>
1 parent 1458aca commit c8c92ef

File tree

11 files changed

+265
-322
lines changed

11 files changed

+265
-322
lines changed

lib/composer/composer/autoload_classmap.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -628,8 +628,6 @@
628628
'OCP\\IRequestId' => $baseDir . '/lib/public/IRequestId.php',
629629
'OCP\\IServerContainer' => $baseDir . '/lib/public/IServerContainer.php',
630630
'OCP\\ISession' => $baseDir . '/lib/public/ISession.php',
631-
'OCP\\ISnowflakeId' => $baseDir . '/lib/public/ISnowflakeId.php',
632-
'OCP\\ISnowflakeIdGenerator' => $baseDir . '/lib/public/ISnowflakeIdGenerator.php',
633631
'OCP\\IStreamImage' => $baseDir . '/lib/public/IStreamImage.php',
634632
'OCP\\ITagManager' => $baseDir . '/lib/public/ITagManager.php',
635633
'OCP\\ITags' => $baseDir . '/lib/public/ITags.php',
@@ -824,6 +822,8 @@
824822
'OCP\\Share_Backend' => $baseDir . '/lib/public/Share_Backend.php',
825823
'OCP\\Share_Backend_Collection' => $baseDir . '/lib/public/Share_Backend_Collection.php',
826824
'OCP\\Share_Backend_File_Dependent' => $baseDir . '/lib/public/Share_Backend_File_Dependent.php',
825+
'OCP\\Snowflake\\IDecoder' => $baseDir . '/lib/public/Snowflake/IDecoder.php',
826+
'OCP\\Snowflake\\IGenerator' => $baseDir . '/lib/public/Snowflake/IGenerator.php',
827827
'OCP\\SpeechToText\\Events\\AbstractTranscriptionEvent' => $baseDir . '/lib/public/SpeechToText/Events/AbstractTranscriptionEvent.php',
828828
'OCP\\SpeechToText\\Events\\TranscriptionFailedEvent' => $baseDir . '/lib/public/SpeechToText/Events/TranscriptionFailedEvent.php',
829829
'OCP\\SpeechToText\\Events\\TranscriptionSuccessfulEvent' => $baseDir . '/lib/public/SpeechToText/Events/TranscriptionSuccessfulEvent.php',
@@ -2107,8 +2107,8 @@
21072107
'OC\\Share\\Constants' => $baseDir . '/lib/private/Share/Constants.php',
21082108
'OC\\Share\\Helper' => $baseDir . '/lib/private/Share/Helper.php',
21092109
'OC\\Share\\Share' => $baseDir . '/lib/private/Share/Share.php',
2110-
'OC\\SnowflakeId' => $baseDir . '/lib/private/SnowflakeId.php',
2111-
'OC\\SnowflakeIdGenerator' => $baseDir . '/lib/private/SnowflakeIdGenerator.php',
2110+
'OC\\Snowflake\\Decoder' => $baseDir . '/lib/private/Snowflake/Decoder.php',
2111+
'OC\\Snowflake\\Generator' => $baseDir . '/lib/private/Snowflake/Generator.php',
21122112
'OC\\SpeechToText\\SpeechToTextManager' => $baseDir . '/lib/private/SpeechToText/SpeechToTextManager.php',
21132113
'OC\\SpeechToText\\TranscriptionJob' => $baseDir . '/lib/private/SpeechToText/TranscriptionJob.php',
21142114
'OC\\StreamImage' => $baseDir . '/lib/private/StreamImage.php',

lib/composer/composer/autoload_static.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -669,8 +669,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
669669
'OCP\\IRequestId' => __DIR__ . '/../../..' . '/lib/public/IRequestId.php',
670670
'OCP\\IServerContainer' => __DIR__ . '/../../..' . '/lib/public/IServerContainer.php',
671671
'OCP\\ISession' => __DIR__ . '/../../..' . '/lib/public/ISession.php',
672-
'OCP\\ISnowflakeId' => __DIR__ . '/../../..' . '/lib/public/ISnowflakeId.php',
673-
'OCP\\ISnowflakeIdGenerator' => __DIR__ . '/../../..' . '/lib/public/ISnowflakeIdGenerator.php',
674672
'OCP\\IStreamImage' => __DIR__ . '/../../..' . '/lib/public/IStreamImage.php',
675673
'OCP\\ITagManager' => __DIR__ . '/../../..' . '/lib/public/ITagManager.php',
676674
'OCP\\ITags' => __DIR__ . '/../../..' . '/lib/public/ITags.php',
@@ -865,6 +863,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
865863
'OCP\\Share_Backend' => __DIR__ . '/../../..' . '/lib/public/Share_Backend.php',
866864
'OCP\\Share_Backend_Collection' => __DIR__ . '/../../..' . '/lib/public/Share_Backend_Collection.php',
867865
'OCP\\Share_Backend_File_Dependent' => __DIR__ . '/../../..' . '/lib/public/Share_Backend_File_Dependent.php',
866+
'OCP\\Snowflake\\IDecoder' => __DIR__ . '/../../..' . '/lib/public/Snowflake/IDecoder.php',
867+
'OCP\\Snowflake\\IGenerator' => __DIR__ . '/../../..' . '/lib/public/Snowflake/IGenerator.php',
868868
'OCP\\SpeechToText\\Events\\AbstractTranscriptionEvent' => __DIR__ . '/../../..' . '/lib/public/SpeechToText/Events/AbstractTranscriptionEvent.php',
869869
'OCP\\SpeechToText\\Events\\TranscriptionFailedEvent' => __DIR__ . '/../../..' . '/lib/public/SpeechToText/Events/TranscriptionFailedEvent.php',
870870
'OCP\\SpeechToText\\Events\\TranscriptionSuccessfulEvent' => __DIR__ . '/../../..' . '/lib/public/SpeechToText/Events/TranscriptionSuccessfulEvent.php',
@@ -2148,8 +2148,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
21482148
'OC\\Share\\Constants' => __DIR__ . '/../../..' . '/lib/private/Share/Constants.php',
21492149
'OC\\Share\\Helper' => __DIR__ . '/../../..' . '/lib/private/Share/Helper.php',
21502150
'OC\\Share\\Share' => __DIR__ . '/../../..' . '/lib/private/Share/Share.php',
2151-
'OC\\SnowflakeId' => __DIR__ . '/../../..' . '/lib/private/SnowflakeId.php',
2152-
'OC\\SnowflakeIdGenerator' => __DIR__ . '/../../..' . '/lib/private/SnowflakeIdGenerator.php',
2151+
'OC\\Snowflake\\Decoder' => __DIR__ . '/../../..' . '/lib/private/Snowflake/Decoder.php',
2152+
'OC\\Snowflake\\Generator' => __DIR__ . '/../../..' . '/lib/private/Snowflake/Generator.php',
21532153
'OC\\SpeechToText\\SpeechToTextManager' => __DIR__ . '/../../..' . '/lib/private/SpeechToText/SpeechToTextManager.php',
21542154
'OC\\SpeechToText\\TranscriptionJob' => __DIR__ . '/../../..' . '/lib/private/SpeechToText/TranscriptionJob.php',
21552155
'OC\\StreamImage' => __DIR__ . '/../../..' . '/lib/private/StreamImage.php',

lib/private/Snowflake/Decoder.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-only
8+
*/
9+
10+
namespace OC\Snowflake;
11+
12+
use OCP\Snowflake\IDecoder;
13+
use OCP\Snowflake\IGenerator;
14+
use Override;
15+
16+
/**
17+
* Nextcloud Snowflake ID
18+
*
19+
* Get information about Snowflake Id
20+
*
21+
* @since 33.0.0
22+
*/
23+
final class Decoder implements IDecoder {
24+
#[Override]
25+
public function decode(string $snowflakeId): array {
26+
if (!ctype_digit($snowflakeId)) {
27+
throw new \Exception('Invalid Snowflake ID: ' . $snowflakeId);
28+
}
29+
30+
/** @var array{seconds: positive-int, milliseconds: int<0,999>, serverId: int<0, 1023>, sequenceId: int<0,4095>, isCli: bool} $data */
31+
$data = PHP_INT_SIZE === 8
32+
? $this->decode64bits((int)$snowflakeId)
33+
: $this->decode32bits($snowflakeId);
34+
35+
$data['createdAt'] = new \DateTimeImmutable(
36+
sprintf(
37+
'@%d.%03d',
38+
$data['seconds'] + IGenerator::TS_OFFSET + intdiv($data['milliseconds'], 1000),
39+
$data['milliseconds'] % 1000,
40+
)
41+
);
42+
43+
return $data;
44+
}
45+
46+
private function decode64bits(int $snowflakeId): array {
47+
$firstHalf = $snowflakeId >> 32;
48+
$secondHalf = $snowflakeId & 0xFFFFFFFF;
49+
50+
$seconds = $firstHalf & 0x7FFFFFFF;
51+
$milliseconds = $secondHalf >> 22;
52+
53+
return [
54+
'seconds' => $seconds,
55+
'milliseconds' => $milliseconds,
56+
'serverId' => ($secondHalf >> 13) & 0x1FF,
57+
'sequenceId' => $secondHalf & 0xFFF,
58+
'isCli' => (bool)(($secondHalf >> 12) & 0x1),
59+
];
60+
}
61+
62+
private function decode32bits(string $snowflakeId): array {
63+
$id = $this->convertBase16($snowflakeId);
64+
65+
$firstQuarter = (int)hexdec(substr($id, 0, 4));
66+
$secondQuarter = (int)hexdec(substr($id, 4, 4));
67+
$thirdQuarter = (int)hexdec(substr($id, 8, 4));
68+
$fourthQuarter = (int)hexdec(substr($id, 12, 4));
69+
70+
$seconds = (($firstQuarter & 0x7FFF) << 16) | ($secondQuarter & 0xFFFF);
71+
$milliseconds = ($thirdQuarter >> 6) & 0x3FF;
72+
73+
return [
74+
'seconds' => $seconds,
75+
'milliseconds' => $milliseconds,
76+
'serverId' => (($thirdQuarter & 0x3F) << 3) | (($fourthQuarter >> 13) & 0x7),
77+
'sequenceId' => $fourthQuarter & 0xFFF,
78+
'isCli' => (bool)(($fourthQuarter >> 12) & 0x1),
79+
];
80+
}
81+
82+
/**
83+
* Convert base 10 number to base 16, padded to 16 characters
84+
*
85+
* Required on 32 bits systems as base_convert will lose precision with large numbers
86+
*/
87+
private function convertBase16(string $decimal): string {
88+
$hex = '';
89+
$digits = '0123456789ABCDEF';
90+
91+
while (strlen($decimal) > 0 && $decimal !== '0') {
92+
$remainder = 0;
93+
$newDecimal = '';
94+
95+
// Perform division by 16 manually for arbitrary precision
96+
for ($i = 0; $i < strlen($decimal); $i++) {
97+
$digit = (int)$decimal[$i];
98+
$current = $remainder * 10 + $digit;
99+
100+
if ($current >= 16) {
101+
$quotient = (int)($current / 16);
102+
$remainder = $current % 16;
103+
$newDecimal .= chr(ord('0') + $quotient);
104+
} else {
105+
$remainder = $current;
106+
// Only add quotient digit if we already have some digits in result
107+
if (strlen($newDecimal) > 0) {
108+
$newDecimal .= '0';
109+
}
110+
}
111+
}
112+
113+
// Add the remainder (0-15) as hex digit
114+
$hex = $digits[$remainder] . $hex;
115+
116+
// Update decimal for next iteration
117+
$decimal = ltrim($newDecimal, '0');
118+
}
119+
120+
return str_pad($hex, 16, '0', STR_PAD_LEFT);
121+
}
122+
}
Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
* SPDX-License-Identifier: AGPL-3.0-only
88
*/
99

10-
namespace OC;
10+
namespace OC\Snowflake;
1111

1212
use OCP\AppFramework\Utility\ITimeFactory;
13-
use OCP\ICacheFactory;
13+
use OCP\Snowflake\IGenerator;
14+
use Override;
1415

1516
/**
1617
* Nextcloud Snowflake ID generator
@@ -19,14 +20,14 @@
1920
*
2021
* @since 33.0.0
2122
*/
22-
class SnowflakeIdGenerator {
23+
final class Generator implements IGenerator {
2324
public function __construct(
2425
private readonly ITimeFactory $timeFactory,
25-
private readonly ICacheFactory $cacheFactory,
2626
) {
2727
}
2828

29-
public function __invoke(): string {
29+
#[Override]
30+
public function nextId(): string {
3031
// Time related
3132
[$seconds, $milliseconds] = $this->getCurrentTime();
3233

@@ -36,7 +37,7 @@ public function __invoke(): string {
3637
if ($sequenceId > 0xFFF) {
3738
// Throttle a bit, wait for next millisecond
3839
usleep(1000);
39-
return $this();
40+
return $this->nextId();
4041
}
4142

4243
if (PHP_INT_SIZE === 8) {
@@ -94,7 +95,7 @@ private function convertToDecimal(array $bytes): string {
9495
private function getCurrentTime(): array {
9596
$time = $this->timeFactory->now();
9697
return [
97-
$time->getTimestamp() - SnowflakeId::TS_OFFSET,
98+
$time->getTimestamp() - self::TS_OFFSET,
9899
(int)$time->format('v'),
99100
];
100101
}
@@ -109,8 +110,10 @@ private function isCli(): bool {
109110

110111
private function getSequenceId(int $seconds, int $milliseconds, int $serverId): int {
111112
$key = 'seq:' . $seconds . ':' . $milliseconds;
113+
112114
// Use APCu as fastest local cache, but not shared between processes in CLI
113-
if (!$this->isCli()) {
115+
if (!$this->isCli() && function_exists('apcu_inc')) {
116+
// TODO Try to detect reseted APCu cache
114117
$sequenceId = apcu_inc($key, ttl: 1);
115118
if ($sequenceId === false) {
116119
throw new \Exception('Failed to generate SnowflakeId with APCu');
@@ -119,25 +122,7 @@ private function getSequenceId(int $seconds, int $milliseconds, int $serverId):
119122
return $sequenceId;
120123
}
121124

122-
// Following lock can be shared between servers, add $serverId in $key
123-
$key .= ':' . $serverId;
124-
125-
if ($this->cacheFactory->isAvailable()) {
126-
$cache = $this->cacheFactory->createLocking('sequence');
127-
// Inc doesn't allow to give TTL so try to add first
128-
if ($cache->add($key, 0, 1)) {
129-
return 0;
130-
}
131-
$sequence = $cache->inc($key);
132-
if (is_int($sequence)) {
133-
return $sequence;
134-
}
135-
136-
throw new \Exception('Unable to generate sequence ID with locking cache');
137-
138-
}
139-
140-
// If all failed, just return a random number
125+
// Otherwise, just return a random number
141126
return random_int(0, 0xFFF - 1);
142127
}
143128
}

0 commit comments

Comments
 (0)