github.com/grafana/pyroscope@v1.18.0/pkg/distributor/writepath/write_path_test.go (about) 1 package writepath 2 3 import ( 4 "context" 5 "io" 6 "sync/atomic" 7 "testing" 8 9 "connectrpc.com/connect" 10 11 "github.com/go-kit/log" 12 "github.com/grafana/dskit/services" 13 "github.com/prometheus/client_golang/prometheus" 14 "github.com/stretchr/testify/mock" 15 "github.com/stretchr/testify/suite" 16 17 pushv1 "github.com/grafana/pyroscope/api/gen/proto/go/push/v1" 18 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 19 distributormodel "github.com/grafana/pyroscope/pkg/distributor/model" 20 "github.com/grafana/pyroscope/pkg/pprof" 21 "github.com/grafana/pyroscope/pkg/test/mocks/mockwritepath" 22 "github.com/grafana/pyroscope/pkg/util/delayhandler" 23 ) 24 25 type routerTestSuite struct { 26 suite.Suite 27 28 router *Router 29 logger log.Logger 30 registry *prometheus.Registry 31 ingester *mockwritepath.MockIngesterClient 32 segwriter *mockwritepath.MockIngesterClient 33 34 request *distributormodel.ProfileSeries 35 } 36 37 func (s *routerTestSuite) SetupTest() { 38 s.logger = log.NewLogfmtLogger(io.Discard) 39 s.registry = prometheus.NewRegistry() 40 s.ingester = new(mockwritepath.MockIngesterClient) 41 s.segwriter = new(mockwritepath.MockIngesterClient) 42 43 s.request = &distributormodel.ProfileSeries{ 44 Labels: []*typesv1.LabelPair{ 45 {Name: "foo", Value: "bar"}, 46 {Name: "qux", Value: "zoo"}, 47 }, 48 Profile: &pprof.Profile{}, 49 50 TenantID: "tenant-a", 51 Annotations: []*typesv1.ProfileAnnotation{ 52 {Key: "foo", Value: "bar"}, 53 }, 54 } 55 56 s.router = NewRouter( 57 s.logger, 58 s.registry, 59 s.ingester, 60 s.segwriter, 61 ) 62 } 63 64 func (s *routerTestSuite) BeforeTest(_, _ string) { 65 svc := s.router.Service() 66 s.Require().NoError(svc.StartAsync(context.Background())) 67 s.Require().NoError(svc.AwaitRunning(context.Background())) 68 s.Require().Equal(services.Running, svc.State()) 69 } 70 71 func (s *routerTestSuite) AfterTest(_, _ string) { 72 svc := s.router.Service() 73 svc.StopAsync() 74 s.Require().NoError(svc.AwaitTerminated(context.Background())) 75 s.Require().Equal(services.Terminated, svc.State()) 76 77 s.ingester.AssertExpectations(s.T()) 78 s.segwriter.AssertExpectations(s.T()) 79 } 80 81 func TestRouterSuite(t *testing.T) { suite.Run(t, new(routerTestSuite)) } 82 83 func (s *routerTestSuite) Test_IngesterPath() { 84 config := Config{ 85 WritePath: IngesterPath, 86 } 87 88 s.ingester.On("Push", mock.Anything, s.request). 89 Return(new(connect.Response[pushv1.PushResponse]), nil). 90 Once() 91 92 s.Assert().NoError(s.router.Send(context.Background(), s.request, config)) 93 } 94 95 func (s *routerTestSuite) Test_SegmentWriterPath() { 96 config := Config{ 97 WritePath: SegmentWriterPath, 98 } 99 100 s.segwriter.On("Push", mock.Anything, mock.Anything). 101 Return(new(connect.Response[pushv1.PushResponse]), nil). 102 Once() 103 104 s.Assert().NoError(s.router.Send(context.Background(), s.request, config)) 105 } 106 107 func (s *routerTestSuite) Test_CombinedPath() { 108 const ( 109 N = 100 110 w = 10 // Concurrent workers. 111 f = 0.5 112 d = 0.3 // Allowed delta: note that f is just a probability. 113 ) 114 115 config := Config{ 116 WritePath: CombinedPath, 117 IngesterWeight: 1, 118 SegmentWriterWeight: f, 119 } 120 121 var sentIngester atomic.Uint32 122 s.ingester.On("Push", mock.Anything, mock.Anything). 123 Run(func(m mock.Arguments) { 124 sentIngester.Add(1) 125 // Assert that no race condition occurs: we delete series 126 // attempting to access it concurrently with segment writer 127 // that should convert the distributor request to a segment 128 // writer request. 129 m.Get(1).(*distributormodel.ProfileSeries).Profile = nil 130 }). 131 Return(new(connect.Response[pushv1.PushResponse]), nil) 132 133 var sentSegwriter atomic.Uint32 134 s.segwriter.On("Push", mock.Anything, mock.Anything). 135 Run(func(m mock.Arguments) { 136 sentSegwriter.Add(1) 137 m.Get(1).(*distributormodel.ProfileSeries).Profile = nil 138 }). 139 Return(new(connect.Response[pushv1.PushResponse]), nil) 140 141 for i := 0; i < w; i++ { 142 for j := 0; j < N; j++ { 143 s.Assert().NoError(s.router.Send(context.Background(), s.request.Clone(), config)) 144 } 145 } 146 147 s.router.inflight.Wait() 148 expected := N * f * w 149 delta := expected * d 150 s.Assert().Equal(N*w, int(sentIngester.Load())) 151 s.Assert().Greater(int(sentSegwriter.Load()), int(expected-delta)) 152 s.Assert().Less(int(sentSegwriter.Load()), int(expected+delta)) 153 } 154 155 func (s *routerTestSuite) Test_UnspecifiedWriterPath() { 156 config := Config{} // Default should route to ingester 157 158 s.ingester.On("Push", mock.Anything, mock.Anything). 159 Return(new(connect.Response[pushv1.PushResponse]), nil). 160 Once() 161 162 s.Assert().NoError(s.router.Send(context.Background(), s.request, config)) 163 } 164 165 func (s *routerTestSuite) Test_CombinedPath_ZeroWeights() { 166 config := Config{ 167 WritePath: CombinedPath, 168 } 169 170 s.Assert().NoError(s.router.Send(context.Background(), s.request, config)) 171 } 172 173 func (s *routerTestSuite) Test_CombinedPath_IngesterError() { 174 config := Config{ 175 WritePath: CombinedPath, 176 // We ensure that request is sent to both. 177 IngesterWeight: 1, 178 SegmentWriterWeight: 1, 179 } 180 181 s.segwriter.On("Push", mock.Anything, mock.Anything). 182 Return(new(connect.Response[pushv1.PushResponse]), nil). 183 Once() 184 185 s.ingester.On("Push", mock.Anything, mock.Anything). 186 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 187 Once() 188 189 s.Assert().Error(s.router.Send(context.Background(), s.request, config), context.Canceled) 190 } 191 192 func (s *routerTestSuite) Test_CombinedPath_SegmentWriterError() { 193 config := Config{ 194 WritePath: CombinedPath, 195 // We ensure that request is sent to both. 196 IngesterWeight: 1, 197 SegmentWriterWeight: 1, 198 } 199 200 s.segwriter.On("Push", mock.Anything, mock.Anything). 201 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 202 Once() 203 204 s.ingester.On("Push", mock.Anything, mock.Anything). 205 Return(new(connect.Response[pushv1.PushResponse]), nil). 206 Once() 207 208 s.Assert().NoError(s.router.Send(context.Background(), s.request, config)) 209 } 210 211 func (s *routerTestSuite) Test_CombinedPath_Ingester_Exclusive_Error() { 212 config := Config{ 213 WritePath: CombinedPath, 214 // The request is only sent to ingester. 215 IngesterWeight: 1, 216 SegmentWriterWeight: 0, 217 } 218 219 s.ingester.On("Push", mock.Anything, mock.Anything). 220 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 221 Once() 222 223 s.Assert().Error(s.router.Send(context.Background(), s.request, config), context.Canceled) 224 } 225 226 func (s *routerTestSuite) Test_CombinedPath_SegmentWriter_Exclusive_Error() { 227 config := Config{ 228 WritePath: CombinedPath, 229 // The request is only sent to segment writer. 230 IngesterWeight: 0, 231 SegmentWriterWeight: 1, 232 } 233 234 s.segwriter.On("Push", mock.Anything, mock.Anything). 235 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 236 Once() 237 238 s.Assert().Error(s.router.Send(context.Background(), s.request, config), context.Canceled) 239 } 240 241 func (s *routerTestSuite) Test_AsyncIngest_Synchronous() { 242 config := Config{ 243 WritePath: SegmentWriterPath, 244 AsyncIngest: false, 245 } 246 247 s.segwriter.On("Push", mock.Anything, mock.Anything). 248 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 249 Once() 250 251 err := s.router.Send(context.Background(), s.request, config) 252 s.Assert().Error(err) 253 } 254 255 func (s *routerTestSuite) Test_AsyncIngest_Asynchronous() { 256 config := Config{ 257 WritePath: SegmentWriterPath, 258 AsyncIngest: true, 259 } 260 261 s.segwriter.On("Push", mock.Anything, mock.Anything). 262 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 263 Once() 264 265 err := s.router.Send(context.Background(), s.request, config) 266 s.Assert().NoError(err) 267 268 s.router.inflight.Wait() 269 } 270 271 func (s *routerTestSuite) Test_AsyncIngest_CombinedPath() { 272 config := Config{ 273 WritePath: CombinedPath, 274 IngesterWeight: 1, 275 SegmentWriterWeight: 1, 276 AsyncIngest: true, 277 } 278 279 s.ingester.On("Push", mock.Anything, mock.Anything). 280 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 281 Once() 282 283 s.segwriter.On("Push", mock.Anything, mock.Anything). 284 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 285 Once() 286 287 err := s.router.Send(context.Background(), s.request, config) 288 s.Assert().Error(err) 289 290 s.router.inflight.Wait() 291 } 292 293 func (s *routerTestSuite) Test_AsyncIngest_DelayCanceled() { 294 config := Config{ 295 WritePath: CombinedPath, 296 IngesterWeight: 1, 297 SegmentWriterWeight: 1, 298 AsyncIngest: true, 299 } 300 301 s.ingester.On("Push", mock.Anything, mock.Anything). 302 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 303 Once() 304 305 s.segwriter.On("Push", mock.Anything, mock.Anything). 306 Return(new(connect.Response[pushv1.PushResponse]), context.Canceled). 307 Once() 308 309 var canceled atomic.Bool 310 ctx := delayhandler.WithDelayCancel(context.Background(), func() { 311 canceled.Store(true) 312 }) 313 314 s.Assert().Error(s.router.Send(ctx, s.request, config)) 315 s.router.inflight.Wait() 316 317 s.Assert().True(canceled.Load()) 318 }