diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go index 7b2ba739..574441c6 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go @@ -289,6 +289,10 @@ func main() { // The seat mesh-catalog's verb revision, a first view of the catalog half answered in this runtime of its own to exactly // the parts whose entry declares it (novox/hq ADR 0663). tools = append(tools, index.RevisionTool(revisions, index.Module)) + tools = append(tools, index.CatalogRevisionTools(revisions, asks, time.Now, func(what, why string) error { + _, err := acts.Record(meshhandactsclient.Request{What: what, Why: why, Cause: index.DropCandidateGraphTool, Verb: "drop-candidate-graph"}) + return err + })...) tools = append(tools, viewTools(os.Getenv, trunk.State, time.Now)...) tools = append(tools, index.PendingTools(keptRead{stdio.State(index.PendingState)}, keptRead{stdio.State(index.OwnBuildStateName)}, asks, time.Now)...) tools = append(tools, index.FingerprintsTools(asks, time.Now, func(what, why string) error { diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go index 37822709..3f010eba 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go @@ -26,6 +26,8 @@ type takerIn interface { DropTryCandidate(ctx context.Context, id, by, why string) index.TakenIn RecoverTriesByHand(ctx context.Context, by, why string) index.TakenIn FingerprintsAgain(ctx context.Context, module string) index.TakenIn + RecomputeByHand(ctx context.Context, by, why string) index.TakenIn + DropCandidateGraph(ctx context.Context, group, heads, by, why string) index.TakenIn } // takenIn answers one value of the takeins state (the tool indexer_take_in, issue 1945): a value not yet answered takes the @@ -44,7 +46,13 @@ func takenIn(ctx context.Context, ix takerIn, kv jetstream.KeyValue, e jetstream return } asked, r := "take-in", index.TakenIn{} - if a.Fingerprints { + if a.Recompute { + asked, r = "recompute of the catalog graph", ix.RecomputeByHand(ctx, a.By, a.Why) + a.Module = r.Module + } else if a.CandidateGroup != "" && a.Drop { + asked, r = "drop of a candidate's catalog half", ix.DropCandidateGraph(ctx, a.CandidateGroup, a.CandidateHeads, a.By, a.Why) + a.Module = r.Module + } else if a.Fingerprints { asked, r = "computing of fingerprints again", ix.FingerprintsAgain(ctx, a.Module) } else if a.RecoverTries { asked, r = "recovery of the tries", ix.RecoverTriesByHand(ctx, a.By, a.Why) diff --git a/modules/mesh-delivery-indexer/internal/index/pending_test.go b/modules/mesh-delivery-indexer/internal/index/pending_test.go index 8487b9fd..86cdaabf 100644 --- a/modules/mesh-delivery-indexer/internal/index/pending_test.go +++ b/modules/mesh-delivery-indexer/internal/index/pending_test.go @@ -18,6 +18,7 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-delivery/sdk/trunk" + stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/busretry" "git.novox.be/novox/mesh-sdk/go/catalogue" @@ -794,6 +795,91 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) t.Errorf("a take-in to 85 %% said filling %+v (%v), pending %v: want it said once and the merge taken in", filling, err, b2.keys()) } }) + t.Run("W4.12 a candidate's half dropped by hand frees its own entries, says room and takes the held merge in; the read tools list it; a recompute that moves nothing is equal", func(t *testing.T) { + bed := func(store *memObjects) *takeInBed { + b := newTakeInBed(t) + b.arrive(t, 2, b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2")) + b.ix.ownMu.Lock() + b.ix.ownFP = b.fp(t, Module) + b.ix.ownMu.Unlock() + b.ix.Revisions = store + return b + } + free := newMemObjects(0) + if _, _, err := bed(free).ix.TakeInAtStart(ctx); err != nil { + t.Fatal(err) + } + size := free.used() // what the merge's revision takes + store := newMemObjects(2 * size) + shared := []byte(`{"module":"b"}`) + if err := KeepRevision(ctx, store, meshcatalog.CatalogRevision{Kind: "candidate", Group: "g1", Heads: "h1", Revision: 1, + Entries: []meshcatalog.EntryRef{{Module: "a", Digest: "da"}, {Module: "b", Digest: "db"}}}, + map[string][]byte{"da": make([]byte, size+10), "db": shared}); err != nil { + t.Fatal(err) + } + if err := KeepRevision(ctx, store, meshcatalog.CatalogRevision{Kind: "candidate", Group: "g2", Heads: "h2", Revision: 1, + Entries: []meshcatalog.EntryRef{{Module: "b", Digest: "db"}}}, map[string][]byte{"db": shared}); err != nil { + t.Fatal(err) + } + b := bed(store) + var said []string + b.ix.SayFill = func(_ context.Context, event, _ string, _ meshcatalog.Fill) error { said = append(said, event); return nil } + if _, _, err := b.ix.TakeInAtStart(ctx); err != nil || len(b.keys()) != 1 { + t.Fatalf("pending %v (%v): want the merge held for objects full", b.keys(), err) + } + var recorded []string + asks := 0 + tools := map[string]stdio.Tool{} + for _, tool := range CatalogRevisionTools(store, func(_ string, value []byte) (TakeInAsk, error) { + asks++ + a, err := DecodeTakeIn(value) + if err != nil { + return a, err + } + var r TakenIn + if a.Recompute { + r = b.ix.RecomputeByHand(ctx, a.By, a.Why) + } else { + r = b.ix.DropCandidateGraph(ctx, a.CandidateGroup, a.CandidateHeads, a.By, a.Why) + } + a.Result = &r + return a, nil + }, time.Now, func(what, why string) error { + recorded = append(recorded, what) + if why == "unrecorded" { + return errors.New("mesh-hand-acts did not answer") + } + return nil + }) { + tools[tool.Name] = tool + } + if _, err := tools[DropCandidateGraphTool].Run(map[string]any{"group": "g1", "heads": "h1", "why": "unrecorded"}); err == nil || asks != 0 { + t.Fatalf("a drop whose hand act is not recorded: %v, %d asks: want refused before it is asked", err, asks) + } + got, err := tools[DropCandidateGraphTool].Run(map[string]any{"group": "g1", "heads": "h1", "why": "its gate event never came"}) + r, _ := got.(*TakenIn) + if err != nil || r == nil || !r.Stored || !slices.Equal(r.Dropped, []string{"entry.da"}) || len(recorded) != 2 { + t.Fatalf("drop answered %+v (%v), recorded %q: want candidate.g1.h1 dropped with entry.da alone, after its hand act", r, err, recorded) + } + if _, err := store.Get(ctx, "entry.db"); err != nil || len(b.keys()) != 0 || !slices.Contains(said, meshcatalog.EventRoom) { + t.Errorf("entry.db %v, pending %v, said %q: want the shared entry kept, room said and the merge taken in", err, b.keys(), said) + } + list, err := tools[CatalogRevisionsTool].Run(map[string]any{}) + l, _ := list.(RevisionsAnswer) + if err != nil || len(l.Revisions) != 2 || l.Revisions[0].Name != "candidate.g2.h2" || l.Revisions[1].Name != "trunk.1" || !l.Revisions[1].Current || + l.Fill.Cap != 2*size || l.Fill.Bytes != store.used() || len(l.Unlisted) != 0 { + t.Errorf("indexer_catalog_revisions = %+v (%v): want candidate.g2.h2 and the current trunk.1, the fill against its cap", l, err) + } + if c, err := tools[CandidateGraphsTool].Run(map[string]any{}); err != nil || len(c.(RevisionsAnswer).Revisions) != 1 { + t.Errorf("indexer_candidate_graphs = %+v (%v): want g2's alone", c, err) + } + if one, err := tools[CatalogRevisionTool].Run(map[string]any{}); err != nil || one.(map[string]any)["name"] != "trunk.1" { + t.Errorf("indexer_catalog_revision = %v (%v): want the newest trunk revision whole", one, err) + } + if got, err := tools[RecomputeTool].Run(map[string]any{"why": "a check"}); err != nil || !strings.Contains(got.(*TakenIn).Answer, "equal to the kept revision 1") { + t.Errorf("indexer_recompute = %+v (%v): want equal to the kept revision 1", got, err) + } + }) t.Run("T7 a toolchain report evaluates pending again: the revision then names the running build and is taken in", func(t *testing.T) { b := newTakeInBed(t) b.arrive(t, 2, b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2")) diff --git a/modules/mesh-delivery-indexer/internal/index/revision_tools.go b/modules/mesh-delivery-indexer/internal/index/revision_tools.go new file mode 100644 index 00000000..afe1860f --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/revision_tools.go @@ -0,0 +1,326 @@ +package index + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "regexp" + "slices" + "strconv" + "strings" + "time" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// The indexer's tools of its catalog graph revisions (design 47, the indexer's tools; the instruction "tools are how a +// module is spoken to"): indexer_catalog_revision and indexer_catalog_revisions read the objects catalog-revisions, +// indexer_candidate_graphs every candidate's catalog half; indexer_recompute and indexer_drop_candidate_graph steer the +// indexer by an ask in takeins, as indexer_take_in does, since the indexer's process serves no request subject. +const ( + CatalogRevisionTool = "indexer_catalog_revision" + CatalogRevisionsTool = "indexer_catalog_revisions" + CandidateGraphsTool = "indexer_candidate_graphs" + RecomputeTool = "indexer_recompute" + DropCandidateGraphTool = "indexer_drop_candidate_graph" +) + +// RecomputeKey is the takeins key of a recompute by hand: one at a time. +const RecomputeKey = "recompute" + +var candidateToken = regexp.MustCompile(`^[A-Za-z0-9_=-]+$`) + +// NewRecomputeAsk is the value indexer_recompute writes: its key in takeins and the value. +func NewRecomputeAsk(why, by string, at time.Time) (string, []byte, error) { + data, err := json.Marshal(TakeInAsk{V: 1, Recompute: true, Why: strings.TrimSpace(why), By: by, At: at.UTC()}) + return RecomputeKey, data, err +} + +// NewDropCandidateGraphAsk is the value indexer_drop_candidate_graph writes: its key in takeins, +// candidate-graph.., and the value. +func NewDropCandidateGraphAsk(group, heads, why, by string, at time.Time) (string, []byte, error) { + group, heads = strings.TrimSpace(group), strings.TrimSpace(heads) + switch { + case !candidateToken.MatchString(group) || !candidateToken.MatchString(heads): + return "", nil, errors.New("name the candidate by its group and its heads, as indexer_candidate_graphs names them") + case strings.TrimSpace(why) == "": + return "", nil, errors.New("say why (why): a drop is a hand act, recorded through mesh-hand-acts") + } + data, err := json.Marshal(TakeInAsk{V: 1, CandidateGroup: group, CandidateHeads: heads, Drop: true, Why: strings.TrimSpace(why), By: by, At: at.UTC()}) + return "candidate-graph." + group + "." + heads, data, err +} + +// RevisionSummary is one kept catalog graph revision as indexer_catalog_revisions and indexer_candidate_graphs list it. +type RevisionSummary struct { + Name string `json:"name"` + Kind string `json:"kind"` + Revision int64 `json:"revision"` + Source int64 `json:"source"` + Group string `json:"group,omitempty"` + Heads string `json:"heads,omitempty"` + At *time.Time `json:"at,omitempty"` + Entries int `json:"entries"` + // Current says it is the trunk's newest revision, which is kept whatever pins it. + Current bool `json:"current,omitempty"` +} + +// RevisionsAnswer is what indexer_catalog_revisions answers: every kept revision, the entries stored once by digest, the +// store's fill against its cap, and each object it could not read. +type RevisionsAnswer struct { + Revisions []RevisionSummary `json:"revisions"` + Entries int `json:"entries"` + Unlisted []string `json:"unlisted,omitempty"` + Fill meshcatalog.Fill `json:"fill"` + FillUnread string `json:"fill-unread,omitempty"` + Unread []string `json:"unread,omitempty"` +} + +// ListRevisions reads every revision the objects keep, candidates included (kind "candidate" alone when candidates). +func ListRevisions(ctx context.Context, o Objects, candidates bool) (RevisionsAnswer, error) { + out := RevisionsAnswer{Revisions: []RevisionSummary{}} + names, err := o.Names(ctx) + if err != nil { + return out, fmt.Errorf("the objects %s cannot be listed: %w", CatalogRevisionsObjects, err) + } + listed := map[string]bool{} + var newest int64 + entries := []string{} + for _, n := range names { + if strings.HasPrefix(n, "entry.") { + entries = append(entries, n) + continue + } + data, err := o.Get(ctx, n) + var rev meshcatalog.CatalogRevision + if err == nil { + err = json.Unmarshal(data, &rev) + } + if err != nil { + out.Unread = append(out.Unread, n+": "+err.Error()) + continue + } + for _, ref := range rev.Entries { + listed[EntryObject(ref.Digest)] = true + } + if rev.Kind == "trunk" && rev.Revision > newest { + newest = rev.Revision + } + if candidates && rev.Kind != "candidate" { + continue + } + out.Revisions = append(out.Revisions, RevisionSummary{Name: n, Kind: rev.Kind, Revision: rev.Revision, Source: rev.Source, + Group: rev.Group, Heads: rev.Heads, At: rev.At, Entries: len(rev.Entries)}) + } + for i := range out.Revisions { + out.Revisions[i].Current = out.Revisions[i].Kind == "trunk" && out.Revisions[i].Revision == newest + } + slices.SortFunc(out.Revisions, func(a, b RevisionSummary) int { return strings.Compare(a.Name, b.Name) }) + out.Entries = len(entries) + for _, e := range entries { + if !listed[e] { + out.Unlisted = append(out.Unlisted, e) + } + } + slices.Sort(out.Unlisted) + used, capacity, err := o.Fill(ctx) + out.Fill = meshcatalog.Fill{Bytes: used, Cap: capacity, Store: CatalogRevisionsObjects} + if err != nil { + out.FillUnread = err.Error() + } + return out, nil +} + +// removeUnlisted deletes every entry no kept revision lists any more: an entry is stored once and shared, so it goes +// only with the last revision that lists it. It answers the entries removed. +func removeUnlisted(ctx context.Context, o Objects) ([]string, error) { + list, err := ListRevisions(ctx, o, false) + if err != nil { + return nil, err + } + if len(list.Unread) > 0 { + // A revision that cannot be read may list any entry: none is removed rather than one it still needs. + return nil, fmt.Errorf("no entry is removed while a revision cannot be read: %s", strings.Join(list.Unread, "; ")) + } + var removed []string + for _, e := range list.Unlisted { + if err := o.Delete(ctx, e); err != nil { + return removed, fmt.Errorf("%s: %w", e, err) + } + removed = append(removed, e) + } + return removed, nil +} + +// DropCandidateGraph drops a candidate's catalog half by hand (indexer_drop_candidate_graph, design 47: for a candidate +// whose gate event never came), the entries no other revision lists with it, and says room when that takes the store +// below its filling mark, which takes in a merge held for objects full (RoomAgain). +func (ix *Indexer) DropCandidateGraph(ctx context.Context, group, heads, by, why string) TakenIn { + name := RevisionObject(meshcatalog.CatalogRevision{Kind: "candidate", Group: group, Heads: heads}) + r := TakenIn{Module: name} + if ix.Revisions == nil { + r.Answer = "the indexer keeps no catalog graph revisions: nothing dropped" + return r + } + if _, err := ix.Revisions.Get(ctx, name); err != nil { + r.Answer = name + " is not kept (" + err.Error() + "): nothing dropped" + return r + } + ix.keepMu.Lock() + err := ix.Revisions.Delete(ctx, name) + var removed []string + if err == nil { + removed, err = removeUnlisted(ctx, ix.Revisions) + } + ix.keepMu.Unlock() + if err != nil { + r.Answer = name + " could not be dropped whole: " + err.Error() + ix.logf("%s", r.Answer) + return r + } + r.Stored, r.Dropped = true, removed + r.Answer = fmt.Sprintf("%s is dropped by %s (%s), with %d entries no other revision lists", name, by, why, len(removed)) + if roomed, err := ix.RoomAgain(ctx); err != nil { + r.Answer += "; room is not evaluated: " + err.Error() + } else if roomed { + r.Answer += "; room said, what waited for it evaluated again" + } + ix.logf("%s", r.Answer) + return r +} + +// RecomputeByHand computes the catalog graph again now from the view of the trunk, as a toolchain report or a merge +// does, and compares it with the newest revision kept: the modules whose entry moved, and the revision kept for them +// (none when the recompute equals the one kept). +func (ix *Indexer) RecomputeByHand(ctx context.Context, by, why string) TakenIn { + r := TakenIn{Module: "catalog graph"} + if ix.Revisions == nil { + r.Answer = "the indexer keeps no catalog graph revisions: nothing recomputed" + return r + } + before, _, err := newestTrunk(ctx, ix.Revisions) + if err != nil { + r.Answer = "the newest catalog graph revision cannot be read: " + err.Error() + return r + } + if err := ix.Recompute(ctx); err != nil { + r.Answer = "the recompute failed: " + err.Error() + return r + } + after, _, err := newestTrunk(ctx, ix.Revisions) + if err != nil { + r.Answer = "the newest catalog graph revision cannot be read after the recompute: " + err.Error() + return r + } + r.Stored = true + if after.Revision == before.Revision { + r.Answer = fmt.Sprintf("recomputed by %s (%s): equal to the kept revision %d", by, why, before.Revision) + } else { + r.Handed = changedEntries(before.Entries, after.Entries) + r.Answer = fmt.Sprintf("recomputed by %s (%s): revision %d differs from the kept %d in %s; revision %d is kept", by, why, + after.Revision, before.Revision, strings.Join(r.Handed, ", "), after.Revision) + } + ix.logf("%s", r.Answer) + return r +} + +// CatalogRevisionTools are the tools of the catalog graph revisions; ask writes an ask in takeins and waits for the +// indexer's answer, record records a hand act through mesh-hand-acts before a drop is asked. +func CatalogRevisionTools(o Objects, ask func(key string, value []byte) (TakeInAsk, error), now func() time.Time, record func(what, why string) error) []stdio.Tool { + str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) } + asked := func(key string, value []byte, err error) (any, error) { + if err != nil { + return nil, err + } + a, err := ask(key, value) + if err != nil { + return nil, err + } + return a.Result, nil + } + return []stdio.Tool{ + {Name: CatalogRevisionTool, + Description: "One catalog graph revision the indexer keeps in its objects catalog-revisions, whole: its list (kind, number, " + + "source revision, trunk heads, a candidate's group and heads) and every entry it lists, each module as its manifest " + + "declares it at that source revision (bases, packages, toolchains, provides, requires, invokes, serves, fingerprint, " + + "acting rules, inputs). The trunk's newest with no input; revision a kept trunk revision; group and heads a candidate's. " + + "An entry the store lost is an error naming it. indexer_catalog_revisions lists what is kept.", + Input: map[string]any{"type": "object", "properties": map[string]any{ + "revision": map[string]any{"type": "integer", "description": "a kept trunk revision's number"}, + "group": map[string]any{"type": "string", "description": "a candidate's group, with heads"}, + "heads": map[string]any{"type": "string", "description": "a candidate's heads, with group"}}}, + Run: func(args map[string]any) (any, error) { + ctx := context.Background() + n, _ := args["revision"].(float64) + name := "" + switch { + case str(args, "group") != "" || str(args, "heads") != "": + name = RevisionObject(meshcatalog.CatalogRevision{Kind: "candidate", Group: str(args, "group"), Heads: str(args, "heads")}) + case n > 0: + name = "trunk." + strconv.FormatInt(int64(n), 10) + default: + newest, ok, err := newestTrunk(ctx, o) + if err != nil { + return nil, fmt.Errorf("the catalog graph revisions cannot be read: %w", err) + } + if !ok { + return nil, ErrNoRevision + } + name = RevisionObject(newest) + } + rev, entries, err := ReadRevision(ctx, o, name) + if err != nil { + return nil, err + } + return map[string]any{"name": name, "revision": rev, "entries": entries}, nil + }}, + {Name: CatalogRevisionsTool, + Description: "Every catalog graph revision the indexer keeps in its objects catalog-revisions, trunk and candidate: each " + + "one's name, kind, number, source revision, when, how many entries it lists and whether it is the trunk's newest " + + "(current); how many entries are stored once by digest, those no revision lists (unlisted), and the store's fill " + + "in bytes against its cap (filling is said at 80 %, full refuses the next revision). An object it cannot read is " + + "named in unread. indexer_catalog_revision reads one whole.", + Input: map[string]any{"type": "object", "properties": map[string]any{}}, + Run: func(map[string]any) (any, error) { + return ListRevisions(context.Background(), o, false) + }}, + {Name: CandidateGraphsTool, + Description: "Every candidate's catalog half the indexer keeps (a delivery group's or a pull request's heads merged onto " + + "a trunk source revision): its group, its heads, its revision, the source revision it is merged onto, since when, " + + "and how many entries it lists; and the store's fill. indexer_drop_candidate_graph drops one whose gate event never came.", + Input: map[string]any{"type": "object", "properties": map[string]any{}}, + Run: func(map[string]any) (any, error) { + return ListRevisions(context.Background(), o, true) + }}, + {Name: RecomputeTool, + Description: "Compute the catalog graph again now, as the indexer does on a toolchain report or a merge, from the view of " + + "the trunk (each module's manifest, its bases, the agreed toolchains and the published packages), and compare it with " + + "the newest revision kept: answers equal, or the modules whose entry differs and the revision kept for them.", + Input: map[string]any{"type": "object", "properties": map[string]any{ + "why": map[string]any{"type": "string", "description": "why it is recomputed now, in plain words"}}}, + Run: func(args map[string]any) (any, error) { + key, value, err := NewRecomputeAsk(str(args, "why"), stdio.CallerOf(args), now()) + return asked(key, value, err) + }}, + {Name: DropCandidateGraphTool, + Description: "Drop one candidate's catalog half by hand, for a candidate whose gate event never came: its revision and the " + + "entries no other revision lists. A hand act, recorded through mesh-hand-acts before it is asked. When the drop takes " + + "the store below its filling mark, room is said and a merge held for objects full is taken in. indexer_candidate_graphs " + + "names the candidates.", + Input: map[string]any{"type": "object", "required": []string{"group", "heads", "why"}, "properties": map[string]any{ + "group": map[string]any{"type": "string", "description": "the candidate's group"}, + "heads": map[string]any{"type": "string", "description": "the candidate's heads"}, + "why": map[string]any{"type": "string", "description": "why it is dropped, in plain words"}}}, + Run: func(args map[string]any) (any, error) { + key, value, err := NewDropCandidateGraphAsk(str(args, "group"), str(args, "heads"), str(args, "why"), stdio.CallerOf(args), now()) + if err != nil { + return nil, err + } + if err := record("dropped the catalog half of the candidate "+str(args, "group")+" at "+str(args, "heads")+" by hand", str(args, "why")); err != nil { + return nil, fmt.Errorf("the hand act was not recorded through mesh-hand-acts, so nothing was dropped: %w", err) + } + return asked(key, value, nil) + }}, + } +} diff --git a/modules/mesh-delivery-indexer/internal/index/takein.go b/modules/mesh-delivery-indexer/internal/index/takein.go index d6a9d524..31e4ef4e 100644 --- a/modules/mesh-delivery-indexer/internal/index/takein.go +++ b/modules/mesh-delivery-indexer/internal/index/takein.go @@ -66,12 +66,17 @@ type TakeInAsk struct { // packages as they are read (indexer_fingerprints_again, fingerprints.go). Fingerprints bool `json:"fingerprints,omitempty"` // RecoverTries asks the open tries' candidates to be recovered now (indexer_recover_tries). - RecoverTries bool `json:"recover-tries,omitempty"` - Drop bool `json:"drop,omitempty"` - By string `json:"by,omitempty"` - At time.Time `json:"at"` - Result *TakenIn `json:"result,omitempty"` - Answered time.Time `json:"answered,omitzero"` + RecoverTries bool `json:"recover-tries,omitempty"` + // Recompute asks the catalog graph to be computed again now and compared with the newest kept revision (indexer_recompute). + Recompute bool `json:"recompute,omitempty"` + // CandidateGroup and CandidateHeads, with Drop, ask that candidate's catalog half to be dropped (indexer_drop_candidate_graph). + CandidateGroup string `json:"candidate-group,omitempty"` + CandidateHeads string `json:"candidate-heads,omitempty"` + Drop bool `json:"drop,omitempty"` + By string `json:"by,omitempty"` + At time.Time `json:"at"` + Result *TakenIn `json:"result,omitempty"` + Answered time.Time `json:"answered,omitzero"` } // Done says the ask is answered. @@ -86,7 +91,7 @@ func DecodeTakeIn(data []byte) (TakeInAsk, error) { if a.V != 1 { return a, fmt.Errorf("a takeins value of version %d, not 1", a.V) } - if a.Module == "" && a.Repository == "" && a.Pending == "" && a.Merge == "" && a.TryCandidate == "" && !a.RecoverTries && !a.Fingerprints && !a.CatchUp { + if a.Module == "" && a.Repository == "" && a.Pending == "" && a.Merge == "" && a.TryCandidate == "" && !a.RecoverTries && !a.Fingerprints && !a.CatchUp && !a.Recompute && a.CandidateGroup == "" { return a, errors.New("a takeins value naming no module, repository, pending wait, merge or try") } return a, nil diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index c94f7d6d..8707e686 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -189,8 +189,10 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build want = append(want, "indexer_first_views", "indexer_first_view_again", index.ConfigReadersTool, index.BusStateTool, index.RequirementsTool, index.PackageUsersTool, index.SayAcceptedTool, // DLV-F3r, DLV-I10, DLV-I11, DLV-I7, DLV-I8, DLV-I9c index.SeatDefinitionTool, index.SeatDefinitionsAgainTool, index.OutOfStepTool, // DLV-I19 index.HoldsTool, index.EngineReadsTool, // ADR 0632 §2: the mesh-wide holds and the engines' reads they weigh - index.PendingTool, index.TakeInPendingTool, index.DropPendingTool, index.TryCandidatesTool, index.DropTryCandidateTool, index.RecoverTriesTool, index.TakeInMergeTool, index.FingerprintsAgainTool) // issue 1966; 1585/T12, T13 (ADR 0633 §5) - if !reflect.DeepEqual(m.Tools, want) || len(want) != 40 || slices.Contains(m.Tools, "modules") { + index.PendingTool, index.TakeInPendingTool, index.DropPendingTool, index.TryCandidatesTool, index.DropTryCandidateTool, index.RecoverTriesTool, index.TakeInMergeTool, index.FingerprintsAgainTool, // issue 1966; 1585/T12, T13 (ADR 0633 §5) + // milestone 47/G W4.12 (design 47, the indexer's tools): the catalog graph revisions read and steered. + index.CatalogRevisionTool, index.CatalogRevisionsTool, index.CandidateGraphsTool, index.RecomputeTool, index.DropCandidateGraphTool) + if !reflect.DeepEqual(m.Tools, want) || len(want) != 45 || slices.Contains(m.Tools, "modules") { t.Errorf("tools = %v, want %v", m.Tools, want) } // Every read tool of the indexer a reader of this repository invokes is one it serves (the checker's seventh first diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index b928b5e3..28251b25 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -56,7 +56,12 @@ "indexer_drop_try_candidate", "indexer_recover_tries", "indexer_take_in_merge", - "indexer_fingerprints_again" + "indexer_fingerprints_again", + "indexer_catalog_revision", + "indexer_catalog_revisions", + "indexer_candidate_graphs", + "indexer_recompute", + "indexer_drop_candidate_graph" ], "consumes": [ "go-package-registry.published",