Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript

Deferreds

Ten completers race to complete one Deferred, each in a different way, while forty awaiters wait and half of them are interrupted.

Run it with dotnet run --project examples/Axial.TortureTest -- deferreds 100.

What it does

  • Each completer picks Deferred.succeed, fail, die, complete, or interrupt at random. Each reports whether it won.
  • The early awaiters are forked before any completion, and a random half are interrupted while the completers run.
  • A late awaiter reads the outcome after everything has settled.
type private Attempt =
    | Succeed of int
    | Fail of int
    | Die of int
    | Complete of int
    | Interrupt

let deferreds (round: Round) : Flow<Axial.ClockEnvironment, Never, Check list> =
    let awaiters = round.Size 40

    flow {
        // Deferred.make's flow fails with the deferred's own error type; creating one never actually fails.
        let! deferred = Deferred.make<Axial.ClockEnvironment, int, int> () |> Flow.fold Flow.ok (fun _ -> Flow.die (InvalidOperationException "unreachable"))
        let! early = [ 1..awaiters ] |> Flow.traverse (fun _ -> deferred |> Deferred.await |> Flow.fork)

        // Several completers race, each with a different kind of completion; exactly one may win.
        let attempts =
            [ for id in 1..10 ->
                  match round.Next 5 with
                  | 0 -> Succeed id
                  | 1 -> Fail id
                  | 2 -> Die id
                  | 3 -> Complete id
                  | _ -> Interrupt ]

        let attempt kind =
            if round.Chance 50 then
                let won =
                    match kind with
                    | Succeed id -> deferred |> Deferred.succeedNow id
                    | Fail id -> deferred |> Deferred.failNow id
                    | Die id -> deferred |> Deferred.dieNow (InvalidOperationException(string id))
                    | Complete id -> deferred |> Deferred.completeNow (Exit.Success(id * 100))
                    | Interrupt -> deferred |> Deferred.interruptNow

                Flow.ok won
            else
                match kind with
                | Succeed id -> deferred |> Deferred.succeed id
                | Fail id -> deferred |> Deferred.fail id
                | Die id -> deferred |> Deferred.die (InvalidOperationException(string id))
                | Complete id -> deferred |> Deferred.complete (Exit.Success(id * 100))
                | Interrupt -> deferred |> Deferred.interrupt

        // Interrupt a random half of the early awaiters while the completers race.
        let interrupter =
            early |> Flow.traverse (fun fiber -> if round.Chance 50 then Fiber.interrupt fiber else Fiber.await fiber)

        let! won, earlyExits = Flow.zipPar (attempts |> Flow.traversePar (Parallelism.bounded 10) attempt) interrupter
        let! late = deferred |> Deferred.await |> exitOf

        let winners = List.zip attempts won |> List.filter snd |> List.map fst

        let expected: Exit<int, int> option =
            match winners with
            | [ Succeed id ] -> Some(Exit.Success id)
            | [ Fail id ] -> Some(Exit.Failure(Cause.Fail id))
            | [ Complete id ] -> Some(Exit.Success(id * 100))
            | [ Interrupt ] -> Some(Exit.Failure Cause.Interrupt)
            | _ -> None

        let matches (exit: Exit<int, int>) =
            match winners, exit with
            | [ Die id ], Exit.Failure(Cause.Die error) -> error.Message = string id
            | _ -> Some exit = expected

        let observed = earlyExits |> List.filter (isInterrupted >> not)

        return
            [ check "exactly one completion won" (winners.Length = 1)
              check "every awaiter that was not interrupted saw the winning outcome" (observed |> List.forall matches)
              check "an awaiter arriving afterwards sees the same outcome" (matches late) ]
    }

What each check proves

Check Guarantee
Exactly one completion won A deferred completes once, and every other completion reports that it lost.
Every awaiter that was not interrupted saw the winning outcome A success, a typed failure, a defect, or an interruption reaches every waiting awaiter unchanged.
An awaiter arriving afterwards sees the same outcome The outcome is kept for every later awaiter.