Supervisor
The supervisor is the core engine of Runix. It manages the full lifecycle of processes — from creation through execution, monitoring, restart, and shutdown.
Located at internal/supervisor/, the supervisor consists of these files:
| File | Responsibility |
|---|---|
supervisor.go | Supervisor struct — the top-level orchestrator |
process.go | Process struct — wraps an exec.Cmd with state tracking, hooks, and log capture |
state.go | State machine logic, valid transition checking |
backoff.go | Exponential backoff calculator for restart delays |
monitor.go | Exit monitor — goroutines that wait on process exit and trigger restart |
dependencies.go | Topological sort for process dependency ordering |
rolling.go | Rolling reload logic — batched concurrent restart with rollback |
Supervisor Struct
type Supervisor struct {
processes map[string]*Process // ID → Process
config *types.RunixConfig
options Options
// ...
}
type Options struct {
LogDir string // Base log directory
Defaults types.DefaultsConfig // Default config values
}
Key Methods
| Method | Description |
|---|---|
New(opts Options) *Supervisor | Creates a new supervisor |
AddProcess(ctx, cfg) (*Process, error) | Adds and starts a process |
StopProcess(id, force, timeout) error | Stops a process |
RestartProcess(ctx, id) error | Stops then starts a process |
ReloadProcess(ctx, id) error | Graceful restart preserving config |
RemoveProcess(id) error | Stops and removes from process table |
Get(target) (*Process, error) | Lookup by ID, name, or unique prefix |
List() []ProcessInfo | Returns info for all processes |
RollingReload(ctx, names, opts) error | Batched concurrent reload |
Save() error | Persist process list to dump.json |
Resurrect() error | Restore processes from dump.json |
Shutdown() | Stop all processes and clean up |
LogPath(name) string | Returns stdout log path for a process |
LogPathStderr(name) string | Returns stderr log path for a process |
Process Lifecycle
Exit Monitor
Each running process has a dedicated goroutine that waits on cmd.Wait(). When the process exits:
- The goroutine reads the exit code
- It calls
handleExit()which atomically transitions the state - If the restart policy allows, it schedules a restart with exponential backoff
- The
exitedchannel is closed exactly once — other goroutines use this to detect termination
func (p *Process) watchExit() {
err := p.cmd.Wait()
// Extract exit code
p.handleExit(exitCode)
}
The exited channel is created fresh on each Start() call and closed exactly once by handleExit(). This prevents double-close panics.
Process Group Isolation
All child processes are started with SysProcAttr{Setpgid: true}, which places them in their own process group. This means:
- Signals are sent to the entire group via
syscall.Kill(-pid, signal) - Child processes spawned by the managed process are also terminated
- Prevents zombie processes from orphaned children
Log Capture
Each process has stdout and stderr captured to separate log files:
~/.runix/apps/<name>/stdout.log
~/.runix/apps/<name>/stderr.log
Log files are written through a PrefixWriter that prepends timestamps:
2025-01-15 10:30:00 [out] Server listening on :8080
2025-01-15 10:30:01 [err] Connection refused
Log rotation is handled by internal/logrot/rotator.go when configured.
Dependency Resolution
When starting multiple processes (e.g., from a config file), the supervisor resolves dependencies using topological sort:
- Processes with
depends_on: ["db", "cache"]wait for those processes to be running first - The sort uses priority as a tiebreaker — higher priority processes start first
- Circular dependencies are detected and rejected at config validation time
See Dependencies for the full feature documentation.
Rolling Reload
The supervisor supports rolling reload for zero-downtime updates:
type RollingReloadOptions struct {
BatchSize int // Concurrent reloads per batch
WaitReady bool // Wait for health checks between batches
ReadyTimeout string // Timeout for health check readiness
RollbackOnFailure bool // Stop and revert on first failure
}
See Rolling Reload for the full feature documentation.
Thread Safety
The supervisor uses fine-grained locking:
- Process state: Lock-free via
atomic.Value+CompareAndSwap - Process map:
sync.RWMutexfor adding/removing processes - Log writers: Mutex-protected
PrefixWriter - Metrics collector:
sync.RWMutexfor the metrics map
The hot path — state transitions — never acquires a mutex. This is critical because state transitions happen on every process exit and health check cycle.
What's Next
- Process State Machine — The 7-state machine in detail
- Daemon — How the supervisor runs inside the daemon
- Restart Policies — Configurable restart behavior and backoff