#Task
Status: standard library specification draft.
This chapter defines the part of std.task that lib/std writes in
ordinary hd over the language tier:
- the
Layertrait, unit and binary tuple implementations, andapply!; - the
retrybuilder, itsRetryandSelectiveRetrypolicies, andBackoff; - the
timeoutandwith_timeoutbuilders, theirTimeLimitpolicy, andTimedOut; - the
Fallback,Guard, andHedgepolicies and their builders; - the
Breakerstate andCircuitOpenmarker; - the
map_all!combinator; - the
CleanupEndenum, whichcancel!()returns; - the
BlockOnenum, whichblock_onreturns.
The language tier keeps the Suspend protocol, block_on, and the
polling combinators all! and race!, which are compiler intrinsics
(Standard Combinators).
#Retry
retry constructs a reusable retry configuration; Retry.when selects a
predicate for one error type:
pub struct Retry:
backoff: Backoff
pub struct SelectiveRetry[E]:
backoff: Backoff
when: fn(E) -> bool
pub fn retry(backoff: Backoff) -> Retry- std-task.policy.retry.decl
std.taskdeclares the public configuration typeRetrywith a privatebackoff: Backofffield. - std-task.policy.selective-retry.decl
SelectiveRetry[E]is public and has private fieldsbackoff: Backoffandwhen: fn(E) -> bool. - std-task.policy.retry.when
Retry.when[E](self, predicate: fn(E) -> bool) -> SelectiveRetry[E]stores that predicate with the retry configuration. - std-task.policy.retry.layer
RetryimplementsLayer[fn!() -> Result[T, E] $ R]withWrapped = fn!() -> Result[T, E] $ R + Clock. - std-task.policy.selective-retry.layer
SelectiveRetry[E]implementsLayer[fn!() -> Result[T, E] $ R]withWrapped = fn!() -> Result[T, E] $ R + Clock. - std-task.policy.retry.builder
retry(backoff: Backoff) -> Retryconstructs a retry configuration. - std-task.policy.retry.loopAn applied retry policy calls work sequentially at most
backoff.attemptstimes, returning the first.Okwithout another call. - std-task.policy.retry.last-errorWhen every attempt returns
.Err, an applied retry policy returns the last error. - std-task.policy.retry.zero-onceAn
attemptsof zero counts as one, so an applied retry policy always calls work at least once. - std-task.policy.retry.predicateAfter
.Err(error), aSelectiveRetryapplication retries only when its predicate returnstrue. - std-task.policy.retry.all-errorsA
Retryapplication retries every error until its attempt limit. - std-task.policy.retry.when-plainA selective predicate is an ordinary function, takes the error, and runs at most once for each
.Err. - std-task.policy.retry.cancelCancelling an applied retry policy cancels its active attempt or sleep and starts no further attempt.
- std-task.policy.retry.clockExecuting either retry policy requires
Clock, including whenbackoff.initialis0ms.
use std.task.{Backoff, apply, retry}
use std.time.Clock
struct Server:
busy_for: i32
fn ask!(server: &mut Server) -> Result[string, string]:
if server.busy_for > 0:
server.busy_for = server.busy_for - 1
return .Err("busy")
.Ok("ready")
fn connect!(server: *mut Server) -> Result[string, string] $ Clock:
apply!(retry(Backoff { initial: 0ms })):
ask!(server)With busy_for at 2, connect! returns .Ok("ready") on the third call.
With busy_for at 3, it returns .Err("busy") after three calls.
With attempts at 0, an applied retry policy would make one call.
use std.task.{Backoff, apply, retry}
use std.time.Clock
fn is_busy(error: string) -> bool:
error == "busy"
fn connect!(replies: *mut List[Result[string, string]]) -> Result[string, string] $ Clock:
apply!(retry(Backoff { attempts: 5, initial: 0ms }).when(is_busy)):
replies.remove_at(0)With replies .Err("busy"), .Err("refused"), .Ok("ready"), connect! returns
.Err("refused") after two calls: is_busy rejects the second error, so a third
call never starts.
The row parameter R lets an attempt require providers. An attempt that
calls println has the row Console, and applying its retry policy then
requires Console and Clock.
Why. Configuration and execution are separate ordinary library code. One Backoff configures immediate and delayed attempts, both using Clock. Zero attempts still makes one attempt, because a result needs an attempt.
See also: Standard Combinators, Suspending Closures And Clauses.
#Retry With Backoff
Backoff configures the retry policies' attempt count and delays:
pub struct Backoff:
pub attempts: usize = 3
pub initial: Duration = 100ms
pub factor: u32 = 2
pub max: Duration = 10s- std-task.backoff.decl.defaults
std.taskdeclaresBackoffwith the four public fields, types and defaults above. - std-task.backoff.value-copyA
Backoffis a value whose copy is independent of the original. - std-task.backoff.eq
BackoffimplementsEqby comparing all four fields. - std-task.policy.retry.sleepAn applied retry policy calls the covering
Clock.sleep!between attempts, with no sleep after the final attempt. - std-task.retry.policy.delay.cappedThe first delay is
initialand later delays multiply the previous delay byfactor, each capped atmaxwithout overflow. - std-task.policy.retry.when-sleepA
SelectiveRetryapplication sleeps only before another attempt that its predicate allowed.
use std.task.{Backoff, apply, retry}
use std.time.{Clock, ms, s}
struct Server:
busy_for: i32
fn ask!(server: &mut Server) -> Result[string, string]:
if server.busy_for > 0:
server.busy_for = server.busy_for - 1
return .Err("busy")
.Ok("ready")
fn connect!(server: *mut Server) -> Result[string, string] $ Clock:
policy := Backoff { attempts: 5, initial: 100ms, factor: 2, max: 5s }
apply!(retry(policy)):
ask!(server)With busy_for at 3, connect! sleeps 100, 200, and 400 milliseconds,
and the fourth call returns .Ok("ready"). With busy_for at 5, it also
sleeps 800 milliseconds and returns .Err("busy") after five calls.
Why.
Backoffis the option set of Deno'sretryin@std/async: a count, a first delay, a multiplier, and a cap. The delays go through theClockrequirement, so a test provider makes them instant and observable.
#All List
all_list! is the private engine of map_all!.
Public callers use map_all! with the input length as the concurrency limit:
use std.task.map_all
fn square!(value: i32) -> i32:
value * value
fn squares!() -> List[i32]:
map_all!([3, 1, 2], 3, square)- std-task.map-all.private-engine
std.task.all_listis private and cannot be imported by a caller. Error:private-import.
squares! returns [9, 1, 4], in input order.
Why.
map_all!accepts items directly, so callers need not construct a list of closures. Its private engine drives one flat list of cold workers and retains results until every worker finishes. Waker readiness routes later polls to unfinished workers without scanning unwoken ones.
See also: Map All, Standard Combinators.
#Timeout
timeout constructs a reusable deadline configuration for Result-returning
work:
pub struct TimedOut:
pub limit: Duration
pub cleanup: CleanupEnd
pub struct TimeLimit:
limit: Duration
cleanup: Duration? = .None
pub fn timeout(limit: Duration, cleanup: Duration? = .None) -> TimeLimit- std-task.policy.time-limit.decl
std.taskdeclares the public configuration typeTimeLimitwith private fieldslimit: Durationandcleanup: Duration?. - std-task.policy.time-limit.layer
TimeLimitimplementsLayer[fn!() -> Result[T, E] $ R]withWrapped = fn!() -> Result[T, E] $ R + ClockwhenE < From[TimedOut]. - std-task.policy.timed-out.from-self
TimedOutimplementsFrom[TimedOut]by returning its argument unchanged. - std-task.timeout.fields-value
std.taskdeclares the struct valueTimedOutwith the public fieldslimit, aDuration, andcleanup, aCleanupEnd.TimedOutimplementsEq,Debug,Display, andError. - std-task.timeout.displayThe
Displaytext of aTimedOutistimed out afterfollowed by theDisplaytext oflimit. Whencleanupis.Abandoned, the text ends with, cleanup abandoned. - std-task.policy.timeout.builder
timeout(limit: Duration, cleanup: Duration? = .None) -> TimeLimitconstructs a deadline configuration. - std-task.policy.time-limit.raceA
TimeLimitexecution races the work against the coveringClock.sleep!(limit), with the work polled first. - std-task.policy.time-limit.completedWhen work completes first, a
TimeLimitexecution returns its Result unchanged. - std-task.policy.time-limit.expiredWhen the deadline wins, a
TimeLimitexecution returns.Err(E::from(TimedOut { limit: limit, cleanup: end })), withendset to the work's cleanup outcome. - std-task.timeout.tieA
workthat completes on its first poll wins even whenlimitis 0, sincerace!polls its children in order. - std-task.policy.time-limit.cancel-loserA
TimeLimitexecution cancels the losing child and waits for its cleanup before returning. - std-task.policy.time-limit.cancelCancelling a
TimeLimitexecution cancels its work and deadline children.
use std.task.{TimedOut, apply, timeout}
use std.time.{Clock, s, sleep}
fn lookup!() -> string $ Clock:
sleep!(5s)
"value"
fn lookup_in_time!() -> Result[string, TimedOut] $ Clock:
apply!(timeout(1s)):
let result: Result[string, TimedOut] = .Ok(lookup!())
resultOn a real clock, lookup_in_time! returns
.Err(TimedOut { limit: 1s, cleanup: .Finished }) after one second, and the
5 second sleep is cancelled. On a ManualClock, every sleep completes at once,
so lookup! finishes first and the result is .Ok("value").
Why. The policy uses the existing race and cancellation rules. Work is polled first, so immediately completing work wins a zero-limit tie. The timeout error converts through the work's declared error type.
See also: Standard Combinators, Clock Helpers, Manual Clock.
#Cleanup Bound
A cleanup argument bounds how long a TimeLimit execution waits for the cleanup of
work once work is cancelled:
use std.ops.Disposable
use std.task.{TimedOut, apply, timeout}
use std.time.{Clock, s}
struct Upload:
log: *mut List[string]
ack: mut Suspend[void]
impl Disposable for Upload:
fn dispose!(&mut self) -> void:
defer: self.log.push("upload closed")
let mut ack = self.ack
ack!() # waits for the peer
self.log.push("upload acknowledged")
fn send!(log: *mut List[string], reply: mut Suspend[string], ack: mut Suspend[void]) -> Result[string, TimedOut] $ Clock:
apply!(timeout(5s, cleanup=1s)):
using upload = &mut Upload { log: log, ack: ack }
let result: Result[string, TimedOut] = .Ok(reply!())
resultWhen neither reply nor ack completes, the limit cancels the work at 5
seconds, and its dispose waits for ack. At 6 seconds the policy abandons the work. The log reads upload closed, and send! returns
.Err(TimedOut { limit: 5s, cleanup: .Abandoned }), whose text is
timed out after 5s, cleanup abandoned. When ack completes in time, the log
also holds upload acknowledged, and cleanup is .Finished.
- std-task.policy.time-limit.cleanup-unboundedWithout
cleanup, aTimeLimitexecution waits for the whole cleanup of cancelled work, however long it takes. - std-task.policy.time-limit.cleanup-deadlineWith
cleanup, the work has a cleanup deadline measured from its first cancellation, whether caused by expiry or cancellation of the execution. - std-task.policy.time-limit.cleanup-fixedLater cancellation of the
TimeLimitexecution never moves its cleanup deadline. - std-task.policy.time-limit.cleanup-outlivesCancellation of a
TimeLimitexecution does not stop its cleanup bound. - std-task.policy.time-limit.cleanup-abandonWhen its cleanup deadline passes before cleanup ends, the
TimeLimitexecution callsabandon()on the work. - std-task.timeout.cleanup-cooperativeThe deadline is cooperative.
abandon()runs at the first point at or after the deadline where the cleanup waits, so synchronous code is never interrupted. - std-task.timeout.cleanup-endedA cleanup that ends before
abandon()runs is not abandoned. - std-task.timeout.cleanup-zeroA
cleanupof 0 abandons the cleanup at its first wait, solimitthen bounds all of the waiting. - std-task.timeout.cleanup-end
TimedOut.cleanupis.Abandonedwhen the deadline cut the cleanup short, or when another part of it was cut short, bystd-task.cleanup-end.cut-short. Otherwise it is.Finished.
The deadline is set once, at the first cancel:
| Time | Event |
|---|---|
| t0 | apply!(timeout(5s, cleanup=1s)) starts its work. |
| t5 | The limit passes. The work is cancelled, and the deadline is t6. |
| t5.2 | The caller cancels the execution. The cleanup goes on, and the deadline stays t6. |
| t6 | The cleanup has not ended, so the policy abandons the work. |
Had the caller cancelled the execution at t3 instead, the deadline would be t4.
Why. A graceful cancel never drops cleanup, so only an explicit
abandon()ends a cleanup early, and thecleanupargument makes that call. Racing a cancel against a sleep would not bound it, sincerace!cancels the losing cancel, which only continues inside the race's own cleanup. The deadline is fixed at the first cancel, so cancelling again and again cannot stretch the bound.
Note. The bound limits waiting, not computation. A dispose that computes for two seconds before its first wait overruns a one-second bound.
See also: Cleanup End, Abandoning.
#Map All
map_all! runs a suspending function over a list with a bound on how many
calls are in flight:
pub fn map_all -> U $ R) -> List[U] $ R- std-task.map-all.decl
std.taskdeclaresmap_all!with the signature above, as an ordinaryfn!function, not an intrinsic. Code imports it withuse std.task.map_all. - std-task.map-all.orderThe result holds
work(item)for each item at the item's index initems, whatever order the calls complete in. - std-task.map-all.start-order
workis called once for each item. Calls start in item order: no call starts before the call for every earlier item has started. - std-task.map-all.limitAt most
limitcalls ofworkare in flight at once. A call is in flight from its start until it completes. - std-task.map-all.windowWhen one call completes and an item has not started, the next item in order starts, so the number in flight stays at
limituntil the items run out. - std-task.map-all.zero-limitA
limitof zero counts as one, so calls run one after another. - std-task.map-all.emptyAn empty
itemsgives an empty list and callsworknever. - std-task.map-all.like-allScheduling, cancellation, failure, and panics follow the rules of
all!: a panic in one call propagates, and cancellingmap_all!cancels every call in flight and starts no more (req.combinator.cancel-children).
use std.task.map_all
use std.time.{Clock, ms, sleep}
fn fetch!(id: i32) -> string $ Clock:
sleep!(ms(100))
"item ${id}"
fn fetch_all!() -> List[string] $ Clock:
map_all!([1, 2, 3, 4, 5], 2, fn!(id: i32) -> string $ Clock: fetch!(id))fetch_all! returns ["item 1", ..., "item 5"] in that order. On a real clock
it runs two calls at a time, so it takes about 300 milliseconds; the third call
starts as soon as the first completes.
Why. Effect's
forEachwithconcurrencyand Go'serrgroup.SetLimitbound fan-out the same way. Settinglimittoitems.len()starts every item together. Smaller limits use a sliding window of worker closures driven by a private flat engine, each taking the next unstarted index.Bang calls are the only yield points, so the shared index needs no lock. A
limitof 0 counts as 1, as for retry attempts.
See also: All List, Cooperative Scheduling.
#Cleanup End
CleanupEnd says how the cleanup of a cancelled suspension ended, and
cancel!()
returns it:
pub enum CleanupEnd:
Finished
Abandoned- std-task.cleanup-end.decl
std.taskdeclares the enumCleanupEndwith the two variants above.CleanupEndimplementsEqandDebug. Code imports it withuse std.task.CleanupEnd. - std-task.cleanup-end.finished
.Finishedmeans the whole cleanup ran to its end, with nothing in it cut short. - std-task.cleanup-end.cut-short
.Abandonedmeans a part of the cleanup was cut short: byabandon(), or by a dispose whose awaited suspension was cancelled elsewhere, byreq.abandon.dependency.result. - std-task.cleanup-end.best-effort
.Finisheddoes not mean that every close succeeded: adispose!returnsvoid, byflow.using.best-effort. - std-task.cleanup-end.debugThe
Debugtext of each variant isCleanupEnd.followed by the variant's name, as inCleanupEnd.Finished.
use std.task.CleanupEnd
fn stop!(job: mut Suspend[string]) -> string:
end := job.cancel!()
if end == CleanupEnd.Finished: "closed" else: "cut short"stop! returns "closed" once the cleanup of job has run to its end.
Why. A
voiddispose cannot report a failed close, but a program can still log a close that was cut short. Code that must know that a close protocol succeeded calls an explicit close that returns aResult, asflow.using.best-effortadvises.
See also: Cancellation.
#Block On
BlockOn[T] says how a
block_on
drive ended, and block_on returns it:
pub enum BlockOn[T]:
Completed(T)
Cancelled
Abandoned- std-task.block-on.decl
std.taskdeclares the generic enumBlockOn[T]with the three variants above. Code imports it withuse std.task.BlockOn. - std-task.block-on.completed
.Completed(value)means the suspension completed withvalue, byreq.drive.block-on.completed. - std-task.block-on.cancelled
.Cancelledmeans the suspension was cancelled and its cleanup ran to its end, asCleanupEnd.Finishedmeans. - std-task.block-on.abandoned
.Abandonedmeans the suspension was cancelled and a part of its cleanup was cut short, asCleanupEnd.Abandonedmeans.
use std.task.block_on
fn fetch!(id: i32) -> string:
"user ${id}"
fn name_of(id: i32) -> string:
match block_on(fetch(id)):
.Completed(name) => name
.Cancelled => "cancelled" # its cleanup ran to its end
.Abandoned => "cut short" # its cleanup was cut shortname_of(7) returns "user 7": nothing else holds the suspension, so
nothing can cancel it.
Why. Synchronous code that drives a suspension cannot be cancelled itself, so the result names every way the drive can end. A caller that holds the only reference matches the other two variants with
passor a default, asprintlndoes.
See also: Driving A Stored Suspension, Cleanup End.
#Result Timeouts
with_timeout places a deadline outside an inner policy through a binary
tuple; applying the tuple keeps the work's error type:
pub fn with_timeout[I](limit: Duration, inner: I, cleanup: Duration? = .None) -> (I, TimeLimit)- std-task.policy.builders.requirement-free
timeout,with_timeout,retry, andRetry.whenare ordinary functions with empty requirement rows. - std-task.policy.builders.configurationConstructing these configurations neither starts work nor acquires a provider.
- std-task.policy.execution.apply
apply!is the public terminal for executing these policies. - std-task.policy.execution.privateThe execution atoms underlying these policies are private implementation details.
- std-task.policy.time-limit.work-cancelledExternal cancellation of work propagates cancellation through its
TimeLimitexecution rather than returning a timeout error. - std-task.policy.with-timeout.builder
with_timeout[I](limit: Duration, inner: I, cleanup: Duration? = .None) -> (I, TimeLimit)returns(inner, timeout(limit, cleanup=cleanup)). - std-task.policy.time-limit.boundThe
TimeLimitimplementation requiresE < std.convert.From[TimedOut]. Error:unsatisfied-trait-bound. - std-task.policy.time-limit.flatNested
TimeLimitapplications preserve the work's Result type. - std-task.policy.time-limit.clockExecuting a
TimeLimitrequiresClockin addition to the work's requirement row.
Why. A time policy preserves one statically typed error channel. Plain work is lifted into a typed Result before applying the policy. The identity conversion lets that error remain
TimedOut.
#Policy Layers
Layer describes work transformations and their resulting types.
The foundation uses ordinary unit and binary tuple values.
use std.task.apply
pub fn answer!() -> usize:
apply!(()):
42answer!() returns 42 without requiring Clock.
#Layer
- std-task.layer.decl
std.taskdeclares the public traitLayer[W]. - std-task.layer.wrapped
Layer[W]declares the associated typeWrapped. - std-task.layer.wrapIts ordinary method is
wrap(self, work: W) -> Self::Wrapped.
#Unit Layer
- std-task.layer.unit.impl
()implementsLayer[W]for every work typeW, withWrapped = W. - std-task.layer.unit.identityThe unit implementation's
wrapreturns itsworkargument unchanged.
#Tuple Layers
- std-task.layer.tuple.impl
(I, O)implementsLayer[W]whenIimplementsLayer[W]andOimplementsLayer[I::Wrapped]. - std-task.layer.tuple.wrappedThat tuple implementation binds
WrappedtoO::Wrapped. - std-task.layer.tuple.orderIts
wrapreturnsself._1.wrap(self._0.wrap(work)), applying the first component's wrapper before the second component's wrapper.
use std.task.Layer
struct Prefix:
text: string
impl Layer[string] for Prefix:
type Wrapped = string
fn wrap(self, work: string) -> string:
self.text + work
pub fn wrapped() -> string:
(Prefix { text: "first:" }, Prefix { text: "second:" }).wrap("work")wrapped() returns "second:first:work": the first component wraps before
the second component.
#Apply
- std-task.apply.decl
std.taskdeclares the public functionapply![T, U, $R, $S, P < Layer[fn!() -> T $ R, Wrapped = fn!() -> U $ S]](policy: P, work: fn!() -> T $ R) -> U $ S. - std-task.apply.wrap-onceWhen executed,
apply!callspolicy.wrap(work)exactly once. - std-task.apply.invoke-onceIt then bang-calls the returned function exactly once.
- std-task.apply.resultIt returns that function call's value unchanged.
- std-task.apply.rowIts declared requirement row is the wrapped function's row
S, without an addedClockrequirement.
Why. The associated output permits different input and output types and requirement rows. Unit preserves work, while a binary tuple composes two ordinary implementations without a separate stack type.
See also: Associated Types, Associated Type Bindings, Requirement Polymorphism, Trailing Blocks.
#Fallback
fallback places a backup operation outside an inner policy.
use std.task.{apply, fallback}
fn backup!() -> Result[string, string]:
.Ok("backup")
pub fn read!() -> Result[string, string]:
apply!(fallback(backup, ())):
let failed: Result[string, string] = .Err("offline")
failedread!() returns .Ok("backup") without requiring Clock.
The unit inner policy makes fallback the only wrapper.
- std-task.policy.fallback.decl
std.taskdeclares publicFallback[F]with a private fieldother: F. - std-task.policy.fallback.builder
fallback[F, I](other: F, inner: I) -> (I, Fallback[F])returns(inner, Fallback { other: other }). - std-task.policy.fallback.layer
Fallback[fn!() -> Result[T, E] $ S]implementsLayer[fn!() -> Result[T, E] $ R], withWrapped = fn!() -> Result[T, E] $ R + S. - std-task.policy.fallback.completedA fallback application invokes work once, returning any
.Okunchanged without invoking the backup. - std-task.policy.fallback.backupAfter work returns
.Err, a fallback application invokes its backup once and returns that Result unchanged. - std-task.policy.fallback.cancelCancelling a fallback application cancels its active child; cancellation of work does not invoke the backup.
- std-task.policy.fallback.requirement-free
fallbackis an ordinary function with an empty requirement row; it neither invokes work nor acquires providers.
Why. The concrete backup function retains its result, error and requirement types. A backup with another error type needs an explicit conversion into the shared error type.
See also: Policy Layers, Unit Layer.
#Circuit Breaker
Guard refuses work after a caller-owned Breaker reaches its failure limit.
use std.convert.From
use std.task.{Breaker, CircuitOpen, apply, guarded}
enum Failure:
Offline
Open
impl From[CircuitOpen] for Failure:
fn from(error: CircuitOpen) -> Failure: .Open
pub fn guard_counts!() -> (usize, usize):
state := &mut Breaker { failures: 0, limit: 2, calls: 0 }
guard := guarded(state)
for _i in 0..4:
_ := apply!(guard):
let failed: Result[string, Failure] = .Err(.Offline)
failed
(state.calls, state.failures)guard_counts!() returns (2, 2): two failures open the breaker,
and two later applications refuse work.
- std-task.policy.circuit-open.decl
std.taskdeclares the public empty structCircuitOpen. - std-task.policy.breaker.decl
std.taskdeclares publicBreakerwith public fieldsfailures: usize,limit: usize, andcalls: usize. - std-task.policy.guard.decl
std.taskdeclares publicGuardwith a private fieldstate: *mut Breaker. - std-task.policy.guard.builder
guarded(state: *mut Breaker) -> Guardreturns a configuration holding that counted reference. - std-task.policy.guard.layer
GuardimplementsLayer[fn!() -> Result[T, E] $ R], withWrapped = fn!() -> Result[T, E] $ R, whenE < From[CircuitOpen]. - std-task.policy.guard.boundThe guard's error type must implement
std.convert.From[CircuitOpen]. Error:unsatisfied-trait-bound. - std-task.policy.guard.refuseWhen
failures >= limit, a guard application returns.Err(E::from(CircuitOpen {}))without invoking work or changing its counters. - std-task.policy.guard.callsOtherwise, a guard application increments
callsbefore invoking work once. - std-task.policy.guard.successWhen work returns
.Ok, its guard resetsfailuresto zero. - std-task.policy.guard.failureWhen work returns
.Err, its guard incrementsfailuresonce. - std-task.policy.guard.resultA guard application returns the work's completed Result unchanged.
- std-task.policy.guard.sharedCopies of a
Guardretain the same countedBreakerreference. - std-task.policy.guard.cancelCancellation before work returns does not perform the guard's completed-result update of
failures. - std-task.policy.guard.requirement-free
guardedis an ordinary function with an empty requirement row; it neither invokes work nor acquires providers.
Why. The caller owns and can inspect or change all breaker state. A counted reference keeps that state across applications. The minimal breaker has no timer, automatic reopening or half-open probe.
See also: Policy Layers, Reference Types.
#Hedging
Hedge races work against a delayed second invocation of the same work.
use std.task.{apply, hedged}
use std.time.Clock
pub fn quick!() -> Result[string, string] $ Clock:
apply!(hedged(10ms)):
let ready: Result[string, string] = .Ok("ready")
readyquick!() returns .Ok("ready") on the first poll, before a second
invocation starts. Its declared row still requires Clock.
- std-task.policy.hedge.decl
std.taskdeclares publicHedgewith a private fielddelay: Duration. - std-task.policy.hedge.builder
hedged(delay: Duration) -> Hedgeconstructs a configuration holding that delay. - std-task.policy.hedge.layer
HedgeimplementsLayer[fn!() -> Result[T, E] $ R], withWrapped = fn!() -> Result[T, E] $ R + Clock. - std-task.policy.hedge.raceA hedge application races immediate work against a child that calls the covering
Clock.sleep!(delay)before invoking fresh work. - std-task.policy.hedge.first-resultThe first completed Result wins, whether
.Okor.Err. - std-task.policy.hedge.unneeded-copyIf immediate work completes before the delayed child invokes work, the second invocation never starts.
- std-task.policy.hedge.tieThe immediate child is polled first, so immediately completing work wins even when
delayis zero. - std-task.policy.hedge.cleanupA hedge application cancels its losing child and waits for its cleanup under the existing
race!rules. - std-task.policy.hedge.cancelCancelling a hedge application cancels both children.
- std-task.policy.hedge.requirement-free
hedgedis an ordinary function with an empty requirement row; it neither invokes work nor acquires providers.
Why. Each invocation creates fresh work. The existing structured race joins the loser cleanup; no detached operation or new cleanup bound is needed.
See also: Standard Combinators, Clock.