runner

package
v1.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package runner owns the daemon-side process supervision: spawning targets in either go-exec or container mode, tracking each instance's lifecycle (phase / exit-reason / health), and enforcing the lifecycle invariants (mutual exclusion, dependency-walk gating, idempotency).

The package exposes one type — Runner — and a set of typed methods. The daemon's server package calls into it from the RPC handlers.

Internal state is guarded by a single sync.RWMutex (Runner.mu). For local dev with a handful of targets the contention is negligible and the reasoning is simple (last-write-wins per target).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type InfraSessionRef

type InfraSessionRef struct {
	Stack   string
	Service string
	Up      bool // false if the session was registered but never reached Up
}

InfraSessionRef identifies one tracked infra session (its stack + service pair). Used by callers that need to iterate every active session — e.g., Shutdown(force=true) which tears down every service's infra in one go.

type InfraState

type InfraState struct {
	ContainerID string
	Phase       anovelv1.Phase
	Health      anovelv1.Health
}

InfraState is one row in the batched podman scan: container ID + translated phase + translated health for a single (stack, service, infraName) tuple.

type Instance

type Instance struct {
	// Identity. Set at construction, immutable thereafter.
	ID      string // <stack>/<service>/<target>
	Target  string // bare target name (e.g., "rest")
	Service string
	Stack   string

	// Lifecycle. Mutated by the runner; readers must hold Runner.mu (RLock).
	Phase        anovelv1.Phase
	ExitReason   anovelv1.ExitReason
	Health       anovelv1.Health
	Mode         Mode
	PID          int32
	ContainerID  string
	StartedAt    time.Time
	TerminatedAt time.Time
	// LastErr captures the most recent non-fatal error observed by the
	// supervisor (e.g., spawn failure, wait error). Surfaced via the
	// LastError method.
	LastErr string
	// contains filtered or unexported fields
}

Instance is one running (or recently-terminated) target. The runner owns every Instance; the server reads them through Runner methods.

type Mode

type Mode int

Mode is how a target instance was spawned. Mirrors anovelv1.Mode but keeps the runner package independent of the proto types.

const (
	ModeGoExec    Mode = 1
	ModeContainer Mode = 2
)

func (Mode) String

func (m Mode) String() string

type PhaseEvent

type PhaseEvent struct {
	TargetID string
	Service  string
	Stack    string
	OldPhase anovelv1.Phase
	NewPhase anovelv1.Phase
	// ExitReason is meaningful only when NewPhase == PHASE_TERMINATED;
	// zero otherwise. Tracked separately from Phase so a subscriber that
	// cares about success-vs-error doesn't have to re-query the runner.
	ExitReason anovelv1.ExitReason
	Ts         time.Time
}

PhaseEvent is the minimum metadata Watch subscribers need to render a state-change line: who transitioned, from what to what, when. The runner emits one for every phase change AND for markTerminated (which carries the exit reason in NewPhase=TERMINATED + a derived OldPhase=RUNNING).

Service / Stack are duplicated from the Instance so subscribers don't have to look the target up — important because by the time the subscriber drains the channel, the daemon may have torn down the instance record.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner supervises every spawned target across every stack.

func New

func New(disc []*discovery.Stack, alloc *env.Allocator, builder *env.Builder, logStore *logs.Store) *Runner

New returns an empty Runner with the discovery snapshot it'll resolve target IDs against, the env allocator for refcount release on termination, the env builder for one-shot env synthesis, and the log store for capturing target stdout/stderr.

func (*Runner) ActiveInfraSessions

func (r *Runner) ActiveInfraSessions() []InfraSessionRef

ActiveInfraSessions returns one InfraSessionRef per currently-tracked infra session. Both Up and not-yet-Up sessions are included; the caller can filter on Ref.Up if only fully-running sessions matter.

func (*Runner) AdoptOrphanContainers

func (r *Runner) AdoptOrphanContainers(ctx context.Context) (int, int)

AdoptOrphanContainers scans podman for containers carrying our adoption labels (anovel.stack / anovel.service / anovel.target) and reconstitutes runner.Instance records for every one. Called at daemon startup so a `core kill` + `core start` cycle doesn't lose container-mode supervision.

Per-service infra sessions are also re-flagged Up so the dep-walker doesn't try to spin infra again when a target start follows adoption. One-shot results aren't restored (no signal for "did this succeed in the session that's now gone") — they'll re-run on the next infra-start cycle, since idempotent one-shots run on every infra-up.

Returns (adopted-containers, adopted-targets) for reporting.

func (*Runner) AllInstances

func (r *Runner) AllInstances() []Instance

AllInstances returns a snapshot of every instance the runner knows about.

func (*Runner) EnsureDepsReady

func (r *Runner) EnsureDepsReady(ctx context.Context, t *discovery.Target, svc *discovery.Service, oneShotsMode Mode, env []string) error

EnsureDepsReady walks `t`'s compose `depends_on` chain and verifies every upstream dependency is ready before allowing the target to start. A dependency counts as ready when:

(phase=running AND (mode=go-exec OR health=healthy))
  OR (phase=terminated AND exitReason=success)

Behavior by dep kind:

  • infra: ensures the service's infra session is Up. Auto-triggers StartInfra if it isn't (cascade).
  • one-shot target: ensures the current infra session recorded success for it. Auto-triggers via the infra session if the session is Up but the one-shot hasn't run yet. Refuses if the previous run failed (caller must re-start infra or fix the underlying issue).
  • long-runner target: must already be running. We DO NOT auto-start long-runners — that's an explicit user action.

func (*Runner) InfraSession

func (r *Runner) InfraSession(stack, service string) (infraSession, bool)

InfraSession returns a snapshot of the per-service infra session, or (zero, false) if no session is recorded yet.

func (*Runner) InfraStateOf

func (r *Runner) InfraStateOf(ctx context.Context, stack, service, infraName string) (anovelv1.Phase, anovelv1.Health, string)

InfraStateOf is the legacy single-infra query — preserved for callers that don't have the batched map yet. Returns (phase, health, containerID). Internally uses InfraStatesOf for consistency.

func (*Runner) InfraStatesOf

func (r *Runner) InfraStatesOf(ctx context.Context, stack string) map[string]InfraState

InfraStatesOf returns the live state of every infra container in the given stack, keyed by "<service>/<infraName>". One podman call total regardless of how many services / infras the stack has — far faster than a per-infra scan (each podman invocation has ~1s cold-start overhead on rootless WSL2 podman, so N infras serial = N seconds, often exceeding the TUI's 2s poll cadence).

Health: deliberately NOT fetched here. podman ps --format json doesn't expose Health.Status, and a per-container `podman inspect` would double the call count. PHASE_RUNNING + HEALTH_UNSPECIFIED renders correctly downstream (green ● in TUI, "running" in CLI ps). Callers needing precise health for a single infra can fall back to InfraStateOf (which does the inspect).

Returns an empty map (not nil) on podman errors so callers can uniformly check `if state, ok := m[key]` without nil-guarding.

func (*Runner) Instance

func (r *Runner) Instance(id string) (Instance, bool)

Instance returns the live record for id, or false if no instance is known for it. Caller does not need to hold the lock — the method copies the relevant fields before returning so the caller has a stable snapshot.

func (*Runner) InvalidateInfraStateCache

func (r *Runner) InvalidateInfraStateCache()

InvalidateInfraStateCache drops cached InfraStatesOf entries — call after any user-initiated state-change (KillInfra, StartInfra, KillInfraContainer, RestartInfraContainer) so the next ListServices reflects reality instantly. Cheap; safe to call when nothing is cached.

func (*Runner) Kill

func (r *Runner) Kill(ctx context.Context, id string, grace time.Duration) error

Kill stops the named instance. Idempotent: killing an already-terminated target returns nil. Sends SIGTERM, waits up to `grace`, then SIGKILL.

`grace` of 0 means "SIGKILL immediately" (no grace period).

func (*Runner) KillInfra

func (r *Runner) KillInfra(ctx context.Context, stack, service string, force bool) error

KillInfra refuses if any long-runner target of the service is still running (unless force). Otherwise tears down all infra containers and resets the session.

func (*Runner) KillInfraContainer

func (r *Runner) KillInfraContainer(ctx context.Context, stack, service, infraName string) error

KillInfraContainer stops a single infra container by (stack, service, infraName). Uses podman stop with a 10s grace; the container survives in Exited state so the user can restart it without losing its pod / network attachments. Returns an error if no container matches (already down, or never up).

func (*Runner) RestartInfraContainer

func (r *Runner) RestartInfraContainer(ctx context.Context, stack, service, infraName string) error

RestartInfraContainer restarts a single infra container in place (`podman restart`, which is stop+start without recreating). Faster than KillInfra → StartInfra because it skips compose-up entirely and preserves the container's existing volume bindings / labels.

func (*Runner) StartContainer

func (r *Runner) StartContainer(ctx context.Context, id string, env []string, warnings []string) (*Instance, error)

StartContainer brings up `id` as a podman-compose container in the target's declared profile, labeling it for adoption and passing the synthesized env so compose's ${VAR} port substitution resolves. Uses --no-deps to sidestep podman-compose 1.5.0's broken depends_on wait machinery.

func (*Runner) StartGoExec

func (r *Runner) StartGoExec(ctx context.Context, id string, env []string, warnings []string) (*Instance, error)

StartGoExec spawns target `id` as a `go run ./cmd/<target>` invocation inside the owning service's directory, with `env` as the process environment (callers — typically the env-synthesis layer in phase 4 — build this list).

Returns the resulting Instance. Already-running idempotency, mutual exclusion vs container mode, and the PHASE_PENDING → PHASE_STARTING → PHASE_RUNNING transitions are handled here so callers don't reimplement them per RPC.

func (*Runner) StartInfra

func (r *Runner) StartInfra(ctx context.Context, stack, service string, oneShotsMode Mode, extraEnv []string) error

StartInfra brings up a service's infrastructure containers, waits for healthchecks, then auto-runs every one-shot target in compose-dep order. Idempotent — calling on an already-up service is a no-op.

`oneShotsMode` controls the mode of auto-run one-shots (defaults to go-exec). `extraEnv` is the daemon's inherited env (PATH/HOME etc.) layered ON TOP of synthesized port allocations. The runner ALLOCATES infra ports itself (via env.Builder.ForServiceUp) so compose's `${POSTGRES_PORT}` substitutes to a real number even when called via the dep-walk path (where the caller can't pre-allocate).

func (*Runner) SubscribePhases

func (r *Runner) SubscribePhases(filter func(PhaseEvent) bool) (<-chan PhaseEvent, func())

SubscribePhases registers a channel-based subscriber for phase events. Returns the channel to read from + an unsubscribe func the caller must invoke on exit (typically deferred). `filter` is optional — pass nil to receive every event.

Buffer of 32 picked so a brief consumer stall (rendering a ps table, say) doesn't drop events under normal load; sustained slowness drops silently (liveness > completeness for the runner).

Jump to

Keyboard shortcuts

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