Results and the store¶
Pass a store to the app to record task states and return values. delay then
returns a handle that reads them:
"""Reading task states and results from the app's store."""
import asyncio
from asynk import App, MemoryStore, TaskError, Worker
from asynk.transport.memory import memory_transport
producer, consumer = memory_transport()
app = App("results", producer=producer, consumer=consumer, store=MemoryStore())
@app.task()
async def add(a: int, b: int) -> int:
return a + b
@app.task()
async def divide(a: int, b: int) -> float:
return a / b
async def main() -> None:
# Published before any worker runs, so the task is still pending.
handle = await add.delay(1, b=2)
print("before the worker:", (await handle.state()).value)
worker = Worker(app)
run = asyncio.create_task(worker.run())
print("result:", await handle.result(timeout=5))
print("after the worker:", (await handle.state()).value)
# Another request or process sharing the store only needs the id.
same = app.get_task(handle.task_id)
print("from the id:", await same.result(timeout=5))
failing = await divide.delay(1, 0)
try:
await failing.result(timeout=5)
except TaskError as exc:
print("failed:", exc.type_name, "-", exc.message)
print("state:", (await failing.state()).value)
worker.request_shutdown()
await run
if __name__ == "__main__":
asyncio.run(main())
Handles¶
delay returns a TaskHandle[R], where R is the type the task returns, with:
task_id, the envelope id;receipt, whatever the producer'spublishreturned;state(), the currentTaskState;result(timeout=None), which waits for the task to finish and returns its value, orNoneif the task was skipped.
app.get_task(task_id) builds a handle from an id alone, for example in
another request or process that shares the store. result(timeout=...) raises
TimeoutError when the task is not done in time, and TaskNotFoundError for an
id the store has no record of. A failed task makes result() raise
TaskError, which carries the failure's type name and message as .type_name
and .message.
Typed results¶
A task declared async def add(a: int, b: int) -> int makes add.delay(...)
return a TaskHandle[int], so a type checker sees await handle.result() as
int | None (None when a middleware skipped the task). app.get_task(id)
and app.publish(...) know no task, so they return a TaskHandle[Any], which
you can assign to a TaskHandle[int] or use untyped.
The type is a declaration. Asynk does not check or convert the value at
runtime: result() returns whatever the codec decodes. Typed results are
meant for values the codec carries as they are: None, booleans, numbers,
strings, and lists and dicts of those. Anything else is not rebuilt as the
declared type. With the default JsonCodec, a task declared to return a
dataclass or a datetime produces a dict or a string, and a tuple comes back
as a list, even though the checker believes result() is the declared type.
Convert such a value yourself, for example Point(**result), or return plain
values from the task. A task whose result the codec cannot encode fails with
ResultNotSerializable (see below).
A task declared -> None has a TaskHandle[None], whose result() is always
None. mypy reports using that value, as in assert await handle.result() is
None, as "does not return a value"; annotate the handle as TaskHandle[Any]
or just await it.
States¶
| State | Meaning |
|---|---|
PENDING |
published, not started yet |
RUNNING |
a worker is running it |
RETRYING |
an attempt failed and will be retried |
SUCCESS |
finished; result() returns the value |
FAILURE |
failed for good; result() raises TaskError |
SKIPPED |
a middleware vetoed the task, so it did not run; result() returns None |
UNKNOWN |
the store has no record for this id |
A failed attempt is final, FAILURE, when the task has no retry policy, when
the error is not in the policy's retry_on, or when its retries are used up.
Otherwise the state is RETRYING until the retry runs, and result() keeps
waiting for a later attempt (see Retries). A message for a task
name the worker does not know is recorded as FAILURE. A return value the codec
cannot encode is recorded as a FAILURE with the type name
ResultNotSerializable.
Store errors never affect a task or its delivery: the worker logs them and carries on, so a record can be missing after a store outage.
Store lifecycle¶
A worker and a scheduler start the app's store before their before_startup
hooks and stop it after their after_shutdown hooks, also when startup fails,
so they need nothing extra. A
producer-only script, which runs neither, starts and stops the store itself
(a fragment: it needs a worker running elsewhere):
await app.store.start()
handle = await add.delay(1, b=2)
print(await handle.result(timeout=10))
await app.store.stop()
Stores¶
MemoryStore(ttl=86400.0) keeps records in one process for ttl seconds, so
the producer, the worker and readers must share that process. It is the
development and testing store. For anything wider, use the
Redis store, or implement the Store protocol for another backend;
see Writing a custom store.
Without a store the app uses a NullStore: it records nothing, and state()
and result() raise NotConfiguredError.
Codec¶
Task results and states are encoded with app.codec, the same codec that
encodes messages (JsonCodec() by default). Pass App(..., codec=MyCodec())
to use another format, for example pickled values that keep tuples. With
JsonCodec, a tuple comes back as a list.