mesh/merge-gate pass: builds mesh-delivery → 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/delivery-group group feat/459-a-stop-re-walks-the-others delivered: every member is delivered
A merge left out for sharing a module read as containing the stopped merge; the safety net also held back merges stranded by their walk, which nobody stopped; a held walk said why only in the log; and a stop of a merge in a batch did not ask the batch.
675 lines
31 KiB
Go
675 lines
31 KiB
Go
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}
|
|
}
|