package delivery import ( "encoding/json" "fmt" "maps" "slices" "strings" "time" ) // How far a repository reached (novox/hq ADR 0556, issue 1280). The indexer says, per repository, the last merge it took in // (TakenBucket); the delivery view keeps, per repository, how far each of its modules reached on the nodes that want it, and // derives from it the repository's reached head (ReachBucket): the newest head of the repository at which every wanted // module runs, on every node, a build at or after its last change. A merge at or before the reached head is on the // machines. Nothing records a merge as delivered (ADR 0429 ยง1): the gate and the tracker derive it from this state. // TakenBucket is the indexer's state `taken`: one key per repository (RepositoryKey), the last merge it took in. const TakenBucket = IndexerModule + "_taken" // ReachBucket is the delivery view's state `reach`: one key per repository (RepositoryKey), how far its modules reached. const ReachBucket = ViewModule + "_reach" // RepositoryKey is a repository's key in TakenBucket and ReachBucket: its forge name `owner/name` (RepositoryName), in // lower case as the forge compares it, escaped as a key part (EscapeName). An empty spelling, or one with no owner, is // refused. func RepositoryKey(repository string) (string, error) { name := strings.ToLower(RepositoryName(repository)) if owner, repo, ok := strings.Cut(name, "/"); !ok || owner == "" || repo == "" { return "", fmt.Errorf("delivery: %q names no repository as owner/name", repository) } return EscapeName(name), nil } // Taken is the value of a taken key: the repository, the merge commit the indexer last took in (applied its message // whole), and when. type Taken struct { V int `json:"v"` Repository string `json:"repository"` Merge string `json:"merge"` At time.Time `json:"at"` } // EncodeTaken is t as JSON at Version; a value with no repository or merge is refused. func EncodeTaken(t Taken) ([]byte, error) { if t.Repository == "" || t.Merge == "" { return nil, fmt.Errorf("delivery: a taken value names its repository and merge") } t.V = Version return json.Marshal(t) } // DecodeTaken reads a taken value; it refuses an unknown version (ErrUnknownVersion) and one with no merge. func DecodeTaken(data []byte) (Taken, error) { var t Taken if err := decode(data, &t); err != nil { return t, err } if t.Repository == "" || t.Merge == "" { return t, fmt.Errorf("delivery: a taken value names no repository or merge") } return t, nil } // Reach is the value of a reach key: how far the modules of one repository reached on the nodes. Indexed is the last // merge the indexer took in for it (Taken.Merge), empty while it took in none. A repository the indexer holds no module // of (a library) has no Modules. Reached is the reached head: the newest head of the repository in the source's history, // not past Indexed, at which every module with a node that wants it has its floor at or after its last change at or before // that head; ReachedSrc is the source revision that named it. Holds says, in words, what keeps the next head of the // repository from being reached (a module on a node, the indexer's lag); empty when the repository's newest head is reached. type Reach struct { V int `json:"v"` Repository string `json:"repository"` Indexed string `json:"indexed,omitempty"` Modules map[string]ModuleReach `json:"modules,omitempty"` Reached string `json:"reached,omitempty"` ReachedSrc uint64 `json:"reached_src,omitempty"` Holds []string `json:"holds,omitempty"` } // ModuleReach is how far one module reached. FP is its fingerprint at the indexer now; Nodes how many nodes want it // (a want not removed); Changes its fingerprints the view knows of, each with the merge commit it first appeared at, // newest first. Floor is the oldest first-appearance commit among the fingerprints its nodes' haves run, a running have // counting whether its health check passes or not; it is empty when a node that wants the module has no running have, or // runs a fingerprint not among Changes. Waiting names, in words, every node where the module is not settled (its have // different from its want, not running, or not healthy), with or without a floor: health is said here and never moves the // floor (ADR 0556 clause 2). A module whose floor is empty while a node wants it holds no head of the repository back: a // reader takes a merge among its Changes as not yet on the machines, with these words. A module no node wants has neither. type ModuleReach struct { FP string `json:"fp,omitempty"` Nodes int `json:"nodes"` Changes []Change `json:"changes,omitempty"` Floor string `json:"floor,omitempty"` Waiting string `json:"waiting,omitempty"` } // Change is one fingerprint of a module, the merge commit it first appeared at (Module.Commit), and the source revision // that named that commit as the repository's head (Module.Src, or the source's history), 0 when not known. type Change struct { Commit string `json:"commit"` FP string `json:"fp"` Src uint64 `json:"src,omitempty"` } // MostChanges is how many changes of a module a reach value keeps: as many revisions as the indexer's modules bucket keeps. const MostChanges = ModulesHistory func (r Reach) check() error { if r.Repository == "" { return fmt.Errorf("delivery: a reach value names no repository") } if (r.Reached == "") != (r.ReachedSrc == 0) { return fmt.Errorf("delivery: the reach of %s names its reached head and its source revision together", r.Repository) } for name, m := range r.Modules { if name == "" { return fmt.Errorf("delivery: a reach value names a module with no name") } if m.Nodes < 0 { return fmt.Errorf("delivery: the reach of %s counts %d nodes", name, m.Nodes) } if len(m.Changes) > MostChanges { return fmt.Errorf("delivery: the reach of %s keeps %d changes, more than %d", name, len(m.Changes), MostChanges) } for _, c := range m.Changes { if c.Commit == "" || c.FP == "" { return fmt.Errorf("delivery: a change of %s names no commit or fingerprint", name) } } } return nil } // EncodeReach is r as JSON at Version; a value that does not check is refused. func EncodeReach(r Reach) ([]byte, error) { if err := r.check(); err != nil { return nil, err } r.V = Version return json.Marshal(r) } // DecodeReach reads a reach value; it refuses an unknown version (ErrUnknownVersion) and one that does not check. func DecodeReach(data []byte) (Reach, error) { var r Reach if err := decode(data, &r); err != nil { return r, err } return r, r.check() } // SameReach says whether two reach values are the same, so the view writes only when they differ. func SameReach(a, b Reach) bool { if a.Repository != b.Repository || a.Indexed != b.Indexed || len(a.Modules) != len(b.Modules) || a.Reached != b.Reached || a.ReachedSrc != b.ReachedSrc || !slices.Equal(a.Holds, b.Holds) { return false } for _, name := range slices.Sorted(maps.Keys(a.Modules)) { x, ok := b.Modules[name] y := a.Modules[name] if !ok || x.FP != y.FP || x.Nodes != y.Nodes || x.Floor != y.Floor || x.Waiting != y.Waiting || !slices.Equal(x.Changes, y.Changes) { return false } } return true }