Skip to content

ExponentialRetry + Cron/Perpetual: exhausted retries cause endless rescheduling #441

Description

@amenck

Bug

When a Cron or Perpetual task uses ExponentialRetry with a finite attempts count and the task fails persistently, the retries exhaust as expected — but then the task is rescheduled for a fresh cycle with attempt reset to 1. The entire retry sequence repeats every cycle, forever. There is no built-in way to say "retries are exhausted, stop scheduling this task."

A secondary issue makes subclassing ExponentialRetry to work around this harder than it should be: ExponentialRetry.__aenter__ constructs ExponentialRetry(...) rather than type(self)(...), so subclass method overrides are silently dropped at runtime unless __aenter__ is also overridden.

Repro

import asyncio
from datetime import timedelta

from docket import Docket
from docket.dependencies import ExponentialRetry, Perpetual
from docket.worker import Worker

fail_count = 0


async def main() -> None:
    global fail_count

    async with Docket(name="repro", url="memory://") as docket:

        async def always_failing(
            _perpetual: Perpetual = Perpetual(every=timedelta(0)),
            _retry: ExponentialRetry = ExponentialRetry(
                attempts=3,
                minimum_delay=timedelta(0),
                maximum_delay=timedelta(0),
            ),
        ) -> None:
            global fail_count
            fail_count += 1
            raise ValueError(f"attempt {fail_count}")

        docket.register(always_failing, names=["always-failing"])
        await docket.add(always_failing)()

        async with Worker(docket, schedule_automatic_tasks=False) as worker:
            try:
                await asyncio.wait_for(worker.run_until_finished(), timeout=2)
            except asyncio.TimeoutError:
                pass

    # Expected: 3 (one per attempt)
    # Actual: hundreds
    print(f"Total executions: {fail_count}")
    assert fail_count == 3, f"Expected 3, got {fail_count}"


asyncio.run(main())

Environment

  • pydocket 0.21.1 (behavior unchanged on 0.22.0)
  • Python 3.11

Happy to contribute a fix here, if we all agree that this is not the intended behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions