Files
mesh-controller/internal/inventory/order.go
T
jochen 2eb9a22c24 Act under a lease, keep accounts by order, one writer at composition (hq to-be 45 Phase 2)
Two controllers could both act (issue 204), a reconcile's report could
overtake the apply after it and the digest decided (issue 267), and a grant
could make a second writer of a machine's report.

- The lease (internal/lease, ADR 0229): mesh-controller_lease key `holder`,
  15 s age, renewed every 5 s by compare-and-set; the epoch is the revision
  it was taken at. The gate is the clock (stops 3 s before expiry); a refused
  renewal is a loss and the process exits; a holder that stops gives it back.
  serve takes it before asserting the bus. Epochs kept in the store
  (migration 0068 controller_epoch) as a floor: a bucket raised from nothing
  is compacted past it. Unleased (no epoch, S12 urgent) only when nobody
  holds it and the bus will not let it be written. A shell command acts
  under the holder's epoch, or its own lease when none.
- Declarations carry `epoch` inside the signed envelope, only to a machine
  whose latest account carried a report_sequence (mesh-host #35); would-send
  is composed with the epoch last sent. Allot and the send both pass the gate.
- Reports: contract in internal/link/order.go (epoch, sequence,
  report_sequence, older_than, refused_older). Accounts kept by epoch, then
  sequence, then report sequence; older refused, counted; unordered reports
  keep the digest rule. Plans by compare-and-set on a revision, with epoch.
  Conditions and calls carry the epoch and are not written off the lease.
- S12 and S13 (naming the writer by epoch) watched, D5 run; reset of the
  bucket said. Writers table compiled in and enforced in PermissionsFor; the
  controller no longer publishes mesh.control.>. A contract per consumed
  kind, and the empty-on-error lint over the repository.
- mesh-host pinned to its main with the epoch in the validator (D1 validates
  the envelope as sent).

Needs mesh-host's genesis lock with the lease grant (mesh-host PR) for
TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose.
2026-10-06 12:29:18 +02:00

251 lines
8.4 KiB
Go

package inventory
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
)
// Order and one writer, as this context keeps them (novox/hq to-be 45 §6, Phase 2): the epochs the
// mesh issued, the epoch each machine was sent, whether its node-engine reads one, and the order of
// the account kept for it.
// ActsUnder gives this inventory the gate every write that acts passes (a plan's): the epoch of the
// controller lease this process acts under, or why it may not act. Nil — a test, a command reading —
// writes with no epoch and refuses nothing.
func (i *Inventory) ActsUnder(epoch func(ctx context.Context) (uint64, error)) { i.acting = epoch }
// actingEpoch is the epoch a write carries, nil when there is no gate or it claims none.
func (i *Inventory) actingEpoch(ctx context.Context) (*int64, error) {
if i.acting == nil {
return nil, nil
}
epoch, err := i.acting(ctx)
if err != nil {
return nil, err
}
if epoch == 0 {
return nil, nil
}
e := int64(epoch)
return &e, nil
}
// Epoch is one epoch the mesh issued.
type Epoch struct {
Epoch uint64
Instance string
Host string
Build string
Taken time.Time
// Ended is when it ended, nil while it is held; How is how: EpochReleased, EpochLost, EpochExpired.
Ended *time.Time
How string
}
// How an epoch ends.
const (
// EpochReleased is a holder that gave the lease back.
EpochReleased = "released"
// EpochLost is a holder whose own renewal was refused or failed, and which said so.
EpochLost = "lost"
// EpochExpired is a holder the next one found gone: it stopped renewing without saying anything.
EpochExpired = "expired"
)
// HighestEpoch is the highest epoch the mesh has issued, zero before the first: the floor no new one
// may be at or under.
func (i *Inventory) HighestEpoch(ctx context.Context) (uint64, error) {
var highest int64
if err := i.store.Pool().QueryRow(ctx, `select coalesce(max(epoch), 0) from controller_epoch`).Scan(&highest); err != nil {
return 0, fmt.Errorf("reading the highest epoch issued: %w", err)
}
return uint64(highest), nil
}
// TookEpoch records an epoch taken, and ends every earlier one still open as found expired: the lease
// was free to take, so whoever held it last neither holds it nor said it let go. Answers the epochs it
// ended that way, newest first.
func (i *Inventory) TookEpoch(ctx context.Context, e Epoch) ([]Epoch, error) {
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback(ctx) }()
rows, err := tx.Query(ctx,
`update controller_epoch set ended = now(), how = $2
where ended is null and epoch < $1
returning epoch, instance, host, build, taken, ended, how`, int64(e.Epoch), EpochExpired)
if err != nil {
return nil, err
}
expired, err := scanEpochs(rows)
if err != nil {
return nil, err
}
if _, err := tx.Exec(ctx,
`insert into controller_epoch (epoch, instance, host, build, taken) values ($1, $2, $3, $4, $5)
on conflict (epoch) do nothing`,
int64(e.Epoch), e.Instance, e.Host, e.Build, e.Taken); err != nil {
return nil, err
}
return expired, tx.Commit(ctx)
}
// EndEpoch records how an epoch ended. One already ended keeps its first word.
func (i *Inventory) EndEpoch(ctx context.Context, epoch uint64, how string) error {
_, err := i.store.Pool().Exec(ctx,
`update controller_epoch set ended = now(), how = $2 where epoch = $1 and ended is null`, int64(epoch), how)
return err
}
// EpochOf is the epoch issued at a number; false when the mesh issued none there.
func (i *Inventory) EpochOf(ctx context.Context, epoch uint64) (Epoch, bool, error) {
rows, err := i.store.Pool().Query(ctx,
`select epoch, instance, host, build, taken, ended, how from controller_epoch where epoch = $1`, int64(epoch))
if err != nil {
return Epoch{}, false, err
}
found, err := scanEpochs(rows)
if err != nil || len(found) == 0 {
return Epoch{}, false, err
}
return found[0], true, nil
}
// EpochsSince is every epoch taken or ended since a moment, newest first.
func (i *Inventory) EpochsSince(ctx context.Context, since time.Time) ([]Epoch, error) {
rows, err := i.store.Pool().Query(ctx,
`select epoch, instance, host, build, taken, ended, how from controller_epoch
where taken >= $1 or ended >= $1 or ended is null order by epoch desc`, since)
if err != nil {
return nil, err
}
return scanEpochs(rows)
}
func scanEpochs(rows pgx.Rows) ([]Epoch, error) {
defer rows.Close()
var out []Epoch
for rows.Next() {
var e Epoch
var epoch int64
if err := rows.Scan(&epoch, &e.Instance, &e.Host, &e.Build, &e.Taken, &e.Ended, &e.How); err != nil {
return nil, err
}
e.Epoch = uint64(epoch)
out = append(out, e)
}
return out, rows.Err()
}
// SentEpoch is the epoch a machine was last sent, by its id; zero for one sent without.
func (i *Inventory) SentEpoch(ctx context.Context, id string) (uint64, error) {
var epoch *int64
if err := i.store.Pool().QueryRow(ctx, `select sent_epoch from node where id = $1`, id).Scan(&epoch); err != nil {
return 0, fmt.Errorf("reading the epoch %s was last sent: %w", id, err)
}
if epoch == nil {
return 0, nil
}
return uint64(*epoch), nil
}
// ReadsEpoch says a machine's node-engine said it reads an epoch in a declaration, by its id.
func (i *Inventory) ReadsEpoch(ctx context.Context, id string) (bool, error) {
var reads bool
if err := i.store.Pool().QueryRow(ctx, `select reads_epoch from node where id = $1`, id).Scan(&reads); err != nil {
return false, fmt.Errorf("reading whether %s reads an epoch: %w", id, err)
}
return reads, nil
}
// RecordReadsEpoch keeps what a machine's latest report said of reading an epoch. Every report of a
// node-engine that orders its reports says it, so a node-engine rolled back says it no longer does.
func (i *Inventory) RecordReadsEpoch(ctx context.Context, id string, reads bool) error {
_, err := i.store.Pool().Exec(ctx, `update node set reads_epoch = $2 where id = $1`, id, reads)
return err
}
// ReportOrder is the order of an account: the declaration's epoch and sequence it is about, and the
// node-engine's own report sequence. Zero in any claims none.
type ReportOrder struct {
Epoch int64
Sequence int64
ReportSequence int64
}
// ErrOlderAccount is an account refused because the one kept is newer.
var ErrOlderAccount = errors.New("an older account than the one kept")
// KeptOrder is the order of the account kept for a machine, by its id; zero when it carried none.
func (i *Inventory) KeptOrder(ctx context.Context, id string) (ReportOrder, error) {
o, err := keptOrder(ctx, i.store.Pool(), id, "")
if errors.Is(err, pgx.ErrNoRows) {
return ReportOrder{}, nil
}
return o, err
}
func keptOrder(ctx context.Context, q queries, id, lock string) (ReportOrder, error) {
var epoch, seq, rseq *int64
err := q.QueryRow(ctx,
`select reported_epoch, reported_sequence, report_sequence from node_report where node = $1`+lock, id).
Scan(&epoch, &seq, &rseq)
if err != nil {
return ReportOrder{}, err
}
var o ReportOrder
for _, f := range []struct {
from *int64
to *int64
}{{epoch, &o.Epoch}, {seq, &o.Sequence}, {rseq, &o.ReportSequence}} {
if f.from != nil {
*f.to = *f.from
}
}
return o, nil
}
// RecordOrderedDoing is RecordDoing for an account that carries its order (novox/hq to-be 45 §6):
// written only when it is not older than the account kept, as olderThan judges — the rule is the
// link's (link.Account), stated once — and in one transaction with the row locked, so two accounts
// arriving together cannot both win. ErrOlderAccount when it is older; nothing is written then.
func (i *Inventory) RecordOrderedDoing(ctx context.Context, id string, d Doing, o ReportOrder,
olderThan func(kept ReportOrder) bool) (news bool, err error) {
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return false, err
}
defer func() { _ = tx.Rollback(ctx) }()
kept, err := keptOrder(ctx, tx, id, " for update")
switch {
case errors.Is(err, pgx.ErrNoRows):
case err != nil:
return false, err
case olderThan(kept):
return false, ErrOlderAccount
}
news, err = recordDoing(ctx, tx, id, d)
if err != nil {
return false, err
}
if _, err := tx.Exec(ctx,
`update node_report set reported_epoch = $2, reported_sequence = $3, report_sequence = $4 where node = $1`,
id, nullIfZero(o.Epoch), nullIfZero(o.Sequence), nullIfZero(o.ReportSequence)); err != nil {
return false, err
}
return news, tx.Commit(ctx)
}
// nullIfZero is a claimed order or none.
func nullIfZero(n int64) *int64 {
if n == 0 {
return nil
}
return &n
}