mesh/delivery-group group feat/take-in rejected: composed together: the checker's verdict at the members' heads (38623984d6ad): mesh-build-go
mesh/delivery closed: a newer head of the same pull request
mesh/check failed (2): mesh-build-go: the indexer's judge at the group's mesh-delivery head 7b7096f37ea76e1…; and 1 more | 7b7096f37ea7 on ae31b66a9e33
- A take-in releases its claims (compare-and-set, only while the claim is still its own) on a lost claim, on a claim it cannot read back, and on a merge it fails to take in: a claim left behind refused every later take-in until a restart. T5 row with a store that fails once, both ways. - The start step waits for nothing: with the build it runs unknown it says so and the forge consumer starts; a merged message left unacknowledged asks the build again (clause 1). T10 row: the start answers at once and takes nothing in. - A toolchain report evaluates pending again (ToolchainReported), since the build a revision names moves with the toolchain. T7 row: the revision is taken in once the report makes it name the running build. - The group's end is told by its whole subject (process.ModuleStreamSubject), so a repository named forge/group is never taken for one; row in the indexer's handle test. - The watchman asks the indexer's waits through indexer_pending at its start (invokes), before its durable consumer, which does not hear again what it acknowledged before a restart; the comment that said the whole stream is heard is corrected. W2 row: a wait seeded within its bound is raised past it. - bridge_groups names each group's last end ask, its who, why and when, and the bridge's answer (state end-asks). - The check's words for the indexer's rules: the trunk's manifest validator. - The bridge's end drops the merged-state test its merge commit already implies. - Test budget: groupask_test folded into groups_test, and the K1 mesh/check test into the owed status one: 804 unit tests (was 806). Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
174 lines
6.5 KiB
Go
174 lines
6.5 KiB
Go
// Package groups is what the forge bridge's process and its tools bundle share of a delivery group's end (novox/hq ADR
|
|
// 0633 §4, issue 1585): the keys of the bridge's state performed, and the asks of the state end-asks, through which the
|
|
// tool bridge_group_end_again asks the process to say a group's end again (the bridge serves no request subject, ADR 0437
|
|
// §4, so a tool asks in a state the process watches, as indexer_take_in asks the indexer in takeins).
|
|
package groups
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The bridge's states the tools read and write.
|
|
const (
|
|
// PerformedState is the bridge's state of what it performed and published.
|
|
PerformedState = "performed"
|
|
// EndAsksState is the state of asks to say a group's end again, keyed by the gate's merges key of the group.
|
|
EndAsksState = "end-asks"
|
|
// EndAsksBucket is its bucket on the bus.
|
|
EndAsksBucket = delivery.ForgeBridgeModule + "_" + EndAsksState
|
|
)
|
|
|
|
// Key is the key of performed that says a group's end is written: `groups.<merges key>`, its value the revision of the
|
|
// gate's merges key it was worked out from.
|
|
func Key(mergesKey string) string { return "groups." + mergesKey }
|
|
|
|
// PublishedKey is the key of performed that says a member's merged message is written: `published.<16 hex digits of the
|
|
// sha256 of <repository>#<number>@<merge>>`.
|
|
func PublishedKey(repository string, number int, merge string) string {
|
|
sum := sha256.Sum256([]byte(fmt.Sprintf("%s#%d@%s", strings.ToLower(repository), number, merge)))
|
|
return "published." + hex.EncodeToString(sum[:8])
|
|
}
|
|
|
|
// EndID is the message id of a group's end, and of one said again by hand at a time: the stream's duplicate window drops a
|
|
// second publish of the same id.
|
|
func EndID(group, lastMerge, again string) string {
|
|
id := "group-end." + group + "." + lastMerge
|
|
if again != "" {
|
|
id += "." + again
|
|
}
|
|
return id
|
|
}
|
|
|
|
// AgainStamp is the time a hand ask appends to a group end's message id.
|
|
func AgainStamp(at time.Time) string { return at.UTC().Format("20060102T150405.000000000Z") }
|
|
|
|
// EndAsk is a value of the end-asks state: which group's end a person asked to be said again, why, who and when, and, once
|
|
// the bridge read it, its answer.
|
|
type EndAsk struct {
|
|
V int `json:"v"`
|
|
Group string `json:"group"`
|
|
Why string `json:"why,omitempty"`
|
|
By string `json:"by,omitempty"`
|
|
At time.Time `json:"at"`
|
|
Written bool `json:"written,omitempty"`
|
|
Answer string `json:"answer,omitempty"`
|
|
Answered time.Time `json:"answered,omitzero"`
|
|
}
|
|
|
|
// Done says the ask is answered.
|
|
func (a EndAsk) Done() bool { return !a.Answered.IsZero() }
|
|
|
|
// NewEndAsk is the value bridge_group_end_again writes for a group: its key (the gate's merges key) and the value.
|
|
func NewEndAsk(group, why, by string, at time.Time) (string, []byte, error) {
|
|
group = strings.TrimSpace(group)
|
|
if group == "" {
|
|
return "", nil, errors.New("name the group whose end is said again (group)")
|
|
}
|
|
key, err := delivery.MergesKey(group)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
data, err := json.Marshal(EndAsk{V: 1, Group: group, Why: strings.TrimSpace(why), By: by, At: at.UTC()})
|
|
return key, data, err
|
|
}
|
|
|
|
// DecodeEndAsk reads an end-asks value.
|
|
func DecodeEndAsk(data []byte) (EndAsk, error) {
|
|
var a EndAsk
|
|
if err := json.Unmarshal(data, &a); err != nil {
|
|
return a, err
|
|
}
|
|
if a.V != 1 || a.Group == "" {
|
|
return a, errors.New("an end-asks value of no version 1 naming no group")
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// Row is one group as bridge_groups answers it: the gate's merge of it and what the bridge kept of its end.
|
|
type Row struct {
|
|
Key string `json:"key"`
|
|
Group string `json:"group"`
|
|
Members []RowMember `json:"members"`
|
|
Ended bool `json:"ended"`
|
|
EndedAt uint64 `json:"ended-at-revision,omitempty"`
|
|
Open bool `json:"open"`
|
|
Words string `json:"words"`
|
|
// LastEndAsk is the last ask to say the group's end again (bridge_group_end_again), its who, why and when, and the
|
|
// bridge's answer once it read it; absent when none was asked. The state end-asks keeps the last per group.
|
|
LastEndAsk *EndAsk `json:"last-end-ask,omitempty"`
|
|
}
|
|
|
|
// RowMember is one approved member of a group: its merge's state at the gate and whether its merged message is written.
|
|
type RowMember struct {
|
|
Repository string `json:"repository"`
|
|
Number int `json:"number"`
|
|
State string `json:"state"`
|
|
Commit string `json:"commit,omitempty"`
|
|
Published bool `json:"published"`
|
|
}
|
|
|
|
// Rows are the groups the gate's merges hold with more than one member, each with what the bridge kept: ended (its end
|
|
// written, the revision it was worked out from) or open (some member is not merged or its merged message not written, or its
|
|
// end not written). merges is the gate's merges by key, performed the bridge's performed keys by key; unread are the gate's
|
|
// keys that do not read. asks are the end-asks values by the same key; one that does not read is named unread as
|
|
// `end-asks.<key>`.
|
|
func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[string]uint64, asks map[string][]byte) (rows []Row, unread []string) {
|
|
lastAsk := map[string]*EndAsk{}
|
|
for key, raw := range asks {
|
|
a, err := DecodeEndAsk(raw)
|
|
if err != nil {
|
|
unread = append(unread, EndAsksState+"."+key)
|
|
continue
|
|
}
|
|
lastAsk[key] = &a
|
|
}
|
|
for key, raw := range merges {
|
|
m, err := delivery.DecodeMerge(raw)
|
|
if err != nil {
|
|
unread = append(unread, key)
|
|
continue
|
|
}
|
|
if len(m.Approved) < 2 {
|
|
continue
|
|
}
|
|
r := Row{Key: key, Group: m.Group}
|
|
allMerged := true
|
|
for _, p := range m.Pulls {
|
|
pub := p.Commit != "" && performed[PublishedKey(p.Repository, p.Number, p.Commit)]
|
|
r.Members = append(r.Members, RowMember{Repository: p.Repository, Number: p.Number, State: string(p.State), Commit: p.Commit, Published: pub})
|
|
if p.State != delivery.MergeMerged || !pub {
|
|
allMerged = false
|
|
}
|
|
}
|
|
if len(m.Pulls) < len(m.Approved) {
|
|
allMerged = false
|
|
}
|
|
r.EndedAt, r.Ended = endedAt[key]
|
|
r.LastEndAsk = lastAsk[key]
|
|
r.Open = !r.Ended
|
|
switch {
|
|
case r.Ended:
|
|
r.Words = "its end is written"
|
|
case allMerged:
|
|
r.Words = "every member is merged and its message written, and its end is not written: bridge_group_end_again says it"
|
|
default:
|
|
r.Words = "gathered: not every member is merged and its merged message written"
|
|
}
|
|
rows = append(rows, r)
|
|
}
|
|
sortRows(rows)
|
|
sort.Strings(unread)
|
|
return rows, unread
|
|
}
|
|
|
|
func sortRows(rows []Row) { sort.Slice(rows, func(i, j int) bool { return rows[i].Key < rows[j].Key }) }
|