diff --git a/node-tools/internal/bus/bus.go b/node-tools/internal/bus/bus.go index 425d96d..ba6cd8b 100644 --- a/node-tools/internal/bus/bus.go +++ b/node-tools/internal/bus/bus.go @@ -28,7 +28,9 @@ import ( "github.com/novox/mesh-tools/node-tools/internal/wire" ) -// RequestTimeout is how long a call waits for its answer — what modules already expect. +// RequestTimeout is how long a call waits for its answer — what modules already expect. The +// controller answers its verbs well inside it (its AnswerWithin, ten seconds), with a call id while a +// verb still runs, so a caller is never left guessing past this (novox/hq issue 265). const RequestTimeout = 30 * time.Second const assignmentsStream = "ASSIGNMENTS" diff --git a/node-tools/internal/console/client.go b/node-tools/internal/console/client.go index c350495..2cc3e08 100644 --- a/node-tools/internal/console/client.go +++ b/node-tools/internal/console/client.go @@ -117,6 +117,9 @@ var ( timedOut = regexp.MustCompile(`(?i)timeout`) ) +// controllerSeat is the seat whose holder keeps what came of every call (novox/hq issue 265). +const controllerSeat = "mesh-controller" + // whyItFailed says why a call failed, so the remedy is in the words. func whyItFailed(key string, err error) string { if err == nil { @@ -135,8 +138,18 @@ func whyItFailed(key string, err error) string { return "this account may not call " + key + ". What it may call was fixed when it was issued — a " + "person's by `operator issue`, the console's by its manifest." case timedOut.MatchString(msg): - return key + " did not answer in time. Something is serving it, so this is the tool being slow " + - "rather than absent." + // **A timeout is not a failure** (novox/hq issue 265). Something took the call, so it may + // still be running and may already have done what was asked: a push said this while it pushed. + said := key + " did not answer within " + bus.RequestTimeout.String() + ". That is not a failure: " + + "something is serving it, so it may still be running and may already have done what was " + + "asked — check before repeating a call that changes the mesh." + if strings.HasPrefix(strings.TrimPrefix(key, "seat:"), controllerSeat+".") { + // The controller answers every call within seconds, with an id while it runs, so silence + // from it is an answer lost on the way — which it keeps. + said += " The controller answers every call within seconds, so its answer was lost on the " + + "way; " + controllerSeat + ".calls lists its recent calls and what came of each." + } + return said } return key + " failed: " + msg } diff --git a/node-tools/internal/console/refusal_test.go b/node-tools/internal/console/refusal_test.go index 4078c51..6e6a87d 100644 --- a/node-tools/internal/console/refusal_test.go +++ b/node-tools/internal/console/refusal_test.go @@ -20,3 +20,18 @@ func TestARefusalIsSaidOnlyForTheBussOwn(t *testing.T) { } } } + +// **A timeout says it is not a failure** (novox/hq issue 265): a push that outlasted the wait had +// pushed, and the caller read "did not answer in time". And from the controller, where its answer is. +func TestATimeoutIsNotAFailure(t *testing.T) { + got := whyItFailed("seat:mesh-controller.push", errors.New("timeout")) + for _, want := range []string{"not a failure", "may already have done", "mesh-controller.calls"} { + if !strings.Contains(got, want) { + t.Errorf("a controller timeout does not say %q: %s", want, got) + } + } + if other := whyItFailed("novox/postgres.postgres_list_databases", errors.New("timeout")); !strings.Contains(other, "not a failure") || + strings.Contains(other, "calls") { + t.Errorf("a module's timeout: %s", other) + } +}