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
+3
View File
@@ -83,6 +83,9 @@ return [
'low_engagement_after_days' => (int) env('COLLECTIONS_V5_LOW_ENGAGEMENT_AFTER_DAYS', 14),
],
'queue' => [
// No Horizon supervisor consumes this queue today. Keep dispatch
// off until a consumer exists (M7). Existing jobs are not deleted.
'dispatch_enabled' => (bool) env('COLLECTIONS_V5_DISPATCH_ENABLED', false),
'name' => env('COLLECTIONS_V5_QUEUE', 'collections'),
'health_batch_size' => (int) env('COLLECTIONS_V5_HEALTH_BATCH_SIZE', 40),
'recommendation_batch_size' => (int) env('COLLECTIONS_V5_RECOMMENDATION_BATCH_SIZE', 40),
+2
View File
@@ -15,6 +15,8 @@ return [
'queues' => [
'moderation' => env('FORUM_SECURITY_MODERATION_QUEUE', 'forum-moderation'),
'firewall' => env('FORUM_SECURITY_FIREWALL_QUEUE', 'forum-security'),
// Plugin jobs have no Horizon consumer. Pause enqueue until a worker exists.
'dispatch_enabled' => (bool) env('FORUM_QUEUE_DISPATCH_ENABLED', false),
],
'firewall' => [
+25 -3
View File
@@ -99,6 +99,7 @@ return [
'waits' => [
'redis:broadcasts' => 15,
'redis:default' => 60,
'redis:mail' => 60,
'redis:notifications' => 90,
],
@@ -209,9 +210,9 @@ return [
'maxJobs' => 0,
'memory' => 128,
'tries' => 1,
// Long-running recommendation rebuild jobs declare timeouts up to 900s.
// Keep the worker timeout above that ceiling so Horizon does not kill
// healthy jobs before Laravel can enforce the job-level timeout.
// Must stay below config/queue.php redis.retry_after (default 1080).
// Nightly recommendation rebuilds are chunked; keep headroom for
// remaining long jobs on the default queue.
'timeout' => 960,
'nice' => 0,
],
@@ -228,6 +229,21 @@ return [
'timeout' => 90,
'nice' => 0,
],
// Mail is intentionally isolated: SMTP latency must not block
// search/recommendation workers, and mail jobs declare tries=3–5.
'supervisor-mail' => [
'connection' => 'redis',
'queue' => ['mail'],
'balance' => 'simple',
'autoScalingStrategy' => 'time',
'maxProcesses' => 1,
'maxTime' => 0,
'maxJobs' => 0,
'memory' => 128,
'tries' => 5,
'timeout' => 90,
'nice' => 0,
],
],
'environments' => [
@@ -242,6 +258,9 @@ return [
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
'supervisor-mail' => [
'maxProcesses' => 2,
],
],
'local' => [
@@ -251,6 +270,9 @@ return [
'supervisor-messaging' => [
'maxProcesses' => 2,
],
'supervisor-mail' => [
'maxProcesses' => 1,
],
],
],
+14
View File
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
return [
'slow_request' => [
'enabled' => (bool) env('HTTP_SLOW_REQUEST_LOG_ENABLED', false),
'threshold_ms' => (int) env('HTTP_SLOW_REQUEST_MS', 750),
/*
* Testing only. When set, replaces measured duration so tests do not sleep.
*/
'test_duration_ms' => env('HTTP_SLOW_REQUEST_TEST_DURATION_MS'),
],
];
+8
View File
@@ -123,6 +123,14 @@ return [
'handler' => NullHandler::class,
],
'slow-http' => [
'driver' => 'daily',
'path' => storage_path('logs/slow-http.log'),
'level' => 'info',
'days' => (int) env('HTTP_SLOW_REQUEST_LOG_DAYS', 14),
'replace_placeholders' => false,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* Hourly artwork metric snapshots (heat, rising, windowed leaderboards).
*
* Default retention is 30 days so monthly leaderboards and Studio 30d KPIs
* have a complete window after warm-up. Override with env; never hardcode.
* Studio 30d KPIs use MAX-MIN of cumulative counters, never SUM.
*/
return [
'hourly_snapshot_retention_days' => (int) env('ARTWORK_METRIC_HOURLY_RETENTION_DAYS', 30),
'hourly_snapshot_prune_chunk' => (int) env('ARTWORK_METRIC_HOURLY_PRUNE_CHUNK', 5000),
'hourly_snapshot_prune_sleep_ms' => (int) env('ARTWORK_METRIC_HOURLY_PRUNE_SLEEP_MS', 50),
];
+4 -1
View File
@@ -68,7 +68,10 @@ return [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
// Must exceed the longest Horizon worker timeout (960s) plus a buffer.
// If retry_after is shorter than a running job, Redis re-queues it as a
// new attempt and Horizon (--tries=1) marks MaxAttemptsExceeded.
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 1080),
'block_for' => null,
'after_commit' => false,
],
+5
View File
@@ -77,6 +77,11 @@ return [
'candidate_pool' => 200, // top N candidates to run diversity filter on
],
// Unique lock for RankBuildScopeListsJob. Must exceed queue wait + job
// timeout (300s). Production wait has been ~2.5h; 6h prevents hourly
// stacking while still recovering a dead lock the same day.
'scope_job_unique_for' => (int) env('RANK_SCOPE_JOB_UNIQUE_FOR', 21600),
// ── Anti-spam / burst-view damping ──────────────────────────────────────
'spam' => [
'views_24h_threshold' => 2000,
+3
View File
@@ -54,6 +54,9 @@ return [
'forum-threads' => [
'size' => (int) env('SITEMAPS_SHARD_FORUM_THREADS_SIZE', 10000),
],
'tags' => [
'size' => (int) env('SITEMAPS_SHARD_TAGS_SIZE', 25000),
],
'collections' => [
'size' => (int) env('SITEMAPS_SHARD_COLLECTIONS_SIZE', 10000),
],
+1
View File
@@ -7,6 +7,7 @@ return [
'queue' => [
'name' => env('SKINBASE_AI_MODERATION_QUEUE', 'forum-moderation'),
'dispatch_enabled' => (bool) env('FORUM_QUEUE_DISPATCH_ENABLED', false),
],
'preflight' => [
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
return [
'online_visitors' => [
// Cap SSCAN/all() so moderation pages never SMEMBERS the 2.4M-member index.
'index_read_limit' => (int) env('ONLINE_VISITOR_INDEX_READ_LIMIT', 2000),
'prune_enabled' => (bool) env('ONLINE_VISITOR_INDEX_PRUNE_ENABLED', false),
'prune_scan_count' => (int) env('ONLINE_VISITOR_INDEX_PRUNE_SCAN_COUNT', 500),
'prune_max_batches' => (int) env('ONLINE_VISITOR_INDEX_PRUNE_MAX_BATCHES', 200),
'prune_max_members' => (int) env('ONLINE_VISITOR_INDEX_PRUNE_MAX_MEMBERS', 100000),
'prune_sleep_ms' => (int) env('ONLINE_VISITOR_INDEX_PRUNE_SLEEP_MS', 25),
],
];