github.com/grafana/pyroscope@v1.18.0/pkg/util/delayhandler/http_test.go (about) 1 package delayhandler 2 3 import ( 4 "context" 5 "net/http" 6 "net/http/httptest" 7 "net/url" 8 "strconv" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/grafana/dskit/middleware" 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/require" 16 "golang.org/x/net/http2" 17 "golang.org/x/net/http2/h2c" 18 "google.golang.org/grpc" 19 "google.golang.org/grpc/credentials/insecure" 20 "google.golang.org/grpc/health/grpc_health_v1" 21 22 "github.com/grafana/pyroscope/pkg/tenant" 23 ) 24 25 // Mock implementation of the Limits interface 26 type mockLimits struct { 27 delays map[string]time.Duration 28 } 29 30 func (m *mockLimits) IngestionArtificialDelay(tenantID string) time.Duration { 31 if delay, ok := m.delays[tenantID]; ok { 32 return delay 33 } 34 return 0 35 } 36 37 func newMockLimits() *mockLimits { 38 return &mockLimits{ 39 delays: make(map[string]time.Duration), 40 } 41 } 42 43 func (m *mockLimits) setDelay(tenantID string, delay time.Duration) { 44 m.delays[tenantID] = delay 45 } 46 47 // Test handler that records what happened 48 type testHandler struct { 49 statusCode int 50 body string 51 called bool 52 cancelDelay bool 53 } 54 55 func (h *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 56 h.called = true 57 if h.cancelDelay { 58 CancelDelay(r.Context()) 59 } 60 if h.statusCode != 0 { 61 w.WriteHeader(h.statusCode) 62 } 63 if h.body != "" { 64 _, _ = w.Write([]byte(h.body)) 65 } 66 } 67 68 func timeNowMock(t *testing.T, values []time.Time) func() { 69 old := timeNow 70 t.Cleanup(func() { 71 timeNow = old 72 }) 73 timeNow = func() time.Time { 74 if len(values) == 0 { 75 t.Fatalf("timeNowMock: no more values") 76 } 77 now := values[0] 78 values = values[1:] 79 return now 80 } 81 return func() { 82 83 timeNow = old 84 } 85 } 86 87 type timeAfterRecorder struct { 88 start time.Time 89 values []time.Duration 90 } 91 92 func timeAfterMock() (*timeAfterRecorder, func()) { 93 m := &timeAfterRecorder{ 94 start: time.Now(), 95 } 96 old := timeAfter 97 timeAfter = func(d time.Duration) <-chan time.Time { 98 m.values = append(m.values, d) 99 100 ch := make(chan time.Time) 101 go func() { 102 ch <- m.start.Add(d) 103 }() 104 105 return ch 106 } 107 return m, func() { 108 timeAfter = old 109 } 110 } 111 112 func fracDuration(d time.Duration, frac float64) time.Duration { 113 return time.Duration(int64(float64(d.Milliseconds())*frac)) * time.Millisecond 114 } 115 116 func TestNewHTTP(t *testing.T) { 117 now := time.Unix(1718211600, 0) 118 tenantID := "my-tenant" 119 120 tests := []struct { 121 name string 122 configDelay time.Duration 123 handlerStatusCode int 124 handlerBody string 125 handlerDelay time.Duration // delay in handler 126 middlewareDelay time.Duration // delay in other middlewares 127 cancelDelay bool 128 expectDelay bool 129 expectDelayHeader bool 130 }{ 131 { 132 name: "enabled/successful request", 133 configDelay: 100 * time.Millisecond, 134 handlerBody: "success", 135 expectDelay: true, 136 expectDelayHeader: true, 137 }, 138 { 139 name: "disabled/successful request", 140 configDelay: 0, 141 handlerBody: "success", 142 }, 143 { 144 name: "enabled/successful request/written headers", 145 configDelay: 100 * time.Millisecond, 146 handlerStatusCode: http.StatusOK, 147 handlerBody: "success", 148 expectDelay: true, 149 expectDelayHeader: true, 150 }, 151 { 152 name: "enabled/failed request/written headers", 153 configDelay: 100 * time.Millisecond, 154 handlerStatusCode: http.StatusInternalServerError, 155 handlerBody: "error", 156 }, 157 { 158 name: "disabled/failed request/written headers", 159 handlerStatusCode: http.StatusInternalServerError, 160 handlerBody: "error", 161 }, 162 { 163 name: "enabled/successful slow request", 164 configDelay: 100 * time.Millisecond, 165 handlerBody: "slow handler success", 166 handlerDelay: 200 * time.Millisecond, 167 }, 168 { 169 name: "enabled/successful slow request/written headers", 170 configDelay: 100 * time.Millisecond, 171 handlerStatusCode: http.StatusOK, 172 handlerBody: "slow handler success", 173 handlerDelay: 200 * time.Millisecond, 174 }, 175 { 176 name: "enabled/successful request/written headers/slow middleware", 177 configDelay: 100 * time.Millisecond, 178 handlerStatusCode: http.StatusOK, 179 handlerBody: "slow middlewares success", 180 middlewareDelay: 200 * time.Millisecond, 181 expectDelayHeader: true, 182 }, 183 { 184 name: "enabled/successful request/slow middleware", 185 configDelay: 100 * time.Millisecond, 186 handlerBody: "slow middlewares success", 187 middlewareDelay: 200 * time.Millisecond, 188 }, 189 { 190 name: "enabled/cancel delay", 191 configDelay: 100 * time.Millisecond, 192 handlerBody: "success", 193 cancelDelay: true, 194 }, 195 { 196 name: "disabled/cancel delay no effect", 197 configDelay: 0, 198 handlerBody: "success", 199 cancelDelay: true, 200 }, 201 } 202 203 for _, tt := range tests { 204 t.Run(tt.name, func(t *testing.T) { 205 handlerDelay := tt.handlerDelay 206 if handlerDelay == 0 { 207 handlerDelay = 5 * time.Millisecond 208 } 209 middlewareDelay := tt.middlewareDelay 210 if middlewareDelay == 0 { 211 middlewareDelay = 1 * time.Millisecond 212 } 213 214 // start of handler 215 nows := []time.Time{ 216 now, 217 } 218 219 // when a upstream handler writes headers, we will have an extra timeNow call 220 if tt.handlerStatusCode != 0 { 221 nows = append(nows, now.Add(handlerDelay)) 222 } 223 // final time now check including both delays 224 nows = append(nows, now.Add(handlerDelay+middlewareDelay)) 225 226 // mock timeNow and timeAfter 227 cleanUpNow := timeNowMock(t, nows) 228 defer cleanUpNow() 229 sleeps, cleanUpSleep := timeAfterMock() 230 defer cleanUpSleep() 231 sleeps.start = now 232 233 limits := newMockLimits() 234 limits.setDelay(tenantID, tt.configDelay) 235 middleware := NewHTTP(limits) 236 237 handler := &testHandler{ 238 statusCode: tt.handlerStatusCode, 239 body: tt.handlerBody, 240 cancelDelay: tt.cancelDelay, 241 } 242 243 req := httptest.NewRequest(http.MethodPost, "/test", strings.NewReader("test")) 244 req = req.WithContext(tenant.InjectTenantID(req.Context(), "my-tenant")) 245 w := httptest.NewRecorder() 246 247 middleware(handler).ServeHTTP(w, req) 248 249 // Verify handler was called 250 assert.True(t, handler.called) 251 252 // Verify response 253 expectedStatusCode := tt.handlerStatusCode 254 if expectedStatusCode == 0 { 255 expectedStatusCode = http.StatusOK 256 } 257 assert.Equal(t, expectedStatusCode, w.Code) 258 assert.Equal(t, tt.handlerBody, w.Body.String()) 259 260 // Expect header or no header depending on delay 261 if tt.expectDelayHeader { 262 serverTiming := w.Header().Get("Server-Timing") 263 require.Contains(t, serverTiming, "artificial_delay") 264 require.Contains(t, serverTiming, "dur=") 265 idx := strings.Index(serverTiming, "dur=") 266 267 durationFloat, err := strconv.ParseFloat(serverTiming[idx+4:], 64) 268 duration := time.Duration(durationFloat) * time.Millisecond 269 assert.NoError(t, err) 270 271 assert.Greater(t, duration, fracDuration(tt.configDelay, 0.8)-handlerDelay-middlewareDelay) 272 assert.Greater(t, fracDuration(tt.configDelay, 1.1), duration) 273 } else { 274 serverTiming := w.Header().Get("Server-Timing") 275 assert.Empty(t, serverTiming) 276 } 277 278 // Expect sleeps when delayed 279 if tt.expectDelay { 280 require.Len(t, sleeps.values, 1) 281 282 // check if delay is within jitter of expected 283 assert.Greater(t, sleeps.values[0], fracDuration(tt.configDelay, 0.8)-handlerDelay-middlewareDelay) 284 assert.Greater(t, fracDuration(tt.configDelay, 1.1), sleeps.values[0]) 285 } else { 286 require.Len(t, sleeps.values, 0) 287 } 288 289 }) 290 } 291 } 292 293 type healthMock struct { 294 grpc_health_v1.UnimplementedHealthServer 295 called bool 296 } 297 298 func (h *healthMock) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { 299 h.called = true 300 return &grpc_health_v1.HealthCheckResponse{Status: grpc_health_v1.HealthCheckResponse_SERVING}, nil 301 } 302 303 func TestGRPCHandler(t *testing.T) { 304 limits := newMockLimits() 305 limits.setDelay("my-tenant", 100*time.Millisecond) 306 delayMiddleware := middleware.Func(func(h http.Handler) http.Handler { 307 return NewHTTP(limits)(h) 308 }) 309 310 sleeps, cleanUpSleep := timeAfterMock() 311 defer cleanUpSleep() 312 313 addTenantMiddleware := middleware.Func(func(h http.Handler) http.Handler { 314 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 315 r = r.WithContext(tenant.InjectTenantID(r.Context(), "my-tenant")) 316 h.ServeHTTP(w, r) 317 }) 318 }) 319 320 h2cMiddleware := middleware.Func(func(h http.Handler) http.Handler { 321 return h2c.NewHandler(h, &http2.Server{}) 322 }) 323 324 grpcServer := grpc.NewServer() 325 healthM := &healthMock{} 326 grpc_health_v1.RegisterHealthServer(grpcServer, healthM) 327 328 handler := middleware.Merge( 329 h2cMiddleware, 330 addTenantMiddleware, 331 delayMiddleware, 332 ).Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 333 grpcServer.ServeHTTP(w, r) 334 })) 335 336 httpServer := httptest.NewServer(handler) 337 defer httpServer.Close() 338 339 // Set up a connection to the server. 340 u, err := url.Parse(httpServer.URL) 341 require.NoError(t, err) 342 conn, err := grpc.NewClient(u.Host, grpc.WithTransportCredentials(insecure.NewCredentials())) 343 require.NoError(t, err) 344 defer conn.Close() 345 346 hc := grpc_health_v1.NewHealthClient(conn) 347 resp, err := hc.Check(context.Background(), &grpc_health_v1.HealthCheckRequest{Service: "pyroscope"}) 348 require.NoError(t, err) 349 assert.Equal(t, grpc_health_v1.HealthCheckResponse_SERVING, resp.Status) 350 assert.True(t, healthM.called) 351 352 // check if the delay is applied 353 require.Len(t, sleeps.values, 1) 354 }