tasks and structured concurrency
βtasks and structured concurrency
this feature is planned for β. the spec describes what it will be, not what's implemented now.
ferrule uses structured concurrency. tasks form trees where:
- child tasks can't outlive their parent scope
- cancellation propagates down the tree
- failures are explicitly aggregated
this eliminates "fire and forget" patterns that lead to resource leaks and hard-to-debug races.
task scopes
create a task scope with scope parallel:
function get_many(urls: View<Url>, cap net: Net, cap clock: Clock) -> Vec<Response>
effects [net, time, alloc, suspend, fail<ClientError>]
{
scope timeout(Duration.seconds(30), clock) {
scope parallel {
urls.map(function(url: Url) -> Response {
return check fetch(url, net)
})
}
} on timeout {
return err Timeout { message: "fetch timed out" }
}
}the scope:
- tracks all spawned tasks
- ensures they complete before the scope exits
- propagates cancellation on failure
spawning
tasks are spawned inside scope parallel blocks:
scope parallel {
const a = check fetch(url_a, net)
const b = check fetch(url_b, net)
return { a, b }
}spawned tasks:
- inherit the scope's cancellation token
- must complete before the scope exits
you can't spawn outside a scope. there's no global task pool.
awaiting
scope blocks implicitly await all spawned tasks before exiting. explicit await is not needed. the scope boundary is the synchronization point.
scope parallel {
const results = urls.map(function(url: Url) -> Response {
return check fetch(url, net)
})
// all tasks complete before scope exits
return results
}cancellation
cancellation propagates through scope blocks:
scope timeout(Duration.seconds(10), clock) {
// all tasks in this scope are cancelled when the timeout fires
scope parallel {
const a = check fetch(url_a, net)
const b = check fetch(url_b, net)
return { a, b }
}
} on timeout {
return err TimedOut {}
}when a scope is cancelled:
- all child tasks receive the cancellation signal
- ongoing operations check the token and return early
- cleanup runs via
defer
handling cancellation in your code:
function fetch(url: Url, cap net: Net) -> Response
effects [net, suspend, fail<ClientError>]
{
const sock = check net.connect(url.host, url.port)
return check request(sock, url)
}failure policies
scope blocks support different failure policies:
// fail-fast (default): cancel siblings on first failure
scope parallel {
check fetch(url_a, net)
check fetch(url_b, net)
}
// collect-all: gather all failures
scope parallel(policy: collect) {
check fetch(url_a, net)
check fetch(url_b, net)
} on fail(errors) {
log.error("failures: {}", errors)
}fail-fast is the default. it's usually what you want: if one request fails, cancel the others and return the error.
regions and tasks
regions created inside a scope block are disposed when the scope exits:
scope parallel {
const arena = region.arena(1024)
defer arena.dispose() // runs even on cancellation
// use arena...
return result
}aborted tasks must release their regions. finalizers run, status events are logged.
context flow
context flows through scope boundaries:
with context { request_id: rid } in {
scope parallel {
check child_operation() // inherits request_id
}
}errors from child tasks include the inherited context frames. this helps with debugging distributed operations.
async model
ferrule's async is effect-based. the suspend effect marks functions that may pause:
function fetch(url: String, cap net: Net) -> Response
effects [net, suspend, fail<NetError>]
{
const socket = check net.connect(url.host, url.port)
return check socket.readAll() // may suspend here
}there's no function coloring. you can call suspend functions from any context that allows the suspend effect. the runtime handles the actual suspension and resumption.
different runtimes can plug in:
- tokio-style for server workloads
- single-threaded for embedded
- deterministic for testing
see the async rfc for details.
example: parallel fetch with timeout
function fetch_all(
urls: View<Url>,
cap net: Net,
cap clock: Clock
) -> Vec<Response> effects [net, time, alloc, suspend, fail<FetchError>] {
scope timeout(Duration.seconds(30), clock) {
scope parallel {
urls.map(function(url: Url) -> Response {
return check fetch(url, net)
})
}
} on timeout {
return err Timeout { message: "fetch timed out" }
}
}