Files
mesh-delivery/modules/mesh-grants/internal/grants/keptpoints.go
T
jschoubben f0c4d564d2 GR16 §554: the recovery module's account is granted the bus names of kept data
The account of the module claiming the seat mesh-recovery is granted the stream snapshot and
restore subjects with their chunk subjects, a stream's deletion and a durable consumer's
creation, only for the bus names of every kept data of every manifest the part holds and of
every restore point or aside kept-points names; never a stream of the mesh's own (CONTROL,
NODES, EVENTS, DEAD_LETTER*, PROV_APPLIED, CHECKS_*, SEAT_*, a seat's KV_). The writers table
names the account as a deleter of kept data's streams (ADR 0672 §4).

Builds: design 47 G §554, §565, A-230

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 09:17:07 +01:00

316 lines
9.6 KiB
Go

package grants
import (
"context"
"encoding/json"
"fmt"
"slices"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/meshrecovery"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"mesh-grants/internal/buscompose"
)
// **The recovery module's restore points' names, read through `kept-points`** (design 47 G §565, A-230). The bus names of the
// recovery module's account include those a restore point (or an aside) names. The part reads them by its `invokes` of
// `seat:mesh-recovery.kept-points`, a kept call (ADR 0668 §1: made again until it answers), at its start and again on every `ended`
// with outcome `done`, every `point-removed` and every `expiry-ended` of the seat, which it consumes. Until the call first answers
// after a start, the part grants the names it last granted from it, kept in its state memberships (KeptNamesKey): it takes no name
// away before that answer, and grants_first_views says the wait.
// KeptPointsVerb is the seat's verb as the manifest's invokes names it.
const (
CallKeptPoints = "seat:" + meshrecovery.Seat + "." + meshrecovery.VerbKeptPoints
// KeptNamesKey is the key of the part's own state memberships that keeps the names last read from `kept-points`; never a
// `<node>.<module>` key, since no machine is named `kept-names`.
KeptNamesKey = "kept-names." + meshrecovery.Seat
)
// KeptNames is the value of KeptNamesKey: every bus name a restore point or an aside of the recovery module names.
type KeptNames struct {
V int `json:"v"`
Names []string `json:"names"`
At time.Time `json:"at,omitzero"`
}
// keptPointsAnswer is the part of kept-points' answer the part reads: `points[].names` and `asides[].names` (A-230). The contract's
// Point gains `names` at the join, as its Aside has it; until then this is the wire's form.
type keptPointsAnswer struct {
Points []struct {
Names []string `json:"names"`
} `json:"points"`
Asides []struct {
Names []string `json:"names"`
} `json:"asides"`
}
// KeptPointsStatus is what grants_first_views says of the kept call.
type KeptPointsStatus struct {
Seat string `json:"seat"`
Verb string `json:"verb"`
Asked time.Time `json:"asked,omitzero"`
Answered time.Time `json:"answered,omitzero"`
Asks int `json:"asks"`
// Names is how many names the last answer named (or, while waiting, the names kept from the last answer before the start).
Names int `json:"names"`
// Waiting says the call has not answered since the start: the part grants the names it kept.
Waiting bool `json:"waiting"`
Why string `json:"why,omitempty"`
Error string `json:"error,omitempty"`
}
// KeptPointsView is the kept call of `kept-points`.
type KeptPointsView struct {
// Ask calls the seat's verb and answers its reply (the process's connection).
Ask func(ctx context.Context) (json.RawMessage, error)
Changed func()
Logf func(string, ...any)
Now func() time.Time
// Pause waits d between two asks that failed and answers false once ctx ended; nil waits on the context's bound.
Pause func(ctx context.Context, d time.Duration) bool
// Wait is the first wait between two asks that failed (ten seconds when zero), doubling to MaxWait (five minutes when zero).
Wait time.Duration
MaxWait time.Duration
mu sync.Mutex
ctx context.Context
part *Part
st KeptPointsStatus
asking bool
again bool
}
func (v *KeptPointsView) now() time.Time {
if v.Now != nil {
return v.Now().UTC()
}
return time.Now().UTC()
}
func (v *KeptPointsView) logf(format string, args ...any) {
if v.Logf != nil {
v.Logf(format, args...)
}
}
// Status is the kept call's status now.
func (v *KeptPointsView) Status() KeptPointsStatus {
v.mu.Lock()
defer v.mu.Unlock()
s := v.st
s.Seat, s.Verb = meshrecovery.Seat, meshrecovery.VerbKeptPoints
return s
}
// Start asks at the part's start, on a goroutine of its own, and until it answers.
func (v *KeptPointsView) Start(ctx context.Context, p *Part) {
v.mu.Lock()
v.ctx, v.part = ctx, p
v.st.Waiting = true
v.mu.Unlock()
if kept, err := p.keptNames(); err == nil {
v.mu.Lock()
v.st.Names = len(kept)
v.mu.Unlock()
}
go v.askUntil(ctx, p, "start")
}
// Heard asks again when an event heard is one that changes the points: `ended` with outcome `done`, `point-removed`, `expiry-ended`
// of the seat mesh-recovery, named `mesh-recovery.<event>` or by its bus subject.
func (v *KeptPointsView) Heard(event string, body []byte) {
name, ok := recoveryEvent(event)
if !ok || !changesPoints(name, body) {
return
}
v.mu.Lock()
ctx, p := v.ctx, v.part
v.mu.Unlock()
if p == nil {
return // not started: the start's ask reads what the event changed
}
go v.askUntil(ctx, p, "the seat's "+name)
}
func recoveryEvent(event string) (string, bool) {
for _, prefix := range []string{meshrecovery.Seat + ".", "mesh.seat." + meshrecovery.Seat + ".event."} {
if name, ok := strings.CutPrefix(event, prefix); ok {
return name, true
}
}
return "", false
}
func changesPoints(name string, body []byte) bool {
switch name {
case meshrecovery.EventPointRemoved, meshrecovery.EventExpiryEnded:
return true
case meshrecovery.EventEnded:
var e meshrecovery.EndedEvent
return json.Unmarshal(body, &e) != nil || e.Outcome == meshrecovery.EndedEventOutcomeDone
}
return false
}
// IsKeptPointsEvent says an event heard is one the kept call asks again on.
func IsKeptPointsEvent(event string, body []byte) bool {
name, ok := recoveryEvent(event)
return ok && changesPoints(name, body)
}
func (v *KeptPointsView) pause(ctx context.Context, d time.Duration) bool {
if v.Pause != nil {
return v.Pause(ctx, d)
}
t, cancel := context.WithTimeout(ctx, d)
<-t.Done()
cancel()
return ctx.Err() == nil
}
// askUntil asks until the seat answers or ctx ends, from ten seconds, doubling, at most every five minutes. An ask made while one
// runs is made once more when it ends, so an event heard meanwhile is never lost.
func (v *KeptPointsView) askUntil(ctx context.Context, p *Part, why string) {
v.mu.Lock()
if v.asking {
v.again = true
v.mu.Unlock()
return
}
v.asking = true
v.st.Why = why
v.mu.Unlock()
wait, most := v.Wait, v.MaxWait
if wait == 0 {
wait = 10 * time.Second
}
if most == 0 {
most = 5 * time.Minute
}
for {
v.mu.Lock()
v.st.Asked = v.now()
v.st.Asks++
v.mu.Unlock()
raw, err := v.Ask(ctx)
var n int
if err == nil {
n, err = p.applyKeptPoints(raw, v.wake)
}
if err != nil {
v.mu.Lock()
v.st.Error = err.Error()
v.mu.Unlock()
v.logf("%s is not answered (%v): asked again in %s; the names last kept stay granted", CallKeptPoints, err, wait)
if !v.pause(ctx, wait) {
v.mu.Lock()
v.asking = false
v.mu.Unlock()
return
}
if wait *= 2; wait > most {
wait = most
}
continue
}
v.mu.Lock()
v.st.Answered, v.st.Waiting, v.st.Error, v.st.Names = v.now(), false, "", n
again := v.again
v.again = false
if !again {
v.asking = false
}
v.mu.Unlock()
v.wake()
if !again {
return
}
wait = v.Wait
if wait == 0 {
wait = 10 * time.Second
}
}
}
func (v *KeptPointsView) wake() {
if v.Changed != nil {
v.Changed()
}
}
// keptNames are the names last read from kept-points, from the part's own state: none before the first answer ever.
func (p *Part) keptNames() ([]string, error) {
raw, _, found, err := p.Store.Get(StateMemberships, KeptNamesKey)
if err != nil {
return nil, fmt.Errorf("its own %s %s cannot be read: %w", StateMemberships, KeptNamesKey, err)
}
if !found {
return nil, nil
}
var k KeptNames
if err := json.Unmarshal(raw, &k); err != nil {
return nil, fmt.Errorf("its own %s %s does not read: %w", StateMemberships, KeptNamesKey, err)
}
return k.Names, nil
}
// applyKeptPoints replaces the kept names with those of an answer (a name no point names any more is taken away), and calls wake
// when they changed. It answers how many names the answer named.
func (p *Part) applyKeptPoints(raw json.RawMessage, wake func()) (int, error) {
var a keptPointsAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return 0, fmt.Errorf("%s answered what it does not read: %w", CallKeptPoints, err)
}
var names []string
for _, pt := range a.Points {
names = append(names, pt.Names...)
}
for _, as := range a.Asides {
names = append(names, as.Names...)
}
slices.Sort(names)
names = slices.Compact(names)
was, err := p.keptNames()
if err != nil {
return 0, err
}
if slices.Equal(was, names) {
return len(names), nil
}
body, _ := json.Marshal(KeptNames{V: 1, Names: names, At: p.now()})
if _, err := p.Store.Put(StateMemberships, KeptNamesKey, body); err != nil {
return 0, fmt.Errorf("its own %s %s cannot be written: %w", StateMemberships, KeptNamesKey, err)
}
if wake != nil {
wake()
}
return len(names), nil
}
// withKeptNames sets the names the recovery module's account is granted for restore points on in (Inputs.KeptNames), read from
// the part's own state in one place for the pass and for its tools.
func (p *Part) withKeptNames(in *Inputs) error {
names, err := p.keptNames()
if err != nil {
return err
}
in.KeptNames = names
return nil
}
// recoveryNamesOf are the bus names of kept data of every manifest of the catalog graph the part holds, and the names a restore
// point or an aside names (Inputs.KeptNames): a name a manifest left stays while a point names it. Sorted, once each. Pure.
func recoveryNamesOf(in Inputs) []string {
names := slices.Clone(in.KeptNames)
for _, group := range []map[string]catalogue.Manifest{in.Manifests, in.Running, in.Ran, in.RanByFingerprint, in.Had, in.Held} {
for _, m := range group {
names = append(names, buscompose.KeptBusNames(m)...)
}
}
slices.Sort(names)
return slices.Compact(names)
}