github.com/grafana/pyroscope@v1.18.0/pkg/querier/worker/scheduler_processor.go (about) 1 // SPDX-License-Identifier: AGPL-3.0-only 2 // Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/querier/worker/scheduler_processor.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 "math/rand" 12 "net/http" 13 "strings" 14 "time" 15 16 "github.com/go-kit/log" 17 "github.com/go-kit/log/level" 18 "github.com/gogo/status" 19 "github.com/grafana/dskit/backoff" 20 "github.com/grafana/dskit/grpcclient" 21 "github.com/grafana/dskit/middleware" 22 "github.com/grafana/dskit/ring" 23 "github.com/grafana/dskit/ring/client" 24 "github.com/grafana/dskit/services" 25 "github.com/grafana/dskit/user" 26 otgrpc "github.com/opentracing-contrib/go-grpc" 27 "github.com/opentracing/opentracing-go" 28 "github.com/prometheus/client_golang/prometheus" 29 "github.com/prometheus/client_golang/prometheus/promauto" 30 "go.uber.org/atomic" 31 "google.golang.org/grpc" 32 "google.golang.org/grpc/health/grpc_health_v1" 33 34 "github.com/grafana/pyroscope/pkg/frontend/frontendpb" 35 querier_stats "github.com/grafana/pyroscope/pkg/querier/stats" 36 "github.com/grafana/pyroscope/pkg/scheduler/schedulerpb" 37 util_log "github.com/grafana/pyroscope/pkg/util" 38 "github.com/grafana/pyroscope/pkg/util/httpgrpc" 39 "github.com/grafana/pyroscope/pkg/util/httpgrpcutil" 40 ) 41 42 var processorBackoffConfig = backoff.Config{ 43 MinBackoff: 250 * time.Millisecond, 44 MaxBackoff: 2 * time.Second, 45 } 46 47 func newSchedulerProcessor(cfg Config, handler RequestHandler, log log.Logger, reg prometheus.Registerer) (*schedulerProcessor, []services.Service) { 48 p := &schedulerProcessor{ 49 log: log, 50 handler: handler, 51 maxMessageSize: cfg.GRPCClientConfig.MaxSendMsgSize, 52 querierID: cfg.QuerierID, 53 grpcConfig: cfg.GRPCClientConfig, 54 maxLoopDuration: cfg.MaxLoopDuration, 55 56 schedulerClientFactory: func(conn *grpc.ClientConn) schedulerpb.SchedulerForQuerierClient { 57 return schedulerpb.NewSchedulerForQuerierClient(conn) 58 }, 59 60 frontendClientRequestDuration: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ 61 Name: "pyroscope_querier_query_frontend_request_duration_seconds", 62 Help: "Time spend doing requests to frontend.", 63 Buckets: prometheus.ExponentialBuckets(0.001, 4, 6), 64 }, []string{"operation", "status_code"}), 65 } 66 67 frontendClientsGauge := promauto.With(reg).NewGauge(prometheus.GaugeOpts{ 68 Name: "pyroscope_querier_query_frontend_clients", 69 Help: "The current number of clients connected to query-frontend.", 70 }) 71 72 poolConfig := client.PoolConfig{ 73 CheckInterval: 5 * time.Second, 74 HealthCheckEnabled: true, 75 HealthCheckTimeout: 1 * time.Second, 76 } 77 78 p.frontendPool = client.NewPool("frontend", poolConfig, nil, p.frontendClientFactory(), frontendClientsGauge, log) 79 return p, []services.Service{p.frontendPool} 80 } 81 82 // Handles incoming queries from query-scheduler. 83 type schedulerProcessor struct { 84 log log.Logger 85 handler RequestHandler 86 grpcConfig grpcclient.Config 87 maxMessageSize int 88 querierID string 89 maxLoopDuration time.Duration 90 91 frontendPool *client.Pool 92 frontendClientRequestDuration *prometheus.HistogramVec 93 94 schedulerClientFactory func(conn *grpc.ClientConn) schedulerpb.SchedulerForQuerierClient 95 } 96 97 // notifyShutdown implements processor. 98 func (sp *schedulerProcessor) notifyShutdown(ctx context.Context, conn *grpc.ClientConn, address string) { 99 client := sp.schedulerClientFactory(conn) 100 101 req := &schedulerpb.NotifyQuerierShutdownRequest{QuerierID: sp.querierID} 102 if _, err := client.NotifyQuerierShutdown(ctx, req); err != nil { 103 // Since we're shutting down there's nothing we can do except logging it. 104 level.Warn(sp.log).Log("msg", "failed to notify querier shutdown to query-scheduler", "address", address, "err", err) 105 } 106 } 107 108 func (sp *schedulerProcessor) processQueriesOnSingleStream(workerCtx context.Context, conn *grpc.ClientConn, address string) { 109 schedulerClient := sp.schedulerClientFactory(conn) 110 111 // Run the querier loop (and so all the queries) in a dedicated context that we call the "execution context". 112 // The execution context is cancelled once the workerCtx is cancelled AND there's no inflight query executing. 113 execCtx, execCancel, inflightQuery := newExecutionContext(workerCtx, sp.log) 114 defer execCancel() 115 116 backoff := backoff.New(execCtx, processorBackoffConfig) 117 for backoff.Ongoing() { 118 func() { 119 if err := sp.querierLoop(execCtx, schedulerClient, address, inflightQuery); err != nil { 120 // Do not log an error is the query-scheduler is shutting down. 121 if s, ok := status.FromError(err); !ok || 122 (!strings.Contains(s.Message(), schedulerpb.ErrSchedulerIsNotRunning.Error()) && 123 !strings.Contains(s.Message(), context.Canceled.Error()) && 124 !strings.Contains(s.Message(), "stream terminated")) { 125 level.Error(sp.log).Log("msg", "error processing requests from scheduler", "err", err, "addr", address) 126 } 127 if strings.Contains(err.Error(), context.Canceled.Error()) || strings.Contains(err.Error(), "stream terminated") { 128 backoff.Reset() 129 return 130 } 131 backoff.Wait() 132 return 133 } 134 135 backoff.Reset() 136 }() 137 } 138 } 139 140 // process loops processing requests on an established stream. 141 func (sp *schedulerProcessor) querierLoop(parentCtx context.Context, schedulerClient schedulerpb.SchedulerForQuerierClient, address string, inflightQuery *atomic.Bool) error { 142 loopCtx, loopCancel := context.WithCancel(parentCtx) 143 defer loopCancel() 144 145 if sp.maxLoopDuration > 0 { 146 go func() { 147 timer := time.NewTimer(jitter(sp.maxLoopDuration, 0.3)) 148 defer timer.Stop() 149 150 select { 151 case <-timer.C: 152 level.Debug(sp.log).Log("msg", "waiting for inflight queries to complete") 153 for inflightQuery.Load() { 154 select { 155 case <-parentCtx.Done(): 156 // In the meanwhile, the execution context has been explicitly canceled, so we should just terminate. 157 return 158 default: 159 // Wait and check again inflight queries. 160 time.Sleep(100 * time.Millisecond) 161 } 162 } 163 level.Debug(sp.log).Log("msg", "refreshing scheduler connection") 164 loopCancel() 165 case <-parentCtx.Done(): 166 return 167 } 168 }() 169 } 170 171 c, err := schedulerClient.QuerierLoop(loopCtx) 172 if err == nil { 173 err = c.Send(&schedulerpb.QuerierToScheduler{QuerierID: sp.querierID}) 174 } 175 176 if err != nil { 177 level.Warn(sp.log).Log("msg", "error contacting scheduler", "err", err, "addr", address) 178 return err 179 } 180 181 for { 182 request, err := c.Recv() 183 if err != nil { 184 return err 185 } 186 187 inflightQuery.Store(true) 188 189 // Handle the request on a "background" goroutine, so we go back to 190 // blocking on c.Recv(). This allows us to detect the stream closing 191 // and cancel the query. We don't actually handle queries in parallel 192 // here, as we're running in lock step with the server - each Recv is 193 // paired with a Send. 194 go func() { 195 defer inflightQuery.Store(false) 196 197 // We need to inject user into context for sending response back. 198 ctx := user.InjectOrgID(c.Context(), request.UserID) 199 200 tracer := opentracing.GlobalTracer() 201 // Ignore errors here. If we cannot get parent span, we just don't create new one. 202 parentSpanContext, _ := httpgrpcutil.GetParentSpanForRequest(tracer, request.HttpRequest) 203 if parentSpanContext != nil { 204 queueSpan, spanCtx := opentracing.StartSpanFromContextWithTracer(ctx, tracer, "querier_processor_runRequest", opentracing.ChildOf(parentSpanContext)) 205 defer queueSpan.Finish() 206 207 ctx = spanCtx 208 } 209 logger := util_log.LoggerWithContext(ctx, sp.log) 210 211 sp.runRequest(ctx, logger, request.QueryID, request.FrontendAddress, request.StatsEnabled, request.HttpRequest) 212 213 // Report back to scheduler that processing of the query has finished. 214 if err := c.Send(&schedulerpb.QuerierToScheduler{}); err != nil { 215 level.Error(logger).Log("msg", "error notifying scheduler about finished query", "err", err, "addr", address) 216 } 217 }() 218 } 219 } 220 221 func jitter(d time.Duration, factor float64) time.Duration { 222 maxJitter := time.Duration(float64(d) * factor) 223 return d - time.Duration(rand.Int63n(int64(maxJitter))) 224 } 225 226 func (sp *schedulerProcessor) runRequest(ctx context.Context, logger log.Logger, queryID uint64, frontendAddress string, statsEnabled bool, request *httpgrpc.HTTPRequest) { 227 var stats *querier_stats.Stats 228 if statsEnabled { 229 stats, ctx = querier_stats.ContextWithEmptyStats(ctx) 230 } 231 232 response, err := sp.handler.Handle(ctx, request) 233 if err != nil { 234 var ok bool 235 response, ok = httpgrpc.HTTPResponseFromError(err) 236 if !ok { 237 response = &httpgrpc.HTTPResponse{ 238 Code: http.StatusInternalServerError, 239 Body: []byte(err.Error()), 240 } 241 } 242 } 243 244 // Ensure responses that are too big are not retried. 245 if len(response.Body) >= sp.maxMessageSize { 246 level.Error(logger).Log("msg", "response larger than max message size", "size", len(response.Body), "maxMessageSize", sp.maxMessageSize) 247 248 errMsg := fmt.Sprintf("response larger than the max message size (%d vs %d)", len(response.Body), sp.maxMessageSize) 249 response = &httpgrpc.HTTPResponse{ 250 Code: http.StatusRequestEntityTooLarge, 251 Body: []byte(errMsg), 252 } 253 } 254 255 c, err := sp.frontendPool.GetClientFor(frontendAddress) 256 if err == nil { 257 // Response is empty and uninteresting. 258 _, err = c.(frontendpb.FrontendForQuerierClient).QueryResult(ctx, &frontendpb.QueryResultRequest{ 259 QueryID: queryID, 260 HttpResponse: response, 261 Stats: stats, 262 }) 263 } 264 if err != nil { 265 level.Error(logger).Log("msg", "error notifying frontend about finished query", "err", err, "frontend", frontendAddress) 266 } 267 } 268 269 type frontendClientFactory struct { 270 opts func() ([]grpc.DialOption, error) 271 } 272 273 func newFrontendClientFactory(opts func() ([]grpc.DialOption, error)) *frontendClientFactory { 274 return &frontendClientFactory{opts: opts} 275 } 276 277 func (f *frontendClientFactory) FromInstance(inst ring.InstanceDesc) (client.PoolClient, error) { 278 opts, err := f.opts() 279 if err != nil { 280 return nil, err 281 } 282 283 conn, err := grpc.Dial(inst.Addr, opts...) 284 if err != nil { 285 return nil, err 286 } 287 288 return &frontendClient{ 289 FrontendForQuerierClient: frontendpb.NewFrontendForQuerierClient(conn), 290 HealthClient: grpc_health_v1.NewHealthClient(conn), 291 conn: conn, 292 }, nil 293 } 294 295 func (sp *schedulerProcessor) frontendClientFactory() client.PoolFactory { 296 return newFrontendClientFactory(func() ([]grpc.DialOption, error) { 297 return sp.grpcConfig.DialOption([]grpc.UnaryClientInterceptor{ 298 otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer()), 299 middleware.ClientUserHeaderInterceptor, 300 middleware.UnaryClientInstrumentInterceptor(sp.frontendClientRequestDuration), 301 }, nil, nil) 302 }) 303 } 304 305 type frontendClient struct { 306 frontendpb.FrontendForQuerierClient 307 grpc_health_v1.HealthClient 308 conn *grpc.ClientConn 309 } 310 311 func (fc *frontendClient) Close() error { 312 return fc.conn.Close() 313 }