0
0
mirror of https://github.com/nextcloud/server.git synced 2024-11-14 20:36:50 +00:00
nextcloud_server/lib/private/DB/QueryBuilder/Sharded/AutoIncrementHandler.php
dependabot[bot] bb598c8451
chore(deps): Bump nextcloud/coding-standard in /vendor-bin/cs-fixer
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>
2024-10-19 07:57:35 +02:00

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;
}
}