mesh/merge-gate pass: builds mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
A number handed out unchecked against hq is a failure that must be loud, so the answer carries a warning, not only the log. The import already calls records_status and records_read; invokes now names them.
313 lines
12 KiB
Go
313 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// The tracker hands out decision and research numbers too (novox/hq ADR 0275 rule 8, ADR 0290 rule 5, issue
|
|
// 424): one counter per sequence in the state `counters`, beside `issue`, advanced the same way, so two
|
|
// sessions writing a record at the same moment never take one number. A reserved number is spent forever,
|
|
// whether or not its record ever lands; each reservation is kept in the state `reservations` and said as the
|
|
// event `reserved`.
|
|
|
|
const (
|
|
// ReservationsState keeps one key per reserved number, named by ReservationKey.
|
|
ReservationsState = "reservations"
|
|
// EventReserved is said for every number reserved.
|
|
EventReserved = "reserved"
|
|
|
|
KindDecision = "decision"
|
|
KindResearch = "research"
|
|
|
|
// DecisionsFolder and ResearchFolder are where hq keeps the records whose numbers a counter starts above.
|
|
DecisionsFolder = "02-DECISIONS"
|
|
ResearchFolder = "01-RESEARCH"
|
|
|
|
// raiseAttempts is how many times a counter behind the record is read and raised again.
|
|
raiseAttempts = 16
|
|
)
|
|
|
|
// ReserveKinds are the sequences reserve hands numbers from; each is also its counter's key in `counters`.
|
|
var ReserveKinds = []string{KindDecision, KindResearch}
|
|
|
|
// sequence is one kind of record numbered by the tracker: where the record keeps it, how many digits its
|
|
// number is written with, and the operator's floor for it.
|
|
type sequence struct {
|
|
kind string
|
|
folder string
|
|
digits int
|
|
floor func(Settings) uint64
|
|
}
|
|
|
|
var sequences = map[string]sequence{
|
|
KindDecision: {KindDecision, DecisionsFolder, 4, func(s Settings) uint64 { return s.DecisionFloor }},
|
|
KindResearch: {KindResearch, ResearchFolder, 3, func(s Settings) uint64 { return s.ResearchFloor }},
|
|
}
|
|
|
|
// recordNumber is a record's name that starts with its number: 0291-some-words.md, 045-some-words.
|
|
var recordNumber = regexp.MustCompile(`^([0-9]{1,9})-`)
|
|
|
|
// ReservationKey is a reservation's key: the kind and the number as the record writes it, such as
|
|
// decision-0292 or research-046.
|
|
func ReservationKey(kind string, n uint64) string {
|
|
return kind + "-" + prefixOf(kind, n)
|
|
}
|
|
|
|
func prefixOf(kind string, n uint64) string {
|
|
return fmt.Sprintf("%0*d", sequences[kind].digits, n)
|
|
}
|
|
|
|
// ReserveAsk is what a reserve carries.
|
|
type ReserveAsk struct {
|
|
Kind string
|
|
For string
|
|
}
|
|
|
|
// Reservation is one reserved number as it is kept.
|
|
type Reservation struct {
|
|
Kind string `json:"kind"`
|
|
Number uint64 `json:"number"`
|
|
For string `json:"for,omitempty"`
|
|
By string `json:"by"`
|
|
At time.Time `json:"at"`
|
|
// ID is the call's own id, so a write whose answer was lost is known as its own.
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// Reserved is the answer to a reserve: kind and number are the contract; the rest says what was kept.
|
|
type Reserved struct {
|
|
Kind string `json:"kind"`
|
|
Number uint64 `json:"number"`
|
|
// Prefix is the number as the record's name starts with it, such as 0292.
|
|
Prefix string `json:"prefix"`
|
|
For string `json:"for,omitempty"`
|
|
By string `json:"by"`
|
|
At time.Time `json:"at"`
|
|
Said string `json:"said"`
|
|
Announced bool `json:"announced"`
|
|
Warning string `json:"warning,omitempty"`
|
|
}
|
|
|
|
// Reserve hands out the next number of a sequence and keeps who reserved it, when and for what. A refused
|
|
// reserve spends no number; a number handed out is spent forever.
|
|
func (t *Tracker) Reserve(a ReserveAsk) (*Reserved, error) {
|
|
kind := a.Kind
|
|
seq, ok := sequences[kind]
|
|
if !ok {
|
|
return nil, fmt.Errorf("kind %q is not a kind of number the tracker hands out: one of %s (an issue's number "+
|
|
"comes from open). No number was taken", a.Kind, strings.Join(ReserveKinds, ", "))
|
|
}
|
|
purpose := ""
|
|
if strings.TrimSpace(a.For) != "" {
|
|
var err error
|
|
if purpose, err = singleLine("for", a.For, MostTitle); err != nil {
|
|
return nil, fmt.Errorf("%v. No number was taken", err)
|
|
}
|
|
}
|
|
records := t.Records
|
|
if records == nil {
|
|
records = busRecords{}
|
|
}
|
|
highest, rerr := highestIn(records, seq)
|
|
n, unread, err := t.nextOf(seq, highest, rerr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r := Reservation{Kind: kind, Number: n, For: purpose, By: t.by(), At: t.now(), ID: newID()}
|
|
if err := t.keep(&r); err != nil {
|
|
return nil, err
|
|
}
|
|
out := &Reserved{Kind: kind, Number: n, Prefix: prefixOf(kind, n), For: purpose, By: r.By, At: r.At,
|
|
Said: fmt.Sprintf("Reserved %s number %s; it is spent, whether or not its record lands.", kind, prefixOf(kind, n)),
|
|
Warning: unread}
|
|
body := map[string]any{"kind": kind, "number": n, "prefix": out.Prefix, "by": r.By, "at": r.At}
|
|
if purpose != "" {
|
|
body["for"] = purpose
|
|
}
|
|
if err := t.Emit(EventReserved, body); err != nil {
|
|
t.Logf("[mesh-issues] %s %s was reserved and its reserved event could not be said: %v", kind, out.Prefix, err)
|
|
out.Warning = joinWarnings(out.Warning, fmt.Sprintf("the reservation is kept, and its reserved event could not be said on the bus (%v): "+
|
|
"what watches the events did not hear of it", err))
|
|
} else {
|
|
out.Announced = true
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func joinWarnings(a, b string) string {
|
|
if a == "" {
|
|
return b
|
|
}
|
|
return a + "; and " + b
|
|
}
|
|
|
|
// highestIn is the highest number the record holds in a sequence's folder, sub-folders and documents alike.
|
|
func highestIn(records Records, seq sequence) (uint64, error) {
|
|
folders, docs, err := records.List(seq.folder)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var highest uint64
|
|
for _, name := range append(folders, docs...) {
|
|
if m := recordNumber.FindStringSubmatch(name); m != nil {
|
|
if n, err := strconv.ParseUint(m[1], 10, 64); err == nil && n > highest {
|
|
highest = n
|
|
}
|
|
}
|
|
}
|
|
return highest, nil
|
|
}
|
|
|
|
// nextOf takes a sequence's next number. A counter not yet started is started once, above the highest the
|
|
// record holds, the operator's floor and every reservation kept; it is never started without the record
|
|
// read. A started counter behind the record (a number taken by hand) is raised to it first; a record that
|
|
// cannot be read then leaves the counter as it is, and the warning answered says so.
|
|
func (t *Tracker) nextOf(seq sequence, highest uint64, recordErr error) (uint64, string, error) {
|
|
warning := ""
|
|
e, err := t.Store.Get(CountersState, seq.kind)
|
|
if err != nil {
|
|
return 0, "", fmt.Errorf("the %s counter cannot be read (%v); no number was taken", seq.kind, err)
|
|
}
|
|
if e == nil {
|
|
if recordErr != nil {
|
|
return 0, "", fmt.Errorf("the %s counter has not started, and what numbers hq's %s holds cannot be read through "+
|
|
"the records module (%v): it starts above them, so no number was taken. Ask again when the records "+
|
|
"module answers", seq.kind, seq.folder, recordErr)
|
|
}
|
|
if err := t.startSequence(seq, highest); err != nil {
|
|
return 0, "", err
|
|
}
|
|
} else if recordErr != nil {
|
|
t.Logf("[mesh-issues] the record's %s could not be read (%v); the %s counter is used as it stands", seq.folder, recordErr, seq.kind)
|
|
warning = fmt.Sprintf("what numbers hq's %s holds could not be read through the records module (%v): the "+
|
|
"number comes from the %s counter as it stands, unchecked against hq. Check that hq holds no %s at or above it",
|
|
seq.folder, recordErr, seq.kind, seq.kind)
|
|
} else if err := t.raise(seq.kind, highest); err != nil {
|
|
return 0, "", err
|
|
}
|
|
for round := 0; round < counterRounds; round++ {
|
|
n, err := t.Store.Increment(CountersState, seq.kind, counterAttempts)
|
|
if err == nil {
|
|
return n, warning, nil
|
|
}
|
|
if !errors.Is(err, stdio.ErrChanged) {
|
|
t.Logf("[mesh-issues] the %s counter could not be advanced: %v", seq.kind, err)
|
|
return 0, "", fmt.Errorf("the %s counter could not be advanced (%v); nothing was reserved. If the bus took the "+
|
|
"write before it failed, one number stays unused", seq.kind, err)
|
|
}
|
|
t.Sleep(jitter(time.Duration(round+1) * 10 * time.Millisecond))
|
|
}
|
|
return 0, "", fmt.Errorf("so many %s numbers are being reserved at this moment that the counter was taken by another "+
|
|
"caller %d times; no number was taken. Reserve again", seq.kind, counterRounds*counterAttempts)
|
|
}
|
|
|
|
// startSequence makes a sequence's counter, only when there is none, at the highest of the record, the
|
|
// floor and the reservations kept: the first number handed out is one above all three.
|
|
func (t *Tracker) startSequence(seq sequence, highest uint64) error {
|
|
s, err := t.Settings()
|
|
if err != nil {
|
|
return fmt.Errorf("the %s counter has not started and the settings cannot be read (%v); no number was taken", seq.kind, err)
|
|
}
|
|
start := max(highest, seq.floor(s))
|
|
keys, err := t.Store.Keys(ReservationsState)
|
|
if err != nil {
|
|
return fmt.Errorf("the %s counter has not started and the reservations kept cannot be listed (%v); no number "+
|
|
"was taken", seq.kind, err)
|
|
}
|
|
for _, k := range keys {
|
|
if rest, ok := strings.CutPrefix(k, seq.kind+"-"); ok {
|
|
if n, err := strconv.ParseUint(rest, 10, 64); err == nil && n > start {
|
|
start = n
|
|
}
|
|
}
|
|
}
|
|
if _, err := t.Store.Create(CountersState, seq.kind, start); err != nil && !errors.Is(err, stdio.ErrChanged) {
|
|
return fmt.Errorf("the %s counter could not be started (%v); no number was taken", seq.kind, err)
|
|
}
|
|
// ErrChanged: another caller started it first; the raise on the next reserve covers any difference.
|
|
return nil
|
|
}
|
|
|
|
// raise sets a counter to the highest number the record holds when it stands below it, written only at the
|
|
// revision read: it only ever goes up.
|
|
func (t *Tracker) raise(key string, highest uint64) error {
|
|
for i := 0; i < raiseAttempts; i++ {
|
|
e, err := t.Store.Get(CountersState, key)
|
|
if err != nil {
|
|
return fmt.Errorf("the %s counter cannot be read (%v); no number was taken", key, err)
|
|
}
|
|
var n uint64
|
|
if e != nil {
|
|
if err := json.Unmarshal(e.Value, &n); err != nil {
|
|
return fmt.Errorf("the %s counter holds %s, not a number; no number was taken", key, e.Value)
|
|
}
|
|
}
|
|
if e == nil || n >= highest {
|
|
return nil
|
|
}
|
|
_, err = t.Store.Update(CountersState, key, highest, e.Revision)
|
|
if err == nil {
|
|
t.Logf("[mesh-issues] the %s counter stood at %d below %d in the record; raised to it", key, n, highest)
|
|
return nil
|
|
}
|
|
if !errors.Is(err, stdio.ErrChanged) {
|
|
return fmt.Errorf("the %s counter stands below the record's %d and could not be raised (%v); no number was "+
|
|
"taken", key, highest, err)
|
|
}
|
|
t.Sleep(jitter(time.Duration(i+1) * 5 * time.Millisecond))
|
|
}
|
|
return fmt.Errorf("the %s counter was changed by other callers %d times while it was raised to the record's %d; "+
|
|
"no number was taken. Reserve again", key, raiseAttempts, highest)
|
|
}
|
|
|
|
// keep writes a reservation under its key, never over one. A write that cannot be confirmed is refused, and
|
|
// the number stays spent: it is never handed out again.
|
|
func (t *Tracker) keep(r *Reservation) error {
|
|
key := ReservationKey(r.Kind, r.Number)
|
|
var err error
|
|
for i := 0; i < createAttempts; i++ {
|
|
if i > 0 {
|
|
t.Sleep(jitter(time.Duration(i) * 50 * time.Millisecond))
|
|
}
|
|
_, err = t.Store.Create(ReservationsState, key, r)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if errors.Is(err, stdio.ErrChanged) {
|
|
if mine, rerr := t.isKept(key, r.ID); rerr == nil && mine {
|
|
return nil
|
|
}
|
|
t.Logf("[mesh-issues] the %s counter handed out %d and a reservation %s is already kept: not overwritten", r.Kind, r.Number, key)
|
|
return fmt.Errorf("a fault, not your mistake: the %s counter handed out %d, and %s is already reserved. "+
|
|
"Nothing was overwritten; %d stays spent. Reserve again for the next number, and tell the operator "+
|
|
"the counter is behind the reservations kept", r.Kind, r.Number, key, r.Number)
|
|
}
|
|
}
|
|
if mine, rerr := t.isKept(key, r.ID); rerr == nil && mine {
|
|
return nil
|
|
}
|
|
t.Logf("[mesh-issues] %s was taken and its reservation's write was not confirmed: %v", key, err)
|
|
return fmt.Errorf("%s number %d was taken and its reservation could not be kept (%v): the number is spent and "+
|
|
"is not yours to use. Reserve again for the next one", r.Kind, r.Number, err)
|
|
}
|
|
|
|
func (t *Tracker) isKept(key, id string) (bool, error) {
|
|
e, err := t.Store.Get(ReservationsState, key)
|
|
if err != nil || e == nil {
|
|
return false, err
|
|
}
|
|
var there Reservation
|
|
if err := json.Unmarshal(e.Value, &there); err != nil {
|
|
return false, err
|
|
}
|
|
return there.ID == id, nil
|
|
}
|