423 lines
14 KiB
Go
423 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The gate merges through the forge bridge, never itself (novox/hq ADR 0437 §8, ADR 0441 §5, ADR 0450 §3). It writes into
|
|
// `merges.<group>` the Approve's heads and ONE member at a time, the next only after the bridge's message on FORGE says the
|
|
// one before it merged, and none after a refusal. It re-reads every member's required statuses before it hands a member
|
|
// (ADR 0441 §2). Once every member is merged it moves `source.main` in one compare-and-set write and the mesh's source has
|
|
// the group as one revision. What it announces for each merge (`merged`) names the ask, its asker the teller, and the heads.
|
|
|
|
// MergeStore is the gate's bucket merges.
|
|
type MergeStore interface {
|
|
Get(group string) (*sdk.Merge, error)
|
|
Put(group string, m sdk.Merge) error
|
|
}
|
|
|
|
// SourceStore is the gate's bucket source, one key, moved by compare-and-set.
|
|
type SourceStore interface {
|
|
// Move reads source.main and writes what update makes of it, unless update says nothing is to be written; a write
|
|
// another writer won is read again and tried again.
|
|
Move(update func(prev *sdk.Source) (sdk.Source, bool)) error
|
|
}
|
|
|
|
// kvMerges is merges over the runtime.
|
|
type kvMerges struct{}
|
|
|
|
func (kvMerges) Get(group string) (*sdk.Merge, error) {
|
|
key, err := sdk.MergesKey(group)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e, err := stdio.State("merges").Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, err
|
|
}
|
|
m, err := sdk.DecodeMerge(e.Value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the merge of %s is not readable: %w", group, err)
|
|
}
|
|
return &m, nil
|
|
}
|
|
|
|
func (kvMerges) Put(group string, m sdk.Merge) error {
|
|
key, err := sdk.MergesKey(group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw, err := sdk.EncodeMerge(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = stdio.State("merges").Put(key, json.RawMessage(raw))
|
|
return err
|
|
}
|
|
|
|
// kvSource is source over the runtime.
|
|
type kvSource struct{}
|
|
|
|
func (kvSource) Move(update func(prev *sdk.Source) (sdk.Source, bool)) error {
|
|
state := stdio.State("source")
|
|
for range changeAttempts {
|
|
e, err := state.Get(sdk.SourceMainKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var prev *sdk.Source
|
|
if e != nil {
|
|
p, err := sdk.DecodeSource(e.Value)
|
|
if err != nil {
|
|
return fmt.Errorf("source.main is not readable: %w", err)
|
|
}
|
|
prev = &p
|
|
}
|
|
next, write := update(prev)
|
|
if !write {
|
|
return nil
|
|
}
|
|
raw, err := sdk.EncodeSource(next)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if e == nil {
|
|
_, err = state.Create(sdk.SourceMainKey, json.RawMessage(raw))
|
|
} else {
|
|
_, err = state.Update(sdk.SourceMainKey, json.RawMessage(raw), e.Revision)
|
|
}
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if !errors.Is(err, stdio.ErrChanged) {
|
|
return err
|
|
}
|
|
}
|
|
return fmt.Errorf("source.main was written by another writer first %d times: %w", changeAttempts, stdio.ErrChanged)
|
|
}
|
|
|
|
// approvedOf are the heads an Approve covers as a merge carries them: `<repository>@<number>` to the full head.
|
|
func (h *Holder) approvedOf(heads map[string]string) map[string]string {
|
|
out := map[string]string{}
|
|
for id, sha := range heads {
|
|
if m := h.deliveries[id]; m != nil {
|
|
out[sdk.ApprovedKey(m.Repository, m.Number)] = sha
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// handTurn is d's turn to be handed to the bridge, when it is: the first member of its group not merged (a lone pull request
|
|
// is a group of one), every member approved at its head and ready to merge. Nil when it is not. Under the lock; the hand
|
|
// runs outside it.
|
|
func (h *Holder) handTurn(g *Group, d *Delivery, now time.Time) func() {
|
|
var reads []memberRead
|
|
c := h.covering(d)
|
|
if g != nil {
|
|
order := h.unmergedLive(g)
|
|
if len(order) == 0 || order[0] != d {
|
|
return nil
|
|
}
|
|
for _, m := range order {
|
|
mc := h.covering(m)
|
|
if m.Ready == nil || m.Ready.Declined != nil || mc == nil || mc.Approved == nil || mc.Heads[m.ID] != m.Commit || h.readyWaits(m) != "" {
|
|
return nil // the others are not approved at their heads yet: nothing is merged before they are
|
|
}
|
|
reads = append(reads, readOf(m, mc.Heads[m.ID]))
|
|
}
|
|
}
|
|
if c == nil || c.Approved == nil || c.Heads[d.ID] == "" {
|
|
return nil
|
|
}
|
|
d.Ready.Tries++
|
|
d.Ready.LastTry, d.Ready.merging = now, true
|
|
h.keep(d)
|
|
gid, name, key, target, approved := "", askGroup(d), c.AskID, d.ID, h.approvedOf(c.Heads)
|
|
if g != nil {
|
|
gid = g.ID
|
|
}
|
|
return func() { h.hand(gid, name, key, target, reads, approved) }
|
|
}
|
|
|
|
// hand reads a group's required statuses and, when all are a success, writes the target as the member being merged.
|
|
func (h *Holder) hand(gid, name, key, target string, reads []memberRead, approved map[string]string) {
|
|
if gid != "" && !h.groupMergeable(gid, target, reads) {
|
|
return
|
|
}
|
|
h.mu.Lock()
|
|
value, ok := h.mergeValue(name, key, approved, target, sdk.MergeMerging)
|
|
h.mu.Unlock()
|
|
if !ok {
|
|
h.giveBack(target, nil)
|
|
return
|
|
}
|
|
cur, err := h.Merges.Get(name)
|
|
if err == nil && cur != nil && sdk.SameMerge(*cur, value) {
|
|
return // handed already, the bridge has the revision
|
|
}
|
|
if err == nil {
|
|
err = h.Merges.Put(name, value)
|
|
}
|
|
if err != nil {
|
|
h.giveBack(target, err)
|
|
return
|
|
}
|
|
h.Logf("[mesh-delivery] %s is handed to the forge bridge to merge, at the heads the operator approved", target)
|
|
}
|
|
|
|
// giveBack gives a target its turn back after a hand that could not be made, to be asked again after a while.
|
|
func (h *Holder) giveBack(target string, err error) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if d := h.deliveries[target]; d != nil && d.Ready != nil {
|
|
d.Ready.merging = false
|
|
d.Ready.LastTry = time.Time{}
|
|
}
|
|
if err != nil {
|
|
h.Logf("[mesh-delivery] %s could not be handed to the forge bridge: %v; asked again in %s", target, err, askAgainAfter)
|
|
}
|
|
h.again("a merge that could not be handed to the forge bridge")
|
|
}
|
|
|
|
// mergeValue is the value of a group's merges key with these members merged, and the target in the state asked (merging, or
|
|
// refused with the forge's words): the group's members in order, those the Approve covers. Under the lock. False when it
|
|
// cannot be made (the target is not among the heads the Approve covers).
|
|
func (h *Holder) mergeValue(name, key string, approved map[string]string, target string, state sdk.MergeState) (sdk.Merge, bool) {
|
|
t := h.deliveries[target]
|
|
if t == nil {
|
|
return sdk.Merge{}, false
|
|
}
|
|
members := []*Delivery{t}
|
|
if g := h.groups[t.Group]; t.Group != "" && g != nil {
|
|
members = h.membersOf(g)
|
|
}
|
|
m := sdk.Merge{Group: name, Ask: key, Approved: approved}
|
|
for _, o := range members {
|
|
head := approved[sdk.ApprovedKey(o.Repository, o.Number)]
|
|
if head == "" {
|
|
continue
|
|
}
|
|
pull := sdk.MergePull{Head: sdk.Head{Repository: o.Repository, Number: o.Number, Head: head}}
|
|
switch {
|
|
case o.MergedAs != "" || (o.Ready != nil && o.Ready.Merged != nil):
|
|
commit := o.MergedAs
|
|
if o.Ready != nil && o.Ready.MergeCommit != "" {
|
|
commit = o.Ready.MergeCommit
|
|
}
|
|
if commit == "" {
|
|
continue
|
|
}
|
|
pull.State, pull.Commit = sdk.MergeMerged, commit
|
|
case o == t:
|
|
pull.State = state
|
|
if state == sdk.MergeRefused {
|
|
pull.Words = o.Ready.Answer
|
|
}
|
|
m.Pulls = append(m.Pulls, pull)
|
|
return m, true
|
|
default:
|
|
return m, false
|
|
}
|
|
m.Pulls = append(m.Pulls, pull)
|
|
}
|
|
return m, len(m.Pulls) > 0 && state == sdk.MergeMerged
|
|
}
|
|
|
|
// Forged takes a message of the forge bridge on FORGE: the gate follows a merge and the refusal of a merge, and nothing else.
|
|
func (h *Holder) Forged(event string, body []byte) {
|
|
f, err := sdk.DecodeForge(body)
|
|
if err != nil {
|
|
h.Refuse(fmt.Errorf("a message of the forge bridge is not readable: %v", err))
|
|
return
|
|
}
|
|
switch {
|
|
case f.Kind == sdk.ForgeMerged:
|
|
h.ForgeMerged(f)
|
|
case f.Kind == sdk.ForgeRefused && f.Act == "merge":
|
|
h.ForgeRefused(f)
|
|
}
|
|
}
|
|
|
|
// memberOfForge is the delivery a forge message is about: at the head it names.
|
|
func (h *Holder) memberOfForge(f sdk.Forge) *Delivery {
|
|
if d := h.deliveries[IDOf(f.Repository, f.Head)]; d != nil && d.Number == f.Number {
|
|
return d
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ForgeMerged takes the bridge's word that a pull request the gate handed to it merged: the merge is announced once,
|
|
// with the ask, its asker the teller and the heads (issue 1025), and the next member's turn comes at the next look.
|
|
func (h *Holder) ForgeMerged(f sdk.Forge) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
d := h.memberOfForge(f)
|
|
if d == nil || d.Ready == nil || d.Ready.Approved == nil || d.Ready.Merged != nil {
|
|
return
|
|
}
|
|
r, now := d.Ready, h.Now()
|
|
d.Owed = append(d.Owed, Effect{Kind: EffectEmit, Event: "merged", Line: string(mustJSON(mergedEvent(d, f.Head))), Since: now})
|
|
r.Merged, r.Answer, r.RefusedSince, r.merging, r.MergeCommit = &now, "merged", nil, false, f.Merge
|
|
h.Logf("[mesh-delivery] %s merged on the operator's Approve at %s, its check passed against its trunk's head", d.ID, shortOf(f.Head, 12))
|
|
h.keep(d)
|
|
}
|
|
|
|
// ForgeRefused takes the bridge's word that the forge refused a merge: nothing after it is merged, and the refusal is said
|
|
// as a stalled line (ADR 0441 §3, §4). The merge is handed again only at the next event that decides whether it may
|
|
// merge (mergeAgainNow).
|
|
func (h *Holder) ForgeRefused(f sdk.Forge) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
d := h.memberOfForge(f)
|
|
if d == nil || d.Ready == nil || d.Ready.Merged != nil {
|
|
return
|
|
}
|
|
r, now := d.Ready, h.Now()
|
|
r.merging, r.refusedNoted = false, false
|
|
answer := oneLine(f.Words)
|
|
var on time.Time
|
|
if d.Check != nil {
|
|
on = d.Check.At
|
|
}
|
|
if r.RefusedSince == nil {
|
|
r.RefusedSince, r.FirstRefusedOn = &now, on
|
|
}
|
|
r.LastRefusedOn = on
|
|
r.noteRefusal(d, answer)
|
|
if !outOfDateRefusal(answer) {
|
|
r.Urgent = true // never a warning again for this head
|
|
}
|
|
if r.Repeats >= maxRepeatedRefusals {
|
|
// Loud, not endless (novox/hq issue 1106): the same refusal again with the trunk not moved is no trunk's move, so
|
|
// the merge is not asked again by a verdict or a status; the operator is told, urgent, and decides.
|
|
r.Urgent = true
|
|
if r.Repeats == maxRepeatedRefusals {
|
|
h.Logf("[mesh-delivery] %s: the merge tool refused it %d times in a row with the same words and the trunk not moved, so "+
|
|
"the merge is asked no more until the trunk moves, a new head is announced or the operator approves again: %s",
|
|
d.ID, r.Repeats, answer)
|
|
}
|
|
}
|
|
if answer != r.Answer {
|
|
h.Logf("[mesh-delivery] %s is approved, and the merge tool did not merge it (asked again at the next verdict, Approve or composed check): %s", d.ID,
|
|
answer)
|
|
}
|
|
r.Answer = answer
|
|
h.keep(d)
|
|
}
|
|
|
|
// settleMerges works out, under the lock, what a merge owes its key and the source once the bridge has spoken: a refused
|
|
// member written refused, and a group whose members are all merged written merged and moved onto source.main in one write.
|
|
// The writing happens outside the lock.
|
|
func (h *Holder) settleMerges() []func() {
|
|
var out []func()
|
|
done := map[string]bool{}
|
|
ids := make([]string, 0, len(h.deliveries))
|
|
for id := range h.deliveries {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Strings(ids)
|
|
for _, id := range ids {
|
|
d := h.deliveries[id]
|
|
r := d.Ready
|
|
if r == nil || r.Approved == nil || r.AskID == "" || len(r.Heads) == 0 {
|
|
continue
|
|
}
|
|
name, key, approved := askGroup(d), r.AskID, h.approvedOf(r.Heads)
|
|
switch {
|
|
case r.Merged == nil && r.RefusedSince != nil && !r.refusedNoted && r.Answer != "" && r.Answer != "merged":
|
|
r.refusedNoted = true
|
|
target := d.ID
|
|
out = append(out, func() {
|
|
h.mu.Lock()
|
|
value, ok := h.mergeValue(name, key, approved, target, sdk.MergeRefused)
|
|
h.mu.Unlock()
|
|
if ok {
|
|
if err := h.Merges.Put(name, value); err != nil {
|
|
h.Logf("[mesh-delivery] the refusal of %s could not be written to its merge: %v", target, err)
|
|
}
|
|
}
|
|
})
|
|
case r.Merged != nil && !r.SourceMoved && !done[key]:
|
|
if !h.allMerged(r.Heads) {
|
|
continue
|
|
}
|
|
done[key] = true
|
|
heads := map[string]string{}
|
|
var members []string
|
|
for hid := range r.Heads {
|
|
m := h.deliveries[hid]
|
|
commit := m.MergedAs
|
|
if m.Ready != nil && m.Ready.MergeCommit != "" {
|
|
commit = m.Ready.MergeCommit
|
|
}
|
|
heads[m.Repository] = commit
|
|
members = append(members, hid)
|
|
}
|
|
sort.Strings(members)
|
|
last := h.deliveries[members[len(members)-1]]
|
|
out = append(out, func() { h.finish(name, key, approved, last.ID, heads, members) })
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// allMerged is whether every delivery an Approve covers is merged.
|
|
func (h *Holder) allMerged(heads map[string]string) bool {
|
|
for id := range heads {
|
|
m := h.deliveries[id]
|
|
if m == nil || (m.MergedAs == "" && (m.Ready == nil || m.Ready.Merged == nil)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// finish writes a group's merge as all merged and moves source.main to the merge commits in one compare-and-set write: the
|
|
// group is one revision of the mesh's source (ADR 0437 §1, §8).
|
|
func (h *Holder) finish(name, key string, approved map[string]string, last string, heads map[string]string, members []string) {
|
|
h.mu.Lock()
|
|
value, ok := h.mergeValue(name, key, approved, last, sdk.MergeMerged)
|
|
h.mu.Unlock()
|
|
var err error
|
|
if ok {
|
|
err = h.Merges.Put(name, value)
|
|
}
|
|
if err == nil {
|
|
err = h.Source.Move(func(prev *sdk.Source) (sdk.Source, bool) {
|
|
next := sdk.Source{Group: name, Heads: map[string]string{}}
|
|
if prev != nil {
|
|
for repository, head := range prev.Heads {
|
|
next.Heads[repository] = head
|
|
}
|
|
}
|
|
for repository, commit := range heads {
|
|
next.Heads[repository] = commit
|
|
}
|
|
return next, prev == nil || !reflect.DeepEqual(prev.Heads, next.Heads)
|
|
})
|
|
}
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if err != nil {
|
|
h.Logf("[mesh-delivery] group %s merged, and its place in the source cannot be written yet: %v; asked again in %s", name, err, askAgainAfter)
|
|
h.again("source.main, moved once every member is merged")
|
|
return
|
|
}
|
|
for _, id := range members {
|
|
if m := h.deliveries[id]; m != nil && m.Ready != nil {
|
|
m.Ready.SourceMoved = true
|
|
h.keep(m)
|
|
}
|
|
}
|
|
h.Logf("[mesh-delivery] group %s is merged whole: source.main holds %s", name, strings.Join(members, ", "))
|
|
}
|