Skip to content

Retry backoff and progress

After the streaming introduction, use this finite example to add selected retries and progress snapshots. One handler call fails once, then succeeds after capped exponential backoff with jitter. The HTTP example imports the same retry_delay callback.

uv run --locked python -m examples.retry_progress
"""Bounded asynchronous input, retries, and progress counters."""

import asyncio
import logging
from collections import Counter
from random import uniform

from aqute import Aqute

logger = logging.getLogger(__name__)
FAIL_ONCE = 3


def retry_delay(failed_attempt: int, _error: Exception) -> float:
    ceiling = min(5.0, 0.01 * 2 ** min(failed_attempt - 1, 10))
    return uniform(0.0, ceiling)


async def main() -> list[int]:
    attempts: Counter[int] = Counter()

    async def source():
        for value in range(8):
            yield value

    async def handle(value: int) -> int:
        attempts[value] += 1
        if value == FAIL_ONCE and attempts[value] == 1:
            raise ValueError("transient example failure")
        return value * 2

    engine = Aqute(
        handle,
        3,
        input_task_queue_size=4,
        result_queue=asyncio.Queue(4),
        retry_count=2,
        retry_delay=retry_delay,
        specific_errors_to_retry=ValueError,
    )
    values = []
    async with engine.iter_results(source()) as results:
        async for task in results:
            if task.error is not None:
                raise task.error
            assert task.result is not None
            values.append(task.result)
            counts = engine.counters
            logger.info(
                "Pending=%s running=%s succeeded=%s failed=%s retries=%s",
                counts.pending,
                counts.running,
                counts.succeeded,
                counts.failed,
                counts.retries,
            )
    assert sorted(values) == [value * 2 for value in range(8)]
    return values


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    logging.info("Results: %s", sorted(asyncio.run(main())))

retry_count=2 permits two extra attempts. This example selects ValueError; applications must select safe retry errors and operations for their own handlers. See retry rules for delay and error contracts.

Read engine.counters during iteration. Helpers stop on context exit, which resets the counters. See progress counters for each field.

This small example collects eight results to verify its output. Its result list and per-input attempt counts grow with the input length, outside Aqute's queue bounds. Use the introductory recipe's incremental consumption for larger inputs.