File indexing completed on 2026-08-12 09:36:21
0001 """Backfill workflow episodes from recorded messages.
0002
0003 Replays a past execution's recorded bus messages through the same
0004 definitions and engine the live builder runs, then drives the
0005 completion pass to closure. The episode carries the recorded times,
0006 not the replay time. Bounded by message-log retention.
0007
0008 Usage (testbed venv, SWF_MONITOR_HTTP_URL and SWF_API_TOKEN in the
0009 environment):
0010
0011 python -m episodes.backfill <execution_id> [<execution_id> ...]
0012 """
0013
0014 import os
0015 import sys
0016 import time
0017
0018 import requests
0019
0020 from swf_common_lib.episodes import EpisodeBuilder, MonitorEpisodeIngest
0021
0022 from . import ALL_DEFINITIONS
0023
0024 COMPLETION_ATTEMPTS = 60
0025 COMPLETION_INTERVAL_SECONDS = 5
0026
0027
0028 def backfill(execution_id: str) -> bool:
0029 base = (os.environ.get('SWF_MONITOR_HTTP_URL') or '').rstrip('/')
0030 token = os.environ.get('SWF_API_TOKEN') or ''
0031 if not base:
0032 print('SWF_MONITOR_HTTP_URL is not set')
0033 return False
0034
0035 session = requests.Session()
0036 if token:
0037 session.headers['Authorization'] = f'Token {token}'
0038 response = session.get(f'{base}/api/workflow-messages/',
0039 params={'execution_id': execution_id},
0040 timeout=60)
0041 response.raise_for_status()
0042 rows = response.json()
0043 if isinstance(rows, dict):
0044 rows = rows.get('results') or []
0045 rows.sort(key=lambda row: row.get('sent_at') or '')
0046 messages = []
0047 for row in rows:
0048 content = row.get('message_content') or {}
0049 if content.get('execution_id') != execution_id:
0050 continue
0051
0052
0053 if row.get('sent_at'):
0054 content.setdefault('sent_at', row['sent_at'])
0055 messages.append(content)
0056 if not messages:
0057 print(f'{execution_id}: no recorded messages')
0058 return False
0059
0060 ingest = MonitorEpisodeIngest(base_url=base, token=token,
0061 builder_identity='episode-backfill')
0062 builder = EpisodeBuilder(
0063 [definition() for definition in ALL_DEFINITIONS], ingest)
0064 handled = sum(1 for m in messages if builder.handle_message(m))
0065 print(f'{execution_id}: {handled}/{len(messages)} recorded messages '
0066 f'replayed')
0067
0068 for _ in range(COMPLETION_ATTEMPTS):
0069 builder.tick()
0070 if not builder.active:
0071 print(f'{execution_id}: episode closed')
0072 return True
0073 time.sleep(COMPLETION_INTERVAL_SECONDS)
0074 print(f'{execution_id}: completion did not converge')
0075 return False
0076
0077
0078 if __name__ == '__main__':
0079 if len(sys.argv) < 2:
0080 print(__doc__)
0081 sys.exit(2)
0082 results = [backfill(execution_id) for execution_id in sys.argv[1:]]
0083 sys.exit(0 if all(results) else 1)