//go:build linux package host import ( "context" "errors" "os" "os/exec" "path/filepath" "testing" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/provision" "golang.org/x/sys/unix" ) type plan11LaunchClient struct{ fixture *launchFixture } func (c plan11LaunchClient) Capability(context.Context) (string, error) { return hostbox.RoleLaunchProtocol, nil } func (c plan11LaunchClient) Launch(ctx context.Context, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchGrant, error) { r, e := c.fixture.root.Serve(ctx, c.fixture.peer, q) if e != nil { return hostbox.RoleLaunchGrant{}, e } return *r.Grant, nil } func (c plan11LaunchClient) Stop(ctx context.Context, q hostbox.RoleLaunchRequest) error { _, e := c.fixture.root.Serve(ctx, c.fixture.peer, q) return e } func TestPlan11FailedIsolatedStartCanRetryAndDrain(t *testing.T) { f := newLaunchFixture(t, servingOp()) dir, err := os.MkdirTemp(os.TempDir(), "p11r-") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.RemoveAll(dir) }) exe := filepath.Join(dir, "app") if err := os.WriteFile(exe, []byte("test executable"), 0700); err != nil { t.Fatal(err) } var st unix.Stat_t if err := unix.Stat(exe, &st); err != nil { t.Fatal(err) } f.kernel.path = exe f.kernel.want = hostbox.LaunchFile{Device: uint64(st.Dev), Inode: st.Ino} f.kernel.observed.Executable = f.kernel.want f.kernel.observed.Running = f.kernel.want f.units.started.MainPID = os.Getpid() f.kernel.observed.ProcessStart, err = claimConstructionProcessStart() if err != nil { t.Fatal(err) } producers, err := NewClaimProducers(f.target, "start-42", f.journal.RecordProducer, f.journal.CompleteProducer) if err != nil { t.Fatal(err) } work, err := (hostwrite.Config{}).Select(t.Context(), f.target.Host, "spawn") if err != nil { t.Fatal(err) } f.root.DataRoot, f.root.runRoot = dir, filepath.Join(dir, "run") launchDir := provision.InstanceDir(dir, f.target.Host) if err := os.MkdirAll(launchDir, 0700); err != nil { t.Fatal(err) } launch := activator.RoleLaunch{Root: plan11LaunchClient{f}, Target: f.target, Journal: f.journal, Work: work, LaunchDir: launchDir, RunDir: filepath.Join(f.root.runRoot, provision.InstanceID(f.target.Host)), LogSocket: filepath.Join(dir, "log.sock"), MemoryMax: cgroup.TierDefault.MemoryMax, ParentPath: "carlos/claim-parent"} launch.Executable = func(context.Context, *exec.Cmd) (*os.File, error) { return os.Open(exe) } launch.RegisterProducer = func(ctx context.Context, role string) (activator.RoleProducer, error) { ctx, finish, err := producers.Register(ctx, f.target, role) return activator.RoleProducer{Context: ctx, Finish: finish}, err } build := func() *exec.Cmd { cmd := exec.Command(exe, "--socket", filepath.Join(launch.RunDir, "app.sock"), "--db", filepath.Join(launchDir, "data.db")) cmd.Env = []string{"CARLOS_ADMIN_TOKEN=test"} return cmd } attempt := hostclaim.WakeRetryAttempt{ID: "first", InvocationID: "inv-first", LaunchInputs: "inputs", Session: f.target.Session, Box: f.target.Box, Enrollment: f.target.Enrollment, BootID: f.target.BootID, ControllerID: f.target.ControllerID} launch.Spec = &hostbox.LaunchSpec{Identity: f.req.Spec.Identity, Executable: exe, Digest: f.kernel.digest, Args: build().Args[1:], Env: build().Env} attempt.LaunchInputs, _ = launch.Spec.Hash() f.claims.set(func(c *hostclaim.Claim) { a := attempt; c.Holder.WakeRetry.Attempt = &a }) f.units.startErr = errors.New("first unit start failed") if _, _, _, _, err := launch.Start(t.Context(), "main", attempt, build, func() error { return nil }); err == nil { t.Fatal("failed start reported success") } f.units.startErr = nil attempt.ID = "second" attempt.InvocationID = "inv-second" f.claims.set(func(c *hostclaim.Claim) { a := attempt c.Holder.WakeRetry.Attempt = &a c.Holder.WakeRetry.AttemptsReserved = 2 }) _, _, settle, producer, err := launch.Start(t.Context(), "main", attempt, build, func() error { return nil }) if err != nil { t.Fatalf("retry after settled failure: %v", err) } if err := settle(t.Context()); err != nil { t.Fatal(err) } producer.Finish(nil) a := &claimWakeAttempt{owner: &ClaimWakeAssembly{Producers: func(context.Context, hostclaim.Observed) (*ClaimProducers, error) { return producers, nil }}, observed: f.claims.obs, inputs: ClaimLaunchInputs{DBPath: filepath.Join(dir, "data.db")}, prelaunch: &launch, writersJournal: f.journal, writersCgroup: &writersCgroupFake{}} if witness, err := a.StopWriters(t.Context()); err != nil || !witness.Valid() { t.Fatalf("writers drain after retry: %+v %v", witness, err) } before, _ := f.units.counts() refused := *f.req.Reservation refused.ID, refused.InvocationID = "after-stop:main", "after-stop" refused.Executable = f.kernel.want q := f.req q.Reservation, q.Executable = &refused, exe if _, err := f.root.Serve(t.Context(), f.peer, q); err == nil { t.Fatal("root launched after writers drain") } if after, _ := f.units.counts(); after != before { t.Fatal("closed writers reached unit start") } } func TestPlan11LateRootStopDoesNotStopAnotherInvocation(t *testing.T) { f := newLaunchFixture(t, servingOp()) if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { t.Fatal(err) } // Gone reports that this reservation's invocation has already ended. if _, err := f.root.Serve(t.Context(), f.peer, hostbox.RoleLaunchRequest{V: 1, Op: hostbox.RoleLaunchStop, Reservation: f.req.Reservation}); err != nil { t.Fatal(err) } if _, stops := f.units.counts(); stops != 0 { t.Fatal("late stop touched the host's replacement invocation") } } func TestPlan11NewLaunchSettlesOldUnstartedReservation(t *testing.T) { f := newLaunchFixture(t, servingOp()) old := *f.req.Reservation if err := f.journal.IntendUnit(t.Context(), old); err != nil { t.Fatal(err) } // A different controller replaces one that crashed after recording intent. next := old next.ID, next.InvocationID = "next:main", "next-inv" next.Target.ControllerID = "controller-2" nextPeer := f.peer nextPeer.ControllerID = next.Target.ControllerID parent, err := claimContainmentParent("/sys/fs/cgroup/system.slice/carlos-edge.service", next.Target) if err != nil { t.Fatal(err) } if err := f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{ControllerPID: nextPeer.PID, ProcessStart: nextPeer.ProcessStart, Target: next.Target, Groups: []hostbox.LaunchGroup{{RelativePath: parent, Device: 1, Inode: 9}}, NoState: true}); err != nil { t.Fatal(err) } q := f.req q.Reservation = &next f.claims.set(func(c *hostclaim.Claim) { a := *c.Holder.WakeRetry.Attempt a.ID = "next" a.InvocationID = next.InvocationID a.ControllerID = next.Target.ControllerID c.Holder.WakeRetry.Attempt = &a c.Holder.WakeRetry.AttemptsReserved = 2 }) if _, err := f.root.Serve(t.Context(), nextPeer, q); err != nil { t.Fatal(err) } previous, err := f.journal.UnitReservation(t.Context(), old.Target, old.ID) if err != nil || previous.State != hostbox.UnitSettled { t.Fatalf("old intent survived proven-empty start: %+v %v", previous, err) } f.units.goneErr = instancectl.ErrClaimUnitBusy if _, err := f.root.Serve(t.Context(), f.peer, hostbox.RoleLaunchRequest{V: 1, Op: hostbox.RoleLaunchStop, Reservation: &old}); err != nil { t.Fatal(err) } if _, stops := f.units.counts(); stops != 0 { t.Fatal("old intended reservation stopped replacement") } }