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.
This commit is contained in:
@@ -7,34 +7,137 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Prune old hourly metric snapshots to prevent unbounded table growth.
|
||||
* Prune old hourly metric snapshots in bounded batches.
|
||||
*
|
||||
* Usage: php artisan nova:prune-metric-snapshots
|
||||
* php artisan nova:prune-metric-snapshots --keep-days=7
|
||||
* 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=7 : Keep snapshots for this many days}';
|
||||
{--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';
|
||||
protected $description = 'Delete old hourly metric snapshots beyond the retention window (batched)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$keepDays = (int) $this->option('keep-days');
|
||||
$cutoff = now()->subDays($keepDays);
|
||||
$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);
|
||||
|
||||
$deleted = DB::table('artwork_metric_snapshots_hourly')
|
||||
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)
|
||||
->delete();
|
||||
->count();
|
||||
|
||||
$this->info("Pruned {$deleted} snapshot rows older than {$keepDays} days.");
|
||||
$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,
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user