Documentation
¶
Overview ¶
Package migration moves CRIU checkpoint images between nodes and owns everything the kernel deliberately does not know about them: where images live on disk, how they are keyed, and how they travel (GOBLIN-DIV-018).
The split is the silo's mechanism/policy line. gapi's core/checkpoint dumps and restores against a local directory and knows nothing about nodes or keys; this package decides which directory, names it by {instance_uuid, checkpoint_epoch} per research section 4.4, and serves it to whichever node is taking the instance over.
Index ¶
- Constants
- Variables
- func DialAndFetch(ctx context.Context, sourceAddr string, tlsConf *tls.Config, store *Store, ...) (string, error)
- type Applier
- type Authorizer
- type Caller
- type Client
- type Coordinator
- type Dialer
- type FileInfo
- type ImagePuller
- type NodeClient
- type Proposer
- type RPCNodes
- func (r *RPCNodes) Checkpoint(ctx context.Context, nodeID, instanceID string, uuid []byte, epoch uint64) error
- func (r *RPCNodes) Ready(ctx context.Context, nodeID, instanceID string) (string, bool, error)
- func (r *RPCNodes) Restore(ctx context.Context, nodeID, instanceID string, uuid []byte, epoch uint64, ...) error
- type RPCPuller
- type RaftProposer
- type Request
- type Resolver
- type Server
- type Store
Constants ¶
const ( MethodCheckpoint = "NodeRPC.CheckpointAgentInstance" MethodRestore = "NodeRPC.RestoreAgentInstance" MethodPull = "NodeRPC.PullCheckpoint" MethodReady = "NodeRPC.MigrationReady" )
RPC method names. Constants because a typo in a method string fails at runtime on a remote node, which is the worst place to discover it.
Variables ¶
var ( // ErrNotFound: no image exists for this {uuid, epoch}. ErrNotFound = errors.New("migration: no image for this instance and epoch") // ErrBadUUID: an instance UUID that is not 16 bytes. Rejected at the // boundary rather than being hex-encoded into a nonsense path. ErrBadUUID = errors.New("migration: instance uuid must be 16 bytes") // ErrTargetNotReady: the destination cannot accept a migration and // said so before anything was done to the source (GOBLIN-DIV-048). // Distinct from every error below it, and the distinction is the // point: this one means the instance is still running where it was. ErrTargetNotReady = errors.New("migration: destination is not ready to accept") // ErrTruncated: the transfer ended before the manifest was // satisfied. Distinct from a read error: the image is incomplete // but the key is still valid, so a retry is the right response. ErrTruncated = errors.New("migration: image transfer truncated") // ErrManifest: the manifest itself is unusable (empty, or naming a // file that escapes the image directory). ErrManifest = errors.New("migration: unusable manifest") // ErrNotFound so a caller cannot probe for image existence by // reading the error, and so retrying with a fresh token is // distinguishable from retrying against another node. ErrUnauthorized = errors.New("migration: fetch refused by source") // ErrSourceInternal: the source could not read its own image. The // key may be fine; another replica of the image may serve it. ErrSourceInternal = errors.New("migration: source failed to read its image") )
Typed failures. The destination branches on these to decide between refetching, re-electing a source, and giving up, so they must be distinguishable without reading prose.
var ErrRolledBack = errors.New("migration rolled back")
ErrRolledBack wraps the original failure of a migration that was successfully undone. The instance is running on its source and the cluster is consistent; the caller may retry.
var ErrStranded = errors.New("migration failed and rollback failed; instance is not running")
ErrStranded is returned when a migration failed AND the rollback also failed. The instance is running nowhere and needs an operator: it is deliberately distinct from ErrRolledBack, because treating the two the same is how a retry loop spins against a broken instance.
Functions ¶
func DialAndFetch ¶
func DialAndFetch(ctx context.Context, sourceAddr string, tlsConf *tls.Config, store *Store, uuid []byte, epoch uint64, token []byte) (string, error)
DialAndFetch dials a peer's goblin-ckpt listener and pulls one image into store, returning the local directory.
The TLS config is supplied by the caller rather than built here: this package must not decide the cluster's verification policy, and a default that skipped verification would be a memory-image disclosure waiting to happen.
Types ¶
type Applier ¶
type Applier interface {
ApplyWithResponse(data []byte, timeout time.Duration) (interface{}, error)
}
Applier is the consensus surface a proposer needs: commit bytes and report the FSM's response. Declared here rather than imported so this package does not depend on the consensus package, which would make a cycle once consensus grows a migration read path.
ApplyWithResponse rather than Apply, because the FSM's refusals - concurrent migration, stale epoch, missing rights - come back as the Apply RESPONSE, not as a transport error. A proposer that only checked the transport would read every rejection as success.
type Authorizer ¶
Authorizer decides whether a fetch may proceed. It is a hook rather than a hardcoded check because the capability rights bitmap is orchestration policy and lives above this package; the transfer only needs a yes or no.
A nil Authorizer refuses every fetch. Defaulting open would mean any peer that negotiated the ALPN could read any instance's memory image.
type Caller ¶
Caller is the RPC surface the migration clients need. It mirrors the scheduler's RPCClient; declared here so this package does not import the scheduler and create a cycle. Call takes proto.Message rather than internal/supervisor's own types for the same reason: importing internal/supervisor from core/migration would itself be a cycle (internal/supervisor already imports core/migration), so the dependency both sides can share without cycling is google.golang.org/protobuf/proto plus the generated goblinv1 messages, not either domain package.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client pulls checkpoint images into this node's image store.
It is the DESTINATION side of a migration. Pull puts flow control and retry on the node that will run the instance next, rather than on the one being torn down, and the {instance_uuid, epoch} key makes a retry idempotent: refetching simply overwrites the same directory.
func (*Client) Fetch ¶
func (c *Client) Fetch(ctx context.Context, conn *quic.Conn, instanceUUID []byte, epoch uint64, token []byte) (string, error)
Fetch pulls one checkpoint over conn and returns the local directory it landed in.
On any failure the directory is left in place rather than cleaned up: the key is still valid, a retry refetches into it, and deleting a partial image would discard the only evidence of what went wrong. Callers must treat a returned error as "this directory is not restorable", not as "nothing was written".
type Coordinator ¶
type Coordinator struct {
// contains filtered or unexported fields
}
Coordinator drives one migration from intent to outcome.
The sequence is: commit the intent through Raft, dump on the source, pull the image to the destination, restore there, then commit the outcome. Every step before the final commit is reversible, and the image is what makes it so - which is why a dump stops the source rather than leaving it running.
Each step is an interface rather than a concrete client so the failure paths are testable. Migration failures are rare in practice and catastrophic when mishandled, so they must be exercised deterministically rather than waited for.
func NewCoordinator ¶
func NewCoordinator(raft Proposer, nodes NodeClient, images ImagePuller, log *slog.Logger) *Coordinator
NewCoordinator wires a coordinator.
func (*Coordinator) Migrate ¶
func (c *Coordinator) Migrate(ctx context.Context, req Request) error
Migrate moves one instance and returns only after the outcome is committed.
On any failure after the dump, the source is restored from the same image and the migration is committed as ABORTED. The instance's lifecycle state never changes and its UUID never moves - only its locator does, and only on success.
type ImagePuller ¶
type ImagePuller interface {
Pull(ctx context.Context, destNodeID, sourceNodeID string, uuid []byte, epoch uint64, token []byte) error
}
ImagePuller moves an image from the source node to the destination. The destination does the pulling; this is the coordinator asking it to, not the coordinator moving bytes itself.
type NodeClient ¶
type NodeClient interface {
// Ready asks a prospective destination whether it can accept a
// migration. It returns (reason, ready, err): a transport failure
// and a considered "no" are different facts.
Ready(ctx context.Context, nodeID, instanceID string) (string, bool, error)
Checkpoint(ctx context.Context, nodeID, instanceID string, uuid []byte, epoch uint64) error
Restore(ctx context.Context, nodeID, instanceID string, uuid []byte, epoch uint64, spec *goblinv1.AgentSpec) error
}
NodeClient reaches the node-local checkpoint and restore RPCs.
type Proposer ¶
type Proposer interface {
ProposeMigrateBegin(ctx context.Context, mb *goblinv1.MigrateBegin) error
ProposeMigrateCommit(ctx context.Context, mc *goblinv1.MigrateCommit) error
}
Proposer commits migration records through Raft. Implemented by the consensus layer; an interface here so the coordinator can be tested without standing up a quorum.
type RPCNodes ¶
type RPCNodes struct {
// contains filtered or unexported fields
}
RPCNodes drives the node-local checkpoint and restore RPCs. It satisfies NodeClient.
func NewRPCNodes ¶
NewRPCNodes wires a production NodeClient.
func (*RPCNodes) Checkpoint ¶
func (r *RPCNodes) Checkpoint(ctx context.Context, nodeID, instanceID string, uuid []byte, epoch uint64) error
Checkpoint asks nodeID to dump the instance.
func (*RPCNodes) Ready ¶
Ready asks nodeID whether it can accept a migration, before anything is done to the source (GOBLIN-DIV-048). A node that is unreachable and a node that answers "not ready" are different failures and are reported as such: the first is an error from the call, the second a populated response the caller turns into a refusal.
type RPCPuller ¶
type RPCPuller struct {
// contains filtered or unexported fields
}
RPCPuller tells the destination to pull an image from the source. It satisfies ImagePuller.
func NewRPCPuller ¶
NewRPCPuller wires a production ImagePuller over the same dialer.
type RaftProposer ¶
type RaftProposer struct {
// contains filtered or unexported fields
}
RaftProposer commits migration records through consensus. It satisfies Proposer.
func NewRaftProposer ¶
func NewRaftProposer(raft Applier, timeout time.Duration) *RaftProposer
NewRaftProposer wires a production Proposer. A zero timeout takes the default rather than meaning "wait forever": a migration blocked on an unreachable quorum should fail and roll back, not hang holding a stopped process.
func (*RaftProposer) ProposeMigrateBegin ¶
func (p *RaftProposer) ProposeMigrateBegin(_ context.Context, mb *goblinv1.MigrateBegin) error
ProposeMigrateBegin commits the intent to migrate.
func (*RaftProposer) ProposeMigrateCommit ¶
func (p *RaftProposer) ProposeMigrateCommit(_ context.Context, mc *goblinv1.MigrateCommit) error
ProposeMigrateCommit commits the outcome.
type Request ¶
type Request struct {
InstanceID string
InstanceUUID []byte
SourceNode string
TargetNode string
Epoch uint64
Rights uint64
Token []byte
Spec *goblinv1.AgentSpec
}
Request is one migration.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server answers checkpoint fetches from this node's image store.
It is the SOURCE side of a migration. It never initiates: the destination pulls, so the node being torn down carries no retry responsibility.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the on-disk layout of checkpoint images for one node.
Layout is <root>/<uuid-hex>/<epoch>/. The UUID comes first so every checkpoint of one instance sits together for audit, and the epoch is a directory rather than a filename suffix so criu can own the directory's contents without this package knowing what it writes.
func (*Store) Create ¶
Create makes the image directory for one checkpoint and returns it.
Deliberately not idempotent about contents: it will happily hand back an existing directory, because a retried pull refetching into the same key is the intended behaviour and is what makes {uuid, epoch} keying worth having.
func (*Store) Dir ¶
Dir is the image directory for one checkpoint. It does not create anything; callers that intend to write use Create.
func (*Store) Manifest ¶
Manifest lists the regular files of one checkpoint, sorted by name so that a source and a destination agree on transfer order without exchanging it.
Subdirectories are not descended: criu writes a flat image directory, and silently flattening a nested tree would produce name collisions that only appear under load.