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.JavaScriptRunning as a service
Axial.Hosting integrates a root Flow with the Generic Host. Register it with Hosting.addApp; supply an environment containing IHasClock for the watchdog.
Register with the Generic Host
Wire the root Flow to the host lifetime once. Hosting.addApp runs the workflow when the host starts and requests
App.Stop() on shutdown; AddSystemd() (or AddWindowsService() on Windows) selects the host lifetime. The
.NET hosting guide shows the full end-to-end composition, including building the application
environment from the container with a layer:
open Microsoft.Extensions.Hosting
open Axial
open Axial.Hosting
let builder = Host.CreateApplicationBuilder args
builder.Services.AddSystemd() |> ignore // or AddWindowsService() on Windows
builder.Services
|> Hosting.addApp id AppError.describe application
|> ignore
use host = builder.Build()
host.Run()systemd
Call AddSystemd() on the HostApplicationBuilder (UseSystemd() on an older IHostBuilder). The extension comes from the Microsoft.Extensions.Hosting.Systemd package and installs a SystemdLifetime that sends the READY=1 and STOPPING=1 notifications the Type=notify unit waits for. The watchdog is a separate transport: construct it at the process edge with SystemdNotify.live and the NOTIFY_SOCKET value, and pass it to Hosting.watchdog or Hosting.watchdogWith inside the application Flow — never to the host builder. Hosting.watchdog detects a wedged Axial scheduler or starved thread pool; its sleeping heartbeat and always-true health check do not establish application progress. Supply Hosting.watchdogWith variables processId notifier isHealthy to tie pings to work your application owns. Record the monotonic time after each successful job and reject stale progress:
Show page setup
open System
open System.Threading
open Axial
open Axial.Hosting
open Axial.PlatformService
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actualopen System
open System.Threading
open Axial
open Axial.Hosting
open Axial.PlatformService
// The host supplies these services at composition time.
let progressWatchdog<'env when 'env :> IHasClock and 'env :> IHasLog>
(clock: IClock) (variables: IEnvironmentVariables) (processId: int) (notifier: ISystemdNotifier) =
let mutable lastCompletedTicks: int64 = clock.Elapsed().Ticks
// Call after each completed unit of work in the job loop.
let recordCompletion () =
Interlocked.Exchange(&lastCompletedTicks, clock.Elapsed().Ticks) |> ignore
let isHealthy : Flow<'env, Never, bool> =
Flow.delay (fun () ->
Flow.succeed (clock.Elapsed().Ticks - Interlocked.Read(&lastCompletedTicks) < TimeSpan.FromSeconds(2.).Ticks))
recordCompletion, Hosting.watchdogWith variables processId notifier isHealthy
SystemThreadingAxialHostingPlatformServiceprogressWatchdog: IClock -> IEnvironmentVariables -> int -> ISystemdNotifier -> (unit -> unit) * Flow<'env,Never,unit>envAxial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
Axial.PlatformService.IHasLogDeclares that an environment supplies the logging service.
clock: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
variables: IEnvironmentVariablesAxial.PlatformService.IEnvironmentVariablesProvides environment-variable access supplied by the application host.
processId: intintAn abbreviation for the CLI type . Basic Types
notifier: ISystemdNotifierAxial.Hosting.ISystemdNotifierTransport for systemd notification datagrams.
lastCompletedTicks: int64int64An abbreviation for the CLI type . Basic Types
Elapsed: unit -> TimeSpanTicks: int64Gets the number of ticks that represent the value of the current structure. The number of ticks contained in this instance.
recordCompletion: unit -> unitSystem.Threading.InterlockedProvides atomic operations for variables that are shared by multiple threads.
Exchange: byref<int64> * int64 -> int64Sets a 64-bit signed integer to a specified value and returns the original value, as an atomic operation. The variable to set to the specified value. The value to which the parameter is set. The original value of . The address of is a null pointer.
(~&): 'T -> byref<'T>Address-of. Uses of this value may result in the generation of unverifiable code. The input object. The managed pointer.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
isHealthy: Flow<'env,Never,bool>Axial.Flow`3Axial.NeverRepresents an error channel that cannot occur.
boolAn abbreviation for the CLI type . Basic Types
Axial.Flowdelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
succeed: 'value -> Flow<'env,'error,'value>Same as ok. The value to wrap in a successful flow. A flow that always succeeds with the provided value. let result = Flow.succeed 42 |> Flow.run () // result = Success 42
(-): ^T1 -> ^T2 -> ^T3Overloaded subtraction operator The first parameter. The second parameter. The result of the operation. 10 - 2 // Evaluates to 8
Read: byref<int64> -> int64Returns a 64-bit value, loaded as an atomic operation. The 64-bit value to be loaded. The loaded value.
(<): 'T -> 'T -> boolStructural less-than comparison The first parameter. The second parameter. The result of the comparison. 1 < 5 // Evaluates to true 5 < 5 // Evaluates to false (1, "a") < (1, "z") // Evaluates to true
System.TimeSpanRepresents a time interval.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
Axial.Hosting.HostingMicrosoft Generic Host registration helpers for root Flow applications.
watchdogWith: IEnvironmentVariables -> int -> ISystemdNotifier -> Flow<'env,Never,bool> -> Flow<'env,Never,unit>Runs the scheduler heartbeat with an application-owned progress check; use this to detect stalled work.
Requires & types
Requires: open Axial; open Axial.Hosting; open Axial.PlatformService; open System; open System.Threading (packages: Axial, Axial.Hosting, Axial.PlatformService)val progressWatchdog: IClock -> IEnvironmentVariables -> int -> ISystemdNotifier -> (unit -> unit) * Flow<'env,Never,unit>recordCompletion is shared between the job loop and the watchdog: the loop calls it after each completed unit of
work, and isHealthy reads the same cell to decide whether progress is recent. Compose the loop in parallel with the
watchdog and register the result with Hosting.addApp:
open System
open System.Threading
open Axial
open Axial.Hosting
open Axial.PlatformService
// The job loop owns the work and records progress after each completed unit.
let jobLoop (recordCompletion: unit -> unit) : Flow<'env, Never, unit> =
let rec loop () = flow {
do! Flow.succeed () // one unit of work
recordCompletion ()
return! loop ()
}
loop ()
let application<'env when 'env :> IHasClock and 'env :> IHasLog>
(clock: IClock) (variables: IEnvironmentVariables) (processId: int) (notifier: ISystemdNotifier)
: Flow<'env, Never, unit> =
let recordCompletion, watchdog = progressWatchdog<'env> clock variables processId notifier
Flow.zipPar (jobLoop recordCompletion) watchdog
|> Flow.map ignore
SystemThreadingAxialHostingPlatformServicejobLoop: (unit -> unit) -> Flow<'env,Never,unit>recordCompletion: unit -> unitunitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.Flow`3envAxial.NeverRepresents an error channel that cannot occur.
loop: unit -> Flow<'a,'b,'c>flow: FlowBuilderThe universal flow { } computation expression.
Axial.Flowsucceed: 'value -> Flow<'env,'error,'value>Same as ok. The value to wrap in a successful flow. A flow that always succeeds with the provided value. let result = Flow.succeed 42 |> Flow.run () // result = Success 42
application: IClock -> IEnvironmentVariables -> int -> ISystemdNotifier -> Flow<'env,Never,unit>Axial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
Axial.PlatformService.IHasLogDeclares that an environment supplies the logging service.
clock: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
variables: IEnvironmentVariablesAxial.PlatformService.IEnvironmentVariablesProvides environment-variable access supplied by the application host.
processId: intintAn abbreviation for the CLI type . Basic Types
notifier: ISystemdNotifierAxial.Hosting.ISystemdNotifierTransport for systemd notification datagrams.
watchdog: Flow<'env,Never,unit>progressWatchdog: IClock -> IEnvironmentVariables -> int -> ISystemdNotifier -> (unit -> unit) * Flow<'env,Never,unit>zipPar: Flow<'env,'error,'left> -> Flow<'env,'error,'right> -> Flow<'env,'error,('left * 'right)>Combines two flows into a tuple of their values, running them concurrently. If either flow fails, the other is interrupted immediately. The two flows share a scope of their own. When both succeed, the scoped resources they acquired move to the caller's scope, as if the caller had acquired them, so zipPar (scopeResource a) (scopeResource b) acquires two resources in parallel and both stay open until the caller's scope closes. When zipPar fails, both flows' scoped resources are released before it returns. The first flow to combine. The second flow to combine. A flow that returns a tuple of both successful values. let combined = Flow.zipPar (Flow.succeed 1) (Flow.succeed 2) combined |> Flow.run ()
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
map: ('value -> 'next) -> Flow<'env,'error,'value> -> Flow<'env,'error,'next>Transforms the successful value of a flow. If the source fails, the is not executed. The original failure cause is preserved, including typed failures, interruption, and defects. Use map for pure value transformations after an effect has succeeded. A function of type 'value -> 'next to transform the successful value. The source flow of type to transform. A new with the transformed success value of type 'next. let flow = Flow.succeed 1 |> Flow.map (fun x -> x + 1)
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
Requires & types
Requires: open Axial; open Axial.Hosting; open Axial.PlatformService (packages: Axial, Axial.Hosting, Axial.PlatformService)val jobLoop: (unit -> unit) -> Flow<'env,Never,unit>val application: IClock -> IEnvironmentVariables -> int -> ISystemdNotifier -> Flow<'env,Never,unit>// The host can compose the watchdog without opening a systemd socket.
type ProbeEnv(clock: IClock, log: ILog) =
interface IHasClock with member _.Clock = clock
interface IHasLog with member _.Log = log
let variables =
{ new IEnvironmentVariables with
member _.TryGet _ = None
member _.Set(_, _) = ()
member _.Expand text = text
member _.GetAll() = System.Collections.Generic.Dictionary<string, string>() }
let notifier = { new ISystemdNotifier with member _.Notify _ = () }
let recordCompletion, watchdog = progressWatchdog<ProbeEnv> Clock.live variables 1 notifier
recordCompletion ()
let log =
{ new ILog with
member _.Log _ _ = ()
member _.LogException _ _ _ = () }
(watchdog |> Flow.run (ProbeEnv(Clock.live, log))) |> shouldEqual (Exit.Success ())
FsLiveDocsGeneratedPage11_10B692608C6D.ProbeEnvclock: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
log: ILogAxial.PlatformService.ILogProvides synchronous access to workflow logging as an explicit service.
Axial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
_: ProbeEnvClock: ProbeEnv -> unit -> IClockAxial.PlatformService.IHasLogDeclares that an environment supplies the logging service.
Log: ProbeEnv -> unit -> ILogvariables: IEnvironmentVariablesAxial.PlatformService.IEnvironmentVariablesProvides environment-variable access supplied by the application host.
_: IEnvironmentVariablesTryGet: string -> string optionNoneThe representation of "No value"
Set: string * string option -> unitExpand: string -> stringtext: stringGetAll: unit -> Collections.Generic.IReadOnlyDictionary<string,string>System``.ctor``: unit -> unitInitializes a new instance of the class that is empty, has the default initial capacity, and uses the default equality comparer for the key type.
CollectionsGenericstringAn abbreviation for the CLI type . Basic Types
notifier: ISystemdNotifierAxial.Hosting.ISystemdNotifierTransport for systemd notification datagrams.
_: ISystemdNotifierNotify: string -> unitSends a notification payload to the service manager. The systemd notification payload.
recordCompletion: unit -> unitwatchdog: Flow<ProbeEnv,Never,unit>progressWatchdog: IClock -> IEnvironmentVariables -> int -> ISystemdNotifier -> (unit -> unit) * Flow<'env,Never,unit>Axial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
_: ILogLog: LogLevel -> string -> unitWrites a log message at the requested level.
LogException: LogLevel -> exn -> string -> unitWrites a log message carrying an exception, preserving its stack trace for the host logger.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
``.ctor``: IClock * ILog -> ProbeEnvshouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Requires & types
Requires: open Axial; open Axial.Hosting; open Axial.PlatformService (packages: Axial, Axial.Hosting, Axial.PlatformService)val variables: IEnvironmentVariablesval notifier: ISystemdNotifierval watchdog: Flow<ProbeEnv,Never,unit>val recordCompletion: unit -> unitval log: ILogThe watchdog reads WATCHDOG_USEC and optional WATCHDOG_PID; it sends WATCHDOG=1 at half the configured interval only when its runtime heartbeat advances and the health flow returns true. Invalid or unrepresentable intervals disable the watchdog and log a warning through the environment's IHasLog. For application work that must gate the heartbeat itself, use Hosting.watchdogWithHeartbeat. All watchdog environments supply IHasClock and IHasLog.
[Unit]
Description=Axial agent
[Service]
Type=notify
ExecStart=/usr/bin/dotnet /opt/axial/Agent.dll
WatchdogSec=3
Restart=always
RestartSec=1
NotifyAccess=main
[Install]
WantedBy=default.target
Type=notify waits for the READY notification that SystemdLifetime (installed by UseSystemd()) sends when the host starts. NotifyAccess=main permits notifications from the main process; Restart=always recovers from a crash or watchdog timeout. Choose WatchdogSec with enough margin for the heartbeat and health check.
On Linux with a user systemd session, run scripts/check-systemd-watchdog.sh from the repository. It builds a transient probe, verifies steady operation, SIGKILL recovery and watchdog timeout, then stops and resets its units. This is a manual check, not part of CI.
Windows
Call AddWindowsService() on the HostApplicationBuilder (UseWindowsService() on an older IHostBuilder) and register the root application with Hosting.addApp. Configure Service Control Manager recovery separately; registration alone does not configure restart policy:
sc failure <svc> reset= 86400 actions= restart/5000/restart/5000/restart/5000

