D2: the domain SDK names the state again, Ended, Passing, PassingCause, HolderSilent, AgainAfter and Drop (ADR 0664 §1, §2, issue 1748)
Shared line for the join with milestone G: sdk/buildstep/ask.go (AgainAfter beside AskRuns). Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -77,6 +77,15 @@ const (
|
||||
AskMaxDeliver = AskRuns + 2
|
||||
)
|
||||
|
||||
// AgainAfter is how long the bus waits before it gives an ask again after its run-th run ended on a transport fault: 1
|
||||
// minute after the first, 4 after the second (ADR 0664 §2). It outlasts a per-source penalty of a forge's sshd.
|
||||
func AgainAfter(run int) time.Duration {
|
||||
if run < 1 {
|
||||
run = 1
|
||||
}
|
||||
return time.Minute << (2 * (run - 1))
|
||||
}
|
||||
|
||||
// StepAskVersion is the layout version of an ask.
|
||||
const StepAskVersion = 1
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -313,6 +314,35 @@ func TestPauseCapacityAndComposerKey(t *testing.T) {
|
||||
if ComposerKey("novox/r", testCommit, "d", "p", tc, nil) != ComposerKey("novox/r", testCommit, "d", "p", tc, PackageVersions{}) {
|
||||
t.Error("an empty ask changed the key")
|
||||
}
|
||||
// Issue 1748 (ADR 0664): a transport fault is told from a wrong source by its words; a step given again is no end; the
|
||||
// bus waits 1 minute after the first run and 4 after the second.
|
||||
for words, want := range map[string]bool{
|
||||
`Get "https://proxy": dial tcp: lookup proxy.golang.org: i/o timeout`: true,
|
||||
`Head http://store.internal:5100/v2/x: dial tcp 10.10.0.1:5100: i/o timeout`: true,
|
||||
"kex_exchange_identification: read: Connection reset by peer": true,
|
||||
"Connection closed by 10.10.0.1 port 222": true,
|
||||
"npm error code EAI_AGAIN": true,
|
||||
"npm error code ETIMEDOUT": true,
|
||||
"npm error code ECONNRESET": true,
|
||||
(&HolderSilent{Seat: "go-package-registry", Holder: "gopkgs", Node: "ace"}).Error(): true,
|
||||
"nats: no responders available for request": false,
|
||||
"fatal: unknown revision or path not in the working tree": false,
|
||||
"verifying module: checksum mismatch": false,
|
||||
"./main.go:3:1: syntax error: unexpected }": false,
|
||||
} {
|
||||
if PassingCause(words) != want {
|
||||
t.Errorf("PassingCause(%q) = %v, want %v", words, !want, want)
|
||||
}
|
||||
}
|
||||
if !IsPassing(fmt.Errorf("fetch: %w", &HolderSilent{Seat: "s"})) || IsPassing(errors.New("i/o timeout")) {
|
||||
t.Error("a HolderSilent is a Passing error and a bare error is none")
|
||||
}
|
||||
if Ended(Step{State: StepRunning}) || Ended(Step{State: StepAgain}) || !Ended(Step{State: StepPassed}) || !Ended(Step{State: StepFailed}) {
|
||||
t.Error("Ended: running and again are no end")
|
||||
}
|
||||
if AgainAfter(1) != time.Minute || AgainAfter(2) != 4*time.Minute {
|
||||
t.Errorf("AgainAfter = %v, %v", AgainAfter(1), AgainAfter(2))
|
||||
}
|
||||
}
|
||||
|
||||
// The part `build-step` is one layout for the three build seats: each seat's fixtures read strictly in the neutral
|
||||
@@ -350,6 +380,9 @@ func TestTheThreeSeatsFixturesFitTheNeutralForms(t *testing.T) {
|
||||
case strings.HasPrefix(f.Of, "state:pauses/paused."):
|
||||
seen++
|
||||
strict(t, f.Given, &Pause{})
|
||||
case strings.HasPrefix(f.Of, "state:pauses/dropped."):
|
||||
seen++
|
||||
strict(t, f.Given, &Drop{})
|
||||
case strings.HasPrefix(f.Of, "state:pauses/capacity."):
|
||||
seen++
|
||||
strict(t, f.Given, &Capacity{})
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package buildstep
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Passing is a fault of the network between a build node and what it reaches (a name lookup, a timeout, a reset
|
||||
// connection, a forge's sshd refusing a burst, a store that does not answer), not of the source or the toolchain. A
|
||||
// build module wraps it, and the shared runner then writes the step again and settles its ask with a delayed nak, up to
|
||||
// AskRuns runs (ADR 0664 §1, §2). Any other error ends the step failed at once.
|
||||
type Passing struct{ Err error }
|
||||
|
||||
func (p *Passing) Error() string { return p.Err.Error() }
|
||||
func (p *Passing) Unwrap() error { return p.Err }
|
||||
|
||||
// IsPassing says err is, or wraps, a Passing.
|
||||
func IsPassing(err error) bool {
|
||||
var p *Passing
|
||||
return errors.As(err, &p)
|
||||
}
|
||||
|
||||
// HolderSilentWords opens the words of a HolderSilent, so PassingCause reads them from a step's reason too.
|
||||
const HolderSilentWords = "the holder is silent"
|
||||
|
||||
// HolderSilent is a seat verb that answered "no responders" while the assigner names a holder of the seat: its tool
|
||||
// runner is being replaced, a moment later it answers. It is a Passing fault (ADR 0664 §1); a seat nothing holds is not.
|
||||
type HolderSilent struct{ Seat, Holder, Node string }
|
||||
|
||||
func (h *HolderSilent) Error() string {
|
||||
return fmt.Sprintf("%s: seat %s, held by %s on %s, answered no responders", HolderSilentWords, h.Seat, h.Holder, h.Node)
|
||||
}
|
||||
|
||||
// Unwrap makes a HolderSilent a Passing.
|
||||
func (h *HolderSilent) Unwrap() error { return &Passing{Err: errors.New(h.Error())} }
|
||||
|
||||
// passingWords are the words of a network that did not answer, lower-cased: go's and git's (a lookup, a timeout, a refused or
|
||||
// reset connection, ssh's key exchange cut by a forge's per-source penalty) and npm's error codes. "no responders" alone is not
|
||||
// one: it is the same for a seat nothing holds (HolderSilent is the case a holder is named).
|
||||
var passingWords = []string{"i/o timeout", "no such host", "temporary failure in name resolution", "connection refused",
|
||||
"connection reset", "tls handshake timeout", "lookup ", "kex_exchange_identification", "connection closed by",
|
||||
"eai_again", "etimedout", "econnreset", "econnrefused", "enotfound", strings.ToLower(HolderSilentWords)}
|
||||
|
||||
// PassingCause says words (a tool's output or a step's reason) are those of a transport fault.
|
||||
func PassingCause(words string) bool {
|
||||
words = strings.ToLower(words)
|
||||
for _, w := range passingWords {
|
||||
if strings.Contains(words, w) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Drop is the value of the key dropped.<ask id> of a build seat's pauses state: an ask its caller no longer waits for.
|
||||
type Drop struct {
|
||||
V int `json:"v,omitempty"`
|
||||
By string `json:"by,omitempty"`
|
||||
Why string `json:"why"`
|
||||
At time.Time `json:"at,omitzero"`
|
||||
}
|
||||
+15
-2
@@ -23,6 +23,9 @@ var ErrUnknownVersion = errors.New("buildstep: unknown value version")
|
||||
// The states of a step.
|
||||
const (
|
||||
StepRunning = "running"
|
||||
// StepAgain is a step that ended on a transport fault with runs left: the bus gives its ask again after AgainAt
|
||||
// (ADR 0664 §1). It is not an end (Ended), and nothing acknowledges the ask.
|
||||
StepAgain = "again"
|
||||
StepPassed = "passed"
|
||||
StepFailed = "failed"
|
||||
StepRefused = "refused"
|
||||
@@ -47,8 +50,18 @@ type Step struct {
|
||||
Phase string `json:"phase,omitempty"`
|
||||
PhaseAt time.Time `json:"phase-at,omitzero"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
// Run is which run of the ask this is, counted by the bus's delivery count (1 for the first); AgainAt when the bus
|
||||
// gives it again (the state again); Passing says the step ended on a transport fault, so a failed end after the
|
||||
// last run says so (ADR 0664 §1, §2).
|
||||
Run int `json:"run,omitempty"`
|
||||
AgainAt time.Time `json:"again-at,omitzero"`
|
||||
Passing bool `json:"passing,omitempty"`
|
||||
}
|
||||
|
||||
// Ended says a step's state is an end: passed, failed or refused. A step running, or ended on a transport fault with runs
|
||||
// left (StepAgain), is not.
|
||||
func Ended(s Step) bool { return s.State != StepRunning && s.State != StepAgain && s.State != "" }
|
||||
|
||||
var (
|
||||
nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`)
|
||||
stepIDOK = regexp.MustCompile(`^[a-z0-9]+$`)
|
||||
@@ -106,9 +119,9 @@ func (s Step) check() error {
|
||||
return fmt.Errorf("buildstep: a step needs its tool")
|
||||
}
|
||||
switch s.State {
|
||||
case StepRunning, StepPassed, StepFailed, StepRefused:
|
||||
case StepRunning, StepAgain, StepPassed, StepFailed, StepRefused:
|
||||
default:
|
||||
return fmt.Errorf("buildstep: the step state %q is not running, passed, failed or refused", s.State)
|
||||
return fmt.Errorf("buildstep: the step state %q is not running, again, passed, failed or refused", s.State)
|
||||
}
|
||||
if len(s.Result) > 0 && !json.Valid(s.Result) {
|
||||
return fmt.Errorf("buildstep: the step's result is not JSON")
|
||||
|
||||
Reference in New Issue
Block a user