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.
144 lines
4.7 KiB
PHP
144 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Prune old hourly metric snapshots in bounded batches.
|
|
*
|
|
* Usage:
|
|
* php artisan nova:prune-metric-snapshots
|
|
* php artisan nova:prune-metric-snapshots --keep-days=30 --chunk=5000 --dry-run
|
|
*/
|
|
class PruneMetricSnapshotsCommand extends Command
|
|
{
|
|
protected $signature = 'nova:prune-metric-snapshots
|
|
{--keep-days= : Keep snapshots for this many days (default: config metrics.hourly_snapshot_retention_days)}
|
|
{--chunk= : Rows to delete per batch (default: config metrics.hourly_snapshot_prune_chunk)}
|
|
{--sleep-ms= : Pause between batches in milliseconds}
|
|
{--max-batches=0 : Stop after N batches (0 = until done)}
|
|
{--dry-run : Count rows that would be deleted without deleting}';
|
|
|
|
protected $description = 'Delete old hourly metric snapshots beyond the retention window (batched)';
|
|
|
|
public function handle(): int
|
|
{
|
|
$keepDays = $this->resolveKeepDays();
|
|
$chunk = $this->resolvePositiveInt('chunk', (int) config('metrics.hourly_snapshot_prune_chunk', 5000), 1);
|
|
$sleepMs = $this->resolveNonNegativeInt('sleep-ms', (int) config('metrics.hourly_snapshot_prune_sleep_ms', 50));
|
|
$maxBatches = max(0, (int) $this->option('max-batches'));
|
|
$dryRun = (bool) $this->option('dry-run');
|
|
$cutoff = now()->subDays($keepDays);
|
|
|
|
if ($keepDays < 1) {
|
|
$this->error('keep-days must be >= 1.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$eligible = (int) DB::table('artwork_metric_snapshots_hourly')
|
|
->where('bucket_hour', '<', $cutoff)
|
|
->count();
|
|
|
|
$this->info(sprintf(
|
|
'[nova:prune-metric-snapshots] cutoff=%s keep_days=%d eligible=%d chunk=%d sleep_ms=%d max_batches=%s%s',
|
|
$cutoff->toDateTimeString(),
|
|
$keepDays,
|
|
$eligible,
|
|
$chunk,
|
|
$sleepMs,
|
|
$maxBatches === 0 ? 'unlimited' : (string) $maxBatches,
|
|
$dryRun ? ' (dry-run)' : ''
|
|
));
|
|
|
|
if ($dryRun) {
|
|
Log::info('[nova:prune-metric-snapshots] dry-run', [
|
|
'eligible' => $eligible,
|
|
'keep_days' => $keepDays,
|
|
'cutoff' => $cutoff->toDateTimeString(),
|
|
]);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$deleted = 0;
|
|
$batches = 0;
|
|
|
|
while (true) {
|
|
if ($maxBatches > 0 && $batches >= $maxBatches) {
|
|
$this->warn("Stopped after max-batches={$maxBatches}.");
|
|
break;
|
|
}
|
|
|
|
$ids = DB::table('artwork_metric_snapshots_hourly')
|
|
->where('bucket_hour', '<', $cutoff)
|
|
->orderBy('id')
|
|
->limit($chunk)
|
|
->pluck('id');
|
|
|
|
if ($ids->isEmpty()) {
|
|
break;
|
|
}
|
|
|
|
$batchDeleted = DB::table('artwork_metric_snapshots_hourly')
|
|
->whereIn('id', $ids->all())
|
|
->delete();
|
|
|
|
$deleted += $batchDeleted;
|
|
$batches++;
|
|
|
|
Log::info('[nova:prune-metric-snapshots] batch', [
|
|
'batch' => $batches,
|
|
'deleted' => $batchDeleted,
|
|
'deleted_total' => $deleted,
|
|
]);
|
|
|
|
if ($sleepMs > 0) {
|
|
usleep($sleepMs * 1000);
|
|
}
|
|
}
|
|
|
|
$this->info("Pruned {$deleted} snapshot rows older than {$keepDays} days in {$batches} batch(es).");
|
|
|
|
Log::info('[nova:prune-metric-snapshots] completed', [
|
|
'deleted' => $deleted,
|
|
'batches' => $batches,
|
|
'keep_days' => $keepDays,
|
|
'cutoff' => $cutoff->toDateTimeString(),
|
|
'eligible_at_start' => $eligible,
|
|
]);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function resolveKeepDays(): int
|
|
{
|
|
$option = $this->option('keep-days');
|
|
|
|
if ($option === null || $option === '') {
|
|
return (int) config('metrics.hourly_snapshot_retention_days', 30);
|
|
}
|
|
|
|
return (int) $option;
|
|
}
|
|
|
|
private function resolvePositiveInt(string $option, int $default, int $minimum): int
|
|
{
|
|
$raw = $this->option($option);
|
|
$value = ($raw === null || $raw === '') ? $default : (int) $raw;
|
|
|
|
return max($minimum, $value);
|
|
}
|
|
|
|
private function resolveNonNegativeInt(string $option, int $default): int
|
|
{
|
|
$raw = $this->option($option);
|
|
$value = ($raw === null || $raw === '') ? $default : (int) $raw;
|
|
|
|
return max(0, $value);
|
|
}
|
|
}
|