package check import ( "context" "encoding/json" "errors" "fmt" "strings" "github.com/nats-io/nats.go" ) // Unheard is the error of a call that reached nobody: the bus had no responder for it, or none answered within the call's // bound. The step it asked for never began, so the same step may be asked of another node (issue 1820). The program wraps // the errors of its calls in it where the bus gives a timeout; a no-responders error is known as it is. type Unheard struct{ Err error } func (u *Unheard) Error() string { return u.Err.Error() } func (u *Unheard) Unwrap() error { return u.Err } // isUnheard says whether err is a call that reached nobody. A step that began and failed, or ended failed, is not. func isUnheard(err error) bool { var u *Unheard return errors.As(err, &u) || errors.Is(err, nats.ErrNoResponders) } // deafNode is a node that did not answer a build module's call during this judging. type deafNode struct{ node, why string } // markDeaf takes a node out of the choice for a build module for the rest of the judging. func (st *state) markDeaf(bm, node string, err error) { if st.deaf == nil { st.deaf = map[string][]deafNode{} } st.deaf[bm] = append(st.deaf[bm], deafNode{node, err.Error()}) } func (st *state) isDeaf(bm, node string) bool { for _, d := range st.deaf[bm] { if d.node == node { return true } } return false } // triedWords names every node that did not answer a build module, with what the bus said. func (st *state) triedWords(bm string) string { var parts []string for _, d := range st.deaf[bm] { parts = append(parts, fmt.Sprintf("%s (%s)", d.node, d.why)) } return strings.Join(parts, "; ") } // moveStep is called when the target's node did not answer: the node is left out of the choice, and another build node of the // same build module takes the target's place. It answers an error naming every node tried when none is left (the words hold // NodesWords, so the verdict is judged again when the nodes change). func (st *state) moveStep(t *target, err error) error { old := t.node st.markDeaf(t.module, old, err) node, _, cerr := st.choose(t.module) if cerr != nil { return fmt.Errorf("%s; tried: %s", cerr, st.triedWords(t.module)) } st.j.logf("%s#%d: %s did not answer (%v), the step goes to %s", t.pull.Repository, t.pull.Number, old, err, node) t.node = node other := false for _, o := range st.targets { if o != t && o.node == old { other = true } } if !other { delete(st.nodes, old) } st.nodes[node] = true return nil } // refetch makes the target's workspace again on its new node: the workspace of a step that was lost is on the node that did // not answer. func (st *state) refetch(ctx context.Context, t *target) error { raw, err := st.step(ctx, t, "fetch", fmt.Sprintf("fetch %s#%d on %s", t.pull.Repository, t.pull.Number, t.node), t.fetchArgs) if err != nil { return err } var f Fetched if err := json.Unmarshal(raw, &f); err != nil || f.Workspace == "" { return fmt.Errorf("the fetch of %s#%d on %s answered no workspace: %s", t.pull.Repository, t.pull.Number, t.node, raw) } t.fetched = f return nil } // stepOnWorkspace sets the workspace a step reads to the target's current one. func stepOnWorkspace(t *target, args map[string]any) { if _, ok := args["workspace"]; ok { args["workspace"] = t.fetched.Workspace } }