The grants part wrote a binding into a membership on the bus module's created, but granted its subjects to no account but the ledger's, so distribution's program and the tool runner carrying it were refused their own key-value, object store and stream entries (issue 1966, ADR 0595 §1, design 25 § 12.2). The kept bindings now reach Declared in the same pass that writes them into the membership, and one function, boundOf, grants each to the module's own account and the tool runner, as its old section is granted; a process's bus.state names a key-value entry as an old state. The composed membership carries the bindings, so grants_membership's two sides agree. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
610 lines
26 KiB
Go
610 lines
26 KiB
Go
package grants
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
|
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
|
"mesh-grants/internal/buscompose"
|
|
)
|
|
|
|
// 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"
|
|
// StateGrants is the part's grants by module and fingerprint, the key layout of the controller's (grants.go).
|
|
StateGrants = grantsstate.PartGrantsState
|
|
KeyList = "list"
|
|
// 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{StateAccounts, StateAssertions, StateGrants, StateMemberships, StateUserList, StateWithheld}
|
|
|
|
// The events the part says.
|
|
const (
|
|
EventUserList = "user-list-changed"
|
|
EventMembership = "membership-changed"
|
|
// EventGrantWritten is said when the part wrote a grant of its state grants (ADR 0547 §5, ADR 0561 §2).
|
|
EventGrantWritten = "grant-written"
|
|
// 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, EventGrantWritten, EventHashesAccepted, 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)
|
|
// Keys and Delete are for the wanted accounts (accounts.go) and the grants (grants.go): every other state is only put.
|
|
Keys(state string) ([]string, error)
|
|
Delete(state, key string) 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`: 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 *nodeengine.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{}, ""
|
|
// When the bus node said it loaded a list is no input: the same list said again, at a later time and the key's later
|
|
// revision, composes nothing new (issue 1126: written again every 3 s, 2026-10-28).
|
|
if s.Loaded != nil {
|
|
l := *s.Loaded
|
|
l.Revision, l.At = 0, time.Time{}
|
|
s.Loaded = &l
|
|
}
|
|
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"`
|
|
// KeptLoaded are the users a pass composing part of the mesh gave the union of what it composes and what the loaded list
|
|
// and the list in force give them (keeploaded.go, issue 1966), read with grants_user_list's last pass.
|
|
KeptLoaded []string `json:"kept_loaded,omitempty"`
|
|
NotPublished []string `json:"not_published,omitempty"`
|
|
// Retired is, by `<node>.<module>`, the seat verbs its last published membership served that mesh-sdk's seat
|
|
// catalogue no longer has (Membership.Retired): the pass keeps it, since the state memberships holds the bare
|
|
// membership alone (issue 774).
|
|
Retired map[string][]string `json:"retired,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"`
|
|
// Consumers is what the pass found and created of the durable consumers an account needs (ADR 0561 §6).
|
|
Consumers *ConsumersState `json:"consumers,omitempty"`
|
|
// Accounts is what the pass wrote of the wanted accounts the vault mints from (ADR 0561 §2).
|
|
Accounts *AccountsWritten `json:"accounts,omitempty"`
|
|
// Grants is what the pass wrote of its grants (grants.go).
|
|
Grants *GrantsWritten `json:"grants,omitempty"`
|
|
// Withheld is what the pass wrote of the hearings it withholds (withheld.go).
|
|
Withheld *WithheldWritten `json:"withheld,omitempty"`
|
|
// PrincipalsRefused is each principal whose composed grants the composer refused, `<user>: <why>`: it keeps its grants
|
|
// of the list in force, or stays out where it holds none (refused.go); the condition mesh-grants.principal-refused says it.
|
|
PrincipalsRefused []string `json:"principals_refused,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 (in its own state memberships, unless one would leave a seat a placed module claims with no holder, or drop a capability its holder still asks for, which stays a shadow difference), 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 bus node follows, the grants and the account consumers (a module's on another module's stream too); a stream or consumer the bus holds only ever gains the subjects and filters a manifest adds"
|
|
|
|
// 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
|
|
// Create creates a declared stream the bus lacks: the process's connection; nil in the tools, which ask the process.
|
|
Create Creator
|
|
// 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
|
|
// Purging takes keys of its own state away whole, values and all (purge.go): the process's connection; nil in the
|
|
// tools, where a key taken away is deleted.
|
|
Purging Purger
|
|
// Consumers creates an account's durable consumer (consumers.go): the process's connection; nil in the tools, which
|
|
// compare only.
|
|
Consumers ConsumerCreator
|
|
// Bus is where the nodes reach the bus, which each wanted account carries (accounts.go): the process's own credential's;
|
|
// empty in the tools, which write no account.
|
|
Bus BusAddress
|
|
// Say says a condition to the operator: the process's; nil in the tools.
|
|
Say func(oc.Condition) error
|
|
Now func() time.Time
|
|
// SeatList is the seat list reader, which gates a module whose seats the list has not named (inputs.go, SeatGate); nil
|
|
// gates none.
|
|
SeatList SeatGate
|
|
|
|
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
|
|
// differSaid is the account consumers' differences last said as a condition, once differKnown (consumers.go).
|
|
differSaid string
|
|
differKnown bool
|
|
// behindSaid is the account consumers far behind last said as a condition, once behindKnown; behindSeen is each one
|
|
// found behind at the pass before, so one look at a burst raises nothing (consumers.go).
|
|
behindSaid string
|
|
behindKnown bool
|
|
behindSeen map[string]bool
|
|
// fullCleared is whether the old history-full key was cleared since the start (full.go, ADR 0595 §6).
|
|
fullCleared bool
|
|
// filterRefused is, by consumer, the refusal of the last addition of filters, so a standing refusal is said once.
|
|
filterRefused map[string]string
|
|
// refusedSaid is the refused principals last said as a condition, once refusedKnown (refused.go).
|
|
refusedSaid string
|
|
refusedKnown bool
|
|
// heldModulesSaid is the held modules' kept users last said as a condition, once heldModulesKnown (held.go).
|
|
heldModulesSaid string
|
|
heldModulesKnown bool
|
|
// missingSaid is the modules without a grant last said as a condition, once missingKnown (grants.go).
|
|
missingSaid string
|
|
missingKnown bool
|
|
// hashHold are the users whose hash changes held the pass's list, or nil (sealed.go).
|
|
hashHold []string
|
|
// brokers are the modules whose manifest claims mesh-broker at the last pass (bindings.go), guarded by mu.
|
|
brokers map[string]bool
|
|
// Views are the part's first views of the owners' states (firstviews.go), and FirstViewContext the process's life
|
|
// they ask in: the process's; nil in the tools.
|
|
Views *Views
|
|
FirstViewContext context.Context
|
|
fvSaid firstViewsSaid
|
|
}
|
|
|
|
// keepBrokers keeps the modules whose manifest claims the seat mesh-broker: the only emitters whose created, recreated
|
|
// and dropped the part applies (bindings.go).
|
|
func (p *Part) keepBrokers(in Inputs) {
|
|
brokers := map[string]bool{}
|
|
for module, m := range in.Manifests {
|
|
if m.ClaimsSeat(meshbroker.Seat) {
|
|
brokers[module] = true
|
|
}
|
|
}
|
|
p.mu.Lock()
|
|
p.brokers = brokers
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
// Brokers are the modules holding mesh-broker as the last pass read them.
|
|
func (p *Part) Brokers() map[string]bool {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
out := map[string]bool{}
|
|
for m := range p.brokers {
|
|
out[m] = true
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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"`
|
|
// Consumer is, from grants_consumer_match, one account consumer `<stream>.<name>` to bring to the composed
|
|
// configuration by hand (ADR 0561 §6).
|
|
Consumer string `json:"consumer,omitempty"`
|
|
// FirstView is, from grants_first_view_again, one first view to ask once more by hand (its state), or AllFirstViews.
|
|
FirstView string `json:"first-view,omitempty"`
|
|
}
|
|
|
|
// AllFirstViews asks every first view once more.
|
|
const AllFirstViews = "all"
|
|
|
|
// asked is a new ask, read once per ask; the zero Ask when none.
|
|
func (p *Part) asked() Ask {
|
|
raw, rev, found, err := p.Store.Get(StateUserList, KeyAsked)
|
|
if err != nil || !found || rev == p.askSeen {
|
|
return Ask{}
|
|
}
|
|
p.askSeen = rev
|
|
var a Ask
|
|
_ = json.Unmarshal(raw, &a)
|
|
return a
|
|
}
|
|
|
|
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()
|
|
}()
|
|
// A first view's condition the condition keeper did not take is said again at every pass until it lands.
|
|
p.Views.SayUnsaid()
|
|
in := p.inputs()
|
|
p.keepBrokers(in)
|
|
if err := p.withBound(&in); err != nil {
|
|
res.Errors = append(res.Errors, err.Error())
|
|
}
|
|
for _, g := range in.Refused {
|
|
res.Refused = append(res.Refused, g.Input+": "+g.Why)
|
|
}
|
|
// recs say what runs (memberships, consumers); listRecs what the user list and the grants carry: what runs and what
|
|
// the trunk names, so the list grants a build's subjects before the want moves to it (issue 774).
|
|
recs, _ := Records(in)
|
|
listRecs, left := ListRecords(in)
|
|
holdings := HoldingsOf(in)
|
|
res.RootNotFree = WithRootFree(&listRecs, p.Call)
|
|
recs.RootFree = listRecs.RootFree
|
|
st := UserListState{V: 1, Mode: InForce, 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, refused, err := ComposeEach(listRecs)
|
|
if err != nil {
|
|
res.Errors = append(res.Errors, "the shadow list could not be composed: "+err.Error())
|
|
}
|
|
// A refused principal keeps its grants of the list in force, said once per change (refused.go).
|
|
// And a module whose manifest is held (refused, or waiting for a judge that knows a field of it) and whose want names no
|
|
// build keeps its users of the list in force (held.go).
|
|
loaded, loadErr := LoadedUsers(p.Call)
|
|
inForce := p.inForceGrants()
|
|
candidates := names(loaded)
|
|
for user := range inForce {
|
|
candidates = append(candidates, user)
|
|
}
|
|
heldUsers := heldModuleUsers(in, candidates)
|
|
for user, why := range runnerUsersKept(listRecs, inForce) {
|
|
heldUsers[user] = why
|
|
}
|
|
allRefused := mergedRefusals(refused, heldUsers)
|
|
kept, _ := p.keptOf(allRefused)
|
|
for _, user := range sortedKeys(refused) {
|
|
res.PrincipalsRefused = append(res.PrincipalsRefused, user+": "+refused[user])
|
|
}
|
|
p.sayPrincipalsRefused(refused, kept)
|
|
p.sayHeldModules(heldUsers, kept)
|
|
users = withKept(users, kept)
|
|
// A pass that composes part of the mesh keeps what the bus loaded and the list in force give each user (keeploaded.go).
|
|
if why := in.keepsLoaded(); why != "" {
|
|
if union := KeepLoaded(users, loaded, inForce); len(union) > 0 {
|
|
merged := make(map[string]string, len(allRefused)+len(union))
|
|
keptAll := make(map[string]User, len(kept)+len(union))
|
|
for u, w := range allRefused {
|
|
merged[u] = w
|
|
}
|
|
for u, k := range kept {
|
|
keptAll[u] = k
|
|
}
|
|
for u, k := range union {
|
|
merged[u], keptAll[u] = "keeps what the loaded list gives it: "+why, k
|
|
}
|
|
users = withKept(slices.DeleteFunc(users, func(u User) bool { _, ok := union[u.User]; return ok }), union)
|
|
allRefused, kept = merged, keptAll
|
|
res.KeptLoaded = sortedKeys(union)
|
|
}
|
|
}
|
|
users = WithHashes(users, in.Hashes)
|
|
st.Digest, st.Users = Digest(users), len(users)
|
|
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(listRecs)))
|
|
if loadErr != nil {
|
|
st.Compare.Unread = loadErr.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": InForce})
|
|
}
|
|
}
|
|
}
|
|
// The accounts the vault mints from, said in the state only it reads (ADR 0561 §2).
|
|
if p.Store != nil {
|
|
p.writeAccounts(&res, in, listRecs)
|
|
}
|
|
// The hearings withheld of what runs, for the self-check's probe D-heard (withheld.go).
|
|
if p.Store != nil {
|
|
p.writeWithheld(&res, in, recs)
|
|
}
|
|
list, n, leftOut, dropped, err := ComposeList(listRecs, in.Hashes, allRefused, kept)
|
|
// The grants the assigner reads (ADR 0547 §4, §5): one that adds a permission only once the bus loaded a list that
|
|
// carries it (issue 774).
|
|
if p.Store != nil {
|
|
inForce, unknown := p.loadedContents(in, loaded, loadErr)
|
|
p.writeGrants(&res, in, listRecs, users, inForce, unknown)
|
|
}
|
|
// The list itself, sealed to the bus node and named in `user-list.sealed` (ADR 0547 clause 7).
|
|
if 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 must keep where the part has not yet written one.
|
|
var bus, lost []string
|
|
if st.Compare.Unread == "" {
|
|
bus = names(loaded)
|
|
lost = holdings.LastHolders(holdings.ByUsers(loaded), holdings.ByUsers(users))
|
|
}
|
|
p.writeSealed(&res, list, n, leftOut, dropped, in, bus, lost, users, loaded)
|
|
}
|
|
ms := Memberships(listRecs)
|
|
res.Memberships = len(ms)
|
|
members := make([]membership.Membership, 0, len(ms))
|
|
for _, m := range ms {
|
|
members = append(members, m.Membership)
|
|
}
|
|
seated := holdings.ByMemberships(members...)
|
|
// The memberships are written to the state alone and never said on their subject, which nothing reads any more
|
|
// (novox/hq ADR 0571 clause 10, issue 774; unconditional since the controller's end, issue 1126).
|
|
for _, key := range sortedKeys(ms) {
|
|
// The last membership is the state's own (ADR 0571 clause 7), never the assignments stream's, which goes with the
|
|
// controller's leftovers (novox/hq ADR 0590 §6).
|
|
// The bus module's bindings of the module, as it said them created (bindings.go, ADR 0595 §1, §7).
|
|
if binds, err := p.bindingsOf(ms[key].Module); err != nil {
|
|
res.Errors = append(res.Errors, err.Error())
|
|
} else if binds != nil {
|
|
withBinds := ms[key]
|
|
withBinds.Membership.Bindings = binds
|
|
withBinds.Digest = digestOf(withBinds.Membership)
|
|
ms[key] = withBinds
|
|
}
|
|
was, _, found, err := p.Store.Get(StateMemberships, key)
|
|
if err != nil {
|
|
res.NotPublished = append(res.NotPublished, key+": not published: the membership last published cannot be read from its own "+StateMemberships+": "+err.Error())
|
|
continue
|
|
}
|
|
// A pass that composes part of the mesh only grows a published membership: it publishes the union of the one last
|
|
// published and the one composed (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it
|
|
// serves and still receives what the pass that ends the hold needs.
|
|
why := in.keepsLoaded()
|
|
if why == "" && in.HadFailed[key] != "" {
|
|
why = "the build it runs is unread (" + in.HadFailed[key] + ")"
|
|
}
|
|
if found && why != "" {
|
|
if bare, err := membership.BareGrantsMembership(was); err == nil {
|
|
if body, err := json.Marshal(ms[key].Membership); err == nil {
|
|
if grown, kept, err := UnionMembership(bare, body); err == nil && len(kept) > 0 {
|
|
var u membership.Membership
|
|
if json.Unmarshal(grown, &u) == nil {
|
|
withUnion := ms[key]
|
|
withUnion.Membership = u
|
|
withUnion.Digest = digestOf(u)
|
|
ms[key] = withUnion
|
|
res.NotPublished = append(res.NotPublished, key+": kept beyond this pass: "+why+", this pass composes it without "+strings.Join(kept, ", "))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
m, now := publish(was, found, ms[key], holdings, seated)
|
|
// Published is written to the state, where every runtime reads it (process.FollowMembership); only one
|
|
// published, so a membership held back never reaches a reader.
|
|
if now {
|
|
body := membership.EncodeGrantsMembership(m.Membership)
|
|
if _, err := p.Store.Put(StateMemberships, key, body); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateMemberships, key, err))
|
|
m.Published, now = "not published (tried again at the next event): its own "+StateMemberships+" cannot be written", false
|
|
} else {
|
|
res.MembershipsWritten++
|
|
}
|
|
}
|
|
// One unchanged is written bare once where the state still holds the wrapper written before 2026-10-30.
|
|
if m.Published == MembershipUnchanged && p.changed(&res, StateMemberships, key, membership.EncodeGrantsMembership(m.Membership)) {
|
|
res.MembershipsWritten++
|
|
}
|
|
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)
|
|
}
|
|
if len(m.Retired) > 0 {
|
|
if res.Retired == nil {
|
|
res.Retired = map[string][]string{}
|
|
}
|
|
res.Retired[key] = m.Retired
|
|
}
|
|
}
|
|
p.clearFull()
|
|
held, heldErr := HeldStreams(p.Call)
|
|
var ask Ask
|
|
if p.Create != nil || p.Consumers != nil {
|
|
ask = p.asked()
|
|
}
|
|
forced := ask.Assert
|
|
for _, module := range sortedKeys(in.Manifests) {
|
|
if !assigned(in, module) {
|
|
continue
|
|
}
|
|
a := Assert(module, in.Digests[module], in.Manifests[module], in.Nodes, 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, raises := createMissing(p.Create, &p.made, &a, in.Manifests[module], in.Nodes, forced == module); len(made) > 0 {
|
|
res.Asserted = append(res.Asserted, fmt.Sprintf("%s at %s: created %s", module, a.Manifest, strings.Join(made, ", ")))
|
|
if p.Logf != nil {
|
|
p.Logf("%s at %s: asserted on the bus: %s", module, a.Manifest, strings.Join(made, ", "))
|
|
}
|
|
p.say(&res, EventAsserted, map[string]any{"module": module, "manifest": a.Manifest, "created": made, "raised": raises})
|
|
}
|
|
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++
|
|
}
|
|
}
|
|
p.consumers(&res, recs, ask.Consumer)
|
|
p.sayFirstViews(&res)
|
|
if ask.FirstView != "" && p.Views != nil && p.FirstViewContext != nil {
|
|
state := ask.FirstView
|
|
if state == AllFirstViews {
|
|
state = ""
|
|
}
|
|
if asked, err := p.Views.Again(p.FirstViewContext, state, "by hand: "+ask.By); err != nil {
|
|
res.Errors = append(res.Errors, err.Error())
|
|
} else if p.Logf != nil {
|
|
p.Logf("asked the first views %v once more by hand, for %s", asked, ask.By)
|
|
}
|
|
}
|
|
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 {
|
|
// The node-engine runs on every node though no node is assigned it (novox/hq ADR 0563 §2, §3): its manifest is
|
|
// placed everywhere, so its declared state is asserted and created as a placed module's.
|
|
if module == buscompose.EngineModule {
|
|
return true
|
|
}
|
|
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
|
|
}
|
|
|
|
// inputs reads what the part composes from, a module whose seats the seat list has not named held back (SeatGate).
|
|
func (p *Part) inputs() Inputs {
|
|
p.cache.mu.Lock()
|
|
p.cache.gate = p.SeatList
|
|
p.cache.mu.Unlock()
|
|
return ReadInputs(p.Read, &p.cache)
|
|
}
|