Files
mesh-delivery/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go
T

267 lines
11 KiB
Go

// Command mesh-delivery-forge-bridge-tools serves the forge bridge's tools (novox/hq ADR 0633 §4, issue 1585): the runtime
// launches it for a call. bridge_groups reads the delivery groups the gate holds and what the bridge kept of their ends;
// bridge_group_end_again writes an ask into the bridge's state end-asks and waits for the bridge's answer there, since the
// bridge is the other program and serves no request subject (ADR 0437 §4).
package main
import (
"context"
"encoding/json"
"fmt"
"maps"
"os"
"slices"
"strconv"
"strings"
"sync"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"mesh-delivery-forge-bridge/internal/bridge"
"mesh-delivery-forge-bridge/internal/groups"
)
// Tool names.
const (
GroupsTool = "bridge_groups"
EndAgainTool = "bridge_group_end_again"
// MergedTool answers the merged message of a merge on request (issue 1585).
MergedTool = "bridge_merged_message"
)
// asker writes an end-asks value and waits for the bridge's answer to it.
type asker func(key string, value []byte) (groups.EndAsk, error)
// Tools are the bridge's tools: groupsRead reads the groups the gate holds with what the bridge kept of them.
func Tools(ask asker, now func() time.Time, groupsRead func() ([]groups.Row, []string, error), merged func(repository, merge string) (json.RawMessage, error)) []stdio.Tool {
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
return []stdio.Tool{
{Name: MergedTool,
Description: "The merged message of one merge, asked again (issue 1585): the message the bridge wrote on its stream FORGE when the " +
"pull request was merged into the trunk, built the same way from the forge (the merge commit's trees and the manifests " +
"and lock files of its module directories) and from the gate's merges state (the pull request's number, head and " +
"group). For the indexer, which asks it when it takes in a held merge it does not hold in memory (a restart, a new " +
"build), and the ask-again a lost message lacked. About 0.5 MB for a repository of many modules. Answers {message} " +
"with the forge message whole, or why it cannot: the gate holds no merge with that commit, or the forge did not answer.",
Input: map[string]any{"type": "object", "required": []string{"repository", "merge"}, "properties": map[string]any{
"repository": map[string]any{"type": "string", "description": "the repository, owner/name, as the forge names it"},
"merge": map[string]any{"type": "string", "description": "the merge commit, 40 hex digits"}}},
Run: func(args map[string]any) (any, error) {
raw, err := merged(str(args, "repository"), str(args, "merge"))
if err != nil {
return nil, err
}
return map[string]any{"message": raw}, nil
}},
{Name: GroupsTool,
Description: "The delivery groups the gate holds (more than one member), each with its members' merge state and whether each " +
"merged message is written on the forge stream, and whether the group's end (forge.group.merged) is written: ended, " +
"or open with why (gathered, or merged and its end not written yet). An open group with every member merged is " +
"what bridge_group_end_again says the end of. Each group names its last end ask (who, why, when) and the bridge's " +
"answer to it, from the state end-asks. A group's name narrows it to that group; open lists only open ones.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"group": map[string]any{"type": "string", "description": "only this group, by the name the gate keys it"},
"open": map[string]any{"type": "boolean", "description": "only the groups whose end is not written"}}},
Run: func(args map[string]any) (any, error) {
rows, unread, err := groupsRead()
if err != nil {
return nil, err
}
only, _ := args["open"].(bool)
group := str(args, "group")
kept := rows[:0]
for _, r := range rows {
if (group != "" && r.Group != group) || (only && !r.Open) {
continue
}
kept = append(kept, r)
}
return map[string]any{"groups": kept, "unread": unread}, nil
}},
{Name: EndAgainTool,
Description: "Say a delivery group's end (forge.group.merged) again, by hand, for a group whose end the indexer did not take " +
"in (a group bridge_groups names as merged with its end not written, or one the indexer waits for). The bridge " +
"writes it again from the gate's merges, with the time appended to the message id so the stream does not drop it as " +
"a duplicate; the indexer takes an end for a group it already took in without changing anything. Refused while " +
"a member is not merged or its merged message is not written. Answers whether it was written.",
Input: map[string]any{"type": "object", "required": []string{"group"}, "properties": map[string]any{
"group": map[string]any{"type": "string", "description": "the group, by the name the gate keys it"},
"why": map[string]any{"type": "string", "description": "why its end is said again, in plain words"}}},
Run: func(args map[string]any) (any, error) {
key, value, err := groups.NewEndAsk(str(args, "group"), 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 map[string]any{"group": a.Group, "written": a.Written, "answer": a.Answer, "by": a.By, "why": a.Why}, nil
}},
}
}
// MergedOf is the merged message of the merge of repository at commit merge, as the bridge writes it: the pull request's
// number, head and group from the gate's merges (raw, by key), the rest from the forge through trees and files
// (bridge.MergedMessage), and, for a member of a delivery group, the group and its members as the process sets them.
func MergedOf(raws map[string][]byte, repository, merge string, trees bridge.Trees, files bridge.Files) (json.RawMessage, error) {
owner, repo, ok := strings.Cut(repository, "/")
if !ok || owner == "" || repo == "" {
return nil, fmt.Errorf("%q is not owner/name", repository)
}
for _, key := range slices.Sorted(maps.Keys(raws)) {
m, err := delivery.DecodeMerge(raws[key])
if err != nil {
continue
}
at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool {
return delivery.SameRepository(p.Repository, repository) && p.Commit == merge
})
if at < 0 {
continue
}
p := m.Pulls[at]
msg, err := bridge.MergedMessage(owner, repo, p.Number, m.Group, p.Head.Head, merge, trees, files)
if err != nil {
return nil, fmt.Errorf("the merged message of %s at %s cannot be built: %w", repository, merge, err)
}
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
return nil, err
}
if _, approved := m.Approved[delivery.ApprovedKey(f.Repository, f.Number)]; approved && len(m.Approved) >= 2 {
f.Group, f.Members = m.Group, groups.MembersOf(m)
}
out, err := forgemsg.EncodeForge(f)
return out, err
}
return nil, fmt.Errorf("the gate's merges hold no merge of %s with the commit %s, so its pull request is not known", repository, merge)
}
// read is the groups the gate holds with what the bridge kept of them: their ends in performed, and the last ask of
// each in end-asks with the bridge's answer.
func read(merges, performed, asks stdio.KeptState) ([]groups.Row, []string, error) {
keys, err := merges.Keys()
if err != nil {
return nil, nil, fmt.Errorf("the gate's merges cannot be listed: %w", err)
}
raws := map[string][]byte{}
for _, k := range keys {
e, err := merges.Get(k)
if err != nil || e == nil {
continue
}
raws[k] = e.Value
}
pkeys, err := performed.Keys()
if err != nil {
return nil, nil, fmt.Errorf("the bridge's performed state cannot be listed: %w", err)
}
published, ended := map[string]bool{}, map[string]uint64{}
for _, k := range pkeys {
switch {
case strings.HasPrefix(k, "published."):
published[k] = true
case strings.HasPrefix(k, "groups."):
if e, err := performed.Get(k); err == nil && e != nil {
rev, _ := strconv.ParseUint(strings.TrimSpace(string(e.Value)), 10, 64)
ended[strings.TrimPrefix(k, "groups.")] = rev
}
}
}
akeys, err := asks.Keys()
if err != nil {
return nil, nil, fmt.Errorf("the bridge's state %s cannot be listed: %w", groups.EndAsksState, err)
}
askRaws := map[string][]byte{}
for _, k := range akeys {
if e, err := asks.Get(k); err == nil && e != nil {
askRaws[k] = e.Value
}
}
rows, unread := groups.Rows(raws, published, ended, askRaws)
return rows, unread, nil
}
// askEnd writes the end-asks value at key and waits, on a watch of the key, at most bound for the bridge's answer.
func askEnd(s stdio.KeptState, bound time.Duration) asker {
return func(key string, value []byte) (groups.EndAsk, error) {
answered := make(chan groups.EndAsk, 1)
var rev uint64
var mu sync.Mutex
take := func(revision uint64, v []byte) {
mu.Lock()
mine := rev
mu.Unlock()
if mine == 0 || revision <= mine {
return
}
if a, err := groups.DecodeEndAsk(v); err == nil && a.Done() {
select {
case answered <- a:
default:
}
}
}
if err := s.Watch(key, func(c stdio.StateChange) error {
if c.Op == "put" {
take(c.Revision, c.Value)
}
return nil
}); err != nil {
return groups.EndAsk{}, fmt.Errorf("watching %s.%s: %w", groups.EndAsksState, key, err)
}
r, err := s.Put(key, json.RawMessage(value))
if err != nil {
return groups.EndAsk{}, fmt.Errorf("the ask could not be written to %s: %w", groups.EndAsksBucket, err)
}
mu.Lock()
rev = r
mu.Unlock()
if e, err := s.Get(key); err == nil && e != nil {
take(e.Revision, e.Value)
}
wait, cancel := context.WithTimeout(context.Background(), bound) // a context's bound, no timer (ADR 0437 §8)
defer cancel()
select {
case a := <-answered:
return a, nil
case <-wait.Done():
return groups.EndAsk{}, fmt.Errorf("the bridge did not answer within %s: its answer will be in %s.%s", bound, groups.EndAsksBucket, key)
}
}
}
// askCaller calls a tool as this bundle's module, through the runtime (stdio.Ask), the module's own invokes granting it.
func askCaller(_ context.Context, key string, body any) (json.RawMessage, error) {
return stdio.Ask(key, body)
}
func main() {
merges, performed, asks := stdio.State(delivery.GateModule+".merges"), stdio.State(groups.PerformedState), stdio.State(groups.EndAsksState)
tools := Tools(askEnd(asks, 25*time.Second), time.Now,
func() ([]groups.Row, []string, error) { return read(merges, performed, asks) },
func(repository, merge string) (json.RawMessage, error) {
keys, err := merges.Keys()
if err != nil {
return nil, fmt.Errorf("the gate's merges cannot be listed: %w", err)
}
raws := map[string][]byte{}
for _, k := range keys {
if e, err := merges.Get(k); err == nil && e != nil {
raws[k] = e.Value
}
}
ctx := context.Background()
return MergedOf(raws, repository, merge, bridge.TreesVia(ctx, askCaller), bridge.FilesVia(ctx, askCaller))
})
if err := stdio.Serve("", tools); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-forge-bridge-tools] %v\n", err)
os.Exit(1)
}
}