# K-ArtSell Aegis v16.0 Operational Runbook **Purpose:** Decision tree + resolution steps for common incidents **Governance:** AGENTS.md v16.0 "Safety & Reliability" (Criterion #10) **Last Updated:** 2026-08-04 --- ## Table of Contents 1. [Quick Reference](#quick-reference) 2. [Incident Classification](#incident-classification) 3. [Common Scenarios & Resolutions](#common-scenarios--resolutions) 4. [Escalation Path](#escalation-path) 5. [Post-Incident Review](#post-incident-review) --- ## Quick Reference | Symptom | Root Cause | Resolution | Time | |---------|-----------|-----------|------| | High API latency (> 2s p99) | DB query backlog | Scale connections or optimize slow queries | 5-15 min | | All requests return 403 | Auth provider misconfigured | Check ASPNETCORE_ENVIRONMENT, redeploy | 10 min | | Hangfire jobs stuck | Distributed lock timeout | Delete stale locks from DB | 3 min | | Outbox/Inbox deadlock | Concurrent writes collision | Trigger manual OutboxPollerJob | 5 min | | Memory leak (usage > 1GB) | Unfreed objects in graph | Graceful restart + drain queue | 20 min | | DB connection pool exhausted | Max connections reached | Increase pool size or kill idle connections | 10 min | --- ## Incident Classification ### By Severity **πŸ”΄ CRITICAL (Page on-call immediately)** - All users cannot access system (Host down, Auth failed) - Data corruption or loss - Security breach (credentials exposed, unauthorized access) - Revenue-impacting transactions failing **🟠 HIGH (Start work within 15 minutes)** - Subset of users affected (single queue stuck) - Degraded performance (p99 > 5s) - Data quality issue (DQ jobs accumulating) - Non-critical feature unavailable **🟑 MEDIUM (Start work within 1 hour)** - Single job failing repeatedly - Increased error rate (but < 1%) - Observability gap (dashboard not updating) - Non-critical background task delayed **🟒 LOW (Schedule in next sprint)** - Code improvements (tech debt) - Documentation updates - Performance optimization (non-critical path) --- ## Common Scenarios & Resolutions ### Scenario 1: High API Response Time (CRITICAL/HIGH) **Detection:** - Monitoring alert: `p99_latency > 2s` - User complaint: "System is slow" - Hangfire queue depth > 1000 jobs **Decision Tree:** ``` Is Host running? β”œβ”€ NO β†’ Restart Host (Scenario 7) β”œβ”€ YES β†’ Is DB reachable? β”œβ”€ NO β†’ SSH tunnel issue (Scenario 5) β”œβ”€ YES β†’ Check queue depth β”œβ”€ Depth > 1000 β†’ Scale Hangfire workers or analyze slowest queries β”œβ”€ Depth < 100 β†’ Analyze application memory/CPU ``` **Resolution Steps:** 1. **Quick Health Check (1 min)** ```bash curl http://127.0.0.1:5002/health psql -U kartsell -d kartsell -c "SELECT now()" # DB latency ``` 2. **Check Queue Depth (1 min)** ```sql SELECT queue, COUNT(*) FROM hangfire.job WHERE state_name='Enqueued' GROUP BY queue; ``` 3. **Identify Slow Queries (3 min)** ```sql SELECT query, calls, mean_time FROM pg_stat_statements WHERE mean_time > 100 ORDER BY mean_time DESC LIMIT 10; ``` 4. **Scale Hangfire Workers (5 min)** - Edit `appsettings.Production.json`: `"WorkerCount": 16` (from 8) - Restart Host - Monitor: Should process queue faster 5. **Optimize Slow Query (10-30 min)** - Run `EXPLAIN ANALYZE` on slowest query - Check for missing indexes: `SELECT * FROM pg_indexes WHERE tablename='...'` - Add index if needed: `CREATE INDEX idx_... ON table(...)` - Test performance: `SELECT ... EXPLAIN ANALYZE` **Success Criteria:** p99_latency < 2s, queue depth < 100 --- ### Scenario 2: Authentication Failures (CRITICAL) **Detection:** - HTTP 403/404 responses on valid endpoints - Error log: "FailClosedAuthenticationHandler denies request" - All users affected **Root Cause Analysis:** ``` Is ASPNETCORE_ENVIRONMENT correct? β”œβ”€ Release mode but missing auth config β†’ Add FailClosedAuthenticationHandler config β”œβ”€ Development mode (wrong for prod) β†’ Redeploy with Release β”œβ”€ API key format incorrect β†’ Check Gitea Secrets vs. code ``` **Resolution Steps:** 1. **Verify Environment (1 min)** ```powershell # Check running process Get-Process -Name dotnet | Select-Object CommandLine # Should show: --configuration Release ``` 2. **Check Auth Configuration (2 min)** ```bash cat src/KArtSell.Host/appsettings.Production.json | grep -A 10 "Authentication" ``` 3. **Verify API Key Format (2 min)** - Check Gitea Secrets: https://gitea.taxbaik.com/kjh2064/KArtSell.Aegis/settings/actions/secrets - Expected: `KRX_OPENAPI=` (not `stub-key-for-testing`) 4. **Temporary Workaround (1 min)** ```powershell # If stuck: Start in Development mode temporarily $env:ASPNETCORE_ENVIRONMENT = "Development" dotnet run --project src/KArtSell.Host --configuration Debug # This uses DevelopmentHeaderAuthenticationHandler (accepts X-KArtSell-User header) ``` 5. **Permanent Fix (5 min)** - Update `appsettings.Production.json` with correct auth provider - Redeploy with Release configuration **Success Criteria:** GET /api/health returns 200 --- ### Scenario 3: Hangfire Job Stuck in "Scheduled" State (HIGH) **Detection:** - Monitoring: Jobs in "Scheduled" state > 5 minutes - Hangfire dashboard: Red warning on recurring job - Log: "Recurring job registration timeout" **Root Cause:** Distributed lock held too long (network latency, DB contention) **Decision Tree:** ``` Is the Hangfire server running? β”œβ”€ NO β†’ Start Host β”œβ”€ YES β†’ Is there a distributed lock? β”œβ”€ NO β†’ Job definition error (check code) β”œβ”€ YES β†’ Is lock stale? β”œβ”€ YES (> 10 min) β†’ Delete lock (Scenario 3 Resolution) β”œβ”€ NO (< 5 min) β†’ Wait or increase timeout (DEBT-015) ``` **Resolution Steps:** 1. **Verify Hangfire Server (1 min)** ```sql SELECT name, last_heartbeat, worker_count FROM hangfire.server; ``` - If empty: Host not running (Scenario 7) - If stale: Server crashed, restart Host 2. **Check Distributed Lock (1 min)** ```sql SELECT * FROM hangfire.lock WHERE Key LIKE 'Recurring:%' ORDER BY TimeOut DESC; ``` 3. **Identify Stale Lock (1 min)** - If `TimeOut > CURRENT_TIMESTAMP` by > 10 minutes β†’ lock is stale - This prevents job from dequeuing 4. **Delete Stale Lock (1 min)** ```sql DELETE FROM hangfire.lock WHERE Key = 'Recurring:JobId' AND TimeOut < CURRENT_TIMESTAMP - INTERVAL '5 minutes'; ``` 5. **Monitor Next Run (2 min)** - Job should dequeue within 15 seconds - Check Hangfire dashboard: Job should move to "Processing" **Prevention:** DEBT-015 already applied (consistent timeout handling) **Success Criteria:** Job processes immediately after lock removal --- ### Scenario 4: Outbox/Inbox Deadlock (HIGH) **Detection:** - Event processing stalled - `SELECT COUNT(*) FROM outbox.outbox WHERE published_at IS NULL` > 100 - Inbox consumers not progressing (check logs) **Root Cause:** Concurrent writes to inbox, or published event not being consumed **Resolution Steps:** 1. **Assess Situation (2 min)** ```sql SELECT COUNT(*) as unpublished FROM outbox.outbox WHERE published_at IS NULL; SELECT COUNT(*) as unprocessed FROM inbox.inbox WHERE processed_at IS NULL; ``` 2. **Check Outbox Poller Logs (3 min)** ```bash grep -i "OutboxPollerJob" host.log | tail -20 # Look for errors: "Duplicate event", "Database timeout", "Constraint violation" ``` 3. **Option A: Trigger Manual Poll (2 min)** ```bash # If queue is small (< 1000), manually trigger: curl -X POST http://127.0.0.1:5002/internal/outbox-poll \ -H "X-KArtSell-User: operator" -H "X-KArtSell-Role: Admin" ``` 4. **Option B: Drain Stuck Events (5 min)** ```sql -- Mark old unpublished events as published (if safe) UPDATE outbox.outbox SET published_at = now() WHERE published_at IS NULL AND created_at < now() - INTERVAL '1 hour'; ``` 5. **Monitor Recovery (5 min)** - Inbox consumer should resume - Check: `SELECT COUNT(*) FROM inbox.inbox WHERE processed_at IS NULL` - Should decrease over time **Success Criteria:** All outbox events published, inbox processing resumes --- ### Scenario 5: SSH Tunnel Disconnected (CRITICAL) **Detection:** - Connection timeout on DB queries - Error: "Connection refused: localhost:5432" - Hangfire jobs failing with DB connection errors **Resolution Steps:** 1. **Verify Tunnel Status (1 min)** ```bash # Check if SSH tunnel is running netstat -an | grep 5432 # Should show LISTENING ps aux | grep ssh # Should show "-L 5432:..." ``` 2. **Reconnect SSH Tunnel (2 min)** ```bash ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7 # Should show "Permission granted" or prompt for password ``` 3. **Verify Tunnel Works (1 min)** ```bash psql -h localhost -p 5432 -U kartsell -d kartsell -c "SELECT 1" # Should return: 1 ``` 4. **Keep Tunnel Open (Ongoing)** - Do not close this terminal/session - If tunnel dies, reconnect immediately **Success Criteria:** `psql` command succeeds, Host can reach DB --- ### Scenario 6: Memory Leak (Application Usage > 1GB) **Detection:** - Monitoring: Application memory > 1GB (baseline ~500MB) - Host CPU spike + memory growth - Response time degradation **Root Cause:** Unfreed cached data, event accumulation, or circular references **Resolution Steps (Graceful):** 1. **Verify Memory Usage (1 min)** ```powershell Get-Process | Where-Object { $_.Name -like "*dotnet*" } | Select-Object Name, @{N="MemMB";E={$_.WorkingSet/1MB}} ``` 2. **Check Outbox Size (2 min)** ```sql SELECT pg_size_pretty(pg_total_relation_size('outbox.outbox')) as size; ``` - If > 500MB: Truncate old published events 3. **Drain Hangfire Queue (5 min)** - Wait for all jobs to complete - Stop accepting new jobs - Monitor queue depth β†’ 0 4. **Graceful Restart (10 min)** ```powershell # Stop Host Stop-Process -Name dotnet -Force Start-Sleep -Seconds 5 # Restart $env:ASPNETCORE_ENVIRONMENT = "Production" dotnet run --project src/KArtSell.Host --configuration Release ``` 5. **Verify Recovery (3 min)** ```powershell # Check memory is back to baseline Get-Process | Where-Object { $_.Name -like "*dotnet*" } | Select-Object Name, @{N="MemMB";E={$_.WorkingSet/1MB}} # Should be ~500MB ``` **Success Criteria:** Memory < 500MB, all services resume --- ### Scenario 7: Host Crashed / Not Running **Detection:** - HTTP connection refused: localhost:5002 - netstat shows no listener on 5002 - Hangfire jobs accumulating (no processing) **Resolution Steps:** 1. **Verify Host is Down (1 min)** ```bash curl http://127.0.0.1:5002/health 2>&1 | grep -i "refused" # If connection refused: Host is down ``` 2. **Check Logs (3 min)** ```bash tail -100 host.log | grep -i "error\|crash\|exception" # Look for root cause ``` 3. **Verify Prerequisites (3 min)** ```bash # SSH tunnel netstat -an | grep 5432 | grep LISTENING # Database psql -h localhost -U kartsell -d kartsell -c "SELECT 1" # .NET SDK dotnet --version ``` 4. **Start Host (1 min)** ```bash cd D:\JobRoomz\KArtSell.Aegis .\scripts\gate-4-startup.ps1 -Environment Debug # or Release for production ``` 5. **Verify Startup (2 min)** ```bash # Wait for "Now listening on: http://127.0.0.1:5002" curl http://127.0.0.1:5002/health # Should return {"status":"healthy"} ``` **Success Criteria:** Health check passes, Hangfire resumes processing --- ## Escalation Path | Scenario | On-Call | Manager | CTO | Action Time | |----------|---------|---------|-----|-------------| | Auth failure | βœ… Page immediately | βœ… Notify | βœ… If > 15 min | < 15 min | | Data loss | βœ… Page immediately | βœ… Notify | βœ… Page | < 5 min | | Host crash | βœ… Try self-heal | βœ… Notify if > 10 min | βœ… If still down | < 20 min | | Slow performance | βœ… Analyze | βœ… Notify if > 1 hour | ⏸️ Info only | < 60 min | | DB connection issue | βœ… Check SSH tunnel | βœ… Notify | ⏸️ Info only | < 10 min | --- ## Post-Incident Review After resolving any CRITICAL or HIGH incident: 1. **Log Incident (15 min)** - Incident ID: [Auto-generated timestamp] - Severity: [Critical/High/Medium] - Detection time: [When first alerted] - Resolution time: [When service restored] - Root cause: [Brief summary] - Steps taken: [What worked, what didn't] 2. **Document Root Cause (30 min)** - Why did this happen? - Is it a known issue or new? - Is there a tech debt item to track? 3. **Implement Prevention (1-4 weeks)** - Can we detect this earlier? - Can we automate the fix? - Should we add monitoring or alerts? 4. **Update This Runbook (15 min)** - Did any steps not work as documented? - Add new scenarios if different from existing 5. **Team Debrief (30 min)** - Share findings in team Slack/meeting - Celebrate quick resolution - Commit to follow-up actions --- ## Contact Information | Role | Name | Slack | Email | On-Call | |------|------|-------|-------|---------| | Engineering Lead | [TBD] | @lead | lead@company.com | Schedule | | DevOps Lead | [TBD] | @devops | devops@company.com | Schedule | | DBA | [TBD] | @dba | dba@company.com | Schedule | --- **Last Updated:** 2026-08-04 **Next Review:** Upon critical incident or quarterly **Maintained by:** Engineering Team