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:
2026-08-25 07:58:47 +02:00
parent f52879edbb
commit 8a80aae21e
114 changed files with 9293 additions and 284 deletions
@@ -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);
}
}