Run an apply in a context the bus's loss cannot end, and say why a killed command was killed (issue 1945)

This commit is contained in:
2026-10-25 12:09:28 +01:00
parent f06058390c
commit b99d0b45bf
4 changed files with 61 additions and 7 deletions
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"context"
"testing"
)
// An apply runs in a context the bus's loss cannot end: the link's context ends when the bus goes, and the bus may go
// because of the apply itself (it replaces the bus's own container), which then must finish (issue 1945, live on
// 2026-10-25 at 11:23: novox's node-engine removed the bus container, its link's context ended, and the docker run that
// would have made the new one was never made).
func TestAnApplyOutlivesTheLinksContext(t *testing.T) {
type key struct{}
link, cancel := context.WithCancel(context.WithValue(context.Background(), key{}, "v"))
ctx := applyContext(link)
cancel()
if ctx.Err() != nil {
t.Fatalf("the apply's context ended with the link's: %v", ctx.Err())
}
if ctx.Value(key{}) != "v" {
t.Fatal("the apply's context lost the link's values")
}
}
+8 -2
View File
@@ -298,13 +298,13 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, sess
Backup: backupNode(session),
Say: say,
Apply: func(ctx context.Context, c delivery.Composed, others []delivery.Composed) agent.Result {
res := applyComposition(session.WithLink(ctx), opts, c, others, sched, say)
res := applyComposition(applyContext(session.WithLink(ctx)), opts, c, others, sched, say)
afterApply()
sayFacts(ctx, ag)
return res
},
Remove: func(ctx context.Context, module string, others []delivery.Composed) agent.Result {
res := removeComposition(session.WithLink(ctx), opts, module, others, sched, say)
res := removeComposition(applyContext(session.WithLink(ctx)), opts, module, others, sched, say)
afterApply()
sayFacts(ctx, ag)
return res
@@ -567,3 +567,9 @@ func declaresNothingToBackUp(said json.RawMessage, module string) (nothing bool,
}
return true, true
}
// applyContext is the context an apply or a removal runs in: the link's values, never its end. The link's context ends
// when the bus goes, and the bus may go because of the apply itself (it replaces the bus's own container), which must
// then finish: a container removed and never made again took the mesh down (issue 1945, live on 2026-10-25 at 11:23).
// An apply is bounded by its own steps' bounds, not by the link.
func applyContext(ctx context.Context) context.Context { return context.WithoutCancel(ctx) }
+11 -5
View File
@@ -2464,12 +2464,18 @@ func ExecRunner(ctx context.Context, name string, args ...string) (string, error
if err != nil {
var exit *exec.ExitError
if errors.As(err, &exit) {
// The words as they always were, and the exit underneath them, so a caller asks the
// code (system.ExitCode) rather than matching text that this line is free to reword.
return string(out), &exited{
words: fmt.Sprintf("%s exited %d: %s", name, exit.ExitCode(), strings.TrimSpace(string(exit.Stderr))),
exit: exit,
words := fmt.Sprintf("%s exited %d: %s", name, exit.ExitCode(), strings.TrimSpace(string(exit.Stderr)))
if exit.ExitCode() == -1 {
// Killed, not exited: say by what, and the context's end when that is what killed it (issue 1945:
// "docker exited -1:" said nothing while a bus container was half replaced).
words = fmt.Sprintf("%s was %s", name, exit.String())
if ctx.Err() != nil {
words += " because its context ended: " + ctx.Err().Error()
}
}
// The words, and the exit underneath them, so a caller asks the code (system.ExitCode) rather than
// matching text that this line is free to reword.
return string(out), &exited{words: words, exit: exit}
}
return string(out), fmt.Errorf("%s: %w", name, err)
}
+19
View File
@@ -0,0 +1,19 @@
package apply
import (
"context"
"strings"
"testing"
"time"
)
// A command killed because its context ended says so, and why: "docker exited -1:" said nothing while novox's bus
// container was half replaced (issue 1945, live on 2026-10-25 at 11:23).
func TestACommandKilledByItsContextSaysWhy(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
go func() { time.Sleep(50 * time.Millisecond); cancel() }()
_, err := ExecRunner(ctx, "sleep", "5")
if err == nil || !strings.Contains(err.Error(), "killed") || !strings.Contains(err.Error(), "context canceled") {
t.Fatalf("a command its context killed said: %v", err)
}
}