Files
mesh-sdk/go/delivery/turns.go
T
jschoubben 9c61918ac3
mesh/delivery merged: source.main moved to novox/mesh-sdk@89c5838176b2
mesh/delivery-group group feat/548-one-check merged: every member is merged
mesh/check checking 9c61918ac36f on 85fa8511c4b3 | novox/mesh-sdk#88 in group feat/548-one-check
delivery: a turn never holds the check of heads that wait in it, so two approved merges in one turn never hold each other's carry (ADR 0548 §3; issue 568, review round 1)
2026-10-26 12:12:53 +01:00

175 lines
7.0 KiB
Go

package delivery
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
)
// **The merge turns** (novox/hq ADR 0548 clauses 3 and 4). An approved merge holds a turn on each repository it goes into:
// before it reads the checker's verdicts for the merge, the gate writes one key per repository in its bucket turns, naming
// the deliveries that wait in the order of their Approve, the verdict key each waits for and since when. The gate is the
// key's one writer; the checker is granted to read the bucket, reads it absent as empty, and claims no check of a repository
// a turn names, except the check of the waiting delivery's own heads (TurnHolds). The key goes when the last delivery in it
// merged or left.
const (
// TurnsBucket is the gate's bucket of merge turns.
TurnsBucket = GateModule + "_turns"
// TurnsHistory is the revisions the turns bucket keeps per key.
TurnsHistory = 5
)
// TurnsKey is the key, in TurnsBucket, of a repository's turn: `<owner>.<name>`, lowercase, each part escaped (EscapeName),
// so the key names the repository the checker's verdicts name as a trunk whatever its case.
func TurnsKey(repository string) (string, error) {
repository = strings.ToLower(repository)
if _, err := escapeRepository(repository); err != nil {
return "", err
}
owner, name, _ := strings.Cut(repository, "/")
return EscapeName(owner) + "." + EscapeName(name), nil
}
// ParseTurnsKey is the inverse of TurnsKey: the repository, lowercase.
func ParseTurnsKey(key string) (string, error) {
owner, name, ok := strings.Cut(key, ".")
if !ok || owner == "" || name == "" || strings.Contains(name, ".") {
return "", fmt.Errorf("delivery: %q is not a turns key, <owner>.<name>", key)
}
o, err := UnescapeName(owner)
if err != nil {
return "", fmt.Errorf("delivery: %q is not a turns key: %w", key, err)
}
n, err := UnescapeName(name)
if err != nil {
return "", fmt.Errorf("delivery: %q is not a turns key: %w", key, err)
}
repository := o + "/" + n
if again, err := TurnsKey(repository); err != nil || again != key {
return "", fmt.Errorf("delivery: %q is not a turns key: it does not name a lowercase <owner>/<name>", key)
}
return repository, nil
}
// Turn is the value of a turns key: the repository and the approved deliveries that wait to merge into it, in the order of
// their Approve. A turn with no delivery is no value: its key is deleted.
type Turn struct {
V int `json:"v"`
Repository string `json:"repository"`
Waiting []TurnWait `json:"waiting"`
}
// TurnWait is one approved delivery in a turn: its group (a lone pull request's branch), the hash of the heads the Approve
// covers (the checker's verdict key's hash, HeadsHash over its members), the Approve's ask key, when it was approved and since
// when it waits; and, while a check of another delivery runs on the repository, that check's verdict key, when the gate saw
// it start, and the step the checker says it is at.
type TurnWait struct {
Group string `json:"group"`
Heads string `json:"heads"`
Ask string `json:"ask,omitempty"`
Approved time.Time `json:"approved"`
Since time.Time `json:"since"`
// WaitsFor is the verdict key (`<group>.<heads hash>`) of the running check the merge waits for; empty when none runs.
WaitsFor string `json:"waits-for,omitempty"`
WaitsSince time.Time `json:"waits-since,omitzero"`
Step string `json:"step,omitempty"`
}
func (t Turn) check() error {
repository := strings.ToLower(t.Repository)
if repository != t.Repository {
return fmt.Errorf("delivery: the turn's repository %q is not lowercase", t.Repository)
}
if _, err := escapeRepository(repository); err != nil {
return err
}
if len(t.Waiting) == 0 {
return fmt.Errorf("delivery: the turn of %s names no delivery: its key is deleted instead", t.Repository)
}
seen := map[string]bool{}
for i, w := range t.Waiting {
if w.Group == "" {
return fmt.Errorf("delivery: a delivery in the turn of %s names no group", t.Repository)
}
if !isHex12(w.Heads) {
return fmt.Errorf("delivery: the heads hash %q in the turn of %s is not 12 lowercase hex digits", w.Heads, t.Repository)
}
if seen[w.Heads] {
return fmt.Errorf("delivery: the turn of %s names the heads %s twice", t.Repository, w.Heads)
}
seen[w.Heads] = true
if w.Approved.IsZero() || w.Since.IsZero() {
return fmt.Errorf("delivery: the delivery %s in the turn of %s says no time of its Approve or of its wait", w.Group, t.Repository)
}
if i > 0 && w.Approved.Before(t.Waiting[i-1].Approved) {
return fmt.Errorf("delivery: the turn of %s is not in the order of the Approve: %s was approved before %s", t.Repository, w.Group, t.Waiting[i-1].Group)
}
if w.WaitsFor != "" {
if _, _, err := ParseCheckerVerdictKey(w.WaitsFor); err != nil {
return fmt.Errorf("delivery: the delivery %s in the turn of %s waits for what is not a verdict key: %w", w.Group, t.Repository, err)
}
} else if !w.WaitsSince.IsZero() || w.Step != "" {
return fmt.Errorf("delivery: the delivery %s in the turn of %s says when and where a check runs, and names no check", w.Group, t.Repository)
}
}
return nil
}
// TurnHolds says whether a turn holds off the check of the heads: it names deliveries, and none of them of these heads. A
// turn never holds the check of heads that wait in it (the carry a waiting merge needs): two approved merges in one turn whose
// trunk moved would otherwise each hold the other's carry, and neither would ever merge (review of ADR 0548's build, round 1).
func TurnHolds(t Turn, heads string) bool {
if len(t.Waiting) == 0 {
return false
}
for _, w := range t.Waiting {
if w.Heads == heads {
return false
}
}
return true
}
// EncodeTurn is t as JSON at Version; it refuses a turn with no delivery, a repository that is not lowercase <owner>/<name>,
// a delivery without its group, heads hash or times, the same heads twice, an order other than the Approve's, and a check
// waited for that is not a verdict key.
func EncodeTurn(t Turn) ([]byte, error) {
if err := t.check(); err != nil {
return nil, err
}
t.V = Version
return json.Marshal(t)
}
// DecodeTurn reads a turn; it refuses an unknown version (ErrUnknownVersion) and what EncodeTurn refuses.
func DecodeTurn(data []byte) (Turn, error) {
var t Turn
if err := decode(data, &t); err != nil {
return Turn{}, err
}
if err := t.check(); err != nil {
return Turn{}, err
}
return t, nil
}
// SameTurn says whether two turns are the same value, so the gate writes only when they differ.
func SameTurn(a, b Turn) bool {
if len(a.Waiting) != len(b.Waiting) || a.Repository != b.Repository || a.V != b.V {
return false
}
for i := range a.Waiting {
x, y := a.Waiting[i], b.Waiting[i]
if !x.Approved.Equal(y.Approved) || !x.Since.Equal(y.Since) || !x.WaitsSince.Equal(y.WaitsSince) {
return false
}
x.Approved, y.Approved, x.Since, y.Since, x.WaitsSince, y.WaitsSince = time.Time{}, time.Time{}, time.Time{}, time.Time{}, time.Time{}, time.Time{}
if !reflect.DeepEqual(x, y) {
return false
}
}
return true
}