Skip to content

Commit 8009563

Browse files
feat: restart delay period
The restart delay period should allow the attempt to be reset if the worker is restarted after that time. This fixes the issue where we might see the attempt climbing even though the restarts over a long period of time. This is backwards compatible with the v4 version, as the restart delay is a constant, so the attempt is disguarded.
1 parent 936b7ce commit 8009563

2 files changed

Lines changed: 77 additions & 39 deletions

File tree

runner.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ const (
2626

2727
// DelayFunc is the type alias of the function that returns the delay between
2828
// worker restarts. It is called with the number of attempts that have been made
29-
// to start the worker, the time the worker was last restarted, and the error
30-
// that caused the worker to exit.
29+
// since the last window period. The lastErr argument is the error that caused
30+
// the worker to exit. Depending on the error, the delay can be adjusted.
3131
//
3232
// The worker id is not passed to the function because we shouldn't to prevent
3333
// the function from being able to make decisions based on the worker id.
34-
type DelayFunc = func(attempts int, lastRestartedTime time.Time, lastErr error) time.Duration
34+
type DelayFunc = func(attempts int, lastErr error) time.Duration
3535

3636
// StartFunc is the type alias of the function that creates a worker.
3737
type StartFunc = func(context.Context) (Worker, error)
@@ -191,6 +191,10 @@ type RunnerParams struct {
191191
// If this is nil, DefaultRestartDelay will be used.
192192
RestartDelay DelayFunc
193193

194+
// RestartDelayPeriod is the length of time that the restart delay
195+
// is calculated over.
196+
RestartDelayPeriod time.Duration
197+
194198
// Clock is used for timekeeping. If it's nil, clock.WallClock
195199
// will be used.
196200
Clock Clock
@@ -239,10 +243,13 @@ func NewRunner(p RunnerParams) (*Runner, error) {
239243
}
240244
}
241245
if p.RestartDelay == nil {
242-
p.RestartDelay = func(attempts int, lastRestartedTime time.Time, lastErr error) time.Duration {
246+
p.RestartDelay = func(attempts int, lastErr error) time.Duration {
243247
return DefaultRestartDelay
244248
}
245249
}
250+
if p.RestartDelayPeriod == 0 {
251+
p.RestartDelayPeriod = 10 * time.Minute
252+
}
246253
if p.Clock == nil {
247254
p.Clock = clock.WallClock
248255
}
@@ -578,12 +585,20 @@ func (runner *Runner) workerDone(info doneInfo) {
578585
return
579586
}
580587

581-
// The worker has exited with a non-fatal error.
582-
// We'll restart it after a delay, increment the attempts, so it's
583-
// possible to track how many times the worker has been restarted.
584-
delay := workerInfo.restartDelay(workerInfo.attempts, workerInfo.restarted, info.err)
585-
workerInfo.attempts++
586-
workerInfo.restarted = params.Clock.Now().UTC()
588+
// The worker has exited with a non-fatal error. We'll restart it after a
589+
// delay, increment the attempts, so it's possible to track how many times
590+
// the worker has been restarted over the restart delay period.
591+
now := params.Clock.Now().UTC()
592+
593+
// If the worker has been restarted after the restart delay period, reset
594+
// the attempts counter. Otherwise, increment the attempts counter.
595+
if now.After(workerInfo.restarted.Add(params.RestartDelayPeriod)) {
596+
workerInfo.attempts = 0
597+
} else {
598+
workerInfo.attempts++
599+
}
600+
delay := workerInfo.restartDelay(workerInfo.attempts, info.err)
601+
workerInfo.restarted = now
587602

588603
// Kick off the worker again taking into account the delay.
589604
pprof.Do(runner.tomb.Context(context.Background()), workerInfo.labels, func(ctx context.Context) {

runner_test.go

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ func (*RunnerSuite) TestOneWorkerRestartDelayAfterFailedAttempts(c *gc.C) {
370370
runner, err := worker.NewRunner(worker.RunnerParams{
371371
Name: "test",
372372
IsFatal: noneFatal,
373-
RestartDelay: func(attempts int, lastRestart time.Time, restartErr error) time.Duration {
373+
RestartDelay: func(attempts int, restartErr error) time.Duration {
374374
received.Add(int64(attempts))
375375

376376
go func(restartErr error) {
@@ -411,31 +411,22 @@ func (*RunnerSuite) TestOneWorkerRestartDelayAfterFailedAttempts(c *gc.C) {
411411
}
412412
c.Assert(worker.Stop(runner), gc.IsNil)
413413

414-
c.Check(received.Load(), gc.Equals, int64(1))
414+
c.Check(received.Load(), gc.Equals, int64(0))
415415
}
416416

417-
func (*RunnerSuite) TestOneWorkerRestartDelayAfterFailedAttemptsLastRestart(c *gc.C) {
417+
func (*RunnerSuite) TestOneWorkerRestartDelayAfterFailedAttemptsLargeDelayPeriod(c *gc.C) {
418418
var received atomic.Int64
419419

420-
restarts := make(chan time.Time)
421-
422420
const delay = 100 * time.Millisecond
423421
runner, err := worker.NewRunner(worker.RunnerParams{
424422
Name: "test",
425423
IsFatal: noneFatal,
426-
RestartDelay: func(attempts int, lastRestart time.Time, restartErr error) time.Duration {
424+
RestartDelay: func(attempts int, restartErr error) time.Duration {
427425
received.Add(int64(attempts))
428426

429-
go func(lastRestart time.Time) {
430-
select {
431-
case restarts <- lastRestart:
432-
case <-time.After(shortWait):
433-
c.Fatalf("timed out sending restart error")
434-
}
435-
}(lastRestart)
436-
437427
return delay
438428
},
429+
RestartDelayPeriod: delay * 2,
439430
})
440431
c.Assert(err, jc.ErrorIsNil)
441432

@@ -448,29 +439,61 @@ func (*RunnerSuite) TestOneWorkerRestartDelayAfterFailedAttemptsLastRestart(c *g
448439
starter.die <- errors.New("fatal error")
449440
starter.assertStarted(c, false)
450441

451-
select {
452-
case restart := <-restarts:
453-
// Initial restart time will be empty, as no restart has happened.
454-
c.Check(restart, gc.Equals, time.Time{})
455-
case <-time.After(shortWait):
456-
c.Fatalf("timed out waiting for restart error ")
457-
}
442+
// First attempt should always be zero.
443+
c.Check(received.Load(), gc.Equals, int64(0))
458444

459445
starter.assertStarted(c, true)
460446
starter.die <- errors.New("fatal error")
461447
starter.assertStarted(c, false)
462448

463-
select {
464-
case restart := <-restarts:
465-
c.Check(restart.IsZero(), jc.IsFalse)
466-
case <-time.After(shortWait):
467-
c.Fatalf("timed out waiting for restart error ")
468-
}
449+
starter.assertStarted(c, true)
450+
451+
c.Assert(worker.Stop(runner), gc.IsNil)
452+
453+
// Second attempt with a large delay period should not reset the attempt
454+
// counter.
455+
c.Check(received.Load(), gc.Equals, int64(1))
456+
}
457+
458+
func (*RunnerSuite) TestOneWorkerRestartDelayAfterFailedAttemptsSmallDelayPeriod(c *gc.C) {
459+
var received atomic.Int64
460+
461+
const delay = 100 * time.Millisecond
462+
runner, err := worker.NewRunner(worker.RunnerParams{
463+
Name: "test",
464+
IsFatal: noneFatal,
465+
RestartDelay: func(attempts int, restartErr error) time.Duration {
466+
received.Add(int64(attempts))
467+
468+
return delay
469+
},
470+
RestartDelayPeriod: delay / 2,
471+
})
472+
c.Assert(err, jc.ErrorIsNil)
473+
474+
starter := newTestWorkerStarter()
475+
476+
err = runner.StartWorker(context.Background(), "id", starter.start)
477+
c.Assert(err, jc.ErrorIsNil)
478+
479+
starter.assertStarted(c, true)
480+
starter.die <- errors.New("fatal error")
481+
starter.assertStarted(c, false)
482+
483+
// First attempt should always be zero.
484+
c.Check(received.Load(), gc.Equals, int64(0))
485+
486+
starter.assertStarted(c, true)
487+
starter.die <- errors.New("fatal error")
488+
starter.assertStarted(c, false)
469489

470490
starter.assertStarted(c, true)
471491

472492
c.Assert(worker.Stop(runner), gc.IsNil)
473-
c.Check(received.Load(), gc.Equals, int64(3))
493+
494+
// Second attempt with a small delay period should reset the attempt
495+
// counter.
496+
c.Check(received.Load(), gc.Equals, int64(0))
474497
}
475498

476499
func (*RunnerSuite) TestOneWorkerShouldNotRestart(c *gc.C) {
@@ -1193,7 +1216,7 @@ func (checker *containsChecker) Check(params []interface{}, names []string) (boo
11931216
}
11941217

11951218
func constDelay(d time.Duration) worker.DelayFunc {
1196-
return func(attempts int, lastRestartTime time.Time, lastError error) time.Duration {
1219+
return func(attempts int, lastError error) time.Duration {
11971220
return d
11981221
}
11991222
}

0 commit comments

Comments
 (0)