Client¶
TaskQ, JobsClient, JobHandle, CancelResult, and ActorsClient.
_taskq ¶
Top-level entry point for non-worker applications.
Provides :class:TaskQ, a Postgres-backed client that manages its own
connection pool and exposes job operations (enqueue, get, list, cancel)
directly.
Two lifecycle patterns are supported:
Async context manager (scripts, tests)::
async with TaskQ(dsn="postgresql://user:pw@host/db") as tq:
handle = await tq.enqueue(my_actor, MyPayload(...))
result = await handle.wait()
Explicit open/close (FastAPI lifespan, long-lived processes)::
tq = TaskQ(dsn=settings.pg_dsn)
@asynccontextmanager
async def lifespan(app: FastAPI):
await tq.open()
yield
await tq.close()
@app.post("/tasks")
async def create_task(payload: MyPayload):
handle = await tq.enqueue(my_actor, payload)
return {"job_id": str(handle.job_id)}
Passing an existing pool (e.g. shared with the rest of the application)::
async with TaskQ(pool=app.state.pool) as tq:
await tq.cancel(job_id)
__all__
module-attribute
¶
EventRow
dataclass
¶
EventRow(
event_id: int,
job_id: JobId,
occurred_at: datetime,
kind: Literal["state_change", "cancel_request"],
detail: dict[str, object],
)
Read-model of a taskq.job_events row.
Mirrors the job_events table shape: monotonic event_id,
the owning job, timestamp, event kind, and a detail payload.
ActorsClient ¶
Pool-wrapping facade for actor configuration operations.
Acquires a connection from the injected pool for each call, delegates
to taskq.actor_config_ops, and returns the result. The
caller must have opened the pool; this class does not manage its
lifecycle.
.. note::
This client is Postgres-only, it delegates to
:mod:taskq.actor_config_ops, which executes raw SQL against
the actor_config table. The :class:~taskq.backend._protocol.Backend
protocol does not include actor config operations, so
:class:~taskq.testing.InMemoryBackend does not support
ActorsClient.
Parameters¶
pool:
An open asyncpg.Pool. The caller retains ownership.
schema:
TaskQ schema name. Defaults to "taskq".
Source code in src/taskq/client/_actors.py
list
async
¶
List all stored actor_config rows, ordered by actor name.
get
async
¶
Get one actor_config row, or None if not found.
set_capacity
async
¶
set_capacity(
actor: str,
*,
max_concurrent: int | Unset | None = UNSET,
max_pending: int | Unset | None = UNSET,
result_ttl: float | Unset | None = UNSET,
) -> ActorConfigRow | None
Update capacity fields on an existing actor_config row.
Source code in src/taskq/client/_actors.py
deregister
async
¶
Deregister an actor with safety checks.
See :func:taskq.actor_config_ops.deregister_actor for
the full semantics.
Source code in src/taskq/client/_actors.py
JobEvent ¶
Bases: BaseModel
A single event yielded by :meth:TaskQ.stream.
Represents a point-in-time snapshot of a job's observable state.
Yielded on every status transition or progress update; the final
event always has terminal=True.
The progress_state and progress_seq fields reflect the last
values written by the worker. They are None / 0 until the
worker emits a progress update.
Serialises cleanly to JSON via model_dump() for SSE or WebSocket
fanout, fields are deliberately flat so the caller can forward the
event without transformation. Both transports below route through
orjson (:func:taskq._json.dumps), never stdlib json::
# HTTP: drop-in replacement for starlette's stdlib-json JSONResponse
OrjsonJSONResponse = orjson_response_class()
@app.get("/tasks/{job_id}/events")
async def events(job_id: UUID) -> Response:
event = latest_event(job_id)
return OrjsonJSONResponse(event.model_dump())
# WebSocket: send_json uses stdlib json.dumps internally, send the
# pre-rendered orjson text instead:
async for event in tq.stream(job_id):
await websocket.send_text(event.model_dump_json())
TaskQ ¶
TaskQ(
*,
dsn: str | None = None,
pool: Pool | None = None,
pool_factory: PoolFactory | None = None,
pg_provider: PgCredentialProvider | None = None,
schema: str | None = None,
min_pool_size: int = 1,
max_pool_size: int = 5,
redis_url: str | None = None,
redis_client: Any | None = None,
pg_conn_factory: ConnFactory | None = None,
listen_conn: Connection | None = None,
poll_timeout: float = 30.0,
reclaim_event_visibility_delay: timedelta | None = None,
reload_interval: float | None = None,
)
Postgres-backed TaskQ client.
Manages a connection pool and exposes job operations directly. Supports
both the async context manager pattern and explicit open() / close()
for frameworks like FastAPI that manage their own lifecycle.
Parameters¶
dsn:
Postgres DSN string. Mutually exclusive with pool.
pool:
An already-open asyncpg.Pool. The caller retains ownership;
close() will not close it.
pool_factory:
A zero-arg async factory returning an asyncpg.Pool - the same
:data:~taskq.connections.PoolFactory the worker takes via
:class:~taskq.connections.WorkerConnections. TaskQ invokes it at
open() and owns the result (close() closes it), and
:meth:reload_credentials re-invokes it to rotate the pool in place.
This is the client-side equivalent of a <role>_pool_factory:
pair it with :func:taskq.auth.make_pg_pool_factory for a
rotating-credential deployment. Mutually exclusive with dsn and
pool.
pg_provider:
A :class:~taskq.auth.PgCredentialProvider. Sugar for
pool_factory=make_pg_pool_factory(dsn, pg_provider,
min_size=min_pool_size, max_size=max_pool_size) - it is exactly
that call and nothing more, so the two paths share one mechanism.
Requires dsn; use pool_factory directly when you need the
factory's other hooks (init, server_settings,
command_timeout).
schema:
TaskQ schema name. When omitted, resolved the way the worker and
CLI resolve it, TASKQ_SCHEMA_NAME from the process environment
or the .env cascade, with "taskq" as the final default, so
a client constructed with no opinion lands in the schema its worker
fleet is listening on. An explicit value always wins.
min_pool_size:
Minimum pool connections. Only used when dsn or pg_provider
is provided (both build the pool through the same factory path;
a caller-supplied pool / pool_factory is used as-is).
max_pool_size:
Maximum pool connections. Only used when dsn or
pg_provider is provided.
redis_url:
Redis URL string. Mutually exclusive with redis_client.
The library creates and owns the Redis client; close() will
close it.
redis_client:
An already-open redis.asyncio.Redis client. The caller retains
ownership; close() will not close it. Mutually exclusive with
redis_url.
pg_conn_factory:
A zero-arg async factory returning an asyncpg.Connection for the
LISTEN/NOTIFY transport used by :meth:watch_reclaims. Mutually
exclusive with listen_conn. Takes precedence over dsn when
set. Use this when you have no DSN (e.g. AAD-managed-identity auth)
but still want the reclaim wake-up. TaskQ owns and closes the
connection produced by the factory per watch_reclaims() call.
listen_conn:
A pre-constructed asyncpg.Connection for the LISTEN transport.
Caller-owned; TaskQ does not close it. Mutually exclusive with
pg_conn_factory. Takes precedence over dsn when set. Use
this to share a dedicated LISTEN conn across callers.
poll_timeout:
Maximum seconds to wait between transport wakeups before re-fetching
job state. Defaults to 30.0. :meth:stream on Postgres alone
has no wakeups and polls every min(poll_timeout, 0.5) seconds
(never below 0.1 s, jittered ±20%).
reload_interval:
Seconds between automatic :meth:reload_credentials rebuilds of a
factory-built pool (pool_factory / pg_provider); the
client-side TASKQ_RELOAD_INTERVAL. When None (the default)
the cadence is derived from the lease the provider grants - a
Vault dynamic credential is rebuilt at half its lease TTL (see
:class:taskq.auth.ReloadSchedule) - and a factory whose
credential carries no lease is never rebuilt automatically
(token providers refresh per connection and need no rebuild).
The rotation runs as a background task for the life of the client;
a rebuild that fails leaves the live pool serving and is retried
on the next tick. Rejected for dsn= and pool= (a
ValueError at construction), which have nothing to rebuild
from — it is not silently ignored, the docstring's earlier claim.
reclaim_event_visibility_delay:
Overrides the margin :meth:watch_reclaims assumes between a
job_events writer's INSERT and its COMMIT (see
:data:taskq.constants.RECLAIM_EVENT_VISIBILITY_DELAY, default
2s). It is passed through to the constructed backend's
poll_reclaim_events and bounds the NOTIFY catch-up retry.
Must match whatever margin the worker fleet's sweep-adjacent
backend uses: the margin's correctness depends on writer
transaction duration, not reader preference, so a client that
widens it alone only silences the gap detection on itself.
None (the default) uses the shipped constant.
Source code in src/taskq/client/_taskq.py
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 | |
actors
property
¶
Actor configuration client, list, get, set capacity, deregister.
Raises RuntimeError if called before open() or outside an
async with block.
backend
property
¶
The Postgres :class:Backend this client built, read-only.
The seam a host that embeds the admin UI needs: create_router
takes the backend verbatim (backend=tq.backend) so the admin
portal's backend-mediated mutations (job cancel, job retry,
schedule run-now) serve from the SAME backend the trigger app
enqueues through, instead of the 503 "not configured" the router
answers when it has none. Raises RuntimeError if called before
open() or outside an async with block, like the other
accessors.
open
async
¶
Open the connection pool and prepare the client.
Called automatically by __aenter__. Safe to call explicitly
for frameworks that manage lifecycle outside an async with block.
Raises :class:RuntimeError if already open.
Source code in src/taskq/client/_taskq.py
672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 | |
close
async
¶
Close the client and release the pool if owned.
Called automatically by __aexit__. Safe to call explicitly.
No-op if already closed.
Source code in src/taskq/client/_taskq.py
reload_credentials
async
¶
Rebuild the pool from pool_factory and swap it in, live.
The client-side counterpart of
:func:taskq.worker.deps.reload_credentials, and the supported
replacement for reaching into _pool / _client: it invokes the
factory (which fetches a fresh credential), atomically points every
subsystem that holds a pool - the backend deps behind
enqueue/get/list/cancel, the :attr:actors client, and
the stream() poll - at the new pool, then closes the old
one with the same bounded drain used at close().
A username-bearing pair (a Vault lease) is rotated automatically on
the client's :class:~taskq.auth.ReloadSchedule - reload_interval
when given, otherwise half the granted lease TTL - so this is the
on-demand path: call it on any signal your issuer gives you, or to
drop sessions opened under a revoked token. It is not needed for
the ordinary token refresh that :func:taskq.auth.make_pg_pool_factory
already does per physical connection. Concurrent calls (the
scheduled rotation and an explicit one) are serialized.
Raises :class:RuntimeError if the client is not open, or if the pool
is caller-owned (pool=) - TaskQ must never close a pool it does not
own, so the caller rotates that one itself.
If the factory fails, the exception propagates and the current pool is left untouched and serving: a transient token-endpoint outage must not turn into a client outage.
Source code in src/taskq/client/_taskq.py
__aenter__
async
¶
__aexit__
async
¶
enqueue
async
¶
enqueue(
ref: ActorRef[P, R],
payload: P,
*,
queue: QueueName | None = None,
scheduled_at: datetime | None = None,
priority: int | None = None,
schedule_to_close: datetime | None = None,
start_to_close: timedelta | None = None,
heartbeat_timeout: timedelta | None = None,
identity_key: IdentityKey | None = None,
fairness_key: str | None = None,
idempotency_key: IdempotencyKey | None = None,
idempotency_scope: str | None = None,
trace_id: str | None = None,
span_id: str | None = None,
metadata: dict[str, object] | None = None,
tags: list[str] | None = None,
) -> JobHandle[R]
Enqueue a job and return a typed handle.
The actor's name is NOT validated against the registration table
at enqueue time (any ActorRef is accepted, the same
defer-to-fire-time doctrine create_schedule applies): a
ref whose actor no worker declares enqueues successfully and the
job is parked at the snooze cadence by every worker that claims
it (released_reason: "actor-not-found", the stranded-jobs
detector surfaces it) rather than being refused here. See
:meth:JobsClient.enqueue for the full enqueue contract:
dedup, max_pending, and idempotency semantics.
schedule_to_close (absolute datetime) is deprecated, it crosses
clock domains (the app clock that produced it vs the database clock
that evaluates it). Declare retry.time_budget on the actor
instead; the interval form is anchored to the database clock.
Source code in src/taskq/client/_taskq.py
enqueue_batch
async
¶
enqueue_batch(
items: list[EnqueueItem],
*,
batch_id: UUID | None = None,
connection: Connection | None = None,
failure_policy: BatchFailurePolicy | None = None,
finalizer: EnqueueItem | None = None,
) -> BatchHandle
Enqueue multiple jobs in a single batched INSERT.
Delegates to :meth:JobsClient.enqueue_batch; see its docstring
for validation rules and idempotency-key collision semantics.
Source code in src/taskq/client/_taskq.py
enqueue_batch_streaming
async
¶
enqueue_batch_streaming(
items: Iterable[EnqueueItem],
*,
batch_id: UUID | None = None,
connection: Connection | None = None,
failure_policy: BatchFailurePolicy | None = None,
finalizer: EnqueueItem | None = None,
chunk_size: int = DEFAULT_CHUNK_SIZE,
) -> BatchHandle
Enqueue jobs from a lazy iterable in chunks.
Delegates to :meth:JobsClient.enqueue_batch_streaming; see its
docstring for chunk_size validation and streaming semantics.
Source code in src/taskq/client/_taskq.py
get_batch
async
¶
Fetch a single batch row by ID.
Delegates to :meth:JobsClient.get_batch. Returns None when
the batch does not exist.
Source code in src/taskq/client/_taskq.py
list_batches
async
¶
List batches matching filter, returning :class:BatchSummary objects.
Delegates to :meth:JobsClient.list_batches.
Source code in src/taskq/client/_taskq.py
enqueue_batch_fast
async
¶
enqueue_batch_fast(
items: list[EnqueueItem],
*,
batch_id: UUID | None = None,
connection: Connection | None = None,
) -> int
Enqueue jobs via COPY FROM protocol for maximum throughput.
Delegates to :meth:JobsClient.enqueue_batch_fast; see its
docstring for tradeoffs vs the regular :meth:enqueue_batch.
Source code in src/taskq/client/_taskq.py
get
async
¶
Look up a job by id. Returns None when the job does not exist.
Delegates to :meth:JobsClient.get (jobs table first, then the
archive tier; None means the id lives in neither). The
result_adapter default and its status-only-idiom rationale are
documented there.
Source code in src/taskq/client/_taskq.py
get_row
async
¶
Look up a job by id and return the raw JobRow, no handle.
Delegates to :meth:JobsClient.get_row: one backend read,
None when the job does not exist, mirroring :meth:get's
contract without the handle machinery.
Source code in src/taskq/client/_taskq.py
list
async
¶
List jobs matching filter, returning a :class:JobPage.
Delegates to :meth:JobsClient.list, note filter.unfinished
selects by terminality ('not yet finished'), not by execution
status ('currently executing'). See :class:JobFilter.
Source code in src/taskq/client/_taskq.py
backpressure
async
¶
Read the submit path's backpressure state for queues.
Delegates to :meth:JobsClient.backpressure; see it for the
full contract (the depth/cap/children halves, the fail-open
unknown state, the advisory semantics). Returns a
:class:~taskq.types.BackpressureSnapshot — a read-only typed
read mirroring the reader methods above, deliberately not an
enqueue kwarg.
Source code in src/taskq/client/_taskq.py
cancel
async
¶
Request cancellation of a job and return a :class:CancelResult.
Delegates to :meth:JobsClient.cancel. The result reports
previous_status (the row as first read) and new_status
(the row re-read after the cancel request was written; the two
are not atomically consistent, a job can move in between), and
cancellation_initiated is False when no cancellation was
needed (the job was already terminal, or had a cancel request
recorded).
Raises :class:KeyError carrying job_id when the job does not
exist (never enqueued, or pruned past retention) — the stdlib
"asked for an entry by id; it isn't there" idiom, matching
:meth:stream and :meth:JobHandle.wait.
Source code in src/taskq/client/_taskq.py
cancel_where
async
¶
cancel_where(
filter: JobFilter,
reason: str | None = None,
*,
allow_empty_filter: bool = False,
) -> BulkCancelResult
Cancel all jobs matching filter. See :meth:JobsClient.cancel_where.
Source code in src/taskq/client/_taskq.py
retry_job
async
¶
Re-pend a job that has come to rest (a terminal status).
The backend admin operation the admin UI's retry route calls:
every terminal status is a valid source (an operator re-run is
"run this again", including after a success), non-terminal
statuses are refused, and the attempt counter is not reset.
Returns False when the job was not in a retryable state, the
spent attempt sits at the smallint ceiling, or the job id does
not exist — the three cases are indistinguishable here by
design (the operation is a single conditional UPDATE); when a
False could mean a typo'd id rather than a busy job, check
existence first with :meth:get. The same conflict surfaces as
a 409 on the admin route.
Source code in src/taskq/client/_taskq.py
create_schedule
async
¶
create_schedule(
actor: str | ActorRef[P, R],
cron_expr: str,
*,
timezone: str = "UTC",
dst_strategy: DstStrategy = "skip",
payload_factory: str | None = None,
static_payload: dict[str, object] | None = None,
name: str = "",
identity_key: IdentityKey | None = None,
enabled: bool = True,
) -> ScheduleHandle
Create a cron schedule. Delegates to :meth:JobsClient.create_schedule.
dst_strategy controls how DST gaps/overlaps are handled; see
:meth:JobsClient.create_schedule for the full semantics.
Source code in src/taskq/client/_taskq.py
list_schedules
async
¶
List cron schedules. Delegates to :meth:JobsClient.list_schedules.
Source code in src/taskq/client/_taskq.py
update_schedule
async
¶
update_schedule(
schedule_id: UUID,
*,
cron_expr: str | None = None,
enabled: bool | None = None,
payload_factory: str | None = None,
static_payload: dict[str, object] | None = None,
clear_payload_factory: bool = False,
) -> ScheduleRecord
Update a cron schedule. Delegates to :meth:JobsClient.update_schedule.
Source code in src/taskq/client/_taskq.py
delete_schedule
async
¶
Delete a cron schedule. Delegates to :meth:JobsClient.delete_schedule.
stream
async
¶
Stream live state changes for a job as :class:JobEvent objects.
Yields one event per observable state transition (status change or
progress update), terminating automatically when the job reaches a
terminal state. The final event always has ``terminal=True``.
Latency contract
----------------
The first event is the row as it stands when ``stream()`` is
called. After that:
* **Redis configured** - the worker publishes every progress and
state-change event to the job's Redis channel, so a transition
is delivered as it happens; the row is re-read from Postgres
every ``poll_timeout`` seconds (default 30) as the safety net.
* **Postgres only** - nothing announces a job's transitions over
NOTIFY, so the row is re-read through the client's pool every
``min(poll_timeout, 0.5)`` seconds: a terminal write is observed
within half a second, at the cost of one primary-key read per
stream per half second. Lower ``poll_timeout`` to tighten it, to
a floor of 0.1 s; each wait is jittered ±20% so streams opened
together do not poll in lockstep. No connection is held per
stream, so this works on a pool-only client as well. A pool or
connection error on a read is retried on the next poll; reads
failing for 30 s straight end the stream with
:class:`~taskq.exceptions.StreamUnavailable`.
Usage::
async for event in tq.stream(job_id):
print(event.status, event.progress_state)
# loop exits automatically when event.terminal is True
# Or wire directly into a FastAPI SSE response:
async def event_generator():
async for event in tq.stream(job_id):
yield f"data: {event.model_dump_json()}
"
Raises
------
RuntimeError
Called before ``tq.open()`` or outside an ``async with`` block.
KeyError
The job does not exist — at the opening read, or on any later
poll whose row has been pruned in the meantime (the stream
cannot fabricate the promised terminal event, so it raises
rather than ending quietly).
StreamUnavailable
The Postgres poll could not re-read the row for 30 s straight.
Source code in src/taskq/client/_taskq.py
1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 | |
watch_reclaims
async
¶
watch_reclaims(
after_id: int = 0, *, poll_timeout: float | None = None
) -> AsyncIterator[EventRow]
Stream fleet-wide crash-reclaim events as :class:EventRow objects.
Delivery guarantee. At-least-once, and gap-free only under a
bounded writer-transaction assumption: an event is silently and
permanently missed if a job_events writer transaction stays
open longer than reclaim_event_visibility_delay (default 2s ,
see :data:taskq.constants.RECLAIM_EVENT_VISIBILITY_DELAY)
between its INSERT and its COMMIT. Ids are allocated at INSERT
time but transactions commit out of order, so a late-committing
lower-id row can land behind the cursor after the cursor has
already advanced past its position; no error is raised anywhere.
Sweep and terminal-write transactions are a handful of
single-round-trip statements, so 2s is a generous bound under
normal operation, but it is an assumption enforced by nothing in
the SQL, not a property the query guarantees. So this watcher
does not leave detection to chance: on a slow cadence
(_VISIBILITY_RISK_CHECK_INTERVAL, 60s) it runs the backend's
check_reclaim_visibility_delay_risk diagnostic (when the
backend implements it, PostgresBackend does) and logs a loud
watch-reclaims-visibility-delay-at-risk warning for every
long-open job_events writer it finds. That is a proxy
warning, not proof of an actual miss.
Yields job_events rows with kind='state_change' and
detail['reason']='lock_expired', ordered by the monotonic
event_id cursor ascending. to_state is 'pending' for
a retried reclaim, 'crashed' or 'cancelled' (cancel was
in-flight when the worker died) for a terminal one. cause
names which deadline fired, 'lock_expired' (the worker's
global lease) or 'heartbeat_timeout' (the per-job promise)
for a leader-sweep reclaim, 'isolate_self' for a reclaim a
worker performed on its own jobs when its heartbeat connection
died (isolate_self), the same channel so a consumer fanning
out on this feed sees heartbeat-loss self-recovery too.
Cursor and duplicate semantics¶
The caller persists the last-seen event_id and passes it back
as after_id on resumption. Persist the cursor after
processing each event: a crash between processing and persisting
re-delivers that event on the next run, delivery is
at-least-once, so consumers must dedupe on event_id. The
cursor is a watermark, not a reference: pruning job_events
rows at or below it is always safe, but rows pruned before the
consumer reads them are gone for good, only prune rows older
than your slowest consumer's cursor. A cursor far behind after
a long outage drains at query speed (full batches are re-polled
immediately, not one batch per poll_timeout).
Your own pruning is not the only deleter. The built-in event
retention sweep deletes these rows by age alone, cursor position
irrelevant: the lock_expired slice is carved out of the
ordinary event_retention_period window but only up to
:data:~taskq.constants.RECLAIM_OUTBOX_RETENTION_MULTIPLIER
times it (100x), after which it is deleted like any other event.
The terminal-job prune is a second deleter: its cascade removes
the archived jobs' events with them. Both deleters record their
highest deleted event id in the event-prune watermark
(job_events_prune_state, migration 01.00.20_02) in the same
transaction as the delete, and every delivery path of this
generator opens on the gap gate: a resumed cursor (a persisted
cursor, after_id > 0) strictly below the watermark ends the
stream with :class:~taskq.exceptions.EventRetentionGapError
before anything is delivered. The signal is conservative (the
watermark is a union bound across event kinds: it proves the feed
is incomplete, not which slice lost a row), and it closes the
silent-loss hole -- a consumer lagging past the horizon
would otherwise skip to live with no error on either side, free to
believe it saw everything. A fresh watcher (after_id=0) is a
new tail and never raises; a cursor at or above the watermark
never raises, every deleted id was at or below a position the
consumer had already been delivered. Size the retention period
against your slowest consumer's worst outage all the same: the
gate turns an over-long outage into a loud, actionable end, not
into a recovered feed.
Shutdown and backpressure¶
This is a pull-based async generator: events are fetched only as
fast as the consumer iterates, so a slow consumer polls
slower, no internal buffer grows. To stop, break out of the
async for (or cancel the consuming task); generator cleanup
removes the LISTEN registration and closes any owned connection.
Transport¶
On Postgres with a LISTEN transport source (dsn,
pg_conn_factory, or listen_conn), the method LISTENs on
wake_channel(schema) purely as a low-latency wakeup, but
always polls backend.poll_reclaim_events(after_id) as the
durable source of truth, NOTIFY is an optimisation, never the
only path, and a dropped LISTEN connection degrades to (and
recovers from) polling automatically. Without a LISTEN
transport or on Redis-configured backends, a plain poll loop
against poll_reclaim_events runs on poll_timeout.
Usage::
cursor = await load_cursor() # your own durable store
async for evt in tq.watch_reclaims(after_id=cursor):
outstanding -= 1 # fan-out completion tracking
cursor = evt.event_id
await save_cursor(cursor) # persist AFTER processing
Raises¶
RuntimeError
Called before tq.open() or outside an async with block.
EventRetentionGapError
The resumed cursor sits behind the event-prune watermark:
retention deleted undelivered events, the stream ends instead
of silently skipping to live.
Source code in src/taskq/client/_taskq.py
1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 | |
orjson_response_class ¶
Return the orjson-backed :class:starlette.responses.JSONResponse.
Drop-in replacement for starlette's stdlib-json JSONResponse (same
constructor, same application/json media type, byte-identical
bodies for JSON-representable payloads), bodies are rendered through
:func:taskq._json.dumps per the project's never-import-stdlib-json
rule. Requires the fastapi extra (starlette) at call time.
Usage with :class:TaskQ, see the :class:JobEvent docstring for the
full pattern::
OrjsonJSONResponse = orjson_response_class()
@app.get("/tasks/{job_id}")
async def read_task(job_id: UUID) -> Response:
return OrjsonJSONResponse(await tq.get_state(job_id))
Source code in src/taskq/client/_taskq.py
_jobs ¶
JobsClient, the primary entry point for enqueuing, querying, and cancelling jobs, and managing cron schedules.
Wraps a :class:~taskq.backend._protocol.Backend instance and adds the
client-layer behaviours the protocol intentionally omits: payload
serialization through the actor's payload_type, CancelResult
construction in :meth:cancel, typed :class:JobHandle[R]
wrapping in :meth:enqueue / :meth:get, and cron schedule management
via :meth:create_schedule, :meth:list_schedules,
:meth:update_schedule, :meth:delete_schedule.
The backend is injected at construction so the same client can target
either an :class:~taskq.testing.in_memory.InMemoryBackend (tests) or a
:class:taskq.backend.postgres.PostgresBackend (production).
JobsClient ¶
JobsClient(
backend: Backend,
*,
clock: Clock | None = None,
settings: TaskQSettings | None = None,
capacity_cache_ttl: float = DEFAULT_CAPACITY_CACHE_TTL,
)
Public API for job operations.
Delegates to the injected :class:Backend and wraps results in
typed :class:JobHandle[R] instances. The client owns the
payload-serialization step that turns a typed P into the
dict[str, object] carried by :class:EnqueueArgs; the backend
sees only erased payloads.
Source code in src/taskq/client/_jobs.py
backend
property
¶
The underlying :class:Backend this client delegates to.
Exposed so :class:JobHandle can read the backend through the
client without accessing the private _backend attribute.
close
async
¶
invalidate_actor_capacity_cache ¶
Drop the cached actor_config.max_pending snapshot.
The next enqueue refreshes from the backend instead of waiting
out the TTL. Not needed in normal operation (staleness is
bounded by capacity_cache_ttl, default 5s); intended for
tests and for tooling that knows it just changed the table and
cannot wait out the TTL.
Source code in src/taskq/client/_jobs.py
enqueue
async
¶
enqueue(
ref: ActorRef[P, R],
payload: P,
*,
queue: QueueName | None = None,
allow_unregistered: bool = False,
scheduled_at: datetime | None = None,
priority: int | None = None,
schedule_to_close: datetime | None = None,
start_to_close: timedelta | None = None,
heartbeat_timeout: timedelta | None = None,
identity_key: IdentityKey | None = None,
fairness_key: str | None = None,
idempotency_key: IdempotencyKey | None = None,
idempotency_scope: str | None = None,
trace_id: str | None = None,
span_id: str | None = None,
metadata: dict[str, object] | None = None,
tags: list[str] | None = None,
) -> JobHandle[R]
Enqueue a job for the given actor and return a typed handle.
The payload is serialized through ref.payload_type so the
EnqueueArgs.payload carried over the backend boundary is a
plain dict[str, object] ready for the JSONB column. The
returned :class:JobHandle[R] carries ref.result_adapter so
:meth:JobHandle.wait can validate the stored result back to
R.
The actor's name is NOT validated against the registration table
here: any :class:~taskq.actor.ActorRef is accepted, and a ref
whose actor no worker fleet declares enqueues successfully and
then parks (every worker that claims it snoozes it back with
released_reason: "actor-not-found", budget-free; the
stranded-jobs detector surfaces it). If "my job never runs",
check the actor spelling against the worker's registry and
taskq job show's released reason before looking at queues or
capacity. This is the same defer-to-fire-time doctrine
:meth:create_schedule documents.
The metadata.singleton key is reserved by the library for
singleton enforcement. When ref.singleton is True the
library unconditionally writes metadata.singleton = True,
overriding any caller-supplied value. Callers MUST NOT set
metadata.singleton manually.
max_pending:
-
When the actor's effective
max_pendingis set, a pre-flight count ofpending+scheduledjobs for the actor is compared to the limit. Ifcount >= max_pending, :class:MaxPendingExceededErroris raised synchronously, the caller decides whether to retry, fail, or wait; the library does not block on capacity. -
The effective limit is operator-owned: a non-NULL stored
actor_config.max_pending(set viataskq actor-config set --max-pending) wins over the@actor(max_pending=...)literal; a cleared or absent stored value falls back to the literal. The client reads the stored value through a TTL-bounded cache (default 5s staleness; see :class:taskq.client._capacity.ActorCapacityCache), so an operator change takes effect fleet-wide within seconds without any redeploy or restart. -
Evaluation order at enqueue:
unique_fordedup → singleton pre-flight →max_pendingcount check →idempotency_keyINSERT → job INSERT. Aunique_forhit bypasses all remaining checks; a singleton collision fires beforemax_pendingto give the caller the more specificSingletonCollisionError. -
idempotency_keydoes not bypassmax_pending, the idempotency ON CONFLICT fires at step 5, after the max_pending check at step 3. Re-enqueuing with a duplicateidempotency_keywhen the queue is full raisesMaxPendingExceededError, not the deduplicated handle. Onlyunique_for(step 1) bypasses max_pending.
idempotency_key:
-
idempotency_keyis unique within itsidempotency_scope(composite(idempotency_scope, idempotency_key)uniqueness). The default scope (idempotency_scope=Noneor"") preserves the prior global-until-prune behavior exactly, so existing callers see zero behavior change. Passing an explicit scope (e.g. a run/batch/epoch id) lets two enqueues with the same business key in different scopes both succeed, decoupling the dedupe horizon fromprune_retention_*. -
Key length is bounded at
idempotency_key_max_bytes(TASKQ_IDEMPOTENCY_KEY_MAX_BYTES, default 1024 UTF-8 bytes) , the bound is the composite unique index's btree entry size, not a round number. Empty and whitespace-only keys raise :class:ValueErrorat the client boundary before any backend call. The same bound applies toidempotency_scope; an empty scope ("") is valid and equivalent toNone(the default/global scope). -
No time-based (TTL) dedupe window.
idempotency_scopedecouples the dedupe horizon fromprune_retention_*by namespace, not by time, there is noidempotency_ttlor equivalent "dedupe for the next N seconds" parameter. A key within a given scope still dedupes until pruned, exactly like the pre-scope global behavior, just scoped to that namespace. This is a deliberate scope decision, not an oversight: a real sliding-window TTL cannot be expressed as a single static unique index the way scope can, a sliding window would require either abandoning the atomicINSERT ... ON CONFLICTfor a check-then-insert lock (weaker concurrency guarantee) or encoding time-bucketing into the key itself (coarser, edge-artifact-prone semantics). If your use case genuinely needs "dedupe for the next hour, not forever," encode the window into the scope yourself (e.g. a time-bucketed scope string) until/unless a TTL parameter ships as a separate feature. -
Rolling-deploy note: if this schema is mid-upgrade (the
01.00.03_01_pre_idempotency_scope.sqlmigration applied but01.00.03_01_post_idempotency_scope_drop_old_index.sqlnot yet applied), reusing the sameidempotency_keyunder two differentidempotency_scopevalues raises :class:~taskq.exceptions.ScopedIdempotencyMigrationPendingErrorrather than silently dedupe against the wrong scope's job. The trigger is a key existing under a different scope, in either direction, an unscoped call reusing a key first written under a non-default scope raises it too. Only brand-new keys and same-scope repeats are unaffected. See that exception's docstring and the migration file's header comment for the full rationale.
unique_for:
-
unique_fordeduplication is serialized, not best-effort: the preflight and the INSERT run under one transaction-scoped advisory lock on(actor, identity_key), so concurrent enqueues of the same identity produce exactly one job and every caller is handed that job. This holds on pool connections and on a caller-supplied connection with no open transaction. On a caller-owned OPEN transaction the lock spans that transaction instead, so single-flight holds until its commit/rollback, a long-lived caller transaction can exhaust another same-identity enqueue's bounded wait (:class:~taskq.exceptions.UniqueForLockTimeoutError) rather than dedup against it. The dispatch CTE'srunning_identitiesfilter remains the execution-level guard behind the enqueue-level one. -
When either dedup mechanism matches an existing job,
JobHandle.was_existingisTrue. This field replaces the need for callers to inspect the row'screated_atto detect a dedup return.
allow_unregistered:
- The per-submit escape from
TASKQ_QUEUES_STRICT: with strict on, a queue no registered actor's assignment routes to raises :class:~taskq.exceptions.UnknownQueueErrorbefore any write;allow_unregistered=Trueskips that verdict for THIS call — for genuinely dynamic queue names (tenant-prefixed, sharded at runtime) that no registration can predict. The unserved-queue NOTE still fires (the diagnostic sits below the gate), so a typo'd name stays visible even on an escaped call. With strict off the flag changes nothing. The batch arms carry no per-item flag: their queues are the items' actors' own declared routes, the thing registration populates; use single enqueues for dynamic names.
Source code in src/taskq/client/_jobs.py
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 | |
enqueue_batch
async
¶
enqueue_batch(
items: list[EnqueueItem],
*,
batch_id: UUID | None = None,
connection: Connection | None = None,
failure_policy: BatchFailurePolicy | None = None,
finalizer: EnqueueItem | None = None,
) -> BatchHandle
Enqueue multiple jobs in a single batched INSERT and return a
:class:~taskq.batch.BatchHandle.
All items share a single batch_id UUID written into each
job's metadata.batch_id field (as a string). When
batch_id is not supplied it is auto-generated as a UUIDv7 via
:func:~taskq._ids.new_job_id.
failure_policy:
When failure_policy is set (e.g.
:class:~taskq.batch_policy.AbortBatchAfter), a batches row
is created with the policy's failure threshold. After each child
job reaches a terminal state, the
:func:~taskq.batch.apply_batch_terminal_outcome hook inspects
the outcome: succeeded resets the consecutive-failure
counter; failed increments it and aborts the batch if the
threshold is reached. Aborting cancels all pending and scheduled
child jobs and sets the batch row to aborted.
finalizer:
When finalizer is set, a finalizer job is enqueued alongside
the batch. The finalizer is NOT stamped with batch_id
metadata (deadlock prevention, if it were, wait_for_batch
would count it as a child and the finalizer would wait for
itself). The batch row's finalizer_job_id column records the
link, and wait_for_batch automatically excludes that job
from counts. The finalizer is dispatched immediately; the
in-actor wait_for_batch snooze pattern gates on child-job
completion.
Transactional enqueue:
When failure_policy or finalizer is set and
connection is None, the entire operation (batch row +
all child jobs + finalizer) is inserted in a single transaction
via :meth:Backend.enqueue_batch_atomic. If any insert fails,
no rows are committed. When a connection is provided, the
caller controls the transaction boundary; the batch row and
finalizer are created as the last statements on that connection.
Validation rules:
len(items) == 0raises :class:ValueError.len(items) > MAX_BATCH_SIZEraises :class:ValueError.- ALL payloads are validated before any INSERT. A single failure
raises :class:
~taskq.exceptions.PayloadValidationErrorand leaves no rows inserted.
max_pending:
Admission is partitioned per actor. The backend issues one
aggregated SELECT actor, count(*) … WHERE actor = ANY($1)
GROUP BY actor for the entire batch, resolves per-actor
effective limits (operator-owned stored value when set, else the
@actor(...) literal, same resolution as :meth:enqueue),
and admits every within-cap actor's items; an over-cap actor's
items are refused as a whole group (never partially filled up to
the cap). When any actor is refused, the within-cap actors'
items are still enqueued and
:class:~taskq.exceptions.BatchMaxPendingExceededError raises
afterwards, it names each refused actor, the refused item
indices into items, and the admitted count, so a retry can
target only the refused items (or rely on idempotency_keys
to deduplicate a whole-batch retry). The exception is
deliberately not a :class:~taskq.exceptions.MaxPendingExceededError
subclass: handlers for that type assume nothing was enqueued,
and under this error part of the batch is already stored.
When failure_policy or finalizer is set and
connection is None (the atomic path), a cap violation
keeps the all-or-nothing contract instead: the whole single
transaction rolls back and plain
:class:~taskq.exceptions.MaxPendingExceededError raises with
nothing committed.
On a caller-supplied connection with an open transaction, the admitted items are inserted but their durability follows that transaction's commit/rollback.
unique_for (not applied on batch paths):
Actor-declared unique_for is a single-enqueue contract (see
:meth:enqueue). The batch INSERT writes every item without a
per-identity preflight, the single path's advisory-lock +
preflight round trips are exactly what bulk throughput exists to
avoid, so two batch items with the same
(actor, identity_key) both insert, and the dispatch CTE's
identity serialization ensures only one runs at a time.
unique_for items are conservatively fully counted toward
max_pending (a batch mixing unique_for retries near the
cap may refuse loudly rather than admit silently). Deduplicate
batch items with per-item idempotency_keys, that arbiter IS
applied, below. Whether the batch tier should honor
unique_for is an open design decision; this documents
current behavior.
idempotency_key collisions:
Items whose idempotency_key collides with an existing row
return the existing :class:~taskq.client.JobHandle (same
semantics as single-item :meth:enqueue).
Source code in src/taskq/client/_jobs.py
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 | |
enqueue_batch_streaming
async
¶
enqueue_batch_streaming(
items: Iterable[EnqueueItem],
*,
batch_id: UUID | None = None,
connection: Connection | None = None,
failure_policy: BatchFailurePolicy | None = None,
finalizer: EnqueueItem | None = None,
chunk_size: int = DEFAULT_CHUNK_SIZE,
) -> BatchHandle
Enqueue jobs from a lazy iterable in chunks, returning a single
:class:~taskq.batch.BatchHandle.
Unlike :meth:enqueue_batch, this method accepts an
:class:~collections.abc.Iterable (including generators) and
inserts in chunks of chunk_size (1-MAX_BATCH_SIZE). All items share
the same batch_id. Payloads are validated on the fly as
each chunk is built.
When failure_policy or finalizer is set and
connection is None, the entire operation is delegated to
:meth:Backend.enqueue_batch_atomic for single-transaction
atomicity. Otherwise, chunks are inserted via
:meth:Backend.enqueue_batch: with a caller-supplied connection
all chunks share that connection's open transaction (one
transaction aggregate, the caller owns the boundary); with NO
connection each chunk is its own pool transaction, so progress
is committed incrementally, chunk by chunk.
No-connection failure surface: when chunk N fails (cap
refusal, payload validation, a driver error), chunks 1..N-1
are already durably committed and nothing is returned, the
BatchHandle is only constructed after the whole stream
drains. A blind retry of the full iterable would duplicate the
committed prefix. Retry safely by giving items
idempotency_keys (a retry deduplicates against the committed
rows) or by resuming from the point of failure.
Failure-policy counting limitation (caller-connection path):
the batch row is created AFTER all chunk inserts, it must carry
the final expected_size and create_batch is INSERT, not
upsert, so a child job enqueued on that connection that
reaches a terminal state BEFORE the row exists is not counted
toward failure_policy: increment_batch_failures finds
no row and returns (0, None, 0). The atomic
(no-connection) path is unaffected, its single transaction
makes the batch row and the child jobs visible together.
max_pending: enforced per chunk with the same per-actor
partition as :meth:enqueue_batch, each chunk's items carry
their actors' effective caps (operator-stored override when
set, else the @actor(...) literal, same resolution as
:meth:enqueue), the backend admits every within-cap actor's
items, and an over-cap actor's items are refused. On the
caller-connection path sequential chunks share one transaction,
so per-chunk admission accumulates to the true aggregate. On
the no-connection path each chunk counts against the rows the
previous committed chunks wrote, which also accumulates to the
true aggregate, but the refusal surfaces after the committed
prefix: :class:~taskq.exceptions.BatchMaxPendingExceededError
raises with STREAM-GLOBAL refused item indices and an
admitted_count covering every committed item; items after
the refusing chunk were never attempted. Retry only the
refused items (or rely on idempotency_keys). The atomic
path (failure_policy/finalizer, no connection) resolves
the same effective caps and keeps all-or-nothing: a cap
violation rolls back the entire single transaction and raises
plain :class:~taskq.exceptions.MaxPendingExceededError with
nothing committed.
unique_for (not applied on batch paths): actor-declared
unique_for is a single-enqueue contract (see
:meth:enqueue_batch's disclosure), every item writes, items
sharing an (actor, identity_key) are not deduplicated, and
unique_for items are fully counted toward max_pending.
This holds on both arms (the per-chunk INSERT and the atomic
path's chunked INSERT). The idempotency_key arbiter IS
applied on both arms: a colliding item returns the existing
row, exactly as :meth:enqueue_batch does.
Source code in src/taskq/client/_jobs.py
1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 | |
get_batch
async
¶
Fetch a single batch row by ID.
Delegates to :meth:Backend.get_batch. Returns None when the
batch does not exist.
Source code in src/taskq/client/_jobs.py
list_batches
async
¶
List batches matching filter, returning :class:BatchSummary objects.
Delegates to :meth:Backend.list_batches and maps each
(BatchRow, BatchCounts) pair to a :class:BatchSummary
with a :class:BatchCompletionStatus derived from the live counts.
Source code in src/taskq/client/_jobs.py
enqueue_batch_fast
async
¶
enqueue_batch_fast(
items: list[EnqueueItem],
*,
batch_id: UUID | None = None,
connection: Connection | None = None,
) -> int
Enqueue jobs via COPY FROM protocol for maximum throughput.
WARNING, bulk-import semantics, not general-purpose enqueue:
this method does NOT detect or
reject idempotency-key collisions (a duplicate key aborts the
whole batch instead of being treated as "already enqueued"), and
returns a bare row count, not per-job handles, there is no
way to await, cancel, or otherwise reference an individual job
from the return value. max_pending IS enforced, with the same
per-actor partition admission and the same effective-cap
resolution (operator-stored override when set, else the
@actor(...) literal) as :meth:enqueue_batch (see its
docstring). Use :meth:enqueue_batch
unless you specifically need COPY-level throughput for a one-shot
bulk import/backfill and have already accounted for these gaps.
Returns the count of inserted rows, no :class:~taskq.batch.BatchHandle,
no :class:~taskq.client.JobHandle instances.
Validation rules:
len(items) == 0raises :class:ValueError.len(items) > 50_000raises :class:ValueError.- ALL payloads are validated before any INSERT, a single failure
raises :class:
~taskq.exceptions.PayloadValidationError.
Tradeoffs vs enqueue_batch:
- No idempotency-key collision handling. A duplicate key
aborts the entire batch with
:class:
~taskq.exceptions.DuplicateIdempotencyKeyError, a same-(idempotency_scope, idempotency_key)pair, whether repeated within the batch or already stored; nothing is written. Callers must pre-deduplicate. One carve-out: during the01.00.03pre→post migration window, a key reused across different scopes raises :class:~taskq.exceptions.ScopedIdempotencyMigrationPendingErrorinstead, matching the other enqueue paths. - No unique_for preflight. Actor-declared
unique_foris a single-enqueue contract (see :meth:enqueue_batch's disclosure): the COPY writes every item, items sharing an(actor, identity_key)are not deduplicated, andunique_foritems are fully counted towardmax_pending, same semantics as the unnest batch tier. - max_pending partition admission. One aggregated count runs
before the COPY: within-cap actors' rows are written, and an
over-cap actor's items are refused, the COPY of the admitted
rows commits first, then
:class:
~taskq.exceptions.BatchMaxPendingExceededErrorraises naming the refused actors and item indices (retry only those, or rely on idempotency keys). - No JobHandle instances. Only the inserted row count is
returned. Use
batch_idto query rows post-insert. - All-or-nothing on constraint violations. The entire COPY fails on any constraint violation (duplicate keys, singleton, CHECK), only cap admission partitions.
Use for bulk import / backfill with 1K-50K rows where throughput matters more than idempotency guarantees.
Source code in src/taskq/client/_jobs.py
1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 | |
get
async
¶
Look up a job by id.
Returns None when the job does not exist; otherwise wraps
the row in a :class:JobHandle[R]. The lookup reads the hot
jobs table first and falls back to jobs_archive (the
same jobs-then-archive probe taskq job show applies), so an
id whose row a prune moved to the archive returns that row's
handle with :attr:JobHandle.archived True instead of
reading as missing; None still means "in neither tier".
The caller may supply
result_adapter because lookups by id do not carry actor
identity, typical sources are
my_actor.result_adapter (when reuniting with an actor) or
TypeAdapter(type(None)) (when only row metadata is needed).
When result_adapter is None it defaults to
TypeAdapter(type(None)), which is suitable for status-only
lookups.
Source code in src/taskq/client/_jobs.py
get_row
async
¶
Look up a job by id and return the raw :class:JobRow.
Mirrors :meth:get's contract, one backend.get (hot table
first, jobs_archive fallback, an archive hit marked
archived=True), None when the job exists in neither
tier, without the handle machinery or
result adapter. For callers that never need a
:class:JobHandle, this is the direct form; for the fresh-read
case that does want a handle, prefer get plus the handle's
row property (still a single round trip).
Source code in src/taskq/client/_jobs.py
backpressure
async
¶
Read the submit path's backpressure state for queues.
A read-only typed read (the LIB-2 design, mirroring the
reader-method precedent of :meth:get_row / :meth:list) —
deliberately NOT an enqueue kwarg: enqueue is generic
JobHandle[R] and no flag may flip the reply type.
Per queue (see :class:~taskq.types.QueueBackpressure):
depth— jobs holding a pending slot IN THIS QUEUE (pending + scheduled), the queue-local ops view, ONE indexed aggregate for the whole call (count_pending_jobs_by_queue, thecount_active_jobspattern). The worker-side leader sampler's cache is in-process on the worker and not readable cross-process; the depth here is a real count, not a pretense. REPORTED, not verdict-bearing — the admission cap is per-ACTOR and governs the actor's count across ALL queues, so a queue-local count against an actor cap would false-OK exactly when an actor's traffic splits across queues.binding_actor/admission_load/effective_max_pending— the VERDICT's basis: the routing actor with the smallest headroom (its OWN pending+scheduled count, ALL queues, fromcount_pending_jobs— theenqueue_max_pending_countgrouping — against its stored cap from theActorCapacityCacheTTL snapshot). The data model has no per-queue depth threshold; the boundary is the actor's.children_depth— when a fan-out parent is in play (parent_id, or the ambient worker contextcurrent_parent_id()), the parent's pending children OUTSIDE the queue, exact via theparent_idledger stamp (01.00.23_01). REPORTED, not verdict-bearing: every pending child already counts toward its OWN actor's cap (wherever it sits), which is the number admission enforces — the fan-out pressure reaches the verdict through the routing actors' admission loads, and this field is the parent-attributed ledger view of the same rows. Children already in the queue are in itsdepthand never counted twice.state—overwhen any routing actor's OWN count is at or over its cap (the next enqueue for that actor is likely refused withMaxPendingExceededError),okonly when every routing actor resolves and none is over (an unresolvable actor may be over — claiming ok would be the false-ok),unknownwhen the verdict could not be computed (sick database, the actor-count read missing, no capacity snapshot, a cap that lives only in the@actorliteral).reasonnames the absent half. Fail-open, never a fabricated verdict.
ADVISORY semantics: the race between this check and the later
enqueue is acceptable (the signal predicts admission, it does
not reserve it); the snapshot carries as_of and
cap_age_seconds so its own staleness travels with it (the
cap age is bounded by the TTL, default 5s, and reports the last
SUCCESS, not the last attempt). Cost: one depth aggregate + one
actor-count aggregate per call, plus one more only when a
parent is in play; unknown on backend trouble — the read
never raises except for a missing schema
(:class:SchemaNotMigratedError, the actionable setup defect).
Source code in src/taskq/client/_jobs.py
1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 | |
list
async
¶
List jobs matching filter, returning a :class:JobPage.
filter.status accepts a single :data:JobStatus or a
sequence of statuses (e.g. JobFilter(status=["pending",
"running"])).
filter.unfinished is a meta-filter: unfinished=True selects
non-terminal statuses (pending, scheduled, running, 'not yet
finished', not 'currently executing') and unfinished=False selects
terminal ones. See :class:JobFilter for full semantics.
next_cursor is returned for every ordering, encoded from the
columns that ordering actually sorts by, and is None exactly on
the last page: the backend is asked for one row past the limit, so
a last page that happens to fill the limit is still known to be the
last, and a caller paging until the cursor runs out never fetches an
empty trailing page. filter.limit is capped at
:data:~taskq.backend._protocol.MAX_JOB_LIST_LIMIT (ValueError
above it); at the ceiling there is no room to look past the limit,
so a full page there carries a cursor that may lead to one empty
page.
Source code in src/taskq/client/_jobs.py
cancel
async
¶
Request cancellation of a job and return a :class:CancelResult.
Reads the row first via :meth:Backend.get. If the job does not
exist, raises :class:KeyError (carrying job_id), matching
Python's stdlib idiom for "asked for an entry by id; it isn't
there" — note this means a typo'd id and a pruned job are both
:class:KeyError, never a "not found" result; check existence
with :meth:get first when the distinction matters.
Then calls :meth:Backend.write_cancel_request and reads the
row again to capture the new status. The previous_status
reflects the row at the first read, not atomically at
write-time: the status can move between the two reads (a TOCTOU
window inherent to a request-write protocol), so a handler that
must not race the transition should re-check new_status
rather than assume previous_status.
Metrics: increments taskq.cancellation.requested exactly once
per call, regardless of cancellation_initiated outcome.
Source code in src/taskq/client/_jobs.py
cancel_where
async
¶
cancel_where(
filter: JobFilter,
reason: str | None = None,
*,
allow_empty_filter: bool = False,
) -> BulkCancelResult
Cancel all jobs matching filter in a single set-based operation.
Pending/scheduled jobs are moved straight to terminal 'cancelled'
(no running actor to cooperate with). Running jobs get
cancel_phase=1 set (cooperative cancel), the worker's
heartbeat-driven cancel controller observes the phase change and
sets the in-process cancel_event.
Guardrail: a filter with no predicates (no queue, status,
actor, identity_key, batch_id, tags, active, or created_before)
is rejected with :class:EmptyFilterError unless
allow_empty_filter=True is passed.
Filter fields used: queue, status, actor,
identity_key, batch_id, tags, active,
created_before. The limit, cursor, and order_by
fields are ignored.
Returns a :class:BulkCancelResult with counts and affected IDs.
Source code in src/taskq/client/_jobs.py
create_schedule
async
¶
create_schedule(
actor: str | ActorRef[P, R],
cron_expr: str,
*,
timezone: str = "UTC",
dst_strategy: DstStrategy = "skip",
payload_factory: str | None = None,
static_payload: dict[str, object] | None = None,
name: str = "",
identity_key: IdentityKey | None = None,
enabled: bool = True,
owner: Literal["code", "operator"] = "operator",
) -> ScheduleHandle
Create a cron schedule. Raises :class:ValueError if both
payload_factory and static_payload are provided, or if
cron_expr is invalid.
owner declares who owns the schedule's enable/disable lifecycle.
"operator" (the default here: a client-created schedule is an
operator act) records operator ownership from birth, so a schedule
created disabled carries disabled_by='operator' and no worker
boot ever reverts it; "code" matches a @cron declaration,
whose stale auto-disable a boot reverts (see
:class:CronScheduleSpec).
The (actor, name) UNIQUE constraint means each (actor, name)
pair may have at most one schedule; a second create_schedule for
the same pair raises asyncpg.UniqueViolationError (PG) or
:class:ValueError (in-memory). Pass distinct name values to run
several cron schedules per actor (e.g. a per-property sync).
When identity_key is set, the cron loop propagates it to cron-fired jobs so they dedup against on-demand jobs for the same business key.
Does NOT validate actor existence at creation time, any string actor name is accepted (validation is deferred to fire time).
The first next_fire_at is seeded from the clock that
arbitrates its due-check: on a Postgres-backed client the PG
server clock is read first (one-row SELECT clock_timestamp()
via the backend's pool, mirroring the worker bootstrap), so
app↔DB clock skew cannot shift the fire chain; on a pool-less
(in-memory) client the seed comes from the client's injected
Clock. This matters permanently: the cron loop's normal path
recomputes every subsequent fire from the STORED fire time
(only a miss beyond cron_catch_up_window re-anchors on the
server clock), so the seed, not any per-tick correction ,
fixes the chain's phase for the schedule's life.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dst_strategy
|
DstStrategy
|
How to handle DST gaps and overlaps.
|
'skip'
|
Source code in src/taskq/client/_jobs.py
2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 | |
list_schedules
async
¶
List cron schedules, optionally filtered by actor or enabled status.
Source code in src/taskq/client/_jobs.py
update_schedule
async
¶
update_schedule(
schedule_id: UUID,
*,
cron_expr: str | None = None,
enabled: bool | None = None,
payload_factory: str | None = None,
static_payload: dict[str, object] | None = None,
clear_payload_factory: bool = False,
) -> ScheduleRecord
Update a cron schedule. Setting enabled=True clears
last_fire_error and resets consecutive_failures to 0.
Raises :class:ValueError if both payload_factory and
static_payload are provided, or if cron_expr is invalid.
To explicitly clear payload_factory (set the column to NULL),
pass clear_payload_factory=True, None for payload_factory
means "don't change this field."
When cron_expr changes, the recomputed next_fire_at is
seeded from the same clock as create_schedule (the PG
server clock on Postgres-backed clients; the client's injected
Clock in-memory), the stored chain keeps its server-anchored
phase.
Source code in src/taskq/client/_jobs.py
2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 | |
delete_schedule
async
¶
_handle ¶
Generic :class:JobHandle, typed handle to an enqueued job.
Carries a :class:pydantic.TypeAdapter for the actor's return type
R, which is the mechanism that prevents R from being a phantom
type parameter. The single blocking accessor :meth:wait returns R
(never R | None); it raises on missing / failed / timeout.
The handle reads the backend through self._backend for
:meth:wait; read-back operations (:meth:status, :meth:refresh,
:meth:attempts, :meth:cancel) require a :class:JobsClient and
raise :class:RuntimeError when the handle was constructed without
one.
JobHandle ¶
JobHandle(
*,
row: JobRow,
result_adapter: TypeAdapter[R],
was_existing: bool,
client: JobsClient | None = None,
backend: Backend | None = None,
_redis_client: Redis | None = None,
_settings: TaskQSettings | None = None,
)
Typed handle to a single enqueued job.
Created by :class:JobsClient methods (:meth:~JobsClient.enqueue,
:meth:~JobsClient.get) or by :class:SubJobEnqueuer (with
backend= only). The type parameter R flows from the
actor's declared return type through :class:ActorRef into this
handle: JobHandle[OrderResult] for an actor returning
OrderResult, JobHandle[None] for fire-and-forget actors.
At least one of client or backend must be supplied. When
client is provided, _backend is filled from
client.backend. When only backend is provided, the four
read-back methods (:meth:status, :meth:refresh,
:meth:attempts, :meth:cancel) raise :class:RuntimeError
because they require the client's higher-level coordination.
:meth:wait always works (it reads through _backend directly).
Why result_adapter: TypeAdapter[R] is a constructor arg: pyright
only infers R for a generic class when the type parameter
appears in at least one field or method signature. The adapter is
that field, without it R would be phantom and inference would
silently fall back to Unknown.
Source code in src/taskq/client/_handle.py
deduplicated_onto_terminal
property
¶
This enqueue deduplicated onto a job that had already finished.
was_existing cannot carry this on its own: it is True for
every dedup, and the overwhelmingly common dedup, onto a live
pending or running job, is the mechanism working, the whole
point of an identity key. The case that needs a signal is the
rare one where the match was a job that already reached a
terminal state: no worker will pick that work up, and with a long
dedup horizon the caller can wait for the rest of the window
before anyone notices, because the strand looks exactly like the
success until the work was needed.
Decided from the row the creating call handed back, so learning
this costs no second round trip, and frozen there rather than
re-read from the live row, because it is a verdict about the
enqueue, not about the job's status now: a dedup onto a running
job that later finishes was never a strand. It pairs with the
WARN-level enqueue_deduplicated line the backend emits
for the same hit: that one is the operator's signal, this one is
the caller's.
archived
property
¶
The last observed row came from the archive tier.
Mirrors :attr:row's last-observed discipline: it reads
:attr:row's archived flag, so it is True exactly when
the job's row now lives in jobs_archive (the hot row was
pruned), and it advances with every :meth:refresh /
:meth:status / :meth:wait fetch like the row itself.
Constructing from an enqueue always starts False.
row
property
¶
The last :class:JobRow this handle observed.
Seeded at construction with the row the creating call fetched,
and advanced by every successful row fetch through the handle ,
:meth:refresh, :meth:status, and :meth:wait's polling loop
each record the row they just read. :meth:progress_stream
does not advance it, its Redis path fetches no rows, and
advancing only on the PG fallback would make the semantics
backend-dependent. Reading the property costs no backend round
trip: handle = await tq.get(id) followed by handle.row
is the single-read pattern for full row state, and a
long-lived handle's row stays current as its owner
refreshes or waits.
The row is returned by reference (it is frozen and backends hand the handle an isolated row); repeated reads between fetches return the same object.
status
async
¶
Return the current status of this job (live read).
Cheap, non-blocking: a single backend.get and a status
projection. No polling. Use this when you want to know the
state without waiting for a terminal transition. Advances
:attr:row to the fetched row. An id whose row the prune moved
to jobs_archive answers from there (its terminal status,
:attr:archived True); only an id that exists in neither
tier raises.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
this handle was constructed without a
:class: |
KeyError
|
the job exists in neither |
Source code in src/taskq/client/_handle.py
refresh
async
¶
Re-read the row from the backend and return the raw
:class:JobRow.
Useful for callers that want full row state (timestamps,
attempt counts, error metadata) without going through
:meth:wait. Does not block on terminal state, returns the
current row whatever its status. Advances :attr:row to the
fetched row, so after a refresh handle.row and the return
value are the same row. An id whose row the prune moved to
jobs_archive refreshes from there (archived=True on the
row); only an id that exists in neither tier raises.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
this handle was constructed without a
:class: |
KeyError
|
the job exists in neither |
Source code in src/taskq/client/_handle.py
attempts
async
¶
Return the attempt rows for this job, ordered by attempt number.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
this handle was constructed without a
:class: |
Source code in src/taskq/client/_handle.py
cancel
async
¶
Delegate to :meth:JobsClient.cancel.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
this handle was constructed without a
:class: |
KeyError
|
the job does not exist (never enqueued, or pruned past retention); the delegate's contract, a typo'd id and a pruned job are indistinguishable here. |
Source code in src/taskq/client/_handle.py
wait
async
¶
Block until the job reaches a terminal status, then return R.
Returns the actor's return value, validated through
:attr:result_adapter. The result type is R exactly ,
never R | None. Missing or failed results raise. Advances
:attr:row to each row the polling loop fetches, on return,
the terminal row the result was extracted from. A job whose row
the prune moved to jobs_archive waits on (and returns) the
archived terminal row (:attr:archived True); only an id
that exists in neither tier raises KeyError.
Raises:
| Type | Description |
|---|---|
ResultUnavailable
|
terminal state reached but no result was
stored (result TTL expired, actor returned |
JobFailed
|
the job ended in a non-success terminal state
( |
KeyError
|
the job exists in neither |
TimeoutError
|
|
Source code in src/taskq/client/_handle.py
progress_stream
async
¶
Stream live progress events for this job.
When Redis is configured, subscribes to the per-job Redis pub/sub
channel and yields :class:~taskq.progress.ProgressEvent objects in
real time. When Redis is not available, falls back to polling Postgres
at 500 ms intervals (jittered ±20%) and synthesising events from row
diffs; a pool or connection error on a poll is retried on the next
one.
Raises :class:NotImplementedError when the backend does not
support progress streaming (the in-memory backend sets its
supports_progress_stream capability to False, it has no
pub/sub), and :class:~taskq.exceptions.StreamUnavailable when
the row could not be re-read for 30 s straight — the Postgres
poll's own reads, and on the Redis path the safety-net re-reads
behind the subscription (an infra error out of a re-read is
retried inside a 30 s budget, one warning per failure; a
non-infra error propagates untouched).
The first event on the Redis path is the row as it stands when
the stream is opened — the same contract :meth:TaskQ.stream
documents: an already-terminal job yields its terminal event and
returns instead of waiting on a channel that will never speak
again, and a non-terminal row carrying content (progress has
moved, or the status moved past the row the handle was
constructed with) is replayed as the first event, so a fresh
consumer — a re-attach after a transport cut — does not starve
until the actor's next publish. A row with nothing to replay
(progress_seq == 0 and no status change) yields nothing
extra.
The stream terminates: every poll timeout re-reads the row as a
safety net (a terminal write whose pub/sub message was dropped —
Redis pub/sub is fire-and-forget — still ends the stream), and a
terminal=True event ends it as on the PG path. The one case
that never terminates on the Redis path is a job id that reads
back as missing: the stream cannot know the job's state, so it
falls through to the subscription (the PG fallback warns and ends
instead).
Does not advance :attr:row, the Redis path fetches no rows
through the subscription (the snapshot read and the on-timeout
safety net below read through _backend directly without
recording), and advancing only there would make the semantics
backend-dependent.
Source code in src/taskq/client/_handle.py
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 | |
ActorsClient ¶
Pool-wrapping facade for actor configuration operations.
Acquires a connection from the injected pool for each call, delegates
to taskq.actor_config_ops, and returns the result. The
caller must have opened the pool; this class does not manage its
lifecycle.
.. note::
This client is Postgres-only, it delegates to
:mod:taskq.actor_config_ops, which executes raw SQL against
the actor_config table. The :class:~taskq.backend._protocol.Backend
protocol does not include actor config operations, so
:class:~taskq.testing.InMemoryBackend does not support
ActorsClient.
Parameters¶
pool:
An open asyncpg.Pool. The caller retains ownership.
schema:
TaskQ schema name. Defaults to "taskq".
Source code in src/taskq/client/_actors.py
list
async
¶
List all stored actor_config rows, ordered by actor name.
get
async
¶
Get one actor_config row, or None if not found.
set_capacity
async
¶
set_capacity(
actor: str,
*,
max_concurrent: int | Unset | None = UNSET,
max_pending: int | Unset | None = UNSET,
result_ttl: float | Unset | None = UNSET,
) -> ActorConfigRow | None
Update capacity fields on an existing actor_config row.
Source code in src/taskq/client/_actors.py
deregister
async
¶
Deregister an actor with safety checks.
See :func:taskq.actor_config_ops.deregister_actor for
the full semantics.