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

STM

Hundreds of STM transfers move money between six accounts while an auditor reads every balance in one transaction, and a fifth of the transfers are interrupted.

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

What it does

  • A transfer checks that the source can cover the amount and uses STM.retry if it cannot. STM.orElse turns that into a skipped transfer instead of a wait.
  • Ten patient withdrawals wait with STM.retry until a reserve account can cover them. The reserve is funded only after the transfers finish.
  • An auditor sums every balance in one transaction, many times, while the transfers run.
let run (round: Round) : Flow<Axial.ClockEnvironment, Never, Check list> =
    let accounts = 6
    let opening = 100
    let transfers = round.Size 400
    let patient = 10

    flow {
        let! balances = [ for _ in 1..accounts -> TRef.make opening ] |> List.map STM.atomically |> Flow.sequence
        let! reserve = STM.atomically (TRef.make 0)
        let total () = balances |> List.fold (fun sum account -> stm { let! so = sum in let! b = TRef.get account in return so + b }) (stm { return 0 })

        // A transfer moves money only if the source can cover it; otherwise orElse records that it was skipped.
        let transfer source target amount : STM<bool> =
            STM.orElse
                (stm {
                    let! available = TRef.get balances[source]
                    if available < amount then return! STM.retry
                    do! balances[source] |> TRef.update (fun value -> value - amount)
                    do! balances[target] |> TRef.update ((+) amount)
                    return true
                 })
                (stm { return false })

        // A patient withdrawal waits, with STM.retry, until the reserve can cover it.
        let withdraw amount : STM<unit> =
            stm {
                let! available = TRef.get reserve
                if available < amount then return! STM.retry
                do! reserve |> TRef.set (available - amount)
            }

        let! waiting = [ for _ in 1..patient -> STM.atomically (withdraw 5) |> Flow.fork ] |> Flow.sequence

        // Transfer fibers, a random fifth of them interrupted, run while an auditor reads every balance at once.
        let! movers =
            [ for _ in 1..transfers ->
                  let source, target = round.Next accounts, round.Next accounts
                  STM.atomically (transfer source target (round.Next 30 + 1)) |> Flow.fork ]
            |> Flow.sequence

        let! audits =
            [ for _ in 1 .. round.Size 100 -> STM.atomically (total ()) ] |> Flow.sequence

        let! moved = movers |> Flow.traverse (fun fiber -> if round.Chance 20 then Fiber.interrupt fiber else Fiber.await fiber)

        // Fund the reserve in small deposits; every patient withdrawal must then complete.
        do! [ for _ in 1..patient -> STM.atomically (reserve |> TRef.update ((+) 5)) ] |> Flow.sequencePar |> Flow.ignore
        let! withdrawn = waiting |> Flow.traverse Fiber.await |> Flow.timeoutToOk (TimeSpan.FromSeconds 10.0) []

        let! final = STM.atomically (total ())
        let! finalBalances = balances |> List.map (TRef.get >> STM.atomically) |> Flow.sequence
        let! left = STM.atomically (TRef.get reserve)
        let completed = moved |> List.filter (function Exit.Success true -> true | _ -> false) |> List.length

        return
            [ check "every audit saw the whole amount, never a transfer half done" (audits |> List.forall ((=) (accounts * opening)))
              check "the total is unchanged after every transfer" (final = accounts * opening)
              check "no account ever went negative" (finalBalances |> List.forall (fun balance -> balance >= 0))
              check "every patient withdrawal completed once the reserve was funded" (withdrawn.Length = patient && withdrawn |> List.forall (function Exit.Success () -> true | _ -> false) && left = 0)
              check "some transfers went through" (completed > 0) ]
    }

What each check proves

Check Guarantee
Every audit saw the whole amount, never a transfer half done A transaction sees a consistent snapshot of every TRef it reads.
The total is unchanged after every transfer An interrupted transaction commits all of its writes or none of them.
No account ever went negative A transaction's check and its write commit together.
Every patient withdrawal completed once the reserve was funded STM.retry waits until a TRef it read changes, then runs the transaction again.
Some transfers went through The contention did not starve every transaction.