Back to home page

EIC code displayed by LXR

 
 

    


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

0001 """Terminalize workflow-execution rows abandoned as 'running'.
0002 
0003 Before the runner finalized its execution record on every exit path
0004 (swf-testbed workflow_runner.py, 2026-07-24), a crashed or killed run
0005 left its row claiming 'running' forever, and every surface reading the
0006 record repeated the lie ("26 executions running" beside all-idle
0007 lanes). This one-shot repair marks such rows terminated, with the end
0008 time taken from the run record's last activity for the execution's run
0009 when one exists, else the execution's own start time. The runner fix
0010 prevents new ones; there is no scheduled janitor — a system needing
0011 one is writing garbage.
0012 
0013 Dry-run by default; --apply writes. A row is stuck when status is
0014 'running' with no end_time and a start older than --hours (default 12).
0015 """
0016 
0017 from datetime import timedelta
0018 
0019 from django.core.management.base import BaseCommand
0020 from django.db.models.fields.json import KeyTextTransform
0021 from django.utils import timezone
0022 
0023 from monitor_app.models import RunState, SystemStateEvent
0024 from monitor_app.workflow_models import WorkflowExecution
0025 
0026 
0027 class Command(BaseCommand):
0028     help = __doc__
0029 
0030     def add_arguments(self, parser):
0031         parser.add_argument('--apply', action='store_true',
0032                             help='write the repair (default: report only)')
0033         parser.add_argument('--hours', type=float, default=12,
0034                             help='running-with-no-end age threshold')
0035 
0036     def handle(self, *args, **options):
0037         cutoff = timezone.now() - timedelta(hours=options['hours'])
0038         stuck = list(
0039             WorkflowExecution.objects
0040             .filter(status='running', end_time__isnull=True,
0041                     start_time__lt=cutoff)
0042             .order_by('start_time'))
0043         execution_runs = {}
0044         for run_number, execution_key in (
0045                 RunState.objects
0046                 .annotate(execution_key=KeyTextTransform(
0047                     'execution_id', 'metadata'))
0048                 .exclude(execution_key__isnull=True)
0049                 .values_list('run_number', 'execution_key')):
0050             execution_runs.setdefault(execution_key, run_number)
0051 
0052         repaired = 0
0053         for execution in stuck:
0054             run_number = execution_runs.get(execution.execution_id)
0055             last_activity = None
0056             if run_number is not None:
0057                 last_activity = (
0058                     SystemStateEvent.objects
0059                     .filter(run_number=run_number)
0060                     .order_by('-timestamp')
0061                     .values_list('timestamp', flat=True)
0062                     .first())
0063             end_time = last_activity or execution.start_time
0064             self.stdout.write(
0065                 f'{execution.execution_id}: running since '
0066                 f'{execution.start_time:%Y-%m-%d %H:%M} -> terminated '
0067                 f'at {end_time:%Y-%m-%d %H:%M}')
0068             if options['apply']:
0069                 execution.status = 'terminated'
0070                 execution.end_time = end_time
0071                 execution.save(update_fields=['status', 'end_time'])
0072             repaired += 1
0073 
0074         mode = 'APPLIED' if options['apply'] else 'DRY RUN'
0075         self.stdout.write(f'{mode}: {repaired} stuck executions')