typestar

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

  1. Un centinela más errors.Is deja a quien llama probar el desenlace.
  2. El contexto se revisa entre intentos, no solo durante ellos.
  3. El error de cada intento se une al resto, así que nada se traga.

Palabras clave y builtins usados aquí

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.

Escribe este fragmento

Paso 1 de 1 en Bis; paso 15 de 15 en Errores.

← Anterior