Task Dependency Injection¶
Background tasks execute outside the scope of an incoming HTTP request. litestar-queues provides two complementary hooks on QueueConfig for injecting database sessions, API clients, and domain services into task executions:
task_dependency_provider: Attempt-scoped async context manager that sets up and tears down resources around each task execution attempt.task_dependency_resolver: Stateless async callable that returns a dictionary of extra keyword arguments before the task body runs.
Mutually Exclusive Configuration
Configuring both task_dependency_provider and task_dependency_resolver on the same QueueConfig raises QueueConfigurationError during initialization.
Runnable Example¶
docs/examples/learn/dependency_injection.py
"""Supplying attempt-scoped resources and stateless dependencies to tasks."""
from collections.abc import AsyncIterator, Mapping
from contextlib import asynccontextmanager
from litestar_queues import QueueConfig, QueueService, Task, TaskExecutionContext, WorkerConfig, task
from litestar_queues.models import QueuedTaskRecord
class BillingClient:
"""Example scoped client managed per task execution attempt."""
__slots__ = ("closed", "prefix")
def __init__(self, prefix: str) -> None:
self.prefix = prefix
self.closed = False
async def charge(self, invoice_id: str) -> str:
"""Issue a charge reference."""
return f"{self.prefix}:{invoice_id}"
@task("billing.capture_payment", queue="payments")
async def capture_payment(invoice_id: str, billing_client: BillingClient) -> str:
"""Execute a task receiving only the parameters declared in its signature."""
return await billing_client.charge(invoice_id)
@asynccontextmanager
async def provide_task_dependencies(
task: Task[..., object], record: QueuedTaskRecord, context: TaskExecutionContext
) -> AsyncIterator[Mapping[str, object]]:
"""Yield attempt-scoped dependencies and guarantee teardown on exit."""
_ = (task, context)
client = BillingClient(prefix=record.queue)
try:
yield {"billing_client": client, "unused_dependency": "filtered-out"}
finally:
client.closed = True
async def main() -> None:
"""Verify attempt-scoped dependency injection and automatic signature filtering."""
config = QueueConfig(
queue_backend="memory",
execution_backend="immediate",
worker=WorkerConfig(placement="external"),
task_dependency_provider=provide_task_dependencies,
)
async with QueueService(config=config) as service:
handle = await service.enqueue(capture_payment, "inv-500")
await handle.refresh()
assert handle.status == "completed"
assert handle.result == "payments:inv-500"
Attempt-Scoped Providers vs Stateless Resolvers¶
| Capability | task_dependency_provider |
task_dependency_resolver |
|---|---|---|
| Callable Shape | (task, record, context) -> AbstractAsyncContextManager[Mapping[str, object]] |
async (task, record, context) -> Mapping[str, object] |
| Per-Attempt Teardown | Yes — __aexit__ is awaited exactly once after every attempt outcome |
No — returns a mapping with no post-attempt teardown hook |
| Service-Level Lifecycle | Optional open() and close() hooks managed by QueueService |
None |
| Attempt Timeout Coverage | Both __aenter__, task body, and __aexit__ run inside task.timeout |
Resolution and task body run inside task.timeout |
| Recommended Use Case | Database transactions, unit-of-work sessions, leased connections | Stateless configuration objects, pre-built singletons, request metadata |
Automatic Signature Filtering¶
Both task_dependency_provider and task_dependency_resolver can yield a shared mapping of application dependencies without coupling every task signature to the full dictionary:
- Standard Signatures: When a task function does not declare a
**kwargs(VAR_KEYWORD) parameter,litestar-queuesfilters the yielded mapping to inject only keys matching parameters declared in the task'sinspect.Signaturethat were not already supplied explicitly atenqueue()time. - Variadic Signatures (
**kwargs): When a task declares**kwargs, all non-overlapping keys from the provider or resolver mapping are forwarded. - Explicit Enqueue Precedence: Arguments passed explicitly to
await queue_service.enqueue(task_fn, ...)always take precedence over keys yielded by a provider or resolver.
Lifecycle & Teardown Guarantees¶
When task_dependency_provider is configured, QueueService enforces strict resource safety:
- Guaranteed Exit: Once
__aenter__succeeds,__aexit__is awaitedexactly oncefor every attempt outcome: completion, retryable exception,non_retryable()failure,job_cancelled(), attempt timeout, external cancellation, heartbeat claim loss, and worker shutdown interruption. - Coroutine Cancellation Semantics: In
__aexit__(exc_type, exc_val, exc_tb),exc_typereceivesasyncio.CancelledErrorfor attempt timeout, external cancellation, heartbeat claim loss, and worker shutdown alike because all four interrupt the running coroutine via cancellation. Inspectrecordandcontext(for examplecontext.is_cancelled) rather than branching onexc_typealone. - No Exception Suppression: Returning a truthy value from
__aexit__is ignored byQueueService; the provider is strictly a resource lifecycle scope, never an exception filter. - Process-Level
open()/close(): If the provider object definesopen()and/orclose()methods (sync or async),QueueService.open()invokesprovider.open()before opening queue backends andQueueService.close()invokesprovider.close()during shutdown (including partial-open rollback).