Event history#
Event history saves task events in the queue backend so you can query them later and review the stages a task moved through. For how history differs from a live SSE or WebSocket stream, see Live delivery versus durable history.
Enable history#
from litestar_queues import QueueConfig
from litestar_queues.events import EventHistoryConfig, QueueEventsConfig
queue_config = QueueConfig(
events=QueueEventsConfig(
history=EventHistoryConfig(
batch_size=20,
flush_interval=1.0,
memory_capacity=1000,
),
),
)
History is independent of live delivery: configure delivery and channels
only when you also want a live stream, as shown in Task events.
SQLSpec, Advanced Alchemy, Redis and Valkey buffer history until batch_size
is reached or the oldest pending batch reaches flush_interval. The timer
runs even when no more events arrive. This interval schedules a write; event-loop
delays and storage failures can postpone commitment.
max_pending defaults to 2000 and must be at least batch_size. It bounds
accepted records awaiting persistence or live release. Admission raises
QueueEventBufferFull when full, regardless of strict. Configuration
errors also propagate. SQLSpec, Advanced Alchemy and Redis/Valkey reject
conflicting reuse of an event ID.
With strict=False, transient write failures retain accepted records for a
bounded retry and withhold their live delivery. With strict=True, foreground
write failures propagate; a timer failure is surfaced at the next foreground
boundary. Neither setting makes uncommitted history safe from process death.
An orderly close attempts the remaining writes; non-strict close logs unresolved
records instead of delivering them live.
Support matrix#
Backend |
Support and persistence boundary |
|---|---|
Memory |
Bounded, temporary history. |
SQLSpec |
History stored in the SQLSpec queue schema. |
Advanced Alchemy |
History stored through an app-owned event-log model and migrations. |
Redis / Valkey |
Shared history. You choose how long it stays and whether it is backed up. |
Query and cleanup#
QueueService.get_event_log() returns the backend’s
QueueEventLog when history is enabled, and
None when it is not. Use it to find events by task ID, task name, or actor,
review stages, flush pending writes, and delete old records. Choose retention
rules that fit your audit and privacy needs. Deleting finished task records does
not delete event history, and vice versa.
Filtering by actor#
An event may carry a QueueEventActor naming who or what caused it, a typed
QueueEventEntityRef naming the affected record, and a scope /
scope_key pair grouping related work. Put a stage in the event’s payload.
History stores the entity as type:id. Actor filters use the existing
extra={"actor_id": ...} or extra={"actor_type": ...} argument;
QueueEventQuery has no typed actor field.
Run this complete example with uv run python examples/event_history.py.
It prints 1 and checks the stored stage and entity:
"""Publish and query one event using process-local history."""
# ruff: noqa: S101, T201 -- This runnable example verifies and prints its result.
import asyncio
from litestar_queues import QueueConfig, QueueService, WorkerConfig
from litestar_queues.events import (
EventHistoryConfig,
QueueEvent,
QueueEventActor,
QueueEventEntityRef,
QueueEventQuery,
QueueEventsConfig,
)
__all__ = ("main",)
async def main() -> None:
config = QueueConfig(
queue_backend="memory",
execution_backend="immediate",
worker=WorkerConfig(placement="external"),
events=QueueEventsConfig(history=EventHistoryConfig()),
)
async with QueueService(config) as service:
event_log = service.get_event_log()
assert event_log is not None
await service.get_event_publisher().publish(
QueueEvent(
type="task.log",
scope="custom",
scope_key="batch-42",
entity=QueueEventEntityRef(type="dataset", id="user-csv"),
payload={"stage": "processing"},
task_id="import-42",
message="importing",
actor=QueueEventActor(type="user", id="u-1", name="Alice"),
)
)
await event_log.flush_events()
page = await event_log.query_events(
QueueEventQuery(scope="custom", scope_key="batch-42", entity="dataset:user-csv"), extra={"actor_id": "u-1"}
)
assert page.total == len(page.items) == 1
assert page.items[0].stage == "processing"
assert page.items[0].entity == "dataset:user-csv"
print(page.total)
if __name__ == "__main__":
asyncio.run(main())
Filters use equality and are ANDed together. Every backend stores these fields and
answers the query; the SQLSpec and Advanced Alchemy tables index
(actor_id, occurred_at), (scope_key, occurred_at), and (entity, occurred_at)
to match the time-ordered read pattern.
The actor’s name is not stored. It is mutable display text that would go
stale against the event it was stamped on, so it travels on the live event
envelope only. Resolve names from your own user or service directory when you
render history.
page.total counts all matching rows before pagination, even when an offset
produces an empty page. SQLSpec selects the page and count in the same session;
consistency during concurrent writes follows the configured transaction
isolation, without an additional snapshot guarantee.
History ownership and custom providers#
QueueService closes its history before the live buffer and backend resources.
Direct event-log callers own await event_log.aclose(). For buffered SQLSpec,
Advanced Alchemy and Redis/Valkey logs, closing stops admission; an explicit
backend or service reopen creates a fresh coordinator. Memory and Ephemeral
history persist immediately within their storage boundary and have no history
timer to close. Flush and close attempt buffered history before releasing its
live callbacks. A live sink failure never puts already committed rows back into
the history buffer.
Custom QueueEventLog providers must implement
publish_event_after_commit(event, *, release, barrier=False) and aclose().
Invoke release only after the history transaction has committed, including
successful exit from any transaction context manager. Missing methods are
rejected when attaching a provider. Persistence strictness belongs to
EventHistoryConfig; the publisher’s former event_log_strict argument and
set_event_log(strict=...) option have been removed.
Scheduling cleanup#
Configure a bounded event-history phase and run it from one external schedule:
from litestar_queues import QueueConfig, QueueMaintenanceConfig
from litestar_queues.maintenance import QueueEventRetentionRule
from litestar_queues.events import EventHistoryConfig, QueueEventQuery, QueueEventsConfig
queue_config = QueueConfig(
queue_backend="redis",
events=QueueEventsConfig(history=EventHistoryConfig()),
maintenance=QueueMaintenanceConfig(
event_retention_rules=(
# Retain debug events for only 7 days
QueueEventRetentionRule(
match=QueueEventQuery(level="debug"),
max_age=7 * 24 * 60 * 60,
),
# Retain other events for 30 days
QueueEventRetentionRule(max_age=30 * 24 * 60 * 60),
),
event_limit=1000,
),
)
Then schedule litestar queues run-maintenance. It deletes at most
event_limit oldest matching rows in one invocation. The rules are evaluated
in declaration order: the first matching rule owns the event. Put specific rules
before a catch-all rule. You can also use exclude
to build negative matches (e.g. retaining everything except “task.started”).
Terminal-task retention is a separate setting, so the two policies can use different cutoffs. See Queue maintenance for coordination, cadence, backend, and migration requirements.
Memory history is bounded by memory_capacity and disappears with the process.
SQLSpec, Advanced Alchemy, Redis, and Valkey history is durable or shared, so
those deployments should include cleanup in their backup and privacy policies.