Pure-Go restart/reconcile state machine: grace debounce, sliding-window anti-thrash, exponential backoff (CGO-free, 6-arch).
Translation pending
Developer toolsGoGitHub @go-procOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A pure-Go (no cgo) restart / reconcile state machine. Feed it a stream of
down / up / unhealthy / healthy signals for named units, and it decides
when to respawn after a death — honouring a grace period (anti-flap),
max_restarts within a sliding window (anti-thrash), and optional exponential
backoff — then effects the respawn through a caller-supplied Actions
interface.
Extracted from a microVM agent and made fully dependency-free: the
RespawnPolicy is a local value type (no external schema), health is a local
SignalKind (no probe/health-checker import), and side effects go through an
interface, so the whole machine is unit-testable with a fixed clock and a fake.
A pure decision core.
Decideis an allocation-light pure function:(policy, history, attempt, signal, now) → Plan. TheReconcileris the concurrent shell around it — one goroutine per watched unit, lazily started, draining a buffered signal channel — but every scheduling decision is a value you can assert against a fixed clock.
down/unhealthy signal parks in grace_wait for
grace_period_ms first, so a transient flap that recovers never triggers a
restart.max_restarts restarts are allowed per rolling
window_ms; exceeding it moves the unit to cooldown until the window rolls.constant or exponential (doubling initial_delay_ms, capped
at 5 minutes) before each respawn.Stop then Start, not a bare Start.Reconciler fans out across many units by
name; distinct units never serialise on each other.CGO-free, dependency-free, 100% test coverage (including every error
branch), race-tested, gofmt + go vet clean, and green across the six 64-bit
Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x).
go get github.com/go-proc/respawn
package main
import (
"context"
"time"
"github.com/go-proc/respawn"
)
// myActions effects the respawn. A real one wraps your process/VM driver;
// tests supply a fake to assert state-machine behaviour.
type myActions struct{ /* ... */ }
func (myActions) StartVM(ctx context.Context, name string) error { /* ... */ return nil }
func (myActions) StopVM(ctx context.Context, name string) error { /* ... */ return nil }
func main() {
r := respawn.New(myActions{}, nil)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
policy := &respawn.RespawnPolicy{
Enabled: true,
GracePeriodMs: 2000, // debounce flaps
MaxRestarts: 5, // within...
WindowMs: 60000, // ...a 60s window
Backoff: "exponential", // 1s, 2s, 4s, ... capped 5m
InitialDelayMs: 1000,
}
r.Watch(ctx, "web-1", policy)
// Feed signals from your event source:
r.Send(respawn.Signal{VMName: "web-1", Kind: respawn.SignalDown, When: time.Now()})
// On shutdown: cancel ctx, then drain.
cancel()
r.Wait()
}
Or drive the pure core directly, no goroutines:
plan := respawn.Decide(policy, history, attempt, respawn.Signal{Kind: respawn.SignalDown}, time.Now())
// plan.State / plan.Action / plan.DelayFor / plan.Reason
type RespawnPolicy struct {
Enabled bool
GracePeriodMs int64
MaxRestarts int32
WindowMs int64
Backoff string // "constant" | "exponential"
InitialDelayMs int64
}
type SignalKind int // SignalDown | SignalUp | SignalUnhealthy | SignalHealthy
type Signal struct { VMName string; Kind SignalKind; When time.Time }
// Actions is the side-effect surface the reconciler drives.
type VMActions interface {
StartVM(ctx context.Context, name string) error
StopVM(ctx context.Context, name string) error
}
// Reconciler — the concurrent shell.
func New(actions VMActions, log *slog.Logger) *Reconciler
func (r *Reconciler) Watch(ctx context.Context, name string, policy *RespawnPolicy)
func (r *Reconciler) Unwatch(name string)
func (r *Reconciler) Send(sig Signal)
func (r *Reconciler) Wait()
// Pure decision core.
type Plan struct { State State; Action Action; DelayFor time.Duration; Reason string }
func Decide(policy *RespawnPolicy, h *History, attempt int, sig Signal, now time.Time) Plan
func DecideAfterGrace(policy *RespawnPolicy, h *History, attempt int, kind SignalKind, now time.Time) Plan
Naming. The
Actionsmethods andSignal.VMNamekeep theVMnames of the origin codebase; the package itself is unit-agnostic — a "VM" here is any named thing you supervise.
The pure Decide core is asserted against a fixed clock across every policy
branch; the concurrent Reconciler is driven through a fake Actions with
injected clock/sleep seams to cover backoff, cooldown, grace, and every error
path deterministically.
COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1 # 100.0%
BSD-3-Clause — see LICENSE. Copyright the go-proc/respawn authors.