nodeagent

package
v0.6.4 Latest Latest
Warning

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

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

Documentation

Overview

Package nodeagent is the Mercator node agent: the process that runs on a reusable machine and executes successive workloads there.

It connects outbound and never listens. It keeps enough local state to refuse a command it already applied, which is what stops a redelivered launch from starting a second container. It spools the facts it owes the control plane while disconnected, so a container that exited during a network partition is still reported rather than inferred.

Index

Constants

View Source
const ArtifactCopySource = "node_artifact_copy"

ArtifactCopySource names what a node measured when it timed itself reading Artifact content, in the words of the process that did it. It is the whole copy: the bytes crossing the path, landing on this disk, and being hashed on the way past, because one read does all three jobs here and the node holds one duration over all of them.

The name says so rather than claiming the link alone, and the difference is not pedantry. A machine on a ten gigabit path whose Artifact root is a slow disk delivers content at the disk's rate, and that rate is the honest answer to both questions anything asks this fact: how long the next read of forty gigabytes takes here, and whether a Run that states a floor on reading its dataset can be served by this machine. Timing the socket alone would answer neither, and would be wrong in the direction that wins placements the machine cannot honour.

View Source
const MeasuredLinkConfidence = 0.9

MeasuredLinkConfidence is how much a node stands behind its own reading of a path. It is more than domain.AssumedLinkConfidence, which is what Mercator's fleet-wide guess about an unmeasured path is worth, and deliberately short of certainty: what is published is the slowest transfer this node has seen, which is a floor over a handful of readings rather than a distribution.

The figure itself is stated rather than derived, and that is the honest description of it. Deriving it from the sample count would be an estimator this slice has not measured, and predicted-versus-actual for the fetch stage is what a calibration slice will replace it with.

View Source
const MeasuredLinkValidity = time.Hour

MeasuredLinkValidity is how long one reading of a path stands. A measurement is evidence about the path as it was, and a node that has moved nothing for an hour is not evidence about the path now: it keeps reporting its liveness, its containers, and its disk, so without an expiry its oldest reading would be published as a current fact forever.

It is the life of one reading, and every reading serves it alone. That is what retires the slow one: the machine that read a hundred megabits while a container shared its link stops standing behind that reading an hour later however busy it has been since, because no later transfer can extend a window it was not measured in.

Variables

This section is empty.

Functions

This section is empty.

Types

type Agent

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

Agent runs one machine's side of the node protocol.

func New

func New(identity Identity, runtime Runtime, transport Transport, state *State, opts ...Option) *Agent

func (*Agent) Run

func (agent *Agent) Run(ctx context.Context) error

Run drives the agent until ctx ends. Each pass enrolls if needed, drains the spool, opens a session, and serves commands until the connection drops.

type DockerRuntime

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

DockerRuntime executes workloads through the local Docker daemon. It is the first Runtime implementation, and it stays behind the narrow Runtime interface so containerd or another OCI runtime can replace it without the control-plane contract moving.

The daemon is reached over its local socket by this process only. Nothing exposes it to the network, and the control plane never talks to it.

func NewDockerRuntime

func NewDockerRuntime(binary string, options ...RuntimeOption) *DockerRuntime

func (*DockerRuntime) Facts

func (docker *DockerRuntime) Facts(ctx context.Context) (capability.NodeFacts, error)

func (*DockerRuntime) LaunchWorkload

func (docker *DockerRuntime) LaunchWorkload(ctx context.Context, command capability.LaunchWorkloadCommand) error

LaunchWorkload starts one container and returns once it is running. The container's name is derived from the run and attempt, so the daemon itself refuses a second container for the same attempt even if the agent's own memory were lost.

func (*DockerRuntime) Observe

Observe reports every Mercator container this daemon knows about, running or exited, and when each of them really started. It is the node's own authority: the control plane learns that a process began and ended here, whatever the application did or did not report.

The start moment is a second read, because `docker ps` does not carry it in any format: the daemon reports `State.StartedAt` only on inspect. It is one inspect for the whole list rather than one per container, and a container the daemon will not describe is reported with no start moment rather than with a guess. A moment the daemon does state and this runtime cannot read fails the whole read instead, which is the same answer the Docker adapter gives to the same daemon: reporting it absent would publish no start for any container on this machine and degrade every start-latency row on the only reusable lane there is, with nothing anywhere saying that is what happened.

func (*DockerRuntime) PrepareArtifact

func (docker *DockerRuntime) PrepareArtifact(ctx context.Context, command capability.PrepareArtifactCommand) error

PrepareArtifact replicates one immutable version onto this machine. It reads through the location the control plane minted for this one fetch and nowhere else: the object store's own key stays in the control plane, so a node holds no standing read of anything and a compromised machine can fetch exactly the content it was told to, until the read expires.

A command with no minted read is refused rather than attempted. The durable location the catalog states is a name for content, and a node that went to the network with it would fail in the object store's vocabulary instead of in Mercator's, which is an operator reading a 404 for a missing configuration.

func (*DockerRuntime) PrepareImage

func (docker *DockerRuntime) PrepareImage(ctx context.Context, command capability.PrepareImageCommand) error

PrepareImage pulls an image by its exact manifest digest. A tag is never image identity, so the reference the control plane sends is already pinned.

It goes to the daemon's own API rather than to the CLI, for one reason: the credential. The CLI reads registry material out of a config file, so pulling a private image through it means writing the material onto the machine and remembering to remove it, and a pull that is killed halfway leaves it there. The API takes it as a header, so the material this node was handed exists in this process's memory for the length of one request and nowhere else. That is what "the machine forgets it afterwards" has to mean on a host an operator rents by the hour.

func (*DockerRuntime) StopWorkload

func (docker *DockerRuntime) StopWorkload(ctx context.Context, command capability.StopWorkloadCommand) error

type HTTPTransport

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

HTTPTransport reaches the control plane over the node protocol. Every call is outbound: the agent opens the session and posts what it owes, and nothing ever connects to the machine.

func NewHTTPTransport

func NewHTTPTransport(baseURL string, client *http.Client) *HTTPTransport

func (*HTTPTransport) Enroll

func (*HTTPTransport) RenewSession

func (transport *HTTPTransport) RenewSession(ctx context.Context, nodeID, sessionToken string) (capability.SessionRenewal, error)

RenewSession takes a later credential using the one this agent still holds. It is the only exchange here authenticated by a credential the agent is about to stop using, and it is what keeps a machine reachable past the life of the invitation it joined with.

func (*HTTPTransport) SendEvents

func (transport *HTTPTransport) SendEvents(ctx context.Context, nodeID, sessionToken string, events []node.Event) error

func (*HTTPTransport) SendResult

func (transport *HTTPTransport) SendResult(ctx context.Context, nodeID, sessionToken string, result node.Result) error

func (*HTTPTransport) Session

func (transport *HTTPTransport) Session(ctx context.Context, nodeID, sessionToken string, commands chan<- node.Command) error

Session holds the command stream open until the connection ends or ctx is cancelled. Commands arrive as newline-delimited JSON.

type Identity

type Identity struct {
	ControlPlaneURL string
	NodeID          string
	RentalID        string
	Generation      uint64
	EnrollmentToken string
	AgentVersion    string
}

Identity is the immutable material a machine was bootstrapped with. The agent never invents any of it.

type Option

type Option func(*Agent)

Option configures an Agent.

func WithClock

func WithClock(now func() time.Time) Option

WithClock replaces the wall clock, so a test states time rather than waits.

func WithHeartbeat

func WithHeartbeat(interval time.Duration) Option

WithHeartbeat sets how often the agent reports its facts. It must stay well inside the control plane's lease, or a healthy machine will be declared lost.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger replaces the agent's logger.

func WithReconnectBackoff

func WithReconnectBackoff(backoff time.Duration) Option

WithReconnectBackoff sets how long the agent waits before reopening a dropped session.

type Runtime

type Runtime interface {
	// Facts reports the host, accelerators, driver, runtime, disk, and exact
	// content inventory this machine holds.
	Facts(ctx context.Context) (capability.NodeFacts, error)
	PrepareImage(ctx context.Context, command capability.PrepareImageCommand) error
	PrepareArtifact(ctx context.Context, command capability.PrepareArtifactCommand) error
	// LaunchWorkload starts the container and returns once it is created. The
	// agent observes its lifecycle separately, so a launch that returns does
	// not claim the workload succeeded.
	LaunchWorkload(ctx context.Context, command capability.LaunchWorkloadCommand) error
	StopWorkload(ctx context.Context, command capability.StopWorkloadCommand) error
	// Observe reports every workload this runtime currently knows about,
	// including recently exited ones the agent has not yet reported.
	Observe(ctx context.Context) ([]capability.WorkloadObservation, error)
}

Runtime is the container runtime the agent drives. It is deliberately narrow: Docker is the first implementation, and nothing above this line knows which one is running, so containerd or another OCI runtime can be added without touching the control-plane contract.

type RuntimeOption

type RuntimeOption func(*DockerRuntime)

RuntimeOption configures the Docker runtime.

func WithAcceleratorTool

func WithAcceleratorTool(binary string) RuntimeOption

WithAcceleratorTool points this node at the vendor tool it asks about its own cards, by absolute path where the bare name will not resolve.

It is operator configuration and not only a test seam. A unit file with a trimmed Environment=PATH, or a distribution that installs the tool somewhere the service PATH does not reach, is a machine whose agent cannot find nvidia-smi by name; on an 8xH100 box that is the difference between a fleet that runs GPU work and one that reports it has none. It is the second reason this exists, the first being that a test can stand in a machine with four A100s or no driver at all, which is the only way the reports this node publishes about hardware can be exercised anywhere but on the hardware.

func WithArtifactRoot

func WithArtifactRoot(root string) RuntimeOption

WithArtifactRoot gives this node somewhere to keep immutable Artifact copies. It is the agent's own directory, stated by whoever started the agent, because only they know which filesystem has the room: a default under the daemon's storage would be a directory this process usually cannot write to, and one under a temporary directory would lose every copy on reboot.

func WithKernelReports

func WithKernelReports(root string) RuntimeOption

WithKernelReports points this node at the kernel's own reports about itself, which is /proc everywhere but in a case that has to stand in a machine whose NVIDIA module is loaded or absent.

type State

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

State is the agent's local durable memory. It exists for one reason: an agent that restarts must be able to say "I already did that" about a command it is handed again. Without it, every redelivered launch is a second container.

It also holds the session credential, so a restarted agent resumes rather than re-enrolling, and the spool of events it owes the control plane.

func OpenState

func OpenState(path, nodeID string) (*State, error)

OpenState loads the agent's state from path, creating it when absent.

func (*State) Applied

func (state *State) Applied(operationID string) bool

Applied reports whether this agent already performed one operation.

func (*State) ClearSpool

func (state *State) ClearSpool() error

func (*State) Enrolled

func (state *State) Enrolled(enrollment capability.Enrollment) error

Enrolled records a fresh enrollment. A new fencing token means the control plane has superseded whatever this agent was doing, so the applied-operation memory stays and the session is replaced.

func (*State) FencingToken

func (state *State) FencingToken() uint64

func (*State) MarkApplied

func (state *State) MarkApplied(operationID string) error

MarkApplied records an operation as performed, durably, before its result is reported. A crash between the two costs a duplicate acknowledgement, never a duplicate container.

func (*State) NextEventID

func (state *State) NextEventID() string

NextEventID mints a stable, monotonic identity for one event, so the same fact carries the same ID however many times it is delivered.

func (*State) RecordWorkload

func (state *State) RecordWorkload(observation capability.WorkloadObservation)

RecordWorkload remembers what the control plane was told, so an unchanged phase is not repeated on every tick.

func (*State) Renewed

func (state *State) Renewed(renewal capability.SessionRenewal) error

Renewed records a later credential for the session this agent already has. The fencing token comes back unchanged from a renewal, and it is written from the answer rather than left alone for the same reason every other field is: what this agent believes about itself is what the control plane last told it.

func (*State) Session

func (state *State) Session() (string, time.Time, bool)

Session returns the credential this agent holds and the moment it lapses, and whether it holds one at all.

Whether the credential is still worth using is the agent's judgment rather than this file's, which is why the expiry is answered instead of compared. An agent has to renew before the lapse, because the material it would need to enroll again is spent, so the interesting question is never "has this expired" but "will it still be valid when the request I am about to make lands".

func (*State) Spool

func (state *State) Spool(event node.Event) error

Spool holds an event the control plane could not be told about. It keeps its ID, so replaying it after a reconnection changes nothing.

func (*State) Spooled

func (state *State) Spooled() []node.Event

func (*State) WorkloadChanged

func (state *State) WorkloadChanged(observation capability.WorkloadObservation) bool

WorkloadChanged reports whether this observation says something the control plane has not already been told.

type Transport

type Transport interface {
	Enroll(ctx context.Context, request capability.EnrollmentRequest) (capability.Enrollment, error)
	// RenewSession takes a later credential using the one the agent holds. It is
	// separate from Enroll because the material is separate: enrolling spends the
	// single-use invitation the machine was bootstrapped with, and renewing
	// spends nothing.
	RenewSession(ctx context.Context, nodeID, sessionToken string) (capability.SessionRenewal, error)
	// Session opens the command stream. It returns when the connection ends;
	// the agent reconnects.
	Session(ctx context.Context, nodeID, sessionToken string, commands chan<- node.Command) error
	SendEvents(ctx context.Context, nodeID, sessionToken string, events []node.Event) error
	SendResult(ctx context.Context, nodeID, sessionToken string, result node.Result) error
}

Transport is the agent's outbound connection to the control plane.

Jump to

Keyboard shortcuts

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