DevGang
Авторизоваться

Коллекция шаблонов повторных вызовов SDK в AWS SDK for Go v2

При использовании AWS SDK (AWS SDK для Go v2) в Golang вы можете повторить вызов SDK.

Есть несколько схем, как это сделать, поэтому я написал это.

Предположения

В примере в этой статье используются версии v1.18.7 Go и aws-sdk-go-v2 v1.17.5.

Кроме того, что касается повторного выполнения запроса, можно централизованно настроить повторы для всех вызовов SDK (устанавливается при создании экземпляра клиента), но в этой статье предполагается, что вы хотите set/change retries for each SDK call (=API call).

Например: изменение поведения повторных попыток между s3.ListObjects и iam.DeleteRole.

Репозиторий

Исходный код этого проекта доступен на GitHub.

[Retry Patterns 1.] Options.RetryMaxAttempts

Вот сначала самый простой.

В структуре параметров, используемой для генерации клиентов и вызовов API в AWS SDK, есть параметры для повторных попыток, называемые RetryMaxAttempts и RetryMode, как показано ниже

  • Пример реализации
input := &iam.DeleteRoleInput{
    RoleName: roleName,
}

optFn := func(o *iam.Options) {
    o.RetryMaxAttempts = 3
    o.RetryMode = aws.RetryModeStandard
}

_, err := i.client.DeleteRole(ctx, input, optFn)

Простое их указание приведет к выполнению экспоненциальных повторных попыток отсрочки столько раз, сколько указано в RetryMaxAttempts.

[Retry Patterns 2.] Options.Retryer

Кроме того, в Options есть параметр Retryer для реализации точно настроенного алгоритма повтора.

Если этот параметр указан, будет применено указанное (реализованное) здесь поведение повтора вместо RetryMaxAttempts и RetryMode, перечисленных выше.

Options.Retryer должен представлять собой интерфейс с именем Retryer или RetryerV2.

Существуют функции для настройки логики принятия решения о повторной попытке (IsErrorRetryable), максимального количества попыток (MaxAttempts) и времени ожидания (RetryDelay), которые позволяют настроить поведение повторной попытки.

В частности, IsErrorRetryable позволяет вам более подробно указать, «когда повторять попытку». RetryDelay позволяет вам настроить такую ​​логику, как «подождать случайное количество секунд (дрожание), а не просто экспоненциальную задержку».

  • Код в модуле SDK (не пример реализации)
type Retryer interface {
    // IsErrorRetryable returns if the failed attempt is retryable. This check
    // should determine if the error can be retried, or if the error is
    // terminal.
    IsErrorRetryable(error) bool

    // MaxAttempts returns the maximum number of attempts that can be made for
    // an attempt before failing. A value of 0 implies that the attempt should
    // be retried until it succeeds if the errors are retryable.
    MaxAttempts() int

    // RetryDelay returns the delay that should be used before retrying the
    // attempt. Will return error if the if the delay could not be determined.
    RetryDelay(attempt int, opErr error) (time.Duration, error)

    // GetRetryToken attempts to deduct the retry cost from the retry token pool.
    // Returning the token release function, or error.
    GetRetryToken(ctx context.Context, opErr error) (releaseToken func(error) error, err error)

    // GetInitialToken returns the initial attempt token that can increment the
    // retry token pool if the attempt is successful.
    GetInitialToken() (releaseToken func(error) error)
}

// RetryerV2 is an interface to determine if a given error from an attempt
// should be retried, and if so what backoff delay to apply. The default
// implementation used by most services is the retry package's Standard type.
// Which contains basic retry logic using exponential backoff.
//
// RetryerV2 replaces the Retryer interface, deprecating the GetInitialToken
// method in favor of GetAttemptToken which takes a context, and can return an error.
//
// The SDK's retry package's Attempt middleware, and utilities will always
// wrap a Retryer as a RetryerV2. Delegating to GetInitialToken, only if
// GetAttemptToken is not implemented.
type RetryerV2 interface {
    Retryer

    // GetInitialToken returns the initial attempt token that can increment the
    // retry token pool if the attempt is successful.
    //
    // Deprecated: This method does not provide a way to block using Context,
    // nor can it return an error. Use RetryerV2, and GetAttemptToken instead.
    GetInitialToken() (releaseToken func(error) error)

    // GetAttemptToken returns the send token that can be used to rate limit
    // attempt calls. Will be used by the SDK's retry package's Attempt
    // middleware to get a send token prior to calling the temp and releasing
    // the send token after the attempt has been made.
    GetAttemptToken(context.Context) (func(error) error, error)
}

Если вы используете этот метод, определите структуру с именем Retryer в отдельном файле.

Логические функции принятия решения о повторной попытке, которые должны быть реализованы в приведенном выше IsErrorRetryable, могут быть определены вызывающей стороной и переданы конструктору для общего использования.

В RetryDelay в середине приведенного ниже примера записана логика «повторить попытку через случайное количество секунд».

  • Пример реализации (retryer_options.go)
package retryer

import (
    "context"
    "math/rand"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
)

const MaxRetryCount = 10

var _ aws.RetryerV2 = (*Retryer)(nil)

type Retryer struct {
    isErrorRetryableFunc func(error) bool
    delayTimeSec         int
}

func NewRetryer(isErrorRetryableFunc func(error) bool, delayTimeSec int) *Retryer {
    return &Retryer{
        isErrorRetryableFunc: isErrorRetryableFunc,
        delayTimeSec:         delayTimeSec,
    }
}

func (r *Retryer) IsErrorRetryable(err error) bool {
    return r.isErrorRetryableFunc(err)
}

func (r *Retryer) MaxAttempts() int {
    return MaxRetryCount
}

func (r *Retryer) RetryDelay(int, error) (time.Duration, error) {
    rand.Seed(time.Now().UnixNano())
    waitTime := 1
    if r.delayTimeSec > 1 {
        waitTime += rand.Intn(r.delayTimeSec)
    }
    return time.Duration(waitTime) * time.Second, nil
}

func (r *Retryer) GetRetryToken(context.Context, error) (func(error) error, error) {
    return func(error) error { return nil }, nil
}

func (r *Retryer) GetInitialToken() func(error) error {
    return func(error) error { return nil }
}

func (r *Retryer) GetAttemptToken(context.Context) (func(error) error, error) {
    return func(error) error { return nil }, nil
}

Затем на основе этого указываются повторы при вызове SDK.

В следующем примере реализации переменная retryable содержит функцию с логикой принятия решения, например « retry if there is an api error Throttling: Rate exceeded message in SDK error response  ».

Затем в optFn определите «a function to specify a Retryer instance created with the retryable and SleepTimeSec to Options.Retryer», и укажите ее в качестве третьего аргумента вызова SDK (в данном случае — DeleteRole).

  • Пример реализации (Caller)(iam.go)
const SleepTimeSec = 5

...
...

input := &iam.DeleteRoleInput{
    RoleName: roleName,
}

retryable := func(err error) bool {
    return strings.Contains(err.Error(), "api error Throttling: Rate exceeded")
}
optFn := func(o *iam.Options) {
    o.Retryer = retryer.NewRetryer(retryable, SleepTimeSec)
}

_, err := i.client.DeleteRole(ctx, input, optFn)

[Retry Patterns 3.] Golang Generics

Вышеупомянутый Options.Retryer следует официальному методу повтора и позволяет вам определить свою собственную логику, но здесь есть метод, который позволяет вам создать свою собственную логику.

Этот метод использует относительно новую функцию Go «generics».

Retryer, сложно свободно создавать сообщения об ошибках, которые будут выводиться при возникновении ошибки посредством повторных попыток (например, вывод такой информации, как имя ресурса, где произошла ошибка, и т. д.).

Здесь я описываю, как сделать эти точки еще более гибкими.

Сначала определите функцию повтора с дженериками в отдельном файле.

  • Пример реализации (retryer_generics.go)
// T: Input type for API Request.
// U: Output type for API Response.
// V: Options type for API Request.
type RetryInput[T, U, V any] struct {
    Ctx              context.Context
    SleepTimeSec     int
    TargetResource   *string
    Input            *T
    ApiOptions       []func(*V)
    ApiCaller        func(ctx context.Context, input *T, optFns ...func(*V)) (*U, error)
    RetryableChecker func(error) bool
}

// T: Input type for API Request.
// U: Output type for API Response.
// V: Options type for API Request.
func Retry[T, U, V any](
    in *RetryInput[T, U, V],
) (*U, error) {
    retryCount := 0

    for {
        output, err := in.ApiCaller(in.Ctx, in.Input, in.ApiOptions...)
        if err == nil {
            return output, nil
        }

        if in.RetryableChecker(err) {
            retryCount++
            if err := waitForRetry(in.Ctx, retryCount, in.SleepTimeSec, in.TargetResource, err); err != nil {
                return nil, err
            }
            continue
        }
        return nil, err
    }
}

func waitForRetry(ctx context.Context, retryCount int, sleepTimeSec int, targetResource *string, err error) error {
    if retryCount > MaxRetryCount {
        errorDetail := err.Error() + "\nRetryCount(" + strconv.Itoa(MaxRetryCount) + ") over, but failed to delete. "
        return fmt.Errorf("RetryCountOverError: %v, %v", *targetResource, errorDetail)
    }

    select {
    case <-ctx.Done():
        return ctx.Err()
    case <-time.After(getRandomSleepTime(sleepTimeSec)):
    }
    return nil
}

func getRandomSleepTime(sleepTimeSec int) time.Duration {
    rand.Seed(time.Now().UnixNano())
    waitTime := 1
    if sleepTimeSec > 1 {
        waitTime += rand.Intn(sleepTimeSec)
    }
    return time.Duration(waitTime) * time.Second
}

Вот объяснение: определите функцию Retry, выполняющую повторную попытку, с типом RetryInput в качестве входных данных.

Во-первых, в качестве типа ([T, U, V Any]), используемого для универсальных шаблонов RetryInput, вызывающая сторона должна передать iam.DeleteRoleInput для T, iam.DeleteRoleOutput для U и iam.Options для V.

ApiCaller — это сама функция SDK (например, iam.DeleteRole).

RetriableChecker — это функция, определяющая логику определения момента повторной попытки.

// T: Input type for API Request.
// U: Output type for API Response.
// V: Options type for API Request.
type RetryInput[T, U, V any] struct {
    Ctx              context.Context
    SleepTimeSec     int
    TargetResource   *string
    Input            *T
    ApiOptions       []func(*V)
    ApiCaller        func(ctx context.Context, input *T, optFns ...func(*V)) (*U, error)
    RetryableChecker func(error) bool
}
// T: Input type for API Request.
// U: Output type for API Response.
// V: Options type for API Request.
func Retry[T, U, V any](
    in *RetryInput[T, U, V],
) (*U, error) {
    retryCount := 0

    for {
        output, err := in.ApiCaller(in.Ctx, in.Input, in.ApiOptions...)
        if err == nil {
            return output, nil
        }

        if in.RetryableChecker(err) {
            retryCount++
            if err := waitForRetry(in.Ctx, retryCount, in.SleepTimeSec, in.TargetResource, err); err != nil {
                return nil, err
            }
            continue
        }
        return nil, err
    }
}

Функция waitForRetry — это процесс, который возвращает ошибку и выводит исходное сообщение об ошибке, когда превышено максимальное количество повторений MaxRetryCount.

Кроме того, используя контекст, переданный в качестве аргумента, проверяет, был ли контекст отменен (Done) каждый раз при повторной попытке (т. е. произошла ли какая-то ошибка в каком-то другом процессе и программа должна быть аварийно завершена), и если это произошло был отменен, он возвращает ctx.Err() без выполнения режима сна для следующей повторной попытки.

func waitForRetry(ctx context.Context, retryCount int, sleepTimeSec int, targetResource *string, err error) error {
    if retryCount > MaxRetryCount {
        errorDetail := err.Error() + "\nRetryCount(" + strconv.Itoa(MaxRetryCount) + ") over, but failed to delete. "
        return fmt.Errorf("RetryCountOverError: %v, %v", *targetResource, errorDetail)
    }

    select {
    case <-ctx.Done():
        return ctx.Err()
    case <-time.After(getRandomSleepTime(sleepTimeSec)):
    }
    return nil
}

Затем с помощью getRandomSleepTime, который появляется в приведенной выше функции waitForRetry, я пишу логику для настройки времени ожидания для повторных попыток.

Здесь я пишу процесс, который случайным образом ожидает (Jitter) в течение указанного верхнего предела времени (sleepTimeSec).

func getRandomSleepTime(sleepTimeSec int) time.Duration {
    rand.Seed(time.Now().UnixNano())
    waitTime := 1
    if sleepTimeSec > 1 {
        waitTime += rand.Intn(sleepTimeSec)
    }
    return time.Duration(waitTime) * time.Second
}

  А вот пример реализации вызывающей функции Retry.

  • Пример реализации (caller)(iam.go)
    input := &iam.DeleteRoleInput{
        RoleName: roleName,
    }

    retryable := func(err error) bool {
        return strings.Contains(err.Error(), "api error Throttling: Rate exceeded")
    }

    _, err := retryer.Retry(
        &retryer.RetryInput[iam.DeleteRoleInput, iam.DeleteRoleOutput, iam.Options]{
            Ctx:              ctx,
            SleepTimeSec:     SleepTimeSec,
            TargetResource:   roleName,
            Input:            input,
            ApiCaller:        i.client.DeleteRole,
            RetryableChecker: retryable,
        },
    )

Особенность этого метода заключается в том, что с помощью дженериков можно реализовать повторную обработку с гарантией связи универсального типа путем объединения типов ввода, вывода и параметров, даже если это функция, созданная пользователем.

Однако, если нет особой причины, думаю, лучше будет использовать Options.Retryer, который предусмотрен официально.

Заключение

Было представлено несколько шаблонов повторных попыток с использованием AWS SDK для Go V2. В частности, некоторые из вас не знакомы с Retryer, поэтому, пожалуйста, воспользуйтесь этой возможностью и используйте его!

Источник:

#Golang #AWS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу