package activator import ( "context" "errors" "github.com/carlosframework/platform/internal/registry" "net/http" "net/http/httptest" "testing" "time" ) func TestSeedExclusionWaitsForExistingPreparation(t *testing.T) { t.Parallel() for _, kind := range []string{"request", "timer", "stop"} { t.Run(kind, func(t *testing.T) { a := &Service{entries: map[string]*actEntry{}} pending := make(chan struct{}) switch kind { case "request": a.entries["b.example"] = &actEntry{state: actWaking, ready: pending, wakeCancel: func() { t.Error("canceled existing wake") }} case "timer": a.tokenPreparations = map[string]chan struct{}{"b.example": pending} case "stop": a.entries["b.example"] = &actEntry{state: actStopping, stopped: pending} } ctx, cancel := context.WithTimeout(t.Context(), 30*time.Millisecond) defer cancel() if _, err := a.AcquireSeedExclusion(ctx, "b.example", nil); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("did not wait: %v", err) } if a.fences["b.example"] != 0 { t.Fatal("fenced the existing preparation") } a.mu.Lock() delete(a.entries, "b.example") delete(a.tokenPreparations, "b.example") close(pending) a.mu.Unlock() token, err := a.AcquireSeedExclusion(t.Context(), "b.example", nil) if err != nil { t.Fatal(err) } if err := token.Check("b.example"); err != nil { t.Fatal(err) } for _, serve := range []func(http.ResponseWriter, *http.Request, registry.Route, http.Handler) bool{a.ServeHTTP, a.ServeScheduled} { response := httptest.NewRecorder() called := false handled := serve(response, httptest.NewRequest("GET", "http://b.example/", nil), registry.Route{Host: "b.example", Kind: "instance", Hibernate: true}, http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })) if !handled || response.Code != 503 || called { t.Fatalf("request escaped exclusion: %v %d %v", handled, response.Code, called) } } if _, err := a.PrepareScheduledToken("b.example", func() (string, error) { t.Fatal("timer entered seed"); return "", nil }); !errors.Is(err, ErrFenced) { t.Fatal(err) } if _, err := a.PrepareScheduledToken("a.example", func() (string, error) { return "ok", nil }); err != nil { t.Fatal("other host blocked", err) } if _, err := a.AcquireSeedExclusion(t.Context(), "b.example", nil); !errors.Is(err, ErrFenceHeld) { t.Fatal("duplicate accepted", err) } again, err := a.AcquireSeedExclusion(t.Context(), "b.example", token) if err != nil || again != token { t.Fatal("unsettled owner could not resume", err) } if err := token.Release(); err != nil { t.Fatal(err) } if err := token.Check("b.example"); !errors.Is(err, ErrFenceHeld) { t.Fatal("stale token accepted", err) } }) } } func TestSeedExclusionRefusesForeignFenceAndToken(t *testing.T) { a := &Service{fences: map[string]uint64{"b.example": 7}} if _, err := a.AcquireSeedExclusion(t.Context(), "b.example", nil); !errors.Is(err, ErrFenceHeld) { t.Fatal(err) } b := &Service{} tok, err := b.AcquireSeedExclusion(t.Context(), "b.example", nil) if err != nil { t.Fatal(err) } if _, err := a.AcquireSeedExclusion(t.Context(), "b.example", tok); !errors.Is(err, ErrFenceHeld) { t.Fatal(err) } if err := tok.Check("a.example"); !errors.Is(err, ErrFenceHeld) { t.Fatal(err) } }