Documentation
¶
Index ¶
- Variables
- func LegalTransition(from, to goblinv1.InstanceState) bool
- type BoltStore
- func (b *BoltStore) Close() error
- func (b *BoltStore) DeleteRange(min, max uint64) error
- func (b *BoltStore) FirstIndex() (uint64, error)
- func (b *BoltStore) Get(k []byte) ([]byte, error)
- func (b *BoltStore) GetLog(idx uint64, raftlog *raft.Log) error
- func (b *BoltStore) GetUint64(key []byte) (uint64, error)
- func (b *BoltStore) LastIndex() (uint64, error)
- func (b *BoltStore) Set(k, v []byte) error
- func (b *BoltStore) SetUint64(key []byte, val uint64) error
- func (b *BoltStore) StoreLog(log *raft.Log) error
- func (b *BoltStore) StoreLogs(logs []*raft.Log) error
- func (b *BoltStore) Sync() error
- type BootstrapPeer
- type BootstrapPlan
- type Consensus
- func (c *Consensus) AddVoter(id, address string) error
- func (c *Consensus) Apply(data []byte, timeout time.Duration) error
- func (c *Consensus) ApplyWithResponse(data []byte, timeout time.Duration) (interface{}, error)
- func (c *Consensus) Bootstrap(servers []raft.Server) error
- func (c *Consensus) GetInstance(instanceID string) (*goblinv1.AgentInstance, bool)
- func (c *Consensus) GetState(namespace, key string) ([]byte, bool)
- func (c *Consensus) GetStateWithVersion(namespace, key string) ([]byte, uint64, bool)
- func (c *Consensus) IsLeader() bool
- func (c *Consensus) IsTombstoned(instanceID string) bool
- func (c *Consensus) Leader() string
- func (c *Consensus) LeaderID() string
- func (c *Consensus) ListInstances() []*goblinv1.AgentInstance
- func (c *Consensus) MigrationInFlight(instanceUUID []byte) (*goblinv1.MigrationRecord, bool)
- func (c *Consensus) MigrationsInFlight() []*goblinv1.MigrationRecord
- func (c *Consensus) NodeID() string
- func (c *Consensus) OperatorKeyCountLocal() int
- func (c *Consensus) OperatorKeysLocal() ([]*goblinv1.OperatorKey, uint64)
- func (c *Consensus) OperatorKeysVerified() ([]*goblinv1.OperatorKey, uint64, error)
- func (c *Consensus) RemoveServer(id string) error
- func (c *Consensus) Scan(namespace, prefix string) map[string][]byte
- func (c *Consensus) Shutdown() error
- func (c *Consensus) Stats() map[string]string
- func (c *Consensus) VerifyLeader() error
- type FSM
- func (f *FSM) Apply(log *raft.Log) interface{}
- func (f *FSM) Get(namespace, key string) ([]byte, bool)
- func (f *FSM) GetInstance(instanceID string) (*goblinv1.AgentInstance, bool)
- func (f *FSM) GetWithVersion(namespace, key string) ([]byte, uint64, bool)
- func (f *FSM) IsTombstoned(instanceID string) bool
- func (f *FSM) ListInstances() []*goblinv1.AgentInstance
- func (f *FSM) MigrationInFlight(instanceUUID []byte) (*goblinv1.MigrationRecord, bool)
- func (f *FSM) MigrationsInFlight() []*goblinv1.MigrationRecord
- func (f *FSM) OperatorKeyCountLocal() int
- func (f *FSM) OperatorKeysLocal() ([]*goblinv1.OperatorKey, uint64)
- func (f *FSM) Restore(rc io.ReadCloser) (err error)
- func (f *FSM) Scan(namespace, prefix string) map[string][]byte
- func (f *FSM) Snapshot() (raft.FSMSnapshot, error)
- type Options
Constants ¶
This section is empty.
Variables ¶
var ( // ErrBootstrapExpectMismatch means peers disagree on how large the // seed set is. Each disagreeing subset would elect its own // bootstrapper and produce two clusters that gossip but never share // a log - the exact failure the seed model was introduced to avoid. ErrBootstrapExpectMismatch = errors.New("peers disagree on bootstrap-expect") // ErrDuplicateNodeID means two members advertise the same Raft // server ID. Raft would treat them as one server at two addresses. ErrDuplicateNodeID = errors.New("duplicate node id in bootstrap set") )
Bootstrap faults are configuration faults, not transient conditions: a node that sees them must fail loudly rather than retry into a split cluster.
var ( // ErrOperatorRegistryEmpty means no operator key is registered. // Every mutating verb refuses while this holds - fail closed is the // point of the registry, not a degraded mode. ErrOperatorRegistryEmpty = errors.New("operator key registry is empty: no mutation is authorized") // ErrOperatorRegistrySeeded means a seed arrived for a registry that // is already populated with a different set. Two nodes configured // with different root keys is a misconfiguration we refuse to paper // over by merging them: quietly unioning the sets would widen the // trust root without anyone deciding to. ErrOperatorRegistrySeeded = errors.New("operator key registry already seeded with a different key set") // ErrOperatorRegistryStale means a signed change named a registry // serial that is no longer current. This is the replay guard: a // change is valid at exactly one serial, so re-submitting a captured // one fails once any other change has landed. ErrOperatorRegistryStale = errors.New("operator key change: stale registry serial") // ErrOperatorLastKey means a remove would empty the registry. // Refusing keeps the operator from locking the cluster out of its // own control plane, and is what guarantees the seed path stays // reachable exactly once. ErrOperatorLastKey = errors.New("operator key change: refusing to remove the last registered key") )
var ( // ErrSnapshotRegistryUnverifiable means the snapshot carries operator // keys and this node holds no root to check them against. ErrSnapshotRegistryUnverifiable = errors.New( "snapshot carries an operator key registry and this node has no configured operator keys to authenticate it against") // ErrSnapshotRegistryUnprovenanced means the snapshot carries a // registry with no provenance - the shape every snapshot had before // GOBLIN-DIV-047. ErrSnapshotRegistryUnprovenanced = errors.New( "snapshot carries an operator key registry with no provenance") // ErrSnapshotSeedMismatch means the snapshot's founding key set is not // this node's configured root of trust. ErrSnapshotSeedMismatch = errors.New( "snapshot's founding operator key set is not this node's configured root of trust") // ErrSnapshotChainInvalid means the recorded provenance does not // replay: a signature failed, an ordering guard failed, or the result // disagreed with the registry the snapshot claims. ErrSnapshotChainInvalid = errors.New("snapshot operator key provenance does not replay") )
var ErrCASMismatch = errors.New("cas: version mismatch")
ErrCASMismatch is returned (as the Apply response) when a CAS command's expected version does not match the key's current version. Errors are data: callers distinguish "CAS lost the race" from transport failures.
var ErrIllegalTransition = errors.New("instance lifecycle: illegal transition")
ErrIllegalTransition is returned (as the Apply response) when an InstanceTransition violates the lifecycle FSM. Errors are data: callers distinguish a rejected transition from transport failures.
var ( // An error indicating a given key does not exist ErrKeyNotFound = errors.New("not found") )
var ErrMigrationInFlight = errors.New("migration already in flight for this instance")
ErrMigrationInFlight is returned when a second MIGRATE_BEGIN arrives for an instance that is already migrating. Typed because a proposer retrying after a lost response must distinguish "someone else is moving this" from "your request was malformed".
var ErrNoMigrationInFlight = errors.New("no migration in flight for this instance")
ErrNoMigrationInFlight is returned when a commit arrives with no matching begin.
Functions ¶
func LegalTransition ¶
func LegalTransition(from, to goblinv1.InstanceState) bool
LegalTransition reports whether the instance lifecycle FSM permits from -> to. The lifecycle is forward-only (DDR-6): ADMITTED -> SCHEDULED -> STARTING -> RUNNING -> DRAINING -> STOPPING -> TERMINATED -> ARCHIVED, with forward skips permitted (a dispatch may go ADMITTED -> RUNNING directly) and TERMINATED reachable from any non-terminal state. Nothing leaves ARCHIVED, nothing re-enters an earlier state, and UNSPECIFIED is never a destination.
Types ¶
type BoltStore ¶
type BoltStore struct {
// contains filtered or unexported fields
}
BoltStore provides access to BoltDB for Raft to store and retrieve log entries. It also provides key/value storage, and can be used as a LogStore and StableStore.
func New ¶
New uses the supplied options to open the BoltDB and prepare it for use as a raft backend.
func NewBoltStore ¶
NewBoltStore takes a file path and returns a connected Raft backend.
func (*BoltStore) DeleteRange ¶
DeleteRange is used to delete logs within a given range inclusively.
func (*BoltStore) FirstIndex ¶
FirstIndex returns the first known index from the Raft log.
type BootstrapPeer ¶
BootstrapPeer is one member's view as gossip reports it: its Raft server ID, its dialable control-plane address, and the bootstrap-expect size it was configured with (0 when it advertised none, which marks it a plain joiner rather than a seed).
type BootstrapPlan ¶
type BootstrapPlan struct {
// Servers is the initial Raft configuration, ordered by node ID.
Servers []raft.Server
// Bootstrapper is the node ID that issues BootstrapCluster. The
// other seeds do nothing and join through the resulting log.
Bootstrapper string
}
BootstrapPlan is the decision every seed node reaches independently once it can see the whole expected set. Because it is a pure function of that set, all seeds compute the same plan and exactly one of them acts on it.
func PlanBootstrap ¶
func PlanBootstrap(expect int, peers []BootstrapPeer) (BootstrapPlan, bool, error)
PlanBootstrap decides whether the seed set is complete and, if so, which member seeds the cluster. It reports ready=false while peers are still missing; callers poll until it turns true, the context expires, or it returns an error.
expect < 2 means bootstrap-expect is not in play: the caller keeps the seed model (bootstrap alone when there is no join target).
type Consensus ¶
type Consensus struct {
// contains filtered or unexported fields
}
Consensus manages cluster consensus using Raft
func NewConsensus ¶
func NewConsensus(nodeID, dataDir string, stream raft.StreamLayer, bootstrap bool, snapshotThreshold uint64, snapshotInterval time.Duration, trailingLogs uint64, trustedRoots []*goblinv1.OperatorKey) (*Consensus, error)
NewConsensus builds the Raft engine over the provided stream layer (the raft-quic plane of the shared control-plane listener; the layer's Addr() is this server's advertised raft address). bootstrap must be true on exactly one seed node (the one with no join target): every node bootstrapping its own single-node cluster yields N independent rafts that gossip but never share state - the failure mode the 2b e2e exposed.
snapshotThreshold, snapshotInterval, and trailingLogs tune Raft's snapshot-compaction behavior (raft.Config.SnapshotThreshold / SnapshotInterval / TrailingLogs); zero keeps raft.DefaultConfig's value for that field (GOBLIN-DIV-040: operators need these to bound trailing-log size and replay-on-join cost, not just the library defaults tuned for a generic workload). trustedRoots is this node's configured operator keys (--operator-key). They reach the FSM at construction so Restore can authenticate a snapshot's registry against material this node already holds rather than material the snapshot supplies (GOBLIN-DIV-047). Threaded through the constructor rather than set afterwards because raft may call Restore as soon as the FSM is handed to it.
func (*Consensus) ApplyWithResponse ¶
ApplyWithResponse applies a command and returns the FSM's response value alongside any commit error. Commands whose outcome is data (CAS) return a typed error as the response; a nil, nil result is an applied success.
func (*Consensus) Bootstrap ¶
Bootstrap installs an initial Raft configuration. It is separate from construction because bootstrap-expect cannot decide the server set until gossip has converged: the engine comes up as a configuration-less follower and is seeded once the peers are known.
ErrCantBootstrap means state already exists (a restart, or another seed won the race) - not an error.
func (*Consensus) GetInstance ¶
func (c *Consensus) GetInstance(instanceID string) (*goblinv1.AgentInstance, bool)
GetInstance returns a live instance record by canonical UUID string.
func (*Consensus) GetStateWithVersion ¶
GetStateWithVersion returns the current FSM state and the key's CAS version (0 when the key is absent).
func (*Consensus) IsTombstoned ¶
IsTombstoned reports whether an instance UUID was ever terminated.
func (*Consensus) ListInstances ¶
func (c *Consensus) ListInstances() []*goblinv1.AgentInstance
ListInstances returns every live instance record.
func (*Consensus) MigrationInFlight ¶
func (c *Consensus) MigrationInFlight(instanceUUID []byte) (*goblinv1.MigrationRecord, bool)
MigrationInFlight reports the in-flight migration for one instance. Local read: the reconciler asks about an instance it is already looking at, and a stale "no migration" only costs the recovery it would have done anyway.
func (*Consensus) MigrationsInFlight ¶
func (c *Consensus) MigrationsInFlight() []*goblinv1.MigrationRecord
MigrationsInFlight lists every recorded in-flight migration, for the orphan sweep (GOBLIN-DIV-049).
func (*Consensus) NodeID ¶
NodeID is this node's raft server id. It exists so a refusal can name WHICH node refused without threading the supervisor's config through every gate (GOBLIN-DIV-048): a diagnostic that says "the registry was empty" is useless if it cannot say empty on whom.
func (*Consensus) OperatorKeyCountLocal ¶
OperatorKeyCountLocal reports how many operator keys THIS NODE has applied. Zero is the fail-closed condition: no key, no mutation. No leadership check, deliberately - see OperatorKeyCountLocal on the FSM for why staleness can only move this answer toward refusal.
func (*Consensus) OperatorKeysLocal ¶
func (c *Consensus) OperatorKeysLocal() ([]*goblinv1.OperatorKey, uint64)
OperatorKeysLocal returns THIS NODE's applied operator key registry and its serial (GOBLIN-DIV-015 piece 1). It performs no leadership check, so the answer may predate a committed change. Use OperatorKeysVerified for anything that authorizes; use this only where a stale answer cannot produce a yes, and say so at the call site.
func (*Consensus) OperatorKeysVerified ¶
func (c *Consensus) OperatorKeysVerified() ([]*goblinv1.OperatorKey, uint64, error)
OperatorKeysVerified returns the operator key registry and its serial only if this node is the leader with a live quorum (GOBLIN-DIV-044).
This is the accessor for anything that AUTHORIZES from key material. The unverified reads below answer from whatever this replica happens to have applied, and a follower that has not applied an OPERATOR_KEY_CHANGE remove still resolves the removed key - so a consumer that says yes on a successful lookup would mint for a revoked operator by asking a lagging replica. VerifyLeader is what makes that unreachable: a follower cannot answer at all, and a leader that lost quorum cannot either.
It is a refusal, not a forward. Routing to the leader is the caller's policy decision; this surface's job is only that a stale reader cannot say yes.
func (*Consensus) RemoveServer ¶
RemoveServer removes a server from the cluster
func (*Consensus) VerifyLeader ¶
VerifyLeader checks if this node is the leader and has a quorum
type FSM ¶
type FSM struct {
// contains filtered or unexported fields
}
FSM implements the Raft finite state machine
func NewFSM ¶
func NewFSM(trustedRoots []*goblinv1.OperatorKey) *FSM
NewFSM creates a new FSM NewFSM builds the state machine. trustedRoots is this node's configured operator keys (--operator-key), which Restore uses to authenticate a snapshot's registry; pass nil for a node configured with none.
It is a CONSTRUCTOR PARAMETER rather than a setter, and that is not a style preference. A setter reintroduces an ordering hazard with real consequences: raft can call Restore as soon as the FSM is handed over, so an anchor installed "shortly after" construction is an anchor that might not be there when the first snapshot lands - and the failure would be silent acceptance of an unverified registry, which is the defect this closes.
func (*FSM) Apply ¶
Apply applies a Raft log entry to the FSM. The returned value is the command's response (retrieved via raft.ApplyFuture.Response): nil on success, ErrCASMismatch on a failed CAS, an error for undecodable or unknown commands. A silent no-op on a distributed write is a data consistency hazard, so every path answers.
func (*FSM) GetInstance ¶
func (f *FSM) GetInstance(instanceID string) (*goblinv1.AgentInstance, bool)
GetInstance returns a copy of a live instance record by canonical UUID string.
func (*FSM) GetWithVersion ¶
GetWithVersion retrieves a value and its CAS version. A missing key reports version 0 (the value CAS create-if-absent expects).
func (*FSM) IsTombstoned ¶
IsTombstoned reports whether a UUID has ever been terminated. Tombstones are append-only forever.
func (*FSM) ListInstances ¶
func (f *FSM) ListInstances() []*goblinv1.AgentInstance
ListInstances returns copies of every live instance record.
func (*FSM) MigrationInFlight ¶
func (f *FSM) MigrationInFlight(instanceUUID []byte) (*goblinv1.MigrationRecord, bool)
MigrationInFlight reports the in-flight migration for an instance. Read path for goblinctl and the reconciler; takes the read lock.
func (*FSM) MigrationsInFlight ¶
func (f *FSM) MigrationsInFlight() []*goblinv1.MigrationRecord
MigrationsInFlight lists every migration currently recorded as in flight.
It exists for the orphan sweep (GOBLIN-DIV-049): nothing else ever clears one of these records. Only a MIGRATE_COMMIT does, and only the leader can propose one - so a leader that dies mid-migration leaves a record no surviving node will ever retire, and the reconciler now honours those records. Without a sweep that would trade a duplicated instance for a permanently unrecoverable one.
func (*FSM) OperatorKeyCountLocal ¶
OperatorKeyCountLocal reports how many operator keys THIS REPLICA has applied. Zero is the fail-closed condition every mutating verb checks, and the count is the one reading that is safe to take locally: a replica behind the seed reads zero and refuses, and a seeded registry can never return to empty because removing the last key is refused. So this can be stale only in the direction that refuses.
func (*FSM) OperatorKeysLocal ¶
func (f *FSM) OperatorKeysLocal() ([]*goblinv1.OperatorKey, uint64)
OperatorKeysLocal returns THIS REPLICA's applied registry sorted by key id, plus the current serial. The Local suffix is the contract, not decoration: an FSM knows only what it has applied, so a follower that has not yet applied an OPERATOR_KEY_CHANGE answers from the registry as it was before the change - including a remove. Callers for whom a stale yes is wrong (anything that authorizes on key material, e.g. a mint path) must use Consensus.OperatorKeysVerified instead. Callers for whom a stale answer can only fail closed may use this and must say at the call site why.
func (*FSM) Restore ¶
func (f *FSM) Restore(rc io.ReadCloser) (err error)
Restore restores the FSM from a snapshot. Only the proto encoding is accepted (GOBLIN-DIV-040 schema reset): a snapshot written by the old JSON encoder is refused outright rather than dual-read, mirroring the CommandType-0 rejection above for the same reason - a compatibility path nothing forces anyone to remove never gets removed.
type Options ¶
type Options struct {
// Path is the file path to the BoltDB to use
Path string
// BoltOptions contains any specific BoltDB options you might
// want to specify [e.g. open timeout]
BoltOptions *bolt.Options
// NoSync causes the database to skip fsync calls after each
// write to the log. This is unsafe, so it should be used
// with caution.
NoSync bool
}
Options contains all the configuration used to open the BoltDB