refactor(client): wrap all external requests in retry transport

This commit is contained in:
2026-09-21 23:15:45 +03:30
parent 8e39fd4503
commit 055c8d8777
5 changed files with 612 additions and 86 deletions

View File

@@ -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(