//go:build linux package hostexec import ( "context" "errors" "testing" ) func TestPlan14ExecutionRecoverySettlesWithoutControllerToken(t *testing.T) { for _, fail := range []string{"", "cleanup", "cc9"} { t.Run(fail, func(t *testing.T) { b, _, remote, _, q, peer := brokerFixture(t) if _, err := b.Execute(t.Context(), peer, q); err != nil { t.Fatal(err) } if err := b.Close(t.Context()); err != nil { t.Fatal(err) } cfg := b.cfg cfg.Identity = "replacement" refused := errors.New("unresolved") if fail == "cleanup" { cfg.Cleanup = func(context.Context, ExecutionRecord) error { return refused } } if fail == "cc9" { cfg.Settle = func(context.Context, ExecutionRecord) error { return refused } } next, err := NewRootBroker(cfg) if err != nil { t.Fatal(err) } defer next.Close(t.Context()) err = next.Recover(t.Context()) r, readErr := next.read(q.Host) if readErr != nil { t.Fatal(readErr) } if fail != "" { if !errors.Is(err, refused) || r.Status == "settled" { t.Fatalf("unproven recovery: %+v %v", r, err) } next.cfg.Cleanup = func(context.Context, ExecutionRecord) error { return nil } next.cfg.Settle = func(context.Context, ExecutionRecord) error { return nil } if err = next.Monitor(t.Context()); err != nil { t.Fatal("retry", err) } r, err = next.read(q.Host) } if err != nil || r.Status != "settled" { t.Fatalf("recovery left an ownerless obligation: %+v %v", r, err) } if remote.calls != 1 { t.Fatal("recovery acquired a lease") } if _, err = next.Execute(t.Context(), peer, q); err == nil { t.Fatal("recovered begin resurrected") } }) } } func TestPlan14ExecutionRecoveryDoesNotReleaseNewOwner(t *testing.T) { b, _, remote, _, q, peer := brokerFixture(t) if _, err := b.Execute(t.Context(), peer, q); err != nil { t.Fatal(err) } if err := b.Close(t.Context()); err != nil { t.Fatal(err) } key, err := Key(q.Host, "box") if err != nil { t.Fatal(err) } _, etag, err := remote.Get(t.Context(), key) if err != nil { t.Fatal(err) } newer := []byte(`{"op":"another-owner","expires":"2099-01-01T00:00:00Z"}`) nextETag, err := remote.Put(t.Context(), key, newer, etag) if err != nil { t.Fatal(err) } cfg := b.cfg cfg.Identity = "replacement" next, err := NewRootBroker(cfg) if err != nil { t.Fatal(err) } defer next.Close(t.Context()) if err = next.Recover(t.Context()); err == nil { t.Fatal("replacement lease counted as our release") } body, got, err := remote.Get(t.Context(), key) if err != nil || got != nextETag || string(body) != string(newer) { t.Fatalf("recovery deleted a newer owner: %s %s %v", body, got, err) } r, err := next.read(q.Host) if err != nil || r.Status == "settled" { t.Fatalf("unresolved release settled: %+v %v", r, err) } } func TestPlan14ExecutionRecoveryJoinsRemoteWrite(t *testing.T) { b, _, remote, clock, q, peer := brokerFixture(t) out, err := b.Execute(t.Context(), peer, q) if err != nil { t.Fatal(err) } entered, resume := make(chan struct{}), make(chan struct{}) remote.onPut = func(int) { close(entered); <-resume } q.Operation, q.ID, q.Token = "executionRenew", "renew-in-flight", out.Token done := make(chan error, 1) go func() { _, err := b.Execute(t.Context(), peer, q); done <- err }() <-entered clock.advance(TTL) _ = b.Monitor(t.Context()) err = b.Monitor(t.Context()) r, readErr := b.read(q.Host) close(resume) writeErr := <-done if err != nil || readErr != nil || r.Status != "lost" || writeErr == nil { t.Fatalf("settled before remote write joined: %+v monitor=%v read=%v write=%v", r, err, readErr, writeErr) } if err = b.Monitor(t.Context()); err != nil { t.Fatal(err) } r, err = b.read(q.Host) if err != nil || r.Status != "settled" { t.Fatalf("joined write cannot settle: %+v %v", r, err) } }