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

235 lines
6.5 KiB
PHP

<?php
namespace classes;
use Throwable;
class system_search_cache
{
public const PREFIX = 'system_search:v1:';
public const QUERY_PREFIX = self::PREFIX . 'query:';
public const INTENT_PREFIX = self::PREFIX . 'intent:';
public const DIRTY_TABLES_KEY = self::PREFIX . 'dirty_tables';
public const REBUILD_REQUEST_KEY = self::PREFIX . 'rebuild_request';
/**
* 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 getIntent(string $hash): ?array
{
$raw = self::redisGet(self::INTENT_PREFIX . $hash);
if ($raw === null) {
return null;
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return null;
}
return $decoded;
}
public static function setIntent(string $hash, array $payload, int $ttlSeconds = 3600): void
{
self::redisSetEx(self::INTENT_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 clearIntentCaches(): void
{
self::clearPattern(self::INTENT_PREFIX . '*');
}
public static function markDirtyTable(string $table): void
{
$table = trim($table, " `\t\n\r\0\x0B");
if ($table === '') {
return;
}
$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);
}
// Query cache depends on mutable data and must be invalidated immediately.
self::clearQueryCaches();
}
public static function consumeDirtyTables(): array
{
$tables = self::redisGetArray(self::DIRTY_TABLES_KEY);
self::redisDelete(self::DIRTY_TABLES_KEY);
return $tables;
}
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;
}
}