diff --git a/client/v1/healthz.go b/client/v1/healthz.go index 05bd391a..674b834e 100644 --- a/client/v1/healthz.go +++ b/client/v1/healthz.go @@ -12,6 +12,8 @@ import ( "github.com/leptonai/gpud/internal/server" ) +var ErrServerNotReady = errors.New("server not ready, timeout waiting") + func CheckHealthz(ctx context.Context, addr string, opts ...OpOption) error { op := &Op{} if err := op.applyOpts(opts); err != nil { @@ -84,5 +86,3 @@ func BlockUntilServerReady(ctx context.Context, addr string, opts ...OpOption) e } return ErrServerNotReady } - -var ErrServerNotReady = errors.New("server not ready, timeout waiting") diff --git a/client/v1/healthz_test.go b/client/v1/healthz_test.go index 05f03ea5..642ab30a 100644 --- a/client/v1/healthz_test.go +++ b/client/v1/healthz_test.go @@ -5,7 +5,6 @@ import ( "context" "net/http" "net/http/httptest" - "sync" "testing" "time" @@ -208,257 +207,3 @@ func TestCheckHealthzTimeout(t *testing.T) { t.Error("CheckHealthz() with timeout should return error") } } - -func TestBlockUntilServerReady(t *testing.T) { - tests := []struct { - name string - serverBehavior func(w http.ResponseWriter, r *http.Request) - expectedError bool - }{ - { - name: "Server ready immediately", - serverBehavior: func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - json, _ := server.DefaultHealthz.JSON() - if _, err := w.Write(json); err != nil { - t.Errorf("Error writing response: %v", err) - } - }, - expectedError: false, - }, - { - name: "Server ready after delay", - serverBehavior: func(w http.ResponseWriter, r *http.Request) { - time.Sleep(100 * time.Millisecond) - w.WriteHeader(http.StatusOK) - json, _ := server.DefaultHealthz.JSON() - if _, err := w.Write(json); err != nil { - t.Errorf("Error writing response: %v", err) - } - }, - expectedError: false, - }, - { - name: "Server never ready", - serverBehavior: func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - }, - expectedError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(tt.serverBehavior)) - defer server.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - err := BlockUntilServerReady(ctx, server.URL, WithCheckInterval(50*time.Millisecond)) - if (err != nil) != tt.expectedError { - t.Errorf("BlockUntilServerReady() error = %v, expectedError %v", err, tt.expectedError) - } - }) - } -} - -func TestBlockUntilServerReadyInvalidURL(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - err := BlockUntilServerReady(ctx, "invalid-url") - if err == nil { - t.Error("BlockUntilServerReady() with invalid URL should return error") - } -} - -func TestBlockUntilServerReadyWithCustomInterval(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - json, _ := server.DefaultHealthz.JSON() - _, err := w.Write(json) - if err != nil { - t.Errorf("Error writing response: %v", err) - } - })) - defer srv.Close() - - ctx := context.Background() - err := BlockUntilServerReady(ctx, srv.URL, WithCheckInterval(10*time.Millisecond)) - if err != nil { - t.Errorf("BlockUntilServerReady() with custom interval error = %v, want nil", err) - } -} - -func TestBlockUntilServerReadyWithInvalidResponse(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte(`{"invalid":"json"}`)) - if err != nil { - t.Errorf("Error writing response: %v", err) - } - })) - defer srv.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) - defer cancel() - - err := BlockUntilServerReady(ctx, srv.URL, WithCheckInterval(50*time.Millisecond)) - if err == nil { - t.Error("BlockUntilServerReady() with invalid response should return error") - } -} - -func TestCheckHealthzConcurrent(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/healthz" { - http.NotFound(w, r) - return - } - w.WriteHeader(http.StatusOK) - json, _ := server.DefaultHealthz.JSON() - _, err := w.Write(json) - require.NoError(t, err) - })) - defer srv.Close() - - var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - err := CheckHealthz(context.Background(), srv.URL) - assert.NoError(t, err) - }() - } - wg.Wait() -} - -func TestBlockUntilServerReadyExactRetries(t *testing.T) { - retryCount := 0 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - retryCount++ - // Only succeed after 29 retries (30th attempt) - if retryCount < 30 { - w.WriteHeader(http.StatusServiceUnavailable) - return - } - w.WriteHeader(http.StatusOK) - json, _ := server.DefaultHealthz.JSON() - _, err := w.Write(json) - require.NoError(t, err) - })) - defer srv.Close() - - ctx := context.Background() - err := BlockUntilServerReady(ctx, srv.URL, WithCheckInterval(1*time.Millisecond)) - assert.NoError(t, err) - assert.Equal(t, 30, retryCount, "Expected exactly 30 retries") -} - -func TestBlockUntilServerReadyWithSlowResponse(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(50 * time.Millisecond) - w.WriteHeader(http.StatusOK) - json, _ := server.DefaultHealthz.JSON() - _, err := w.Write(json) - require.NoError(t, err) - })) - defer srv.Close() - - // Test with a client timeout shorter than server response time - client := &http.Client{Timeout: 20 * time.Millisecond} - err := BlockUntilServerReady(context.Background(), srv.URL, - WithHTTPClient(client), - WithCheckInterval(10*time.Millisecond)) - assert.Error(t, err) - assert.Equal(t, err, ErrServerNotReady) - - // Test with a client timeout longer than server response time - client = &http.Client{Timeout: 100 * time.Millisecond} - err = BlockUntilServerReady(context.Background(), srv.URL, - WithHTTPClient(client), - WithCheckInterval(10*time.Millisecond)) - assert.NoError(t, err) -} - -func TestBlockUntilServerReadyWithContextDeadline(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - })) - defer srv.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() - - start := time.Now() - err := BlockUntilServerReady(ctx, srv.URL, WithCheckInterval(10*time.Millisecond)) - duration := time.Since(start) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "context done") - assert.True(t, duration >= 100*time.Millisecond, "Should have waited for context deadline") - assert.True(t, duration < 150*time.Millisecond, "Should not have waited much longer than deadline") -} - -func TestBlockUntilServerReadyWithNetworkErrors(t *testing.T) { - // Start and immediately close a server to get an unused address - srv := httptest.NewServer(http.HandlerFunc(nil)) - addr := srv.URL - srv.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() - - err := BlockUntilServerReady(ctx, addr, WithCheckInterval(10*time.Millisecond)) - assert.Error(t, err) - assert.Contains(t, err.Error(), "context done") -} - -func TestBlockUntilServerReadyWithIntermittentFailures(t *testing.T) { - attempts := 0 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - attempts++ - // Fail the first 2 attempts with different status codes - switch attempts { - case 1: - w.WriteHeader(http.StatusServiceUnavailable) - case 2: - w.WriteHeader(http.StatusInternalServerError) - default: - w.WriteHeader(http.StatusOK) - json, _ := server.DefaultHealthz.JSON() - _, err := w.Write(json) - require.NoError(t, err) - } - })) - defer srv.Close() - - err := BlockUntilServerReady(context.Background(), srv.URL, WithCheckInterval(10*time.Millisecond)) - assert.NoError(t, err) - assert.Equal(t, 3, attempts, "Expected exactly 3 attempts before success") -} - -func TestBlockUntilServerReadyWithConnectionErrors(t *testing.T) { - attempts := 0 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - attempts++ - if attempts <= 2 { - // Simulate connection errors for first two attempts - conn, _, err := w.(http.Hijacker).Hijack() - require.NoError(t, err) - conn.Close() - return - } - w.WriteHeader(http.StatusOK) - json, _ := server.DefaultHealthz.JSON() - _, err := w.Write(json) - require.NoError(t, err) - })) - defer srv.Close() - - err := BlockUntilServerReady(context.Background(), srv.URL, WithCheckInterval(10*time.Millisecond)) - assert.NoError(t, err) - assert.Equal(t, 3, attempts, "Expected exactly 3 attempts before success") -}