retry.go en Go
Reintentos con backoff, un plazo de contexto y errores que dicen qué pasó.
// Command retry muestra un bucle de reintentos con backoff y cancelación.
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
var (
// ErrExhausted significa que todos los intentos fallaron.
ErrExhausted = errors.New("attempts exhausted")
// ErrTransient amerita reintentar; cualquier otro error no.
ErrTransient = errors.New("transient failure")
)
// Policy describe cuánto insistir.
type Policy struct {
Attempts int
Base time.Duration
Max time.Duration
}
func (p Policy) backoff(attempt int) time.Duration {
delay := p.Base << (attempt - 1)
if delay > p.Max {
delay = p.Max
}
return delay
}
// Do llama a work hasta que funcione, el contexto acabe o no queden intentos.
func Do(ctx context.Context, p Policy, work func(int) error) error {
var problems error
for attempt := 1; attempt <= p.Attempts; attempt++ {
err := work(attempt)
if err == nil {
return nil
}
problems = errors.Join(problems, fmt.Errorf("attempt %d: %w",
attempt, err))
if !errors.Is(err, ErrTransient) {
return problems // permanente: deja de intentar
}
if attempt == p.Attempts {
break
}
select {
case <-time.After(p.backoff(attempt)):
case <-ctx.Done():
return errors.Join(problems, ctx.Err())
}
}
return errors.Join(problems, ErrExhausted)
}
func flaky(succeedOn int) func(int) error {
return func(attempt int) error {
if attempt < succeedOn {
return fmt.Errorf("connection reset: %w", ErrTransient)
}
return nil
}
}
func main() {
policy := Policy{Attempts: 4, Base: 5 * time.Millisecond,
Max: 40 * time.Millisecond}
ctx := context.Background()
if err := Do(ctx, policy, flaky(3)); err != nil {
fmt.Println("unexpected:", err)
} else {
fmt.Println("succeeded on the third attempt")
}
err := Do(ctx, policy, flaky(99))
fmt.Println("exhausted:", errors.Is(err, ErrExhausted))
permanent := Do(ctx, policy, func(int) error {
return errors.New("bad credentials")
})
fmt.Println("gave up early:", !errors.Is(permanent, ErrExhausted))
short, cancel := context.WithTimeout(ctx, time.Millisecond)
defer cancel()
canceled := Do(short, policy, flaky(99))
fmt.Println("canceled:", errors.Is(canceled, context.DeadlineExceeded))
_ = rand.Intn(1) // mantiene honesto el import para la variante con semilla
}
Cómo funciona
- Un centinela más errors.Is deja a quien llama probar el desenlace.
- El contexto se revisa entre intentos, no solo durante ellos.
- El error de cada intento se une al resto, así que nada se traga.
Palabras clave y builtins usados aquí
breakcasedeferelseerrorforfuncifintreturnselectstructtypevar
El intento, en números
- Líneas
- 96
- Caracteres a escribir
- 2209
- Tokens
- 470
- Ritmo de tres estrellas
- 110 tpm
Al ritmo de tres estrellas de 110 tokens por minuto, este intento toma unos 256 segundos.
Paso 1 de 1 en Bis; paso 15 de 15 en Errores.