<commit_before>package client

import (
	"context"
	"time"

	"github.com/datawire/dlib/dlog"
	"github.com/datawire/dlib/dutil"
	"github.com/pkg/errors"
)

const defaultRetryDelay = 100 * time.Millisecond
const defaultMaxDelay = 3 * time.Second

func Retry(c context.Context, f func(context.Context) error, durations ...time.Duration) error {
	delay := defaultRetryDelay
	maxDelay := defaultMaxDelay
	maxTime := time.Duration(0)

	switch len(durations) {
	case 3:
		maxTime = durations[2]
		fallthrough
	case 2:
		maxDelay = durations[1]
		if maxDelay == 0 {
			maxDelay = defaultMaxDelay
		}
		fallthrough
	case 1:
		delay = durations[0]
		if delay == 0 {
			delay = defaultRetryDelay
		}
	}

	if maxDelay < delay {
		maxDelay = delay
	}
	var endTime <-chan time.Time
	if maxTime > 0 {
		endTime = time.After(maxTime)
	} else {
		endTime = make(chan time.Time)
	}

	r := make(chan error)
	for {
		fatal := false
		go func() {
			defer func() {
				if err := dutil.PanicToError(recover()); err != nil {
					fatal = true
					r <- err
				}
			}()
			r <- f(c)
		}()
		select {
		case err := <-r:
			if fatal || err == nil {
				return err
			}
			// Logging at higher log levels should be done in the called function
			dlog.Trace(c, err.Error())
		case <-c.Done():
			return nil
		case <-endTime:
			return errors.New("retry timed out")
		}
		time.Sleep(delay)
		delay *= 2
		if delay > maxDelay {
			delay = maxDelay
		}
	}
}<commit_msg>Improve the Retry function to use timers and react Context cancel.<commit_after>package client

import (
	"context"
	"time"

	"github.com/datawire/dlib/dlog"
	"github.com/datawire/dlib/dutil"
)

const defaultRetryDelay = 100 * time.Millisecond
const defaultMaxDelay = 3 * time.Second

// Retry will run the given function repeatedly with an increasing delay until it returns without error.
//
// The function takes 0 to 3 durations with the following meaning
//  Delay - initial delay, i.e. the delay between the first and the second call.
//  MaxDelay - maximum delay between calling the functions (delay will never grow beyond this value)
//  MaxTime - maximum time before giving up.
func Retry(c context.Context, f func(context.Context) error, durations ...time.Duration) (err error) {
	delay := defaultRetryDelay
	maxDelay := defaultMaxDelay
	maxTime := time.Duration(0)

	switch len(durations) {
	case 3:
		maxTime = durations[2]
		fallthrough
	case 2:
		maxDelay = durations[1]
		if maxDelay == 0 {
			maxDelay = defaultMaxDelay
		}
		fallthrough
	case 1:
		delay = durations[0]
		if delay == 0 {
			delay = defaultRetryDelay
		}
	}

	if maxDelay < delay {
		maxDelay = delay
	}

	done := make(chan bool)
	if maxTime > 0 {
		var cancel context.CancelFunc
		c, cancel = context.WithCancel(c)
		go func() {
			select {
			case <-done:
			case <-c.Done():
			case <-time.After(maxTime):
				cancel()
			}
		}()
	}

	defer func() {
		err = dutil.PanicToError(recover())
		close(done)
	}()

	for retry := 0; ; retry++ {
		funcErr := f(c)
		if funcErr == nil {
			// success
			return nil
		}

		// Logging at higher log levels should be done in the called function
		dlog.Debugf(c, "waiting %s before retrying after error: %s", delay.String(), funcErr.Error())

		select {
		case <-c.Done():
			return nil
		case <-time.After(delay):
		}
		delay *= 2
		if delay > maxDelay {
			delay = maxDelay
		}
	}
}
{'fraction_non_alphanumeric': 0.11965300628178283, 'fraction_numerical': 0.009871373018247084, 'mean_word_length': 3.0289156626506024, 'pattern_counts': {'":': 0, '<': 16, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6395085', 'n_tokens_mistral': 1315, 'n_tokens_neox': 1139, 'n_words': 345}