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 }