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.JavaScriptLayers
Each round provisions an environment from layers. Some resources fail while they are acquired, and the flow that uses the environment may fail or be interrupted.
Run it with dotnet run --project examples/Axial.TortureTest -- layers 100.
What it does
- Each resource layer is built with
Layer.acquireReleaseand fails one time in twenty, throughLayer.fromAsync, or on .NETLayer.fromTaskorLayer.fromValueTask. - Two resources are provisioned in sequence with
Layer.zip, a pool of three withLayer.poolin parallel withLayer.zipPar, and the region comes from the input environment withLayer.envWith. - The flow reads the environment with
Flow.envandFlow.envWith, uses the pool, and may fail. It runs underLayer.provideand is interrupted about a third of the time.
let run (round: Round) : Flow<Axial.ClockEnvironment, Never, Check list> =
flow {
let! clock = Flow.envWith (fun (env: ClockEnvironment) -> env.Clock)
let gate = obj ()
let acquired = ResizeArray<string>()
let released = ResizeArray<string>()
// A resource layer that may fail while acquiring, and logs its release.
let resource (name: string) : Layer<string, string, string> =
Layer.acquireRelease
(Layer.envWith id
|> Layer.bind (fun region ->
if round.Chance 5 then
let unavailable = Exit.Failure(Cause.Fail $"{name} unavailable")
#if FABLE_COMPILER
Layer.fromAsync (fun _ _ -> async { return unavailable })
#else
match round.Next 3 with
| 0 -> Layer.fromAsync (fun _ _ -> async { return unavailable })
| 1 -> Layer.fromTask (fun _ _ -> Tasks.Task.FromResult unavailable)
| _ -> Layer.fromValueTask (fun _ _ -> Tasks.ValueTask<_>(unavailable))
#endif
else
lock gate (fun () -> acquired.Add name)
Layer.succeed $"{name}@{region}"))
(fun _ -> finalizer (fun () -> lock gate (fun () -> released.Add name)))
let pool =
Layer.pool 3 (fun index -> resource $"worker{index}" |> Layer.map (fun _ -> index))
// Two resources provisioned in sequence and one pool in parallel with them, plus a plain finalizer.
let services : Layer<string, string, Services> =
Layer.map3
(fun (primary, secondary) workers region -> { Primary = primary; Secondary = secondary; Workers = workers; Region = region; Clock = clock })
(Layer.zip (resource "primary") (resource "secondary"))
(Layer.zipPar pool (Layer.widenError (Layer.succeed () : Layer<string, Never, unit>)) |> Layer.map fst)
(Layer.merge (Layer.envWith id) (Layer.addFinalizer (finalizer ignore)) |> Layer.map fst)
|> Layer.mapError id
let applied : Layer<string, string, int> =
Layer.apply (Layer.succeed (fun (text: string) -> text.Length)) (Layer.map2 (+) (Layer.succeed "ab") (Layer.succeed "c"))
// The flow that uses the services may fail or be interrupted; every provisioned resource must still be released.
let work : Flow<Services, string, string * int> =
flow {
let! services = Flow.env
let! region = Flow.envWith _.Region
let first = services.Workers.Next()
let second = services.Workers.Next()
do! Flow.sleep (TimeSpan.FromMilliseconds(float (round.Next 3)))
if round.Chance 10 then return! Flow.fail "work failed"
return $"{services.Primary},{services.Secondary},{region}", first + second + services.Workers.Count + Seq.length services.Workers.Instances
}
let! runner = work |> Layer.provide services |> Flow.localEnv (fun (_: ClockEnvironment) -> "eu") |> Flow.fork
if round.Chance 30 then do! Flow.sleep (TimeSpan.FromMilliseconds(float (round.Next 3)))
let! outcome = if round.Chance 30 then Fiber.interrupt runner else Fiber.await runner
let! length = Flow.ok () |> Flow.map (fun () -> 0) |> Layer.provide (applied |> Layer.map id) |> Flow.localEnv (fun (_: ClockEnvironment) -> "eu") |> exitOf
let acquiredNames, releasedNames = lock gate (fun () -> List.ofSeq acquired, List.ofSeq released)
let outcomeValid =
match outcome with
| Exit.Success(names, total) -> names = "primary@eu,secondary@eu,eu" && total = 0 + 1 + 3 + 3
| Exit.Failure cause -> isInterrupted outcome || not (Cause.failures cause).IsEmpty
return
[ check "every provisioned resource was released exactly once" (List.sort acquiredNames = List.sort releasedNames)
check "the provisioned environment was built from its layers" outcomeValid
check "a layer provided to a flow ran it" (length = Exit.Success 0) ]
}What each check proves
| Check | Guarantee |
|---|---|
| Every provisioned resource was released exactly once | A layer's resources are released when the flow ends, and a failure part way through provisioning releases what was already acquired. |
| The provisioned environment was built from its layers | The flow sees the values its layers produced, or fails with a provisioning or work failure, or is interrupted. |
| A layer provided to a flow ran it | Layer.provide runs the flow with the layer's output. |

