servicespec

package
v0.44.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package servicespec defines the portable fak.service.v1 desired-state and restart-semantics contract (#4749, parent #4748). Service intent was encoded independently in SCM verbs, Scheduled Task scripts, cron concepts, and deployment docs; this leaf is the ONE versioned schema all of them render from: identity, command, cwd, environment references, readiness, restart / backoff, checkpoint/resume, dependencies, and intentional-stop state.

Two axes are kept strictly disjoint:

  • DESIRED state ("desired-running" / "desired-stopped") is intent — what the operator or a reconciler wants. It has exactly two values.
  • OBSERVED state (starting / ready / degraded / failed / fenced / stopped / unknown) is what a supervisor last saw. No string is shared between the axes, so a reader can never conflate intent with observation.

Recurring jobs (#749) versus always-on services: a KindService with desired=running is a steady-state obligation — ANY exit (even clean) leaves the desire unmet and the supervisor restarts it. A KindJob is a bounded run whose SCHEDULE owns recurrence: a clean exit is completion (never restarted here); only a non-clean exit is retried under the same backoff contract.

The leaf is pure and portable: no SCM, systemd, launchd, or cron calls — platform supervisors (#4750, #4756) render from and report into this contract. Secrets are referenced (EnvRef.SecretRef), never serialized.

Index

Constants

View Source
const (
	DefaultInitialBackoffMS  int64 = 1000
	DefaultMaxBackoffMS      int64 = 60000
	DefaultBackoffFactor     int64 = 2
	DefaultWindowMS          int64 = 600000
	DefaultWindowMaxRestarts int   = 5
	DefaultStableRunResetMS  int64 = 300000
)

Restart-policy defaults, filled by Normalize when a spec omits them.

View Source
const (
	ReasonDesiredStopped = "desired-stopped"
	ReasonOperatorStop   = "operator-stop"
	ReasonJobComplete    = "job-complete"
	ReasonCircuitOpen    = "circuit-open"
	ReasonBootRecovery   = "boot-recovery"
	ReasonRestart        = "restart"
)

Decision reasons — the closed vocabulary a supervisor logs with each restart decision.

View Source
const ObservedSchemaV1 = "fak.service.observed.v1"

ObservedSchemaV1 names the observed-state schema a supervisor reports.

View Source
const SchemaV1 = "fak.service.v1"

SchemaV1 names the versioned desired-state schema this package parses.

Variables

AllExitClasses enumerates the exit vocabulary (stable order).

AllPhases enumerates the observed vocabulary (stable order).

Functions

func CanTransition

func CanTransition(from, to Phase) bool

CanTransition reports whether an observed-state transition is legal. Every phase may decay to unknown (observation loss is always possible); ready is only reachable through starting (or a re-observation from unknown), never directly from stopped or fenced.

Types

type DesiredState

type DesiredState string

DesiredState is the intent axis. Exactly two values: a service is wanted running or wanted stopped. Everything else is observation, not desire. The wire strings carry the "desired-" prefix (the issue's own vocabulary) so no desired value ever shares a string with an observed Phase — a log line or a JSON field can never be misread across the axes.

const (
	DesiredRunning DesiredState = "desired-running"
	DesiredStopped DesiredState = "desired-stopped"
)

type EnvRef

type EnvRef struct {
	Name      string `json:"name"`
	Value     string `json:"value,omitempty"`
	SecretRef string `json:"secret_ref,omitempty"`
}

EnvRef is one environment binding. A ref carries EITHER a literal value OR an opaque secret reference — never both, so a resolved secret can never be serialized next to its reference. The contract never carries secret bytes.

type ExitClass

type ExitClass string

ExitClass classifies why a run ended. Every restart decision starts from one of these; no exit is left unclassified.

const (
	ExitClean          ExitClass = "clean"            // process exited with success
	ExitCrash          ExitClass = "crash"            // nonzero exit, signal, or panic
	ExitWatchdog       ExitClass = "watchdog-timeout" // liveness/readiness deadline missed
	ExitBootRecovery   ExitClass = "boot-recovery"    // host rebooted; the service did not fail
	ExitDependencyLoss ExitClass = "dependency-loss"  // a depends_on service left ready
	ExitOperatorStop   ExitClass = "operator-stop"    // a human or control plane stopped it on purpose
)

type ExitRecord

type ExitRecord struct {
	Class    ExitClass `json:"class"`
	Code     int       `json:"code,omitempty"`
	AtUnixMS int64     `json:"at_unix_ms,omitempty"`
	RunMS    int64     `json:"run_ms,omitempty"`
}

ExitRecord classifies the most recent run's end.

type Identity

type Identity struct {
	Node     string `json:"node"`
	Service  string `json:"service"`
	Workload string `json:"workload,omitempty"`
}

Identity gives the service, the node it is bound to, and the concrete workload instance stable names. Node and Service are required; Workload defaults to Service (a single-instance service is its own workload).

type Kind

type Kind string

Kind separates always-on services from recurring jobs (#749).

const (
	// KindService is an always-on service: desired=running is a steady-state
	// obligation, so any exit — clean included — is restarted.
	KindService Kind = "service"
	// KindJob is a bounded recurring run: the schedule owns recurrence, a
	// clean exit is completion, only failures are retried.
	KindJob Kind = "job"
)

type Observed

type Observed struct {
	Schema   string      `json:"schema"`
	Identity Identity    `json:"identity"`
	Phase    Phase       `json:"phase"`
	Attempt  int         `json:"attempt,omitempty"`
	LastExit *ExitRecord `json:"last_exit,omitempty"`
}

Observed is the fak.service.observed.v1 report a supervisor emits. It never carries intent — Desired lives only in Spec.

type Phase

type Phase string

Phase is the observed axis — what the supervisor last saw, never what is wanted.

const (
	PhaseStarting Phase = "starting" // launched, readiness not yet met
	PhaseReady    Phase = "ready"    // readiness met
	PhaseDegraded Phase = "degraded" // readiness previously met, now impaired
	PhaseFailed   Phase = "failed"   // exited or unhealthy, restart pending
	PhaseFenced   Phase = "fenced"   // held off: circuit open or operator fence
	PhaseStopped  Phase = "stopped"  // intentionally not running
	PhaseUnknown  Phase = "unknown"  // observation lost
)

type Readiness

type Readiness struct {
	Kind      string `json:"kind"`
	Target    string `json:"target,omitempty"`
	TimeoutMS int64  `json:"timeout_ms,omitempty"`
}

Readiness declares how a supervisor decides PhaseStarting -> PhaseReady. Kind and Target are opaque to this leaf (platform supervisors interpret them); the contract only fixes their shape and the deadline.

type RestartDecision

type RestartDecision struct {
	Restart     bool   `json:"restart"`
	DelayMS     int64  `json:"delay_ms"`
	CircuitOpen bool   `json:"circuit_open"`
	NextAttempt int    `json:"next_attempt"`
	Reason      string `json:"reason"`
}

RestartDecision is the portable verdict: restart or not, after what delay, and whether the circuit is now open (PhaseFenced until an operator or a window expiry releases it).

type RestartInput

type RestartInput struct {
	Kind    Kind
	Desired DesiredState
	Class   ExitClass
	// Attempt is the consecutive restart count since the last stable run.
	Attempt int
	// RunMS is how long the exited run lasted.
	RunMS int64
	// WindowCount is how many restarts already happened inside the rolling
	// restart window.
	WindowCount int
}

RestartInput is what a supervisor knows when a run ends.

type RestartPolicy

type RestartPolicy struct {
	InitialBackoffMS  int64 `json:"initial_backoff_ms"`
	MaxBackoffMS      int64 `json:"max_backoff_ms"`
	BackoffFactor     int64 `json:"backoff_factor"`
	WindowMS          int64 `json:"window_ms"`
	WindowMaxRestarts int   `json:"window_max_restarts"`
	StableRunResetMS  int64 `json:"stable_run_reset_ms"`
}

RestartPolicy bounds how a supervisor re-runs an exited workload: bounded exponential backoff (initial * factor^attempt, capped at max), a rolling restart-window cap that opens the circuit, and a stable-run threshold that resets the attempt counter. All durations are integer milliseconds so the wire form is deterministic on every platform.

func (RestartPolicy) Decide

Decide applies the v1 restart semantics, in priority order:

  1. desired=stopped never restarts (intent wins).
  2. operator-stop never restarts even under desired=running: an intentional stop is recorded, not fought.
  3. a KindJob clean exit is completion — the schedule owns the next run.
  4. the rolling restart-window cap opens the circuit (no restart, fenced).
  5. boot-recovery restarts immediately with a fresh attempt counter: the HOST failed, not the service, so prior backoff does not carry over.
  6. a run that outlived stable_run_reset_ms resets the attempt counter.
  7. otherwise restart after bounded exponential backoff: min(initial * factor^attempt, max).

type Spec

type Spec struct {
	Schema        string        `json:"schema"`
	Identity      Identity      `json:"identity"`
	Kind          Kind          `json:"kind"`
	Desired       DesiredState  `json:"desired"`
	Command       []string      `json:"command"`
	Dir           string        `json:"dir,omitempty"`
	Env           []EnvRef      `json:"env,omitempty"`
	Readiness     *Readiness    `json:"readiness,omitempty"`
	Restart       RestartPolicy `json:"restart"`
	CheckpointDir string        `json:"checkpoint_dir,omitempty"`
	DependsOn     []string      `json:"depends_on,omitempty"`
}

Spec is the fak.service.v1 desired-state document.

func ParseSpec

func ParseSpec(data []byte) (*Spec, error)

ParseSpec strictly decodes, validates, and normalizes one fak.service.v1 document. Unknown fields are refused (a typo'd field must never silently drop intent). The returned spec is normalized: defaults filled, env sorted by name, depends_on sorted and deduplicated.

func (*Spec) CanonicalJSON

func (s *Spec) CanonicalJSON() ([]byte, error)

CanonicalJSON returns the deterministic wire form of the normalized spec: fixed field order, sorted lists, integer durations. Parsing the canonical form and re-canonicalizing is the identity.

func (*Spec) Normalize

func (s *Spec) Normalize()

Normalize fills portable defaults and puts list fields in canonical order so CanonicalJSON is deterministic. Safe to call repeatedly (idempotent).

func (*Spec) Validate

func (s *Spec) Validate() error

Validate checks the normalized spec against the v1 contract.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL