Tutorial: Extract and Verify a GenServer
Copy MarkdownThis tutorial walks through the full pipeline: extract a spec skeleton from a GenServer module, enrich it with invariants, and verify it with TLC.
Prerequisites
- TLX installed (
{:tlx, "~> 0.4.0"}) - A GenServer module to extract from
- Java installed (for TLC)
1. Extract the skeleton
Suppose you have a reconciler GenServer:
defmodule MyApp.Reconciler do
use GenServer
def init(_), do: {:ok, %{status: :idle, deps_met: true}}
def handle_call(:check, _from, state) do
{:reply, :ok, %{state | status: :in_sync}}
end
def handle_cast(:drift_signal, state) do
{:noreply, %{state | status: :drifted}}
end
endRun the extractor:
mix tlx.gen.from_gen_server MyApp.Reconciler --output specs/reconciler_skeleton.ex
This produces:
defmodule ReconcilerSpec do
use TLX.Patterns.OTP.GenServer,
fields: [status: :idle, deps_met: true],
calls: [
check: [next: [status: :in_sync]]
],
casts: [
drift_signal: [next: [status: :drifted]]
]
end2. Review the extraction
The extractor found:
- Two fields from
init/1:status(:idle) anddeps_met(true) - One call (
:check) and one cast (:drift_signal) - All transitions are
:highconfidence (literal atoms)
What's missing:
:checkalways succeeds — in reality it might find drift- No guard on
:check— should it only work from:idle? - No invariant beyond the auto-generated
valid_status
3. Enrich the skeleton
Switch from the pattern to a defspec for more control:
import TLX
defspec ReconcilerSpec do
variable :status, :idle
variable :deps_met, true
action :check do
guard(e(status == :idle))
branch :in_sync do
next :status, :in_sync
end
branch :drifted do
next :status, :drifted
end
end
action :apply do
guard(e(status == :drifted and deps_met == true))
branch :success do
next :status, :in_sync
end
branch :failure do
next :status, :drifted
end
end
action :drift_signal do
next :status, :drifted
end
action :return_to_idle do
guard(e(status == :in_sync))
next :status, :idle
end
invariant :valid_status,
e(status == :idle or status == :in_sync or status == :drifted)
property :eventually_idle, always(eventually(e(status == :idle)))
endChanges from the skeleton:
- Added success/failure branches to
:check - Added an
:applyaction with guard requiringdeps_met - Added a
return_to_idleaction to close the cycle - Added a liveness property
4. Verify with TLC
mix tlx.check ReconcilerSpec
TLC exhaustively explores all reachable states. If the liveness
property holds, every execution path eventually returns to :idle.
If it fails, TLC prints a counterexample trace showing the stuck path.
5. Visualize
mix tlx.emit ReconcilerSpec --format mermaid
stateDiagram-v2
[*] --> idle
idle --> in_sync: check/in_sync
idle --> drifted: check/drifted
drifted --> in_sync: apply/success
drifted --> drifted: apply/failure
drifted --> drifted: drift_signal
in_sync --> idle: return_to_idleNext steps
- Add refinement to an abstract spec (
refines AbstractReconciler do ... end) - See the enrichment checklist for more patterns
- See OTP Patterns Reference for all options