On the control node the store-bound packages of the controller's suite ran five to seven times slower than on any other holder, and every controller check that landed there ran past the suite's thirty minutes: a throwaway store flushing to a disk the mesh's own store, bus and forge keep busy. A store that lives for one check needs no crash safety. A holder recreated mid-check left the check's store and bus running, and the redelivery went to another machine, so nothing removed them: eleven pairs across four machines. A starting holder has taken nothing, so every container labelled with an ask of the seat is an earlier holder's.
141 lines
4.9 KiB
Go
141 lines
4.9 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A build killed by hand (novox/hq ADR 0219) ends every command it started: the context's end kills
|
|
// the command's whole process group, not the one process the context knows about.
|
|
func TestAKilledBuildEndsItsWholeProcessGroup(t *testing.T) {
|
|
dir := t.TempDir()
|
|
child := filepath.Join(dir, "child")
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
began := time.Now()
|
|
go func() {
|
|
// A shell that starts a grandchild and waits on it: killing the shell alone would leave the
|
|
// grandchild running, holding the output open.
|
|
_, err := Command(ctx, dir, "sh", "-c", `sleep 60 & echo $! > child; wait`)
|
|
done <- err
|
|
}()
|
|
var pid int
|
|
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
|
|
if raw, err := os.ReadFile(child); err == nil && strings.TrimSpace(string(raw)) != "" {
|
|
pid, _ = strconv.Atoi(strings.TrimSpace(string(raw)))
|
|
break
|
|
}
|
|
}
|
|
if pid == 0 {
|
|
t.Fatal("the command never started its child")
|
|
}
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if err == nil {
|
|
t.Fatal("a killed command reported success")
|
|
}
|
|
case <-time.After(KillWait + 5*time.Second):
|
|
t.Fatal("the killed command never returned")
|
|
}
|
|
if took := time.Since(began); took > KillWait {
|
|
t.Errorf("ending the command took %s: the group was not killed, the wait ran out", took)
|
|
}
|
|
for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
|
|
if err := syscall.Kill(pid, 0); errors.Is(err, syscall.ESRCH) {
|
|
return
|
|
}
|
|
}
|
|
_ = syscall.Kill(pid, syscall.SIGKILL)
|
|
t.Fatalf("the grandchild %d outlived the kill", pid)
|
|
}
|
|
|
|
// Every `docker run` a build starts carries its id as a label; nothing else is touched.
|
|
func TestEveryContainerABuildStartsCarriesItsID(t *testing.T) {
|
|
got := LabelledArgs("docker", []string{"run", "--rm", "img", "sh"}, "build-1")
|
|
if want := []string{"run", "--label", "mesh.build=build-1", "--rm", "img", "sh"}; !reflect.DeepEqual(got, want) {
|
|
t.Errorf("docker run became %v", got)
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
args []string
|
|
}{{"docker", []string{"build", "."}}, {"git", []string{"run"}}, {"docker", nil}} {
|
|
if got := LabelledArgs(c.name, c.args, "build-1"); !reflect.DeepEqual(got, c.args) {
|
|
t.Errorf("%s %v became %v", c.name, c.args, got)
|
|
}
|
|
}
|
|
var ran [][]string
|
|
run := Labelled(func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
ran = append(ran, append([]string{name}, args...))
|
|
return "", nil
|
|
}, "build-2")
|
|
_, _ = run(context.Background(), "", "docker", "run", "img")
|
|
if want := [][]string{{"docker", "run", "--label", "mesh.build=build-2", "img"}}; !reflect.DeepEqual(ran, want) {
|
|
t.Errorf("ran %v", ran)
|
|
}
|
|
}
|
|
|
|
// What a kill removes is found by the label, and only what it finds.
|
|
func TestAKillRemovesTheContainersLabelledWithTheBuild(t *testing.T) {
|
|
var ran []string
|
|
run := func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
ran = append(ran, name+" "+strings.Join(args, " "))
|
|
if args[0] == "ps" {
|
|
return "c1\nc2\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
n, err := RemoveContainersOf(context.Background(), run, "build-3")
|
|
if err != nil || n != 2 {
|
|
t.Fatalf("%d %v", n, err)
|
|
}
|
|
if want := []string{"docker ps -aq --filter label=mesh.build=build-3", "docker rm -f c1 c2"}; !reflect.DeepEqual(ran, want) {
|
|
t.Errorf("ran %v", ran)
|
|
}
|
|
}
|
|
|
|
// **Issue 306**: a holder starting removes what earlier holders on its machine left — every container
|
|
// labelled with an ask of the seat — and nothing labelled otherwise.
|
|
func TestAStartingHolderRemovesWhatEarlierHoldersLeftAndNothingElse(t *testing.T) {
|
|
var ran [][]string
|
|
run := func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
ran = append(ran, append([]string{name}, args...))
|
|
if args[0] == "ps" {
|
|
return "c1 build-1791418000320743455\nc2 build-1791418000320743455\n" +
|
|
"c3 check-here-1791418000320743455\nc4 check-test-17\nc5 build-x\nc6 build-\n\nc7 build-1791319895953076871\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
n, err := RemoveLeftBehind(context.Background(), run)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 3 {
|
|
t.Errorf("removed %d", n)
|
|
}
|
|
if len(ran) != 2 || !reflect.DeepEqual(ran[1], []string{"docker", "rm", "-f", "c1", "c2", "c7"}) {
|
|
t.Fatalf("ran %v", ran)
|
|
}
|
|
if !strings.Contains(strings.Join(ran[0], " "), "--filter label=mesh.build") {
|
|
t.Errorf("listed with %v", ran[0])
|
|
}
|
|
|
|
// Nothing left behind: nothing removed, and no rm asked.
|
|
ran = nil
|
|
quiet := func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
ran = append(ran, append([]string{name}, args...))
|
|
return "c3 check-here-1\n", nil
|
|
}
|
|
if n, err := RemoveLeftBehind(context.Background(), quiet); err != nil || n != 0 || len(ran) != 1 {
|
|
t.Errorf("with nothing of the seat's: %d %v %v", n, err, ran)
|
|
}
|
|
}
|