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.
251 lines
8.4 KiB
Go
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
|
|
}
|