//go:build linux package hostexec import ( "context" "errors" "sync/atomic" "testing" "time" ) func TestPlan14ExecutionClientHasOneRenewalOwner(t *testing.T) { b, _, remote, clock, q, peer := brokerFixture(t) var calls, cleanup atomic.Int32 c, err := OpenRootClient(t.Context(), RootClientConfig{ Binding: Binding{Host: q.Host, Incarnation: q.Incarnation, Op: q.Op, Box: "box", Enrollment: "en", BootID: peer.KernelBootID}, Clock: clock, Observe: func(context.Context) (ExecutionRequest, error) { return q, nil }, Call: func(ctx context.Context, q ExecutionRequest) (ExecutionResponse, error) { calls.Add(1) return b.Execute(ctx, peer, q) }, CloseAdmission: func() {}, Cleanup: func(context.Context) error { cleanup.Add(1); return nil }, }) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = c.Close(context.Background()) }) s, err := c.Ready(t.Context()) if err != nil || s.ControllerID == "" || s.LeaseETag == "" { t.Fatalf("ready %+v %v", s, err) } if err = c.Refresh(t.Context()); err != nil { t.Fatal(err) } if remote.calls != 1 { t.Fatalf("view renewed: %d writes", remote.calls) } clock.advance(20 * time.Second) if err = c.Refresh(t.Context()); err != nil { t.Fatal(err) } if remote.calls != 2 || calls.Load() < 3 { t.Fatalf("owners: writes=%d calls=%d", remote.calls, calls.Load()) } clock.advance(TTL) if _, err = c.Ready(t.Context()); err == nil { t.Fatal("client admitted expired root response") } if err = c.Close(t.Context()); err != nil { t.Fatal(err) } if cleanup.Load() != 1 { t.Fatalf("cleanup=%d", cleanup.Load()) } } func TestPlan14ExecutionClientLostBeginResponseSettles(t *testing.T) { b, _, remote, clock, q, peer := brokerFixture(t) var begin ExecutionRequest var lost bool c, err := OpenRootClient(t.Context(), RootClientConfig{ Binding: Binding{Host: q.Host, Incarnation: q.Incarnation, Op: q.Op, Box: "box", Enrollment: "en", BootID: peer.KernelBootID}, Clock: clock, Observe: func(context.Context) (ExecutionRequest, error) { return q, nil }, Call: func(ctx context.Context, request ExecutionRequest) (ExecutionResponse, error) { if request.Operation == "executionBegin" { if begin.ID != "" && begin != request { t.Error("begin replay changed identity") } begin = request if _, err := b.Execute(ctx, peer, request); err != nil { return ExecutionResponse{}, err } if !lost { lost = true return ExecutionResponse{}, context.DeadlineExceeded } } if request.Host != begin.Host || request.Incarnation != begin.Incarnation || request.Stamp != begin.Stamp || request.Session != begin.Session || request.Op != begin.Op { t.Error("cleanup lost the captured begin tuple") } return b.Execute(ctx, peer, request) }, CloseAdmission: func() {}, Cleanup: func(context.Context) error { return nil }, }) if !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } if err = c.Close(t.Context()); err != nil { t.Fatal("lost begin cannot settle", err) } record, err := b.read(q.Host) if err != nil || record.Status != "settled" || remote.calls != 1 { t.Fatalf("record=%+v err=%v calls=%d", record, err, remote.calls) } } func TestPlan14ExecutionClientLostTransitionResponseSettles(t *testing.T) { b, _, remote, clock, q, peer := brokerFixture(t) initial := q original := b.cfg.Authority b.cfg.Authority = func(ctx context.Context, got ExecutionRequest, peer ControllerProcess) (ExecutionAuthority, error) { got.Session = initial.Session return original(ctx, got, peer) } b.cfg.Transition = func(context.Context, ExecutionRecord, ExecutionRequest, ExecutionAuthority) error { return nil } var uncertain ExecutionRequest lost := false c, err := OpenRootClient(t.Context(), RootClientConfig{ Binding: Binding{Host: q.Host, Incarnation: q.Incarnation, Op: q.Op, Box: "box", Enrollment: "en", BootID: peer.KernelBootID}, Clock: clock, Observe: func(context.Context) (ExecutionRequest, error) { return q, nil }, Call: func(ctx context.Context, request ExecutionRequest) (ExecutionResponse, error) { out, err := b.Execute(ctx, peer, request) if err == nil && request.Operation == "executionRenew" { if uncertain.ID != "" && uncertain != request { t.Error("transition replay changed request") } uncertain = request if !lost { lost = true return ExecutionResponse{}, context.DeadlineExceeded } } return out, err }, CloseAdmission: func() {}, Cleanup: func(context.Context) error { return nil }, }) if err != nil { t.Fatal(err) } q.Session++ if err = c.Refresh(t.Context()); !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } if err = c.Close(t.Context()); err != nil { t.Fatal("lost transition cannot settle", err) } r, err := b.read(q.Host) if err != nil || r.Status != "settled" || r.Request.Session != q.Session || remote.calls != 2 { t.Fatalf("transition recovery: %+v %v writes=%d", r, err, remote.calls) } }