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.
117 lines
3.1 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|