mesh/delivery merged: source.main moved to novox/mesh-host@1195cf45971e
mesh/check pass 99285cb080db on 1195cf45971e | unit 303 run, 1080 kept, 5 skipped | integration 306 run, 1082 kept, 4 skipped | over bound: unit (slow…
The bus node's engine writes the bus user list (ADR 0547 bridge); each write makes the bus reload its accounts, and for about three seconds every JetStream request meets no responders. An apply that failed then was kept failed until a key changed (mesh-vault on novox, 2026-10-25 23:04:24), holding every module that needs the secret seat. A transient bus failure is now retried with a backoff (2 s, 5 s, 15 s, 60 s, then every 5 min), the have saying retrying meanwhile; after the engine writes the user list, the transiently failed modules are tried again once the reload is over. A have write that met it is written again the same way. shorten keeps the head and the tail, the cause.
125 lines
4.4 KiB
Go
125 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"github.com/novox/mesh-host/internal/apply"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
"github.com/novox/mesh-host/internal/system"
|
|
"github.com/novox/mesh-host/internal/userlist"
|
|
)
|
|
|
|
// The bridge of novox/hq ADR 0547 §8, wired: the bus node's engine writes the bus's user list alone from the newest
|
|
// composition of the bus module, under the apply's two locks as a filled file is written (filling.go), and says what
|
|
// it wrote in the bus module's have.
|
|
|
|
// userListFollower is the follower of the bus's user list for one link: the node's composition bucket, the keys the
|
|
// agent trusts, the write alone, and the have.
|
|
func userListFollower(opts options, session *link.Session, composedBucket string, trusted func() []ed25519.PublicKey,
|
|
say link.Announce, wrote func()) *userlist.Follower {
|
|
return userlist.New(userlist.Env{
|
|
Node: session.Membership.Node,
|
|
Composed: bucketWatch{session, composedBucket},
|
|
Trusted: trusted,
|
|
Resolve: func(ctx context.Context, raw json.RawMessage) ([]byte, error) {
|
|
return resolveUserList(session.WithLink(ctx), raw)
|
|
},
|
|
OnDisk: userListOnDisk,
|
|
Write: func(ctx context.Context, module string, raw json.RawMessage, body []byte) (string, bool, error) {
|
|
digest, changed, err := writeUserListAlone(session.WithLink(ctx), opts, raw, body, say)
|
|
if changed && wrote != nil {
|
|
wrote()
|
|
}
|
|
return digest, changed, err
|
|
},
|
|
Report: func(ctx context.Context, module string, w userlist.Written) {
|
|
session.NoteUserList(ctx, module, w)
|
|
},
|
|
Say: announceOr(say),
|
|
})
|
|
}
|
|
|
|
// userListFile is a user-list resource as the file it declares.
|
|
func userListFile(raw []byte) (*declaration.File, error) {
|
|
r, err := declaration.ParseResource(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the user list is not a resource: %w", err)
|
|
}
|
|
f, ok := r.(*declaration.File)
|
|
if !ok {
|
|
return nil, fmt.Errorf("the user list is a %s, not a file", r.Kind())
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// resolveUserList is the list a user-list resource holds: its content, or the object its digest names in this node's
|
|
// file bucket, fetched and verified (apply.ResolveFileReferences, ADR 0486 §1).
|
|
func resolveUserList(ctx context.Context, raw []byte) ([]byte, error) {
|
|
f, err := userListFile(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d := &declaration.Declaration{Version: declaration.Version, Resources: []declaration.Resource{f}}
|
|
if err := apply.ResolveFileReferences(ctx, d, link.FilesFrom(ctx)); err != nil {
|
|
return nil, fmt.Errorf("the user list could not be had by its digest: %w", err)
|
|
}
|
|
return []byte(d.Resources[0].(*declaration.File).Content), nil
|
|
}
|
|
|
|
// userListOnDisk is what the user list's file holds on this machine now; nil when there is none.
|
|
func userListOnDisk(raw json.RawMessage) ([]byte, error) {
|
|
f, err := userListFile(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body, err := os.ReadFile(f.Path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
return body, err
|
|
}
|
|
|
|
// writeUserListAlone writes one user-list resource alone (apply.WriteAlone) with body as its content, the node's state
|
|
// read and saved under the apply's locks, and answers the digest of the file on the machine after.
|
|
func writeUserListAlone(ctx context.Context, opts options, raw []byte, body []byte, say link.Announce) (string, bool, error) {
|
|
declared, err := userListFile(raw)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
file := *declared
|
|
file.Content, file.Digest, file.Size = string(body), "", 0
|
|
|
|
applying.Lock()
|
|
defer applying.Unlock()
|
|
unlock, err := store.Lock(opts.state, nil)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
defer unlock()
|
|
known, err := store.Load(opts.state)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
built, err := system.For(builtFor)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
out, updated, writeErr := apply.WriteAlone(ctx, built, &file, known, apply.ExecRunner, announceOr(say))
|
|
// Saved whichever way it went, as an apply's: what was written is on the machine either way.
|
|
if err := store.Save(opts.state, updated); err != nil {
|
|
writeErr = errors.Join(writeErr, errors.New("written, and the node's state could not be saved: "+err.Error()))
|
|
}
|
|
digest := ""
|
|
if body, err := os.ReadFile(file.Path); err == nil {
|
|
digest = userlist.Digest(body)
|
|
}
|
|
return digest, writeErr == nil && out.Action != "unchanged", writeErr
|
|
}
|