Lockstep.History (Lockstep v0.1.0)

Copy Markdown View Source

Jepsen-style operation history recorder.

A history is a sequence of :invoke events (operations starting) paired with :ok / :fail / :info completion events. It's the raw input for consistency checkers like Lockstep.Checker.Linearizable.

Recording

Each operation has three lifecycle events:

  • invoke — a process announces it's starting an operation
  • ok — the operation completed and the caller knows the result
  • fail — the operation explicitly didn't apply (returned an error the caller can interpret as "no change")
  • info — the operation's outcome is unknown (caller crashed, timed out, lost the connection mid-call). Linearizability checkers must consider that the op MIGHT or MIGHT NOT have applied.

Quick example

history = Lockstep.History.start_link!()

# Concurrent workers issuing reads/writes
for i <- 1..3 do
  Lockstep.spawn(fn ->
    Lockstep.History.op(history, :write, i, fn ->
      Register.put(reg, i)
    end)
  end)
end

# ... wait for them ...

events = Lockstep.History.events(history)

{:ok, _info} =
  Lockstep.Checker.Linearizable.check(events, Lockstep.Model.Register)

Order

Events are indexed in the order they reach the recorder. Under Lockstep's deterministic controller this is also the real-time order — every invoke/ok call is a sync point routed through the controller, so the strategy controls when each event is recorded relative to others.

Summary

Functions

Return events in invocation order (oldest first).

Record a :fail (explicit non-apply) completion for f.

Record an :info (unknown outcome) completion for f. Use for crashes, timeouts, or anything where the caller can't be sure whether the op applied.

Record an :invoke event for f with input value from self().

Record an :ok (success) completion for f with output value.

Convenience wrapper: record :invoke before running fun, then record :ok with the result on normal return, :info on raise/ exit/throw. Returns the function's result (or re-raises).

Start a history recorder under the current Lockstep iteration. Returns the pid; pass it into worker closures so they can record.

Functions

events(history)

@spec events(pid()) :: [Lockstep.History.Event.t()]

Return events in invocation order (oldest first).

fail(history, f, reason)

@spec fail(pid(), atom(), any()) :: :ok

Record a :fail (explicit non-apply) completion for f.

info(history, f, reason)

@spec info(pid(), atom(), any()) :: :ok

Record an :info (unknown outcome) completion for f. Use for crashes, timeouts, or anything where the caller can't be sure whether the op applied.

init(state)

invoke(history, f, value)

@spec invoke(pid(), atom(), any()) :: :ok

Record an :invoke event for f with input value from self().

ok(history, f, value)

@spec ok(pid(), atom(), any()) :: :ok

Record an :ok (success) completion for f with output value.

op(history, f, args, fun)

@spec op(pid(), atom(), any(), (-> result)) :: result when result: var

Convenience wrapper: record :invoke before running fun, then record :ok with the result on normal return, :info on raise/ exit/throw. Returns the function's result (or re-raises).

Use this when the operation has clear "succeeded with this value" or "crashed, who knows" outcomes. For explicit "didn't apply" failures, call invoke/3 + fail/3 manually.

Lockstep.History.op(history, :write, 42, fn ->
  Register.put(reg, 42)
  :ok
end)

start_link!()

@spec start_link!() :: pid()

Start a history recorder under the current Lockstep iteration. Returns the pid; pass it into worker closures so they can record.