Skip to content

Timeouts & Cancellation

Overview

The Timeout module provides a robust and flexible way to enforce time limits on operations. This is a critical resilience pattern that prevents requests from hanging indefinitely, which could otherwise consume and exhaust system resources.

The key feature of Athomic's implementation is Contextual Timeout Cancellation. This allows a single, top-level timeout to be respected across a deep call stack of multiple, nested asynchronous operations.


How It Works: Contextual Timeout Cancellation

The system works through the coordinated use of two decorators and the Context module.

  1. Setting the Deadline with @timeout: You apply the main @timeout(seconds=...) decorator to a high-level function, like an API route handler. When this function is called, the decorator records an absolute deadline timestamp (current time + timeout seconds) in the current ExecutionContext.

  2. Enforcing the Deadline with @cancellable_operation: You apply the @cancellable_operation decorator to lower-level, internal functions that are called within the initial @timeout block (e.g., service methods, repository calls).

  3. Dynamic Cancellation: Each @cancellable_operation decorator reads the deadline from the context, calculates the remaining time, and applies that remaining time as its own timeout for the function it wraps.

This creates a powerful effect: if an API endpoint has a 30-second timeout, and the first 20 seconds are spent in one service call, a subsequent cancellable operation will automatically have a timeout of less than 10 seconds to ensure the total deadline is met. If the deadline has already passed, it will be cancelled immediately.


Usage

1. The @timeout Decorator

Use this decorator on top-level functions that represent a complete unit of work, like an API endpoint handler. This sets the overall deadline for the entire operation.

from nala.athomic.resilience.timeout import timeout, TimeoutException

@router.post("/process-data")
@timeout(seconds=15.0)
async def process_data_endpoint(data: dict):
    """
    This entire request, including all downstream calls,
    must complete within 15 seconds.
    """
    try:
        # This service call is decorated with @cancellable_operation
        result = await data_service.process(data)
        return {"status": "success", "result": result}
    except TimeoutException:
        return {"status": "error", "message": "Processing timed out."}

2. The @cancellable_operation Decorator

Use this decorator on internal functions that are part of a larger workflow initiated by a function with a @timeout.

from nala.athomic.resilience.timeout import cancellable_operation

class DataService:
    @cancellable_operation
    async def process(self, data: dict):
        # This operation will respect the deadline set by the
        # @timeout decorator on the API endpoint.
        await self.repository.save(data)
        await self.external_api.notify(data)

3. The @dynamic_timeout Decorator

As an alternative to @timeout, you can use @dynamic_timeout if you need the timeout duration to be specified at call time rather than at decoration time.

from nala.athomic.resilience.timeout import dynamic_timeout

@dynamic_timeout(default=10.0)
async def fetch_data(url: str):
    # ...
    pass

# This call will use a 5-second timeout
await fetch_data("[http://example.com](http://example.com)", seconds=5.0)

# This call will use the 10-second default
await fetch_data("[http://another-example.com](http://another-example.com)")

Configuration

This module does not require any configuration in settings.toml. All timeout behavior is defined directly in the code via decorator arguments.


API Reference

nala.athomic.resilience.timeout.decorators.timeout(seconds, fallback=None, *, preserve_context=False)

Decorator to apply a maximum execution time limit (timeout) to synchronous or asynchronous functions.

The timeout uses the application's internal mechanism (run_with_timeout) to handle both concurrency types and ensures consistent context propagation and cleanup.

Parameters:

Name Type Description Default
seconds float

The total timeout duration in seconds.

required
fallback Optional[Callable]

An optional zero-argument callable (sync or async) to execute if the primary function times out.

None
preserve_context bool

Internal flag. If True, skips cleaning the timeout deadline from the context upon exiting the wrapper (primarily for testing purposes).

False

Returns:

Type Description
Callable[[F], F]

Callable[[F], F]: The wrapped function with timeout behavior.

nala.athomic.resilience.timeout.cancellation.cancellable_operation(func)

Decorator that wraps an asynchronous operation to respect a global timeout deadline set earlier in the execution context by the @timeout decorator.

This mechanism enforces dynamic timeout cancellation: if the remaining time until the context deadline is less than the function's execution time, asyncio.wait_for is used to enforce the remaining duration.

Parameters:

Name Type Description Default
func Callable[..., Awaitable[Any]]

The asynchronous function to wrap.

required

Returns:

Type Description
Callable[..., Awaitable[Any]]

Callable[..., Awaitable[Any]]: The wrapped function with cancellation logic.

Raises:

Type Description
CancelledException

If the deadline has already passed before the function starts.

TimeoutException

If the function exceeds the remaining time before the deadline.

nala.athomic.resilience.timeout.exceptions.TimeoutException

Bases: Exception

Raised when the execution of a function exceeds the defined timeout.

nala.athomic.resilience.timeout.exceptions.CancelledException

Bases: Exception

Raised when execution is cancelled gracefully before timeout.