Skip to content

Receive results without keeping a client connected

Ledgence 0.5.0 supports both bounded client waiting and durable completion subscriptions. Use waiting for an interactive caller; use a subscription when the application should receive a notification after disconnecting.

Install the Python client and use the tenant and namespace configured for your instance. The following function accepts an existing task ID; it never submits another task:

from ledgence.client import AsyncClient, WaitTimeout
async def observe(task_id: str):
async with AsyncClient(
"http://127.0.0.1:8080", tenant="acme", namespace="demo",
) as client:
task = client.tasks.handle(task_id)
try:
print(await task.result(timeout=60))
except WaitTimeout:
print("No terminal result observed within the budget; task ID:", task.id)

result(timeout=60) polls for at most 60 seconds and returns successful JSON output. A wait timeout stops local observation: it does not cancel, retry or resubmit execution. Recreate the handle in a later client session with the saved ID. Successful JSON null is a valid result, so do not use a return value’s truthiness to infer execution status.

Method Behavior
await task.status() Read compact current scheduling state.
await task.outcome() Read immediately; .outcome is None means the task is still pending.
await task.wait(timeout=60) Observe a terminal result, including failures/cancellation as values.
await task.result(timeout=60) Return successful output, or raise TaskFailed / TaskCancelled for terminal failure/cancellation.
await task.cancel() Explicitly request cancellation. Closing a client does not do this.

Workflow handles have the same observation methods. Use client.workflows.handle(workflow_id) for the complete workflow’s output; the internal controller task’s output is a workflow decision. Workflow failures, cancellation and wait expiry have corresponding workflow exception types. Timeouts are positive finite seconds; they are observation budgets, separate from execution retry and attempt timeout policies.

The operator runs an HTTP POST receiver and supplies an immutable destination alias to the orchestrator. For example, save this as completion.json:

{
"destinations": [{
"scope": {"tenant_id": "acme", "namespace": "demo"},
"destination": "application-results",
"url": "http://127.0.0.1:9000/completions"
}]
}

Add --completion-config /absolute/path/to/completion.json to your existing ledgence orchestrator serve command. Retain its database, program store and instance configuration. The URL must be reachable from the orchestrator; inside a container, loopback refers to that container. Ledgence does not start the receiver for you. The source Compose example includes a local receiver; the base image distribution does not enable one by default.

The subscription selects the alias, not an arbitrary URL. Keep at least one orchestrator configured for that destination to dispatch its durable backlog.

With an open AsyncClient and a task handle:

subscription = await task.subscribe(
destination="application-results",
idempotency_key="application-result-v1",
)
print(subscription.id) # Persist for later status inspection.

A workflow supports await workflow.subscribe(...) as well. Once the server accepts the subscription, the client can disconnect. There is no listener coroutine or open subscription socket to keep alive. Subscription also works after completion while the execution is retained.

Submission and subscription are separate durable commands. Persist their identities and reconcile uncertain replies using the original prepared commands and idempotency keys. task.prepare_subscribe(...) produces a command that can be passed to task.subscribe(command). A lost reply raises CompletionSubscriptionUncertain; it does not prove registration failed.

The receiver gets a CloudEvent referring to the terminal task or workflow, including a relative ldgresultref. Resolve that result reference against your configured Ledgence API, or inspect through the corresponding SDK handle. The notification does not contain the full application output. Durably store the event before returning 2xx, deduplicate by (source, id), then retrieve the result when needed. There is no cross-execution delivery ordering guarantee.

subscription = client.completions.handle(saved_subscription_id)
delivery = await subscription.status()
print(delivery.state, delivery.generation)

Failed or uncertain deliveries retry with backoff, with eight leased attempts per generation. Exhaustion affects notification delivery, not execution. After repairing the receiver, use prepare_retry(expected_generation=...) and retry(command) to rearm an exhausted subscription within the supported generation limit. Redelivery preserves the completion event’s identity.

For waits inside a workflow, use durable joins, timers or external-event waits, which release the worker slot. An application’s HTTP callback subscription is a separate integration.

Source: Completion contract · Result contract