// Package view derives the operator's rows from the delivery buckets. package view import ( "encoding/json" "fmt" "sort" "strings" "sync" "time" "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 } // 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"` } // 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"` } // 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" // 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" ) // 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 asks map[string]askRecord // the gate's asks, by key answers map[string]string // by key: the router id of the asking the teller's answer is for ("" when unreadable) 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 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{}, asks: map[string]askRecord{}, answers: map[string]string{}, 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. func (m *Model) Apply(e Entry) { m.mu.Lock() defer m.mu.Unlock() switch e.Bucket { case delivery.HaveBucket: node, module, 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 := node + "." + module 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.AnswersBucket: if e.Deleted { delete(m.answers, e.Key) return } // The answer's warrant is `@