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.JavaScriptScopes
Each round builds a random tree of scopes. Every scope acquires a few resources, opens child scopes, and forks children, then may fail or die, and the whole tree may be interrupted part way.
Run it with dotnet run --project examples/Axial.TortureTest -- scopes 100.
What it does
- Each resource is acquired in one of the ways Axial offers:
Flow.scopeResourcewithResource.ofAsync,Resource.create,Resource.finalizer, orResource.asyncFinalizer;Flow.scopeAsyncFinalizer,scopeFinalizer,scopeAcquireRelease,scopeDisposable,scopeAsyncDisposable; orScope.AddFinalizer. Each logs when it is acquired and released. - A scope's children are nested with
Flow.scoped, forked and joined, forked and interrupted, or forked and left for the scope to interrupt when it closes. - Every scope counts itself in, and counts itself out with
Flow.ensuringandFlow.onInterrupt.
let run (round: Round) : Flow<Axial.ClockEnvironment, Never, Check list> =
flow {
let events = ResizeArray<Event>()
let nextId = ref 0
let fresh () = increment nextId
let record event = lock events (fun () -> events.Add event)
let started = ref 0
let finished = ref 0
let interrupted = ref 0
// Acquires one resource in the current scope, in one of the ways Axial offers, and logs its release.
let acquire (scope: int) : Flow<Axial.ClockEnvironment, string, unit> =
let id = fresh ()
let release () = record (Released(scope, id))
let acquired value = Flow.delay (fun () -> record (Acquired(scope, id)); Flow.ok value)
match round.Next 8 with
| 0 -> Flow.scopeResource (Resource.ofAsync (acquired id) (fun _ _ -> async { release () })) |> Flow.ignore
| 1 -> acquired () |> Flow.bind (fun () -> Flow.scopeAsyncFinalizer (fun _ -> async { release () }))
| 2 -> acquired () |> Flow.bind (fun () -> Flow.scopeResource (Resource.asyncFinalizer (fun _ -> async { release () })))
| 3 -> Flow.scope |> Flow.bind (fun current -> acquired () |> Flow.map (fun () -> current.AddFinalizer(finalizer release)))
#if FABLE_COMPILER
| _ -> Flow.scopeResource (Resource.ofAsync (acquired id) (fun _ _ -> async { release () })) |> Flow.ignore
#else
| 4 -> Flow.scopeAcquireRelease (acquired id) (fun _ _ -> release (); Tasks.Task.CompletedTask) |> Flow.ignore
| 5 -> acquired () |> Flow.bind (fun () -> Flow.scopeFinalizer (finalizer release))
| 6 -> acquired (new Disposable(release)) |> Flow.bind (fun resource -> Flow.scopeDisposable resource)
| _ ->
match round.Next 3 with
| 0 -> acquired (new Disposable(release)) |> Flow.bind (fun resource -> Flow.scopeAsyncDisposable resource)
| 1 -> Flow.scopeResource (Resource.create (acquired id) (fun _ _ -> release (); Tasks.Task.CompletedTask)) |> Flow.ignore
| _ -> acquired () |> Flow.bind (fun () -> Flow.scopeResource (Resource.finalizer (finalizer release)))
#endif
// A scope acquires a few resources, then may open child scopes, fork children (joined, interrupted, or left
// running for the scope to interrupt), fail, or die. Every level counts itself in and out.
let rec nest depth : Flow<Axial.ClockEnvironment, string, unit> =
flow {
let scope = fresh ()
for _ in 1 .. round.Next 4 do
do! acquire scope
if depth < 3 then
for _ in 1 .. round.Next 3 do
match round.Next 4 with
| 0 -> do! nest (depth + 1) |> Flow.orElse (Flow.ok ())
| 1 ->
let! child = nest (depth + 1) |> Flow.fork
let! _ = Fiber.await child
()
| 2 ->
let! child = nest (depth + 1) |> Flow.fork
do! Flow.sleep (TimeSpan.FromMilliseconds(float (round.Next 2)))
let! _ = Fiber.interrupt child
()
| _ ->
// Left running: closing this scope interrupts it.
let! _ = nest (depth + 1) |> Flow.fork
()
do! Flow.sleep (TimeSpan.FromMilliseconds(float (round.Next 2)))
if round.Chance 10 then return! Flow.fail "boom"
if round.Chance 5 then return! Flow.die (InvalidOperationException "crash")
}
|> Flow.scoped
|> Flow.onInterrupt (Flow.delay (fun () -> increment interrupted |> ignore; Flow.ok ()))
|> Flow.ensuring (Flow.delay (fun () -> increment finished |> ignore; Flow.ok ()))
|> fun body -> Flow.delay (fun () -> increment started |> ignore; body)
// The whole tree runs as a fiber that is sometimes interrupted part way.
let! root = nest 0 |> Flow.fork
if round.Chance 50 then do! Flow.sleep (TimeSpan.FromMilliseconds(float (round.Next 4)))
let! _ = if round.Chance 40 then Fiber.interrupt root else Fiber.await root
let log = lock events (fun () -> List.ofSeq events)
let acquiredIds = log |> List.choose (function Acquired(_, id) -> Some id | _ -> None)
let releasedIds = log |> List.choose (function Released(_, id) -> Some id | _ -> None)
// Within one scope, resources are released in the reverse of the order they were acquired.
let reverseOrderPerScope =
log
|> List.groupBy (function Acquired(scope, _) | Released(scope, _) -> scope)
|> List.forall (fun (_, scopeEvents) ->
let acquiredOrder = scopeEvents |> List.choose (function Acquired(_, id) -> Some id | _ -> None)
let releasedOrder = scopeEvents |> List.choose (function Released(_, id) -> Some id | _ -> None)
releasedOrder = List.rev acquiredOrder)
let releasedAfterAcquired =
let position event = log |> List.tryFindIndex ((=) event)
log |> List.forall (function
| Released(scope, id) -> position (Acquired(scope, id)) < position (Released(scope, id))
| Acquired _ -> true)
return
[ check "every acquired resource was released exactly once" (List.sort acquiredIds = List.sort releasedIds && List.distinct releasedIds = releasedIds)
check "each scope released its resources in reverse order" reverseOrderPerScope
check "every resource was released after it was acquired" releasedAfterAcquired
check "every scope that started ran its ensuring finalizer exactly once" (started.Value = finished.Value)
check "no more scopes were interrupted than started" (interrupted.Value <= started.Value) ]
}What each check proves
| Check | Guarantee |
|---|---|
| Every acquired resource was released exactly once | However a scope ends, it releases what was acquired in it, once. |
| Each scope released its resources in reverse order | Finalizers run in the reverse of their registration order. |
| Every resource was released after it was acquired | Release never runs early. |
| Every scope that started ran its ensuring finalizer exactly once | Flow.ensuring runs on success, failure, defect, and interruption. |
| No more scopes were interrupted than started | Flow.onInterrupt runs only for interrupted scopes. |

