A node's plans took 2.6-13.6 s of CPU on the provisioner: one composition resolved every machine of the mesh some two hundred times (who is on the private network, and the rest of the mesh, worked out again on every look), three compositions did so again each, and the holder composed twice. A composer now works each out once over its unchanging Input; Composing gives Resolve, Render and Generators one composer; PlansHeldTwice composes the wanted and the newest builds over one read of the bus, says where the time went by phase, and the states reader keeps a build's take-in by its content. WatchedStates takes a process's own writes into the copy at once, so a holder reads its own states from it. Measured on a captured novox mesh: 11.7 s to 0.49 s per composition of an unchanged node, plans and provisions identical on all four nodes. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
148 lines
4.7 KiB
Go
148 lines
4.7 KiB
Go
package frombus
|
|
|
|
// **A node's plans at two sets of builds, over one read** (issue 1954; novox/hq ADR 0570 clause 2, ADR 0580 clause 2):
|
|
// the seat mesh-provisioner's holder composes each node at the builds its wants name and at the newest build of each
|
|
// placed module. Both are composed from one read of the bus (placement, records, the indexer's modules and manifests,
|
|
// values: base); only the builds the node's modules are composed from differ, each build read once for both, and a
|
|
// build taken in once per content by a reader that keeps it (StatesReader). Where the time went is said by phase.
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// HeldPlans is one scope's plans, as PlansHeld answers them: err a refusal that is no single module's.
|
|
type HeldPlans struct {
|
|
Plans map[string]catalogue.Plan
|
|
Held map[string]string
|
|
Err error
|
|
}
|
|
|
|
// Phase is one step of a composition and how long it took.
|
|
type Phase struct {
|
|
Name string `json:"phase"`
|
|
Took time.Duration `json:"took"`
|
|
}
|
|
|
|
// PlansHeldTwice is PlansHeld at s and at s with no kept builds (each placed module at its newest build), over one
|
|
// read of the bus: what each answers is what PlansHeld answers for that scope. A read that fails is both scopes' err.
|
|
// took names each phase in order: the read, then each scope's builds and its plans.
|
|
func PlansHeldTwice(ctx context.Context, r Reader, mesh Mesh, s Scope) (at, newest HeldPlans, took []Phase) {
|
|
start := time.Now()
|
|
mark := func(name string) {
|
|
now := time.Now()
|
|
took = append(took, Phase{Name: name, Took: now.Sub(start)})
|
|
start = now
|
|
}
|
|
b, err := readBase(ctx, r, mesh, s)
|
|
mark("read")
|
|
builds := onceBuilds(r)
|
|
newestScope := s
|
|
newestScope.KeptAt = nil
|
|
for _, sc := range []struct {
|
|
scope Scope
|
|
into *HeldPlans
|
|
name string
|
|
}{{s, &at, "wanted"}, {newestScope, &newest, "newest"}} {
|
|
held := map[string]string{}
|
|
sc.into.Held = held
|
|
refuse := func(module string, err error) { held[module] = err.Error() }
|
|
b.refuseOwn(refuse)
|
|
if err != nil {
|
|
sc.into.Err = err
|
|
continue
|
|
}
|
|
in, err := b.finish(ctx, r, builds, mesh, sc.scope, b.cloned(), refuse, nil)
|
|
mark("builds at " + sc.name)
|
|
if err != nil {
|
|
sc.into.Err = err
|
|
continue
|
|
}
|
|
sc.into.Plans, sc.into.Err = plansHeldOf(in, s.Node, held)
|
|
mark("plans at " + sc.name)
|
|
}
|
|
return at, newest, took
|
|
}
|
|
|
|
// onceBuilds is r's Build, each module's build at a fingerprint read once: both scopes of one composition read the
|
|
// builds of the same moment.
|
|
func onceBuilds(r Reader) func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) {
|
|
type answer struct {
|
|
b *delivery.BuilderBuild
|
|
err error
|
|
}
|
|
read := map[string]answer{}
|
|
return func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) {
|
|
key := module + " " + fp
|
|
if a, ok := read[key]; ok {
|
|
return a.b, a.err
|
|
}
|
|
b, err := r.Build(ctx, module, fp)
|
|
read[key] = answer{b, err}
|
|
return b, err
|
|
}
|
|
}
|
|
|
|
// takenInKeeper is a Reader that keeps what a build's take-in made (StatesReader does): the resolved manifest's bytes,
|
|
// by the content it was made from, so a composition made again on every change takes in only a build it has not seen.
|
|
type takenInKeeper interface {
|
|
takenIn(key string) ([]byte, bool)
|
|
keepTakenIn(key string, raw []byte)
|
|
}
|
|
|
|
// takenInKey is what a take-in is made from: the artifact store's address and the build's record (its manifest's digest, its artifacts by digest), read
|
|
// whole; an archive is checked against its digest on every read, so the same record takes in to the same bytes.
|
|
func takenInKey(module, fp, address string, b delivery.BuilderBuild) (string, bool) {
|
|
raw, err := json.Marshal(b)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
sum := sha256.Sum256(append([]byte(module+" "+fp+" "+address+" "), raw...))
|
|
return hex.EncodeToString(sum[:]), true
|
|
}
|
|
|
|
// takenInAtMost bounds the take-ins a reader keeps; past it they are emptied, which costs only a take-in again.
|
|
const takenInAtMost = 512
|
|
|
|
// keptTakeIns is a reader's kept take-ins.
|
|
type keptTakeIns struct {
|
|
mu sync.Mutex
|
|
kept map[string][]byte
|
|
}
|
|
|
|
func (k *keptTakeIns) takenIn(key string) ([]byte, bool) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
raw, ok := k.kept[key]
|
|
return raw, ok
|
|
}
|
|
|
|
func (k *keptTakeIns) keepTakenIn(key string, raw []byte) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
if k.kept == nil || len(k.kept) >= takenInAtMost {
|
|
k.kept = map[string][]byte{}
|
|
}
|
|
k.kept[key] = raw
|
|
}
|
|
|
|
// PhasesSaid says the phases in one line, each in milliseconds.
|
|
func PhasesSaid(took []Phase) string {
|
|
out := ""
|
|
for i, p := range took {
|
|
if i > 0 {
|
|
out += ", "
|
|
}
|
|
out += fmt.Sprintf("%s %d ms", p.Name, p.Took.Milliseconds())
|
|
}
|
|
return out
|
|
}
|