Skip to content
Kavachकवच

Timeouts & Cancellation

Also known as: cancel abort kill stop thread interrupt cancellation token abortcontroller context cancel deadline time limit stuck task infinite loop hang watchdog

● Advanced5 minBefore this: TasksSpec 9.6

.timeout(d) puts a time limit on a wait. task.cancel() and spawn … within d stop a task, even one stuck in a loop that never waits.

use std.time

fn slow_answer() -> Int uses Clock {
    time.sleep_ms(1_000)
    42
}

fn main() {
    const t = spawn slow_answer()
    const answer = t.wait().timeout(100.ms) catch e => {
        print("gave up: {e}")
        0
    }
    print(answer)
}

The program ran and printed:

kavach run timeout.kav

gave up: timed out after 100 ms0
timeout.kav: Kavach code, then what running it prints
fn main() {
    const (send, receive) = Channel<String>(capacity: 1)
    const next = receive.take().timeout(200.ms) catch e => {
        print("no message: {e}")
        Taken.Closed
    }
    match next {
        Taken.Item(text) => print(text),
        Taken.Closed => print("nothing came"),
    }
    send.close()
}

The program ran and printed:

kavach run take-timeout.kav

no message: timed out after 200 msnothing came
take-timeout.kav: Kavach code, then what running it prints
  • A timed-out .wait() leaves the task running; it is still cancelled when its function returns.
  • A put that timed out puts nothing.

Cancelling a task

task.cancel() stops a task, even one in a loop that never waits:

fn main() {
    const counting = spawn {
        let n = 0
        while true {        // never waits
            n += 1
        }
    }
    counting.cancel()       // stops it all the same
    print("stopped")
    counting.wait() catch e => print(e)
}

The program ran and printed:

kavach run cancel.kav

stoppedthe task was cancelled
cancel.kav: Kavach code, then what running it prints
  • The task stops at the first of these: a wait, the start of a function call, or the start of a loop's round.
  • It ends there, as if every function it is in returned at once: what they hold is released, and their cleanup (Drop) runs. The tasks it spawned are cancelled too.
  • Cancelling is not an error: catch and match don't see it, so a task can't refuse to stop. Only .wait() on a cancelled task fails, with the task was cancelled.
  • Cancelling a task that has ended does nothing.

A cancel always wins, and nothing is left half-done all the same. Inside Shared.update, the task stops once the update's function returns, so the value is never half-changed. An agent's event that is cut short stays unfinished in its journal and runs again after a restart. A durable write is all there or not at all, as after a crash.

A function that returns cancels its tasks more gently: each stops at its next wait, so a task that only computes finishes its work first, and the function waits for it:

fn start() {
    const _sum = spawn {
        let total = 0
        for i in 0..1_000_000 {
            total += i % 7
        }
        print("sum: {total}")
    }
    print("start returns")
}

fn main() {
    start()
    print("back in main")
}

The program ran and printed:

kavach run finish-at-return.kav

start returnssum: 2999997back in main
finish-at-return.kav: Kavach code, then what running it prints

A time limit at spawn

A task stuck in a loop holds its turn, so no other task runs to cancel it. within d after the spawned call or block gives it d to finish. Then it stops itself, as if cancelled, and .wait() on it fails:

fn search(depth: Int) -> Int {
    let best = 0
    while true {            // a bug: the search never ends
        best = best % 1000 + depth
    }
    best
}

fn main() {
    const result = spawn search(3) within 50.ms
    const best = result.wait() catch e => {
        print(e)
        0
    }
    print("best: {best}")
}

The program ran and printed:

kavach run spawn-within.kav

timed out after 50 msbest: 0
spawn-within.kav: Kavach code, then what running it prints

The error is the same as from .timeout(d). The difference: .timeout(d) stops only the wait and leaves the task running, while within d stops the task.

Only a program that calls cancel() or gives a time limit somewhere gets these checks; others pay nothing. A counted loop whose body runs straight through, with no call and no inner loop, gets none: it ends anyway, and it stays a loop the compiler can vectorize or run on all cores.

Cleanup however a task ends

Cleanup that must run however a task ends, cancelled, failed or done, is a value whose type implements Drop. There is no defer or on_cancel block: one way covers every exit. Here a guest is cancelled while it waits, and its seat still comes back:

use std.time

/// A seat at a table of two. Giving it back is its `drop`, so it is
/// given back however the task that holds it ends.
type Seat: Drop {
    free: Shared<Int>

    fn drop(self) uses Console {
        const _ = self.free.update(n => n + 1)
        print("seat given back")
    }
}

fn sit(free: Shared<Int>) -> Seat {
    const _ = free.update(n => n - 1)
    Seat(free: free)
}

fn main() uses Console, Clock {
    const free = Shared(2)
    const (seated, ready) = Channel<Bool>(capacity: 1)
    const guest = spawn {
        const _seat = sit(free)
        seated.put(true)
        time.sleep(1.hours)
    }
    const _ = ready.take()
    print("free seats: {free.get()}")
    guest.cancel()
    guest.wait() catch _ => {}
    print("free seats: {free.get()}")
}

The program ran and printed:

kavach run cancel-cleanup.kav

free seats: 1seat given backfree seats: 2
cancel-cleanup.kav: Kavach code, then what running it prints

A panic is the exception: it runs no drop, and what the task held is never dropped. See Drop.