The tool runner now takes a call's caller from the subject it arrived on and hands it to the module through the SDK (stdio.CallerOf); the tracker records that bus user on what each verb writes instead of unproven. Never from the arguments, which still refuse any caller, and unproven where the runtime proves none. Needs mesh-sdk's go/caller.go (pinned at its branch head until merged).
316 lines
12 KiB
Go
316 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) { return t.reserve(t.by(), a) }
|
|
|
|
// reserve is Reserve as the one who acts.
|
|
func (t *Tracker) reserve(by string, 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: 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
|
|
}
|