github.com/grafana/pyroscope@v1.18.0/pkg/querier/worker/worker_test.go (about)

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  // Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/querier/worker/worker_test.go
     3  // Provenance-includes-license: Apache-2.0
     4  // Provenance-includes-copyright: The Cortex Authors.
     5  
     6  package worker
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/go-kit/log"
    15  	"github.com/grafana/dskit/flagext"
    16  	"github.com/grafana/dskit/services"
    17  	"github.com/grafana/dskit/test"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  	"google.golang.org/grpc"
    21  
    22  	"github.com/grafana/pyroscope/pkg/scheduler/schedulerdiscovery"
    23  	"github.com/grafana/pyroscope/pkg/util/servicediscovery"
    24  )
    25  
    26  func TestConfig_Validate(t *testing.T) {
    27  	tests := map[string]struct {
    28  		setup       func(cfg *Config)
    29  		expectedErr string
    30  	}{
    31  		"should pass with default config": {
    32  			setup: func(cfg *Config) {},
    33  		},
    34  		"should pass if scheduler address is configured": {
    35  			setup: func(cfg *Config) {
    36  				cfg.SchedulerAddress = "localhost:9095"
    37  			},
    38  		},
    39  		"should pass if query-scheduler service discovery is set to ring, and no frontend and scheduler address is configured": {
    40  			setup: func(cfg *Config) {
    41  				cfg.QuerySchedulerDiscovery.Mode = schedulerdiscovery.ModeRing
    42  			},
    43  		},
    44  		"should fail if query-scheduler service discovery is set to ring, and scheduler address is configured": {
    45  			setup: func(cfg *Config) {
    46  				cfg.QuerySchedulerDiscovery.Mode = schedulerdiscovery.ModeRing
    47  				cfg.SchedulerAddress = "localhost:9095"
    48  			},
    49  			expectedErr: `scheduler address cannot be specified when query-scheduler service discovery mode is set to 'ring'`,
    50  		},
    51  	}
    52  
    53  	for testName, testData := range tests {
    54  		t.Run(testName, func(t *testing.T) {
    55  			cfg := Config{}
    56  			flagext.DefaultValues(&cfg)
    57  			testData.setup(&cfg)
    58  
    59  			actualErr := cfg.Validate(log.NewNopLogger())
    60  			if testData.expectedErr == "" {
    61  				require.NoError(t, actualErr)
    62  			} else {
    63  				require.Error(t, actualErr)
    64  				assert.ErrorContains(t, actualErr, testData.expectedErr)
    65  			}
    66  		})
    67  	}
    68  }
    69  
    70  func TestResetConcurrency(t *testing.T) {
    71  	tests := []struct {
    72  		name                string
    73  		maxConcurrent       int
    74  		numTargets          int
    75  		numInUseTargets     int
    76  		expectedConcurrency int
    77  	}{
    78  		{
    79  			name:                "Create at least one processor per target if max concurrent = 0, with all targets in use",
    80  			maxConcurrent:       0,
    81  			numTargets:          2,
    82  			numInUseTargets:     2,
    83  			expectedConcurrency: 2,
    84  		},
    85  		{
    86  			name:                "Create at least one processor per target if max concurrent = 0, with some targets in use",
    87  			maxConcurrent:       0,
    88  			numTargets:          2,
    89  			numInUseTargets:     1,
    90  			expectedConcurrency: 2,
    91  		},
    92  		{
    93  			name:                "Max concurrent dividing with a remainder, with all targets in use",
    94  			maxConcurrent:       7,
    95  			numTargets:          4,
    96  			numInUseTargets:     4,
    97  			expectedConcurrency: 7,
    98  		},
    99  		{
   100  			name:            "Max concurrent dividing with a remainder, with some targets in use",
   101  			maxConcurrent:   7,
   102  			numTargets:      4,
   103  			numInUseTargets: 2,
   104  			expectedConcurrency:/* in use:  */ 7 + /* not in use : */ 2,
   105  		},
   106  		{
   107  			name:                "Max concurrent dividing evenly, with all targets in use",
   108  			maxConcurrent:       6,
   109  			numTargets:          2,
   110  			numInUseTargets:     2,
   111  			expectedConcurrency: 6,
   112  		},
   113  		{
   114  			name:            "Max concurrent dividing evenly, with some targets in use",
   115  			maxConcurrent:   6,
   116  			numTargets:      4,
   117  			numInUseTargets: 2,
   118  			expectedConcurrency:/* in use:  */ 6 + /* not in use : */ 2,
   119  		},
   120  	}
   121  
   122  	for _, tt := range tests {
   123  		t.Run(tt.name, func(t *testing.T) {
   124  			cfg := Config{
   125  				MaxConcurrent: tt.maxConcurrent,
   126  			}
   127  
   128  			w, err := newQuerierWorkerWithProcessor(cfg, log.NewNopLogger(), &mockProcessor{}, nil, nil)
   129  			require.NoError(t, err)
   130  			require.NoError(t, services.StartAndAwaitRunning(context.Background(), w))
   131  
   132  			for i := 0; i < tt.numTargets; i++ {
   133  				// gRPC connections are virtual... they don't actually try to connect until they are needed.
   134  				// This allows us to use dummy ports, and not get any errors.
   135  				w.InstanceAdded(servicediscovery.Instance{
   136  					Address: fmt.Sprintf("127.0.0.1:%d", i),
   137  					InUse:   i < tt.numInUseTargets,
   138  				})
   139  			}
   140  
   141  			test.Poll(t, 250*time.Millisecond, tt.expectedConcurrency, func() interface{} {
   142  				return getConcurrentProcessors(w)
   143  			})
   144  
   145  			require.NoError(t, services.StopAndAwaitTerminated(context.Background(), w))
   146  			assert.Equal(t, 0, getConcurrentProcessors(w))
   147  		})
   148  	}
   149  }
   150  
   151  func TestQuerierWorker_getDesiredConcurrency(t *testing.T) {
   152  	tests := map[string]struct {
   153  		instances     []servicediscovery.Instance
   154  		maxConcurrent int
   155  		expected      map[string]int
   156  	}{
   157  		"should return empty map on no instances": {
   158  			instances:     nil,
   159  			maxConcurrent: 4,
   160  			expected:      map[string]int{},
   161  		},
   162  		"should divide the max concurrency between in-use instances, and create 1 connection for each instance not in-use": {
   163  			instances: []servicediscovery.Instance{
   164  				{Address: "1.1.1.1", InUse: true},
   165  				{Address: "2.2.2.2", InUse: false},
   166  				{Address: "3.3.3.3", InUse: true},
   167  				{Address: "4.4.4.4", InUse: false},
   168  			},
   169  			maxConcurrent: 4,
   170  			expected: map[string]int{
   171  				"1.1.1.1": 2,
   172  				"2.2.2.2": 1,
   173  				"3.3.3.3": 2,
   174  				"4.4.4.4": 1,
   175  			},
   176  		},
   177  		"should create 1 connection for each instance if max concurrency is set to 0": {
   178  			instances: []servicediscovery.Instance{
   179  				{Address: "1.1.1.1", InUse: true},
   180  				{Address: "2.2.2.2", InUse: false},
   181  				{Address: "3.3.3.3", InUse: true},
   182  				{Address: "4.4.4.4", InUse: false},
   183  			},
   184  			maxConcurrent: 0,
   185  			expected: map[string]int{
   186  				"1.1.1.1": 1,
   187  				"2.2.2.2": 1,
   188  				"3.3.3.3": 1,
   189  				"4.4.4.4": 1,
   190  			},
   191  		},
   192  		"should create 1 connection for each instance if max concurrency is > 0 but less than the number of in-use instances": {
   193  			instances: []servicediscovery.Instance{
   194  				{Address: "1.1.1.1", InUse: true},
   195  				{Address: "2.2.2.2", InUse: false},
   196  				{Address: "3.3.3.3", InUse: true},
   197  				{Address: "4.4.4.4", InUse: false},
   198  			},
   199  			maxConcurrent: 1,
   200  			expected: map[string]int{
   201  				"1.1.1.1": 1,
   202  				"2.2.2.2": 1,
   203  				"3.3.3.3": 1,
   204  				"4.4.4.4": 1,
   205  			},
   206  		},
   207  	}
   208  
   209  	for testName, testData := range tests {
   210  		t.Run(testName, func(t *testing.T) {
   211  			cfg := Config{
   212  				MaxConcurrent: testData.maxConcurrent,
   213  			}
   214  
   215  			w, err := newQuerierWorkerWithProcessor(cfg, log.NewNopLogger(), &mockProcessor{}, nil, nil)
   216  			require.NoError(t, err)
   217  
   218  			for _, instance := range testData.instances {
   219  				w.instances[instance.Address] = instance
   220  			}
   221  
   222  			assert.Equal(t, testData.expected, w.getDesiredConcurrency())
   223  		})
   224  	}
   225  }
   226  
   227  func getConcurrentProcessors(w *querierWorker) int {
   228  	result := 0
   229  	w.mu.Lock()
   230  	defer w.mu.Unlock()
   231  
   232  	for _, mgr := range w.managers {
   233  		result += int(mgr.currentProcessors.Load())
   234  	}
   235  
   236  	return result
   237  }
   238  
   239  type mockProcessor struct{}
   240  
   241  func (m mockProcessor) processQueriesOnSingleStream(ctx context.Context, _ *grpc.ClientConn, _ string) {
   242  	<-ctx.Done()
   243  }
   244  
   245  func (m mockProcessor) notifyShutdown(_ context.Context, _ *grpc.ClientConn, _ string) {}