File size: 24,033 Bytes
44c4c2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6ada92
 
44c4c2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2378dfe
44c4c2d
 
0ece8e9
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
"""
Task 3: Cascading Service Failure (Hard)
=========================================
Scenario: Four production services are down simultaneously. A recent deployment
of config-service v1.2.0 introduced a broken service-discovery URL, causing all
services that depend on it to fail to resolve each other.

Traps:
  - user-service has an unrelated memory warning (red herring)
  - order-service v2.1.0 was deployed 2h ago (not the cause)
  - Restarting any individual service without fixing config-service makes no progress
  - Need to investigate and identify config-service as the common dependency

Optimal: check_config(config-service) OR query_logs(any-down-service) β†’
         rollback_deployment(config-service) β†’ resolve_incident()
Max steps: 20  |  Passing score: 0.6
"""

from typing import Dict, Any, Tuple, List
from app.models import Observation, Alert, ServiceStatus
from app.tasks.base import BaseTask, AVAILABLE_ACTIONS, BASE_INCIDENT_TIME


class CascadingFailureTask(BaseTask):
    task_id = "task3"
    name = "Cascading Service Failure"
    description = (
        "Four services (api-gateway, user-service, order-service, payment-service) "
        "are simultaneously down. Identify the common root cause and remediate "
        "the entire incident with minimal blast radius."
    )
    difficulty = "hard"
    max_steps = 20
    passing_score = 0.6

    def initial_state(self, seed: int = 42) -> Dict[str, Any]:
        return {
            "services": {
                "api-gateway": {
                    "status": "down", "cpu": 0.5, "memory": 12.0,
                    "error_rate": 100.0, "version": "1.4.2", "replicas": 3,
                },
                "user-service": {
                    "status": "down", "cpu": 0.8, "memory": 88.0,
                    "error_rate": 100.0, "version": "4.0.1", "replicas": 2,
                },
                "order-service": {
                    "status": "down", "cpu": 0.3, "memory": 45.0,
                    "error_rate": 100.0, "version": "2.1.0", "replicas": 2,
                },
                "payment-service": {
                    "status": "down", "cpu": 0.2, "memory": 40.0,
                    "error_rate": 100.0, "version": "5.2.3", "replicas": 2,
                },
                "config-service": {
                    "status": "healthy", "cpu": 18.0, "memory": 35.0,
                    "error_rate": 0.0, "version": "1.2.0", "replicas": 1,
                },
                "db-primary": {
                    "status": "healthy", "cpu": 22.0, "memory": 55.0,
                    "error_rate": 0.0, "connections": 15, "max_connections": 100,
                    "version": "14.8",
                },
                "message-queue": {
                    "status": "healthy", "cpu": 10.0, "memory": 42.0,
                    "error_rate": 0.0, "version": "3.12.0",
                },
            },
            "alerts": [
                {"id": "ALT-020", "sev": "critical", "svc": "api-gateway",
                 "msg": "api-gateway is DOWN β€” all requests returning 503",
                 "ack": False},
                {"id": "ALT-021", "sev": "critical", "svc": "user-service",
                 "msg": "user-service is DOWN β€” health check failing for 8 minutes",
                 "ack": False},
                {"id": "ALT-022", "sev": "critical", "svc": "order-service",
                 "msg": "order-service is DOWN β€” all replicas unhealthy",
                 "ack": False},
                {"id": "ALT-023", "sev": "critical", "svc": "payment-service",
                 "msg": "payment-service is DOWN β€” cannot process transactions",
                 "ack": False},
                {"id": "ALT-024", "sev": "warning", "svc": "user-service",
                 "msg": "user-service memory at 88% (elevated but not critical)",
                 "ack": False},
            ],
            "recent_deployments": [
                {"service": "config-service", "version": "1.2.0", "previous": "1.1.9",
                 "deployed_at": "2024-11-15T09:30:00Z", "deployer": "platform-team",
                 "change": "Updated service discovery URLs for new datacenter migration"},
                {"service": "order-service", "version": "2.1.0", "previous": "2.0.8",
                 "deployed_at": "2024-11-15T07:45:00Z", "deployer": "ci-pipeline",
                 "change": "New checkout flow feature"},
                {"service": "user-service", "version": "4.0.1", "previous": "4.0.0",
                 "deployed_at": "2024-11-14T14:00:00Z", "deployer": "ci-pipeline",
                 "change": "Bug fix for profile update endpoint"},
            ],
            # Tracking agent progress
            "logs_queried": [],
            "metrics_checked": [],
            "configs_checked": [],
            "services_restarted": [],
            "rollbacks_attempted": {},
            "wrong_rollbacks": 0,
            "config_service_rolledback": False,
            "services_recovered": [],
            "incident_resolved": False,
            "_all_services_down_due_to_config": True,
        }

    def _check_recovery(self, state: Dict[str, Any]) -> None:
        """Update service statuses based on whether config-service was fixed."""
        if state["config_service_rolledback"]:
            for svc in ["api-gateway", "user-service", "order-service", "payment-service"]:
                if svc not in state["services_recovered"]:
                    state["services_recovered"].append(svc)
                state["services"][svc]["status"] = "healthy"
                state["services"][svc]["error_rate"] = 0.0
                state["services"][svc]["cpu"] = float(
                    {"api-gateway": 8.0, "user-service": 22.0,
                     "order-service": 15.0, "payment-service": 12.0}[svc]
                )

    def process_action(
        self, action_type: str, params: Dict[str, Any], state: Dict[str, Any]
    ) -> Tuple[Dict[str, Any], float, bool, str]:
        reward = 0.0
        done = False
        message = ""
        service = params.get("service", "").strip()

        if action_type == "query_logs":
            if not service:
                return state, -0.02, False, "Parameter 'service' is required."
            if service in state["logs_queried"]:
                return state, 0.0, False, f"[Cached] Logs for {service} already retrieved."
            state["logs_queried"].append(service)

            down_services = ["api-gateway", "user-service", "order-service", "payment-service"]

            if service in down_services:
                reward = 0.08
                service_logs = {
                    "api-gateway": (
                        "2024-11-15T09:31:00Z [ERROR] api-gateway: failed to resolve "
                        "user-service endpoint via config-service: "
                        "GET http://config-service/discover/user-service β†’ "
                        "returned 'http://svc-mesh-BROKEN.internal:8080' (unreachable)\n"
                        "2024-11-15T09:31:01Z [ERROR] api-gateway: failed to resolve "
                        "order-service β€” same issue\n"
                        "2024-11-15T09:31:05Z [FATAL] api-gateway: no healthy upstreams "
                        "available β€” entering 503 mode\n"
                        "PATTERN: All service discovery calls returning broken URLs from config-service."
                    ),
                    "user-service": (
                        "2024-11-15T09:31:00Z [ERROR] user-service: startup failed β€” cannot "
                        "resolve db endpoint via config-service: received 'db-BROKEN.internal' "
                        "(expected 'db-primary.internal')\n"
                        "2024-11-15T09:31:02Z [FATAL] user-service: health check failed β€” "
                        "cannot connect to database\n"
                        "PATTERN: config-service returning incorrect service discovery data."
                    ),
                    "order-service": (
                        "2024-11-15T09:31:00Z [ERROR] order-service: failed to start β€” "
                        "config-service returned broken payment-service URL\n"
                        "2024-11-15T09:31:03Z [FATAL] order-service: dependency check failed, "
                        "exiting\n"
                        "NOTE: order-service v2.1.0 deployed at 07:45 ran fine until 09:30 "
                        "when config-service was updated."
                    ),
                    "payment-service": (
                        "2024-11-15T09:31:00Z [ERROR] payment-service: cannot resolve fraud-check "
                        "service β€” config-service lookup returned null endpoint\n"
                        "2024-11-15T09:31:05Z [FATAL] payment-service: aborting startup due to "
                        "missing required service dependencies\n"
                    ),
                }
                message = service_logs.get(service, "No logs found.")

            elif service == "config-service":
                reward = 0.12
                message = (
                    "2024-11-15T09:28:00Z [INFO]  config-service: v1.2.0 deployment started\n"
                    "2024-11-15T09:29:50Z [INFO]  config-service: service discovery URLs updated "
                    "for datacenter migration\n"
                    "2024-11-15T09:30:00Z [INFO]  config-service: v1.2.0 deployment complete\n"
                    "2024-11-15T09:30:05Z [WARN]  config-service: 4 downstream services "
                    "reporting connection failures immediately after deploy\n"
                    "2024-11-15T09:30:10Z [ERROR] config-service: config validation failed "
                    "in post-deploy check β€” service_discovery_urls contain unreachable hosts\n"
                    "ROOT CAUSE CONFIRMED: config-service v1.2.0 deployed broken service "
                    "discovery URLs. All dependent services cannot resolve each other."
                )
            else:
                reward = 0.02
                message = f"No anomalies in logs for {service}."

        elif action_type == "check_metrics":
            if not service:
                return state, -0.02, False, "Parameter 'service' is required."
            if service in state["metrics_checked"]:
                return state, 0.0, False, f"[Cached] Metrics for {service} already retrieved."
            state["metrics_checked"].append(service)

            if service in ["api-gateway", "user-service", "order-service", "payment-service"]:
                reward = 0.06
                message = (
                    f"{service} metrics:\n"
                    f"  status: DOWN\n"
                    f"  error_rate: 100% (all requests failing)\n"
                    f"  last_healthy: 2024-11-15T09:30:02Z\n"
                    f"  restart_attempts: 3 (all failed)\n"
                    f"  failure_reason: dependency resolution failure at startup\n"
                    f"CORRELATES: All 4 services went down within 15 seconds of each other "
                    f"at 09:30 β€” timing matches config-service v1.2.0 deployment."
                )
            elif service == "config-service":
                reward = 0.08
                message = (
                    "config-service metrics:\n"
                    "  status: healthy\n"
                    "  cpu: 18%\n"
                    "  requests_per_sec: 240\n"
                    "  cache_hit_rate: 12%  ← very low (normal: 95%+)\n"
                    "  discovery_errors_per_sec: 180  ← HIGH\n"
                    "  version: 1.2.0  (deployed 17min ago)\n"
                    "SUSPICIOUS: High discovery_errors and low cache_hit_rate after recent deploy."
                )
            else:
                reward = 0.02
                message = f"Metrics for {service}: Normal."

        elif action_type == "check_config":
            state["configs_checked"].append(service)
            if service == "config-service":
                reward = 0.15
                message = (
                    "config-service LIVE CONFIG (v1.2.0):\n"
                    "  service_discovery:\n"
                    "    user-service:    http://svc-mesh-BROKEN.dc2.internal:8080\n"
                    "    order-service:   http://svc-mesh-BROKEN.dc2.internal:8081\n"
                    "    payment-service: http://svc-mesh-BROKEN.dc2.internal:8082\n"
                    "    db-primary:      http://db-BROKEN.dc2.internal:5432\n"
                    "    api-gateway:     http://gw-BROKEN.dc2.internal:80\n\n"
                    "config-service PREVIOUS CONFIG (v1.1.9):\n"
                    "  service_discovery:\n"
                    "    user-service:    http://user-service.svc.cluster.local:8080  βœ“\n"
                    "    order-service:   http://order-service.svc.cluster.local:8081  βœ“\n"
                    "    payment-service: http://payment-service.svc.cluster.local:8082 βœ“\n"
                    "    db-primary:      http://db-primary.svc.cluster.local:5432  βœ“\n\n"
                    "ROOT CAUSE CONFIRMED: v1.2.0 changed ALL service discovery URLs to "
                    "non-existent dc2.internal addresses. Datacenter migration was incomplete."
                )
            elif service in ["api-gateway", "user-service", "order-service", "payment-service"]:
                reward = 0.04
                message = (
                    f"{service} config appears normal. Service discovery endpoint "
                    f"points to config-service (as expected). The issue is in what "
                    f"config-service returns, not in {service}'s config itself."
                )
            else:
                reward = 0.01
                message = f"Config for {service}: Nothing unusual."

        elif action_type == "examine_trace":
            trace_id = params.get("trace_id", "unknown")
            state["logs_queried"].append(f"trace:{trace_id}")
            reward = 0.06
            message = (
                f"Trace {trace_id}:\n"
                "  api-gateway β†’ [service discovery lookup] β†’ config-service (2ms)\n"
                "  config-service β†’ returned URL: http://svc-mesh-BROKEN.dc2.internal\n"
                "  api-gateway β†’ [connection attempt to broken URL] β†’ TIMEOUT after 5000ms\n"
                "  Root span: 100% of failures originate from bad service discovery response."
            )

        elif action_type == "restart_service":
            if service in ["api-gateway", "user-service", "order-service", "payment-service"]:
                if not state["config_service_rolledback"]:
                    state["services_restarted"].append(service)
                    # Restarting without fixing config does nothing
                    reward = -0.05
                    message = (
                        f"Restarted {service}... but it failed to start again.\n"
                        f"  Startup error: cannot resolve service dependencies via config-service\n"
                        f"  Status: still DOWN\n"
                        f"The underlying config-service issue must be fixed first."
                    )
                else:
                    # After config fix, manual restart not needed (auto-recovery)
                    reward = 0.0
                    message = f"{service} already recovering after config-service rollback."
            elif service == "config-service":
                # Restarting config-service doesn't fix the bad config
                state["services_restarted"].append(service)
                reward = -0.08
                message = (
                    "config-service restarted β€” but it loaded the same broken v1.2.0 config.\n"
                    "  All downstream services still failing.\n"
                    "  A restart does not fix a misconfiguration. Use rollback_deployment."
                )
            else:
                reward = 0.0
                message = f"{service} is healthy and does not need a restart."

        elif action_type == "rollback_deployment":
            if service == "config-service":
                state["config_service_rolledback"] = True
                self._check_recovery(state)
                reward = 0.45
                message = (
                    "βœ“ config-service rolled back from v1.2.0 β†’ v1.1.9.\n"
                    "  Service discovery URLs restored to cluster-internal addresses.\n"
                    "  api-gateway:       DOWN β†’ healthy (restarted automatically)\n"
                    "  user-service:      DOWN β†’ healthy (restarted automatically)\n"
                    "  order-service:     DOWN β†’ healthy (restarted automatically)\n"
                    "  payment-service:   DOWN β†’ healthy (restarted automatically)\n"
                    "All 4 services recovered within 45 seconds of config-service rollback."
                )
            elif service == "order-service":
                # Red herring β€” order-service v2.1.0 was NOT the cause
                state["rollbacks_attempted"][service] = True
                state["wrong_rollbacks"] += 1
                reward = -0.08
                message = (
                    "Rolled back order-service to v2.0.8... but it immediately failed again.\n"
                    "  Error: still cannot resolve service dependencies via config-service.\n"
                    "  RESULT: order-service v2.1.0 was not the root cause. "
                    "The issue is upstream."
                )
            elif service in ["api-gateway", "user-service", "payment-service"]:
                state["rollbacks_attempted"][service] = True
                state["wrong_rollbacks"] += 1
                reward = -0.06
                message = (
                    f"Rolled back {service}... but it still cannot start.\n"
                    f"  Error: service discovery failing β€” same as before.\n"
                    f"  This service is not the root cause."
                )
            else:
                reward = -0.02
                message = f"Rolling back {service} has no effect on the current incident."

        elif action_type == "scale_service":
            reward = -0.05
            message = (
                "Scaling has no effect β€” services are failing due to misconfiguration, "
                "not insufficient capacity."
            )

        elif action_type == "acknowledge_alert":
            alert_id = params.get("alert_id", "")
            for a in state["alerts"]:
                if a["id"] == alert_id:
                    a["ack"] = True
            reward = 0.01
            message = f"Alert {alert_id} acknowledged."

        elif action_type == "resolve_incident":
            all_healthy = all(
                state["services"][svc]["status"] == "healthy"
                for svc in ["api-gateway", "user-service", "order-service", "payment-service"]
            )
            if all_healthy:
                state["incident_resolved"] = True
                done = True
                reward = 0.25
                message = (
                    "βœ“ Incident resolved.\n"
                    "Post-mortem: config-service v1.2.0 was deployed with incorrect service "
                    "discovery URLs targeting a non-existent dc2 datacenter. This caused all "
                    "dependent services to fail at startup. Rollback to v1.1.9 restored service.\n"
                    "Recommendation: Add config validation to deployment pipeline."
                )
            else:
                still_down = [
                    s for s in ["api-gateway", "user-service", "order-service", "payment-service"]
                    if state["services"][s]["status"] != "healthy"
                ]
                reward = -0.05
                message = (
                    f"Cannot resolve: {len(still_down)} services still down: "
                    f"{', '.join(still_down)}. Fix the root cause first."
                )

        else:
            reward = -0.03
            message = f"Unknown or inapplicable action: {action_type}."

        return state, reward, done, message

    def get_observation(self, state: Dict[str, Any], session_id: str, step: int) -> Observation:
        services = {}
        for name, s in state["services"].items():
            services[name] = ServiceStatus(
                name=name, status=s["status"],
                cpu_percent=s["cpu"], memory_percent=s["memory"],
                error_rate=s["error_rate"],
                connections=s.get("connections"),
                max_connections=s.get("max_connections"),
                version=s.get("version", "1.0.0"),
                replicas=s.get("replicas", 1),
            )

        alerts = [
            Alert(
                alert_id=a["id"], severity=a["sev"], service=a["svc"],
                message=a["msg"], triggered_at=BASE_INCIDENT_TIME,
                acknowledged=a["ack"],
            )
            for a in state["alerts"]
        ]

        return Observation(
            session_id=session_id,
            task_id=self.task_id,
            step=step,
            timestamp=BASE_INCIDENT_TIME,
            alerts=alerts,
            services=services,
            available_actions=AVAILABLE_ACTIONS,
            incident_resolved=state["incident_resolved"],
            message="",
            recent_deployments=state["recent_deployments"],
            runbook_hints=[
                "When multiple services fail simultaneously, look for a common dependency.",
                "Check the timing: what changed just before the incident?",
                "check_config reveals live runtime configuration values.",
                "Restarting services without fixing the root cause will not help.",
                "rollback_deployment reverts to the previous known-good version.",
            ],
        )

    def grade(self, state: Dict[str, Any], history: List[Dict]) -> Tuple[float, Dict[str, float]]:
        breakdown = {}
        score = 0.0

        # Root cause investigated?
        root_investigated = (
            "config-service" in state.get("configs_checked", []) or
            "config-service" in state.get("logs_queried", []) or
            "config-service" in state.get("metrics_checked", []) or
            any(svc in state.get("logs_queried", [])
                for svc in ["api-gateway", "user-service", "order-service", "payment-service"])
        )
        if root_investigated:
            breakdown["investigated_root_cause"] = 0.14
            score += 0.14

        # config-service identified and rolled back?
        if state.get("config_service_rolledback", False):
            breakdown["correct_rollback"] = 0.40
            score += 0.40

        # All services recovered?
        recovered = state.get("services_recovered", [])
        if len(recovered) >= 4:
            breakdown["full_recovery"] = 0.20
            score += 0.20
        elif len(recovered) >= 2:
            breakdown["partial_recovery"] = 0.10
            score += 0.10

        # Incident formally resolved?
        if state.get("incident_resolved", False):
            breakdown["incident_resolved"] = 0.15
            score += 0.15

        # Efficiency bonus
        steps = len(history)
        if steps <= 4:
            breakdown["efficiency_bonus"] = 0.10
            score += 0.10
        elif steps <= 7:
            breakdown["efficiency_bonus"] = 0.07
            score += 0.07
        elif steps <= 12:
            breakdown["efficiency_bonus"] = 0.03
            score += 0.03

        # Penalty for wrong rollbacks
        wrong_rollbacks = state.get("wrong_rollbacks", 0)
        if wrong_rollbacks > 0:
            p = min(wrong_rollbacks * 0.08, 0.20)
            breakdown["wrong_rollback_penalty"] = p
            score -= p

        score = round(min(max(score, 0.0), 1.0), 4)
        return self.clamp_score_strict(score), breakdown