Documentation
¶
Overview ¶
internal/runnerd/agent.go
internal/runnerd/registry.go
internal/runnerd/runnerd.go
Index ¶
- type AgentConfig
- type Server
- func (s *Server) Announce() []runner.SessionInfo
- func (s *Server) CreateWithID(ctx context.Context, id string, spec driver.Spec, allow []string) error
- func (s *Server) Delete(ctx context.Context, id string) error
- func (s *Server) Handler() http.Handler
- func (s *Server) Op(ctx context.Context, id, op string, warm bool) error
- func (s *Server) OpSnapshot(ctx context.Context, id, ref string) (string, error)
- func (s *Server) Recover(ctx context.Context) error
- func (s *Server) RemoveWorkspace(ctx context.Context, id string) error
- func (s *Server) RunAgent(ctx context.Context, cfg AgentConfig) error
- func (s *Server) SetOnEvent(f func(sessionID, state, detail string))
- func (s *Server) SetOnSessionRPC(f func(sessionID string, env runner.RPCEnvelope))
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AgentConfig ¶
type AgentConfig struct {
ControldURL string // e.g. ws://host:9090 — /v0/runners/connect appended
Token string
RunnerName string
ProxyURL string // forwarded into every driver.Spec (egress R4)
// Capabilities are the portable capabilities this runner announces —
// claims about itself and nothing else (a GPU it has, a rootless docker
// it runs). They are passed through verbatim: controld validates them
// and answers with the set it accepted. Empty announces none.
Capabilities []string
}
AgentConfig configures RunAgent's outbound dial to controld.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
func (*Server) Announce ¶
func (s *Server) Announce() []runner.SessionInfo
Announce snapshots the registry in runner's session-state vocabulary, for the agent's announce message (and reconnect re-announces).
"starting" entries are skipped — they're mid-CreateWithID, and that call's own result (a "create" command's result, or the id simply appearing once it finishes) speaks for them once they land. Omitting "starting" is safe specifically because Postgres shows that session as "creating" on controld's side, and controld's reconciliation requeues a "creating" session it doesn't see in an announce idempotently — a later announce, or a later create result, is enough to correct it either way.
"suspending" (mid-cold-suspend — drv.Suspend's `docker stop` is in flight, hub not yet confirmed dead) is NOT skipped, unlike "starting" — review round 2's finding: omitting it creates an unhealable race. If the controld connection reconnects while a cold suspend is mid-flight, omitting that session from the announce reads to controld's reconciliation as "in Postgres but absent from announce", which marks it dead — a TERMINAL state that no later announce can revive, for a session that is (or will shortly be) perfectly healthy. So "suspending" reports suspended_cold immediately instead: if the stop then fails and Op's suspend case rolls the entry back to "running", the very next announce simply reports "running" and controld adopts it. Every non-terminal state heals itself on the next announce; only omission does not — so nothing runnerd doesn't know for certain is dead, dead, or gone should ever be omitted here.
func (*Server) CreateWithID ¶
func (s *Server) CreateWithID(ctx context.Context, id string, spec driver.Spec, allow []string) error
CreateWithID runs a session's full creation sequence under a caller-chosen id: atomically claim the id, push its egress rules, create it via the driver, then land the handle and "running" state. Both fronts (POST /sessions' HTTP handler and the agent's execute) drive this same sequence — id minting (POST /sessions mints one via newID; the agent uses controld's session id verbatim) and status-code/JSON mapping are each caller's own job, not this one's.
The id claim (reg.putIfAbsent) is a single locked call and runs FIRST, before anything else — this is what makes create idempotent-by-id under real concurrency (review round 1, finding 2, design §4.8's binding requirement). Before this fix, the caller (the agent's execute) checked existence via a separate reg.get, then this function did its own reg.put — two distinct lock acquisitions with a window between them where two racing create commands for the same controld-supplied id (e.g. a retried create the sender is unsure landed) could both observe "absent" and both reach drv.Create: `docker run` has no name-uniqueness guard, so that produced a duplicate, orphaned container. Now there is exactly one lock acquisition deciding "is this id claimed", so at most one caller ever gets past it; every other one gets errSessionExists back immediately, before touching egress or the driver at all — the caller (agent's execute, or in principle the HTTP handler) treats that the same as a successful create, since the desired state (a session exists under this id) is already reached either way.
This does mean pushEgress now runs AFTER the entry is claimed, not before as the pre-Task-6 handler had it — a required reordering, not incidental: the claim has to be the very first thing that can fail nothing else can jump the queue on. A pushEgress failure now rolls the claim back (reg.remove) so the id isn't left stuck "starting" forever.
spec's SessionID/DialURL/ProxyURL are set here, not by the caller: they're this server's own concerns (the id parameter, s.dialBase, s.proxyURL), not anything a caller — HTTP body or runner.Spec — should be trusted to supply.
func (*Server) Delete ¶
Delete tears down a session: close its hub (if it ever registered) before removing the registry entry and destroying the driver resource, then remove the entry. hub.Close() cancels its ctx, which is what unblocks register()'s own `<-hub.Done()` wait so that goroutine cleans up synchronously with this deliberate teardown instead of being left to find out later. register() also calls reg.remove/hub.Close on its own unblock — both are safe, idempotent no-ops the second time.
The entry is marked "destroying" BEFORE hub.Close(), exactly as Op's cold suspend marks "suspending" before `docker stop` and for the same reason: the hub death this is about to cause is indistinguishable at the socket level from a crash, so the register goroutine it unblocks would otherwise run its crash path — Inspect the (already destroyed) container, find it gone, and both destroy it a second time and fire a spurious "dead" event while this Delete is still between its hub.Close() and its reg.remove(). controld would then mark the session dead instead of destroyed. The marker makes that goroutine stand down (see register's hub-death tail).
func (*Server) Op ¶
Op runs suspend/resume against a session's current driver handle. Both fronts (sessionOp's HTTP handler and the agent's execute) drive this same sequence; warm is pre-parsed by the caller (HTTP's `?warm=` query, or the agent's runner.ToRunner.Warm) since query-string parsing is an HTTP concern this function has no business knowing about. Snapshot has its own entry point — see OpSnapshot.
func (*Server) OpSnapshot ¶
OpSnapshot commits a session's current filesystem as an image and returns the resulting ref.
It is separate from Op rather than another case inside it because ref is a snapshot-only concern: threading it through Op would put an image tag in suspend's and resume's signature, where it means nothing and every caller would pass "". Both fronts drive this one function — the HTTP dev surface with an empty ref (it has no environment to name, so the driver mints the tag) and the agent with controld's content-addressed rainier-env: ref.
func (*Server) Recover ¶
Recover rebuilds the in-memory registry from the driver's labeled containers, so a restarted runnerd is truthful about sessions that outlived it instead of forgetting them outright. Hubs stay nil until each sessiond redials /register — with the entry already present, that redial finds its session instead of 404ing; actually driving sessiond to redial on reconnect is Task 5's job, not this one.
Recovered entries carry allow: nil — egress rules were pushed to egressd (a separate process) at create time and it still holds them, so there is nothing to re-push here. If egressd itself restarts and loses its rules, that's a separate, already-ledgered known gap: v0 has no reconciliation between the two processes' restarts.
func (*Server) RemoveWorkspace ¶
RemoveWorkspace reclaims a session's workspace volume: the second act of an explicit teardown, run when the crash path deliberately kept it and the user has now asked for the session to be gone for good. controld dispatches it as a "remove_workspace" command.
It refuses while the registry still holds the session. remove_workspace names a session rather than a container precisely because it is meant to run when no container is left, so a command arriving for a LIVE one is a misrouted or stale message — and honoring it would erase a running agent's working tree. Docker itself would refuse (the volume is in use by a container), which makes this guard belt-and-braces against the real driver; against the fake it is the belt, and it is what keeps the two agreeing.
func (*Server) RunAgent ¶
func (s *Server) RunAgent(ctx context.Context, cfg AgentConfig) error
RunAgent dials controld and serves its commands until ctx is canceled, redialing with jittered backoff (1s..30s cap) whenever the connection ends. It only returns once ctx is done — any other agentSession error is logged and retried, since a runner with no control conn is still useful on its local HTTP surface but should keep trying to phone home.
func (*Server) SetOnEvent ¶
SetOnEvent installs f as the session-event callback (nil clears it). Synchronized against fireEvent via atomic.Pointer so register()'s and the hub-death path's request goroutines can call fireEvent concurrently with agentSession swapping the callback in on every (re)connect and clearing it via defer on exit — see the onEvent field's doc comment.
func (*Server) SetOnSessionRPC ¶
func (s *Server) SetOnSessionRPC(f func(sessionID string, env runner.RPCEnvelope))
SetOnSessionRPC installs f as the sink for session-RPC envelopes coming up out of a sandbox (nil clears it). The agent installs one per controld connection; see the onSessionRPC field.