github.com/grafana/pyroscope@v1.18.0/pkg/scheduler/scheduler.go (about)

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  // Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/scheduler/scheduler.go
     3  // Provenance-includes-license: Apache-2.0
     4  // Provenance-includes-copyright: The Cortex Authors.
     5  
     6  package scheduler
     7  
     8  import (
     9  	"context"
    10  	"flag"
    11  	"io"
    12  	"net/http"
    13  	"sync"
    14  	"time"
    15  
    16  	"connectrpc.com/connect"
    17  	"github.com/go-kit/log"
    18  	"github.com/go-kit/log/level"
    19  	"github.com/grafana/dskit/grpcclient"
    20  	"github.com/grafana/dskit/middleware"
    21  	"github.com/grafana/dskit/ring"
    22  	"github.com/grafana/dskit/services"
    23  	"github.com/grafana/dskit/tenant"
    24  	"github.com/grafana/dskit/user"
    25  	otgrpc "github.com/opentracing-contrib/go-grpc"
    26  	"github.com/opentracing/opentracing-go"
    27  	"github.com/pkg/errors"
    28  	"github.com/prometheus/client_golang/prometheus"
    29  	"github.com/prometheus/client_golang/prometheus/promauto"
    30  	"google.golang.org/grpc"
    31  
    32  	"github.com/grafana/pyroscope/pkg/frontend/frontendpb"
    33  	"github.com/grafana/pyroscope/pkg/scheduler/queue"
    34  	"github.com/grafana/pyroscope/pkg/scheduler/schedulerdiscovery"
    35  	"github.com/grafana/pyroscope/pkg/scheduler/schedulerpb"
    36  	"github.com/grafana/pyroscope/pkg/util"
    37  	"github.com/grafana/pyroscope/pkg/util/httpgrpc"
    38  	"github.com/grafana/pyroscope/pkg/util/httpgrpcutil"
    39  	"github.com/grafana/pyroscope/pkg/util/validation"
    40  )
    41  
    42  // Scheduler is responsible for queueing and dispatching queries to Queriers.
    43  type Scheduler struct {
    44  	services.Service
    45  
    46  	cfg Config
    47  	log log.Logger
    48  
    49  	limits Limits
    50  
    51  	connectedFrontendsMu sync.Mutex
    52  	connectedFrontends   map[string]*connectedFrontend
    53  
    54  	requestQueue *queue.RequestQueue
    55  	activeUsers  *util.ActiveUsersCleanupService
    56  
    57  	pendingRequestsMu sync.Mutex
    58  	pendingRequests   map[requestKey]*schedulerRequest // Request is kept in this map even after being dispatched to querier. It can still be canceled at that time.
    59  
    60  	// The ring is used to let other components discover query-scheduler replicas.
    61  	// The ring is optional.
    62  	schedulerLifecycler *ring.BasicLifecycler
    63  
    64  	// Subservices manager.
    65  	subservices        *services.Manager
    66  	subservicesWatcher *services.FailureWatcher
    67  
    68  	// Metrics.
    69  	queueLength              *prometheus.GaugeVec
    70  	discardedRequests        *prometheus.CounterVec
    71  	cancelledRequests        *prometheus.CounterVec
    72  	connectedQuerierClients  prometheus.GaugeFunc
    73  	connectedFrontendClients prometheus.GaugeFunc
    74  	queueDuration            prometheus.Histogram
    75  	inflightRequests         prometheus.Summary
    76  
    77  	schedulerpb.UnimplementedSchedulerForFrontendServer
    78  	schedulerpb.UnimplementedSchedulerForQuerierServer
    79  }
    80  
    81  type requestKey struct {
    82  	frontendAddr string
    83  	queryID      uint64
    84  }
    85  
    86  type connectedFrontend struct {
    87  	connections int
    88  
    89  	// This context is used for running all queries from the same frontend.
    90  	// When last frontend connection is closed, context is canceled.
    91  	ctx    context.Context
    92  	cancel context.CancelFunc
    93  }
    94  
    95  type Config struct {
    96  	MaxOutstandingPerTenant int                       `yaml:"max_outstanding_requests_per_tenant"`
    97  	QuerierForgetDelay      time.Duration             `yaml:"querier_forget_delay" category:"experimental"`
    98  	GRPCClientConfig        grpcclient.Config         `yaml:"grpc_client_config" doc:"description=This configures the gRPC client used to report errors back to the query-frontend."`
    99  	ServiceDiscovery        schedulerdiscovery.Config `yaml:",inline"`
   100  
   101  	// Dial options used to initiate outgoing gRPC connections.
   102  	// Intended to be used by tests to use in-memory network connections.
   103  	DialOpts []grpc.DialOption `yaml:"-"`
   104  }
   105  
   106  func (cfg *Config) RegisterFlags(f *flag.FlagSet, logger log.Logger) {
   107  	f.IntVar(&cfg.MaxOutstandingPerTenant, "query-scheduler.max-outstanding-requests-per-tenant", 100, "Maximum number of outstanding requests per tenant per query-scheduler. In-flight requests above this limit will fail with HTTP response status code 429.")
   108  	f.DurationVar(&cfg.QuerierForgetDelay, "query-scheduler.querier-forget-delay", 0, "If a querier disconnects without sending notification about graceful shutdown, the query-scheduler will keep the querier in the tenant's shard until the forget delay has passed. This feature is useful to reduce the blast radius when shuffle-sharding is enabled.")
   109  	cfg.GRPCClientConfig.RegisterFlagsWithPrefix("query-scheduler.grpc-client-config", f)
   110  	cfg.ServiceDiscovery.RegisterFlags(f, logger)
   111  }
   112  
   113  func (cfg *Config) Validate() error {
   114  	return cfg.ServiceDiscovery.Validate()
   115  }
   116  
   117  // NewScheduler creates a new Scheduler.
   118  func NewScheduler(cfg Config, limits Limits, log log.Logger, registerer prometheus.Registerer) (*Scheduler, error) {
   119  	var err error
   120  
   121  	s := &Scheduler{
   122  		cfg:    cfg,
   123  		log:    log,
   124  		limits: limits,
   125  
   126  		pendingRequests:    map[requestKey]*schedulerRequest{},
   127  		connectedFrontends: map[string]*connectedFrontend{},
   128  		subservicesWatcher: services.NewFailureWatcher(),
   129  	}
   130  
   131  	s.queueLength = promauto.With(registerer).NewGaugeVec(prometheus.GaugeOpts{
   132  		Name: "pyroscope_query_scheduler_queue_length",
   133  		Help: "Number of queries in the queue.",
   134  	}, []string{"tenant"})
   135  
   136  	s.cancelledRequests = promauto.With(registerer).NewCounterVec(prometheus.CounterOpts{
   137  		Name: "pyroscope_query_scheduler_cancelled_requests_total",
   138  		Help: "Total number of query requests that were cancelled after enqueuing.",
   139  	}, []string{"tenant"})
   140  	s.discardedRequests = promauto.With(registerer).NewCounterVec(prometheus.CounterOpts{
   141  		Name: "pyroscope_query_scheduler_discarded_requests_total",
   142  		Help: "Total number of query requests discarded.",
   143  	}, []string{"tenant"})
   144  	s.requestQueue = queue.NewRequestQueue(cfg.MaxOutstandingPerTenant, cfg.QuerierForgetDelay, s.queueLength, s.discardedRequests)
   145  
   146  	s.queueDuration = promauto.With(registerer).NewHistogram(prometheus.HistogramOpts{
   147  		Name:    "pyroscope_query_scheduler_queue_duration_seconds",
   148  		Help:    "Time spend by requests in queue before getting picked up by a querier.",
   149  		Buckets: prometheus.DefBuckets,
   150  	})
   151  	s.connectedQuerierClients = promauto.With(registerer).NewGaugeFunc(prometheus.GaugeOpts{
   152  		Name: "pyroscope_query_scheduler_connected_querier_clients",
   153  		Help: "Number of querier worker clients currently connected to the query-scheduler.",
   154  	}, s.requestQueue.GetConnectedQuerierWorkersMetric)
   155  	s.connectedFrontendClients = promauto.With(registerer).NewGaugeFunc(prometheus.GaugeOpts{
   156  		Name: "pyroscope_query_scheduler_connected_frontend_clients",
   157  		Help: "Number of query-frontend worker clients currently connected to the query-scheduler.",
   158  	}, s.getConnectedFrontendClientsMetric)
   159  
   160  	s.inflightRequests = promauto.With(registerer).NewSummary(prometheus.SummaryOpts{
   161  		Name:       "pyroscope_query_scheduler_inflight_requests",
   162  		Help:       "Number of inflight requests (either queued or processing) sampled at a regular interval. Quantile buckets keep track of inflight requests over the last 60s.",
   163  		Objectives: map[float64]float64{0.5: 0.05, 0.75: 0.02, 0.8: 0.02, 0.9: 0.01, 0.95: 0.01, 0.99: 0.001},
   164  		MaxAge:     time.Minute,
   165  		AgeBuckets: 6,
   166  	})
   167  
   168  	s.activeUsers = util.NewActiveUsersCleanupWithDefaultValues(s.cleanupMetricsForInactiveUser)
   169  	subservices := []services.Service{s.requestQueue, s.activeUsers}
   170  
   171  	// Init the ring only if the ring-based service discovery mode is used.
   172  	if cfg.ServiceDiscovery.Mode == schedulerdiscovery.ModeRing {
   173  		s.schedulerLifecycler, err = schedulerdiscovery.NewRingLifecycler(cfg.ServiceDiscovery.SchedulerRing, log, registerer)
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  
   178  		subservices = append(subservices, s.schedulerLifecycler)
   179  	}
   180  
   181  	s.subservices, err = services.NewManager(subservices...)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	s.Service = services.NewBasicService(s.starting, s.running, s.stopping)
   187  	return s, nil
   188  }
   189  
   190  // Limits needed for the Query Scheduler - interface used for decoupling.
   191  type Limits interface {
   192  	// MaxQueriersPerTenant returns max queriers to use per tenant, or 0 if shuffle sharding is disabled.
   193  	MaxQueriersPerTenant(tenant string) int
   194  }
   195  
   196  type schedulerRequest struct {
   197  	frontendAddress string
   198  	userID          string
   199  	queryID         uint64
   200  	request         *httpgrpc.HTTPRequest
   201  	statsEnabled    bool
   202  
   203  	enqueueTime time.Time
   204  
   205  	ctx       context.Context
   206  	ctxCancel context.CancelFunc
   207  	queueSpan opentracing.Span
   208  
   209  	// This is only used for testing.
   210  	parentSpanContext opentracing.SpanContext
   211  }
   212  
   213  // FrontendLoop handles connection from frontend.
   214  func (s *Scheduler) FrontendLoop(ctx context.Context, frontend *connect.BidiStream[schedulerpb.FrontendToScheduler, schedulerpb.SchedulerToFrontend]) error {
   215  	frontendAddress, frontendCtx, err := s.frontendConnected(frontend)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	defer s.frontendDisconnected(frontendAddress)
   220  
   221  	// Response to INIT. If scheduler is not running, we skip for-loop, send SHUTTING_DOWN and exit this method.
   222  	if s.State() == services.Running {
   223  		if err := frontend.Send(&schedulerpb.SchedulerToFrontend{Status: schedulerpb.SchedulerToFrontendStatus_OK}); err != nil {
   224  			return err
   225  		}
   226  	}
   227  
   228  	// We stop accepting new queries in Stopping state. By returning quickly, we disconnect frontends, which in turns
   229  	// cancels all their queries.
   230  	for s.State() == services.Running {
   231  		msg, err := frontend.Receive()
   232  		if err != nil {
   233  			// No need to report this as error, it is expected when query-frontend performs SendClose() (as frontendSchedulerWorker does).
   234  			if errors.Is(err, io.EOF) {
   235  				return nil
   236  			}
   237  			return err
   238  		}
   239  
   240  		if s.State() != services.Running {
   241  			break // break out of the loop, and send SHUTTING_DOWN message.
   242  		}
   243  
   244  		var resp *schedulerpb.SchedulerToFrontend
   245  
   246  		switch msg.GetType() {
   247  		case schedulerpb.FrontendToSchedulerType_ENQUEUE:
   248  			err = s.enqueueRequest(frontendCtx, frontendAddress, msg)
   249  			switch {
   250  			case err == nil:
   251  				resp = &schedulerpb.SchedulerToFrontend{Status: schedulerpb.SchedulerToFrontendStatus_OK}
   252  			case errors.Is(err, queue.ErrTooManyRequests):
   253  				resp = &schedulerpb.SchedulerToFrontend{Status: schedulerpb.SchedulerToFrontendStatus_TOO_MANY_REQUESTS_PER_TENANT}
   254  			default:
   255  				resp = &schedulerpb.SchedulerToFrontend{Status: schedulerpb.SchedulerToFrontendStatus_ERROR, Error: err.Error()}
   256  			}
   257  
   258  		case schedulerpb.FrontendToSchedulerType_CANCEL:
   259  			s.cancelRequestAndRemoveFromPending(frontendAddress, msg.QueryID)
   260  			resp = &schedulerpb.SchedulerToFrontend{Status: schedulerpb.SchedulerToFrontendStatus_OK}
   261  
   262  		default:
   263  			level.Error(s.log).Log("msg", "unknown request type from frontend", "addr", frontendAddress, "type", msg.GetType())
   264  			return errors.New("unknown request type")
   265  		}
   266  
   267  		err = frontend.Send(resp)
   268  		// Failure to send response results in ending this connection.
   269  		if err != nil {
   270  			return err
   271  		}
   272  	}
   273  
   274  	// Report shutdown back to frontend, so that it can retry with different scheduler. Also stop the frontend loop.
   275  	return frontend.Send(&schedulerpb.SchedulerToFrontend{Status: schedulerpb.SchedulerToFrontendStatus_SHUTTING_DOWN})
   276  }
   277  
   278  func (s *Scheduler) frontendConnected(frontend *connect.BidiStream[schedulerpb.FrontendToScheduler, schedulerpb.SchedulerToFrontend]) (string, context.Context, error) {
   279  	msg, err := frontend.Receive()
   280  	if err != nil {
   281  		return "", nil, err
   282  	}
   283  	if msg.Type != schedulerpb.FrontendToSchedulerType_INIT || msg.FrontendAddress == "" {
   284  		return "", nil, errors.New("no frontend address")
   285  	}
   286  
   287  	s.connectedFrontendsMu.Lock()
   288  	defer s.connectedFrontendsMu.Unlock()
   289  
   290  	cf := s.connectedFrontends[msg.FrontendAddress]
   291  	if cf == nil {
   292  		cf = &connectedFrontend{
   293  			connections: 0,
   294  		}
   295  		cf.ctx, cf.cancel = context.WithCancel(context.Background())
   296  		s.connectedFrontends[msg.FrontendAddress] = cf
   297  	}
   298  
   299  	cf.connections++
   300  	return msg.FrontendAddress, cf.ctx, nil
   301  }
   302  
   303  func (s *Scheduler) frontendDisconnected(frontendAddress string) {
   304  	s.connectedFrontendsMu.Lock()
   305  	defer s.connectedFrontendsMu.Unlock()
   306  
   307  	cf := s.connectedFrontends[frontendAddress]
   308  	cf.connections--
   309  	if cf.connections == 0 {
   310  		delete(s.connectedFrontends, frontendAddress)
   311  		cf.cancel()
   312  	}
   313  }
   314  
   315  func (s *Scheduler) enqueueRequest(frontendContext context.Context, frontendAddr string, msg *schedulerpb.FrontendToScheduler) error {
   316  	// Create new context for this request, to support cancellation.
   317  	ctx, cancel := context.WithCancel(frontendContext)
   318  	shouldCancel := true
   319  	defer func() {
   320  		if shouldCancel {
   321  			cancel()
   322  		}
   323  	}()
   324  
   325  	// Extract tracing information from headers in HTTP request. FrontendContext doesn't have the correct tracing
   326  	// information, since that is a long-running request.
   327  	tracer := opentracing.GlobalTracer()
   328  	parentSpanContext, err := httpgrpcutil.GetParentSpanForRequest(tracer, msg.HttpRequest)
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	userID := msg.GetUserID()
   334  
   335  	req := &schedulerRequest{
   336  		frontendAddress: frontendAddr,
   337  		userID:          msg.UserID,
   338  		queryID:         msg.QueryID,
   339  		request:         msg.HttpRequest,
   340  		statsEnabled:    msg.StatsEnabled,
   341  	}
   342  
   343  	now := time.Now()
   344  
   345  	req.parentSpanContext = parentSpanContext
   346  	req.queueSpan, req.ctx = opentracing.StartSpanFromContextWithTracer(ctx, tracer, "queued", opentracing.ChildOf(parentSpanContext))
   347  	req.enqueueTime = now
   348  	req.ctxCancel = cancel
   349  
   350  	// aggregate the max queriers limit in the case of a multi tenant query
   351  	tenantIDs, err := tenant.TenantIDsFromOrgID(userID)
   352  	if err != nil {
   353  		return err
   354  	}
   355  	maxQueriers := validation.SmallestPositiveNonZeroIntPerTenant(tenantIDs, s.limits.MaxQueriersPerTenant)
   356  
   357  	s.activeUsers.UpdateUserTimestamp(userID, now)
   358  	return s.requestQueue.EnqueueRequest(userID, req, maxQueriers, func() {
   359  		shouldCancel = false
   360  
   361  		s.pendingRequestsMu.Lock()
   362  		s.pendingRequests[requestKey{frontendAddr: frontendAddr, queryID: msg.QueryID}] = req
   363  		s.pendingRequestsMu.Unlock()
   364  	})
   365  }
   366  
   367  // This method doesn't do removal from the queue.
   368  func (s *Scheduler) cancelRequestAndRemoveFromPending(frontendAddr string, queryID uint64) {
   369  	s.pendingRequestsMu.Lock()
   370  	defer s.pendingRequestsMu.Unlock()
   371  
   372  	key := requestKey{frontendAddr: frontendAddr, queryID: queryID}
   373  	req := s.pendingRequests[key]
   374  	if req != nil {
   375  		req.ctxCancel()
   376  	}
   377  
   378  	delete(s.pendingRequests, key)
   379  }
   380  
   381  // BidiStreamCloser is a wrapper around BidiStream that allows to close it.
   382  // Once closed, it will return io.EOF on Receive and Send.
   383  type BidiStreamCloser[Req, Res any] struct {
   384  	stream *connect.BidiStream[Req, Res]
   385  	lock   sync.Mutex
   386  }
   387  
   388  func (c *BidiStreamCloser[Req, Res]) Close() {
   389  	c.lock.Lock()
   390  	defer c.lock.Unlock()
   391  
   392  	if c.stream != nil {
   393  		c.stream = nil
   394  	}
   395  }
   396  
   397  func (c *BidiStreamCloser[Req, Res]) Receive() (*Req, error) {
   398  	c.lock.Lock()
   399  	defer c.lock.Unlock()
   400  
   401  	if c.stream == nil {
   402  		return nil, io.EOF
   403  	}
   404  
   405  	return c.stream.Receive()
   406  }
   407  
   408  func (b *BidiStreamCloser[Req, Res]) Send(msg *Res) error {
   409  	b.lock.Lock()
   410  	defer b.lock.Unlock()
   411  
   412  	if b.stream == nil {
   413  		return io.EOF
   414  	}
   415  	return b.stream.Send(msg)
   416  }
   417  
   418  // QuerierLoop is started by querier to receive queries from scheduler.
   419  func (s *Scheduler) QuerierLoop(ctx context.Context, bidi *connect.BidiStream[schedulerpb.QuerierToScheduler, schedulerpb.SchedulerToQuerier]) error {
   420  	querier := &BidiStreamCloser[schedulerpb.QuerierToScheduler, schedulerpb.SchedulerToQuerier]{
   421  		stream: bidi,
   422  	}
   423  	defer querier.Close()
   424  	resp, err := querier.Receive()
   425  	if err != nil {
   426  		return err
   427  	}
   428  
   429  	querierID := resp.GetQuerierID()
   430  
   431  	s.requestQueue.RegisterQuerierConnection(querierID)
   432  	defer s.requestQueue.UnregisterQuerierConnection(querierID)
   433  
   434  	lastUserIndex := queue.FirstUser()
   435  
   436  	// In stopping state scheduler is not accepting new queries, but still dispatching queries in the queues.
   437  	for s.isRunningOrStopping() {
   438  		req, idx, err := s.requestQueue.GetNextRequestForQuerier(ctx, lastUserIndex, querierID)
   439  		if err != nil {
   440  			// Return a more clear error if the queue is stopped because the query-scheduler is not running.
   441  			if errors.Is(err, queue.ErrStopped) && !s.isRunning() {
   442  				return schedulerpb.ErrSchedulerIsNotRunning
   443  			}
   444  
   445  			return err
   446  		}
   447  		lastUserIndex = idx
   448  
   449  		r := req.(*schedulerRequest)
   450  
   451  		s.queueDuration.Observe(time.Since(r.enqueueTime).Seconds())
   452  		r.queueSpan.Finish()
   453  
   454  		/*
   455  		  We want to dequeue the next unexpired request from the chosen tenant queue.
   456  		  The chance of choosing a particular tenant for dequeueing is (1/active_tenants).
   457  		  This is problematic under load, especially with other middleware enabled such as
   458  		  querier.split-by-interval, where one request may fan out into many.
   459  		  If expired requests aren't exhausted before checking another tenant, it would take
   460  		  n_active_tenants * n_expired_requests_at_front_of_queue requests being processed
   461  		  before an active request was handled for the tenant in question.
   462  		  If this tenant meanwhile continued to queue requests,
   463  		  it's possible that it's own queue would perpetually contain only expired requests.
   464  		*/
   465  
   466  		if r.ctx.Err() != nil {
   467  			// Remove from pending requests.
   468  			s.cancelRequestAndRemoveFromPending(r.frontendAddress, r.queryID)
   469  
   470  			lastUserIndex = lastUserIndex.ReuseLastUser()
   471  			continue
   472  		}
   473  
   474  		if err := s.forwardRequestToQuerier(querier, r); err != nil {
   475  			return err
   476  		}
   477  	}
   478  
   479  	return schedulerpb.ErrSchedulerIsNotRunning
   480  }
   481  
   482  func (s *Scheduler) NotifyQuerierShutdown(ctx context.Context, req *connect.Request[schedulerpb.NotifyQuerierShutdownRequest]) (*connect.Response[schedulerpb.NotifyQuerierShutdownResponse], error) {
   483  	level.Info(s.log).Log("msg", "received shutdown notification from querier", "querier", req.Msg.GetQuerierID())
   484  	s.requestQueue.NotifyQuerierShutdown(req.Msg.GetQuerierID())
   485  
   486  	return connect.NewResponse(&schedulerpb.NotifyQuerierShutdownResponse{}), nil
   487  }
   488  
   489  func (s *Scheduler) forwardRequestToQuerier(querier *BidiStreamCloser[schedulerpb.QuerierToScheduler, schedulerpb.SchedulerToQuerier], req *schedulerRequest) error {
   490  	// Make sure to cancel request at the end to cleanup resources.
   491  	defer s.cancelRequestAndRemoveFromPending(req.frontendAddress, req.queryID)
   492  
   493  	// Handle the stream sending & receiving on a goroutine so we can
   494  	// monitoring the contexts in a select and cancel things appropriately.
   495  	errCh := make(chan error, 1)
   496  	go func() {
   497  		err := querier.Send(&schedulerpb.SchedulerToQuerier{
   498  			UserID:          req.userID,
   499  			QueryID:         req.queryID,
   500  			FrontendAddress: req.frontendAddress,
   501  			HttpRequest:     req.request,
   502  			StatsEnabled:    req.statsEnabled,
   503  		})
   504  		if err != nil {
   505  			errCh <- err
   506  			return
   507  		}
   508  
   509  		_, err = querier.Receive()
   510  		errCh <- err
   511  	}()
   512  
   513  	select {
   514  	case <-req.ctx.Done():
   515  		// If the upstream request is cancelled (eg. frontend issued CANCEL or closed connection),
   516  		// we need to cancel the downstream req. Only way we can do that is to close the stream (by returning error here).
   517  		// Querier is expecting this semantics.
   518  		s.cancelledRequests.WithLabelValues(req.userID).Inc()
   519  		return req.ctx.Err()
   520  
   521  	case err := <-errCh:
   522  		// Is there was an error handling this request due to network IO,
   523  		// then error out this upstream request _and_ stream.
   524  
   525  		if err != nil {
   526  			s.forwardErrorToFrontend(req.ctx, req, err)
   527  		}
   528  		return err
   529  	}
   530  }
   531  
   532  func (s *Scheduler) forwardErrorToFrontend(ctx context.Context, req *schedulerRequest, requestErr error) {
   533  	opts, err := s.cfg.GRPCClientConfig.DialOption([]grpc.UnaryClientInterceptor{
   534  		otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer()),
   535  		middleware.ClientUserHeaderInterceptor,
   536  	},
   537  		nil, nil)
   538  	if err != nil {
   539  		level.Warn(s.log).Log("msg", "failed to create gRPC options for the connection to frontend to report error", "frontend", req.frontendAddress, "err", err, "requestErr", requestErr)
   540  		return
   541  	}
   542  
   543  	opts = append(opts, s.cfg.DialOpts...)
   544  	conn, err := grpc.DialContext(ctx, req.frontendAddress, opts...)
   545  	if err != nil {
   546  		level.Warn(s.log).Log("msg", "failed to create gRPC connection to frontend to report error", "frontend", req.frontendAddress, "err", err, "requestErr", requestErr)
   547  		return
   548  	}
   549  
   550  	defer func() {
   551  		_ = conn.Close()
   552  	}()
   553  
   554  	client := frontendpb.NewFrontendForQuerierClient(conn)
   555  
   556  	userCtx := user.InjectOrgID(ctx, req.userID)
   557  	_, err = client.QueryResult(userCtx, &frontendpb.QueryResultRequest{
   558  		QueryID: req.queryID,
   559  		HttpResponse: &httpgrpc.HTTPResponse{
   560  			Code: http.StatusInternalServerError,
   561  			Body: []byte(requestErr.Error()),
   562  		},
   563  	})
   564  
   565  	if err != nil {
   566  		level.Warn(s.log).Log("msg", "failed to forward error to frontend", "frontend", req.frontendAddress, "err", err, "requestErr", requestErr)
   567  		return
   568  	}
   569  }
   570  
   571  func (s *Scheduler) isRunning() bool {
   572  	st := s.State()
   573  	return st == services.Running
   574  }
   575  
   576  func (s *Scheduler) isRunningOrStopping() bool {
   577  	st := s.State()
   578  	return st == services.Running || st == services.Stopping
   579  }
   580  
   581  func (s *Scheduler) starting(ctx context.Context) error {
   582  	s.subservicesWatcher.WatchManager(s.subservices)
   583  
   584  	if err := services.StartManagerAndAwaitHealthy(ctx, s.subservices); err != nil {
   585  		return errors.Wrap(err, "unable to start scheduler subservices")
   586  	}
   587  
   588  	return nil
   589  }
   590  
   591  func (s *Scheduler) running(ctx context.Context) error {
   592  	// We observe inflight requests frequently and at regular intervals, to have a good
   593  	// approximation of max inflight requests over percentiles of time. We also do it with
   594  	// a ticker so that we keep tracking it even if we have no new queries but stuck inflight
   595  	// requests (e.g. queriers are all crashing).
   596  	inflightRequestsTicker := time.NewTicker(250 * time.Millisecond)
   597  	defer inflightRequestsTicker.Stop()
   598  
   599  	for {
   600  		select {
   601  		case <-inflightRequestsTicker.C:
   602  			s.pendingRequestsMu.Lock()
   603  			inflight := len(s.pendingRequests)
   604  			s.pendingRequestsMu.Unlock()
   605  
   606  			s.inflightRequests.Observe(float64(inflight))
   607  		case <-ctx.Done():
   608  			return nil
   609  		case err := <-s.subservicesWatcher.Chan():
   610  			return errors.Wrap(err, "scheduler subservice failed")
   611  		}
   612  	}
   613  }
   614  
   615  // Close the Scheduler.
   616  func (s *Scheduler) stopping(_ error) error {
   617  	// This will also stop the requests queue, which stop accepting new requests and errors out any pending requests.
   618  	return services.StopManagerAndAwaitStopped(context.Background(), s.subservices)
   619  }
   620  
   621  func (s *Scheduler) cleanupMetricsForInactiveUser(user string) {
   622  	s.queueLength.DeleteLabelValues(user)
   623  	s.discardedRequests.DeleteLabelValues(user)
   624  	s.cancelledRequests.DeleteLabelValues(user)
   625  }
   626  
   627  func (s *Scheduler) getConnectedFrontendClientsMetric() float64 {
   628  	s.connectedFrontendsMu.Lock()
   629  	defer s.connectedFrontendsMu.Unlock()
   630  
   631  	count := 0
   632  	for _, workers := range s.connectedFrontends {
   633  		count += workers.connections
   634  	}
   635  
   636  	return float64(count)
   637  }
   638  
   639  func (s *Scheduler) RingHandler(w http.ResponseWriter, req *http.Request) {
   640  	if s.schedulerLifecycler != nil {
   641  		s.schedulerLifecycler.ServeHTTP(w, req)
   642  		return
   643  	}
   644  
   645  	ringDisabledPage := `
   646  		<!DOCTYPE html>
   647  		<html>
   648  			<head>
   649  				<meta charset="UTF-8">
   650  				<title>Query-scheduler Status</title>
   651  			</head>
   652  			<body>
   653  				<h1>Query-scheduler Status</h1>
   654  				<p>Query-scheduler hash ring is disabled.</p>
   655  			</body>
   656  		</html>`
   657  	util.WriteHTMLResponse(w, ringDisabledPage)
   658  }