Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-21 08:46:24

0001 #!/usr/bin/env python3
0002 """Evaluate one coherent Snapper capture opportunity for SWF scopes."""
0003 
0004 import argparse
0005 import json
0006 import os
0007 import sys
0008 import time
0009 from datetime import datetime, timedelta
0010 
0011 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
0012 sys.path.insert(0, os.path.join(THIS_DIR, '..', 'src'))
0013 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_monitor_project.settings')
0014 
0015 import django  # noqa: E402
0016 django.setup()
0017 
0018 from django.utils import timezone  # noqa: E402
0019 
0020 from monitor_app.models import SysConfig  # noqa: E402
0021 from snapper_ai.capture import (  # noqa: E402
0022     aligned_boundary,
0023     capture_scope,
0024     report_capture_failure,
0025 )
0026 from snapper_ai.models import CaptureCursor  # noqa: E402
0027 
0028 
0029 SCOPES = ('testbed', 'epicprod')
0030 DEFAULT_OPPORTUNITY_SECONDS = 10
0031 DEFAULT_BASELINE_EVERY = 10
0032 DEFAULT_LOCK_TIMEOUT_MS = 5000
0033 
0034 
0035 def _boundary(value):
0036     if not value:
0037         return None
0038     normalized = value[:-1] + '+00:00' if value.endswith('Z') else value
0039     try:
0040         parsed = datetime.fromisoformat(normalized)
0041     except ValueError as exc:
0042         raise argparse.ArgumentTypeError(str(exc)) from exc
0043     if not timezone.is_aware(parsed):
0044         raise argparse.ArgumentTypeError('boundary must include a timezone')
0045     return parsed
0046 
0047 
0048 def _positive_setting(value, key, minimum=1):
0049     try:
0050         value = int(value)
0051     except (TypeError, ValueError) as exc:
0052         raise ValueError(f'{key} must be an integer, got {value!r}') from exc
0053     if value < minimum:
0054         raise ValueError(f'{key} must be at least {minimum}, got {value!r}')
0055     return value
0056 
0057 
0058 def _scope_config(scope, args):
0059     opportunity_key = f'snapper_opportunity_seconds_{scope}'
0060     baseline_key = f'snapper_baseline_every_{scope}'
0061     policy_key = f'snapper_capture_policy_{scope}'
0062     opportunity = (
0063         args.opportunity_seconds
0064         if args.opportunity_seconds is not None
0065         else SysConfig.get_setting(
0066             opportunity_key, DEFAULT_OPPORTUNITY_SECONDS)
0067     )
0068     baseline = (
0069         args.baseline_every
0070         if args.baseline_every is not None
0071         else SysConfig.get_setting(baseline_key, DEFAULT_BASELINE_EVERY)
0072     )
0073     policy = (
0074         args.capture_policy
0075         if args.capture_policy is not None
0076         else SysConfig.get_setting(policy_key, f'{scope}-v1')
0077     )
0078     lock_timeout = (
0079         args.lock_timeout_ms
0080         if args.lock_timeout_ms is not None
0081         else SysConfig.get_setting(
0082             'snapper_lock_timeout_ms', DEFAULT_LOCK_TIMEOUT_MS)
0083     )
0084     return {
0085         'opportunity_seconds': _positive_setting(
0086             opportunity, opportunity_key, minimum=10),
0087         'baseline_every': _positive_setting(baseline, baseline_key),
0088         'capture_policy': str(policy or '').strip(),
0089         'lock_timeout_ms': _positive_setting(
0090             lock_timeout, 'snapper_lock_timeout_ms'),
0091     }
0092 
0093 
0094 def _target_boundary(scope, args, opportunity_seconds):
0095     if args.boundary is not None:
0096         return args.boundary
0097     now = args.requested_at or timezone.now()
0098     boundary = aligned_boundary(now, opportunity_seconds)
0099     if not args.manual:
0100         return boundary
0101     latest = CaptureCursor.objects.filter(scope=scope).values_list(
0102         'latest_boundary_at', flat=True).first()
0103     if latest is not None and latest >= boundary:
0104         boundary += timedelta(seconds=opportunity_seconds)
0105     delay = (boundary - timezone.now()).total_seconds()
0106     if delay > 0:
0107         time.sleep(delay)
0108     return boundary
0109 
0110 
0111 def _result_payload(scope, result, config):
0112     return {
0113         'scope': scope,
0114         'boundary_at': result.boundary_at.isoformat(),
0115         'outcome': result.outcome,
0116         'reasons': list(result.reasons),
0117         'changed_components': list(result.changed_components),
0118         'coverage_gap_started_at': (
0119             result.coverage_gap_started_at.isoformat()
0120             if result.coverage_gap_started_at else None
0121         ),
0122         'snap_id': str(result.snap.pk) if result.snap else None,
0123         'state_hash': result.snap.state_hash if result.snap else None,
0124         'capture_policy': config['capture_policy'],
0125         'opportunity_seconds': config['opportunity_seconds'],
0126         'baseline_every': config['baseline_every'],
0127     }
0128 
0129 
0130 def main(argv):
0131     ap = argparse.ArgumentParser(
0132         description='Evaluate one SWF Snapper capture opportunity.')
0133     ap.add_argument('--scope', choices=(*SCOPES, 'all'), default='all')
0134     ap.add_argument('--manual', action='store_true',
0135                     help='Force a full snap at this boundary.')
0136     ap.add_argument('--boundary', type=_boundary,
0137                     help='Aligned ISO timestamp; current boundary by default.')
0138     ap.add_argument('--requested-at', type=_boundary,
0139                     help='Scheduler invocation time used to select the boundary.')
0140     ap.add_argument('--opportunity-seconds', type=int,
0141                     help='Override the scope SysConfig value.')
0142     ap.add_argument('--baseline-every', type=int,
0143                     help='Override the scope SysConfig value.')
0144     ap.add_argument('--capture-policy',
0145                     help='Override the scope SysConfig policy identifier.')
0146     ap.add_argument('--lock-timeout-ms', type=int,
0147                     help='Override the shared SysConfig lock timeout.')
0148     args = ap.parse_args(argv[1:])
0149 
0150     scopes = SCOPES if args.scope == 'all' else (args.scope,)
0151     reports = []
0152     failed = False
0153     for scope in scopes:
0154         boundary = None
0155         config = None
0156         try:
0157             config = _scope_config(scope, args)
0158             if not config['capture_policy']:
0159                 raise ValueError(
0160                     f'snapper_capture_policy_{scope} must not be blank')
0161             boundary = _target_boundary(
0162                 scope, args, config['opportunity_seconds'])
0163             result = capture_scope(
0164                 scope=scope,
0165                 boundary_at=boundary,
0166                 capture_policy=config['capture_policy'],
0167                 opportunity_seconds=config['opportunity_seconds'],
0168                 baseline_every=config['baseline_every'],
0169                 manual=args.manual,
0170                 lock_timeout_ms=config['lock_timeout_ms'],
0171             )
0172         except Exception as exc:
0173             failed = True
0174             report = {
0175                 'scope': scope,
0176                 'boundary_at': boundary.isoformat() if boundary else None,
0177                 'outcome': 'failed',
0178                 'error': str(exc),
0179             }
0180             if boundary is not None:
0181                 try:
0182                     report_capture_failure(
0183                         scope=scope,
0184                         boundary_at=boundary,
0185                         error=str(exc),
0186                     )
0187                 except Exception as report_exc:
0188                     report['failure_report_error'] = str(report_exc)
0189             if config is not None:
0190                 report.update({
0191                     'capture_policy': config['capture_policy'],
0192                     'opportunity_seconds': config['opportunity_seconds'],
0193                     'baseline_every': config['baseline_every'],
0194                 })
0195             reports.append(report)
0196         else:
0197             reports.append(_result_payload(scope, result, config))
0198 
0199     print(json.dumps(reports, indent=2, sort_keys=True))
0200     return 1 if failed else 0
0201 
0202 
0203 if __name__ == '__main__':
0204     sys.exit(main(sys.argv))