Heartbeat
A self-driving health machine: a timeout schedules the next check; .producing runs the check
and feeds the result back as an event. Degraded and critical tiers use shorter or recovery
deadlines.
(The timeout event is named HeartbeatTick here so it does not clash with this page’s object
name.)
enum ServiceState derives Finite:
case Stopped, Started, Degraded, Critical
enum ServiceEvent derives Finite:
case Start, Stop, ManualReset
case HeartbeatTick, DegradedCheck
case Healthy, Unstable, Failed
val machine = Machine(
assemblyAll[ServiceState, ServiceEvent]:
(Stopped via Start to Started) @@ Aspect.timeout(10.seconds, HeartbeatTick)
(Started via HeartbeatTick to Started)
.producing { (_, _) => ZIO.succeed(Healthy) } @@ Aspect.timeout(10.seconds, HeartbeatTick)
(Started via Healthy to Started) @@ Aspect.timeout(10.seconds, HeartbeatTick)
(Started via Unstable to Degraded) @@ Aspect.timeout(3.seconds, DegradedCheck)
(Degraded via DegradedCheck to Degraded)
.producing { (_, _) => ZIO.succeed(Healthy) } @@ Aspect.timeout(3.seconds, DegradedCheck)
(Degraded via Healthy to Started) @@ Aspect.timeout(10.seconds, HeartbeatTick)
(Degraded via Failed to Critical) @@ Aspect.timeout(30.seconds, ManualReset)
anyOf(Started, Degraded, Critical) via Stop to Stopped
Critical via ManualReset to Started
)
The graph
Mermoid.diagram(
MermaidVisualizer.flowchart(machine),
DocsDiagrams.diagramConfig,
)Features in play: assemblyAll, .producing, @@ Aspect.timeout, anyOf. Full stack with
Postgres stores and TimeoutSweeper: examples/.../heartbeat.
A timeout tick
DocSpecs send the timeout event the sweeper would fire. After HeartbeatTick, the producing
effect returns Healthy and the machine stays in Started with a fresh deadline.
ZIO.scoped {
for
fsm <- machine.start(Stopped)
_ <- fsm.send(Start)
_ <- fsm.send(HeartbeatTick)
_ <- ZIO.sleep(50.millis)
state <- fsm.currentState
yield state
}.asDocStartedNext: Orders for rich event[T] payloads, or Durable Timeouts
for the production sweeper story.