Files
api/services/nginx/app/classes/system_search_cache.php
T

261 lines
7.2 KiB
PHP

<?php
namespace classes;
use Throwable;
class system_search_cache
{
public const PREFIX = 'system_search:v2:';
public const QUERY_PREFIX = self::PREFIX . 'query:';
public const DIRTY_TABLES_KEY = self::PREFIX . 'dirty_tables';
public const REBUILD_REQUEST_KEY = self::PREFIX . 'rebuild_request';
public const TABLE_VERSION_PREFIX = self::PREFIX . 'table_version:';
/**
* Optional runtime adapter for tests.
*/
private static ?object $adapter = null;
public static function setAdapterForTests(?object $adapter): void
{
self::$adapter = $adapter;
}
public static function getQuery(string $hash): ?array
{
$raw = self::redisGet(self::QUERY_PREFIX . $hash);
if ($raw === null) {
return null;
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return null;
}
return $decoded;
}
public static function setQuery(string $hash, array $payload, int $ttlSeconds = 120): void
{
self::redisSetEx(self::QUERY_PREFIX . $hash, json_encode($payload, JSON_UNESCAPED_UNICODE), $ttlSeconds);
}
public static function clearAll(): void
{
self::clearPattern(self::PREFIX . '*');
}
public static function clearQueryCaches(): void
{
self::clearPattern(self::QUERY_PREFIX . '*');
}
public static function markDirtyTable(string $table): void
{
$table = trim($table, " `\t\n\r\0\x0B");
if ($table === '') {
return;
}
self::bumpTableVersion($table);
$tables = self::redisGetArray(self::DIRTY_TABLES_KEY);
if (!in_array($table, $tables, true)) {
$tables[] = $table;
self::redisSetArray(self::DIRTY_TABLES_KEY, $tables);
// Keep dirty markers briefly in case cron is delayed.
self::redisExpire(self::DIRTY_TABLES_KEY, 3600);
}
}
public static function consumeDirtyTables(): array
{
$tables = self::redisGetArray(self::DIRTY_TABLES_KEY);
self::redisDelete(self::DIRTY_TABLES_KEY);
return $tables;
}
public static function peekDirtyTables(): array
{
return self::redisGetArray(self::DIRTY_TABLES_KEY);
}
public static function bumpTableVersion(string $table): int
{
$table = trim($table, " `\t\n\r\0\x0B");
if ($table === '') {
return 0;
}
$key = self::TABLE_VERSION_PREFIX . $table;
try {
$client = self::redisClient();
if ($client === null) {
return 0;
}
if (method_exists($client, 'incr')) {
return (int)$client->incr($key);
}
$current = self::redisGet($key);
$next = max(1, (int)$current + 1);
self::redisSet($key, (string)$next);
return $next;
} catch (Throwable) {
return 0;
}
}
/**
* @param array<int, string> $tables
*/
public static function tableVersionFingerprint(array $tables): string
{
$versions = [];
foreach ($tables as $table) {
if (!is_string($table)) {
continue;
}
$normalized = trim($table, " `\t\n\r\0\x0B");
if ($normalized === '') {
continue;
}
$versions[$normalized] = (int)(self::redisGet(self::TABLE_VERSION_PREFIX . $normalized) ?? 0);
}
ksort($versions);
return md5(json_encode($versions, JSON_UNESCAPED_UNICODE));
}
public static function enqueueRebuild(string $scope = 'all', array $types = []): array
{
$payload = [
'scope' => in_array($scope, ['all', 'types', 'dirty'], true) ? $scope : 'all',
'types' => array_values(array_unique(array_filter(array_map('strval', $types)))),
'requested_at' => time(),
];
self::redisSet(self::REBUILD_REQUEST_KEY, json_encode($payload, JSON_UNESCAPED_UNICODE));
self::redisExpire(self::REBUILD_REQUEST_KEY, 86400);
return $payload;
}
public static function consumeRebuildRequest(): ?array
{
$raw = self::redisGet(self::REBUILD_REQUEST_KEY);
if ($raw === null) {
return null;
}
self::redisDelete(self::REBUILD_REQUEST_KEY);
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : null;
}
private static function clearPattern(string $pattern): void
{
try {
$client = self::redisClient();
if ($client === null) {
return;
}
$client->clear_keys($pattern);
} catch (Throwable) {
// Cache clear must never break request flow.
}
}
private static function redisSetEx(string $key, string $value, int $ttl): void
{
try {
$client = self::redisClient();
if ($client === null) {
return;
}
$client->setEx($key, $value, $ttl);
} catch (Throwable) {
}
}
private static function redisSet(string $key, string $value): void
{
try {
$client = self::redisClient();
if ($client === null) {
return;
}
$client->set($key, $value);
} catch (Throwable) {
}
}
private static function redisGet(string $key): ?string
{
try {
$client = self::redisClient();
if ($client === null) {
return null;
}
$value = $client->get($key);
return is_string($value) ? $value : null;
} catch (Throwable) {
return null;
}
}
private static function redisDelete(string $key): void
{
try {
$client = self::redisClient();
if ($client === null) {
return;
}
$client->delete($key);
} catch (Throwable) {
}
}
private static function redisSetArray(string $key, array $value): void
{
try {
$client = self::redisClient();
if ($client === null) {
return;
}
$client->set_array($key, $value);
} catch (Throwable) {
}
}
private static function redisGetArray(string $key): array
{
try {
$client = self::redisClient();
if ($client === null) {
return [];
}
$value = $client->get_array($key);
return is_array($value) ? $value : [];
} catch (Throwable) {
return [];
}
}
private static function redisExpire(string $key, int $ttl): void
{
try {
$client = self::redisClient();
if ($client === null) {
return;
}
$client->expire($key, $ttl);
} catch (Throwable) {
}
}
private static function redisClient(): ?object
{
if (self::$adapter !== null) {
return self::$adapter;
}
if (!defined('redis')) {
return null;
}
$client = constant('redis');
return is_object($client) ? $client : null;
}
}