voluta 0.x default reference

Checkpoints

Why every barrier can persist — threads that outlive the process.

A checkpoint is a coherent snapshot of a thread after a superstep barrier: channel values, run status, interrupt payload if any, enough bookkeeping to continue.

Without checkpoints, voluta would be an in-process state machine. With them, a run can pause, survive restart, and resume under the same ThreadId — including from another process.

Why after every barrier?

Because that is when state is consistent:

  • all node tasks for the tick finished
  • writes were merged through reducers
  • edges have not yet started the next tick (or the run stopped)

Checkpointing mid-handler would freeze half-thoughts. Checkpointing only at process exit loses the “human approved three hours later” story.

The only storage seam: ICheckpointer

Task PutAsync(CheckpointSnapshot snapshot, CancellationToken cancellationToken = default);
Task<CheckpointSnapshot?> GetAsync(string threadId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<CheckpointSnapshot>> ListAsync(string threadId, CancellationToken cancellationToken = default);
MethodContract
PutAsyncPersist this snapshot for the thread
GetAsyncLatest snapshot, or null if never written
ListAsyncHistory ascending by step; if you can’t, throw NotSupportedException

Your app should depend on the interface. Swap in-memory → file → future store without rewriting nodes.

What a snapshot carries

FieldWhy it exists
ThreadIdIsolation between conversations / jobs
StepSuperstep index — ordering and debugging
StatusDone / Interrupted / Failed / …
ChannelValuesThe actual graph state
InterruptPayloadWhat to show a human when paused
PendingSends / PendingWritesMid-run recovery details

You rarely construct these by hand — the runtime does. You read them for ops UIs, support tools, and tests.

Providers on main

TypePackageUse when
InMemoryCheckpointerVolutaSamples, unit tests, throwaway demos
FileCheckpointerVoluta.Checkpoints.FileSingle-node durability, JSON on disk
var checkpointer = new FileCheckpointer("/var/lib/myapp/voluta-checkpoints");

In-memory is not “fake voluta.” It is the same protocol with a store that dies with the process — fine until durability is a product requirement.

Testing storage without lying

Voluta.Testing exists so you don’t invent half-broken doubles:

  • RecordingCheckpointer — see every put/get/list
  • FaultInjectingCheckpointer — fail the n-th write; assert the run survives or fails cleanly
  • CheckpointerConformance.RunAllAsync — the suite your ICheckpointer should pass, interrupt fields included

If you implement a new store, conformance is the bar — not a happy-path unit test that never interrupts.

Operational habits

  1. Stable thread ids from your domain (ticket id, session id), not random per request unless you truly want a one-shot run.
  2. Treat Interrupted as normal — poll or webhook → ResumeAsync.
  3. Don’t hand-edit checkpoint files in production; use the API and UI.

Next: Interrupts for the pause/resume path that makes checkpoints earn their keep.