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.
Decision table
Section titled “Decision table”| Requirement | API | First failure | Losers |
|---|---|---|---|
| observe the first terminal result | awaitAny | propagate | continue |
| accept the first terminal result and reclaim capacity | awaitAnyAndCancelOthers | propagate | cancel |
| skip failed replicas and accept first success | firstSuccessTaskScope | wait for others | cancel after winner |
| require both results | zip / zipWith | propagate | parent policy |
Empty input is invalid. A single element has the same result as awaiting that deferred directly.
First completion with cleanup
Section titled “First completion with cleanup”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.
First successful result
Section titled “First successful 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.
Owning DeferredValue
Section titled “Owning DeferredValue”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.
Transform and combine
Section titled “Transform and combine”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.
Operational checks
Section titled “Operational checks”| Signal | Why |
|---|---|
| winner latency and identity | detect replicas that fail fastest |
| loser cancellation duration | find I/O that ignores cancellation |
| in-flight bound per race | prevent retry-times-race amplification |
| all-failed ratio | ensure first-success is not hiding an outage |
Source and representative tests
Section titled “Source and representative tests”For ordering and concurrency across stream items, continue with Ordered and parallel Flow.