from __future__ import annotations from datetime import datetime, timedelta, timezone STEP_SETTINGS_GROUP = { "publish": "publish", "comment": "comment", } def parse_iso(value: str | None) -> datetime | None: if not value: return None try: return datetime.fromisoformat(value) except ValueError: return None def retry_schedule_seconds( settings: dict[str, object], *, count_key: str, backoff_key: str, default_count: int, default_backoff: int, ) -> list[int]: raw_schedule = settings.get("retry_schedule_minutes") if isinstance(raw_schedule, list): schedule: list[int] = [] for item in raw_schedule: if isinstance(item, int) and not isinstance(item, bool) and item >= 0: schedule.append(item * 60) if schedule: return schedule retry_count = settings.get(count_key, default_count) retry_count = retry_count if isinstance(retry_count, int) and not isinstance(retry_count, bool) else default_count retry_count = max(retry_count, 0) retry_backoff = settings.get(backoff_key, default_backoff) retry_backoff = retry_backoff if isinstance(retry_backoff, int) and not isinstance(retry_backoff, bool) else default_backoff retry_backoff = max(retry_backoff, 0) return [retry_backoff] * retry_count def publish_retry_schedule_seconds(settings: dict[str, object]) -> list[int]: return retry_schedule_seconds( settings, count_key="retry_count", backoff_key="retry_backoff_seconds", default_count=5, default_backoff=300, ) def comment_retry_schedule_seconds(settings: dict[str, object]) -> list[int]: return retry_schedule_seconds( settings, count_key="max_retries", backoff_key="base_delay_seconds", default_count=5, default_backoff=180, ) def retry_meta_for_step(step, settings_by_group: dict[str, object]) -> dict[str, object] | None: # type: ignore[no-untyped-def] if getattr(step, "status", None) != "failed_retryable" or getattr(step, "retry_count", 0) <= 0: return None step_name = getattr(step, "step_name", None) settings_group = STEP_SETTINGS_GROUP.get(step_name) if settings_group is None: return None group_settings = settings_by_group.get(settings_group, {}) if not isinstance(group_settings, dict): group_settings = {} if step_name == "publish": schedule = publish_retry_schedule_seconds(group_settings) elif step_name == "comment": schedule = comment_retry_schedule_seconds(group_settings) else: return None attempt_index = step.retry_count - 1 if attempt_index >= len(schedule): return { "retry_due": False, "retry_exhausted": True, "retry_wait_seconds": None, "retry_remaining_seconds": None, "next_retry_at": None, } wait_seconds = schedule[attempt_index] reference = parse_iso(getattr(step, "finished_at", None)) or parse_iso(getattr(step, "started_at", None)) if reference is None: return { "retry_due": True, "retry_exhausted": False, "retry_wait_seconds": wait_seconds, "retry_remaining_seconds": 0, "next_retry_at": datetime.now(timezone.utc).isoformat(), } next_retry_at = reference + timedelta(seconds=wait_seconds) now = datetime.now(timezone.utc) remaining_seconds = max(int((next_retry_at - now).total_seconds()), 0) return { "retry_due": now >= next_retry_at, "retry_exhausted": False, "retry_wait_seconds": wait_seconds, "retry_remaining_seconds": remaining_seconds, "next_retry_at": next_retry_at.isoformat(), }