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
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
- A timed-out
.wait()leaves the task running; it is still cancelled when its function returns. - A
putthat 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
- 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:
catchandmatchdon't see it, so a task can't refuse to stop. Only.wait()on a cancelled task fails, withthe 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
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
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
A panic is the exception: it runs no drop, and what the task held is never dropped. See Drop.