Skip to content
Bluetape4k docs1.11

Deferred coordination

Latest stable Based on Bluetape4k release 1.11.0

“The fastest task” is ambiguous. The first task to finish may have failed; the first task to succeed must produce a value. Whether losers are cancelled is a third decision.

Comparison of awaitAny, awaitAnyAndCancelOthers, and firstSuccessTaskScope

RequirementAPIFirst failureLosers
observe the first terminal resultawaitAnypropagatecontinue
accept the first terminal result and reclaim capacityawaitAnyAndCancelOtherspropagatecancel
skip failed replicas and accept first successfirstSuccessTaskScopewait for otherscancel after winner
require both resultszip / zipWithpropagateparent policy

Empty input is invalid. A single element has the same result as awaiting that deferred directly.

Start every request in the same caller scope; the coordinator does not create a long-lived owner.

suspend fun <T> fastestReplica(
replicas: List<suspend () -> T>,
): T = coroutineScope {
replicas
.map { load -> async { load() } }
.awaitAnyAndCancelOthers()
}

The implementation wraps each await in an indexed signal, selects the first signal to finish, and attempts to cancel every other signal and original deferred before returning or rethrowing the winner result.

  • first success: return its value and cancel losers;
  • first failure: throw it and cancel losers;
  • first cancelled task: propagate cancellation and cancel losers;
  • caller cancellation: ensureActive() prevents it from being mistaken for an ordinary candidate result.

Skipping failed replicas requires the structured first-success policy.

suspend fun loadFromAnyProvider(): Quote = firstSuccessTaskScope {
fork { primary.loadQuote() }
fork { secondary.loadQuote() }
fork { archive.loadQuote() }
join().result { cause ->
QuoteUnavailable("all providers failed", cause)
}
}

Remaining tasks are cleaned up after a winner. If every task fails, the result mapper creates the domain exception.

DeferredValue starts immediately in its own DefaultCoroutineScope, so its owner must close it.

suspend fun loadAnswer(): Int {
val value = deferredValueOf { repository.loadAnswer() }
return try {
value.await()
} finally {
value.close()
}
}

Use await() in coroutine code. Blocking value access on a constrained dispatcher can starve or deadlock that pool.

Deferred.map, mapAll, concatMap, and zipWith represent the transformed result as another deferred. Source failure, source cancellation, and transform exceptions propagate unchanged.

val profile = async { loadProfile(id) }
val quota = async { loadQuota(id) }
val summary = profile.zipWith(quota) { p, q -> Summary(p, q) }
return summary.await()

The helper does not transfer ownership: the scope that started the source deferreds still owns their lifecycle.

SignalWhy
winner latency and identitydetect replicas that fail fastest
loser cancellation durationfind I/O that ignores cancellation
in-flight bound per raceprevent retry-times-race amplification
all-failed ratioensure first-success is not hiding an outage

For ordering and concurrency across stream items, continue with Ordered and parallel Flow.