mirror of
https://github.com/nextcloud/server.git
synced 2024-11-14 20:36:50 +00:00
bb598c8451
Bumps [nextcloud/coding-standard](https://github.com/nextcloud/coding-standard) from 1.3.1 to 1.3.2. - [Release notes](https://github.com/nextcloud/coding-standard/releases) - [Changelog](https://github.com/nextcloud/coding-standard/blob/master/CHANGELOG.md) - [Commits](https://github.com/nextcloud/coding-standard/compare/v1.3.1...v1.3.2) --- updated-dependencies: - dependency-name: nextcloud/coding-standard dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: provokateurin <kate@provokateurin.de>
153 lines
5.1 KiB
PHP
153 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OC\DB\QueryBuilder\Sharded;
|
|
|
|
use OCP\ICacheFactory;
|
|
use OCP\IMemcache;
|
|
use OCP\IMemcacheTTL;
|
|
|
|
/**
|
|
* A helper to atomically determine the next auto increment value for a sharded table
|
|
*
|
|
* Since we can't use the database's auto-increment (since each db doesn't know about the keys in the other shards)
|
|
* we need external logic for doing the auto increment
|
|
*/
|
|
class AutoIncrementHandler {
|
|
public const MIN_VALID_KEY = 1000;
|
|
public const TTL = 365 * 24 * 60 * 60;
|
|
|
|
private ?IMemcache $cache = null;
|
|
|
|
public function __construct(
|
|
private ICacheFactory $cacheFactory,
|
|
private ShardConnectionManager $shardConnectionManager,
|
|
) {
|
|
if (PHP_INT_SIZE < 8) {
|
|
throw new \Exception('sharding is only supported with 64bit php');
|
|
}
|
|
}
|
|
|
|
private function getCache(): IMemcache {
|
|
if (is_null($this->cache)) {
|
|
$cache = $this->cacheFactory->createDistributed('shared_autoincrement');
|
|
if ($cache instanceof IMemcache) {
|
|
$this->cache = $cache;
|
|
} else {
|
|
throw new \Exception('Distributed cache ' . get_class($cache) . ' is not suitable');
|
|
}
|
|
}
|
|
return $this->cache;
|
|
}
|
|
|
|
/**
|
|
* Get the next value for the given shard definition
|
|
*
|
|
* The returned key is unique and incrementing, but not sequential.
|
|
* The shard id is encoded in the first byte of the returned value
|
|
*
|
|
* @param ShardDefinition $shardDefinition
|
|
* @return int
|
|
* @throws \Exception
|
|
*/
|
|
public function getNextPrimaryKey(ShardDefinition $shardDefinition, int $shard): int {
|
|
$retries = 0;
|
|
while ($retries < 5) {
|
|
$next = $this->getNextInner($shardDefinition);
|
|
if ($next !== null) {
|
|
if ($next > ShardDefinition::MAX_PRIMARY_KEY) {
|
|
throw new \Exception('Max primary key of ' . ShardDefinition::MAX_PRIMARY_KEY . ' exceeded');
|
|
}
|
|
// we encode the shard the primary key was originally inserted into to allow guessing the shard by primary key later on
|
|
return ($next << 8) | $shard;
|
|
} else {
|
|
$retries++;
|
|
}
|
|
}
|
|
throw new \Exception('Failed to get next primary key');
|
|
}
|
|
|
|
/**
|
|
* auto increment logic without retry
|
|
*
|
|
* @param ShardDefinition $shardDefinition
|
|
* @return int|null either the next primary key or null if the call needs to be retried
|
|
*/
|
|
private function getNextInner(ShardDefinition $shardDefinition): ?int {
|
|
$cache = $this->getCache();
|
|
// because this function will likely be called concurrently from different requests
|
|
// the implementation needs to ensure that the cached value can be cleared, invalidated or re-calculated at any point between our cache calls
|
|
// care must be taken that the logic remains fully resilient against race conditions
|
|
|
|
// in the ideal case, the last primary key is stored in the cache and we can just do an `inc`
|
|
// if that is not the case we find the highest used id in the database increment it, and save it in the cache
|
|
|
|
// prevent inc from returning `1` if the key doesn't exist by setting it to a non-numeric value
|
|
$cache->add($shardDefinition->table, 'empty-placeholder', self::TTL);
|
|
$next = $cache->inc($shardDefinition->table);
|
|
|
|
if ($cache instanceof IMemcacheTTL) {
|
|
$cache->setTTL($shardDefinition->table, self::TTL);
|
|
}
|
|
|
|
// the "add + inc" trick above isn't strictly atomic, so as a safety we reject any result that to small
|
|
// to handle the edge case of the stored value disappearing between the add and inc
|
|
if (is_int($next) && $next >= self::MIN_VALID_KEY) {
|
|
return $next;
|
|
} elseif (is_int($next)) {
|
|
// we hit the edge case, so invalidate the cached value
|
|
if (!$cache->cas($shardDefinition->table, $next, 'empty-placeholder')) {
|
|
// someone else is changing the value concurrently, give up and retry
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// discard the encoded initial shard
|
|
$current = $this->getMaxFromDb($shardDefinition) >> 8;
|
|
$next = max($current, self::MIN_VALID_KEY) + 1;
|
|
if ($cache->cas($shardDefinition->table, 'empty-placeholder', $next)) {
|
|
return $next;
|
|
}
|
|
|
|
// another request set the cached value before us, so we should just be able to inc
|
|
$next = $cache->inc($shardDefinition->table);
|
|
if (is_int($next) && $next >= self::MIN_VALID_KEY) {
|
|
return $next;
|
|
} elseif (is_int($next)) {
|
|
// key got cleared, invalidate and retry
|
|
$cache->cas($shardDefinition->table, $next, 'empty-placeholder');
|
|
return null;
|
|
} else {
|
|
// cleanup any non-numeric value other than the placeholder if that got stored somehow
|
|
$cache->ncad($shardDefinition->table, 'empty-placeholder');
|
|
// retry
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the maximum primary key value from the shards
|
|
*/
|
|
private function getMaxFromDb(ShardDefinition $shardDefinition): int {
|
|
$max = 0;
|
|
foreach ($shardDefinition->getAllShards() as $shard) {
|
|
$connection = $this->shardConnectionManager->getConnection($shardDefinition, $shard);
|
|
$query = $connection->getQueryBuilder();
|
|
$query->select($shardDefinition->primaryKey)
|
|
->from($shardDefinition->table)
|
|
->orderBy($shardDefinition->primaryKey, 'DESC')
|
|
->setMaxResults(1);
|
|
$result = $query->executeQuery()->fetchOne();
|
|
if ($result) {
|
|
$max = max($max, $result);
|
|
}
|
|
}
|
|
return $max;
|
|
}
|
|
}
|