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; a blank field on the add target form gives 1, and on the
edit form keeps the stored value. 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:34:18 +00:00
parent c0f7cc29fd
commit 14a38df093
37 changed files with 843 additions and 716 deletions
+20 -15
View File
@@ -32,7 +32,7 @@ const (
viewMaskedOrigin = viewExampleOrigin + "/..."
viewUnavailable = "(unavailable)"
viewExpiryNever = "never"
viewMaxRetries = "Delivery attempts"
viewMaxAttempts = "Delivery attempts"
)
func TestMaskedWebhookURL(t *testing.T) {
@@ -171,6 +171,7 @@ func slackTarget() database.Target {
Active: true,
Config: `{"webhookUrl":"` +
slackWebhookURL + `"}`,
MaxAttempts: 1,
}
}
@@ -189,28 +190,29 @@ func TestNewTargetViews_Slack(t *testing.T) {
assert.Equal(
t,
map[string]string{
"Webhook URL": slackMaskedURL,
viewMaxRetries: "1 (fire-and-forget: no retries, no circuit breaker)",
"Webhook URL": slackMaskedURL,
viewMaxAttempts: "1",
},
fieldMap(view.Config),
)
}
// TestNewTargetViews_SlackRetries proves a Slack target shows
// its retry count the same way an HTTP target does.
func TestNewTargetViews_SlackRetries(t *testing.T) {
// TestNewTargetViews_SlackAttempts proves a Slack target shows
// its number of delivery attempts the same way an HTTP target
// does.
func TestNewTargetViews_SlackAttempts(t *testing.T) {
t.Parallel()
target := slackTarget()
target.MaxRetries = 2
target.MaxAttempts = 2
view := viewFor(t, target)
assert.Equal(
t,
map[string]string{
"Webhook URL": slackMaskedURL,
viewMaxRetries: "2",
"Webhook URL": slackMaskedURL,
viewMaxAttempts: "2",
},
fieldMap(view.Config),
)
@@ -225,7 +227,7 @@ func TestNewTargetViews_HTTP(t *testing.T) {
`"timeout":30,` +
`"headers":{"Authorization":"Bearer sekrit"},` +
`"forwardQuery":true}`,
MaxRetries: 5,
MaxAttempts: 5,
})
fields := fieldMap(view.Config)
@@ -237,7 +239,7 @@ func TestNewTargetViews_HTTP(t *testing.T) {
"Timeout": "30s",
"Headers": "1 configured",
"Query string": "passed on to this target",
viewMaxRetries: "5",
viewMaxAttempts: "5",
},
fields,
)
@@ -248,19 +250,22 @@ func TestNewTargetViews_HTTP(t *testing.T) {
}
}
func TestNewTargetViews_HTTPFireAndForget(t *testing.T) {
// TestNewTargetViews_HTTPSingleAttempt proves a single attempt is
// shown as the plain number, like any other.
func TestNewTargetViews_HTTPSingleAttempt(t *testing.T) {
t.Parallel()
view := viewFor(t, database.Target{
Type: database.TargetTypeHTTP,
Config: `{"url":"` + viewExampleHook + `"}`,
Type: database.TargetTypeHTTP,
Config: `{"url":"` + viewExampleHook + `"}`,
MaxAttempts: 1,
})
assert.Equal(
t,
map[string]string{
"Destination URL": viewMaskedOrigin,
viewMaxRetries: "1 (fire-and-forget: no retries, no circuit breaker)",
viewMaxAttempts: "1",
},
fieldMap(view.Config),
)