Skip to main content

Tasks

View Markdown

The handler

interface TaskHandler<I : Any, O : Any> {
val type: TaskType<I, O>
suspend fun handle(input: I, ctx: TaskContext): O
}

ctx.attempt is 1-based. ctx.runId is the run's id. ctx.heartbeat(details) keeps the attempt alive and is where cancellation is delivered.

Retries

Retrying belongs to the orchestrator. A handler that throws is simply retried per the run's RetryPolicy, with the interval doubling between attempts. Watch it in the UI: every attempt is listed, with the error and the interval before the next one.

override suspend fun handle(input: GreetingInput, ctx: TaskContext): Greeting {
if (ctx.attempt < input.failAttemptsBelow) error("simulated failure on attempt ${ctx.attempt}")
return Greeting("${input.greeting}, ${input.name}!")
}

Stopping the retries

throw NonRetryableTaskError("BVN format is invalid; retrying cannot help")

The run ends Failed on that attempt. RetryPolicy.nonRetryableErrors does the same by exception class name, for exceptions you do not own.

Long tasks

Anything that runs for more than a few seconds should heartbeat and set heartbeatTimeout in its options:

override suspend fun handle(input: SyncInput, ctx: TaskContext): SyncOutput {
for (page in pages(input)) {
process(page)
ctx.heartbeat("page=${page.number}")
}
return SyncOutput(done = true)
}

With heartbeatTimeout = 30.seconds, a worker that dies mid-sync is noticed within 30 seconds and the attempt is retried elsewhere, instead of waiting out the full attemptTimeout.

Standalone or inside a flow

The same handler serves both. From application code it is started with RunOptions, which include the id and the queue. From inside a flow it is started with TaskOptions, and the orchestrator assigns the id and the flow's queue.