Store a target's number of delivery attempts, at least one (closes #540)
check / check (push) Waiting to run
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. The column is NOT NULL and a check constraint refuses a value below 1, so every target, whatever its type, is stored with at least 1. 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 was merged in pull request #547.
This commit is contained in:
@@ -25,10 +25,11 @@ func TestEventDBCreateOmitsAssociations(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
target := database.Target{
|
||||
WebhookID: webhookID,
|
||||
Name: "leaky-target",
|
||||
Type: database.TargetTypeSlack,
|
||||
Config: `{"webhookUrl":"https://hooks.example/secret"}`,
|
||||
WebhookID: webhookID,
|
||||
Name: "leaky-target",
|
||||
Type: database.TargetTypeSlack,
|
||||
Config: `{"webhookUrl":"https://hooks.example/secret"}`,
|
||||
MaxAttempts: 1,
|
||||
}
|
||||
target.ID = uuid.New().String()
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ func TestPreloadedModelsMarshalWithoutTheirParent(t *testing.T) {
|
||||
Name: testWebhookName,
|
||||
Entrypoints: []database.Entrypoint{{Path: uuid.New().String()}},
|
||||
Targets: []database.Target{{
|
||||
Name: "log",
|
||||
Type: database.TargetTypeLog,
|
||||
Name: "log",
|
||||
Type: database.TargetTypeLog,
|
||||
MaxAttempts: 1,
|
||||
}},
|
||||
}
|
||||
require.NoError(t, db.Create(&stored).Error)
|
||||
|
||||
@@ -46,9 +46,10 @@ func TestModelsDoNotMarshalTheirSecrets(t *testing.T) {
|
||||
{
|
||||
name: "target config",
|
||||
model: database.Target{
|
||||
Name: keptField,
|
||||
Type: database.TargetTypeSlack,
|
||||
Config: `{"webhookUrl":"https://h/s/` + marker + `"}`,
|
||||
Name: keptField,
|
||||
Type: database.TargetTypeSlack,
|
||||
Config: `{"webhookUrl":"https://h/s/` + marker + `"}`,
|
||||
MaxAttempts: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -97,8 +98,9 @@ func TestWebhookMarshalsNoTargetConfig(t *testing.T) {
|
||||
encoded := marshalModel(t, database.Webhook{
|
||||
Name: keptField,
|
||||
Targets: []database.Target{{
|
||||
Name: "slack",
|
||||
Config: `{"webhookUrl":"https://h/s/` + marker + `"}`,
|
||||
Name: "slack",
|
||||
Config: `{"webhookUrl":"https://h/s/` + marker + `"}`,
|
||||
MaxAttempts: 1,
|
||||
}},
|
||||
})
|
||||
|
||||
|
||||
@@ -29,9 +29,12 @@ type Target struct {
|
||||
// handler that marshals the model itself.
|
||||
Config string `gorm:"type:text" json:"-"` // JSON configuration
|
||||
|
||||
// For HTTP targets (max_retries=0 means fire-and-forget,
|
||||
// >0 enables retries with backoff)
|
||||
MaxRetries int `json:"maxRetries,omitempty"`
|
||||
// MaxAttempts is how many times an HTTP or Slack target attempts
|
||||
// each delivery in all, at least 1. 1 is a single attempt with no
|
||||
// circuit breaker; above 1, a failed attempt is retried with
|
||||
// backoff behind the breaker. The table's check constraint refuses
|
||||
// a target saved with less than 1, whatever its type.
|
||||
MaxAttempts int `gorm:"not null;check:max_attempts >= 1" json:"maxAttempts,omitempty"`
|
||||
|
||||
// Relations. No model marshals the record it belongs to:
|
||||
// Webhook.Targets leads back here, and the JSON could loop.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/webhooker/internal/database"
|
||||
)
|
||||
|
||||
// newHTTPTarget returns an http target that makes maxAttempts attempts
|
||||
// at each delivery.
|
||||
func newHTTPTarget(maxAttempts int) *database.Target {
|
||||
return &database.Target{
|
||||
WebhookID: uuid.New().String(),
|
||||
Name: "target",
|
||||
Type: database.TargetTypeHTTP,
|
||||
Config: `{"url":"https://example.com/"}`,
|
||||
MaxAttempts: maxAttempts,
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetCreate_RefusesZeroAttempts relies on the targets table
|
||||
// alone: nothing in the model checks the number, so the check
|
||||
// constraint is what keeps a target with 0 attempts out of the
|
||||
// database.
|
||||
func TestTargetCreate_RefusesZeroAttempts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := startedTestDB(t)
|
||||
|
||||
err := db.Create(newHTTPTarget(0)).Error
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "CHECK constraint failed")
|
||||
}
|
||||
|
||||
func TestTargetCreate_AcceptsOneAttempt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := startedTestDB(t)
|
||||
|
||||
require.NoError(t, db.Create(newHTTPTarget(1)).Error)
|
||||
}
|
||||
@@ -87,13 +87,13 @@ func TestMainSchemaHoldsEveryModel(t *testing.T) {
|
||||
Active: true,
|
||||
})
|
||||
assertRoundTrip(t, db, &database.Target{
|
||||
BaseModel: newBaseModel(),
|
||||
WebhookID: uuid.New().String(),
|
||||
Name: "target",
|
||||
Type: database.TargetTypeHTTP,
|
||||
Active: true,
|
||||
Config: `{"url":"https://example.com/"}`,
|
||||
MaxRetries: 3,
|
||||
BaseModel: newBaseModel(),
|
||||
WebhookID: uuid.New().String(),
|
||||
Name: "target",
|
||||
Type: database.TargetTypeHTTP,
|
||||
Active: true,
|
||||
Config: `{"url":"https://example.com/"}`,
|
||||
MaxAttempts: 3,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user