Source code for litestar_queues.config

import os
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
from contextlib import suppress
from dataclasses import dataclass, field
from logging import getLogger
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, cast, get_args, runtime_checkable

from litestar_queues.exceptions import QueueConfigurationError
from litestar_queues.namespace import DEFAULT_NAMESPACE, QueueNamespace

if TYPE_CHECKING:
    from contextlib import AbstractAsyncContextManager

    from litestar.datastructures import State

    from litestar_queues.backends import BaseQueueBackend
    from litestar_queues.events import QueueEventProducer, QueueEventPublisher, QueueEventsConfig, TaskExecutionContext
    from litestar_queues.events.typing import ChannelsLike
    from litestar_queues.execution import BaseExecutionBackend
    from litestar_queues.maintenance import QueueMaintenanceConfig
    from litestar_queues.models import QueuedTaskRecord
    from litestar_queues.observability import ObservabilityConfig
    from litestar_queues.service import QueueService
    from litestar_queues.task import Task

__all__ = (
    "STALE_REQUEUE_PRIORITY",
    "ExecutionBackendConfig",
    "ExecutionBackendConfigProtocol",
    "MigrationConfiguringBackend",
    "QueueBackendConfig",
    "QueueBackendConfigProtocol",
    "QueueConfig",
    "QueueNamespace",
    "StaleRequeuePriority",
    "TaskDependencyProvider",
    "TaskDependencyResolver",
    "TaskErrorSanitizer",
    "WorkerConfig",
    "WorkerPlacement",
    "execution_backend_name",
    "queue_backend_name",
)

logger = getLogger(__name__)

STALE_REQUEUE_PRIORITY: "StaleRequeuePriority" = "preserve"
"""Default stale-requeue policy: recovered work keeps the priority it was enqueued with."""

_SERVICE_STATE_KEY = "queue_service"
_WORKER_STATE_KEY = "queue_worker"
_EVENT_PUBLISHER_STATE_KEY = "queue_event_publisher"
_EVENT_CHANNELS_STATE_KEY = "queue_event_channels"
_OBSERVABILITY_RUNTIME_STATE_KEY = "queue_observability_runtime"


def _cgroup_cpu_limit(
    *,
    cpu_max: "Path" = Path("/sys/fs/cgroup/cpu.max"),
    cpu_quota: "Path" = Path("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"),
    cpu_period: "Path" = Path("/sys/fs/cgroup/cpu/cpu.cfs_period_us"),
) -> "int | None":
    """Return the rounded-up cgroup v2 or v1 CPU quota when available."""
    with suppress(OSError, ValueError):
        quota_text, period_text = cpu_max.read_text(encoding="utf-8").split()
        if quota_text != "max":
            quota = int(quota_text)
            period = int(period_text)
            if quota > 0 and period > 0:
                return max(1, (quota + period - 1) // period)
    with suppress(OSError, ValueError):
        quota = int(cpu_quota.read_text(encoding="utf-8"))
        period = int(cpu_period.read_text(encoding="utf-8"))
        if quota > 0 and period > 0:
            return max(1, (quota + period - 1) // period)
    return None


def _effective_cpu_count() -> "int":
    """Return CPUs usable by this process, including a Linux cgroup quota."""
    counts: "list[int]" = []
    process_cpu_count = cast("Callable[[], int | None] | None", getattr(os, "process_cpu_count", None))
    if process_cpu_count is not None:
        with suppress(OSError):
            count = process_cpu_count()
            if count is not None and count > 0:
                counts.append(count)
    sched_getaffinity = cast("Callable[[int], set[int]] | None", getattr(os, "sched_getaffinity", None))
    if sched_getaffinity is not None:
        with suppress(OSError):
            affinity_count = len(sched_getaffinity(0))
            if affinity_count > 0:
                counts.append(affinity_count)
    host_count = os.cpu_count()
    if host_count is not None and host_count > 0:
        counts.append(host_count)
    cgroup_count = _cgroup_cpu_limit()
    if cgroup_count is not None:
        counts.append(cgroup_count)
    return max(1, min(counts, default=1))


def _default_sync_thread_pool_size() -> "int":
    """Match the modern Python executor default using process-usable CPUs."""
    return min(32, _effective_cpu_count() + 4)


[docs] class QueueBackendConfigProtocol(Protocol): """Protocol for typed queue backend configuration objects.""" backend_name: "ClassVar[str]"
[docs] @runtime_checkable class MigrationConfiguringBackend(Protocol): """Backend config that registers its own migrations during plugin init. A backend owns whatever application wiring its storage needs. ``QueuePlugin`` only asks whether the configured backend provides this hook, so selecting one backend never imports another backend's package -- or requires its extra to be installed. """
[docs] def configure_migrations(self, config: "QueueConfig") -> "None": """Register backend-owned migrations with the application.""" ...
[docs] class ExecutionBackendConfigProtocol(Protocol): """Protocol for typed execution backend configuration objects.""" backend_name: "ClassVar[str]"
QueueBackendConfig = str | QueueBackendConfigProtocol """Type alias for queue backend selectors.""" ExecutionBackendConfig = str | ExecutionBackendConfigProtocol """Type alias for execution backend selectors.""" WorkerPlacement = Literal["server", "asgi", "external"] """Which process owns the queue worker. ``server`` The Litestar CLI server lifespan owns exactly one fresh worker process per ``litestar run`` invocation. This is the default. ``asgi`` Each ASGI worker owns one queue worker inside its own application lifespan. Deliberately multiplicative with the web-worker count. ``external`` Nothing is started automatically; a separate process manager runs ``litestar queues run``, or the caller executes tasks inline. """ _PLACEMENTS: "tuple[str, ...]" = get_args(WorkerPlacement) _MANAGED_PLACEMENTS = ("server", "asgi")
[docs] def queue_backend_name(backend: "QueueBackendConfig") -> "str": """Return the registered queue backend name for a selector.""" return backend if isinstance(backend, str) else backend.backend_name
[docs] def execution_backend_name(backend: "ExecutionBackendConfig") -> "str": """Return the registered execution backend name for a selector.""" return backend if isinstance(backend, str) else backend.backend_name
[docs] class TaskDependencyProvider(Protocol): """Per-attempt async context manager supplying task keyword arguments. The queue enters this scope inside the attempt timeout, merges the yielded mapping into the task's keyword arguments, and awaits ``__aexit__`` exactly once for every attempt outcome: success, retryable failure, terminal failure, timeout, cancellation, claim loss, and shutdown interruption. ``__aexit__`` receives :class:`asyncio.CancelledError` for timeout, cancellation, claim loss, and shutdown alike, because all four reach the attempt as a coroutine cancellation. Read ``record`` and ``context`` to distinguish outcomes; never the exception type. A truthy ``__aexit__`` return value is ignored: this is a resource scope, not an exception filter. An implementation that also owns process-level state may expose ``open()`` and ``close()`` (synchronous or asynchronous); :class:`QueueService` includes them in its own lifecycle and partial-open rollback. """ def __call__( self, task: "Task[..., object]", record: "QueuedTaskRecord", context: "TaskExecutionContext" ) -> "AbstractAsyncContextManager[Mapping[str, object]]": """Return the attempt-scoped dependency context manager.""" ...
TaskDependencyResolver = Callable[ ["Task[..., object]", "QueuedTaskRecord", "TaskExecutionContext"], Awaitable[Mapping[str, object]] ] """User-supplied callable that resolves extra kwargs for a task before execution.""" TaskErrorSanitizer = Callable[["BaseException", "QueuedTaskRecord"], str] """User-supplied callable that converts task exceptions into persisted error messages.""" StaleRequeuePriority = Literal["preserve"] | int | Callable[[int], int] """Priority policy applied to work recovered by the stale sweep. ``"preserve"`` keeps the original priority, an ``int`` is a ceiling clamp, and a callable maps the current priority to the recovered one. """
[docs] @dataclass(slots=True) class WorkerConfig: """Configuration shared by in-app and standalone workers.""" placement: "WorkerPlacement" = "server" """Which process owns this worker; see :data:`WorkerPlacement`.""" id: "str | None" = None """Explicit worker identity; ``None`` uses a process-derived identifier.""" batch_size: "int" = 10 """Maximum task records claimed in one worker iteration.""" poll_interval: "float" = 0.1 """Base worker polling interval in seconds.""" poll_backoff_max: "float | None" = 30.0 """Maximum adaptive polling interval in seconds; ``None`` disables backoff.""" poll_backoff_multiplier: "float" = 2.0 """Multiplier applied after an empty polling iteration.""" poll_jitter: "float" = 0.15 """Symmetric polling jitter ratio from zero through one.""" max_concurrency: "int" = 1 """Maximum number of tasks executed concurrently.""" queue_concurrency: "Mapping[str, int]" = field(default_factory=dict) """Per-worker concurrency caps for named queues.""" heartbeat_interval: "float" = 30 """Interval between bulk heartbeat writes in seconds.""" heartbeat_jitter_fraction: "float" = 0.1 """Positive heartbeat delay jitter ratio from zero through one.""" heartbeat_miss_threshold: "int" = 2 """Consecutive heartbeat misses tolerated before claim loss.""" cancel_on_claim_loss: "bool" = True """Whether heartbeat claim loss cancels the locally running coroutine. Writes are already fenced on the claim, but side effects are not: while a lost claim keeps running, a replacement worker executes the same record. Cancelling closes that duplicate-execution window. Set to ``False`` to let a lost attempt run to completion and have its terminal write rejected. """ cancellation_poll_interval: "float" = 1.0 """Interval between durable running-cancellation reconciliation passes.""" reconcile_interval: "float" = 30 """Interval between external-execution reconciliation passes in seconds.""" stale_after: "float | None" = None """Running-task age threshold in seconds; ``None`` disables stale recovery.""" stale_check_interval: "float" = 60.0 """Interval between stale-task recovery passes in seconds.""" expiry_check_interval: "float | None" = 60.0 """Interval between pending-job expiration passes; ``None`` disables sweeps.""" graceful_shutdown_timeout: "float" = 30 """Maximum graceful drain time in seconds.""" final_cancel_timeout: "float" = 5 """Maximum post-cancellation drain time in seconds.""" hard_exit_timeout: "float | None" = 10.0 """Wall-clock budget from forced shutdown to process exit; ``None`` disables the watchdog.""" requeue_on_shutdown: "bool" = False """Whether cancelled executions are requeued after shutdown drain timeout.""" max_interruptions: "int" = 3 """Shutdown requeues an attempt may absorb before interruptions consume the retry budget.""" startup_timeout: "float" = 30 """Maximum time to wait for worker startup readiness in seconds.""" queues: "tuple[str, ...]" = () """Queue names claimed by this worker; empty claims every queue.""" def __post_init__(self) -> "None": # noqa: C901 """Validate worker placement, concurrency, intervals, and adaptive polling.""" if self.placement not in _PLACEMENTS: msg = f"WorkerConfig.placement must be one of {_PLACEMENTS}, not {self.placement!r}." raise QueueConfigurationError(msg) positive = { "batch_size": self.batch_size, "poll_interval": self.poll_interval, "max_concurrency": self.max_concurrency, "heartbeat_interval": self.heartbeat_interval, "heartbeat_miss_threshold": self.heartbeat_miss_threshold, "cancellation_poll_interval": self.cancellation_poll_interval, "reconcile_interval": self.reconcile_interval, "stale_check_interval": self.stale_check_interval, "graceful_shutdown_timeout": self.graceful_shutdown_timeout, "final_cancel_timeout": self.final_cancel_timeout, "max_interruptions": self.max_interruptions, "startup_timeout": self.startup_timeout, } for name, value in positive.items(): if value <= 0: msg = f"WorkerConfig.{name} must be greater than 0." raise QueueConfigurationError(msg) if self.poll_backoff_max is not None and self.poll_backoff_max < self.poll_interval: msg = "WorkerConfig.poll_backoff_max must be greater than or equal to poll_interval." raise QueueConfigurationError(msg) if self.poll_backoff_multiplier < 1.0: msg = "WorkerConfig.poll_backoff_multiplier must be greater than or equal to 1.0." raise QueueConfigurationError(msg) if not 0.0 <= self.poll_jitter <= 1.0: msg = "WorkerConfig.poll_jitter must be between 0.0 and 1.0, inclusive." raise QueueConfigurationError(msg) if not 0.0 <= self.heartbeat_jitter_fraction <= 1.0: msg = "WorkerConfig.heartbeat_jitter_fraction must be between 0.0 and 1.0, inclusive." raise QueueConfigurationError(msg) if self.hard_exit_timeout is not None and self.hard_exit_timeout <= 0: msg = "WorkerConfig.hard_exit_timeout must be greater than 0 when set." raise QueueConfigurationError(msg) if self.stale_after is not None and self.stale_after <= 0: msg = "WorkerConfig.stale_after must be greater than 0 when set." raise QueueConfigurationError(msg) if self.expiry_check_interval is not None and self.expiry_check_interval < 0: msg = "WorkerConfig.expiry_check_interval must be greater than or equal to 0 when set." raise QueueConfigurationError(msg) for queue, cap in self.queue_concurrency.items(): if not queue: msg = "WorkerConfig.queue_concurrency queue names must not be empty." raise QueueConfigurationError(msg) if cap <= 0: msg = "WorkerConfig.queue_concurrency values must be greater than 0." raise QueueConfigurationError(msg) if self.queues and queue not in self.queues: msg = f"WorkerConfig.queue_concurrency contains {queue!r}, which is not in WorkerConfig.queues." raise QueueConfigurationError(msg)
[docs] @dataclass(slots=True) class QueueConfig: """Configuration for QueuePlugin.""" namespace: "str" = DEFAULT_NAMESPACE """Root used to derive package-owned runtime identifiers.""" queue_backend: "QueueBackendConfig" = "ephemeral" """Queue-record persistence backend selector or typed backend configuration.""" execution_backend: "ExecutionBackendConfig" = "local" """Default placement backend used to execute claimed tasks.""" task_dependency_resolver: "TaskDependencyResolver | None" = None """Per-attempt dependency resolver; ``None`` injects no additional task keyword arguments.""" task_dependency_provider: "TaskDependencyProvider | None" = None """Per-attempt dependency scope; ``None`` opens no scope around the task body. Mutually exclusive with :attr:`task_dependency_resolver`. Use the resolver for stateless keyword arguments and the provider when the attempt must own a resource that has to be released on every outcome. """ error_sanitizer: "TaskErrorSanitizer | None" = None """Persisted task-error formatter; ``None`` stores the default exception representation.""" worker: "WorkerConfig" = field(default_factory=WorkerConfig) """Shared configuration for in-app and standalone workers.""" service_dependency_key: "str | None" = None """Litestar dependency key for the injected queue service; ``None`` derives it from the namespace.""" events_dependency_key: "str | None" = None """Litestar dependency key for the event producer; ``None`` derives it from the namespace.""" events: "QueueEventsConfig | None" = None """Task-event capabilities; ``None`` disables delivery, streams, and history.""" observability: "ObservabilityConfig | None" = None """Package telemetry configuration; ``None`` disables the observability runtime.""" task_modules: "tuple[str, ...]" = () """Module names imported during startup to register decorated tasks.""" initialize_schedules: "bool" = True """Whether application startup synchronizes registered recurring schedules.""" log_success: "bool" = False """Whether successful task completion emits an informational log by default.""" sync_thread_pool_size: "int" = field(default_factory=_default_sync_thread_pool_size) """Maximum threads running synchronous tasks concurrently. Defaults to the cgroup-aware effective CPU count plus four, capped at 32. Threads are created on demand, so this is a ceiling rather than a startup cost. """ sync_thread_name_prefix: "str | None" = None """Thread-name prefix for synchronous tasks; ``None`` derives it from the namespace.""" scheduler_canary_task: "str | None" = None """Scheduler-health task name; ``None`` derives the package-owned default.""" maintenance: "QueueMaintenanceConfig | None" = None """Automatic maintenance policy; ``None`` disables the maintenance loop.""" max_argument_identity_bytes: "int | None" = None """Maximum canonical argument-identity size in bytes; ``None`` disables the bound.""" stale_requeue_priority: "StaleRequeuePriority" = STALE_REQUEUE_PRIORITY """Priority applied to stale-recovered work; see :data:`StaleRequeuePriority`.""" names: "QueueNamespace" = field(init=False) """Validated format-specific runtime-name renderer.""" def __post_init__(self) -> "None": """Validate the synchronous task thread pool and the placement matrix.""" self.names = QueueNamespace(self.namespace) if self.service_dependency_key is None: self.service_dependency_key = self.names.registration("service") if self.events_dependency_key is None: self.events_dependency_key = self.names.registration("events") if self.sync_thread_name_prefix is None: self.sync_thread_name_prefix = self.names.resource() if self.scheduler_canary_task is None: self.scheduler_canary_task = self.names.package_task("scheduler", "heartbeat") if self.sync_thread_pool_size <= 0: msg = "QueueConfig.sync_thread_pool_size must be greater than 0." raise QueueConfigurationError(msg) self._validate_task_dependencies() self._validate_stale_requeue_priority() self._validate_placement() def _validate_task_dependencies(self) -> "None": """Reject configuring both task dependency hooks. Raises: QueueConfigurationError: If a resolver and a provider are both set. """ if self.task_dependency_resolver is not None and self.task_dependency_provider is not None: msg = ( "QueueConfig sets both task_dependency_resolver and task_dependency_provider. " "Two mappings merging into one task call has no defined precedence. " "Use task_dependency_provider when the attempt owns a resource, or " "task_dependency_resolver when it only supplies stateless keyword arguments." ) raise QueueConfigurationError(msg) def _validate_stale_requeue_priority(self) -> "None": """Reject a stale-requeue policy that is neither ``"preserve"``, a priority, nor a mapper. Raises: QueueConfigurationError: If the configured policy cannot be applied. """ policy = self.stale_requeue_priority if callable(policy) or policy == "preserve": return if isinstance(policy, bool) or not isinstance(policy, int) or policy < 0: msg = ( "QueueConfig.stale_requeue_priority must be 'preserve', a non-negative priority ceiling, " f"or a callable mapping the old priority to the new one, not {policy!r}." ) raise QueueConfigurationError(msg) def _validate_placement(self) -> "None": """Reject storage, execution, and placement combinations that cannot work. Selectors are compared by registered name so validating a configuration never imports an optional backend or its driver extra. Raises: QueueConfigurationError: If the combination has no coherent owner. """ backend = queue_backend_name(self.queue_backend) execution = execution_backend_name(self.execution_backend) placement = self.worker.placement if placement in _MANAGED_PLACEMENTS and execution == "immediate": msg = ( f"execution_backend='immediate' runs tasks inline at enqueue time, so " f"placement={placement!r} would start a worker with nothing to claim. " f"Use execution_backend='local', or placement='external'." ) raise QueueConfigurationError(msg) if execution == "cloudtasks": if isinstance(self.execution_backend, str): msg = ( "execution_backend='cloudtasks' has no defaults for the project, queue, " "delivery target, or audience. Configure it as " "execution_backend=CloudTasksExecutionConfig(...)." ) raise QueueConfigurationError(msg) if placement != "external": msg = ( "execution_backend='cloudtasks' schedules work itself and requires " "WorkerConfig(placement='external')." ) raise QueueConfigurationError(msg) if backend in {"memory", "ephemeral"}: msg = ( "execution_backend='cloudtasks' delivers each record to a separate process, " "which cannot reach process-local storage. Use a shared persistent queue backend." ) raise QueueConfigurationError(msg) # Ephemeral storage is deliberately unconstrained by placement. Placement # does not decide whether the private database exists: whatever entered # EphemeralServerContext created it, and the backend refuses to open when # that environment is absent. Restating that check as a placement rule # only blocked embedders that create the database themselves. if backend == "memory" and placement == "server": msg = ( "queue_backend='memory' is process-local, so a separate server-owned worker " "process could never see its records. Use the default queue_backend='ephemeral' " "for zero-setup background work, or a persistent backend." ) raise QueueConfigurationError(msg) # memory + external is deliberately allowed: it describes an application # that enqueues into a process-local queue and starts nothing itself, # which is what inline callers and direct Worker tests do. Pointing the # standalone 'litestar queues run' command at it is the incoherent case, # and that command rejects process-local storage itself. @property def signature_namespace(self) -> "dict[str, Any]": """Names Litestar must resolve that an application cannot supply itself. This carries only the types named in this package's own dependency providers. ``provide_service_dependency`` and ``provide_event_producer_dependency`` annotate their return types as strings while importing those types under ``TYPE_CHECKING``, so Litestar needs them here to resolve the injected ``queue_service`` and ``queue_events`` dependencies. Nothing else belongs here. Config, backend, worker, and event types are named in application setup code, not in handler signatures, and a handler that does annotate one has already imported it. Registering the whole public API instead made ``QueuePlugin.on_app_init`` import every installed adapter on every application startup, which defeated the package's lazy-import boundary and charged applications for extras they never selected. """ from litestar.di import NamedDependency from litestar_queues.events import QueueEventProducer from litestar_queues.service import QueueService return { "NamedDependency": NamedDependency, "QueueEventProducer": QueueEventProducer, "QueueService": QueueService, } @property def dependencies(self) -> "dict[str, Any]": """Dependency providers for Litestar's DI system.""" from litestar.di import Provide return { cast("str", self.service_dependency_key): Provide(self.provide_service_dependency), cast("str", self.events_dependency_key): Provide(self.provide_event_producer_dependency), } @property def service_state_key(self) -> "str": """Litestar state key holding the queue service.""" return self.names.registration("service") @property def worker_state_key(self) -> "str": """Litestar state key holding the in-process worker.""" return self.names.registration("worker") @property def event_publisher_state_key(self) -> "str": """Litestar state key holding the event publisher.""" return self.names.registration("event", "publisher") @property def event_channels_state_key(self) -> "str": """Litestar state key holding the resolved channels backend.""" return self.names.registration("event", "channels") @property def observability_runtime_state_key(self) -> "str": """Litestar state key holding the observability runtime.""" return self.names.registration("observability", "runtime") @property def maintenance_name(self) -> "str": """Distributed maintenance coordination name.""" return self.names.coordination("maintenance")
[docs] def get_service(self, state: "State | None" = None) -> "QueueService": """Return a QueueService for this configuration.""" from litestar_queues.service import QueueService if state is None: return QueueService(self) state_key = self.service_state_key if state_key not in state: msg = ( f"QueueService is not available in app state under {state_key!r}; " "ensure QueuePlugin startup has completed before resolving the queue service." ) raise RuntimeError(msg) cached = state[state_key] if isinstance(cached, QueueService): return cached msg = f"QueueService has not been opened in app state under {state_key!r}; found {type(cached).__name__}." raise RuntimeError(msg)
[docs] def get_queue_backend(self) -> "BaseQueueBackend": """Return a configured queue backend instance.""" from litestar_queues.backends import get_queue_backend return get_queue_backend(self.queue_backend, config=self)
[docs] def get_execution_backend(self) -> "BaseExecutionBackend": """Return a configured execution backend instance.""" from litestar_queues.execution import get_execution_backend return get_execution_backend(self.execution_backend, config=self)
[docs] def get_event_publisher( self, *, channels_backend: "ChannelsLike | None" = None, manage_channels_lifecycle: "bool" = False ) -> "QueueEventPublisher": """Return a configured queue event publisher. Args: channels_backend: Fallback live sink target used only when ``QueueEventsConfig.channels`` is unset. ``QueuePlugin`` passes the app's registered ``ChannelsPlugin`` here so event delivery needs no manual channel wiring. manage_channels_lifecycle: Whether the publisher owns the resolved Channels target lifecycle. """ from litestar_queues.events import ( ChannelsQueueEventSink, CompositeQueueEventSink, NoopQueueEventSink, QueueEventPublisher, QueueEventSink, ) events_config = self.events if events_config is None or events_config.delivery is None: sink: "QueueEventSink" = NoopQueueEventSink() return QueueEventPublisher(sink, namespace=self.names) delivery = events_config.delivery sinks: "list[QueueEventSink]" = [] live_backend = events_config.channels if events_config.channels is not None else channels_backend if live_backend is not None: sinks.append( ChannelsQueueEventSink( live_backend, manage_lifecycle=manage_channels_lifecycle, max_payload_bytes=delivery.max_payload_bytes, payload_size_estimator=delivery.payload_size_estimator, ) ) sinks.extend(delivery.sinks) if not sinks: msg = "EventDeliveryConfig requires events.channels, an app ChannelsPlugin, or at least one custom sink." raise QueueConfigurationError(msg) sink = ( sinks[0] if len(sinks) == 1 else CompositeQueueEventSink(sinks, strict=delivery.strict, namespace=self.names) ) return QueueEventPublisher( sink, buffer_config=delivery.buffer, strict=delivery.strict, publish_task_channel=delivery.publish_task_channel, publish_queue_channel=delivery.publish_queue_channel, publish_global_lifecycle=delivery.publish_global_lifecycle, namespace=self.names, )
[docs] async def provide_service_dependency(self, state: "State") -> 'AsyncIterator["QueueService"]': """Yield the application-scoped QueueService for Litestar dependency injection.""" yield self.get_service(state)
[docs] async def provide_event_producer_dependency(self, state: "State") -> "AsyncIterator[QueueEventProducer]": """Yield the application-scoped QueueEventProducer for Litestar dependency injection.""" from litestar_queues.events import QueueEventProducer state_key = self.event_publisher_state_key if state_key not in state: msg = ( "Queue event publisher is not available in app state under " f"{state_key!r}; ensure QueuePlugin startup has completed before " "resolving queue events." ) raise RuntimeError(msg) yield QueueEventProducer(state[state_key])