Files
mesh-catalog/modules/pacman/cmd/pacman-tools/keeper.go
T

640 lines
21 KiB
Go

package main
// The keeper (novox/hq ADR 0507 §1-§4; ADR 0508): the module's process on every node, under an account of its
// own with a bus account of its own (ADR 0436 §1). It keeps the node's packages in the state `installed` and what
// an upgrade would change in `updates`, and says each change of the packages as `package.changed`. It reads the
// machine at its start and whenever the machine may have changed, never on a timer but the one daily read of
// `updates` ADR 0507 §4 allows:
//
// - pacman's database lock released (an inotify watch on its directory: the lock file deleted) ends a
// transaction, whoever ran it: `installed` and then `updates` are read again;
// - the mirror list rewritten (reflector writes /etc/pacman.d/mirrorlist): `updates` is read again;
// - a day after `updates` was last read: it is read again, since the repositories change what an upgrade
// would do without any transaction on the node.
//
// A key is written only when what it holds changed (ADR 0289 §1). A value over the bus's cap is not written:
// the key keeps its last value and `<node>.not-current` says the size and since when, written once and deleted
// once the node's packages are current again (ADR 0507 §2).
//
// Everything the keeper touches is injected (its states, its events, the machine, the clock, the watch), so the
// loop is tested without a machine or a bus. One goroutine runs it all: the watch's signals, the clock, and every
// change a hook watches are handled one at a time, in the order they came.
//
// # Hooks (the upgrade parts of ADR 0508 join here)
//
// Hooks are where further work joins the loop, each run on the loop's goroutine:
//
// - Start: once, after the first reads of `installed` and `updates`, before anything else;
// - AfterTransaction: after every released lock, once `installed` and `updates` were read again, with what the
// transaction changed (nil when nothing);
// - Changes: each a state and a key pattern (`*` one name, `**` the rest) watched for the life of the keeper;
// its Handle sees the current values first (Current set), then every put and delete in order;
// - Wakes: each a function the keeper calls at its start and again at the time it last answered; it answers when
// it next wants to be called (a zero time: not again). It is how a hook keeps a time (a round's opening, a
// stall) without polling.
//
// A hook's error is logged and the keeper goes on: a fault in one part never stops the view. keeperHooks builds
// the hooks the keeper runs with, once connected; it is the one place a part is added.
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/process"
)
// ValueCap is the bus's cap on one value of a state (ADR 0201 §14): the mesh's bound, not the module's.
const ValueCap = 256 << 10
// Where pacman keeps its database and its lock, and where the mirror list is.
const (
PacmanDBDir = "/var/lib/pacman"
LockName = "db.lck"
MirrorDir = "/etc/pacman.d"
MirrorName = "mirrorlist"
)
// UpdatesEvery is how long after the last read of `updates` it is read again with nothing else asking (ADR 0507
// §4: once a day).
const UpdatesEvery = 24 * time.Hour
// Store is one state as the keeper uses it: a process state on the bus (process.State), or a fake in a test.
type Store interface {
Get(key string) (*stdio.StateEntry, error)
Create(key string, value any) (uint64, error)
Update(key string, value any, revision uint64) (uint64, error)
Put(key string, value any) (uint64, error)
Delete(key string) error
Keys() ([]string, error)
Watch(key string, handle func(stdio.StateChange) error) error
}
// Clock is the time as the keeper sees it.
type Clock interface {
Now() time.Time
// After fires once, d from now.
After(d time.Duration) <-chan time.Time
}
type realClock struct{}
func (realClock) Now() time.Time { return time.Now() }
func (realClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
// Signal is what the machine's watch saw.
type Signal int
const (
// LockReleased: pacman's database lock was deleted, a transaction (or a database sync) ended.
LockReleased Signal = iota + 1
// MirrorsRewritten: the mirror list was written or replaced.
MirrorsRewritten
)
func (s Signal) String() string {
switch s {
case LockReleased:
return "the lock released"
case MirrorsRewritten:
return "the mirror list rewritten"
}
return fmt.Sprintf("signal %d", int(s))
}
// Watcher is the machine's watch: its signals, until it is closed or fails (the channel closes).
type Watcher interface {
Signals() <-chan Signal
Close() error
}
// Transaction is a released lock as AfterTransaction sees it: when, and what it changed in `installed`
// (nil when it changed nothing, or the value could not be written).
type Transaction struct {
At time.Time
Changed *PackageChanged
}
// StateHook is a watch on one of the module's states for the life of the keeper.
type StateHook struct {
State string
Key string
Handle func(k *Keeper, c stdio.StateChange) error
}
// Wake is called at the keeper's start and then at the time it last answered; it answers when next.
type Wake func(k *Keeper, now time.Time) time.Time
// Hooks are the parts that join the keeper's loop (see the file's comment).
type Hooks struct {
Start []func(k *Keeper) error
AfterTransaction []func(k *Keeper, t Transaction) error
Changes []StateHook
Wakes []Wake
}
// keeperHooks is the hooks the keeper runs with, built once the keeper is connected: the one place a part of
// the keeper is added. Today: the upgrades of ADR 0508 (upgrades.go, record.go, transaction.go, rounds.go), for
// this node.
//
// - Start: the package manager's log read into `runs` once (ImportLog), records past record-days pruned
// (PruneRecords), and every unfinished run carried on (Follow), a run a keeper that went away left included;
// - AfterTransaction: every unfinished run carried on (Follow): a released lock may be a run's transaction ending;
// - Changes: `asks` at `<node>.*`, each change handed to TakeAsk; `rounds` and `runs`, every change, to the
// round's step (RoundStep): a node starts the moment the node before it passed;
// - Wakes: PruneRecords once a day; the round's step at the time it answered (the Saturday's opening, a pass
// whose answer is read again, a refused run tried again).
func keeperHooks(k *Keeper) Hooks {
root := k.Root
if root == nil {
root = newRootActs()
}
u := &Upgrades{Node: k.Node, Config: k.Config, Machine: k.Machine, Root: root, Events: k.Events,
Installed: k.State(StateInstalled), Updates: k.State(StateUpdates), Runs: k.State(StateRuns),
Records: k.State(StateRecords), Rounds: k.State(StateRounds), Asks: k.State(StateAsks), AskRecord: k.AskRecord,
Check: k.Check}
k.upgrades = u
const roundWake = 1 // the round's step is the second wake
roundStep := func(k *Keeper, now time.Time) time.Time {
next, err := u.RoundStep()
if err != nil {
k.logf("[pacman] keeper: the round: %v", err)
}
return next
}
// onRoundChange takes the round's step on a change of `rounds` or `runs`, and keeps the time it answers. The
// current values handed at the watch's start are not each a step: the wake at the start takes one.
onRoundChange := func(k *Keeper, c stdio.StateChange) error {
if c.Current {
return nil
}
k.wakeAt(roundWake, roundStep(k, k.Clock.Now()))
return nil
}
return Hooks{
Start: []func(*Keeper) error{
func(k *Keeper) error {
n, err := u.ImportLog()
if n > 0 {
k.logf("[pacman] keeper: %d past upgrades read from the package manager's log into runs", n)
}
return err
},
func(*Keeper) error { _, err := u.PruneRecords(); return err },
func(*Keeper) error { return u.Follow() },
},
AfterTransaction: []func(*Keeper, Transaction) error{
func(*Keeper, Transaction) error { return u.Follow() },
},
Changes: []StateHook{
{State: StateAsks, Key: k.Node + ".*", Handle: func(_ *Keeper, c stdio.StateChange) error {
return u.TakeAsk(c)
}},
{State: StateRounds, Key: "**", Handle: onRoundChange},
{State: StateRuns, Key: "**", Handle: onRoundChange},
},
Wakes: []Wake{
func(k *Keeper, now time.Time) time.Time {
if !now.Equal(k.started) {
if _, err := u.PruneRecords(); err != nil {
k.logf("[pacman] keeper: pruning records: %v", err)
}
}
return now.Add(UpdatesEvery)
},
roundStep,
},
}
}
// newRootActs is the client of the module's root helper (ADR 0510), which carries out the acts that need root,
// at the socket the manifest names to the keeper; with none named, every act is refused, named.
var newRootActs = func() RootActs {
if sock := os.Getenv(EnvRootSocket); sock != "" {
return rootClient{Socket: sock}
}
return noRoot{}
}
type noRoot struct{}
const noRootSaid = "the keeper was started without " + EnvRootSocket + ", the root helper's socket"
func (noRoot) Transaction(kind, run string, ignore []string) error {
return errors.New(noRootSaid + ": no transaction can start")
}
func (noRoot) Journal(unit string, n int) ([]string, error) {
return nil, errors.New(noRootSaid + ": no journal can be read")
}
func (noRoot) Rollback(run string, plan []RollbackItem) error {
return errors.New(noRootSaid + ": no rollback can start")
}
func (noRoot) Maps() ([]Mapped, error) {
return nil, errors.New(noRootSaid + ": no process's maps can be read")
}
// Keeper is the module's process on one node.
type Keeper struct {
Node string
Config KeeperConfig
Machine *Machine
State func(name string) Store
// Events emits the keeper's events; package.changed carries the id `installed.<node>.<revision>`, the bus's
// dedup id, so the same change said twice is dropped.
Events EventSink
Clock Clock
Watcher Watcher
// Locked says pacman's lock is held now: a released lock followed at once by another transaction is read
// at that one's end.
Locked func() bool
// DBChanged is when the local package database last changed, the time of a value read at the start.
DBChanged func() time.Time
Logf func(format string, a ...any)
// Hooks are the parts joining the loop; MakeHooks, when set, builds them once the keeper is connected
// (its State and Events set), replacing Hooks.
Hooks Hooks
MakeHooks func(k *Keeper) Hooks
// AskRecord reads the operator channel's record of one of the module's asks (process.Conn.Ask).
AskRecord func(id string) (*stdio.StateEntry, error)
// Root carries out the acts that need root; nil is the root helper's client (newRootActs). Check is the
// after-upgrade check; nil is the machine's (AfterCheck). A test sets both.
Root RootActs
Check func(run Run, record *Record) Check
upgrades *Upgrades
started time.Time
nextUpdates time.Time
wakes []time.Time
}
func (k *Keeper) logf(format string, a ...any) {
if k.Logf != nil {
k.Logf(format, a...)
}
}
type change struct {
hook StateHook
c stdio.StateChange
}
// Run reads the machine into the bus and then follows it until ctx ends or the watch fails.
func (k *Keeper) Run(ctx context.Context) error {
if k.MakeHooks != nil {
k.Hooks = k.MakeHooks(k)
}
k.started = k.Clock.Now()
at := k.started
if k.DBChanged != nil {
if t := k.DBChanged(); !t.IsZero() {
at = t
}
}
k.refreshInstalled(at)
k.refreshUpdates()
for _, h := range k.Hooks.Start {
if err := h(k); err != nil {
k.logf("[pacman] keeper: a start hook: %v", err)
}
}
changes := make(chan change, 256)
for _, h := range k.Hooks.Changes {
h := h
err := k.State(h.State).Watch(h.Key, func(c stdio.StateChange) error {
if c.Current {
// the current values are handed over inside Watch, on this goroutine.
k.handle(h, c)
return nil
}
select {
case changes <- change{h, c}:
return nil
case <-ctx.Done():
return ctx.Err()
}
})
if err != nil {
return fmt.Errorf("watching %s %q: %w", h.State, h.Key, err)
}
}
now := k.started
k.wakes = make([]time.Time, len(k.Hooks.Wakes))
for i, w := range k.Hooks.Wakes {
k.wakes[i] = w(k, now)
}
signals := k.Watcher.Signals()
for {
now = k.Clock.Now()
next := k.nextUpdates
for _, w := range k.wakes {
if !w.IsZero() && w.Before(next) {
next = w
}
}
timer := k.Clock.After(next.Sub(now))
select {
case <-ctx.Done():
return ctx.Err()
case s, open := <-signals:
if !open {
return errors.New("the machine's watch ended")
}
k.signal(s)
case c := <-changes:
k.handle(c.hook, c.c)
case <-timer:
k.due()
}
}
}
// wakeAt sets when wake i is called next, from a change a hook handled; before the wakes are first called
// (the current values handed at a watch's start) there is nothing to set.
func (k *Keeper) wakeAt(i int, at time.Time) {
if i < len(k.wakes) {
k.wakes[i] = at
}
}
func (k *Keeper) handle(h StateHook, c stdio.StateChange) {
if err := h.Handle(k, c); err != nil {
k.logf("[pacman] keeper: handling %s %s: %v", h.State, c.Key, err)
}
}
// signal acts on what the machine's watch saw.
func (k *Keeper) signal(s Signal) {
switch s {
case LockReleased:
if k.Locked != nil && k.Locked() {
return
}
at := k.Clock.Now()
changed := k.refreshInstalled(at)
k.refreshUpdates()
for _, h := range k.Hooks.AfterTransaction {
if err := h(k, Transaction{At: at, Changed: changed}); err != nil {
k.logf("[pacman] keeper: after a transaction: %v", err)
}
}
case MirrorsRewritten:
k.refreshUpdates()
}
}
// due runs what the clock has reached: the daily read of `updates`, and every wake whose time came.
func (k *Keeper) due() {
now := k.Clock.Now()
if !now.Before(k.nextUpdates) {
k.refreshUpdates()
}
for i, w := range k.wakes {
if !w.IsZero() && !now.Before(w) {
k.wakes[i] = k.Hooks.Wakes[i](k, now)
}
}
}
// refreshInstalled reads the node's packages and writes `installed.<node>` when they differ from the key,
// emitting `package.changed` with what changed. It answers that change, nil when nothing was written.
func (k *Keeper) refreshInstalled(at time.Time) *PackageChanged {
pkgs, skipped, err := k.Machine.ReadInstalled()
if err != nil {
k.logf("[pacman] keeper: reading the installed packages: %v", err)
return nil
}
if skipped > 0 {
k.logf("[pacman] keeper: %d lines of pacman's list were not a package and were left out", skipped)
}
st := k.State(StateInstalled)
var before InstalledValue
entry, err := st.Get(k.Node)
if err != nil {
k.logf("[pacman] keeper: reading installed.%s: %v", k.Node, err)
return nil
}
if entry != nil {
if err := json.Unmarshal(entry.Value, &before); err != nil {
k.logf("[pacman] keeper: installed.%s holds no package list, so it is written whole: %v", k.Node, err)
entry = nil
}
}
if entry != nil && samePackages(before.Packages, pkgs) {
k.current()
return nil
}
value := InstalledValue{At: at.UTC(), Packages: pkgs}
data, err := json.Marshal(value)
if err != nil {
k.logf("[pacman] keeper: %v", err)
return nil
}
if len(data) > ValueCap {
k.notCurrent(len(data), at)
return nil
}
rev, err := st.Put(k.Node, value)
if err != nil {
k.logf("[pacman] keeper: writing installed.%s: %v", k.Node, err)
return nil
}
k.current()
c := DiffInstalled(before.Packages, pkgs)
if c.Empty() {
return nil
}
ev := &PackageChanged{Node: k.Node, At: at.UTC(), Added: c.Added, Removed: c.Removed, Changed: c.Changed}
id := fmt.Sprintf("%s.%s.%d", StateInstalled, k.Node, rev)
if err := k.Events.Emit(EventPackageChanged, *ev, stdio.EmitOptions{MessageID: id}); err != nil {
k.logf("[pacman] keeper: emitting %s for installed.%s at revision %d: %v", EventPackageChanged, k.Node, rev, err)
}
return ev
}
func samePackages(a, b []InstalledPackage) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// notCurrent marks the node's packages as not current, once: the first size and moment stay until it is current
// again.
func (k *Keeper) notCurrent(size int, at time.Time) {
st := k.State(StateInstalled)
key := NotCurrentKey(k.Node)
k.logf("[pacman] keeper: the node's packages come to %d bytes, over the bus's cap of %d: installed.%s keeps its last value", size, ValueCap, k.Node)
if e, err := st.Get(key); err != nil || e != nil {
if err != nil {
k.logf("[pacman] keeper: reading %s: %v", key, err)
}
return
}
if _, err := st.Put(key, NotCurrent{Bytes: size, Since: at.UTC()}); err != nil {
k.logf("[pacman] keeper: writing %s: %v", key, err)
}
}
// current deletes the not-current mark, when there is one: the key holds the node's packages again.
func (k *Keeper) current() {
st := k.State(StateInstalled)
key := NotCurrentKey(k.Node)
e, err := st.Get(key)
if err != nil {
k.logf("[pacman] keeper: reading %s: %v", key, err)
return
}
if e == nil {
return
}
if err := st.Delete(key); err != nil {
k.logf("[pacman] keeper: deleting %s: %v", key, err)
}
}
// refreshUpdates reads what a full upgrade would change and writes `updates.<node>` when the list or reachable
// changed. A failed read keeps the last list and says reachable false, never an empty list in its place.
func (k *Keeper) refreshUpdates() {
k.nextUpdates = k.Clock.Now().Add(UpdatesEvery)
list, err := k.Machine.UpdateList()
st := k.State(StateUpdates)
entry, gerr := st.Get(k.Node)
if gerr != nil {
k.logf("[pacman] keeper: reading updates.%s: %v", k.Node, gerr)
return
}
var before UpdatesValue
if entry != nil {
if uerr := json.Unmarshal(entry.Value, &before); uerr != nil {
entry = nil
}
}
next := UpdatesValue{Reachable: true, Packages: list}
if err != nil {
k.logf("[pacman] keeper: reading what an upgrade would change: %v", err)
next = UpdatesValue{Reachable: false, Packages: before.Packages}
}
if next.Packages == nil {
next.Packages = []VersionChange{}
}
if entry != nil && sameUpdates(before, next) {
return
}
if _, err := st.Put(k.Node, next); err != nil {
k.logf("[pacman] keeper: writing updates.%s: %v", k.Node, err)
}
}
func sameUpdates(a, b UpdatesValue) bool {
if a.Reachable != b.Reachable || len(a.Packages) != len(b.Packages) {
return false
}
for i := range a.Packages {
if a.Packages[i] != b.Packages[i] {
return false
}
}
return true
}
// The keeper as a process.
// ReadKeeperConfig reads the keeper's config file, which the mesh writes from the module's config values.
func ReadKeeperConfig(path string) (KeeperConfig, error) {
var c KeeperConfig
if path == "" {
return c, fmt.Errorf("the keeper was started without %s, its config file", EnvConfig)
}
data, err := os.ReadFile(path)
if err != nil {
return c, fmt.Errorf("reading the keeper's config: %w", err)
}
if err := json.Unmarshal(data, &c); err != nil {
return c, fmt.Errorf("the keeper's config at %s is not its JSON: %w", path, err)
}
return c, nil
}
// keeper is `pacman-tools keeper`: it reads its bus account and its config, watches the machine, connects and
// runs until stopped. It refuses to start naming what is missing, and never logs a password.
func keeper() int {
logf := func(f string, a ...any) { fmt.Fprintf(os.Stderr, f+"\n", a...) }
brokerFile := os.Getenv(EnvBroker)
if brokerFile == "" {
logf("[pacman] keeper was started without %s, its bus account's file", EnvBroker)
return 2
}
account, err := process.ReadAccount(brokerFile)
if err != nil {
logf("[pacman] keeper: %v", err)
return 1
}
cfg, err := ReadKeeperConfig(os.Getenv(EnvConfig))
if err != nil {
logf("[pacman] keeper: %v", err)
return 1
}
// the watch starts before the first read, so a transaction between the two is not missed.
w, err := WatchMachine(PacmanDBDir, LockName, MirrorDir, MirrorName)
if err != nil {
logf("[pacman] keeper: watching the machine: %v", err)
return 1
}
defer w.Close()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
k := &Keeper{
Node: account.Node, Config: cfg, Machine: ThisMachine(), Clock: realClock{}, Watcher: w, Logf: logf,
Locked: func() bool { _, err := os.Stat(filepath.Join(PacmanDBDir, LockName)); return err == nil },
DBChanged: func() time.Time { return modified(filepath.Join(PacmanDBDir, "local")) },
MakeHooks: keeperHooks,
}
logf("[pacman] keeper of %s is starting as %s", account.Node, account.User)
if err := runKeeper(ctx, account, k); err != nil && ctx.Err() == nil {
logf("[pacman] keeper stopped: %v", err)
return 1
}
return 0
}
func modified(path string) time.Time {
if fi, err := os.Stat(path); err == nil {
return fi.ModTime()
}
return time.Time{}
}
// runKeeper connects as the keeper's own account and runs k on that connection until ctx ends. Apart from keeper
// so a test can drive it over a bus of its own.
func runKeeper(ctx context.Context, account process.Account, k *Keeper) error {
conn, err := process.Connect(account)
if err != nil {
return fmt.Errorf("the bus cannot be reached: %w", err)
}
defer conn.Close()
if k.Node == "" {
k.Node = account.Node
}
k.State = func(name string) Store { return conn.State(name) }
k.Events = conn
k.AskRecord = func(id string) (*stdio.StateEntry, error) { return conn.Ask(MessengerAsksBucket, id) }
return k.Run(ctx)
}