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.
-
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 absolutedeadlinetimestamp (current time + timeout seconds) in the currentExecutionContext. -
Enforcing the Deadline with
@cancellable_operation: You apply the@cancellable_operationdecorator to lower-level, internal functions that are called within the initial@timeoutblock (e.g., service methods, repository calls). -
Dynamic Cancellation: Each
@cancellable_operationdecorator reads thedeadlinefrom 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.