Skip to main content
retry adds per-attempt timeout, retry/backoff, and optional semaphore-based concurrency control around async and sync callables. It works for ordinary functions without creating an EventBus. Sync callables stay sync and block synchronously for retry sleeps and semaphore waits. Async callables stay async and await retry sleeps and semaphore waits. Supported callable forms:
  • Python: sync and async functions, methods, and lambdas via @retry(...) or retry(...)(fn).
  • TypeScript: sync and async functions and arrow functions via retry(...)(fn), plus sync and async class methods via @retry(...).
  • Rust: sync and async free functions and methods via abxbus::retry!; call those retried functions from Rust closures such as EventBus handlers.
  • Go: retry is not implemented yet.

Signature

def retry(
    retry_after: float = 0,
    max_attempts: int = 1,
    timeout: float | None = None,
    slow_timeout: float | None = None,
    retry_on_errors: list[type[Exception] | re.Pattern[str]] | tuple[type[Exception] | re.Pattern[str], ...] | None = None,
    retry_backoff_factor: float = 1.0,
    semaphore_limit: int | None = None,
    semaphore_name: str | Callable[..., str] | None = None,
    semaphore_lax: bool = True,
    semaphore_scope: Literal['multiprocess', 'global', 'class', 'instance'] = 'global',
    semaphore_timeout: float | None = None,
) -> Callable[[Callable[P, T]], Callable[P, T]]

Options

OptionDescription
max_attemptsTotal attempts including the first call (1 disables retries).
retry_afterBase delay between retries, in seconds.
retry_backoff_factorDelay multiplier applied after each failed attempt.
retry_on_errorsOptional matcher list to restrict which errors are retried.
timeoutPer-attempt timeout in seconds (None/undefined means no per-attempt timeout).
slow_timeoutWarning threshold for a decorated call, in seconds. Warnings are throttled to at most one every 2 seconds per decorated method/function.
semaphore_limitMax concurrent executions sharing the same semaphore.
semaphore_nameSemaphore key (string or function deriving a key from call args).
semaphore_scopeSemaphore sharing scope (multiprocess, global, class, instance).
semaphore_timeoutMax wait time for semaphore acquisition before timeout/lax fallback.
semaphore_laxIf true, continue execution without semaphore limit when acquisition times out.

Example: Standalone function

from abxbus.retry import retry

@retry(max_attempts=3, retry_after=1, timeout=5)
async def fetch_with_retry(url: str) -> dict:
    return await fetch_json(url)

Example: Inline wrapper

from abxbus import EventBus, BaseEvent
from abxbus.retry import retry

class FetchEvent(BaseEvent[dict]):
    url: str

bus = EventBus('AppBus')

async def fetch_with_retry(event: FetchEvent) -> dict:
    return await fetch_json(event.url)

bus.on(
    FetchEvent,
    retry(max_attempts=3, retry_after=1, timeout=5)(fetch_with_retry),
)

Example: Decorated class method

from abxbus.retry import retry

class ApiService:
    @retry(max_attempts=4, retry_after=1, timeout=10, semaphore_limit=2, semaphore_scope='class')
    async def get_user(self, user_id: str) -> dict:
        return await call_remote_api(user_id)

Behavior

  • Semaphore acquisition happens once per call, then all retry attempts run within that acquired slot.
  • Backoff delay per retry is: retry_after * retry_backoff_factor^(attempt - 1).
  • Retries stop immediately when the thrown error does not match retry_on_errors.
  • Bus/event timeouts act as outer execution budgets; retry.timeout is per-attempt.
  • Sync wrappers return synchronously and block synchronously for retry sleeps and semaphore waits.
  • Rust retry functions return Result<T, E> where E: From<abxbus::retry::RetryError>.

Runtime differences

  • Python and TypeScript both support multiprocess, global, class, and instance.
  • Rust supports multiprocess, global, class, and instance through abxbus::retry!.
  • Go does not include retry yet.
  • TypeScript uses async-context re-entrancy tracking in Node/Bun to avoid same-semaphore nested deadlocks.
  • retry_on_errors matching differs slightly:
    • Python: exception classes or compiled regex patterns (matched against "ErrorClass: message").
    • TypeScript: error constructors, error-name strings, or regex patterns.
    • Rust: typed predicate function via retry_if = should_retry.