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) {}