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); } }