Skip to content
This repository was archived by the owner on Jul 19, 2023. It is now read-only.

Commit 05b16c9

Browse files
committed
Improve Worker timeout (#506)
* Setup timeout for frontend and querier worker * Increase timeout by 2 when using query-scheduler * Review feedback
1 parent 8f4777c commit 05b16c9

File tree

5 files changed

+127
-58
lines changed

5 files changed

+127
-58
lines changed

pkg/frontend/frontend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type Config struct {
5252

5353
// This configuration is injected internally.
5454
QuerySchedulerDiscovery schedulerdiscovery.Config `yaml:"-"`
55+
MaxLoopDuration time.Duration `yaml:"-"`
5556
}
5657

5758
func (cfg *Config) RegisterFlags(f *flag.FlagSet, logger log.Logger) {

pkg/frontend/frontend_scheduler_worker.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package frontend
88
import (
99
"context"
1010
"io"
11+
"math/rand"
1112
"net/http"
1213
"sync"
1314
"time"
@@ -28,6 +29,10 @@ import (
2829
"github.com/grafana/phlare/pkg/util/servicediscovery"
2930
)
3031

32+
func init() {
33+
rand.Seed(time.Now().UnixNano())
34+
}
35+
3136
const (
3237
schedulerAddressLabel = "scheduler_address"
3338
// schedulerWorkerCancelChanCapacity should be at least as big as the number of sub-queries issued by a single query
@@ -135,7 +140,7 @@ func (f *frontendSchedulerWorkers) addScheduler(address string) {
135140
}
136141

137142
// No worker for this address yet, start a new one.
138-
w = newFrontendSchedulerWorker(conn, address, f.frontendAddress, f.requestsCh, f.cfg.WorkerConcurrency, f.enqueuedRequests.WithLabelValues(address), f.log)
143+
w = newFrontendSchedulerWorker(conn, address, f.frontendAddress, f.requestsCh, f.cfg.WorkerConcurrency, f.enqueuedRequests.WithLabelValues(address), f.cfg.MaxLoopDuration, f.log)
139144

140145
f.mu.Lock()
141146
defer f.mu.Unlock()
@@ -229,9 +234,11 @@ type frontendSchedulerWorker struct {
229234

230235
// Number of queries sent to this scheduler.
231236
enqueuedRequests prometheus.Counter
237+
238+
maxLoopDuration time.Duration
232239
}
233240

234-
func newFrontendSchedulerWorker(conn *grpc.ClientConn, schedulerAddr string, frontendAddr string, requestCh <-chan *frontendRequest, concurrency int, enqueuedRequests prometheus.Counter, log log.Logger) *frontendSchedulerWorker {
241+
func newFrontendSchedulerWorker(conn *grpc.ClientConn, schedulerAddr string, frontendAddr string, requestCh <-chan *frontendRequest, concurrency int, enqueuedRequests prometheus.Counter, maxLoopDuration time.Duration, log log.Logger) *frontendSchedulerWorker {
235242
w := &frontendSchedulerWorker{
236243
log: log,
237244
conn: conn,
@@ -241,6 +248,7 @@ func newFrontendSchedulerWorker(conn *grpc.ClientConn, schedulerAddr string, fro
241248
requestCh: requestCh,
242249
cancelCh: make(chan uint64, schedulerWorkerCancelChanCapacity),
243250
enqueuedRequests: enqueuedRequests,
251+
maxLoopDuration: maxLoopDuration,
244252
}
245253
w.ctx, w.cancel = context.WithCancel(context.Background())
246254

@@ -308,6 +316,11 @@ func (w *frontendSchedulerWorker) runOne(ctx context.Context, client schedulerpb
308316
}
309317
}
310318

319+
func jitter(d time.Duration, factor float64) time.Duration {
320+
maxJitter := time.Duration(float64(d) * factor)
321+
return d - time.Duration(rand.Int63n(int64(maxJitter)))
322+
}
323+
311324
func (w *frontendSchedulerWorker) schedulerLoop(loop schedulerpb.SchedulerForFrontend_FrontendLoopClient) error {
312325
if err := loop.Send(&schedulerpb.FrontendToScheduler{
313326
Type: schedulerpb.FrontendToSchedulerType_INIT,
@@ -323,7 +336,22 @@ func (w *frontendSchedulerWorker) schedulerLoop(loop schedulerpb.SchedulerForFro
323336
return errors.Errorf("unexpected status received for init: %v", resp.Status)
324337
}
325338

326-
ctx := loop.Context()
339+
ctx, cancel := context.WithCancel(loop.Context())
340+
defer cancel()
341+
if w.maxLoopDuration > 0 {
342+
go func() {
343+
timer := time.NewTimer(jitter(w.maxLoopDuration, 0.3))
344+
defer timer.Stop()
345+
346+
select {
347+
case <-ctx.Done():
348+
return
349+
case <-timer.C:
350+
cancel()
351+
return
352+
}
353+
}()
354+
}
327355

328356
for {
329357
select {
@@ -335,7 +363,6 @@ func (w *frontendSchedulerWorker) schedulerLoop(loop schedulerpb.SchedulerForFro
335363
// Reporting error here would delay reopening the stream (if the worker context is not done yet).
336364
level.Debug(w.log).Log("msg", "stream context finished", "err", ctx.Err())
337365
return nil
338-
339366
case req := <-w.requestCh:
340367
err := loop.Send(&schedulerpb.FrontendToScheduler{
341368
Type: schedulerpb.FrontendToSchedulerType_ENQUEUE,

pkg/phlare/modules.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"time"
78

8-
"github.com/bufbuild/connect-go"
99
"github.com/felixge/fgprof"
1010
"github.com/go-kit/log"
1111
"github.com/go-kit/log/level"
@@ -116,28 +116,24 @@ func (f *Phlare) initQueryScheduler() (services.Service, error) {
116116
return nil, errors.Wrap(err, "query-scheduler init")
117117
}
118118
schedulerpbconnect.RegisterSchedulerForFrontendHandler(f.Server.HTTP, s)
119-
schedulerpbconnect.RegisterSchedulerForQuerierHandler(f.Server.HTTP, s, f.schedulerQuerierTimeout())
119+
schedulerpbconnect.RegisterSchedulerForQuerierHandler(f.Server.HTTP, s)
120120
return s, nil
121121
}
122122

123-
// schedulerQuerierTimeout returns a HandlerOption that sets the timeout for the
124-
// communication between the scheduler and the querier.
125-
// This is required because connect streaming handler does not propagate timeouts
126-
// through the context.
127-
// Adding a timeout options to the handler enforce the timeout to be propagated
128-
// and cancel the stream if the timeout is reached.
129-
// Querier expects this and will gracefully reconnects.
130-
func (f *Phlare) schedulerQuerierTimeout() connect.HandlerOption {
131-
opts := []connect.HandlerOption{}
123+
// setupWorkerTimeout sets the max loop duration for the querier worker and frontend worker
124+
// to 90% of the read or write http timeout, whichever is smaller.
125+
// This is to ensure that the worker doesn't timeout before the http handler and that the connection
126+
// is refreshed.
127+
func (f *Phlare) setupWorkerTimeout() {
132128
timeout := f.Cfg.Server.HTTPServerReadTimeout
133129
if f.Cfg.Server.HTTPServerWriteTimeout < timeout {
134130
timeout = f.Cfg.Server.HTTPServerWriteTimeout
135131
}
136132

137133
if timeout > 0 {
138-
opts = append(opts, connect.WithInterceptors(util.WithTimeout(timeout)))
134+
f.Cfg.Worker.MaxLoopDuration = time.Duration(float64(timeout) * 0.9)
135+
f.Cfg.Frontend.MaxLoopDuration = time.Duration(float64(timeout) * 0.9)
139136
}
140-
return connect.WithHandlerOptions(opts...)
141137
}
142138

143139
func (f *Phlare) initQuerier() (services.Service, error) {
@@ -309,6 +305,12 @@ func (f *Phlare) initServer() (services.Service, error) {
309305
// see https://github.com/grafana/phlare/issues/231
310306
f.Cfg.Server.DoNotAddDefaultHTTPMiddleware = true
311307

308+
f.setupWorkerTimeout()
309+
if f.isModuleActive(QueryScheduler) {
310+
// to ensure that the query scheduler is always able to handle the request, we need to double the timeout
311+
f.Cfg.Server.HTTPServerReadTimeout = 2 * f.Cfg.Server.HTTPServerReadTimeout
312+
f.Cfg.Server.HTTPServerWriteTimeout = 2 * f.Cfg.Server.HTTPServerWriteTimeout
313+
}
312314
serv, err := server.New(f.Cfg.Server)
313315
if err != nil {
314316
return nil, err

pkg/querier/worker/scheduler_processor.go

Lines changed: 79 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package worker
88
import (
99
"context"
1010
"fmt"
11+
"math/rand"
1112
"net/http"
1213
"strings"
1314
"time"
@@ -37,18 +38,23 @@ import (
3738
"github.com/grafana/phlare/pkg/util/httpgrpcutil"
3839
)
3940

41+
func init() {
42+
rand.Seed(time.Now().UnixNano())
43+
}
44+
4045
var processorBackoffConfig = backoff.Config{
4146
MinBackoff: 250 * time.Millisecond,
4247
MaxBackoff: 2 * time.Second,
4348
}
4449

4550
func newSchedulerProcessor(cfg Config, handler RequestHandler, log log.Logger, reg prometheus.Registerer) (*schedulerProcessor, []services.Service) {
4651
p := &schedulerProcessor{
47-
log: log,
48-
handler: handler,
49-
maxMessageSize: cfg.GRPCClientConfig.MaxSendMsgSize,
50-
querierID: cfg.QuerierID,
51-
grpcConfig: cfg.GRPCClientConfig,
52+
log: log,
53+
handler: handler,
54+
maxMessageSize: cfg.GRPCClientConfig.MaxSendMsgSize,
55+
querierID: cfg.QuerierID,
56+
grpcConfig: cfg.GRPCClientConfig,
57+
maxLoopDuration: cfg.MaxLoopDuration,
5258

5359
schedulerClientFactory: func(conn *grpc.ClientConn) schedulerpb.SchedulerForQuerierClient {
5460
return schedulerpb.NewSchedulerForQuerierClient(conn)
@@ -78,11 +84,12 @@ func newSchedulerProcessor(cfg Config, handler RequestHandler, log log.Logger, r
7884

7985
// Handles incoming queries from query-scheduler.
8086
type schedulerProcessor struct {
81-
log log.Logger
82-
handler RequestHandler
83-
grpcConfig grpcclient.Config
84-
maxMessageSize int
85-
querierID string
87+
log log.Logger
88+
handler RequestHandler
89+
grpcConfig grpcclient.Config
90+
maxMessageSize int
91+
querierID string
92+
maxLoopDuration time.Duration
8693

8794
frontendPool *client.Pool
8895
frontendClientRequestDuration *prometheus.HistogramVec
@@ -111,42 +118,68 @@ func (sp *schedulerProcessor) processQueriesOnSingleStream(workerCtx context.Con
111118

112119
backoff := backoff.New(execCtx, processorBackoffConfig)
113120
for backoff.Ongoing() {
114-
c, err := schedulerClient.QuerierLoop(execCtx)
115-
if err == nil {
116-
err = c.Send(&schedulerpb.QuerierToScheduler{QuerierID: sp.querierID})
117-
}
118-
119-
if err != nil {
120-
level.Warn(sp.log).Log("msg", "error contacting scheduler", "err", err, "addr", address)
121-
backoff.Wait()
122-
continue
123-
}
124-
125-
if err := sp.querierLoop(c, address, inflightQuery); err != nil {
126-
// Do not log an error is the query-scheduler is shutting down.
127-
if s, ok := status.FromError(err); !ok ||
128-
(!strings.Contains(s.Message(), schedulerpb.ErrSchedulerIsNotRunning.Error()) &&
129-
!strings.Contains(s.Message(), context.DeadlineExceeded.Error()) &&
130-
!strings.Contains(s.Message(), "stream terminated")) {
131-
level.Error(sp.log).Log("msg", "error processing requests from scheduler", "err", err, "addr", address)
132-
}
133-
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) || strings.Contains(err.Error(), "stream terminated") {
134-
backoff.Reset()
135-
continue
121+
func() {
122+
if err := sp.querierLoop(execCtx, schedulerClient, address, inflightQuery); err != nil {
123+
// Do not log an error is the query-scheduler is shutting down.
124+
if s, ok := status.FromError(err); !ok ||
125+
(!strings.Contains(s.Message(), schedulerpb.ErrSchedulerIsNotRunning.Error()) &&
126+
!strings.Contains(s.Message(), context.Canceled.Error()) &&
127+
!strings.Contains(s.Message(), "stream terminated")) {
128+
level.Error(sp.log).Log("msg", "error processing requests from scheduler", "err", err, "addr", address)
129+
}
130+
if strings.Contains(err.Error(), context.Canceled.Error()) || strings.Contains(err.Error(), "stream terminated") {
131+
backoff.Reset()
132+
return
133+
}
134+
backoff.Wait()
135+
return
136136
}
137-
backoff.Wait()
138-
continue
139-
}
140137

141-
backoff.Reset()
138+
backoff.Reset()
139+
}()
142140
}
143141
}
144142

145143
// process loops processing requests on an established stream.
146-
func (sp *schedulerProcessor) querierLoop(c schedulerpb.SchedulerForQuerier_QuerierLoopClient, address string, inflightQuery *atomic.Bool) error {
147-
// Build a child context so we can cancel a query when the stream is closed.
148-
ctx, cancel := context.WithCancel(c.Context())
149-
defer cancel()
144+
func (sp *schedulerProcessor) querierLoop(parentCtx context.Context, schedulerClient schedulerpb.SchedulerForQuerierClient, address string, inflightQuery *atomic.Bool) error {
145+
loopCtx, loopCancel := context.WithCancel(parentCtx)
146+
defer loopCancel()
147+
148+
if sp.maxLoopDuration > 0 {
149+
go func() {
150+
timer := time.NewTimer(jitter(sp.maxLoopDuration, 0.3))
151+
defer timer.Stop()
152+
153+
select {
154+
case <-timer.C:
155+
level.Debug(sp.log).Log("msg", "waiting for inflight queries to complete")
156+
for inflightQuery.Load() {
157+
select {
158+
case <-parentCtx.Done():
159+
// In the meanwhile, the execution context has been explicitly canceled, so we should just terminate.
160+
return
161+
default:
162+
// Wait and check again inflight queries.
163+
time.Sleep(100 * time.Millisecond)
164+
}
165+
}
166+
level.Debug(sp.log).Log("msg", "refreshing scheduler connection")
167+
loopCancel()
168+
case <-parentCtx.Done():
169+
return
170+
}
171+
}()
172+
}
173+
174+
c, err := schedulerClient.QuerierLoop(loopCtx)
175+
if err == nil {
176+
err = c.Send(&schedulerpb.QuerierToScheduler{QuerierID: sp.querierID})
177+
}
178+
179+
if err != nil {
180+
level.Warn(sp.log).Log("msg", "error contacting scheduler", "err", err, "addr", address)
181+
return err
182+
}
150183

151184
for {
152185
request, err := c.Recv()
@@ -165,7 +198,7 @@ func (sp *schedulerProcessor) querierLoop(c schedulerpb.SchedulerForQuerier_Quer
165198
defer inflightQuery.Store(false)
166199

167200
// We need to inject user into context for sending response back.
168-
ctx := user.InjectOrgID(ctx, request.UserID)
201+
ctx := user.InjectOrgID(c.Context(), request.UserID)
169202

170203
tracer := opentracing.GlobalTracer()
171204
// Ignore errors here. If we cannot get parent span, we just don't create new one.
@@ -188,6 +221,11 @@ func (sp *schedulerProcessor) querierLoop(c schedulerpb.SchedulerForQuerier_Quer
188221
}
189222
}
190223

224+
func jitter(d time.Duration, factor float64) time.Duration {
225+
maxJitter := time.Duration(float64(d) * factor)
226+
return d - time.Duration(rand.Int63n(int64(maxJitter)))
227+
}
228+
191229
func (sp *schedulerProcessor) runRequest(ctx context.Context, logger log.Logger, queryID uint64, frontendAddress string, statsEnabled bool, request *httpgrpc.HTTPRequest) {
192230
var stats *querier_stats.Stats
193231
if statsEnabled {

pkg/querier/worker/worker.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Config struct {
3535
// This configuration is injected internally.
3636
MaxConcurrentRequests int `yaml:"-"` // Must be same as passed to PromQL Engine.
3737
QuerySchedulerDiscovery schedulerdiscovery.Config `yaml:"-"`
38+
MaxLoopDuration time.Duration `yaml:"-"`
3839
}
3940

4041
func (cfg *Config) RegisterFlags(f *flag.FlagSet) {

0 commit comments

Comments
 (0)