// Package view derives the operator's rows from the delivery buckets. package view import ( "encoding/json" "fmt" "slices" "sort" "strings" "sync" "time" "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" ) // Entry is one change of a bucket's key, as the watch delivers it. type Entry struct { Bucket string Key string Value []byte At time.Time Deleted bool // Revision is the bucket revision of the change, when the watch gives one (the source revision is the gate's source key's). Revision uint64 } // Row is one module on one node. type Row struct { Node string `json:"node"` Module string `json:"module"` Status string `json:"status"` Assigned string `json:"assigned,omitempty"` Have string `json:"have,omitempty"` Phase string `json:"phase,omitempty"` Health string `json:"health,omitempty"` Since time.Time `json:"since,omitzero"` Detail string `json:"detail,omitempty"` Waits string `json:"waits,omitempty"` // RequiredBy is what pulled the module onto the node, for an assignment that is pulled in. RequiredBy string `json:"required-by,omitempty"` Error string `json:"error,omitempty"` } // NodeRow is a node's own facts as counts. type NodeRow struct { Node string `json:"node"` Windows int `json:"windows"` Busy int `json:"busy"` Strays int `json:"strays"` Since time.Time `json:"since,omitzero"` Error string `json:"error,omitempty"` } // Snapshot is everything the view shows. type Snapshot struct { Rows []Row `json:"rows"` Nodes []NodeRow `json:"nodes"` // Offline is every node and part instance whose presence is offline (the watchman's word). Offline []OfflineRow `json:"offline"` // Stuck is every key that stopped moving and has not moved again. Stuck []StuckRow `json:"stuck"` // Unreadable is every watchman value the view could not read, named by its key: never dropped silently. Unreadable []UnreadableRow `json:"unreadable"` // Groups is every candidate group the indexer holds: the pull requests that share a branch, with the modules each touches. Groups []GroupRow `json:"groups"` // Fingerprints is every module the indexer fingerprinted at a trunk merge, beside its newest successful trunk build's. Fingerprints []FingerprintRow `json:"fingerprints"` // Asks is every ask the gate holds that has no answer yet: what waits for the operator's Approve. Asks []AskRow `json:"asks"` // Wants is every want of the assigner beside the have of its node and where it stands (novox/hq ADR 0539 §8, issue 1906 D7). Wants []WantRow `json:"wants"` // Source is the state of the mesh's source revision, derived from the wants and the haves (ADR 0429 §1): recorded by no part. // Nil when the gate holds no source yet. Source *SourceRow `json:"source,omitempty"` // Backlog is, for each language and kind of build ask with one waiting or held, the asks waiting for a node, the asks a node // holds and the oldest waiting ask's age (issue 1887 A8a). Backlog []BacklogRow `json:"backlog"` } // The states of a want against the have of its node. const ( WantApplied = "applied" // the node runs the want's fingerprint, running and healthy WantPending = "pending" // the node has not yet run the want's fingerprint, or not healthy yet WantFailed = "failed" // the node runs the want's fingerprint and its health check failed WantHeldBack = "held back" // the assigner holds the node back from the newest fingerprint (Rule), or set it back WantRemoved = "removed" // the node's want is removed: the module is no longer assigned to it ) // WantRow is a want of the assigner beside the have of its node: the fingerprint wanted, the one the node runs with its phase and // health, the state of the two, and for a want held back the newest fingerprint, the rule and the assigner's words. type WantRow struct { Node string `json:"node"` Module string `json:"module"` Want string `json:"want,omitempty"` Have string `json:"have,omitempty"` Phase string `json:"phase,omitempty"` Health string `json:"health,omitempty"` State string `json:"state"` Newest string `json:"newest,omitempty"` Rule int `json:"rule,omitempty"` Words string `json:"words,omitempty"` // Judgement is the controller's judgement of the move to the composition the want names (recreates, moves-data, neither or // cannot-judge), and Judged its words; empty when the controller wrote none (ADR 0539 §14). Judgement string `json:"judgement,omitempty"` Judged string `json:"judged,omitempty"` } // The states of a source revision (ADR 0429 §1). const ( SourceDelivered = "delivered" SourceNotDelivered = "not delivered" ) // SourceRow is the mesh's source revision (the revision of the gate's source key) and whether it is delivered: every want equals its // module's fingerprint at the indexer and every have equals its want, running and healthy, and every node a module is assigned to // has a want. Waiting names each module on a node that is not there yet, and why. type SourceRow struct { Revision uint64 `json:"revision"` State string `json:"state"` Waiting []string `json:"waiting,omitempty"` // Wants is how many wants of modules at the indexer were looked at; a revision with none is delivered of nothing. Wants int `json:"wants"` } // AskRow is an open ask (novox/hq ADR 0437 §10): the gate's ask with no answer under its key in the teller's answers. type AskRow struct { Key string `json:"key"` Group string `json:"group"` Headline string `json:"headline"` Words string `json:"words"` Pulls []string `json:"pulls"` Expires time.Time `json:"expires,omitzero"` How string `json:"how"` // Options is what the view offers to answer with, for an ask that needs no proof of who answered (the assigner's hold and // restore, ADR 0528 clause 6); Chosen is the option the view wrote that the teller has not filled yet. Options []string `json:"options,omitempty"` Chosen string `json:"chosen,omitempty"` } // AnswerOnThePhone is what an open ask says about answering it: the view only shows, and the Approve is answered on the // operator's phone (ADR 0437 §10: answering in the view is release 5's). const AnswerOnThePhone = "answer on the phone" // AnswerInTheView is what an open ask that needs no proof of who answered says (ADR 0528 clause 6): the operator answers it here. const AnswerInTheView = "answer in the view" // GroupRow is a candidate group (novox/hq ADR 0437 §5): the branch its pull requests share and the pull requests. type GroupRow struct { Group string `json:"group"` Pulls []GroupPull `json:"pulls"` } // GroupPull is one pull request of a group, its head shortened to 12 digits, and the modules it touches. type GroupPull struct { Repository string `json:"repository"` Number int `json:"number"` Head string `json:"head"` Modules []string `json:"modules"` } // The states of a module's fingerprint against its newest successful trunk build's. const ( FPSame = "same" FPDiffers = "differs" FPNone = "none" FPNoBuild = "no build" // The states of a fingerprint of the new form against the builder's key of it (I5, issue 1851): built (the key is done), // build failed (Reason is its error), building (queued, claimed or building; Reason says what it waits for) and no key. FPBuilt = "built" FPBuildFailed = "build failed" FPBuilding = "building" FPNoKey = "no key" ) // FingerprintRow compares the indexer's fingerprint of a module at a trunk merge with the source fingerprint its newest // successful trunk build recorded. State is same, differs (both are given), none (the indexer could not fingerprint it: // Reason says why) or no build (the indexer's alone is given, no successful trunk build recorded one). type FingerprintRow struct { Module string `json:"module"` Repository string `json:"repository,omitempty"` Commit string `json:"commit,omitempty"` State string `json:"state"` Indexer string `json:"indexer,omitempty"` Build string `json:"build,omitempty"` Reason string `json:"reason,omitempty"` } // OfflineRow is a node ("node") or a part's instance ("part") that is offline, since the presence entry's time. type OfflineRow struct { Kind string `json:"kind"` Node string `json:"node,omitempty"` Part string `json:"part,omitempty"` Instance string `json:"instance,omitempty"` Since time.Time `json:"since,omitzero"` } // StuckRow is a key that stopped moving: its bucket and key, what it waited for and since when. type StuckRow struct { Bucket string `json:"bucket"` Key string `json:"key"` Waited string `json:"waited,omitempty"` Since time.Time `json:"since,omitzero"` } // UnreadableRow is a value of the watchman's buckets that did not decode, with the reader's error. type UnreadableRow struct { Bucket string `json:"bucket"` Key string `json:"key"` Error string `json:"error"` } // Model holds the rows' inputs in memory and nothing else: a restart fills it again from the buckets' last values. type Model struct { mu sync.Mutex assigns map[string]assignment builds map[string]buildEntry // by .build. groups map[string]groupEntry // the indexer's candidates, by group modules map[string]moduleEntry // the indexer's modules, by module keys map[string]keyEntry // the builder's build keys, by . asks map[string]askRecord // the gate's asks, by key wants map[string]wantEntry // the assigner's wants, by . source *sourceEntry // the gate's source key deliveredEvent func(DeliveredEvent) motionEvent func(MotionEvent) told map[string]bool // the motion events said, by their ids initial map[string]bool // the buckets whose watch handed over what stood at its start said uint64 // the source revision whose delivery was said composed map[string]composedEntry // the controller's digests and judgements, by .. aasks map[string]assignerAskRecord // the assigner's asks (hold, restore), by key chosen map[string]delivery.ViewAnswer // the answers the view wrote, by ask key writer Writer backlog *Backlog answers map[string]string // by key: the router id of the asking the teller's answer is for ("" when unreadable) answerOf map[string]delivery.Answer // by key: the teller's answer whole, when readable (holds.go) holds map[string]heldEntry // the assigner's holds, by held key (holds.go) newest map[string]string // module -> id haves map[string]haveEntry nodes map[string]nodeEntry // the watchman's buckets, by key presence map[string]presenceEntry stuck map[string]stuckEntry } type presenceEntry struct { value delivery.Presence err string at time.Time } type stuckEntry struct { value delivery.Stuck err string } // assignment holds only the fields of the controller's Assignment the view reads. type assignment struct { Node string `json:"node"` Module string `json:"module"` Build string `json:"build"` Commit string `json:"commit"` Since time.Time `json:"since"` Ended time.Time `json:"ended"` State string `json:"state"` PulledIn bool `json:"pulled-in"` RequiredBy string `json:"required-by"` } // open says whether the value is an assignment that stands for the module on its node, as the controller's // Assignment.Stands does. A module pulled in by another stands too. func (a assignment) open() bool { return a.Ended.IsZero() && a.State == "" } // build holds only the fields of the controller's Build record the view reads. A record with no state and no failure // is a done build. type build struct { ID string `json:"id"` Module string `json:"module"` State string `json:"state"` Commit string `json:"commit"` Failed string `json:"failed"` } func (b build) done() bool { return b.State == "" && b.Failed == "" } // buildEntry is a build as the view reads it: its own fields (state, failure) and the SDK's record (the source // fingerprint, the ref, the time), decoded as the controller writes them. type buildEntry struct { build rec delivery.BuildRecord } type groupEntry struct { value delivery.Candidate err string } type keyEntry struct { value delivery.BuilderBuild err string } type wantEntry struct { value delivery.Want err string at time.Time } type composedEntry struct { value delivery.ComposedDigest err string } type sourceEntry struct { revision uint64 group string heads map[string]string err string } type assignerAskRecord struct { ask asks.Ask err string } type moduleEntry struct { value delivery.Module err string } type askRecord struct { value delivery.Ask err string } type haveEntry struct { have delivery.Have err string at time.Time } type nodeEntry struct { node delivery.HaveNode err string at time.Time } // New is an empty model. func New() *Model { return &Model{assigns: map[string]assignment{}, builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, asks: map[string]askRecord{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, composed: map[string]composedEntry{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]delivery.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]delivery.Answer{}, holds: map[string]heldEntry{}, newest: map[string]string{}, haves: map[string]haveEntry{}, nodes: map[string]nodeEntry{}, presence: map[string]presenceEntry{}, stuck: map[string]stuckEntry{}} } // Apply takes one change of a key. A value it cannot read is kept as an error on its row, never guessed. When the change makes // the source revision delivered, the event of it is said once (SetDeliveredEvent), after the model's lock is released. func (m *Model) Apply(e Entry) { m.mu.Lock() m.apply(e) event := m.deliveredNow() say := m.deliveredEvent motion, sayMotion := m.motionNow(e), m.motionEvent m.mu.Unlock() if event != nil && say != nil { say(*event) } if sayMotion != nil { for _, ev := range motion { sayMotion(ev) } } } // DeliveredEvent is what the view says when the source revision is delivered (event `delivered` of the view, issue 1906 D9, ADR // 0539 §17): the revision of the gate's source key, the group that moved it and every repository's merged head at it. A // consumer takes every earlier revision as covered by it, and takes the same revision twice as once (after a restart of the view). type DeliveredEvent struct { Revision uint64 `json:"revision"` Group string `json:"group"` Heads map[string]string `json:"heads"` } // SetDeliveredEvent sets where the event of a delivered source revision goes; nothing is said until it is set. func (m *Model) SetDeliveredEvent(say func(DeliveredEvent)) { m.mu.Lock() defer m.mu.Unlock() m.deliveredEvent = say } // deliveredNow is the event of the source revision when it is delivered now and has not been said, or nil. A revision that went // bad after it was delivered and recovered is not said again. func (m *Model) deliveredNow() *DeliveredEvent { if m.deliveredEvent == nil || m.source == nil || m.source.err != "" || m.said == m.source.revision { return nil } row := m.sourceRow() if row == nil || row.State != SourceDelivered || row.Wants == 0 { return nil } m.said = m.source.revision return &DeliveredEvent{Revision: m.source.revision, Group: m.source.group, Heads: m.source.heads} } func (m *Model) apply(e Entry) { switch e.Bucket { case delivery.HaveBucket: node, _, isNode, err := delivery.ParseHaveKey(e.Key) if err != nil { return } if isNode { if e.Deleted { delete(m.nodes, node) return } n, err := delivery.DecodeHaveNode(e.Value) m.nodes[node] = nodeEntry{node: n, err: errText(err), at: e.At} return } key := e.Key // the bucket's own key: a module name with a dot is escaped in it (issue 1150) if e.Deleted { delete(m.haves, key) return } h, err := delivery.DecodeHave(e.Value) m.haves[key] = haveEntry{have: h, err: errText(err), at: e.At} case delivery.PresenceBucket: if e.Deleted { delete(m.presence, e.Key) return } p, err := delivery.DecodePresence(e.Value) m.presence[e.Key] = presenceEntry{value: p, err: errText(err), at: e.At} case delivery.StuckBucket: if e.Deleted { delete(m.stuck, e.Key) return } st, err := delivery.DecodeStuck(e.Value) m.stuck[e.Key] = stuckEntry{value: st, err: errText(err)} case AssignmentsBucket: if e.Deleted { delete(m.assigns, e.Key) return } var a assignment if json.Unmarshal(e.Value, &a) != nil || a.Node == "" || a.Module == "" { return } m.assigns[a.Node+"."+a.Module] = a case delivery.CandidatesBucket: if e.Deleted { delete(m.groups, e.Key) return } c, err := delivery.DecodeCandidate(e.Value) m.groups[e.Key] = groupEntry{value: c, err: errText(err)} case delivery.ModulesBucket: if e.Deleted { delete(m.modules, e.Key) return } mod, err := delivery.DecodeModule(e.Value) m.modules[e.Key] = moduleEntry{value: mod, err: errText(err)} case delivery.AsksBucket: if e.Deleted { delete(m.asks, e.Key) return } a, err := delivery.DecodeAsk(e.Value) m.asks[e.Key] = askRecord{value: a, err: errText(err)} case delivery.AssignerAsksBucket: if e.Deleted { delete(m.aasks, e.Key) return } a, err := decodeAssignerAsk(e.Key, e.Value) m.aasks[e.Key] = assignerAskRecord{ask: a, err: errText(err)} case delivery.ViewAnswersBucket: askKey, err := delivery.ParseViewAnswerKey(e.Key) if err != nil { return } if e.Deleted { delete(m.chosen, askKey) return } if a, err := delivery.DecodeViewAnswer(e.Value); err == nil { m.chosen[askKey] = a } case delivery.AssignerWantsBucket: if m.applyHeld(e) { return } node, module, err := delivery.ParseWantKey(e.Key) if err != nil { return // the node's own key is not a module's want } key, kerr := delivery.HaveKey(node, module) if kerr != nil { return } if e.Deleted { delete(m.wants, key) return } w, err := delivery.DecodeWant(e.Value) m.wants[key] = wantEntry{value: w, err: errText(err), at: e.At} case delivery.ControllerComposedBucket: if e.Deleted { delete(m.composed, e.Key) return } d, err := delivery.DecodeComposedDigest(e.Value) m.composed[e.Key] = composedEntry{value: d, err: errText(err)} case delivery.SourceBucket: if e.Key != delivery.SourceMainKey { return } if e.Deleted { m.source = nil return } src, err := delivery.DecodeSource(e.Value) m.source = &sourceEntry{revision: e.Revision, group: src.Group, heads: src.Heads, err: errText(err)} case delivery.AnswersBucket: if e.Deleted { delete(m.answers, e.Key) delete(m.answerOf, e.Key) return } // The answer's warrant is `@