package main import ( "errors" "fmt" "strings" "time" ) // The state table (novox/hq ADR 0239 decision 2, to-be 47): every transition a delivery may take, each with // its guard, compiled once. The code moves a delivery only through Apply, which finds the row for the // delivery's state and the event, runs its guard and refuses anything else, naming what was asked. A test // walks the table: every row it holds, and every pair it does not. // State is where a delivery is. type State string // The states, in the order a delivery passes through them. None is a delivery not yet made: the rows from // it are how one comes to exist. const ( None State = "" Proposed State = "proposed" Checked State = "checked" Ready State = "ready" Rejected State = "rejected" Published State = "published" Delivering State = "delivering" Held State = "held" Delivered State = "delivered" Failed State = "failed" Superseded State = "superseded" Stopped State = "stopped" // Retired is history: a merge this owner heard only from the forge's word, long after it happened — // replayed from the bus when it first held the seat. It reached the machines before this owner existed, // and nothing is delivered, released or put back for it (novox/hq issue 313). Retired State = "retired" // Covered is a merge a later one that contains it delivered: every module it moves was sent to every machine // running it by the finished walk of a later merge into the same trunk, whose own check passed and which the // forge says contains this one. It is not walked itself, and it is no failure: its pull request says covered, // as a success (novox/hq ADR 0270). A revert merged in between is still possible: covered says what was // delivered, not what runs. Covered State = "covered" ) // AllStates are every state a delivery can be in, for the table's own test and its verb. var AllStates = []State{Proposed, Checked, Ready, Rejected, Published, Delivering, Held, Delivered, Failed, Superseded, Stopped, Retired, Covered} // Final is whether nothing follows a state. func (s State) Final() bool { return s == Delivered || s == Failed || s == Superseded || s == Stopped || s == Retired || s == Covered } // Reached is whether a final state says the change reached the machines, or had nothing to reach them // with: what a group counts as delivered. func (s State) Reached() bool { return s == Delivered || s == Retired || s == Covered } // Event is what moves a delivery. type Event string // The events. Observed ones are taken by settle whenever their guard holds; the others are acts — a // person's, or healer H2's — taken only when asked. const ( EvAnnounced Event = "announced" // the forge announced a pull request's head EvAppeared Event = "appeared" // a trunk commit the mesh is delivering, with no pull request known EvAdopted Event = "adopted" // a walk already running at the switch, or on the controller's own path EvChecked Event = "checked" // its check's verdict arrived EvAccepted Event = "accepted" // the verdict passes EvRefused Event = "refused" // the verdict fails, or the check could not run EvRecheck Event = "recheck" // a person asked for it again, or its group changed EvReask Event = "re-ask" // healer H2 asked a check asked and not answered once more EvNewHead Event = "new-head" // a newer head of the same pull request EvClosed Event = "closed" // the pull request closed unmerged EvMerged Event = "merged" // it reached the trunk EvMergedUnchecked Event = "merged-unchecked" // it reached the trunk without a passing check EvGo Event = "go" // its walk started EvHold Event = "hold" // something holds it for a person EvRelease Event = "release" // a person's word that it goes on EvDone Event = "done" // every machine of its deploy plan took it, or nothing was to be sent EvFailed Event = "failed" // its walk failed: a gate, a build, a machine EvSuperseded Event = "superseded" // a newer delivery to the same trunk took over its walk EvStop Event = "stop" // a person stopped it, or its group did EvHistory Event = "history" // made from the forge's word for a merge long past: history EvRetire Event = "retire" // a person's word that a held delivery is history, never to be walked EvCovered Event = "covered" // a later merge into the same trunk delivered every module it moves EvLifted Event = "lifted" // what held it for a person is no longer so: its group's composed check passed, or a walk now answers its merge EvRewalk Event = "rewalk" // the walk that carried a later commit for it failed, or was stopped for another merge (ADR 0299): it waits for the walk that walks it again ) // Facts are what a guard reads beyond the delivery itself: the moment, and who asks, for an act. type Facts struct { Now time.Time By string Why string } // Row is one transition the table holds. type Row struct { From []State Event Event To State // Guard says, in words, what must be true; Holds checks it, answering why not. Guard string Holds func(d *Delivery, f Facts) error // Act says the row is taken only when asked — a person's or H2's — never by settle. Act bool // Says is the transition's why when nobody gave one: the fact that moved it, rather than the guard. Says func(d *Delivery) string } func unless(cond bool, why string) error { if cond { return nil } return fmt.Errorf("%s", why) } var notFinal = []State{Proposed, Checked, Ready, Rejected, Published, Delivering, Held} // Table is every transition there is. The order within one state is the order settle tries them. var Table = []Row{ {From: []State{None}, Event: EvAnnounced, To: Proposed, Guard: "a pull request's head the forge announced", Holds: func(d *Delivery, f Facts) error { return unless(d.Number > 0 && d.Commit != "", "no pull request's head") }}, {From: []State{None}, Event: EvAppeared, To: Held, Guard: "a commit on the trunk, its walk waiting, and no pull request known: it waits for a person", Holds: func(d *Delivery, f Facts) error { return unless(d.MergedAs != "" && d.HeldWhy != "", "no trunk commit, or nothing says why it is held") }}, {From: []State{None}, Event: EvAdopted, To: Delivering, Guard: "a walk already running: at the switch, or on the controller's own path", Holds: func(d *Delivery, f Facts) error { return unless(d.Walk != nil && d.Walk.Started(), "no running walk") }}, // History is taken before anything else of a delivery off the trunk: a merge long past, heard only from // the forge's word, is neither checked, held nor walked (novox/hq issue 313). {From: []State{Proposed, Checked, Ready, Rejected}, Event: EvHistory, To: Retired, Guard: "history: made from the forge's word for a merge on the trunk heard " + historyLate.String() + " or more " + "after it happened — replayed when this owner first held the seat — and no walk of it open", Holds: func(d *Delivery, f Facts) error { if d.History == "" { return errors.New("nothing says it is history") } return isHistory(d) }}, {From: []State{Held}, Event: EvRetire, To: Retired, Act: true, Guard: "a person's word, with why, for history only: made from the forge's word for a merge on the trunk heard " + historyLate.String() + " or more after it happened, and no walk of it open — never released or delivered", Holds: func(d *Delivery, f Facts) error { if f.Why == "" || f.By == "" { return errors.New("a retire says who and why") } return isHistory(d) }}, {From: []State{Proposed, Checked, Ready, Rejected}, Event: EvNewHead, To: Superseded, Guard: "a newer head of the same pull request", Holds: func(d *Delivery, f Facts) error { return unless(d.NewerHead != "", "no newer head") }}, {From: []State{Proposed, Checked, Ready, Rejected}, Event: EvClosed, To: Stopped, Guard: "the pull request closed unmerged", Holds: func(d *Delivery, f Facts) error { return unless(d.ClosedUnmerged, "the pull request is open") }}, {From: []State{Ready}, Event: EvMerged, To: Published, Guard: "on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move", Holds: func(d *Delivery, f Facts) error { return unless(d.MergedAs != "" && (d.Walk != nil || d.MovesNothing()), "not on the trunk yet") }}, {From: []State{Proposed}, Event: EvChecked, To: Checked, Guard: "the verdict names this commit", Holds: func(d *Delivery, f Facts) error { return unless(d.Check != nil, "no verdict") }}, {From: []State{Checked}, Event: EvAccepted, To: Ready, Guard: "the gate passed or warned, and the repository's own check did not fail", Holds: func(d *Delivery, f Facts) error { return unless(d.Check.Passes(), "the verdict does not pass") }}, {From: []State{Checked}, Event: EvRefused, To: Rejected, Guard: "the gate failed or could not run, or the repository's own check failed", Holds: func(d *Delivery, f Facts) error { return unless(!d.Check.Passes(), "the verdict passes") }}, {From: []State{Proposed, Checked, Rejected, Held}, Event: EvDone, To: Delivered, Guard: "merged, and its plan builds nothing for a walk to move — nothing at all, or only a new module the " + "controller builds and sends nowhere: nothing to deliver and nothing for a person to decide, whatever its check said", Holds: func(d *Delivery, f Facts) error { return unless(d.MergedAs != "" && d.MovesNothing() && d.Walk == nil, "not merged, or a walk moves it") }, Says: func(d *Delivery) string { return "merged, and nothing for a walk to move: " + d.Plan.Summary }}, {From: []State{Ready, Held}, Event: EvCovered, To: Covered, Guard: "merged, no walk of its own open, and every module it moves sent to every machine running it by the " + "finished walk of a later merge into the same trunk, whose own check passed and which the forge says contains " + "it: a later merge that contains it was delivered", Holds: func(d *Delivery, f Facts) error { switch { case d.MergedAs == "" || d.CoveredBy == "": return errors.New("not merged, or nothing later carried its modules") case d.Walk != nil && (d.Walk.State == walkBuilding || d.Walk.State == walkRolling): return fmt.Errorf("its own walk %s is open: the controller delivers it", d.Walk.ID) } return nil }, Says: func(d *Delivery) string { return "covered: a later merge that contains it was delivered: " + d.CoveredBy }}, {From: []State{Proposed, Checked, Rejected}, Event: EvMergedUnchecked, To: Held, Guard: "merged without a passing check — a verdict known with it is taken first: only a person decides that it goes on", Holds: func(d *Delivery, f Facts) error { return unless(d.MergedAs != "", "not merged") }}, {From: []State{Rejected, Ready}, Event: EvRecheck, To: Proposed, Act: true, Guard: "a person asked, with why, or its group changed", Holds: func(d *Delivery, f Facts) error { return unless(f.Why != "", "a recheck says why") }}, {From: []State{Proposed}, Event: EvReask, To: Proposed, Act: true, Guard: "its check was asked and no verdict came within its bound, and it was not re-asked before: healer H2 " + "asks it once more, with why — after that it is the operator's", Holds: func(d *Delivery, f Facts) error { switch { case f.Why == "" || f.By == "": return errors.New("a re-ask says who and why") case d.Check != nil: return errors.New("its verdict is known") case d.CheckAsk == nil: return errors.New("its check was never asked: the tick asks it, not H2") case d.CheckAsk.Reasked > 0: return errors.New("its check was re-asked once already: the operator's") } return nil }}, {From: []State{Published}, Event: EvSuperseded, To: Superseded, Guard: "its walk was superseded and names no walk that took it over: a walk that took it over answers its merge, and it follows that walk (novox/hq ADR 0276)", Holds: func(d *Delivery, f Facts) error { if d.Walk != nil && d.Walk.State == walkSuperseded && d.Walk.TakenOverBy != "" { return fmt.Errorf("its walk was taken over by %s, which answers its merge: it follows that walk", d.Walk.TakenOverBy) } return unless(d.Walk != nil && d.Walk.State == walkSuperseded, "its walk is not superseded") }}, {From: []State{Published}, Event: EvDone, To: Delivered, Guard: "nothing for a walk to move; or its walk, carrying its own commit, ended done before its word having built " + "every module it moves (novox/hq issue 474)", Holds: func(d *Delivery, f Facts) error { if d.Walk == nil && d.MovesNothing() { return nil } moved, _ := d.walkMovedItsModules() return unless(d.endedUnstarted() && !d.carriedLater() && moved, "a walk moves it") }, Says: func(d *Delivery) string { if d.Walk == nil { return "nothing for a walk to move" } return fmt.Sprintf("its walk %s ended done before its word, having built every module it moves", d.Walk.ID) }}, {From: []State{Published, Ready}, Event: EvHold, To: Held, Guard: "something holds it for a person: its group's composed check did not pass for the heads that merged, " + "or it merged and no walk was opened for it", Holds: func(d *Delivery, f Facts) error { return unless(d.HeldWhy != "" && d.MergedAs != "", "nothing holds it, or it is not on the trunk") }}, {From: []State{Published}, Event: EvGo, To: Delivering, Guard: "its walk started: let go by this owner in its group's order, by a person, or on the controller's own path " + "— not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit", Holds: func(d *Delivery, f Facts) error { if d.Walk != nil && d.Walk.State == walkFailed && d.carriedLater() { return errors.New("its walk, which carried a later commit for it, failed: it waits for a walk of its own commit") } if d.walkStopped() { return errors.New("its walk was stopped: it waits for the walk that walks its merge again, or ends stopped") } return unless(d.Walk != nil && d.Walk.Started(), "its walk waits") }}, // **A walk that ended before its word settles its delivery** (novox/hq issue 474): a batch's walk the // controller judged to move nothing it holds is done when it is cut, and a merge it carried was left published, // waiting for a start that cannot come. Covered when that walk, carrying a later merge for it, built its modules, // or when a later delivered merge carried them (ADR 0270's covered test); stopped, saying why, when neither. {From: []State{Published}, Event: EvCovered, To: Covered, Guard: "its walk ended done before its word, and its modules were delivered by a later merge that contains it: " + "that walk, carrying it, built them, or a later merge's finished walk sent them everywhere (ADR 0270)", Holds: func(d *Delivery, f Facts) error { if !d.endedUnstarted() { return errors.New("its walk has not ended before its word") } moved, _ := d.walkMovedItsModules() return unless(d.carriedLater() && moved || d.MergedAs != "" && d.CoveredBy != "", "nothing later delivered its modules") }, Says: func(d *Delivery) string { if moved, _ := d.walkMovedItsModules(); d.carriedLater() && moved { return "covered: a later merge that contains it was delivered: " + d.Carrier } return "covered: a later merge that contains it was delivered: " + d.CoveredBy }}, {From: []State{Published}, Event: EvStop, To: Stopped, Guard: "its walk ended done before its word without building every module it moves, and no later merge that " + "contains it delivered them: no start can come, and nothing delivered it (novox/hq issue 474); or the walk " + "it waits on was stopped through this owner and walks its merge again nowhere — left out, or the walk " + "ended whole (novox/hq ADR 0299)", Holds: func(d *Delivery, f Facts) error { if d.walkStopped() && !d.walkedAgain() { return nil } return unless(d.endedUnstarted() && d.Stranded != "", "its walk has not ended unstarted, or something may cover it") }, Says: func(d *Delivery) string { if d.walkStopped() { return d.stoppedWords() } return d.Stranded }}, {From: []State{Held}, Event: EvGo, To: Delivering, Guard: "its walk started on a word that was not this owner's: the controller's own path, which waits for " + "nobody, or a person's `plans go`", Holds: func(d *Delivery, f Facts) error { return unless(d.Walk != nil && d.Walk.Started() && (!d.Walk.Waited() || d.Walk.LetGoByAPerson()), "its walk was not started by a person or the controller's own path") }}, {From: []State{Held}, Event: EvLifted, To: Published, Guard: "held only for its group's composed check, which has since passed or warned for the heads that merged, or " + "held because no walk answered its merge, and one now does: it is published again and waits for its turn", Holds: func(d *Delivery, f Facts) error { return unless(d.Lifted != "" && d.HeldWhy == "" && d.MergedAs != "" && walkCanGoOn(d), "nothing lifted its hold") }, Says: func(d *Delivery) string { return "lifted: " + d.Lifted }}, {From: []State{Held}, Event: EvRelease, To: Delivering, Act: true, Guard: "a person's decision, with why", Holds: func(d *Delivery, f Facts) error { return unless(f.Why != "" && f.By != "", "a release is a person's word, with why") }}, {From: []State{Delivering}, Event: EvSuperseded, To: Superseded, Guard: "its walk was superseded and names no walk that took it over: a walk that took it over answers its merge, and it follows that walk (novox/hq ADR 0276)", Holds: func(d *Delivery, f Facts) error { if d.Walk != nil && d.Walk.State == walkSuperseded && d.Walk.TakenOverBy != "" { return fmt.Errorf("its walk was taken over by %s, which answers its merge: it follows that walk", d.Walk.TakenOverBy) } return unless(d.Walk != nil && d.Walk.State == walkSuperseded, "its walk is not superseded") }}, {From: []State{Delivering}, Event: EvDone, To: Delivered, Guard: "its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says", Holds: func(d *Delivery, f Facts) error { return unless(d.Walk != nil && d.Walk.State == walkDone && !d.carriedLater(), "its walk is not done, or carried a later commit for it") }}, {From: []State{Delivering}, Event: EvCovered, To: Covered, Guard: "its walk is done, carrying a later merge of its branch that contains its merge: answered by its successor in " + "one walk (novox/hq ADR 0276)", Holds: func(d *Delivery, f Facts) error { return unless(d.Walk != nil && d.Walk.State == walkDone && d.carriedLater(), "its walk is not done, or carried its own commit") }, Says: func(d *Delivery) string { return "covered: a later merge that contains it was delivered: " + d.Carrier }}, {From: []State{Delivering}, Event: EvStop, To: Stopped, Guard: "its walk was stopped through this owner, and walks its merge again nowhere: left out, or the walk ended " + "whole (novox/hq ADR 0299)", Holds: func(d *Delivery, f Facts) error { if d.walkedAgain() { return fmt.Errorf("its walk was stopped without it, and %s walks its merge again", d.Walk.WalkedAgainBy) } return unless(d.walkStopped(), "its walk was not stopped") }, Says: func(d *Delivery) string { return d.stoppedWords() }}, {From: []State{Delivering}, Event: EvFailed, To: Failed, Guard: "its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine", Holds: func(d *Delivery, f Facts) error { return unless(d.Walk != nil && d.Walk.State == walkFailed && d.Walk.StoppedBy == "" && !d.carriedLater(), "its walk did not fail, or carried a later commit for it") }}, {From: []State{Delivering}, Event: EvRewalk, To: Published, Guard: "its walk failed carrying a later merge of its branch for it: a failed walk marks no merge delivered, and this " + "merge waits, published, for a walk of its own commit (novox/hq ADR 0276 decision 3); or its walk was stopped " + "for another merge, and its merge is walked again without that one (novox/hq ADR 0299)", Holds: func(d *Delivery, f Facts) error { if d.walkedAgain() { return nil } return unless(d.Walk != nil && d.Walk.State == walkFailed && d.Walk.StoppedBy == "" && d.carriedLater(), "its walk did not fail, or carried its own commit") }, Says: func(d *Delivery) string { if d.walkedAgain() { return fmt.Sprintf("its walk %s was stopped for another merge (%s): %s walks its merge again without that one", d.Walk.ID, d.Walk.StoppedWhy, d.Walk.WalkedAgainBy) } return fmt.Sprintf("its walk %s, which carried %s for it, failed: it waits for a walk of its own commit", d.Walk.ID, shortOf(d.AnsweredBy, 12)) }}, {From: notFinal, Event: EvStop, To: Stopped, Act: true, Guard: "a person, with why, or its group stopped by a member before it", Holds: func(d *Delivery, f Facts) error { return unless(f.Why != "" && f.By != "", "a stop says who and why") }}, } // walkCanGoOn is whether a delivery published again would be delivered by what it has: its own walk still // open, or nothing for a walk to move. func walkCanGoOn(d *Delivery) bool { if d.Walk == nil { return d.MovesNothing() } return d.Walk.State == walkBuilding || d.Walk.State == walkRolling } // historyLate is how long after its merge a merge heard only from the forge's word is history: the forge // announces a merge within seconds, so one heard this late was replayed from the bus's history. const historyLate = 15 * time.Minute // isHistory is whether a delivery is history, answering why not: made from the forge's word at its merge, on // the trunk, its merge heard historyLate or more after it happened, and no walk of it open or waiting — a // walk the controller keeps for it is a delivery, never history. func isHistory(d *Delivery) error { switch { case d.MergedAs == "": return errors.New("it is not on the trunk") case !d.FromForgesWord(): return errors.New("this owner heard its head: it is not history made from the forge's word") case d.MergedAt.IsZero() || d.Created.Sub(d.MergedAt) < historyLate: return fmt.Errorf("its merge was heard %s after it happened, within %s: a live merge, not history", d.Created.Sub(d.MergedAt).Round(time.Second), historyLate) case d.Walk != nil && (d.Walk.Waits || d.Walk.State == walkBuilding || d.Walk.State == walkRolling): return fmt.Errorf("its walk %s is open: the controller delivers it", d.Walk.ID) } return nil } // rowFor is the row a state and event name; nil when the table holds none. func rowFor(from State, ev Event, act bool) *Row { for i := range Table { r := &Table[i] if r.Event != ev || r.Act != act { continue } for _, s := range r.From { if s == from { return r } } } return nil } // ErrRefused is a transition the table does not hold, or whose guard does not. type ErrRefused struct { ID string From State Event Event Why string } func (e ErrRefused) Error() string { from := string(e.From) if from == "" { from = "nothing" } return fmt.Sprintf("%s: %s from %s is refused — %s", e.ID, e.Event, from, e.Why) } // Apply takes one transition, or refuses it. The delivery keeps the transition, bounded. func Apply(d *Delivery, ev Event, act bool, f Facts) (Transition, error) { r := rowFor(d.State, ev, act) if r == nil { kind := "observed" if act { kind = "asked" } return Transition{}, ErrRefused{ID: d.ID, From: d.State, Event: ev, Why: "the table holds no such transition (" + kind + ")"} } if err := r.Holds(d, f); err != nil { return Transition{}, ErrRefused{ID: d.ID, From: d.State, Event: ev, Why: "its guard does not hold: " + err.Error()} } why := f.Why if why == "" && r.Says != nil { why = r.Says(d) } if why == "" { why = r.Guard } t := Transition{At: f.Now, From: d.State, To: r.To, Event: ev, Why: why, By: f.By} d.State, d.Since = r.To, f.Now d.Transitions = append(d.Transitions, t) if len(d.Transitions) > keptTransitions { d.Transitions = d.Transitions[len(d.Transitions)-keptTransitions:] } return t, nil } // keptTransitions is how many transitions a delivery keeps; its note on the commit keeps every one. const keptTransitions = 100 // settle takes every observed transition whose guard holds, in the table's order, until none does — so a // delivery always stands where its facts put it, whatever order they arrived in. func settle(d *Delivery, f Facts) []Transition { var taken []Transition for range len(Table) + 1 { moved := false for i := range Table { r := &Table[i] if r.Act || !contains(r.From, d.State) { continue } if t, err := Apply(d, r.Event, false, f); err == nil { taken = append(taken, t) moved = true break } } if !moved { break } } return taken } func contains(states []State, s State) bool { for _, x := range states { if x == s { return true } } return false } // Bounds are how long a delivery may be in a state before `stalled` lists it, and what healer H2 may do // then — always a transition the table holds, never one of its own (novox/hq ADR 0239 decision 9). type Bound struct { For time.Duration // H2 is the transitions H2 may take after the bound, by event; empty when the state is the operator's. H2 []Event Says string } // Bounds are every state's. var Bounds = map[State]Bound{ Proposed: {For: time.Hour, H2: []Event{EvReask}, Says: "its check was asked and not answered: H2 asks it once " + "more, then the check's own watchdog (S6) and the operator speak for it"}, Checked: {For: time.Minute, Says: "a verdict is decided at once"}, Published: {For: 30 * time.Minute, H2: []Event{EvSuperseded, EvGo}, Says: "its walk waits for its turn or its word"}, Held: {For: 24 * time.Hour, Says: "it waits for the operator"}, Delivering: {For: 2 * time.Hour, H2: []Event{EvDone, EvSuperseded, EvFailed}, Says: "its walk runs"}, } // checkGrace is how long a proposed delivery is left to the controller's own check of its announcement before // this owner asks the check itself: longer than a check takes from announcement to verdict. A head adopted while // it already waited — announced again from the bus's history — is asked once it has waited this long too. const checkGrace = 15 * time.Minute // boundSince is when a delivery's bound runs from: its state's start, except for a proposed delivery whose // check this owner asked — its bound is "asked and not answered", so it runs from the ask. One never asked // runs from its start plus the grace before it is asked: an ask that cannot be made is still listed. func boundSince(d *Delivery) time.Time { if d.State == Proposed { if d.CheckAsk != nil { return d.CheckAsk.At } return d.Since.Add(checkGrace) } return d.Since } // heldUnwalked is the bound of a delivery held because no walk answers its merge (novox/hq issue 362): none. It // waited the ten minutes a walk opens within already, and nothing but a person moves it now. var heldUnwalked = Bound{For: 0, Says: "merged, and no walk answers its merge: the operator's"} // rewalkBound is the bound of a merge whose walk carried a later commit for it and failed: a walk of its own // commit opens within the ten minutes a walk opens within (ADR 0276 decision 3), or nothing but a person moves it. var rewalkBound = Bound{For: unwalked, Says: "the walk that carried a later commit for it failed, and no walk of " + "its own commit opened: the operator's"} // boundOf is a delivery's bound: its state's, or none for a delivery held because no walk answers it. func boundOf(d *Delivery) (Bound, bool) { if d.State == Held && d.Unwalked && d.Walk == nil { return heldUnwalked, true } if d.waitsForItsOwnWalk() { return rewalkBound, true } if (d.State == Published || d.State == Delivering) && d.HeldBack != "" { // Its walk carries a stopped merge and is not let go (ADR 0299): said with why, past the bound of its state. b := Bounds[d.State] b.For, b.Says = Bounds[Published].For, d.HeldBack // a walk not let go waits for a word, whatever its state return b, true } if d.State == Published && d.TurnAwaits != "" && d.TurnAwaitsUntil != nil { // Its turn waits for a later merge to carry a member before it: bounded by that carrier's own bound, and // said with it, never the generic "waits for its turn or its word" (novox/hq issue 445). b := Bounds[Published] b.For, b.Says = max(b.For, d.TurnAwaitsUntil.Sub(boundSince(d))), d.TurnAwaits return b, true } b, bounded := Bounds[d.State] return b, bounded } // pastBound is whether a delivery has been where it is longer than its state's bound. func pastBound(d *Delivery, now time.Time) bool { b, bounded := boundOf(d) return bounded && !d.State.Final() && (b.For == 0 || now.Sub(boundSince(d)) > b.For) } // StepState is where one machine stands in a delivering walk. type StepState string // The machine steps (ADR 0239 decision 2): sent, judged, passed or failed, and put back. const ( StepSent StepState = "sent" StepJudging StepState = "judging" StepPassed StepState = "passed" StepFailed StepState = "failed" StepRolledBack StepState = "rolled-back" ) // StepTable is every move a machine's step may make. From "" is a step first heard of. var StepTable = map[StepState][]StepState{ "": {StepSent, StepJudging, StepPassed, StepFailed, StepRolledBack}, StepSent: {StepJudging, StepPassed, StepFailed}, StepJudging: {StepPassed, StepFailed}, StepFailed: {StepRolledBack}, } // StepMay is whether a step may move from one state to another: directly, or through states the walk's // record passed between two readings of it (sent, judged and failed and put back, read once). func StepMay(from, to StepState) bool { if from == to { return true } seen := map[StepState]bool{} next := []StepState{from} for len(next) > 0 { s := next[0] next = next[1:] for _, t := range StepTable[s] { if t == to { return true } if !seen[t] { seen[t] = true next = append(next, t) } } } return false } // TableText is the table as the `table` verb answers it. func TableText() map[string]any { var rows []map[string]any for _, r := range Table { from := make([]string, 0, len(r.From)) for _, s := range r.From { if s == None { from = append(from, "(none)") continue } from = append(from, string(s)) } kind := "observed" if r.Act { kind = "asked" } rows = append(rows, map[string]any{"from": strings.Join(from, ", "), "event": r.Event, "to": r.To, "guard": r.Guard, "taken": kind}) } bounds := map[string]any{} for s, b := range Bounds { var h2 []string for _, e := range b.H2 { h2 = append(h2, string(e)) } bounds[string(s)] = map[string]any{"bound": b.For.String(), "h2": h2, "says": b.Says} } steps := map[string][]StepState{} for from, to := range StepTable { name := string(from) if name == "" { name = "(first heard)" } steps[name] = to } return map[string]any{"transitions": rows, "bounds": bounds, "machine-steps": steps} }