Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/reserve.go
T
jschoubben 9d4d9d5cd2
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
mesh-issues: say in reserve's answer when hq could not be read, and grant the import its records calls (review of #204)
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.
2026-10-11 04:30:28 +02:00

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
}