package main import ( "encoding/json" "errors" "fmt" "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`. // // A counter starts, and is raised, above the highest number the knowledge base holds (its knowledge_highest, // hq issue 488): the records live there, and hq's folders empty when they leave it (hq issue 426). 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" // HighestTool is the knowledge base's tool that answers the highest number it holds of each kind. HighestTool = "mesh-knowledge.knowledge_highest" // 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: how many digits its number is written with, and the // operator's floor for it. type sequence struct { kind string digits int floor func(Settings) uint64 } var sequences = map[string]sequence{ KindDecision: {KindDecision, 4, func(s Settings) uint64 { return s.DecisionFloor }}, KindResearch: {KindResearch, 3, func(s Settings) uint64 { return s.ResearchFloor }}, } // 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) } } highest, herr := t.highestHeld(seq) n, unread, err := t.nextOf(seq, highest, herr) 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 } // highestHeld is the highest number of a sequence the knowledge base holds. A knowledge base that holds none of // the kind answers nothing: a counter started from 0 would hand out 1. func (t *Tracker) highestHeld(seq sequence) (uint64, error) { ask := t.Highest if ask == nil { ask = busHighest } held, err := ask() if err != nil { return 0, err } n := held[seq.kind] if n == 0 { return 0, fmt.Errorf("the knowledge base holds no %s record", seq.kind) } return n, nil } // busHighest asks the knowledge base for the highest number it holds of each kind. func busHighest() (map[string]uint64, error) { raw, err := stdio.Ask(HighestTool, map[string]any{}) if err != nil { return nil, fmt.Errorf("%s did not answer: %v", HighestTool, err) } var h struct { Decision *uint64 `json:"decision"` Research *uint64 `json:"research"` } if err := json.Unmarshal(raw, &h); err != nil || h.Decision == nil || h.Research == nil { return nil, fmt.Errorf("%s answered no highest numbers: %.200s", HighestTool, raw) } return map[string]uint64{KindDecision: *h.Decision, KindResearch: *h.Research}, nil } // nextOf takes a sequence's next number. A counter not yet started is started once, above the highest the // knowledge base holds, the operator's floor and every reservation kept; with the knowledge base silent it is not // started, floor or no floor, and nothing is spent. A started counter // behind the knowledge base (a number taken by hand, or imported) is raised to it first; a knowledge base that // does not answer then leaves the counter as it is, and the warning answered says so. func (t *Tracker) nextOf(seq sequence, highest uint64, heldErr 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 heldErr != nil { return 0, "", fmt.Errorf("the %s counter has not started, and the knowledge base does not answer the highest %s "+ "number it holds (%v): the counter starts above it, and a floor alone cannot say a number is free, so no "+ "number was taken. Ask again when the knowledge base answers", seq.kind, seq.kind, heldErr) } if err := t.startSequence(seq, highest); err != nil { return 0, "", err } } else if heldErr != nil { t.Logf("[mesh-issues] the knowledge base did not answer its highest %s (%v); the %s counter is used as it stands", seq.kind, heldErr, seq.kind) warning = fmt.Sprintf("the knowledge base did not answer the highest %s number it holds (%v): the number comes "+ "from the %s counter as it stands, unchecked against the records held. Check that the knowledge base holds no "+ "%s at or above it", seq.kind, heldErr, 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 knowledge base, 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 knowledge base 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 knowledge base; raised to it", key, n, highest) return nil } if !errors.Is(err, stdio.ErrChanged) { return fmt.Errorf("the %s counter stands below the knowledge base'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 knowledge base'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 }