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  }