diff --git a/go/catalogue/delivery_seat.go b/go/catalogue/delivery_seat.go index 82b0803..f1d2d24 100644 --- a/go/catalogue/delivery_seat.go +++ b/go/catalogue/delivery_seat.go @@ -77,8 +77,8 @@ func deliveryVerbs() []Verb { deliveryRemovedInR6("retire-history"), {Name: "settle", Description: "Ask the operator to settle deliveries held because their merge reached the " + "trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. " + - "The verb moves nothing; on the operator's answer each goes to merged, and to delivered once every node " + - "runs a build containing it (novox/hq ADR 0556). Refused for a delivery not held, or held for another " + + "The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines " + + "is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another " + "reason, naming why.", Input: map[string]any{"type": "object", "additionalProperties": false, "required": []string{"ids", "why"}, "properties": map[string]any{ diff --git a/go/delivery/reach.go b/go/delivery/reach.go index 6562c49..292bce9 100644 --- a/go/delivery/reach.go +++ b/go/delivery/reach.go @@ -9,10 +9,11 @@ import ( "time" ) -// How far a merge reached (novox/hq ADR 0556, issue 1280). The indexer says, per repository, the last merge it took in -// (TakenBucket); the delivery view derives, per repository, how far each of its modules reached on the nodes that want it -// (ReachBucket); the gate, the one writer of a delivery's record, watches the view's state and moves a merged delivery to -// delivered when every module its merge changed runs, on every node that wants it, at a build containing the merge. +// 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" @@ -63,12 +64,18 @@ func DecodeTaken(data []byte) (Taken, error) { // 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. +// 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 @@ -84,10 +91,12 @@ type ModuleReach struct { Waiting string `json:"waiting,omitempty"` } -// Change is one fingerprint of a module and the merge commit it first appeared at (Module.Commit). +// 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. @@ -97,6 +106,9 @@ 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") @@ -139,7 +151,8 @@ func DecodeReach(data []byte) (Reach, error) { // 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) { + 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)) { diff --git a/go/delivery/source_test.go b/go/delivery/source_test.go index 0f084ac..11d3734 100644 --- a/go/delivery/source_test.go +++ b/go/delivery/source_test.go @@ -325,12 +325,16 @@ func TestSourceValuesRefused(t *testing.T) { // 1200/AC4 r.Modules["gitea"] = m _, err = EncodeReach(r) wantErr("reach keeping too many changes", err, "more than") + r = sampleReach() + r.ReachedSrc = 0 + _, err = EncodeReach(r) + wantErr("reach with a head and no revision", err, "reached head") } // sampleReach is a reach value of one repository with a module at its floor and a library-like empty one. func sampleReach() Reach { - return Reach{V: Version, Repository: "novox/mesh-catalog", Indexed: "cccc", Modules: map[string]ModuleReach{ - "gitea": {FP: "src2:a", Nodes: 1, Floor: "cccc", Changes: []Change{{Commit: "cccc", FP: "src2:a"}, {Commit: "bbbb", FP: "src2:b"}}}, + return Reach{V: Version, Repository: "novox/mesh-catalog", Indexed: "cccc", Reached: "cccc", ReachedSrc: 9, Modules: map[string]ModuleReach{ + "gitea": {FP: "src2:a", Nodes: 1, Floor: "cccc", Changes: []Change{{Commit: "cccc", FP: "src2:a", Src: 9}, {Commit: "bbbb", FP: "src2:b", Src: 7}}}, "zsh": {FP: "src2:z", Changes: []Change{{Commit: "aaaa", FP: "src2:z"}}}, }} } @@ -373,6 +377,8 @@ func TestSourceSame(t *testing.T) { // 1200/AC5 "floor": func(r *Reach) { m := r.Modules["gitea"]; m.Floor = "bbbb"; r.Modules["gitea"] = m }, "changes": func(r *Reach) { m := r.Modules["zsh"]; m.Changes = nil; r.Modules["zsh"] = m }, "modules": func(r *Reach) { delete(r.Modules, "zsh") }, + "reached": func(r *Reach) { r.Reached, r.ReachedSrc = "bbbb", 7 }, + "holds": func(r *Reach) { r.Holds = []string{"gitea on ace does not yet run src2:a"} }, } { r := sampleReach() change(&r)