Вышел retry 3.0

Вышел retry 3.0

Улучшено качество кода консольной утилиты и добавлена поддержка сигнала os.Interrupt

Для разработки внутреннего HTTP-клиента в компании Lazada, в качестве компонента, который отвечает за повторение неуспешных запросов, я выбрал довольно хороший пакет github.com/Rican7/retry. Мне потребовалось внести в него небольшие доработки и, как это принято в Open Source, я сделал pull request, который, к сожалению или к счастью, не был принят и был мною закрыт. Суть патча была в том, что нам не хотелось получить бесконечный цикл во время выполнения приложения из-за невнимательности разработчиков, которые, как правило, не любят читать документацию и тем более заглядывать под капот сторонних библиотек. Так появился fork этого пакета, а после выпуска версии 1.0, где я добавил поддержку context для прерывания повторений, библиотека обрела свою самостоятельную жизнь.

Что нового

  • Описание релиза: 3.0.0.
  • Полный список изменений: 2.1.2…3.0.0.
  • Документация по консольной утилите: cmd/retry.

Для прерывания исполнения были взяты наработки из другого моего пакета semaphore:

// WithDeadline returns empty struct channel based on Time channel.
func WithDeadline(deadline time.Time) <-chan struct{} {
	// go 1.5 doesn't support time.Until(deadline)
	return WithTimeout(deadline.Sub(time.Now())) //nolint: gosimple
}

// WithSignal returns empty struct channel based on Signal channel.
func WithSignal(s os.Signal) <-chan struct{} {
	ch := make(chan struct{})
	if s == nil {
		close(ch)
		return ch
	}
	go func() {
		c := make(chan os.Signal, 1)
		signal.Notify(c, s)
		<-c
		close(ch)
		signal.Stop(c)
	}()
	return ch
}

// WithTimeout returns empty struct channel based on Time channel.
func WithTimeout(timeout time.Duration) <-chan struct{} {
	ch := make(chan struct{})
	if timeout <= 0 {
		close(ch)
		return ch
	}
	go func() {
		<-time.After(timeout)
		close(ch)
	}()
	return ch
}

Теперь можно делать так:

interrupter := retry.Multiplex(
	retry.WithTimeout(time.Second),
	retry.WithSignal(os.Interrupt),
)
if err := retry.Retry(interrupter, action, strategies...); err != nil {
	// an unattainable result
}

Функция Multiplex может принимать неограниченное количество “прерывателей”:

// Multiplex combines multiple empty struct channels into one.
func Multiplex(channels ...<-chan struct{}) <-chan struct{} {
	ch := make(chan struct{})
	if len(channels) == 0 {
		close(ch)
		return ch
	}
	go func() {
		cases := make([]reflect.SelectCase, 0, len(channels))
		for _, ch := range channels {
			cases = append(cases, reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)})
		}
		reflect.Select(cases)
		close(ch)
	}()
	return ch
}

Возможности консольной утилиты

Синтаксис определения настроек был изменён для совместимости с Zsh.

Следующая команда:

$ retry -limit=3 -backoff=lin:100ms -- git pull

будет выполнять git pull три раза с паузой между повторами в 100 и 200 миллисекунд, пока команда завершается ошибкой.

retry в действии:

Примеры использования

Вместе с рефакторингом кода были переосмысленны примеры использования библиотеки, в частности пример создания HTTP-клиента.

type client struct {
	base       *http.Client
	strategies []strategy.Strategy
}

func New(timeout time.Duration, strategies ...strategy.Strategy) *client {
	return &client{
		base:       &http.Client{Timeout: timeout},
		strategies: strategies,
	}
}

func (c *client) Get(deadline <-chan struct{}, url string) (*http.Response, error) {
	var response *http.Response
	err := retry.Retry(deadline, func(uint) error {
		resp, err := c.base.Get(url)
		if err != nil {
			return err
		}
		response = resp
		return nil
	}, c.strategies...)
	return response, err
}

Теги

go retry

Автор

Камиль Самигуллин
Камиль Самигуллин

Разработчик

В этом релизе retry я решил заменить использование context на каналы, а также улучшил качество кода консольной утилиты и добавил поддержку сигнала os.Interrupt.

Спонсоры

Опубликовано

3.12.2017