Ctrl K
ComunidadGuías…

retry.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"strconv"
	"time"

	"github.com/go-telegram/bot"
	maxbot "github.com/max-messenger/max-bot-api-client-go/v2"
)

var errRejected = errors.New("платформа отклонила публикацию")

type publishError struct {
	message string
	delay   time.Duration // positive only when the platform confirms a retry is safe
}

func (e *publishError) Error() string { return e.message }
func (e *publishError) Unwrap() error { return errRejected }

func rateLimitDelay(header string) time.Duration {
	if seconds, err := strconv.Atoi(header); err == nil && seconds > 0 {
		return time.Duration(min(seconds, 600)) * time.Second
	}

	return 2 * time.Second
}

func definitivelyRejected(status int) bool {
	return status >= http.StatusBadRequest && status < http.StatusInternalServerError && status != http.StatusRequestTimeout
}

// retryPublish repeats only explicit rejections. A lost response or a 5xx
// might follow a successful publication and must go to the delivery journal.
func retryPublish(ctx context.Context, fn func() ([]string, error)) ([]string, error) {
	for attempt := 1; ; attempt++ {
		ids, err := fn()
		known, ok := errors.AsType[*publishError](err)
		if !ok || known.delay <= 0 || attempt == 5 {
			return ids, err
		}
		select {
		case <-ctx.Done():
			return ids, err
		case <-time.After(max(known.delay, time.Duration(1<<attempt)*time.Second)):
		}
	}
}

// retry repeats transient failures — network, 429, 5xx, "attachment not
// ready" — with a growing pause: 2, 4, 8, 16 seconds.
func retry(ctx context.Context, fn func() error) error {
	const attempts = 5
	delay := 2 * time.Second
	for attempt := 1; ; attempt++ {
		err := fn()
		if err == nil || attempt == attempts || permanent(err) {
			return err
		}
		slog.Warn("повтор после ошибки", "attempt", attempt, "err", err)
		select {
		case <-ctx.Done():
			return err
		case <-time.After(delay):
		}
		delay *= 2
	}
}

// permanent reports errors that another attempt will not fix.
func permanent(err error) bool {
	if rejection, ok := errors.AsType[*publishError](err); ok {
		return rejection.delay == 0
	}
	if maxErr, ok := errors.AsType[*maxbot.Error](err); ok {
		return !maxErr.IsAttachmentNotReady()
	}

	return errors.Is(err, errMaxMediaInvalid) || errors.Is(err, errTooBig) || errors.Is(err, errNotOnVolume) || errors.Is(err, bot.ErrorBadRequest) || errors.Is(err, bot.ErrorForbidden) ||
		errors.Is(err, bot.ErrorUnauthorized) || errors.Is(err, bot.ErrorNotFound) || errors.Is(err, errMaxMediaExpired) || errors.Is(err, errMaxMediaSize)
}