2020-07-10 00:58:00 +01:00
|
|
|
<?php
|
|
|
|
|
2021-10-10 09:26:18 +01:00
|
|
|
declare(strict_types = 1);
|
|
|
|
|
2020-07-10 00:58:00 +01:00
|
|
|
// {{{ License
|
2020-11-06 19:47:15 +00:00
|
|
|
|
2020-07-10 00:58:00 +01:00
|
|
|
// This file is part of GNU social - https://www.gnu.org/software/social
|
|
|
|
//
|
|
|
|
// GNU social is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// GNU social is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
2020-11-06 19:47:15 +00:00
|
|
|
|
2020-07-10 00:58:00 +01:00
|
|
|
// }}}
|
|
|
|
|
2020-07-10 14:13:53 +01:00
|
|
|
namespace App\Core;
|
2020-07-10 00:58:00 +01:00
|
|
|
|
2021-09-21 16:28:54 +01:00
|
|
|
use App\Entity\Actor;
|
|
|
|
use App\Entity\LocalUser;
|
2021-09-21 15:35:07 +01:00
|
|
|
use App\Entity\Note;
|
2020-10-06 21:22:54 +01:00
|
|
|
use App\Util\Common;
|
2021-04-01 23:26:17 +01:00
|
|
|
use App\Util\Exception\ConfigurationException;
|
2021-11-25 23:08:30 +00:00
|
|
|
use App\Util\Exception\NotImplementedException;
|
2020-07-10 00:58:00 +01:00
|
|
|
use Functional as F;
|
2021-10-10 09:26:18 +01:00
|
|
|
use InvalidArgumentException;
|
2022-10-19 22:38:49 +01:00
|
|
|
use Memcached;
|
2020-07-17 23:58:00 +01:00
|
|
|
use Redis;
|
|
|
|
use RedisCluster;
|
|
|
|
use Symfony\Component\Cache\Adapter;
|
2020-07-10 00:58:00 +01:00
|
|
|
use Symfony\Component\Cache\Adapter\ChainAdapter;
|
2021-11-25 23:08:30 +00:00
|
|
|
use Symfony\Component\Cache\CacheItem;
|
2020-07-10 00:58:00 +01:00
|
|
|
|
|
|
|
abstract class Cache
|
|
|
|
{
|
2020-07-16 01:04:25 +01:00
|
|
|
protected static $pools;
|
|
|
|
protected static $redis;
|
2020-07-10 00:58:00 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Configure a cache pool, with adapters taken from `ENV_VAR`.
|
|
|
|
* We may want multiple of these in the future, but for now it seems
|
|
|
|
* unnecessary
|
|
|
|
*/
|
2020-07-11 23:43:05 +01:00
|
|
|
public static function setupCache()
|
2020-07-10 00:58:00 +01:00
|
|
|
{
|
2020-07-16 01:04:25 +01:00
|
|
|
self::$pools = [];
|
2021-04-01 23:26:17 +01:00
|
|
|
self::$redis = null;
|
2020-07-16 01:04:25 +01:00
|
|
|
|
2020-07-11 23:43:05 +01:00
|
|
|
$adapters = [];
|
2020-10-06 21:22:54 +01:00
|
|
|
foreach (Common::config('cache', 'adapters') as $pool => $val) {
|
2020-07-16 01:04:25 +01:00
|
|
|
self::$pools[$pool] = [];
|
|
|
|
self::$redis[$pool] = [];
|
2020-07-11 23:43:05 +01:00
|
|
|
foreach (explode(',', $val) as $dsn) {
|
2021-07-20 15:06:29 +01:00
|
|
|
if (str_contains($dsn, '://')) {
|
|
|
|
[$scheme, $rest] = explode('://', $dsn);
|
|
|
|
} else {
|
|
|
|
$scheme = $dsn;
|
|
|
|
$rest = '';
|
|
|
|
}
|
2020-07-11 23:43:05 +01:00
|
|
|
switch ($scheme) {
|
2022-10-19 22:38:49 +01:00
|
|
|
case 'redis':
|
|
|
|
// Redis can have multiple servers, but we want to take proper advantage of
|
|
|
|
// redis, not just as a key value store, but using it's datastructures
|
|
|
|
$dsns = explode(';', $dsn);
|
|
|
|
if (\count($dsns) === 1) {
|
|
|
|
$class = Redis::class;
|
|
|
|
$r = new Redis();
|
|
|
|
$r->pconnect($rest);
|
|
|
|
} else {
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
// This requires extra server configuration, but the code was tested
|
|
|
|
// manually and works, so it'll be excluded from automatic tests, for now, at least
|
|
|
|
if (F\Every($dsns, function ($str) { [$scheme, $rest] = explode('://', $str);
|
|
|
|
return str_contains($rest, ':'); }) == false) {
|
|
|
|
throw new ConfigurationException('The configuration of a redis cluster requires specifying the ports to use');
|
|
|
|
}
|
|
|
|
$class = RedisCluster::class; // true for persistent connection
|
|
|
|
$seeds = F\Map($dsns, fn ($str) => explode('://', $str)[1]);
|
|
|
|
$r = new RedisCluster(name: null, seeds: $seeds, timeout: 0.0, readTimeout: 0.0, persistent: true);
|
|
|
|
// Distribute reads randomly
|
|
|
|
$r->setOption($class::OPT_SLAVE_FAILOVER, $class::FAILOVER_DISTRIBUTE);
|
|
|
|
// @codeCoverageIgnoreEnd
|
2021-04-01 23:26:17 +01:00
|
|
|
}
|
2022-10-19 22:38:49 +01:00
|
|
|
// Improved serializer
|
|
|
|
$r->setOption($class::OPT_SERIALIZER, $class::SERIALIZER_MSGPACK);
|
|
|
|
// Persistent connection
|
|
|
|
$r->setOption($class::OPT_TCP_KEEPALIVE, true);
|
|
|
|
// Use LZ4 for the improved decompression speed (while keeping an okay compression ratio)
|
|
|
|
$r->setOption($class::OPT_COMPRESSION, $class::COMPRESSION_LZ4);
|
|
|
|
self::$redis[$pool] = $r;
|
|
|
|
$adapters[$pool][] = new Adapter\RedisAdapter($r);
|
|
|
|
break;
|
|
|
|
case 'memcached':
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
// These all are excluded from automatic testing, as they require an unreasonable amount
|
|
|
|
// of configuration in the testing environment. The code is really simple, so it should work
|
|
|
|
// memcached can also have multiple servers
|
|
|
|
// host:port:weight?
|
|
|
|
$dsns = explode(';', $dsn);
|
|
|
|
$servers = F\map($dsns, fn ($dsn) => explode(':', $dsn));
|
|
|
|
$memcached = new Memcached();
|
|
|
|
$memcached->addServers($servers);
|
|
|
|
$adapters[$pool][] = new Adapter\MemcachedAdapter($memcached);
|
|
|
|
break;
|
|
|
|
case 'filesystem':
|
|
|
|
$adapters[$pool][] = new Adapter\FilesystemAdapter($rest);
|
|
|
|
break;
|
|
|
|
case 'apcu':
|
|
|
|
$adapters[$pool][] = new Adapter\ApcuAdapter();
|
|
|
|
break;
|
|
|
|
case 'opcache':
|
|
|
|
$adapters[$pool][] = new Adapter\PhpArrayAdapter($rest, new Adapter\FilesystemAdapter($rest . '.fallback'));
|
|
|
|
break;
|
|
|
|
case 'doctrine':
|
|
|
|
$adapters[$pool][] = new Adapter\PdoAdapter($dsn);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
Log::error("Unknown or discouraged cache scheme '{$scheme}'");
|
|
|
|
return;
|
2021-07-20 15:06:29 +01:00
|
|
|
// @codeCoverageIgnoreEnd
|
2020-07-11 23:43:05 +01:00
|
|
|
}
|
|
|
|
}
|
2020-07-16 01:04:25 +01:00
|
|
|
|
2021-04-01 23:26:17 +01:00
|
|
|
if (self::$redis[$pool] == null) {
|
|
|
|
unset(self::$redis[$pool]);
|
|
|
|
}
|
|
|
|
|
2021-10-10 09:26:18 +01:00
|
|
|
if (\count($adapters[$pool]) === 1) {
|
2020-07-11 23:43:05 +01:00
|
|
|
self::$pools[$pool] = array_pop($adapters[$pool]);
|
|
|
|
} else {
|
2021-07-20 15:06:29 +01:00
|
|
|
self::$pools[$pool] = new ChainAdapter($adapters[$pool]);
|
2020-07-11 23:43:05 +01:00
|
|
|
}
|
2020-07-10 00:58:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-16 10:44:23 +00:00
|
|
|
/**
|
|
|
|
* Worker function for doing redis requests that may need to be recomputed. Given a key, it
|
|
|
|
* either uses the configuration value or the object lifetime to statistically determine if a
|
|
|
|
* cache entry specified by $key should be recomputed, to avoid cache stampedes (when a lot of keys expire at once)
|
|
|
|
*/
|
2021-12-12 17:02:16 +00:00
|
|
|
private static function redisMaybeRecompute(string $key, callable $recompute, callable $no_recompute, string $pool = 'default', float $beta = 1.0): mixed
|
|
|
|
{
|
|
|
|
$should_recompute = $beta === \INF || !self::$redis[$pool]->exists($key);
|
|
|
|
if (!$should_recompute) {
|
|
|
|
$er = Common::config('cache', 'early_recompute');
|
|
|
|
if (\is_float($er)) {
|
|
|
|
if ($should_recompute = (mt_rand() / mt_getrandmax() > $er)) {
|
|
|
|
Log::info('Item "{key}" elected for early recomputation', ['key' => $key]);
|
|
|
|
}
|
|
|
|
} elseif ($er === true) {
|
|
|
|
if ($should_recompute = ($idletime = self::$redis[$pool]->object('idletime', $key) ?? false) && ($expiry = self::$redis[$pool]->ttl($key) ?? false) && $expiry <= $idletime / 1000 * $beta * log(random_int(1, \PHP_INT_MAX) / \PHP_INT_MAX)) {
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
Log::info('Item "{key}" elected for early recomputation {delta}s before its expiration', [
|
|
|
|
'key' => $key,
|
|
|
|
'delta' => sprintf('%.1f', $expiry - microtime(true)),
|
|
|
|
]);
|
|
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$should_recompute = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($should_recompute) {
|
|
|
|
return $recompute();
|
|
|
|
} else {
|
|
|
|
return $no_recompute();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-04 18:53:35 +00:00
|
|
|
public static function incr(string $key, string $pool = 'default')
|
|
|
|
{
|
|
|
|
if (isset(self::$redis[$pool])) {
|
|
|
|
return self::$redis[$pool]->incr($key);
|
|
|
|
} else {
|
|
|
|
return self::$pools[$pool]->set($key, 1 + self::$pools[$pool]->get($key, fn ($i) => 0), \INF);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-16 01:04:25 +01:00
|
|
|
public static function set(string $key, mixed $value, string $pool = 'default')
|
2020-07-10 14:13:53 +01:00
|
|
|
{
|
2021-12-12 17:02:16 +00:00
|
|
|
if (isset(self::$redis[$pool])) {
|
|
|
|
return self::$redis[$pool]->set($key, $value);
|
|
|
|
} else {
|
|
|
|
// there's no set method, must be done this way
|
|
|
|
return self::$pools[$pool]->get($key, fn ($i) => $value, \INF);
|
|
|
|
}
|
2020-07-10 14:13:53 +01:00
|
|
|
}
|
|
|
|
|
2021-12-26 22:17:26 +00:00
|
|
|
/**
|
|
|
|
* @param callable(mixed, bool &$save): mixed $calculate
|
|
|
|
*/
|
2020-07-16 01:04:25 +01:00
|
|
|
public static function get(string $key, callable $calculate, string $pool = 'default', float $beta = 1.0)
|
2020-07-10 00:58:00 +01:00
|
|
|
{
|
2021-12-12 17:02:16 +00:00
|
|
|
if (isset(self::$redis[$pool])) {
|
|
|
|
return self::redisMaybeRecompute(
|
|
|
|
$key,
|
|
|
|
recompute: function () use ($key, $calculate, $pool) {
|
|
|
|
$save = true; // Pass by reference
|
2022-10-19 22:38:49 +01:00
|
|
|
$res = $calculate(null, $save);
|
2021-12-12 17:02:16 +00:00
|
|
|
if ($save) {
|
|
|
|
self::set($key, $res, $pool);
|
|
|
|
}
|
|
|
|
return $res;
|
|
|
|
},
|
|
|
|
no_recompute: fn () => self::$redis[$pool]->get($key),
|
|
|
|
pool: $pool,
|
|
|
|
beta: $beta,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return self::$pools[$pool]->get($key, $calculate, $beta);
|
|
|
|
}
|
2020-07-10 14:13:53 +01:00
|
|
|
}
|
|
|
|
|
2020-07-16 01:04:25 +01:00
|
|
|
public static function delete(string $key, string $pool = 'default'): bool
|
2020-07-10 17:41:26 +01:00
|
|
|
{
|
2021-12-12 17:02:16 +00:00
|
|
|
if (isset(self::$redis[$pool])) {
|
2021-12-16 11:01:23 +00:00
|
|
|
return self::$redis[$pool]->del($key) === 1;
|
2021-12-12 17:02:16 +00:00
|
|
|
} else {
|
|
|
|
return self::$pools[$pool]->delete($key);
|
|
|
|
}
|
2020-07-16 01:04:25 +01:00
|
|
|
}
|
2020-07-10 17:41:26 +01:00
|
|
|
|
2021-11-26 17:11:28 +00:00
|
|
|
public static function exists(string $key, string $pool = 'default'): bool
|
|
|
|
{
|
2021-12-12 17:02:16 +00:00
|
|
|
if (isset(self::$redis[$pool])) {
|
2022-02-27 00:29:26 +00:00
|
|
|
return self::$redis[$pool]->exists($key) === 1;
|
2021-12-12 17:02:16 +00:00
|
|
|
} else {
|
|
|
|
// there's no set method, must be done this way
|
|
|
|
return self::$pools[$pool]->hasItem($key);
|
|
|
|
}
|
2021-11-26 17:11:28 +00:00
|
|
|
}
|
|
|
|
|
2020-11-06 19:47:15 +00:00
|
|
|
/**
|
|
|
|
* Retrieve a list from the cache, with a different implementation
|
|
|
|
* for redis and others, trimming to $max_count if given
|
2022-01-04 23:13:41 +00:00
|
|
|
* TODO(hugo): $calculate = [] is the same as false miss
|
2021-11-08 15:05:12 +00:00
|
|
|
*
|
2022-02-27 00:29:26 +00:00
|
|
|
* @param string $key Cache key
|
2021-11-25 23:08:30 +00:00
|
|
|
* @param callable(?CacheItem $item, bool &$save): (string|object|array<int,mixed>) $calculate
|
2022-02-27 00:29:26 +00:00
|
|
|
* @param string $pool Cache pool being used (between different cache managers (of the same or different systems))
|
|
|
|
* @param null|int $max_count When setting cache, it trims to max count
|
|
|
|
* @param null|int $left Offset on Get
|
|
|
|
* @param null|int $right Limit on Get
|
|
|
|
* @param float $beta Likelihood of recomputing value (1 to infinity)
|
2020-11-06 19:47:15 +00:00
|
|
|
*/
|
2021-09-21 15:35:07 +01:00
|
|
|
public static function getList(string $key, callable $calculate, string $pool = 'default', ?int $max_count = null, ?int $left = null, ?int $right = null, float $beta = 1.0): array
|
2020-07-16 01:04:25 +01:00
|
|
|
{
|
|
|
|
if (isset(self::$redis[$pool])) {
|
2022-03-01 11:07:21 +00:00
|
|
|
return self::redisMaybeRecompute(
|
2021-12-12 17:02:16 +00:00
|
|
|
$key,
|
2021-12-16 10:44:23 +00:00
|
|
|
recompute: /**
|
|
|
|
* Caculate and trim the list to the correct size
|
|
|
|
*/
|
|
|
|
function () use ($key, $calculate, $pool, $max_count, $left, $right, $beta) {
|
2021-12-12 17:02:16 +00:00
|
|
|
$save = true; // Pass by reference
|
2022-10-19 22:38:49 +01:00
|
|
|
$res = $calculate(null, $save);
|
2021-12-12 17:02:16 +00:00
|
|
|
if ($save) {
|
|
|
|
self::setList($key, $res, $pool, $max_count, $beta);
|
2020-08-28 21:14:31 +01:00
|
|
|
}
|
2021-10-28 18:05:28 +01:00
|
|
|
$offset = $left ?? 0;
|
|
|
|
if (\is_null($right) && \is_null($max_count)) {
|
|
|
|
$length = null;
|
|
|
|
} else {
|
|
|
|
$length = ($right ?? $max_count) - $offset;
|
|
|
|
}
|
|
|
|
return \array_slice($res, $offset, $length);
|
2021-12-12 17:02:16 +00:00
|
|
|
},
|
2021-12-16 10:44:23 +00:00
|
|
|
no_recompute: /**
|
|
|
|
* Fetch (a portion of) the list from the cache
|
|
|
|
*/
|
2022-03-01 11:07:21 +00:00
|
|
|
function () use ($pool, $key, $left, $right, $max_count) {
|
|
|
|
$res = self::$redis[$pool]->lRange($key, $left ?? 0, ($right ?? $max_count ?? 0) - 1);
|
|
|
|
if ($res === false) {
|
|
|
|
// Empty lists are not supported by redis, but MSGPACK stores them as a
|
|
|
|
// string, so check if they exist
|
|
|
|
return self::$redis[$pool]->get($key);
|
|
|
|
} else {
|
|
|
|
return $res;
|
|
|
|
}
|
|
|
|
},
|
2021-12-12 17:02:16 +00:00
|
|
|
pool: $pool,
|
|
|
|
beta: $beta,
|
|
|
|
);
|
2020-07-16 01:04:25 +01:00
|
|
|
} else {
|
2021-12-16 10:44:23 +00:00
|
|
|
return self::get(
|
|
|
|
$key,
|
|
|
|
/**
|
|
|
|
* Fetch the list from the cache and possibly trim the length
|
|
|
|
*/
|
|
|
|
function () use ($calculate, $max_count) {
|
|
|
|
$save = true;
|
2022-10-19 22:38:49 +01:00
|
|
|
$res = $calculate(null, $save);
|
2021-12-16 10:44:23 +00:00
|
|
|
if ($max_count != -1) {
|
|
|
|
$res = \array_slice($res, 0, $max_count);
|
|
|
|
}
|
|
|
|
return $res;
|
|
|
|
},
|
|
|
|
$pool,
|
|
|
|
$beta,
|
|
|
|
);
|
2020-07-10 17:41:26 +01:00
|
|
|
}
|
2020-07-16 01:04:25 +01:00
|
|
|
}
|
2020-07-10 17:41:26 +01:00
|
|
|
|
2020-11-06 19:47:15 +00:00
|
|
|
/**
|
2021-04-01 23:26:17 +01:00
|
|
|
* Set the list
|
2020-11-06 19:47:15 +00:00
|
|
|
*/
|
2021-04-01 23:26:17 +01:00
|
|
|
public static function setList(string $key, array $value, string $pool = 'default', ?int $max_count = null, float $beta = 1.0): void
|
2020-07-16 01:04:25 +01:00
|
|
|
{
|
|
|
|
if (isset(self::$redis[$pool])) {
|
2021-10-28 18:05:28 +01:00
|
|
|
if (empty($value)) {
|
2022-01-08 17:13:08 +00:00
|
|
|
self::$redis[$pool]->set($key, []);
|
2021-10-28 18:05:28 +01:00
|
|
|
} else {
|
|
|
|
self::$redis[$pool] // Ensure atomic
|
|
|
|
->multi(Redis::MULTI)
|
|
|
|
->del($key)
|
|
|
|
->rPush($key, ...$value)
|
|
|
|
// trim to $max_count, unless it's 0
|
|
|
|
->lTrim($key, 0, $max_count ?? -1)
|
|
|
|
->exec();
|
|
|
|
}
|
2020-07-16 01:04:25 +01:00
|
|
|
} else {
|
2021-10-28 18:05:28 +01:00
|
|
|
self::set($key, \array_slice($value, 0, $max_count), $pool);
|
2020-07-16 01:04:25 +01:00
|
|
|
}
|
2020-07-10 17:41:26 +01:00
|
|
|
}
|
|
|
|
|
2022-02-27 21:33:24 +00:00
|
|
|
/**
|
|
|
|
* Get a list partially from cache, partially from the DB
|
|
|
|
*
|
|
|
|
* Uses two fetches in the worst case, but the first is likely cached
|
|
|
|
*
|
|
|
|
* @param callable(int $limit, int $offset): (array<int,mixed>) $calculate
|
|
|
|
*/
|
|
|
|
public static function getListPartialCache(string $key, callable $calculate, int $max_cache_count, int $left, ?int $right = null, string $pool = 'default', float $beta = 1.0): array
|
|
|
|
{
|
|
|
|
if ($left < $max_cache_count) {
|
|
|
|
$middle = $max_cache_count;
|
|
|
|
$cached_portion = self::getList(
|
|
|
|
$key,
|
|
|
|
fn () => $calculate(limit: $max_cache_count, offset: 0),
|
|
|
|
pool: $pool,
|
|
|
|
max_count: $max_cache_count,
|
|
|
|
left: $left,
|
|
|
|
right: $right,
|
|
|
|
beta: $beta,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$middle = $left;
|
|
|
|
$cached_portion = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
$right ??= \PHP_INT_MAX;
|
|
|
|
$limit = $right - $middle;
|
|
|
|
if ($right > $max_cache_count) {
|
|
|
|
$non_cached_portion = $calculate(limit: $limit, offset: $middle);
|
|
|
|
} else {
|
|
|
|
$non_cached_portion = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
return [...$cached_portion, ...$non_cached_portion];
|
|
|
|
}
|
|
|
|
|
2020-11-06 19:47:15 +00:00
|
|
|
/**
|
2021-04-01 23:26:17 +01:00
|
|
|
* Push a value to the list
|
2020-11-06 19:47:15 +00:00
|
|
|
*/
|
2022-02-27 00:29:26 +00:00
|
|
|
public static function listPushLeft(string $key, mixed $value, string $pool = 'default', ?int $max_count = null, float $beta = 1.0): void
|
2020-07-10 17:41:26 +01:00
|
|
|
{
|
2020-07-16 01:04:25 +01:00
|
|
|
if (isset(self::$redis[$pool])) {
|
2022-03-01 11:07:21 +00:00
|
|
|
$res = self::$redis[$pool]
|
2021-04-01 23:26:17 +01:00
|
|
|
// doesn't need to be atomic, adding at one end, deleting at the other
|
|
|
|
->multi(Redis::PIPELINE)
|
2021-09-21 15:35:07 +01:00
|
|
|
->lPush($key, $value)
|
2021-04-01 23:26:17 +01:00
|
|
|
// trim to $max_count, if given
|
2021-10-28 18:05:28 +01:00
|
|
|
->lTrim($key, 0, ($max_count ?? 0) - 1)
|
2021-04-01 23:26:17 +01:00
|
|
|
->exec();
|
2022-03-01 11:07:21 +00:00
|
|
|
// $res has a bool per operation in the pipeline. False means it failed
|
|
|
|
if (\in_array(false, $res)) {
|
|
|
|
// Empty lists are stored as strings, so you can't push to it. Delete it and then push
|
|
|
|
self::$redis[$pool]->multi(Redis::PIPELINE)->del($key)->lPush($key, $value)->exec();
|
|
|
|
}
|
2020-07-16 01:04:25 +01:00
|
|
|
} else {
|
2021-10-28 18:05:28 +01:00
|
|
|
$res = self::get($key, fn () => [], $pool, $beta);
|
|
|
|
array_unshift($res, $value);
|
|
|
|
if (!\is_null($max_count)) {
|
|
|
|
$res = \array_slice($res, 0, $max_count); // Trim away the older values
|
2020-07-16 01:04:25 +01:00
|
|
|
}
|
2021-09-06 20:59:36 +01:00
|
|
|
self::set($key, $res, $pool);
|
2020-07-10 17:41:26 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-27 00:29:26 +00:00
|
|
|
/**
|
|
|
|
* Push a value to the list
|
|
|
|
*/
|
|
|
|
public static function listPushRight(string $key, mixed $value, string $pool = 'default', ?int $max_count = null, float $beta = 1.0): void
|
|
|
|
{
|
|
|
|
if (isset(self::$redis[$pool])) {
|
2022-03-01 11:07:21 +00:00
|
|
|
$res = self::$redis[$pool]
|
2022-02-27 00:29:26 +00:00
|
|
|
// doesn't need to be atomic, adding at one end, deleting at the other
|
|
|
|
->multi(Redis::PIPELINE)
|
|
|
|
->rPush($key, $value)
|
|
|
|
// trim to $max_count, if given
|
|
|
|
->lTrim($key, -($max_count ?? 0), -1)
|
|
|
|
->exec();
|
2022-03-01 11:07:21 +00:00
|
|
|
// $res has a bool per operation in the pipeline. False means it failed
|
|
|
|
if (\in_array(false, $res)) {
|
|
|
|
// Empty lists are stored as strings, so you can't push to it. Delete it and then push
|
|
|
|
self::$redis[$pool]->multi(Redis::PIPELINE)->del($key)->rPush($key, $value)->exec();
|
|
|
|
}
|
2022-02-27 00:29:26 +00:00
|
|
|
} else {
|
|
|
|
$res = self::get($key, fn () => [], $pool, $beta);
|
|
|
|
$res[] = $value;
|
|
|
|
if (!\is_null($max_count)) {
|
|
|
|
$res = \array_slice($res, -$max_count, null); // Trim away the older values
|
|
|
|
}
|
|
|
|
self::set($key, $res, $pool);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-06 19:47:15 +00:00
|
|
|
/**
|
2021-04-01 23:26:17 +01:00
|
|
|
* Delete a whole list at $key
|
2020-11-06 19:47:15 +00:00
|
|
|
*/
|
2021-04-01 23:26:17 +01:00
|
|
|
public static function deleteList(string $key, string $pool = 'default'): bool
|
2020-07-10 14:13:53 +01:00
|
|
|
{
|
2021-04-01 23:26:17 +01:00
|
|
|
if (isset(self::$redis[$pool])) {
|
2021-12-16 11:01:23 +00:00
|
|
|
return self::$redis[$pool]->del($key) === 1;
|
2021-04-01 23:26:17 +01:00
|
|
|
} else {
|
|
|
|
return self::delete($key, $pool);
|
|
|
|
}
|
2020-07-10 00:58:00 +01:00
|
|
|
}
|
2021-09-21 15:35:07 +01:00
|
|
|
|
2021-11-08 15:05:12 +00:00
|
|
|
/**
|
|
|
|
* Retrieve a hashmap from the cache, with a different implementation
|
|
|
|
* for redis and others. Different from lists, works with string map_keys
|
|
|
|
*
|
2021-11-25 23:08:30 +00:00
|
|
|
* @param callable(?CacheItem $item, bool &$save): (string|object|array<string,mixed>) $calculate
|
2022-10-19 22:38:49 +01:00
|
|
|
*
|
2021-11-08 15:05:12 +00:00
|
|
|
* @TODO cleanup
|
|
|
|
*/
|
|
|
|
public static function getHashMap(string $map_key, callable $calculate, string $pool = 'default', float $beta = 1.0): array
|
|
|
|
{
|
|
|
|
if (isset(self::$redis[$pool])) {
|
2021-12-12 17:02:16 +00:00
|
|
|
return self::redisMaybeRecompute(
|
|
|
|
$map_key,
|
|
|
|
recompute: function () use ($map_key, $calculate, $pool) {
|
|
|
|
$save = true; // Pass by reference
|
2022-10-19 22:38:49 +01:00
|
|
|
$res = $calculate(null, $save);
|
2021-12-12 17:02:16 +00:00
|
|
|
if ($save) {
|
|
|
|
self::setHashMap($map_key, $res, $pool);
|
2021-11-08 15:05:12 +00:00
|
|
|
}
|
|
|
|
return $res;
|
2021-12-12 17:02:16 +00:00
|
|
|
},
|
|
|
|
no_recompute: fn () => self::$redis[$pool]->hGetAll($map_key),
|
|
|
|
pool: $pool,
|
|
|
|
beta: $beta,
|
|
|
|
);
|
2021-11-08 15:05:12 +00:00
|
|
|
} else {
|
|
|
|
throw new NotImplementedException();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the list
|
|
|
|
*/
|
2021-11-24 15:47:49 +00:00
|
|
|
public static function setHashMap(string $map_key, array $value, string $pool = 'default'): void
|
2021-11-08 15:05:12 +00:00
|
|
|
{
|
|
|
|
if (isset(self::$redis[$pool])) {
|
|
|
|
if (empty($value)) {
|
|
|
|
self::$redis[$pool]->del($map_key); // Redis doesn't support empty lists
|
|
|
|
} else {
|
|
|
|
self::$redis[$pool] // Ensure atomic
|
|
|
|
->multi(Redis::MULTI)
|
|
|
|
->del($map_key);
|
|
|
|
foreach ($value as $field_key => $field_value) {
|
2021-11-24 15:47:49 +00:00
|
|
|
self::$redis[$pool]->hSet($map_key, (string) $field_key, $field_value);
|
2021-11-08 15:05:12 +00:00
|
|
|
}
|
|
|
|
self::$redis[$pool]->exec();
|
|
|
|
}
|
|
|
|
} else {
|
2021-11-25 23:08:30 +00:00
|
|
|
self::set($map_key, $value, $pool);
|
2021-11-08 15:05:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-25 23:16:04 +00:00
|
|
|
/**
|
|
|
|
* Fetch the value(s) for key $key in the hashmap identified by
|
|
|
|
* $map_key. If not found, use $calculate_map to calculate the
|
|
|
|
* _entire_ hashmap (not just those in $key)
|
|
|
|
*/
|
2021-11-24 15:47:49 +00:00
|
|
|
public static function getHashMapKey(string $map_key, string|array $key, callable $calculate_map, string $pool = 'default')
|
2021-11-08 15:05:12 +00:00
|
|
|
{
|
|
|
|
if (isset(self::$redis[$pool])) {
|
2021-11-24 15:47:49 +00:00
|
|
|
$get = function () use ($key, $map_key, $pool) {
|
|
|
|
if (\is_string($key)) {
|
|
|
|
return self::$redis[$pool]->hget($map_key, $key);
|
|
|
|
} else {
|
|
|
|
return self::$redis[$pool]->hmget($map_key, $key);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
$res = $get();
|
2021-11-28 11:16:10 +00:00
|
|
|
if (empty($res)) {
|
2021-11-24 15:47:49 +00:00
|
|
|
self::setHashMap($map_key, $calculate_map(), $pool);
|
|
|
|
$res = $get();
|
2021-11-15 17:07:32 +00:00
|
|
|
}
|
2021-11-24 15:47:49 +00:00
|
|
|
return $res;
|
2021-11-08 15:05:12 +00:00
|
|
|
} else {
|
|
|
|
throw new NotImplementedException;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-21 15:35:07 +01:00
|
|
|
/**
|
2021-09-21 16:28:54 +01:00
|
|
|
* Create a cached stream of Notes, paged
|
|
|
|
*
|
|
|
|
* Note: the number of notes per page may not always be the same,
|
|
|
|
* because of scoping. This would make this even more complicated
|
|
|
|
* and is left as an exercise to the reader :^)
|
|
|
|
* TODO Ensure same number of notes per page
|
|
|
|
*
|
|
|
|
* @return Note[]
|
2021-09-21 15:35:07 +01:00
|
|
|
*/
|
2021-09-21 16:28:54 +01:00
|
|
|
public static function pagedStream(string $key, string $query, array $query_args, LocalUser|Actor|null $actor = null, int $page = 1, ?int $per_page = null, string $pool = 'default', ?int $max_count = null, float $beta = 1.0): array
|
2021-09-21 15:35:07 +01:00
|
|
|
{
|
2021-10-10 09:26:18 +01:00
|
|
|
$max_count ??= Common::config('cache', 'max_note_count');
|
2021-09-21 16:28:54 +01:00
|
|
|
if ($per_page > $max_count) {
|
2021-10-10 09:26:18 +01:00
|
|
|
throw new InvalidArgumentException;
|
2021-09-21 16:28:54 +01:00
|
|
|
}
|
2021-09-21 15:35:07 +01:00
|
|
|
|
2021-10-10 09:26:18 +01:00
|
|
|
if (\is_null($per_page)) {
|
2022-01-02 20:21:00 +00:00
|
|
|
$per_page = Common::config('feeds', 'entries_per_page');
|
2021-09-21 15:35:07 +01:00
|
|
|
}
|
|
|
|
|
2021-11-28 13:09:04 +00:00
|
|
|
$filter_scope = fn (Note|Actor $o) => $o->isVisibleTo($actor);
|
2021-09-21 15:35:07 +01:00
|
|
|
|
2021-10-28 18:05:28 +01:00
|
|
|
$getter = fn (int $offset, int $length) => DB::dql($query, $query_args, options: ['offset' => $offset, 'limit' => $length]);
|
2021-09-21 15:35:07 +01:00
|
|
|
|
2022-10-19 22:38:49 +01:00
|
|
|
$requested_left = $offset = $per_page * ($page - 1);
|
2021-09-21 15:35:07 +01:00
|
|
|
$requested_right = $requested_left + $per_page;
|
2021-09-21 16:28:54 +01:00
|
|
|
[$stored_left, $stored_right] = F\map(
|
|
|
|
explode(':', self::get("{$key}-bounds", fn () => "{$requested_left}:{$requested_right}")),
|
2021-10-10 09:26:18 +01:00
|
|
|
fn (string $v) => (int) $v,
|
2021-09-21 16:28:54 +01:00
|
|
|
);
|
2021-10-28 18:05:28 +01:00
|
|
|
$length = $stored_right - $stored_left;
|
2021-09-21 15:35:07 +01:00
|
|
|
|
2021-10-28 18:05:28 +01:00
|
|
|
if (!\is_null($max_count) && $length > $max_count) {
|
|
|
|
$length = $max_count;
|
2021-09-21 15:35:07 +01:00
|
|
|
$requested_right = $requested_left + $max_count;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stored_left > $requested_left || $stored_right < $requested_right) {
|
|
|
|
$res = $getter($stored_left, $stored_right);
|
|
|
|
self::setList($key, value: $res, pool: $pool, max_count: $max_count, beta: $beta);
|
|
|
|
self::set("{$key}-bounds", "{$stored_left}:{$stored_right}");
|
2021-09-21 16:28:54 +01:00
|
|
|
return F\filter($res, $filter_scope);
|
2021-09-21 15:35:07 +01:00
|
|
|
}
|
|
|
|
|
2021-09-21 16:28:54 +01:00
|
|
|
return F\filter(
|
|
|
|
self::getList(
|
|
|
|
$key,
|
2021-10-28 18:05:28 +01:00
|
|
|
fn () => $getter($requested_left, $length),
|
2021-10-10 09:26:18 +01:00
|
|
|
max_count: $max_count,
|
|
|
|
left: $requested_left,
|
|
|
|
right: $requested_right,
|
|
|
|
beta: $beta,
|
|
|
|
),
|
|
|
|
$filter_scope,
|
2021-09-21 16:28:54 +01:00
|
|
|
);
|
2021-09-21 15:35:07 +01:00
|
|
|
}
|
2020-07-10 00:58:00 +01:00
|
|
|
}
|