Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-12 09:36:18

0001 """Publish testbed workflow activity to Snapper (Phase 6 component).
0002 
0003 The testbed's workflow layer — executions launched per namespace, and
0004 the STF prompt-processing PanDA tasks they submit (processingtype
0005 stfprocessing) — becomes a contracted Snapper component, so the Time
0006 history shows workflow activity as curves alongside the datataking
0007 lanes: executions in flight, and STF task counts split by target site,
0008 which is the decision box's site assignment made visible.
0009 """
0010 
0011 import json
0012 from dataclasses import dataclass
0013 from datetime import timedelta
0014 
0015 from django.db import connections, transaction
0016 from django.utils import timezone
0017 
0018 from snapper_ai.services import ComponentUpdate, publish_component, register_component
0019 
0020 from .panda.constants import PANDA_SCHEMA
0021 from .workflow_models import WorkflowExecution
0022 
0023 
0024 PUBLISHER_IDENTITY = "swf-monitor:workflow-activity"
0025 ASSESSMENT_POLICY_VERSION = "swf-workflow-activity-v1"
0026 MAX_MAP_ITEMS = 32
0027 MAX_SERIALIZED_BYTES = 32 * 1024
0028 STF_PROCESSING_TYPE = "stfprocessing"
0029 
0030 # JEDI terminal task states; nonterminal STF tasks are the in-flight set.
0031 TASK_TERMINAL_STATUSES = (
0032     "done", "finished", "failed", "aborted", "broken", "exhausted",
0033 )
0034 
0035 WORKFLOW_REGISTRATION = {
0036     "title": "Testbed workflow activity",
0037     "description": (
0038         "Workflow executions and their STF prompt-processing PanDA tasks "
0039         "(processingtype stfprocessing), with task counts split by target "
0040         "site — the decision box's site assignment as recorded state."
0041     ),
0042     "visibility": "public",
0043     "owning_subsystem": "SWF testbed workflow layer",
0044     "assessment_policy": ASSESSMENT_POLICY_VERSION,
0045     "max_serialized_bytes": MAX_SERIALIZED_BYTES,
0046     "quantities": {
0047         "executions_active": {
0048             "path": "executions.active",
0049             "type": "integer",
0050             "required": True,
0051             "kind": "gauge",
0052             "description": "Workflow executions currently in running status.",
0053         },
0054         "executions_started_24h": {
0055             "path": "executions.started_24h",
0056             "type": "integer",
0057             "required": True,
0058             "kind": "window_count",
0059             "description": "Workflow executions started in the trailing 24 hours.",
0060         },
0061         "executions_by_workflow": {
0062             "path": "executions.by_workflow",
0063             "type": "object",
0064             "required": True,
0065             "kind": "bounded_map",
0066             "max_items": MAX_MAP_ITEMS,
0067             "description": (
0068                 "Trailing-24-hour execution starts by workflow name."
0069             ),
0070         },
0071         "stf_tasks_in_flight": {
0072             "path": "stf_tasks.in_flight_total",
0073             "type": "integer",
0074             "required": True,
0075             "kind": "gauge",
0076             "description": "STF processing PanDA tasks in nonterminal states.",
0077         },
0078         "stf_tasks_by_site_status": {
0079             "path": "stf_tasks.by_site_status",
0080             "type": "object",
0081             "required": True,
0082             "kind": "bounded_map",
0083             "max_items": MAX_MAP_ITEMS,
0084             "description": (
0085                 "In-flight STF processing task counts keyed site/status — "
0086                 "the decision box's per-site assignment."
0087             ),
0088         },
0089         "stf_tasks_modified_24h": {
0090             "path": "stf_tasks.modified_24h",
0091             "type": "object",
0092             "required": True,
0093             "kind": "bounded_map",
0094             "max_items": MAX_MAP_ITEMS,
0095             "description": (
0096                 "Trailing-24-hour STF processing task counts keyed "
0097                 "site/status, terminal states included."
0098             ),
0099         },
0100     },
0101 }
0102 
0103 
0104 @dataclass(frozen=True)
0105 class WorkflowPublication:
0106     registration_update: ComponentUpdate
0107     update: ComponentUpdate
0108     projection: dict
0109 
0110 
0111 def _bounded_map(pairs, label) -> dict:
0112     ordered = sorted(pairs.items(), key=lambda item: (-item[1], item[0]))
0113     if len(ordered) > MAX_MAP_ITEMS:
0114         kept = ordered[: MAX_MAP_ITEMS - 1]
0115         other = sum(count for _, count in ordered[MAX_MAP_ITEMS - 1:])
0116         ordered = kept + [(f"other {label}", other)]
0117     return dict(ordered)
0118 
0119 
0120 def _execution_activity(now) -> dict:
0121     active = WorkflowExecution.objects.filter(status="running").count()
0122     day_ago = now - timedelta(hours=24)
0123     started = (
0124         WorkflowExecution.objects.filter(start_time__gte=day_ago)
0125         .values_list("workflow_definition__workflow_name", flat=True)
0126     )
0127     by_workflow: dict[str, int] = {}
0128     for name in started:
0129         key = str(name or "unknown")
0130         by_workflow[key] = by_workflow.get(key, 0) + 1
0131     return {
0132         "active": active,
0133         "started_24h": sum(by_workflow.values()),
0134         "by_workflow": _bounded_map(by_workflow, "workflows"),
0135     }
0136 
0137 
0138 def _stf_task_counts(where_sql: str, params) -> dict[str, int]:
0139     sql = f"""
0140         SELECT COALESCE("site", 'unknown'),
0141                COALESCE("status", 'unknown'),
0142                COUNT(*)
0143         FROM "{PANDA_SCHEMA}"."jedi_tasks"
0144         WHERE "processingtype" = %s AND ({where_sql})
0145         GROUP BY 1, 2
0146     """
0147     with connections["panda"].cursor() as cursor:
0148         cursor.execute(sql, [STF_PROCESSING_TYPE, *params])
0149         return {
0150             f"{site}/{status}": int(count or 0)
0151             for site, status, count in cursor.fetchall()
0152         }
0153 
0154 
0155 def _stf_task_activity(now) -> dict:
0156     placeholders = ", ".join(["%s"] * len(TASK_TERMINAL_STATUSES))
0157     in_flight = _stf_task_counts(
0158         f'"status" IS NULL OR "status" NOT IN ({placeholders})',
0159         list(TASK_TERMINAL_STATUSES))
0160     modified = _stf_task_counts(
0161         '"modificationtime" >= %s', [now - timedelta(hours=24)])
0162     return {
0163         "in_flight_total": sum(in_flight.values()),
0164         "by_site_status": _bounded_map(in_flight, "site/status"),
0165         "modified_24h": _bounded_map(modified, "site/status"),
0166     }
0167 
0168 
0169 def workflow_projection(now=None) -> dict:
0170     now = now or timezone.now()
0171     return {
0172         "executions": _execution_activity(now),
0173         "stf_tasks": _stf_task_activity(now),
0174     }
0175 
0176 
0177 def publish_workflow_activity() -> WorkflowPublication:
0178     """Query and atomically publish testbed workflow activity."""
0179     projection = workflow_projection()
0180     assessed_at = timezone.now()
0181     with transaction.atomic():
0182         registration_update = register_component(
0183             scope="testbed",
0184             name="workflow",
0185             publisher_identity=PUBLISHER_IDENTITY,
0186             registration=WORKFLOW_REGISTRATION,
0187             component_schema_version=1,
0188         )
0189         update = publish_component(
0190             scope="testbed",
0191             name="workflow",
0192             publisher_identity=PUBLISHER_IDENTITY,
0193             data=projection,
0194             assessed_at=assessed_at,
0195             assessment_policy_version=ASSESSMENT_POLICY_VERSION,
0196         )
0197     return WorkflowPublication(
0198         registration_update=registration_update,
0199         update=update,
0200         projection=projection,
0201     )
0202 
0203 
0204 def compact_workflow_publication_report(
0205     publication: WorkflowPublication,
0206 ) -> str:
0207     """Render a bounded operator-facing publication report."""
0208     executions = publication.projection["executions"]
0209     stf_tasks = publication.projection["stf_tasks"]
0210     return json.dumps({
0211         "revision": publication.update.revision,
0212         "content_changed": publication.update.content_changed,
0213         "executions_active": executions["active"],
0214         "executions_started_24h": executions["started_24h"],
0215         "stf_tasks_in_flight": stf_tasks["in_flight_total"],
0216     }, indent=2, sort_keys=True)