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  }