Skip to content

Commit 5a48b15

Browse files
committed
feat: implement sharded auto-increment for primary keys and encode the initial shard in the primary key
Signed-off-by: Robin Appelman <robin@icewind.nl>
1 parent 87d8727 commit 5a48b15

File tree

7 files changed

+53
-34
lines changed

7 files changed

+53
-34
lines changed

lib/private/DB/Connection.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use OC\DB\QueryBuilder\Partitioned\PartitionedQueryBuilder;
2727
use OC\DB\QueryBuilder\Partitioned\PartitionSplit;
2828
use OC\DB\QueryBuilder\QueryBuilder;
29+
use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler;
2930
use OC\DB\QueryBuilder\Sharded\CrossShardMoveHelper;
3031
use OC\DB\QueryBuilder\Sharded\RoundRobinShardMapper;
3132
use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
@@ -87,6 +88,7 @@ class Connection extends PrimaryReadReplicaConnection {
8788
/** @var ShardDefinition[] */
8889
protected array $shards = [];
8990
protected ShardConnectionManager $shardConnectionManager;
91+
protected AutoIncrementHandler $autoIncrementHandler;
9092

9193
const SHARD_PRESETS = [
9294
'filecache' => [
@@ -128,6 +130,7 @@ public function __construct(
128130
$this->tablePrefix = $params['tablePrefix'];
129131

130132
$this->shardConnectionManager = $this->params['shard_connection_manager'] ?? Server::get(ShardConnectionManager::class);
133+
$this->autoIncrementHandler = Server::get(AutoIncrementHandler::class);
131134
$this->systemConfig = \OC::$server->getSystemConfig();
132135
$this->clock = Server::get(ClockInterface::class);
133136
$this->logger = Server::get(LoggerInterface::class);
@@ -248,6 +251,7 @@ public function getQueryBuilder(): IQueryBuilder {
248251
$builder,
249252
$this->shards,
250253
$this->shardConnectionManager,
254+
$this->autoIncrementHandler,
251255
);
252256
foreach ($this->partitions as $name => $tables) {
253257
$partition = new PartitionSplit($name, $tables);

lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
use OC\DB\QueryBuilder\CompositeExpression;
2727
use OC\DB\QueryBuilder\QuoteHelper;
28+
use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler;
2829
use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
2930
use OC\DB\QueryBuilder\Sharded\ShardedQueryBuilder;
3031
use OCP\DB\IResult;
@@ -46,7 +47,7 @@
4647
*
4748
* For example:
4849
* ```
49-
* $query->select("mount_point", "mimetype")
50+
* $query->select("mount_point", "mimetype")
5051
* ->from("mounts", "m")
5152
* ->innerJoin("m", "filecache", "f", $query->expr()->eq("root_id", "fileid"));
5253
* ```
@@ -114,11 +115,12 @@ class PartitionedQueryBuilder extends ShardedQueryBuilder {
114115
private QuoteHelper $quoteHelper;
115116

116117
public function __construct(
117-
IQueryBuilder $builder,
118-
private array $shardDefinitions,
119-
private ShardConnectionManager $shardConnectionManager,
118+
IQueryBuilder $builder,
119+
array $shardDefinitions,
120+
ShardConnectionManager $shardConnectionManager,
121+
AutoIncrementHandler $autoIncrementHandler,
120122
) {
121-
parent::__construct($builder, $this->shardDefinitions, $this->shardConnectionManager);
123+
parent::__construct($builder, $shardDefinitions, $shardConnectionManager, $autoIncrementHandler);
122124
$this->quoteHelper = new QuoteHelper();
123125
}
124126

@@ -132,6 +134,7 @@ private function newQuery(): IQueryBuilder {
132134
$builder,
133135
$this->shardDefinitions,
134136
$this->shardConnectionManager,
137+
$this->autoIncrementHandler,
135138
);
136139
}
137140

@@ -177,8 +180,8 @@ private function applySelects(): void {
177180
foreach ($this->selects as $select) {
178181
foreach ($this->partitions as $partition) {
179182
if (is_string($select['select']) && (
180-
$select['select'] === '*' ||
181-
$partition->isColumnInPartition($select['select']))
183+
$select['select'] === '*' ||
184+
$partition->isColumnInPartition($select['select']))
182185
) {
183186
if (isset($this->splitQueries[$partition->name])) {
184187
if ($select['alias']) {

lib/private/DB/QueryBuilder/Sharded/AutoIncrementHandler.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@ public function __construct(
2525
ICacheFactory $cacheFactory,
2626
private ShardConnectionManager $shardConnectionManager,
2727
) {
28+
if (PHP_INT_SIZE < 8) {
29+
throw new \Exception("sharding is only supported with 64bit php");
30+
}
31+
2832
$cache = $cacheFactory->createDistributed("shared_autoincrement");
2933
if ($cache instanceof IMemcache) {
3034
$this->cache = $cache;
3135
} else {
32-
throw new \Exception('Distributed cache ' . get_class($cache) . ' does not suitable');
36+
throw new \Exception('Distributed cache ' . get_class($cache) . ' is not suitable');
3337
}
3438
}
3539

@@ -38,6 +42,9 @@ public function getNextPrimaryKey(ShardDefinition $shardDefinition): int {
3842
while ($retries < 5) {
3943
$next = $this->getNextPrimaryKeyInner($shardDefinition);
4044
if ($next !== null) {
45+
if ($next > ShardDefinition::MAX_PRIMARY_KEY) {
46+
throw new \Exception("Max primary key of " . ShardDefinition::MAX_PRIMARY_KEY . " exceeded");
47+
}
4148
return $next;
4249
} else {
4350
$retries++;
@@ -50,7 +57,7 @@ public function getNextPrimaryKey(ShardDefinition $shardDefinition): int {
5057
* @param ShardDefinition $shardDefinition
5158
* @return int|null either the next primary key or null if the call needs to be retried
5259
*/
53-
private function getNextPrimaryKeyInner(ShardDefinition $shardDefinition): int|null {
60+
private function getNextPrimaryKeyInner(ShardDefinition $shardDefinition): ?int {
5461
// because this function will likely be called concurrently from different requests
5562
// the implementation needs to ensure that the cached value can be cleared, invalidated or re-calculated at any point between our cache calls
5663
// care must be taken that the logic remains fully resilient against race conditions
@@ -75,7 +82,8 @@ private function getNextPrimaryKeyInner(ShardDefinition $shardDefinition): int|n
7582
}
7683
}
7784

78-
$current = $this->getMaxFromDb($shardDefinition);
85+
// discard the encoded initial shard
86+
$current = $this->getMaxFromDb($shardDefinition) & ShardDefinition::PRIMARY_KEY_MASK;
7987
$next = max($current, self::MIN_VALID_KEY) + 1;
8088
if ($this->cache->cas($shardDefinition->table, "empty-placeholder", $next)) {
8189
return $next;

lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@
1414
* Keeps the configuration for a shard setup
1515
*/
1616
class ShardDefinition {
17+
// we reserve the top byte of the primary key for the initial shard
18+
// but since php doesn't have unsigned integers, we loose the top bit to the sign
19+
// so we can only encode 127 shards in the top byte
20+
public const MAX_SHARDS = 127;
21+
22+
const PRIMARY_KEY_MASK = 0x00_FF_FF_FF_FF_FF_FF_FF;
23+
const PRIMARY_KEY_SHARD_MASK = 0x7F_00_00_00_00_00_00_00;
24+
const MAX_PRIMARY_KEY = PHP_INT_MAX & self::PRIMARY_KEY_MASK;
25+
1726
/**
1827
* @param string $table
1928
* @param string $primaryKey
@@ -32,6 +41,9 @@ public function __construct(
3241
public array $companionTables = [],
3342
public array $shards = [],
3443
) {
44+
if (count($this->shards) >= self::MAX_SHARDS) {
45+
throw new \Exception("Only allowed maximum of " . self::MAX_SHARDS . " shards allowed");
46+
}
3547
}
3648

3749
public function hasTable(string $table): bool {

lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ class ShardedQueryBuilder extends ExtendedQueryBuilder {
2828

2929
public function __construct(
3030
IQueryBuilder $builder,
31-
private array $shardDefinitions,
32-
private ShardConnectionManager $shardConnectionManager,
31+
protected array $shardDefinitions,
32+
protected ShardConnectionManager $shardConnectionManager,
33+
protected AutoIncrementHandler $autoIncrementHandler,
3334
) {
3435
parent::__construct($builder);
3536
}
@@ -309,25 +310,11 @@ public function executeStatement(?IDBConnection $connection = null): int {
309310
foreach ($shards as $shard) {
310311
$shardConnection = $this->shardConnectionManager->getConnection($this->shardDefinition, $shard);
311312
if (!$this->primaryKeys && $this->shardDefinition->table === $this->insertTable) {
312-
// todo: is random primary key fine, or do we need to do shared-autoincrement
313-
/**
314-
* atomic autoincrement:
315-
*
316-
* $next = $cache->inc('..');
317-
* if (!$next) {
318-
* $last = $this->getMaxValue();
319-
* $success = $cache->add('..', $last + 1);
320-
* if ($success) {
321-
* return $last + 1;
322-
* } else {
323-
* / somebody else set it
324-
* return $cache->inc('..');
325-
* }
326-
* } else {
327-
* return $next
328-
* }
329-
*/
330-
$id = random_int(0, PHP_INT_MAX);
313+
$rawId = $this->autoIncrementHandler->getNextPrimaryKey($this->shardDefinition);
314+
315+
// we encode the shard the primary key was originally inserted into to allow guessing the shard by primary key later on
316+
$encodedShard = $shard << 56;
317+
$id = $rawId | $encodedShard;
331318
parent::setValue($this->shardDefinition->primaryKey, $this->createParameter('__generated_primary_key'));
332319
$this->setParameter('__generated_primary_key', $id, self::PARAM_INT);
333320
$this->lastInsertId = $id;

tests/lib/DB/QueryBuilder/Partitioned/PartitionedQueryBuilderTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use OC\DB\QueryBuilder\Partitioned\PartitionSplit;
1212
use OC\DB\QueryBuilder\Partitioned\PartitionedQueryBuilder;
13+
use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler;
1314
use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
1415
use OCP\DB\QueryBuilder\IQueryBuilder;
1516
use OCP\IDBConnection;
@@ -22,10 +23,12 @@
2223
class PartitionedQueryBuilderTest extends TestCase {
2324
private IDBConnection $connection;
2425
private ShardConnectionManager $shardConnectionManager;
26+
private AutoIncrementHandler $autoIncrementHandler;
2527

2628
protected function setUp(): void {
2729
$this->connection = Server::get(IDBConnection::class);
2830
$this->shardConnectionManager = Server::get(ShardConnectionManager::class);
31+
$this->autoIncrementHandler = Server::get(AutoIncrementHandler::class);
2932

3033
$this->setupFileCache();
3134
}
@@ -41,7 +44,7 @@ private function getQueryBuilder(): PartitionedQueryBuilder {
4144
if ($builder instanceof PartitionedQueryBuilder) {
4245
return $builder;
4346
} else {
44-
return new PartitionedQueryBuilder($builder, [], $this->shardConnectionManager);
47+
return new PartitionedQueryBuilder($builder, [], $this->shardConnectionManager, $this->autoIncrementHandler);
4548
}
4649
}
4750

tests/lib/DB/QueryBuilder/Sharded/SharedQueryBuilderTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,27 @@
88

99
namespace Test\DB\QueryBuilder\Sharded;
1010

11+
use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler;
1112
use OC\DB\QueryBuilder\Sharded\InvalidShardedQueryException;
1213
use OC\DB\QueryBuilder\Sharded\RoundRobinShardMapper;
1314
use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
1415
use OC\DB\QueryBuilder\Sharded\ShardDefinition;
1516
use OC\DB\QueryBuilder\Sharded\ShardedQueryBuilder;
16-
use OC\SystemConfig;
1717
use OCP\DB\QueryBuilder\IQueryBuilder;
1818
use OCP\IDBConnection;
1919
use OCP\Server;
20-
use Psr\Log\LoggerInterface;
2120
use Test\TestCase;
2221

2322
/**
2423
* @group DB
2524
*/
2625
class SharedQueryBuilderTest extends TestCase {
2726
private IDBConnection $connection;
27+
private AutoIncrementHandler $autoIncrementHandler;
2828

2929
protected function setUp(): void {
3030
$this->connection = Server::get(IDBConnection::class);
31+
$this->autoIncrementHandler = Server::get(AutoIncrementHandler::class);
3132
}
3233

3334

@@ -38,6 +39,7 @@ private function getQueryBuilder(string $table, string $shardColumn, string $pri
3839
new ShardDefinition($table, $primaryColumn, [], $shardColumn, new RoundRobinShardMapper(), $companionTables, []),
3940
],
4041
$this->createMock(ShardConnectionManager::class),
42+
$this->autoIncrementHandler,
4143
);
4244
}
4345

0 commit comments

Comments
 (0)