Files
mesh-delivery/modules/mesh-delivery-indexer/internal/index/pending_tools.go
T

207 lines
11 KiB
Go

package index
import (
"context"
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"strings"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// PendingTools are the indexer's tools of what waits for a take-in (novox/hq ADR 0633 §5): indexer_pending reads the
// pending state, and indexer_take_in_pending and indexer_drop_pending steer it by an ask in takeins, as indexer_take_in
// does. pending is the pending state as the tools bundle reads it; ask writes an ask and waits for the indexer's answer.
func PendingTools(pending, ownBuild Store, ask func(key string, value []byte) (TakeInAsk, error), now func() time.Time) []stdio.Tool {
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return s }
hand := func(drop bool) func(map[string]any) (any, error) {
return func(args map[string]any) (any, error) {
key, value, err := NewPendingAsk(str(args, "key"), str(args, "why"), stdio.CallerOf(args), drop, now())
if err != nil {
return nil, err
}
a, err := ask(key, value)
if err != nil {
return nil, err
}
return a.Result, nil
}
}
merge := func(args map[string]any) (any, error) {
seq, _ := args["sequence"].(float64)
key, value, err := NewMergeAsk(str(args, "repository"), str(args, "merge"), uint64(seq), str(args, "why"), stdio.CallerOf(args), now())
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: PendingTool,
Description: "What waits for a take-in (ADR 0633): merges the indexer holds in its pending state instead of taking them in. " +
"A revision (what revision, its key its oldest merge's m.<sequence>) waits for the build of the indexer it names " +
"(waits-for): the running indexer wrote only what building and moving that indexer needs, and the new indexer takes " +
"it in at its start. A group (what group, key g.<group>) is a delivery group whose members are gathered until the " +
"forge bridge says its end; a merge in one of its members' repositories waits behind it (behind). Each names its " +
"merges, since when it waits, the modules it holds back (holds), the merges whose message the forge bridge's stream no " +
"longer holds (lost: they are not taken in; indexer_drop_pending drops them) and the last take-in event said of it. Empty waits: " +
"every merge is taken in. The watchman raises take-in-waits past 60 minutes for a revision, 30 for a group; " +
"indexer_take_in_pending and indexer_drop_pending steer a wait. own-build says which build the indexer knows it runs and own-build-since since when; own-build-unknown, when set, says the indexer cannot learn the build it " +
"runs (since when): it fixes it once at its start, so every merge waits (a take-in.pending that says so) until the module is started again.",
Input: map[string]any{"type": "object", "properties": map[string]any{}},
Run: func(map[string]any) (any, error) {
var unread []string
entries, err := ReadPending(context.Background(), pending, func(format string, args ...any) {
unread = append(unread, fmt.Sprintf(format, args...))
})
if err != nil {
return nil, err
}
answer := takein.PendingAnswer{Waits: PendingWaits(entries), Unread: unread}
// Whether this indexer knows the build it runs: when it does not, every merge waits, and this says since when and why.
if ownBuild != nil {
if v, _, found, err := ownBuild.Get(context.Background(), OwnBuildKey); err == nil && found {
var o OwnBuild
if json.Unmarshal(v, &o) == nil {
answer.OwnBuildSince = o.Since
if o.Known {
answer.OwnBuild = o.FP
} else {
answer.OwnBuildUnknown = o.Why
}
}
}
}
return answer, nil
}},
{Name: TakeInPendingTool,
Description: "Take a wait in by hand with the running indexer and its judge (ADR 0633 §5): the revision under its m. key, " +
"whatever build of the indexer it names, or a delivery group under its g. key with the members gathered so far. For " +
"when the named indexer can never run, or a group's end never comes. A hand act: who and why are said in the " +
"indexer's journal and on the take-in.taken event, which clears take-in-waits. indexer_pending names the keys.",
Input: map[string]any{"type": "object", "required": []string{"key", "why"}, "properties": map[string]any{
"key": map[string]any{"type": "string", "description": "the wait's key, m.<sequence> or g.<group>, as indexer_pending names it"},
"why": map[string]any{"type": "string", "description": "why it is taken in by hand, in plain words"}}},
Run: hand(false)},
{Name: DropPendingTool,
Description: "Drop one pending merge by hand, abandoned because a later merge replaced it (ADR 0633 §5): it is removed from " +
"the pending state and nothing of it is taken in; what waited with it is worked out again. A hand act: who and why are " +
"said in the journal and on the take-in.taken event (dropped).",
Input: map[string]any{"type": "object", "required": []string{"key", "why"}, "properties": map[string]any{
"key": map[string]any{"type": "string", "description": "the pending merge's key, m.<sequence>, as indexer_pending names its merges' wait"},
"why": map[string]any{"type": "string", "description": "why it is dropped, in plain words"}}},
Run: hand(true)},
{Name: TakeInMergeTool,
Description: "Take one merge in by hand (ADR 0633 §5, issue 1585): the indexer asks the forge bridge for the merge's merged message " +
"(bridge_merged_message) and runs it through the take-in as a delivered merged message, so it is held for the " +
"indexer its revision names or taken in. For a merge the take-in did not take in (indexer_heard names its sequence " +
"and why). A hand act: who and why are said in the indexer's journal. The bridge's own words answer when it cannot " +
"build the message (the gate holds no such merge, the forge does not answer).",
Input: map[string]any{"type": "object", "required": []string{"repository", "merge", "sequence", "why"}, "properties": map[string]any{
"repository": map[string]any{"type": "string", "description": "the repository, owner/name"},
"merge": map[string]any{"type": "string", "description": "the merge commit, 40 hex digits"},
"sequence": map[string]any{"type": "integer", "description": "the merge's sequence on the forge bridge's stream, as indexer_heard names it"},
"why": map[string]any{"type": "string", "description": "why it is taken in by hand, in plain words"}}},
Run: merge},
}
}
// TryCandidateTools are the tools of a group try's candidate (issue 1921, trycandidates.go): indexer_try_candidates reads the
// candidates the indexer holds for tries (the try, its heads, each member's modules and whether each has its fingerprint),
// and indexer_drop_try_candidate drops one by an ask in takeins. candidates is the candidates state as the tools bundle reads it.
func TryCandidateTools(candidates, recovery Store, ask func(key string, value []byte) (TakeInAsk, error), now func() time.Time) []stdio.Tool {
return []stdio.Tool{
{Name: TryCandidatesTool,
Description: "The candidates the indexer holds for group tries (a try of several repositories at exact commits, mesh-delivery-checker try): " +
"per try its heads hash, each member's repository and commit, the modules the try touches and, per module, its manifest digest, its " +
"fingerprint or why it has none yet (a merge preview not yet written). The checker's manifest validator and the node-engines' " +
"compose-candidate read this candidate. Read-only; an empty answer means no whole try waits or runs.",
Input: map[string]any{"type": "object", "properties": map[string]any{}},
Run: func(map[string]any) (any, error) {
ctx := context.Background()
keys, err := candidates.Keys(ctx)
if err != nil {
return nil, err
}
var out []meshcatalog.CandidatesByGroupValue
for _, k := range keys {
if !meshcatalogclient.IsTryGroup(k) {
continue
}
v, _, found, err := candidates.Get(ctx, k)
if err != nil || !found {
continue
}
if c, err := DecodeCandidate(v); err == nil {
out = append(out, c)
}
}
// How the recovery of the open tries went: loud when it is not done (since when, why).
said := map[string]any{"done": false, "why": "no recovery has been written yet"}
if v, _, found, err := recovery.Get(ctx, TryRecoveryKey); err == nil && found {
var r TryRecovery
if json.Unmarshal(v, &r) == nil {
said = map[string]any{"done": r.Done}
if !r.Done {
said["why"], said["since"] = r.Why, r.Since
said["says"] = "recovery not done, since " + r.Since.Format(time.RFC3339) + ": " + r.Why
}
}
}
return map[string]any{"tries": out, "recovery": said}, nil
}},
{Name: RecoverTriesTool,
Description: "Run the recovery of the open tries now: the indexer lists them through the checker's checker_tries, writes the candidate of each " +
"whole try that waits or runs and deletes the candidate of a try no longer listed. It runs by itself at the indexer's start; when it failed " +
"(indexer_try_candidates says recovery not done, since when and why) this runs it again. A hand act: who and why are said in the journal.",
Input: map[string]any{"type": "object", "required": []string{"why"}, "properties": map[string]any{
"why": map[string]any{"type": "string", "description": "why it is run now, in plain words"}}},
Run: func(args map[string]any) (any, error) {
why, _ := args["why"].(string)
if strings.TrimSpace(why) == "" {
return nil, fmt.Errorf("say why: a hand act is said in the journal")
}
data, err := json.Marshal(TakeInAsk{V: 1, RecoverTries: true, Why: strings.TrimSpace(why), By: stdio.CallerOf(args), At: now().UTC()})
if err != nil {
return nil, err
}
a, err := ask("tries.recover."+now().UTC().Format("20060102T150405.000000000"), data)
if err != nil {
return nil, err
}
return a.Result, nil
}},
{Name: DropTryCandidateTool,
Description: "Drop a group try's candidate by hand (the indexer deletes it itself when the try ends): for a candidate left for a try that " +
"is gone, or to make the indexer write it again from the previews at the try's next change. A hand act: who and why are said in the journal.",
Input: map[string]any{"type": "object", "required": []string{"try", "why"}, "properties": map[string]any{
"try": map[string]any{"type": "string", "description": "the try's id, try-<digits>"},
"why": map[string]any{"type": "string", "description": "why it is dropped, in plain words"}}},
Run: func(args map[string]any) (any, error) {
id, _ := args["try"].(string)
why, _ := args["why"].(string)
id = strings.TrimSpace(id)
if !meshcatalogclient.IsTryGroup(id) || strings.TrimSpace(why) == "" {
return nil, fmt.Errorf("name the try (try-<digits>) and say why")
}
data, err := json.Marshal(TakeInAsk{V: 1, TryCandidate: id, Drop: true, Why: strings.TrimSpace(why), By: stdio.CallerOf(args), At: now().UTC()})
if err != nil {
return nil, err
}
a, err := ask("try-candidate.drop."+id, data)
if err != nil {
return nil, err
}
return a.Result, nil
}},
}
}