118 lines
3.7 KiB
Python
118 lines
3.7 KiB
Python
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(),
|
|
}
|