github.com/grafana/pyroscope@v1.18.0/pkg/distributor/writepath/router.go (about)

     1  package writepath
     2  
     3  import (
     4  	"context"
     5  	"math/rand"
     6  	"net/http"
     7  	"sync"
     8  	"time"
     9  
    10  	"connectrpc.com/connect"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/grafana/dskit/services"
    14  	"github.com/opentracing/opentracing-go"
    15  	"github.com/pkg/errors"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  
    18  	pushv1 "github.com/grafana/pyroscope/api/gen/proto/go/push/v1"
    19  	segmentwriterv1 "github.com/grafana/pyroscope/api/gen/proto/go/segmentwriter/v1"
    20  	distributormodel "github.com/grafana/pyroscope/pkg/distributor/model"
    21  	"github.com/grafana/pyroscope/pkg/tenant"
    22  	"github.com/grafana/pyroscope/pkg/util"
    23  	"github.com/grafana/pyroscope/pkg/util/connectgrpc"
    24  	"github.com/grafana/pyroscope/pkg/util/delayhandler"
    25  	httputil "github.com/grafana/pyroscope/pkg/util/http"
    26  )
    27  
    28  type SegmentWriterClient interface {
    29  	Push(context.Context, *segmentwriterv1.PushRequest) (*segmentwriterv1.PushResponse, error)
    30  }
    31  
    32  type IngesterClient interface {
    33  	Push(context.Context, *distributormodel.ProfileSeries) (*connect.Response[pushv1.PushResponse], error)
    34  }
    35  
    36  type IngesterFunc func(
    37  	context.Context,
    38  	*distributormodel.ProfileSeries,
    39  ) (*connect.Response[pushv1.PushResponse], error)
    40  
    41  func (f IngesterFunc) Push(
    42  	ctx context.Context,
    43  	req *distributormodel.ProfileSeries,
    44  ) (*connect.Response[pushv1.PushResponse], error) {
    45  	return f(ctx, req)
    46  }
    47  
    48  type Router struct {
    49  	service  services.Service
    50  	inflight sync.WaitGroup
    51  
    52  	logger  log.Logger
    53  	metrics *metrics
    54  
    55  	ingester  IngesterClient
    56  	segwriter IngesterClient
    57  }
    58  
    59  func NewRouter(
    60  	logger log.Logger,
    61  	registerer prometheus.Registerer,
    62  	ingester IngesterClient,
    63  	segwriter IngesterClient,
    64  ) *Router {
    65  	r := &Router{
    66  		logger:    logger,
    67  		metrics:   newMetrics(registerer),
    68  		ingester:  ingester,
    69  		segwriter: segwriter,
    70  	}
    71  	r.service = services.NewBasicService(r.starting, r.running, r.stopping)
    72  	return r
    73  }
    74  
    75  func (m *Router) Service() services.Service { return m.service }
    76  
    77  func (m *Router) starting(context.Context) error { return nil }
    78  
    79  func (m *Router) stopping(_ error) error {
    80  	// We expect that no requests are routed after the stopping call.
    81  	m.inflight.Wait()
    82  	return nil
    83  }
    84  
    85  func (m *Router) running(ctx context.Context) error {
    86  	<-ctx.Done()
    87  	return nil
    88  }
    89  
    90  func (m *Router) Send(ctx context.Context, req *distributormodel.ProfileSeries, config Config) error {
    91  	sp, ctx := opentracing.StartSpanFromContext(ctx, "Router.Send")
    92  	defer sp.Finish()
    93  	if config.AsyncIngest {
    94  		delayhandler.CancelDelay(ctx)
    95  	}
    96  	switch config.WritePath {
    97  	case SegmentWriterPath:
    98  		return m.sendToSegmentWriterOnly(ctx, req, &config)
    99  	case CombinedPath:
   100  		return m.sendToBoth(ctx, req, &config)
   101  	default:
   102  		return m.sendToIngesterOnly(ctx, req)
   103  	}
   104  }
   105  
   106  func (m *Router) ingesterRoute() *route {
   107  	return &route{
   108  		path:    IngesterPath,
   109  		primary: true, // Ingester is always the primary route.
   110  		client:  m.ingester,
   111  	}
   112  }
   113  
   114  func (m *Router) segwriterRoute(primary bool) *route {
   115  	return &route{
   116  		path:    SegmentWriterPath,
   117  		primary: primary,
   118  		client:  m.segwriter,
   119  	}
   120  }
   121  
   122  func (m *Router) sendToBoth(ctx context.Context, req *distributormodel.ProfileSeries, config *Config) error {
   123  	r := rand.Float64() // [0.0, 1.0)
   124  	shouldIngester := config.IngesterWeight > 0.0 && config.IngesterWeight >= r
   125  	shouldSegwriter := config.SegmentWriterWeight > 0.0 && config.SegmentWriterWeight >= r
   126  
   127  	// Client sees errors and latency of the primary write
   128  	// path, secondary write path does not affect the client.
   129  	var ingester, segwriter *route
   130  	if shouldIngester {
   131  		// If the request is sent to ingester (regardless of anything),
   132  		// the response is returned to the client immediately after the old
   133  		// write path returns. Failure of the new write path should be logged
   134  		// and counted in metrics but NOT returned to the client.
   135  		ingester = m.ingesterRoute()
   136  		if !shouldSegwriter {
   137  			return m.send(ingester)(ctx, req)
   138  		}
   139  	}
   140  	if shouldSegwriter {
   141  		segwriter = m.segwriterRoute(!shouldIngester)
   142  		if segwriter.primary && !config.AsyncIngest {
   143  			// The request is sent to segment-writer exclusively, and the client
   144  			// must block until the response returns.
   145  			// Failure of the new write is returned to the client.
   146  			// Failure of the old write path is NOT returned to the client.
   147  			return m.send(segwriter)(ctx, req)
   148  		}
   149  		// Request to the segment writer will be sent asynchronously.
   150  	}
   151  
   152  	// No write routes. This is possible if the write path is configured
   153  	// to "combined" and both weights are set to 0.0.
   154  	if ingester == nil && segwriter == nil {
   155  		return nil
   156  	}
   157  
   158  	if segwriter != nil && ingester != nil {
   159  		// The request is to be sent to both asynchronously, therefore we're
   160  		// cloning it. We do not wait for the secondary request to complete.
   161  		// On shutdown, however, we will wait for all inflight requests.
   162  		segwriter.client = m.detachedClient(ctx, req.Clone(), segwriter.client, config)
   163  	}
   164  
   165  	if segwriter != nil {
   166  		m.sendAsync(ctx, req, segwriter)
   167  	}
   168  
   169  	if ingester != nil {
   170  		select {
   171  		case err := <-m.sendAsync(ctx, req, ingester):
   172  			return err
   173  		case <-ctx.Done():
   174  			return ctx.Err()
   175  		}
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  func (m *Router) sendToSegmentWriterOnly(ctx context.Context, req *distributormodel.ProfileSeries, config *Config) error {
   182  	r := m.segwriterRoute(true)
   183  	if !config.AsyncIngest {
   184  		return m.send(r)(ctx, req)
   185  	}
   186  	r.client = m.detachedClient(ctx, req, r.client, config)
   187  	m.sendAsync(ctx, req, r)
   188  	return nil
   189  }
   190  
   191  func (m *Router) sendToIngesterOnly(ctx context.Context, req *distributormodel.ProfileSeries) error {
   192  	// NOTE(kolesnikovae): If we also want to support async requests to ingesters,
   193  	// we should implement it here and in sendToBoth.
   194  	return m.send(m.ingesterRoute())(ctx, req)
   195  }
   196  
   197  type sendFunc func(context.Context, *distributormodel.ProfileSeries) error
   198  
   199  type route struct {
   200  	path    WritePath // IngesterPath | SegmentWriterPath
   201  	client  IngesterClient
   202  	primary bool
   203  }
   204  
   205  // detachedClient creates a new IngesterFunc that wraps the call with a local context
   206  // that has a timeout and tenant ID injected so it can be used for asynchronous requests.
   207  func (m *Router) detachedClient(ctx context.Context, req *distributormodel.ProfileSeries, client IngesterClient, config *Config) IngesterFunc {
   208  	return func(context.Context, *distributormodel.ProfileSeries) (*connect.Response[pushv1.PushResponse], error) {
   209  		localCtx, cancel := context.WithTimeout(context.Background(), config.SegmentWriterTimeout)
   210  		localCtx = tenant.InjectTenantID(localCtx, req.TenantID)
   211  		if sp := opentracing.SpanFromContext(ctx); sp != nil {
   212  			localCtx = opentracing.ContextWithSpan(localCtx, sp)
   213  		}
   214  		defer cancel()
   215  		return client.Push(localCtx, req)
   216  	}
   217  }
   218  
   219  func (m *Router) sendAsync(ctx context.Context, req *distributormodel.ProfileSeries, r *route) <-chan error {
   220  	c := make(chan error, 1)
   221  	m.inflight.Add(1)
   222  	go func() {
   223  		defer m.inflight.Done()
   224  		c <- m.send(r)(ctx, req)
   225  	}()
   226  	return c
   227  }
   228  
   229  func (m *Router) send(r *route) sendFunc {
   230  	return func(ctx context.Context, req *distributormodel.ProfileSeries) (err error) {
   231  		start := time.Now()
   232  		defer func() {
   233  			if p := recover(); p != nil {
   234  				err = util.PanicError(p)
   235  			}
   236  			// Note that the upstream expects "connect" codes.
   237  			code := http.StatusOK // HTTP status code.
   238  			if err != nil {
   239  				var connectErr *connect.Error
   240  				if ok := errors.As(err, &connectErr); ok {
   241  					// connect errors are passed as is, we only
   242  					// identify the HTTP status code.
   243  					code = int(connectgrpc.CodeToHTTP(connectErr.Code()))
   244  				} else {
   245  					// We identify the HTTP status code based on the
   246  					// error and then convert the error to connect error.
   247  					code, _ = httputil.ClientHTTPStatusAndError(err)
   248  					err = connect.NewError(connectgrpc.HTTPToCode(int32(code)), err)
   249  				}
   250  			}
   251  			m.metrics.durationHistogram.
   252  				WithLabelValues(newDurationHistogramDims(r, code)...).
   253  				Observe(time.Since(start).Seconds())
   254  		}()
   255  		_, err = r.client.Push(ctx, req)
   256  		return err
   257  	}
   258  }