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  }