Lockstep.Strategy behaviour (Lockstep v0.1.0)

Copy Markdown View Source

Behaviour for scheduling strategies.

A strategy is a pure module + state. The controller threads the state through every interaction.

Built-in strategies:

Choosing a strategy

Different races are most reliably caught by different strategies. In our experience:

  • :pct (the default) is the all-purpose choice. PCT samples schedules according to a bug-depth bound d: a bug requiring at most d well-placed priority swaps is found with probability 1 / (n * k^(d-1)) per iteration, where n is the step count and k the number of procs. Strong on classic data races (lost-update, TOCTOU) and partial-order-shaped bugs.

  • :pos uniformly samples partial orders. Better than PCT when the bug requires the strategy to consistently pick a specific proc over several consecutive sync points — PCT's priority-shuffle under-explores those long runs, but POS's uniform sampling hits them. The leader-follower async replication race in test/leader_follower_register_test.exs is a real example: PCT didn't find it in 100 iterations, POS found it on iteration 1.

    Rule of thumb: if PCT can't find your race, try POS before cranking iteration counts.

  • :idpct runs PCT but cycles bug_depth across iterations (default [2, 6]). Use when you don't know how deep your bug is. Iterations at depth 2 are fast and find shallow bugs quickly; iterations at depth 6 catch deeper races. Costs nothing extra per iteration vs. fixed-depth PCT, but amortizes coverage across the depth range. Particularly nice for runs of 1000+ iterations.

  • :fair_pct is PCT for the first K steps, then a fair random walk. Use when your test has long-running infrastructure (heartbeats, timers, supervisors) that would otherwise dominate the schedule under pure PCT.

  • :random is the baseline — uniform random over ready procs each step. Useful as a sanity check or for stress workloads where the bug is high-probability under random scheduling.

  • :replay isn't really for hunting bugs — it follows a saved trace exactly, with optional random fallback for partial schedules. Used by the shrinker.

Strategies are deterministic given a seed, so any iteration that finds a bug is reproducible (and shrinkable).

Summary

Types

pid_set()

@type pid_set() :: MapSet.t(pid())

state()

@type state() :: term()

Callbacks

init(opts)

@callback init(opts :: keyword()) :: state()

on_register(pid, state)

@callback on_register(pid(), state()) :: state()

pick(pid_set, state)

@callback pick(pid_set(), state()) :: {pid(), state()}