refactor(client): wrap all external requests in retry transport
This commit is contained in:
@@ -54,14 +54,48 @@ func NewClient(
|
||||
logger = loggers[0]
|
||||
}
|
||||
|
||||
return &Client{
|
||||
if httpClient == nil {
|
||||
httpClient = http.DefaultClient
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
httpClient: httpClient,
|
||||
endpointURL: endpointURL,
|
||||
tokenProvider: tokenProvider,
|
||||
maxRetries: maxRetries,
|
||||
retryWait: waitForRetry,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if _, ok := httpClient.Transport.(*RetryTransport); !ok && maxRetries > 0 {
|
||||
rt := NewRetryTransport(
|
||||
httpClient.Transport,
|
||||
httpClient.Timeout,
|
||||
maxRetries,
|
||||
logger,
|
||||
)
|
||||
rt.retryWait = func(ctx context.Context, delay time.Duration) bool {
|
||||
if client.retryWait != nil {
|
||||
return client.retryWait(ctx, delay)
|
||||
}
|
||||
return waitForRetry(ctx, delay)
|
||||
}
|
||||
client.httpClient = &http.Client{
|
||||
Transport: rt,
|
||||
}
|
||||
} else if rt, ok := httpClient.Transport.(*RetryTransport); ok {
|
||||
origWait := rt.retryWait
|
||||
rt.retryWait = func(ctx context.Context, delay time.Duration) bool {
|
||||
if client.retryWait != nil {
|
||||
return client.retryWait(ctx, delay)
|
||||
}
|
||||
if origWait != nil {
|
||||
return origWait(ctx, delay)
|
||||
}
|
||||
return waitForRetry(ctx, delay)
|
||||
}
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) log() *slog.Logger {
|
||||
@@ -110,88 +144,7 @@ func (c *Client) Convert(
|
||||
ctx context.Context,
|
||||
cardNumber string,
|
||||
) (service.ConversionResult, error) {
|
||||
var (
|
||||
result service.ConversionResult
|
||||
err error
|
||||
)
|
||||
|
||||
waitFn := c.retryWait
|
||||
if waitFn == nil {
|
||||
waitFn = waitForRetry
|
||||
}
|
||||
|
||||
for attempt := 0; attempt <= c.maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
c.log().InfoContext(
|
||||
ctx,
|
||||
fmt.Sprintf("[RETRY] zarinhub request attempt %d", attempt),
|
||||
"is_retry", true,
|
||||
"retry_count", attempt,
|
||||
)
|
||||
} else {
|
||||
c.log().InfoContext(
|
||||
ctx,
|
||||
"[INITIAL] zarinhub request",
|
||||
"is_retry", false,
|
||||
"retry_count", 0,
|
||||
)
|
||||
}
|
||||
|
||||
result, err = c.convertOnce(
|
||||
ctx,
|
||||
cardNumber,
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Don't retry permanent/non-transient errors.
|
||||
if !isRetryable(err) {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// We already used all configured retries.
|
||||
if attempt == c.maxRetries {
|
||||
if attempt > 0 {
|
||||
c.log().ErrorContext(
|
||||
ctx,
|
||||
"[RETRY] zarinhub request failed: all retries exhausted",
|
||||
"is_retry", true,
|
||||
"retry_count", attempt,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// If the caller canceled the request, stop immediately.
|
||||
if ctx.Err() != nil {
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return result, fmt.Errorf("zarinhub request canceled: %w", ctx.Err())
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
delay := retryDelay(attempt)
|
||||
c.log().WarnContext(
|
||||
ctx,
|
||||
fmt.Sprintf("[RETRY] zarinhub request failed, retrying in %v (retry %d)", delay, attempt+1),
|
||||
"is_retry", true,
|
||||
"next_retry", attempt + 1,
|
||||
"delay_ms", delay.Milliseconds(),
|
||||
"error", err,
|
||||
)
|
||||
|
||||
if !waitFn(ctx, delay) {
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return result, fmt.Errorf("zarinhub request canceled: %w", ctx.Err())
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, err
|
||||
return c.convertOnce(ctx, cardNumber)
|
||||
}
|
||||
|
||||
func (c *Client) convertOnce(
|
||||
|
||||
Reference in New Issue
Block a user