Store a target's number of delivery attempts, at least one (closes #540)
check / check (push) Canceled after 0s

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
This commit is contained in:
2026-10-15 11:10:29 +00:00
parent c0f7cc29fd
commit e639032483
37 changed files with 843 additions and 716 deletions
+20 -18
View File
@@ -102,9 +102,10 @@ func (h *Handlers) newTarget(
in targetFormInput,
) (*database.Target, string, error) {
target := &database.Target{
WebhookID: webhookID,
Type: in.Type,
Active: true,
WebhookID: webhookID,
Type: in.Type,
Active: true,
MaxAttempts: 1,
}
errMsg, err := h.setTargetFromForm(ctx, target, in)
@@ -117,9 +118,10 @@ func (h *Handlers) newTarget(
// setTargetFromForm validates a target form against the target's type
// and, when it accepts it, sets the target's name, configuration and
// retry count from it. It returns the message the form shows for
// anything it refuses, an unknown type among them, and then leaves the
// target unchanged; an error is the server's fault, as for newTarget.
// number of delivery attempts from it. It returns the message the form
// shows for anything it refuses, an unknown type among them, and then
// leaves the target unchanged; an error is the server's fault, as for
// newTarget.
// The add target form and the target edit form both go through here,
// so the two cannot come to disagree about what a target may be.
func (h *Handlers) setTargetFromForm(
@@ -136,20 +138,20 @@ func (h *Handlers) setTargetFromForm(
return errMsg, err
}
// An empty max_retries keeps the target's count: the
// fire-and-forget default of 0 for a new target, and the stored
// count for an edited one, since the forms for target types that
// do not retry have no such field. A value that is filled in but
// invalid is refused rather than becoming that count, so a typo
// cannot destroy the count a target is delivering with.
maxRetries, err := parseMaxRetries(in.MaxRetries, target.MaxRetries)
// An empty max_attempts keeps the target's count: the default of
// 1 for a new target, and the stored count for an edited one,
// since the forms for target types that do not retry have no such
// field. A value that is filled in but invalid is refused rather
// than becoming that count, so a typo cannot destroy the count a
// target is delivering with.
maxAttempts, err := parseMaxAttempts(in.MaxAttempts, target.MaxAttempts)
if err != nil {
return "Invalid delivery attempts: " + retriesErrorMessage(err), nil
return "Invalid delivery attempts: " + attemptsErrorMessage(err), nil
}
target.Name = in.Name
target.Config = configJSON
target.MaxRetries = maxRetries
target.MaxAttempts = maxAttempts
return "", nil
}
@@ -176,8 +178,8 @@ type targetFormInput struct {
// ForwardQuery is an HTTP target's checkbox that passes each
// event's query string on to it.
ForwardQuery bool
// MaxRetries is an HTTP or Slack target's max_retries.
MaxRetries string
// MaxAttempts is an HTTP or Slack target's max_attempts.
MaxAttempts string
// Expiry is a database (archive) target's row expiry.
Expiry string
// Rotation is a database (archive) target's rotation.
@@ -204,7 +206,7 @@ func targetFormInputFrom(r *http.Request) targetFormInput {
Headers: r.PostFormValue("headers"),
Timeout: r.PostFormValue("timeout"),
ForwardQuery: r.PostFormValue("forward_query") != "",
MaxRetries: r.PostFormValue("max_retries"),
MaxAttempts: r.PostFormValue("max_attempts"),
Expiry: r.PostFormValue("expiry"),
Rotation: r.PostFormValue("rotation"),
}