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 ¶
- 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) 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 ¶
This section is empty.
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 — an ordinary Do under the hood — 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 Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine orchestrates runs: it owns the workflow registry and the swappable seams (store, codec, clock, observer).
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. If the run is executing in this process, its context is cancelled immediately, so a parked Sleep or a ctx-respecting step unwinds and the run finishes Cancelled. Otherwise, if the store is a Canceller, the request is recorded durably and a worker running the run elsewhere picks it up on its next poll (see WithCancelPoll) — cancellation is then eventual. With neither a local run nor a Canceller store, it returns 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) 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.
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 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 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 |
|---|---|
|
examples
|
|
|
capstone
command
Command capstone is the day-7 acceptance test: a 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 the day-7 acceptance test: a 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 is the day-3 milestone: a Sleep is waited once and skipped on recovery.
|
Command durablesleep is the day-3 milestone: a Sleep is waited once and skipped on recovery. |
|
durabletimer
command
Command durabletimer is Part II hard problem 2: a durable timer that waits only the time remaining until a journaled deadline.
|
Command durabletimer is Part II hard problem 2: a durable timer that waits only the time remaining until a journaled deadline. |
|
recover
command
Command recover is the day-2 milestone: a run resumes from a partial journal, executing only the unfinished step.
|
Command recover is the day-2 milestone: a run resumes from a partial journal, executing only the unfinished step. |
|
signals
command
Command signals is Part II hard problem 3: a step whose value comes from outside.
|
Command signals is Part II hard problem 3: a step whose value comes from outside. |
|
skeleton
command
Command skeleton is the day-1 milestone: a two-step workflow runs forward and journals each step.
|
Command skeleton is the day-1 milestone: a two-step workflow runs forward and journals each step. |
|
versioning
command
Command versioning is Part II hard problem 4: safe deploys across in-flight runs.
|
Command versioning is Part II hard problem 4: safe deploys across in-flight runs. |
|
workers
command
Command workers is Part II hard problem 1: exactly-once dispatch across concurrent workers.
|
Command workers is Part II hard problem 1: exactly-once dispatch across concurrent workers. |
|
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.
|
Package storetest is the importable contract suite. |
|
tools
|
|
|
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. |