mesh/delivery merged: source.main moved to novox/mesh-delivery@1d416827157d
mesh/check being carried to ce600df98afc on 1d416827157d
364 lines
14 KiB
Go
364 lines
14 KiB
Go
package grants
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The part's own states, as its manifest declares them, and the keys of the two that hold one value.
|
|
const (
|
|
StateUserList = "user-list"
|
|
StateMemberships = "memberships"
|
|
StateAssertions = "assertions"
|
|
StateTakeover = "takeover"
|
|
KeyList = "list"
|
|
KeySwitch = "switch"
|
|
// KeyPass is the last pass's result, and KeyAsked a tool's ask to compose again now, both in StateUserList.
|
|
KeyPass = "pass"
|
|
KeyAsked = "asked"
|
|
)
|
|
|
|
// States is every state the part keeps.
|
|
var States = []string{StateAssertions, StateMemberships, StateTakeover, StateUserList}
|
|
|
|
// The events the part says.
|
|
const (
|
|
EventUserList = "user-list-changed"
|
|
EventMembership = "membership-changed"
|
|
// EventAsserted is said when the part created a bucket, log or stream a manifest declares (issue 1790).
|
|
EventAsserted = "asserted"
|
|
)
|
|
|
|
// Emits is every event the part says.
|
|
var Emits = []string{EventAsserted, EventMembership, EventUserList}
|
|
|
|
// Store is the part's own state, by local name: the only thing the part writes.
|
|
type Store interface {
|
|
Get(state, key string) ([]byte, uint64, bool, error)
|
|
Put(state, key string, value []byte) (uint64, error)
|
|
}
|
|
|
|
// Emitter says one of the part's events.
|
|
type Emitter interface {
|
|
Emit(event string, body any) error
|
|
}
|
|
|
|
// UserListState is the value of `user-list` in shadow: the list the grants part composed (users and grants, no hashes),
|
|
// what the bus loaded, and how they differ. Inputs is the digest of all it says but At: a value is written again only
|
|
// when it changes.
|
|
type UserListState struct {
|
|
V int `json:"v"`
|
|
Mode string `json:"mode"`
|
|
Inputs string `json:"inputs"`
|
|
Digest string `json:"digest"`
|
|
Users int `json:"users"`
|
|
// LeftOut are the assignments the shadow list leaves out, each with why.
|
|
LeftOut []Gap `json:"left_out"`
|
|
Compare Comparison `json:"compare"`
|
|
Loaded *delivery.UserListWritten `json:"loaded,omitempty"`
|
|
// Credentials says per user whether the vault holds its hash and how its line would differ (ADR 0547 clause 9).
|
|
Credentials Credentials `json:"credentials"`
|
|
BusNode string `json:"bus_node,omitempty"`
|
|
Gaps []Gap `json:"gaps"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
func (s UserListState) inputs() string {
|
|
s.At, s.Inputs = time.Time{}, ""
|
|
raw, _ := json.Marshal(s)
|
|
sum := sha256.Sum256(raw)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// Result is what one composition did.
|
|
type Result struct {
|
|
UserList UserListState `json:"user_list"`
|
|
Revision uint64 `json:"revision"`
|
|
Written bool `json:"written"`
|
|
Memberships int `json:"memberships"`
|
|
MembershipsWritten int `json:"memberships_written"`
|
|
Published []string `json:"published,omitempty"`
|
|
NotPublished []string `json:"not_published,omitempty"`
|
|
Assertions int `json:"assertions"`
|
|
AssertionsWritten int `json:"assertions_written"`
|
|
// Asserted is each module whose declared streams this pass created, `<module> at <digest>: created …`, and
|
|
// NotAsserted each creation refused, `<module> <stream>: <why>` (issue 1790).
|
|
Asserted []string `json:"asserted,omitempty"`
|
|
NotAsserted []string `json:"not_asserted,omitempty"`
|
|
// Refused is every manifest the indexer holds that this pass could not read, `manifest of <module>: <why>`: its
|
|
// module counts as placed nowhere (issue 1945).
|
|
Refused []string `json:"refused,omitempty"`
|
|
// RootNotFree is each machine the composition needed a root-free word for and did not get one, `<machine>: not
|
|
// free, <why>`: a verified sender is composed as from no root-free machine there (issue 1945).
|
|
RootNotFree []string `json:"root_not_free,omitempty"`
|
|
// Sealed is the list sealed to the bus node as the key `user-list.sealed` names it (ADR 0547 clause 7).
|
|
Sealed *SealedState `json:"sealed,omitempty"`
|
|
Errors []string `json:"errors,omitempty"`
|
|
PublishedOrAsserted string `json:"published_or_asserted"`
|
|
}
|
|
|
|
// NothingTakesEffect is what every composition says it did on the bus beyond its own state and the memberships.
|
|
const NothingTakesEffect = "memberships (on the assignments stream, a first one or one that only adds to the last published or drops only a verb the seat catalogue retired; any other loss stays a shadow difference), and each bucket, log or stream a manifest declares that the bus lacks, created once with the manifest's limits (the operator, 2026-10-26 08:54); the user list, the grants and any change to a stream the bus holds stay in shadow until take-over"
|
|
|
|
// Part is the grants part: it reads, calls and writes through these, and nothing else.
|
|
type Part struct {
|
|
Read Reader
|
|
Call Caller
|
|
Store Store
|
|
Emit Emitter
|
|
// Publish publishes memberships: the process's connection; nil in the tools, which ask the process instead.
|
|
Publish Publisher
|
|
// Create creates a declared stream the bus lacks: the process's connection; nil in the tools, which ask the process.
|
|
Create Creator
|
|
// Asker asks the operator (take-over, hand-back): the process's client of the router; nil in the tools, which write
|
|
// the request and let the process ask it.
|
|
Asker Asker
|
|
// Logf logs what no state or condition says (an error of acting on an answer): the process's; nil in the tools.
|
|
Logf func(string, ...any)
|
|
// Objects is the part's object bucket of sealed lists: the process's connection; nil in the tools.
|
|
Objects Objects
|
|
// Say says a condition to the operator: the process's; nil in the tools.
|
|
Say func(asks.Condition) error
|
|
// Forget takes away the record the asker kept of an ask it failed to publish; Nonce is a test's id suffix.
|
|
Forget func(id string) error
|
|
Nonce func() string
|
|
// deciding is set while a take-over is decided, so the composition it makes asks and reconciles nothing.
|
|
deciding atomic.Bool
|
|
Now func() time.Time
|
|
|
|
cache Manifests
|
|
mu sync.Mutex
|
|
last Result
|
|
made creations
|
|
// askSeen is the revision of the last ask to compose (KeyAsked) this part acted on.
|
|
askSeen uint64
|
|
// heldSaid is the reason of the held-list condition last said (empty: none raised); heldKnown is whether one was said
|
|
// since the start, so the first pass after a start always says its state (a condition raised before a restart is cleared
|
|
// by it). holdMu guards both.
|
|
holdMu sync.Mutex
|
|
heldSaid string
|
|
heldKnown bool
|
|
}
|
|
|
|
// Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing
|
|
// streams again even where they were created at this manifest before.
|
|
type Ask struct {
|
|
By string `json:"by"`
|
|
At time.Time `json:"at"`
|
|
Assert string `json:"assert,omitempty"`
|
|
}
|
|
|
|
// askedAssert is the module a new ask names to assert by hand, read once per ask; empty when none.
|
|
func (p *Part) askedAssert() string {
|
|
raw, rev, found, err := p.Store.Get(StateUserList, KeyAsked)
|
|
if err != nil || !found || rev == p.askSeen {
|
|
return ""
|
|
}
|
|
p.askSeen = rev
|
|
var a Ask
|
|
_ = json.Unmarshal(raw, &a)
|
|
return a.Assert
|
|
}
|
|
|
|
func (p *Part) now() time.Time {
|
|
if p.Now == nil {
|
|
return time.Now().UTC()
|
|
}
|
|
return p.Now().UTC()
|
|
}
|
|
|
|
// Compose composes the user list, the memberships and the assertions from what the bus holds now, compares them, and
|
|
// writes each of its own keys whose value changed, saying each change. It never stops at a refusal: a read refused is
|
|
// a gap, a write refused is said in Errors, and the next event composes again.
|
|
func (p *Part) Compose() Result {
|
|
// No lock is held across a round trip on the bus: the reads, calls, publishes and writes below take none, and the
|
|
// lock is taken only to keep the result, so a tool answers while a pass waits on the bus.
|
|
res := Result{PublishedOrAsserted: NothingTakesEffect}
|
|
defer func() {
|
|
p.mu.Lock()
|
|
p.last = res
|
|
p.mu.Unlock()
|
|
}()
|
|
in := ReadInputs(p.Read, &p.cache)
|
|
for _, g := range in.Refused {
|
|
res.Refused = append(res.Refused, g.Input+": "+g.Why)
|
|
}
|
|
recs, left := Records(in)
|
|
res.RootNotFree = WithRootFree(&recs, p.Call)
|
|
st := UserListState{V: 1, Mode: p.SwitchState(), LeftOut: left, Loaded: in.Loaded, BusNode: in.BusNode,
|
|
Gaps: append(append([]Gap(nil), in.Gaps...), MembershipGaps...), At: p.now()}
|
|
if st.LeftOut == nil {
|
|
st.LeftOut = []Gap{}
|
|
}
|
|
users, err := ComposeUsers(recs)
|
|
if err != nil {
|
|
res.Errors = append(res.Errors, "the shadow list could not be composed: "+err.Error())
|
|
}
|
|
users = WithHashes(users, in.Hashes)
|
|
st.Digest, st.Users = Digest(users), len(users)
|
|
loaded, err := LoadedUsers(p.Call)
|
|
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(recs)))
|
|
if err != nil {
|
|
st.Compare.Unread = err.Error()
|
|
}
|
|
st.Credentials = CredentialsOf(users, loaded, in.Hashes)
|
|
st.Inputs = st.inputs()
|
|
res.UserList = st
|
|
if raw, rev, found, err := p.Store.Get(StateUserList, KeyList); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be read: %v", StateUserList, err))
|
|
} else {
|
|
var was UserListState
|
|
if found && json.Unmarshal(raw, &was) == nil && was.Inputs == st.Inputs {
|
|
res.Revision = rev
|
|
} else if data, err := json.Marshal(st); err == nil {
|
|
if rev, err := p.Store.Put(StateUserList, KeyList, data); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be written: %v", StateUserList, err))
|
|
} else {
|
|
res.Revision, res.Written = rev, true
|
|
p.say(&res, EventUserList, map[string]any{"digest": st.Digest, "revision": rev, "users": st.Users,
|
|
"differing": len(st.Compare.Differing()), "mode": Shadow})
|
|
}
|
|
}
|
|
}
|
|
// The list itself, sealed to the bus node and named in `user-list.sealed` (ADR 0547 clause 7).
|
|
if list, n, leftOut, err := ComposeList(recs, in.Hashes); err != nil {
|
|
res.Errors = append(res.Errors, "the user list could not be composed whole: "+err.Error())
|
|
} else {
|
|
// The users the bus loaded: what a first list after take-over must keep where the part has not yet written one.
|
|
var bus []string
|
|
if st.Compare.Unread == "" {
|
|
bus = names(loaded)
|
|
}
|
|
p.writeSealed(&res, list, n, leftOut, in, bus)
|
|
}
|
|
ms := Memberships(recs)
|
|
res.Memberships = len(ms)
|
|
for _, key := range sortedKeys(ms) {
|
|
m, now := publish(p.Publish, ms[key])
|
|
switch {
|
|
case now:
|
|
res.Published = append(res.Published, key)
|
|
p.say(&res, EventMembership, map[string]any{"node": m.Node, "module": m.Module, "digest": m.Digest, "published": true})
|
|
case m.Published != MembershipUnchanged:
|
|
res.NotPublished = append(res.NotPublished, key+": "+m.Published)
|
|
}
|
|
data, _ := json.Marshal(m)
|
|
if p.changed(&res, StateMemberships, key, data) {
|
|
res.MembershipsWritten++
|
|
}
|
|
}
|
|
held, heldErr := HeldStreams(p.Call)
|
|
forced := ""
|
|
if p.Create != nil {
|
|
forced = p.askedAssert()
|
|
}
|
|
for _, module := range sortedKeys(in.Manifests) {
|
|
if !assigned(in, module) {
|
|
continue
|
|
}
|
|
a := Assert(module, in.Digests[module], in.Manifests[module], held, heldErr)
|
|
// Created only where the bus was read and lacks it, never changed where it holds it (issue 1790).
|
|
if p.Create != nil && heldErr == nil {
|
|
if made := createMissing(p.Create, &p.made, &a, in.Manifests[module], forced == module); len(made) > 0 {
|
|
res.Asserted = append(res.Asserted, fmt.Sprintf("%s at %s: created %s", module, a.Manifest, strings.Join(made, ", ")))
|
|
p.say(&res, EventAsserted, map[string]any{"module": module, "manifest": a.Manifest, "created": made})
|
|
}
|
|
for _, why := range notCreated(a) {
|
|
res.NotAsserted = append(res.NotAsserted, module+" "+why)
|
|
}
|
|
}
|
|
res.Assertions++
|
|
data, _ := json.Marshal(a)
|
|
if p.changed(&res, StateAssertions, module, data) {
|
|
res.AssertionsWritten++
|
|
}
|
|
}
|
|
if _, _, found, err := p.Store.Get(StateTakeover, KeySwitch); err == nil && !found {
|
|
data, _ := json.Marshal(Switch{V: 1, State: Shadow, Acts: []Act{}})
|
|
if _, err := p.Store.Put(StateTakeover, KeySwitch, data); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be written: %v", StateTakeover, err))
|
|
}
|
|
}
|
|
if !p.deciding.Load() {
|
|
if err := p.RaiseRequested(); err != nil {
|
|
res.Errors = append(res.Errors, err.Error())
|
|
}
|
|
if err := p.Reconcile(); err != nil {
|
|
res.Errors = append(res.Errors, err.Error())
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// Last is the result of the last pass this part made, kept under the lock alone.
|
|
func (p *Part) Last() Result {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.last
|
|
}
|
|
|
|
func assigned(in Inputs, module string) bool {
|
|
for _, ms := range in.Assigned {
|
|
for _, m := range ms {
|
|
if m == module {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// changed writes key when its value differs from the one held, and says whether it wrote.
|
|
func (p *Part) changed(res *Result, state, key string, data []byte) bool {
|
|
raw, _, found, err := p.Store.Get(state, key)
|
|
if err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", state, key, err))
|
|
return false
|
|
}
|
|
if found && bytes.Equal(raw, data) {
|
|
return false
|
|
}
|
|
if _, err := p.Store.Put(state, key, data); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", state, key, err))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (p *Part) say(res *Result, event string, body any) {
|
|
if p.Emit == nil {
|
|
return
|
|
}
|
|
if err := p.Emit.Emit(event, body); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("the event %s could not be said: %v", event, err))
|
|
}
|
|
}
|
|
|
|
func sortedKeys[V any](m map[string]V) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func names(us []User) []string {
|
|
out := make([]string, 0, len(us))
|
|
for _, u := range us {
|
|
out = append(out, u.User)
|
|
}
|
|
return out
|
|
}
|