github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/frontend/v2/frontend_test.go (about) 1 package v2 2 3 import ( 4 "context" 5 "net" 6 "strconv" 7 "strings" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/go-kit/log" 13 "github.com/grafana/dskit/flagext" 14 "github.com/grafana/dskit/services" 15 "github.com/stretchr/testify/require" 16 "github.com/weaveworks/common/httpgrpc" 17 "github.com/weaveworks/common/user" 18 "go.uber.org/atomic" 19 "google.golang.org/grpc" 20 21 "github.com/cortexproject/cortex/pkg/frontend/v2/frontendv2pb" 22 "github.com/cortexproject/cortex/pkg/querier/stats" 23 "github.com/cortexproject/cortex/pkg/scheduler/schedulerpb" 24 "github.com/cortexproject/cortex/pkg/util/test" 25 ) 26 27 const testFrontendWorkerConcurrency = 5 28 29 func setupFrontend(t *testing.T, schedulerReplyFunc func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend) (*Frontend, *mockScheduler) { 30 l, err := net.Listen("tcp", "") 31 require.NoError(t, err) 32 33 server := grpc.NewServer() 34 35 h, p, err := net.SplitHostPort(l.Addr().String()) 36 require.NoError(t, err) 37 38 grpcPort, err := strconv.Atoi(p) 39 require.NoError(t, err) 40 41 cfg := Config{} 42 flagext.DefaultValues(&cfg) 43 cfg.SchedulerAddress = l.Addr().String() 44 cfg.WorkerConcurrency = testFrontendWorkerConcurrency 45 cfg.Addr = h 46 cfg.Port = grpcPort 47 48 //logger := log.NewLogfmtLogger(os.Stdout) 49 logger := log.NewNopLogger() 50 f, err := NewFrontend(cfg, logger, nil) 51 require.NoError(t, err) 52 53 frontendv2pb.RegisterFrontendForQuerierServer(server, f) 54 55 ms := newMockScheduler(t, f, schedulerReplyFunc) 56 schedulerpb.RegisterSchedulerForFrontendServer(server, ms) 57 58 require.NoError(t, services.StartAndAwaitRunning(context.Background(), f)) 59 t.Cleanup(func() { 60 _ = services.StopAndAwaitTerminated(context.Background(), f) 61 }) 62 63 go func() { 64 _ = server.Serve(l) 65 }() 66 67 t.Cleanup(func() { 68 _ = l.Close() 69 }) 70 71 // Wait for frontend to connect to scheduler. 72 test.Poll(t, 1*time.Second, 1, func() interface{} { 73 ms.mu.Lock() 74 defer ms.mu.Unlock() 75 76 return len(ms.frontendAddr) 77 }) 78 79 return f, ms 80 } 81 82 func sendResponseWithDelay(f *Frontend, delay time.Duration, userID string, queryID uint64, resp *httpgrpc.HTTPResponse) { 83 if delay > 0 { 84 time.Sleep(delay) 85 } 86 87 ctx := user.InjectOrgID(context.Background(), userID) 88 _, _ = f.QueryResult(ctx, &frontendv2pb.QueryResultRequest{ 89 QueryID: queryID, 90 HttpResponse: resp, 91 Stats: &stats.Stats{}, 92 }) 93 } 94 95 func TestFrontendBasicWorkflow(t *testing.T) { 96 const ( 97 body = "all fine here" 98 userID = "test" 99 ) 100 101 f, _ := setupFrontend(t, func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend { 102 // We cannot call QueryResult directly, as Frontend is not yet waiting for the response. 103 // It first needs to be told that enqueuing has succeeded. 104 go sendResponseWithDelay(f, 100*time.Millisecond, userID, msg.QueryID, &httpgrpc.HTTPResponse{ 105 Code: 200, 106 Body: []byte(body), 107 }) 108 109 return &schedulerpb.SchedulerToFrontend{Status: schedulerpb.OK} 110 }) 111 112 resp, err := f.RoundTripGRPC(user.InjectOrgID(context.Background(), userID), &httpgrpc.HTTPRequest{}) 113 require.NoError(t, err) 114 require.Equal(t, int32(200), resp.Code) 115 require.Equal(t, []byte(body), resp.Body) 116 } 117 118 func TestFrontendRetryEnqueue(t *testing.T) { 119 // Frontend uses worker concurrency to compute number of retries. We use one less failure. 120 failures := atomic.NewInt64(testFrontendWorkerConcurrency - 1) 121 const ( 122 body = "hello world" 123 userID = "test" 124 ) 125 126 f, _ := setupFrontend(t, func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend { 127 fail := failures.Dec() 128 if fail >= 0 { 129 return &schedulerpb.SchedulerToFrontend{Status: schedulerpb.SHUTTING_DOWN} 130 } 131 132 go sendResponseWithDelay(f, 100*time.Millisecond, userID, msg.QueryID, &httpgrpc.HTTPResponse{ 133 Code: 200, 134 Body: []byte(body), 135 }) 136 137 return &schedulerpb.SchedulerToFrontend{Status: schedulerpb.OK} 138 }) 139 140 _, err := f.RoundTripGRPC(user.InjectOrgID(context.Background(), userID), &httpgrpc.HTTPRequest{}) 141 require.NoError(t, err) 142 } 143 144 func TestFrontendEnqueueFailure(t *testing.T) { 145 f, _ := setupFrontend(t, func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend { 146 return &schedulerpb.SchedulerToFrontend{Status: schedulerpb.SHUTTING_DOWN} 147 }) 148 149 _, err := f.RoundTripGRPC(user.InjectOrgID(context.Background(), "test"), &httpgrpc.HTTPRequest{}) 150 require.Error(t, err) 151 require.True(t, strings.Contains(err.Error(), "failed to enqueue request")) 152 } 153 154 func TestFrontendCancellation(t *testing.T) { 155 f, ms := setupFrontend(t, nil) 156 157 ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) 158 defer cancel() 159 160 resp, err := f.RoundTripGRPC(user.InjectOrgID(ctx, "test"), &httpgrpc.HTTPRequest{}) 161 require.EqualError(t, err, context.DeadlineExceeded.Error()) 162 require.Nil(t, resp) 163 164 // We wait a bit to make sure scheduler receives the cancellation request. 165 test.Poll(t, time.Second, 2, func() interface{} { 166 ms.mu.Lock() 167 defer ms.mu.Unlock() 168 169 return len(ms.msgs) 170 }) 171 172 ms.checkWithLock(func() { 173 require.Equal(t, 2, len(ms.msgs)) 174 require.True(t, ms.msgs[0].Type == schedulerpb.ENQUEUE) 175 require.True(t, ms.msgs[1].Type == schedulerpb.CANCEL) 176 require.True(t, ms.msgs[0].QueryID == ms.msgs[1].QueryID) 177 }) 178 } 179 180 func TestFrontendFailedCancellation(t *testing.T) { 181 f, ms := setupFrontend(t, nil) 182 183 ctx, cancel := context.WithCancel(context.Background()) 184 defer cancel() 185 186 go func() { 187 time.Sleep(100 * time.Millisecond) 188 189 // stop scheduler workers 190 addr := "" 191 f.schedulerWorkers.mu.Lock() 192 for k := range f.schedulerWorkers.workers { 193 addr = k 194 break 195 } 196 f.schedulerWorkers.mu.Unlock() 197 198 f.schedulerWorkers.AddressRemoved(addr) 199 200 // Wait for worker goroutines to stop. 201 time.Sleep(100 * time.Millisecond) 202 203 // Cancel request. Frontend will try to send cancellation to scheduler, but that will fail (not visible to user). 204 // Everything else should still work fine. 205 cancel() 206 }() 207 208 // send request 209 resp, err := f.RoundTripGRPC(user.InjectOrgID(ctx, "test"), &httpgrpc.HTTPRequest{}) 210 require.EqualError(t, err, context.Canceled.Error()) 211 require.Nil(t, resp) 212 213 ms.checkWithLock(func() { 214 require.Equal(t, 1, len(ms.msgs)) 215 }) 216 } 217 218 type mockScheduler struct { 219 t *testing.T 220 f *Frontend 221 222 replyFunc func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend 223 224 mu sync.Mutex 225 frontendAddr map[string]int 226 msgs []*schedulerpb.FrontendToScheduler 227 } 228 229 func newMockScheduler(t *testing.T, f *Frontend, replyFunc func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend) *mockScheduler { 230 return &mockScheduler{t: t, f: f, frontendAddr: map[string]int{}, replyFunc: replyFunc} 231 } 232 233 func (m *mockScheduler) checkWithLock(fn func()) { 234 m.mu.Lock() 235 defer m.mu.Unlock() 236 237 fn() 238 } 239 240 func (m *mockScheduler) FrontendLoop(frontend schedulerpb.SchedulerForFrontend_FrontendLoopServer) error { 241 init, err := frontend.Recv() 242 if err != nil { 243 return err 244 } 245 246 m.mu.Lock() 247 m.frontendAddr[init.FrontendAddress]++ 248 m.mu.Unlock() 249 250 // Ack INIT from frontend. 251 if err := frontend.Send(&schedulerpb.SchedulerToFrontend{Status: schedulerpb.OK}); err != nil { 252 return err 253 } 254 255 for { 256 msg, err := frontend.Recv() 257 if err != nil { 258 return err 259 } 260 261 m.mu.Lock() 262 m.msgs = append(m.msgs, msg) 263 m.mu.Unlock() 264 265 reply := &schedulerpb.SchedulerToFrontend{Status: schedulerpb.OK} 266 if m.replyFunc != nil { 267 reply = m.replyFunc(m.f, msg) 268 } 269 270 if err := frontend.Send(reply); err != nil { 271 return err 272 } 273 } 274 }