Say a first merge that waits past ten minutes for mesh-delivery's word, with probe D17 (issue 1146, AC8)
This commit is contained in:
@@ -120,6 +120,11 @@ var probeRegistry = []probe{
|
|||||||
{ID: probeDeliveriesID, Asserts: "no delivery is held past its state's bound unsaid: mesh-delivery's " +
|
{ID: probeDeliveriesID, Asserts: "no delivery is held past its state's bound unsaid: mesh-delivery's " +
|
||||||
"`stalled`, each with the transition its table lets healer H2 take", From: "ADR 0239",
|
"`stalled`, each with the transition its table lets healer H2 take", From: "ADR 0239",
|
||||||
Kind: kindDeliveryStalled, Phase: 3, run: probeDeliveries},
|
Kind: kindDeliveryStalled, Phase: 3, run: probeDeliveries},
|
||||||
|
// A first merge into a repository the mesh builds nothing from, kept waiting for mesh-delivery's word on the
|
||||||
|
// operator's Approve (novox/hq ADR 0432, issue 1146): said past a bound, never only in a log line (ADR 0426 §3, §8).
|
||||||
|
{ID: probeFirstBuildsID, Asserts: "no first merge into a repository the mesh builds nothing from waits more than ten " +
|
||||||
|
"minutes for mesh-delivery's announcement of the operator's Approve of its head", From: "ADR 0432, issue 1146",
|
||||||
|
Kind: kindFirstBuildUnasked, Phase: 3, run: probeFirstBuilds},
|
||||||
// The delivery budgets (novox/hq ADR 0282 decision 7): the newest delivery of each class within its budget,
|
// The delivery budgets (novox/hq ADR 0282 decision 7): the newest delivery of each class within its budget,
|
||||||
// read from mesh-delivery's `times`; a measurement said, never a delivery held.
|
// read from mesh-delivery's `times`; a measurement said, never a delivery held.
|
||||||
{ID: probeBudgetsID, Asserts: "the newest delivery of a leaf module ran on every machine within three minutes of " +
|
{ID: probeBudgetsID, Asserts: "the newest delivery of a leaf module ran on every machine within three minutes of " +
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/conditions"
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
"github.com/novox/mesh-controller/internal/link"
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
)
|
)
|
||||||
@@ -161,3 +162,51 @@ func firstBuildsOf(ctx context.Context, inv catalogueReader, m link.SourceMoved,
|
|||||||
// Not announced yet, or the bus or the store could not be asked: the caller waits or tries again, and asks nothing.
|
// Not announced yet, or the bus or the store could not be asked: the caller waits or tries again, and asks nothing.
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// probeFirstBuildsID is the probe that reads the first merges waiting for mesh-delivery's word.
|
||||||
|
const probeFirstBuildsID = "D17"
|
||||||
|
|
||||||
|
// kindFirstBuildUnasked is what a first merge waiting too long for mesh-delivery's word raises.
|
||||||
|
const kindFirstBuildUnasked = "first-build-unasked"
|
||||||
|
|
||||||
|
// firstBuildWaitBound is how long a first merge waits for mesh-delivery's announcement of the operator's Approve before
|
||||||
|
// it is said: a bound in the sense of ADR 0426 §3, not a delay. Nothing waits on it: the word's arrival asks the builds
|
||||||
|
// at once, and the merge is said, never dropped, once it is past this (issue 1146).
|
||||||
|
const firstBuildWaitBound = 10 * time.Minute
|
||||||
|
|
||||||
|
// firstBuildObservations are the conditions of the first merges waiting past the bound, one for each; a merge answered is
|
||||||
|
// no longer listed as waiting, so its condition clears with the next look.
|
||||||
|
func firstBuildObservations(waiting []inventory.BatchedMerge, now time.Time) []conditions.Observation {
|
||||||
|
var out []conditions.Observation
|
||||||
|
for _, w := range waiting {
|
||||||
|
k := announcedOf(w)
|
||||||
|
if !k.FirstBuild || now.Sub(w.Heard) <= firstBuildWaitBound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
repository := k.Owner + "/" + k.Repo
|
||||||
|
for_ := now.Sub(w.Heard).Round(time.Minute)
|
||||||
|
out = append(out, conditions.Observation{Scope: conditions.ScopeMerge,
|
||||||
|
ID: fmt.Sprintf("%s_%.8s", repository, k.Commit), Kind: kindFirstBuildUnasked, Severity: conditions.Warning,
|
||||||
|
Summary: fmt.Sprintf("the merge %.8s of %s adds modules the mesh builds nothing from yet, and has waited %s for "+
|
||||||
|
"mesh-delivery's announcement of the operator's Approve of its head: no build is asked", k.Commit, repository, for_),
|
||||||
|
Said: fmt.Sprintf("%s at %s waits for mesh-delivery's word on pull request %d (head %.8s)", repository, k.Commit,
|
||||||
|
k.Number, k.HeadSHA),
|
||||||
|
Headline: fmt.Sprintf("The first build of %s is not asked: no Approve was announced", repository),
|
||||||
|
Explanation: fmt.Sprintf("The merge %.8s into %s adds modules the mesh builds nothing from yet. It registers them "+
|
||||||
|
"only when mesh-delivery announces that it merged the pull request on your Approve of that head, and no such "+
|
||||||
|
"announcement has come in %s. The merge is kept, and the builds are asked the moment the announcement is kept.",
|
||||||
|
k.Commit, repository, for_),
|
||||||
|
Resolved: "mesh-delivery announces the merge, or a hand `build` registers the modules",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// probeFirstBuilds is D17: no first merge waits past its bound for mesh-delivery's word unsaid.
|
||||||
|
func probeFirstBuilds(ctx context.Context, d *doctor) ([]conditions.Observation, error) {
|
||||||
|
waiting, err := d.open.inventory.WaitingMerges(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return firstBuildObservations(waiting, time.Now().UTC()), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||||
|
"github.com/novox/mesh-controller/internal/conditions"
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
"github.com/novox/mesh-controller/internal/link"
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
)
|
)
|
||||||
@@ -72,3 +75,32 @@ func TestAFirstMergeIntoANewRepositoryAsksItsModulesBuildsOnlyOnTheApprove(t *te
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1146/AC8: a first merge waiting past the bound for mesh-delivery's word is said, naming the repository, the commit and
|
||||||
|
// the missing word; one inside the bound, one that is no first merge, and one answered (no longer waiting) are not.
|
||||||
|
func TestAFirstMergeWaitingPastItsBoundIsSaidAndClearsWhenAnswered(t *testing.T) {
|
||||||
|
now := time.Date(2026, 10, 19, 1, 0, 0, 0, time.UTC)
|
||||||
|
waiting := func(commit string, first bool, heard time.Time) inventory.BatchedMerge {
|
||||||
|
event, _ := json.Marshal(keptMerge{SourceMoved: link.SourceMoved{Owner: "novox", Repo: "mesh-delivery", Base: "main",
|
||||||
|
Commit: commit, Number: 1, HeadSHA: "a1a1a1a1a1a1"}, FirstBuild: first})
|
||||||
|
return inventory.BatchedMerge{Repository: "novox/mesh-delivery", Branch: "main", Commit: commit, Heard: heard, Event: event}
|
||||||
|
}
|
||||||
|
got := firstBuildObservations([]inventory.BatchedMerge{
|
||||||
|
waiting("0177ea7a00", true, now.Add(-firstBuildWaitBound-time.Minute)),
|
||||||
|
waiting("1111111100", true, now.Add(-time.Minute)),
|
||||||
|
waiting("2222222200", false, now.Add(-time.Hour)),
|
||||||
|
}, now)
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("1146/AC8: %d conditions raised, want the one merge past the bound: %+v", len(got), got)
|
||||||
|
}
|
||||||
|
o := got[0]
|
||||||
|
if o.Kind != kindFirstBuildUnasked || o.Scope != conditions.ScopeMerge || o.Severity != conditions.Warning ||
|
||||||
|
!strings.Contains(o.Summary, "novox/mesh-delivery") || !strings.Contains(o.Summary, "0177ea7a") ||
|
||||||
|
!strings.Contains(o.Summary, "announcement of the operator's Approve") {
|
||||||
|
t.Fatalf("1146/AC8: the condition is %+v", o)
|
||||||
|
}
|
||||||
|
// Answered, the merge is no longer listed as waiting: nothing is observed, and the condition clears.
|
||||||
|
if cleared := firstBuildObservations(nil, now); len(cleared) != 0 {
|
||||||
|
t.Fatalf("1146/AC8: an answered merge still raises %+v", cleared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user