Files
SkinbaseNova/app/Services/Traffic/PresenceIndexPruner.php
T
klevze 8a80aae21e Ship production optimization M1-M12.5A: queues, metrics, HTTP observability, and vector search reliability.
Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
2026-08-25 07:58:47 +02:00

117 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Traffic;
use Illuminate\Support\Facades\Redis;
final class PresenceIndexPruner
{
private const SREM_IF_EXPIRED = <<<'LUA'
if redis.call('EXISTS', KEYS[1]) == 0 then
return redis.call('SREM', KEYS[2], ARGV[1])
end
return 0
LUA;
/**
* @return array{scanned:int,stale:int,removed:int,live:int,batches:int,dry_run:bool}
*/
public function prune(
int $scanCount,
int $maxBatches,
int $maxMembers,
int $sleepMs,
bool $dryRun,
?int $timeLimitSeconds = null,
): array {
$scanCount = max(10, $scanCount);
$maxBatches = max(1, $maxBatches);
$maxMembers = max(1, $maxMembers);
$started = microtime(true);
$cursor = 0;
$scanned = 0;
$stale = 0;
$removed = 0;
$live = 0;
$batches = 0;
while ($batches < $maxBatches && $scanned < $maxMembers) {
if ($timeLimitSeconds !== null && (microtime(true) - $started) >= $timeLimitSeconds) {
break;
}
$result = Redis::sscan(OnlineVisitorRepository::INDEX_KEY, $cursor, ['count' => $scanCount]);
if (! is_array($result) || count($result) < 2) {
break;
}
$cursor = (int) $result[0];
$members = $result[1];
$batches++;
if ($members === []) {
if ($cursor === 0) {
break;
}
continue;
}
$recordKeys = [];
foreach ($members as $member) {
$member = (string) $member;
$recordKeys[$member] = OnlineVisitorRepository::KEY_PREFIX.':'.$member;
}
$exists = Redis::pipeline(function ($pipe) use ($recordKeys): void {
foreach ($recordKeys as $recordKey) {
$pipe->exists($recordKey);
}
});
if (! is_array($exists)) {
$exists = [];
}
$i = 0;
foreach ($recordKeys as $member => $recordKey) {
$scanned++;
$isLive = (int) ($exists[$i] ?? 0) === 1;
$i++;
if ($isLive) {
$live++;
continue;
}
$stale++;
if ($dryRun) {
continue;
}
$removed += (int) Redis::eval(
self::SREM_IF_EXPIRED,
2,
$recordKey,
OnlineVisitorRepository::INDEX_KEY,
$member,
);
}
if ($sleepMs > 0) {
usleep($sleepMs * 1000);
}
if ($cursor === 0) {
break;
}
}
return [
'scanned' => $scanned,
'stale' => $stale,
'removed' => $removed,
'live' => $live,
'batches' => $batches,
'dry_run' => $dryRun,
];
}
}