The view's reach carries the reached head and each change's source revision; nothing records a merge as delivered (ADR 0556 v8)

This commit is contained in:
2026-10-26 21:12:31 +01:00
parent 43de3c937d
commit 54a8afac73
3 changed files with 30 additions and 11 deletions
+2 -2
View File
@@ -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{
+20 -7
View File
@@ -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)) {
+8 -2
View File
@@ -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)