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.
140 lines
4.1 KiB
PHP
140 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Predis\Client as PredisClient;
|
|
|
|
class RedisCleanupLegacyPrefixCommand extends Command
|
|
{
|
|
protected $signature = 'skinbase:redis-cleanup-legacy-prefix
|
|
{--dry-run : SCAN and report only}
|
|
{--execute : UNLINK matching obsolete keys}
|
|
{--max-keys=500 : Cap keys this run}';
|
|
|
|
protected $description = 'UNLINK obsolete skinbasenova-database-* and skinbasenova_horizon:* keys only';
|
|
|
|
private const LEGACY_PREFIXES = [
|
|
'skinbasenova-database-',
|
|
'skinbasenova_horizon:',
|
|
];
|
|
|
|
private const PROTECTED_PREFIXES = [
|
|
'skinbase-database-',
|
|
'skinbase_horizon:',
|
|
];
|
|
|
|
public function handle(): int
|
|
{
|
|
$dryRun = (bool) $this->option('dry-run');
|
|
$execute = (bool) $this->option('execute');
|
|
if ($dryRun === $execute) {
|
|
$this->error('Pass exactly one of --dry-run or --execute.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$maxKeys = max(1, (int) $this->option('max-keys'));
|
|
$currentPrefix = (string) config('database.redis.options.prefix');
|
|
$horizonPrefix = (string) config('horizon.prefix');
|
|
|
|
$this->info(sprintf(
|
|
'[redis-cleanup-legacy-prefix] current_prefix=%s horizon_prefix=%s max_keys=%d %s',
|
|
$currentPrefix,
|
|
$horizonPrefix,
|
|
$maxKeys,
|
|
$dryRun ? 'dry-run' : 'execute'
|
|
));
|
|
|
|
if (in_array($currentPrefix, self::LEGACY_PREFIXES, true) || in_array($horizonPrefix, self::LEGACY_PREFIXES, true)) {
|
|
$this->error('Current process still uses a legacy prefix. Aborting.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$client = $this->unprefixedClient();
|
|
$found = [];
|
|
$cursor = '0';
|
|
do {
|
|
[$cursor, $keys] = $client->scan($cursor, ['COUNT' => 200, 'MATCH' => '*']);
|
|
foreach ($keys as $key) {
|
|
$key = (string) $key;
|
|
if ($this->isProtected($key)) {
|
|
continue;
|
|
}
|
|
if (! $this->isLegacy($key)) {
|
|
continue;
|
|
}
|
|
$found[] = $key;
|
|
if (count($found) >= $maxKeys) {
|
|
$cursor = '0';
|
|
break;
|
|
}
|
|
}
|
|
} while ($cursor !== '0' && $cursor !== 0);
|
|
|
|
$this->info('matched='.count($found));
|
|
foreach (array_slice($found, 0, 20) as $key) {
|
|
$this->line('key='.$key);
|
|
}
|
|
if (count($found) > 20) {
|
|
$this->line('... truncated listing');
|
|
}
|
|
|
|
$unlinked = 0;
|
|
if (! $dryRun && $found !== []) {
|
|
foreach (array_chunk($found, 50) as $chunk) {
|
|
$unlinked += (int) $client->unlink(...$chunk);
|
|
}
|
|
}
|
|
|
|
$this->info('unlinked='.$unlinked);
|
|
|
|
Log::info('[redis-cleanup-legacy-prefix] completed', [
|
|
'matched' => count($found),
|
|
'unlinked' => $unlinked,
|
|
'dry_run' => $dryRun,
|
|
]);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function isLegacy(string $key): bool
|
|
{
|
|
foreach (self::LEGACY_PREFIXES as $prefix) {
|
|
if (str_starts_with($key, $prefix)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function isProtected(string $key): bool
|
|
{
|
|
foreach (self::PROTECTED_PREFIXES as $prefix) {
|
|
if (str_starts_with($key, $prefix)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function unprefixedClient(): PredisClient
|
|
{
|
|
$redis = config('database.redis.default', []);
|
|
|
|
return new PredisClient([
|
|
'scheme' => 'tcp',
|
|
'host' => $redis['host'] ?? '127.0.0.1',
|
|
'port' => (int) ($redis['port'] ?? 6379),
|
|
'password' => $redis['password'] ?? null,
|
|
'database' => (int) ($redis['database'] ?? 0),
|
|
]);
|
|
}
|
|
}
|