Documentation
¶
Overview ¶
Package rerun is a lightweight durable execution library. Workflows survive crashes, restarts, and deploys by journaling every step to a pluggable store; on recovery the engine replays the journal, skipping completed steps, and resumes exactly where it left off.
The whole API fits on a napkin. Do runs a step once and journals its result; Sleep is a durable delay; Recover resumes every unfinished run after a restart. A workflow is an ordinary Go function.
e := rerun.New(sqlite.New("rerun.db"))
e.Handle("signup", func(w *rerun.W) error {
id, _ := rerun.Do(w, "create-account", func(ctx context.Context) (string, error) {
return createAccount(ctx)
})
rerun.Sleep(w, time.Hour)
rerun.Do(w, "welcome-email", func(ctx context.Context) (struct{}, error) {
return struct{}{}, sendEmail(ctx, id)
})
return nil
})
e.Start(ctx, "signup", "user-42")
// ...and after a restart, in the same process:
e.Recover(ctx)
Determinism ¶
A workflow body must be deterministic in its control flow and its step tags. Anything nondeterministic — the current time, a random number, a read whose result steers a branch — must be captured inside a Do so its value is journaled and replayed, never recomputed. On replay, a tag that diverges from the journal panics, loudly and early, because a silent divergence corrupts every later step.
Guarantees ¶
rerun is durable and resumable; at-least-once for side effects; exactly-once only when steps are idempotent. A step repeats only if the process dies in the narrow window after its side effect runs and before its journal entry commits, which is why production steps are written to be idempotent. See the README's Non-goals for what rerun deliberately does not do.
Backends ¶
Persistence is the Store seam, and swapping a backend changes no engine code: internal.MemStore for tests and single-process programs, package sqlite for a single persistent node, package postgres for multi-process execution behind an advisory-lock lease. Any Store that passes storetest.RunStoreContract is a drop-in.
Index ¶
- Variables
- func Do[T any](w *W, tag string, fn func(context.Context) (T, error)) (T, error)
- func DoTimeout[T any](w *W, tag string, d time.Duration, fn func(context.Context) (T, error)) (T, error)
- func ExpBackoff(base, max time.Duration) func(int) time.Duration
- func FixedBackoff(d time.Duration) func(int) time.Duration
- func Input[T any](w *W) (T, error)
- func Result[T any](ctx context.Context, e *Engine, runID string) (T, error)
- func Retry[T any](w *W, tag string, p RetryPolicy, fn func(context.Context) (T, error)) (T, error)
- func Return[T any](w *W, v T)
- func Sleep(w *W, d time.Duration) error
- func Version(w *W, changeID string, min, max int) int
- func Wait[T any](w *W, name string) (T, error)
- type Canceller
- type Clock
- type Codec
- type Engine
- func (e *Engine) Cancel(ctx context.Context, runID string) error
- func (e *Engine) Deliver(ctx context.Context, runID, name string, payload any) error
- func (e *Engine) Handle(name string, fn Func)
- func (e *Engine) Recover(ctx context.Context) error
- func (e *Engine) Redrive(ctx context.Context, runID string) error
- func (e *Engine) Shutdown(ctx context.Context) error
- func (e *Engine) Start(ctx context.Context, workflow, runID string, in ...any) error
- type Func
- type Guarder
- type Log
- type Observer
- type Opt
- type Reader
- type RetryPolicy
- type Run
- type Signaler
- type Status
- type StepError
- type Store
- type W
- type Writer
Constants ¶
This section is empty.
Variables ¶
var ErrEngineClosed = errors.New("rerun: engine is shut down")
ErrEngineClosed is returned by engine methods called after Shutdown.
var ErrRunExists = errors.New("rerun: run already exists")
ErrRunExists reports a Create for a run ID the store already has — the signal Start callers use to dedupe retried requests.
var ErrSeqConflict = errors.New("rerun: journal sequence conflict")
ErrSeqConflict reports an Append at a (run, seq) position that is already journaled: another worker owns the run (a lost lease) or the code shrank. The engine parks the run when it sees this.
Functions ¶
func Do ¶
Do runs a step exactly once. On the first run it executes fn, journals the result, and returns it; on replay it returns the journaled result without executing fn. It panics if the tag diverges from the journal: that means the workflow is no longer deterministic, and every later positional match would be meaningless.
func DoTimeout ¶
func DoTimeout[T any](w *W, tag string, d time.Duration, fn func(context.Context) (T, error)) (T, error)
DoTimeout is Do with a per-step deadline: fn runs under a context that cancels after d. Whatever fn returns — a value, its own error, or a deadline error if it honors the context — is journaled as the step's outcome, so replay is deterministic and a timed-out step is never silently retried into a success.
func ExpBackoff ¶
ExpBackoff doubles the wait each attempt, starting at base and capped at max.
func FixedBackoff ¶
FixedBackoff waits the same duration before every retry.
func Input ¶
Input returns the value passed to Start for this run, decoded as T. It is journaled as the run's seed at Start, so replay sees the same value every time; a run started without input yields the zero value.
func Result ¶
Result reads the value a run recorded with Return, decoded as T. If the run failed, it returns the terminal error the workflow returned; if the run has recorded no result yet (still running, or never called Return), it returns an error saying so. It reads through the store, so it works from any process.
func Retry ¶
Retry runs fn until it succeeds or MaxAttempts is reached. Each attempt is its own journaled step (tag:attempt-N), and the wait between attempts uses the durable Sleep — so the backoff itself survives a crash, and on replay the whole attempt-and-wait sequence is reproduced from the journal without calling fn again. It returns the first success, or the last error wrapped after the final attempt.
func Return ¶
Return records the run's result. Call it once, at a deterministic point (typically the workflow's last act). It is journaled like any step — a doStep under the hood (Do itself would reject the reserved result tag) — so replay reproduces it without re-running.
func Sleep ¶
Sleep is a durable delay. It journals an absolute deadline as an instant step, then waits only the time still remaining until that deadline, recomputed on every run. A restart before the deadline waits out the remainder; a restart after it returns immediately. The wait itself is never journaled — it is a function of the deadline and the clock, so it is always recomputed, never stored.
func Version ¶
Version pins a run to a code path so in-flight runs survive a deploy that changes the workflow. On the first run it journals max, the newest version this build knows; on replay it returns the journaled value, so a run started before the change keeps taking its original branch while new runs take the new one. Branch on the returned int, and give every change its own changeID.
It panics if the journaled version falls outside [min, max]: that means a rollback to code too old to honor a version some run already committed to, and failing loud beats silently taking the wrong branch.
func Wait ¶
Wait blocks the workflow until a signal named name arrives, then journals and returns its payload. On replay it returns the journaled payload without waiting, so a workflow that waited three days for an approval resumes with the approval already in hand and never waits again. It is an ordinary Do whose function happens to block on a mailbox instead of computing — which is why the received value is journaled and replayed like any other step result.
Types ¶
type Canceller ¶
type Canceller interface {
RequestCancel(ctx context.Context, runID string) error
CancelRequested(ctx context.Context, runID string) (bool, error)
}
Canceller is an optional Store capability: a durable record that a run should be cancelled. A backend that implements it lets Cancel reach a run executing in another process (see WithCancelPoll). Like Signaler, it is kept separate so the core Store contract stays minimal — a store without it supports only in-process cancellation.
type Codec ¶
Codec is the serialization seam: how a step's result and a run's input are encoded to and decoded from the journal. The default is JSON; override it with WithCodec (for example to compress or encrypt payloads).
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine orchestrates runs: it owns the workflow registry, the swappable seams (store, codec, clock, observer), and — since runs must outlive the contexts of whoever starts them — the root context every run descends from. Only Shutdown ends that root, and it ends it with a "parked" cause so in-flight runs stop without being marked terminal.
func New ¶
New builds an Engine over a Store, applying defaults (JSON codec, wall clock, no-op observer) before any options.
func (*Engine) Cancel ¶
Cancel stops a run. When the store can record cancels durably, the request is recorded FIRST — so a crash between this call and the run's terminal write cannot resurrect the run: the next claim sees the request and finishes it Cancelled. Then, if the run is executing in this process, its context unwinds immediately. With neither a local run nor a Canceller store, there is nothing to cancel with, and that is an error.
func (*Engine) Deliver ¶
Deliver records an external event for a run. Call it from outside the workflow — an HTTP handler, a webhook, another service. Delivery may arrive before the workflow reaches its Wait; the event queues in the mailbox until Wait consumes it, so it is never lost.
func (*Engine) Handle ¶
Handle registers a workflow body under a name. It panics on a duplicate: two workflows sharing a name is a build-time programmer error, not a runtime condition to recover from.
func (*Engine) Recover ¶
Recover relaunches every incomplete run through exec, resuming workflows a process restart left in flight.
func (*Engine) Redrive ¶ added in v0.2.0
Redrive resets a Stuck run to Pending so the next Recover (or, from M2, the dispatcher) claims it again — the operator's lever after shipping a fix. M1 note: this resets unconditionally and does not clear a durable cancel request, so a run that was ever cancel-requested is re-finished Cancelled at the next claim rather than re-run (start a fresh run ID in that case). M2's Inspector adds status verification and cancel-aware redrive.
func (*Engine) Shutdown ¶ added in v0.2.0
Shutdown parks the engine: the dispatcher-facing methods start refusing, every run's context is cancelled with errParked (runs unwind, stay incomplete in the store, and resume on a later engine), and Shutdown waits for the run goroutines to return or ctx to expire.
func (*Engine) Start ¶
Start creates a Pending run and launches it in its own goroutine. It returns as soon as the run is durably created, so the caller is never blocked on execution and one process can drive many thousands of runs. An optional input is journaled as the run's seed and read back inside the workflow with Input.
type Guarder ¶
type Guarder interface {
Acquire(ctx context.Context, runID string) (release io.Closer, acquired bool, err error)
}
Guarder leases a run to one worker. Acquire is a non-blocking try-lock: acquired is false when another worker already holds the run, so recovery across many processes never replays the same run twice. A single-process store that never contends may always report acquired.
type Log ¶
Log is one immutable journal entry: the result a completed step produced. Persisting the result a step produced — not merely the fact that a step ran — is what lets recovery replay a step instead of executing it a second time.
type Observer ¶
type Observer interface {
OnStart(r Run)
OnStep(runID string, l Log)
OnFinish(runID string, s Status)
}
Observer receives lifecycle events for logging and metrics. Events are passed by value so observers cannot mutate engine state.
type Opt ¶
type Opt func(*Engine)
Opt configures an Engine at construction time.
func WithCancelPoll ¶
WithCancelPoll enables cross-process cancellation: each running run checks the store for a cancel request every d (the store must implement Canceller). Zero, the default, disables the check so a run parks for free and only in-process Cancel applies. Cross-process cancellation is eventual, within one interval.
func WithObserver ¶
WithObserver overrides the observability seam.
func WithStoreTimeout ¶ added in v0.2.0
WithStoreTimeout bounds every journal and status write. The write context is detached from the run's cancellation on purpose: a cancel must never be able to lose the record of work that already happened. The default is 30s.
type Reader ¶
type Reader interface {
LoadLogs(ctx context.Context, runID string) ([]Log, error)
Incomplete(ctx context.Context) ([]Run, error)
}
Reader is the cold path: loading a journal to replay it and finding the runs a restart left unfinished.
type RetryPolicy ¶
RetryPolicy configures Retry: how many attempts to make and how long to wait before each retry (Backoff is called with the zero-based attempt that just failed). A nil Backoff means retry immediately.
type Run ¶
Run is the metadata for a single workflow instance. Input is the optional seed passed to Start, persisted atomically with the run's creation so a crash can never separate a run from its input.
type Signaler ¶
type Signaler interface {
PushSignal(ctx context.Context, runID, name string, payload []byte) error
PopSignal(ctx context.Context, runID, name string) (payload []byte, ok bool, err error)
}
Signaler is an optional Store capability: a per-run mailbox for external events. A backend that implements it enables Wait and Deliver. Keeping it a separate interface — not a method on Store — is interface segregation: a backend with no use for signals simply does not implement it.
type Status ¶
type Status int
Status is a run's lifecycle state.
const ( // Pending is the initial state: a run created but not yet claimed and executed. Pending Status = iota // Running marks a run currently leased and executing (or replaying its journal). Running // Done is terminal: the workflow returned without error. Done // Failed is terminal: the workflow returned an error, recorded in the journal. Failed // Cancelled is terminal, like Done and Failed. It is appended after Failed // so the existing values keep their numbers in journals and databases. Cancelled // Stuck marks a run the engine refuses to execute — a determinism panic, a // corrupt journal, a workflow that shrank. It is excluded from Incomplete so // nothing claims it again automatically; Redrive re-admits it after the // operator ships a fix. Appended so values stay stable. Stuck )
type StepError ¶
StepError carries a failed step's tag and message so the failure survives being journaled and is reconstructed identically on replay.
type Store ¶
Store is the whole persistence seam. It is split three ways so a consumer can depend on only the face it uses — a dashboard on Reader, a lease manager on Guarder, a log shipper on Writer — and so any backend is a drop-in the engine cannot tell apart.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
crashtest
|
|
|
child
command
Command child is the crash-test subject: a four-step workflow whose every step appends its tag to an effects file, self-killed with SIGKILL after a configured number of live steps.
|
Command child is the crash-test subject: a four-step workflow whose every step appends its tag to an effects file, self-killed with SIGKILL after a configured number of live steps. |
|
examples
|
|
|
capstone
command
Command capstone is an end-to-end signup saga — create account, charge with retry, durable wait, welcome email — that crashes after the charge and recovers without charging the card again.
|
Command capstone is an end-to-end signup saga — create account, charge with retry, durable wait, welcome email — that crashes after the charge and recovers without charging the card again. |
|
durablesleep
command
Command durablesleep shows a durable Sleep: the wait happens once on the first run and is skipped on recovery.
|
Command durablesleep shows a durable Sleep: the wait happens once on the first run and is skipped on recovery. |
|
durabletimer
command
Command durabletimer shows a durable timer that, after a crash, waits only the time remaining until its journaled deadline.
|
Command durabletimer shows a durable timer that, after a crash, waits only the time remaining until its journaled deadline. |
|
recover
command
Command recover shows crash recovery: a run with a partial journal resumes and executes only its unfinished step, replaying the rest.
|
Command recover shows crash recovery: a run with a partial journal resumes and executes only its unfinished step, replaying the rest. |
|
signals
command
Command signals shows a step whose value comes from outside the workflow.
|
Command signals shows a step whose value comes from outside the workflow. |
|
skeleton
command
Command skeleton is the simplest rerun example: a two-step workflow runs forward and prints the journal each step leaves behind.
|
Command skeleton is the simplest rerun example: a two-step workflow runs forward and prints the journal each step leaves behind. |
|
versioning
command
Command versioning shows safe deploys across in-flight runs.
|
Command versioning shows safe deploys across in-flight runs. |
|
workers
command
Command workers shows exactly-once dispatch across concurrent workers: four goroutines race to recover the same twelve runs, and the Guarder try-lock ensures each run's side effect fires exactly once.
|
Command workers shows exactly-once dispatch across concurrent workers: four goroutines race to recover the same twelve runs, and the Guarder try-lock ensures each run's side effect fires exactly once. |
|
Package internal holds the in-memory Store: the engine's default and a test aid.
|
Package internal holds the in-memory Store: the engine's default and a test aid. |
|
Package postgres is a persistent, multi-process rerun.Store backed by PostgreSQL via the pure-Go, cgo-free driver github.com/lib/pq.
|
Package postgres is a persistent, multi-process rerun.Store backed by PostgreSQL via the pure-Go, cgo-free driver github.com/lib/pq. |
|
Package sqlite is a persistent rerun.Store backed by SQLite via the pure-Go, cgo-free driver modernc.org/sqlite, so the result stays a single static binary with no C toolchain required.
|
Package sqlite is a persistent rerun.Store backed by SQLite via the pure-Go, cgo-free driver modernc.org/sqlite, so the result stays a single static binary with no C toolchain required. |
|
Package storetest is the importable contract suite for rerun.Store backends.
|
Package storetest is the importable contract suite for rerun.Store backends. |
|
tools
|
|
|
doccheck
command
Command doccheck fails when any exported top-level symbol or exported method lacks a doc comment.
|
Command doccheck fails when any exported top-level symbol or exported method lacks a doc comment. |
|
mutate
command
Command mutate is a dependency-free mutation tester for rerun's core.
|
Command mutate is a dependency-free mutation tester for rerun's core. |