package activator import ( "context" "errors" "net/http" "net/http/httptest" "testing" "time" "github.com/carlosframework/platform/internal/registry" ) func TestStartupWaitUsesWakeTimeoutResponse(t *testing.T) { old := wakeWaitBound wakeWaitBound = 20 * time.Millisecond t.Cleanup(func() { wakeWaitBound = old }) a := New(Config{}, &fakeBackend{}, nil) a.Startup = make(chan struct{}) defer a.lifeStop() rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true} w := httptest.NewRecorder() a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://shop.example.com/", nil), rt, http.NotFoundHandler()) if w.Code != http.StatusServiceUnavailable || w.Header().Get("Retry-After") != "5" || w.Body.String() != "instance is waking; retry shortly\n" { t.Fatalf("startup timeout: %d, %v, %q", w.Code, w.Header(), w.Body.String()) } if a.Owns(rt.Host) { t.Fatal("timed-out request created a wake") } } func TestStartupBlocksWorkerAndSidecarAdmission(t *testing.T) { old := wakeWaitBound wakeWaitBound = 20 * time.Millisecond t.Cleanup(func() { wakeWaitBound = old }) a := New(Config{}, &fakeBackend{}, nil) a.Startup = make(chan struct{}) defer a.lifeStop() rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true} if err := a.WakeWorker(rt); !errors.Is(err, errWakeTimeout) { t.Fatalf("worker: %v", err) } if err := a.BeginSidecarRequest(t.Context(), rt); !errors.Is(err, errWakeTimeout) { t.Fatalf("sidecar: %v", err) } ctx, cancel := context.WithCancel(t.Context()) cancel() if _, err := a.begin(ctx, rt, true, nil); !errors.Is(err, context.Canceled) { t.Fatalf("cancelled request: %v", err) } if a.Owns(rt.Host) { t.Fatal("startup admission created a wake") } } func TestStartupShutdownAnswersServiceUnavailable(t *testing.T) { for _, ready := range []bool{false, true} { t.Run(map[bool]string{false: "held", true: "released"}[ready], func(t *testing.T) { a := New(Config{}, &fakeBackend{}, nil) started := make(chan struct{}) a.Startup = started if ready { close(started) } a.Shutdown(t.Context()) rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true} w := httptest.NewRecorder() a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://shop.example.com/", nil), rt, http.NotFoundHandler()) if w.Code != http.StatusServiceUnavailable { t.Fatalf("service shutdown answered %d: %q", w.Code, w.Body.String()) } if a.Owns(rt.Host) { t.Fatal("shutdown request created a wake") } }) } } type startupRoutes map[string]registry.Route func (s startupRoutes) Get(host string) (*registry.Route, error) { rt, ok := s[host] if !ok { return nil, nil } return &rt, nil } // A request held through startup carries the proxy the edge chose before // recovery ran. When recovery moved the host's socket, it is answered as a // wake in progress, so the retry is routed afresh, never sent to the old // socket. func TestStartupHeldRequestRefusesStaleUpstream(t *testing.T) { t.Parallel() for _, moved := range []bool{false, true} { a := New(Config{}, &fakeBackend{}, nil) ready := make(chan struct{}) a.Startup = ready rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true, Socket: "/run/carlos/shop.example.com.sock"} fresh := rt if moved { fresh.Socket = "/run/carlos/instances/shop.example.com/app.sock" } a.Routes = startupRoutes{rt.Host: fresh} proxied := false proxy := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proxied = true }) w := httptest.NewRecorder() done := make(chan struct{}) go func() { defer close(done) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://shop.example.com/", nil).WithContext(ctx), rt, proxy) }() time.Sleep(20 * time.Millisecond) close(ready) <-done a.lifeStop() if moved { if proxied || w.Code != http.StatusServiceUnavailable || w.Header().Get("Retry-After") != "5" { t.Fatalf("moved socket: proxied=%v code=%d", proxied, w.Code) } } else if w.Code == http.StatusServiceUnavailable && w.Body.String() == "instance is waking; retry shortly\n" { t.Fatalf("unchanged socket was refused as stale") } } } // The race the waiting check misses: the edge chose its proxy, recovery // moved the socket and Startup closed before serve looked. A request near // startup is still checked; one long after it is not. func TestStartupClosedNearStartupRefusesStaleUpstream(t *testing.T) { for _, late := range []bool{false, true} { a := New(Config{}, &fakeBackend{}, nil) ready := make(chan struct{}) close(ready) a.Startup = ready if late { a.startupSeenAt.Store(time.Now().Add(-2 * startupFreshWindow).UnixNano()) } rt := registry.Route{Host: "shop.example.com", Kind: "instance", Hibernate: true, Socket: "/run/carlos/shop.example.com.sock"} fresh := rt fresh.Socket = "/run/carlos/instances/shop.example.com/app.sock" a.Routes = startupRoutes{rt.Host: fresh} proxied := false proxy := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proxied = true }) w := httptest.NewRecorder() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) a.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://shop.example.com/", nil).WithContext(ctx), rt, proxy) cancel() a.lifeStop() stale := !proxied && w.Code == http.StatusServiceUnavailable && w.Body.String() == "instance is waking; retry shortly\n" if !late && !stale { t.Fatalf("near startup: moved socket not refused: proxied=%v code=%d", proxied, w.Code) } if late && stale { t.Fatal("long after startup: the route was re-checked") } } }