check / check (push) Waiting to run
Target.MaxRetries (max_retries) becomes Target.MaxAttempts (max_attempts), and the form field, parser and ceiling are renamed to match. 1 is a single attempt with no circuit breaker; above 1 retries behind the breaker up to that many attempts. Both target forms refuse 0 and less and default a blank field to 1. The target page shows the plain number, and the README's two paragraphs on 0 against 1 become one sentence. Model: opus-5-5
82 lines
2.6 KiB
Go
82 lines
2.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// maxTargetAttempts bounds a target's max_attempts.
|
|
//
|
|
// Both target forms already declare max="20" on the input, so this
|
|
// enforces server-side what the UI has always advertised rather than
|
|
// introducing a new limit.
|
|
//
|
|
// The number is not cosmetic. Every attempt writes a delivery_results
|
|
// row that the event log then loads and renders, and the engine backs
|
|
// off by 2^(n-1) seconds, so attempt 20 is already about six days
|
|
// after the first. A value beyond this buys no additional durability
|
|
// and only costs rows.
|
|
const maxTargetAttempts = 20
|
|
|
|
// Errors returned when a max_attempts form value cannot be turned into
|
|
// a number of attempts.
|
|
var (
|
|
// errAttemptsInvalid signals a max_attempts form value that is not
|
|
// a whole number of at least 1.
|
|
errAttemptsInvalid = errors.New(
|
|
"must be a whole number, at least 1",
|
|
)
|
|
|
|
// errAttemptsTooLarge signals a max_attempts form value that is a
|
|
// whole number but above maxTargetAttempts. It is distinguished
|
|
// from errAttemptsInvalid so the message can name the ceiling
|
|
// instead of implying the input was not a number.
|
|
errAttemptsTooLarge = errors.New("out of range")
|
|
)
|
|
|
|
// parseMaxAttempts interprets a max_attempts form value.
|
|
//
|
|
// An ABSENT value — the field empty or not submitted — yields
|
|
// fallback, which lets the create path apply its default and the edit
|
|
// path leave the stored value alone. A value that is SET BUT INVALID
|
|
// is an error: unparseable, below 1, or above maxTargetAttempts.
|
|
//
|
|
// The distinction is the whole point of this function. Returning a
|
|
// default for input the operator typed but that did not parse would
|
|
// silently turn retries off on a store-and-forward proxy — and on the
|
|
// edit path destroy a working configuration over a typo. A default
|
|
// answers a question that was not asked; it never answers one that
|
|
// was asked badly.
|
|
func parseMaxAttempts(raw string, fallback int) (int, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return fallback, nil
|
|
}
|
|
|
|
v, err := strconv.Atoi(raw)
|
|
if err != nil || v < 1 {
|
|
return 0, errAttemptsInvalid
|
|
}
|
|
|
|
if v > maxTargetAttempts {
|
|
return 0, errAttemptsTooLarge
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// attemptsErrorMessage returns the message the create and edit forms
|
|
// show for a rejected max_attempts value. Any error other than
|
|
// errAttemptsTooLarge falls back to the generic wording, so an
|
|
// unrecognised parse failure still produces a sensible 400.
|
|
func attemptsErrorMessage(err error) string {
|
|
if errors.Is(err, errAttemptsTooLarge) {
|
|
return errAttemptsTooLarge.Error() +
|
|
": at most " + strconv.Itoa(maxTargetAttempts) +
|
|
" attempts"
|
|
}
|
|
|
|
return errAttemptsInvalid.Error()
|
|
}
|