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; the runner in turn is split across files by concern:
runner.go — Runner type, tracker, lifecycle invariants goexec.go — go-exec spawn/wait/kill plumbing container.go — container mode (added in a follow-up chunk)
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 ¶
- type InfraSessionRef
- type InfraState
- type Instance
- type Mode
- type PhaseEvent
- type Runner
- func (r *Runner) ActiveInfraSessions() []InfraSessionRef
- func (r *Runner) AdoptOrphanContainers(ctx context.Context) (int, int)
- func (r *Runner) AllInstances() []Instance
- func (r *Runner) EnsureDepsReady(ctx context.Context, t *discovery.Target, svc *discovery.Service, ...) error
- func (r *Runner) InfraSession(stack, service string) (infraSession, bool)
- func (r *Runner) InfraStateOf(ctx context.Context, stack, service, infraName string) (anovelv1.Phase, anovelv1.Health, string)
- func (r *Runner) InfraStatesOf(ctx context.Context, stack string) map[string]InfraState
- func (r *Runner) Instance(id string) (Instance, bool)
- func (r *Runner) InvalidateInfraStateCache()
- func (r *Runner) Kill(ctx context.Context, id string, grace time.Duration) error
- func (r *Runner) KillInfra(ctx context.Context, stack, service string, force bool) error
- func (r *Runner) KillInfraContainer(ctx context.Context, stack, service, infraName string) error
- func (r *Runner) RestartInfraContainer(ctx context.Context, stack, service, infraName string) error
- func (r *Runner) StartContainer(ctx context.Context, id string, env []string) (*Instance, error)
- func (r *Runner) StartGoExec(ctx context.Context, id string, env []string) (*Instance, error)
- func (r *Runner) StartInfra(ctx context.Context, stack, service string, oneShotsMode Mode, ...) error
- func (r *Runner) SubscribePhases(filter func(PhaseEvent) bool) (<-chan PhaseEvent, func())
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 ¶
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.
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 ¶
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. Spec §3.4 step 3.
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 ¶
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. Spec §5.4 "ready" definition:
(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 ¶
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 ¶
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 — drastically faster than the prior per-infra approach (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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
StartContainer brings up `id` as a podman-compose container in the target's declared profile, with: --no-deps to avoid the broken depends_on wait machinery in podman-compose 1.5.0+, --label injection so adoption works on the next daemon start, and env passthrough so compose's ${VAR} substitution resolves to our allocated ports.
Phase 3 chunk 2 first cut. The actual log streaming is deferred to phase 5 (logs hub); for now container stdout/stderr stay in podman's own journal.
func (*Runner) StartGoExec ¶
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).