Files
webhooker/internal/handlers/target_retries.go
T
clawbot 7e9e441d42
check / check (push) Waiting to run
Describe code in its own terms, not by an earlier build's data (closes #522)
Several comments justified code by what an earlier build would have
left in a database or on disk. Each now says what the code does:
reserveSQLiteFile sets the mode on every open whoever created the
file, and expired treats a session lacking a deadline's timestamp as
expired. The comments on retention, resubmitted events, archive
pruning, target headers, the redactor and the delivery result view
drop their mentions of older rows. parseMaxRetries loses its
paragraph on targets saved before the ceiling existed, and neither
BeforeSave nor retentionCutoff cites a migration. Comments only.

Model: opus-5-5
2026-10-13 06:53:38 +00:00

84 lines
2.7 KiB
Go

package handlers
import (
"errors"
"strconv"
"strings"
)
// maxTargetRetries bounds a target's max_retries.
//
// 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 maxTargetRetries = 20
// Errors returned when a max_retries form value cannot be turned into
// a retry count.
var (
// errRetriesInvalid signals a max_retries form value that is not
// a non-negative whole number.
errRetriesInvalid = errors.New(
"must be a whole number",
)
// errRetriesTooLarge signals a max_retries form value that is a
// whole number but above maxTargetRetries. It is distinguished
// from errRetriesInvalid so the message can name the ceiling
// instead of implying the input was not a number.
errRetriesTooLarge = errors.New("out of range")
)
// parseMaxRetries interprets a max_retries 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, negative, or above maxTargetRetries.
//
// The distinction is the whole point of this function. max_retries=0
// means fire-and-forget, so returning 0 for input the operator typed
// but that did not parse silently disables retries on a
// store-and-forward proxy — and on the edit path it destroys a
// working retry configuration over a typo. A default answers a
// question that was not asked; it never answers one that was asked
// badly.
func parseMaxRetries(raw string, fallback int) (int, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return fallback, nil
}
v, err := strconv.Atoi(raw)
if err != nil || v < 0 {
return 0, errRetriesInvalid
}
if v > maxTargetRetries {
return 0, errRetriesTooLarge
}
return v, nil
}
// retriesErrorMessage returns the message the create and edit forms
// show for a rejected max_retries value. Any error other than
// errRetriesTooLarge falls back to the generic wording, so an
// unrecognised parse failure still produces a sensible 400.
func retriesErrorMessage(err error) string {
if errors.Is(err, errRetriesTooLarge) {
return errRetriesTooLarge.Error() +
": at most " + strconv.Itoa(maxTargetRetries) +
" attempts"
}
return errRetriesInvalid.Error() +
", or 0 for fire-and-forget"
}