check / check (push) Successful in 3m34s
Each webhook's event database keeps running totals: one row for its events, with when the newest arrived, and one row per target for its deliveries, delivered and failed, each with what retention removed. Every write to them shares the transaction of the rows it counts, and a delivery already delivered or failed is not settled again. Deliveries get a finished_at column; it and target_id end the status index, so each target's deliveries finished in a window come from one index-range query grouped by target. Retention deletes 1000 expired events per transaction, pausing 200 ms between them so other writers get in, and stops between them on shutdown. The pane is its own template, its figures in tables. The schema changes in place with nothing back-filled, so an existing database must be recreated. Model: opus-5-5
178 lines
4.4 KiB
Go
178 lines
4.4 KiB
Go
package delivery_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// targetTotals reads one target's totals from a webhook database, all
|
|
// zero when it has no row.
|
|
func targetTotals(
|
|
t *testing.T, db *gorm.DB, targetID string,
|
|
) database.TargetTotals {
|
|
t.Helper()
|
|
|
|
var rows []database.TargetTotals
|
|
|
|
require.NoError(t, db.Where("target_id = ?", targetID).
|
|
Find(&rows).Error)
|
|
|
|
if len(rows) == 0 {
|
|
return database.TargetTotals{TargetID: targetID}
|
|
}
|
|
|
|
return rows[0]
|
|
}
|
|
|
|
// TestUpdateDeliveryStatus_FinishTimeAndTargetTotals pins what a status
|
|
// write records for the webhook page's statistics: the time a delivery
|
|
// finished, set only when it becomes delivered or failed, and one more
|
|
// on its target's delivered or failed total.
|
|
func TestUpdateDeliveryStatus_FinishTimeAndTargetTotals(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
status database.DeliveryStatus
|
|
finished bool
|
|
delivered int64
|
|
failed int64
|
|
}{
|
|
{database.DeliveryStatusRetrying, false, 0, 0},
|
|
{database.DeliveryStatusDelivered, true, 1, 0},
|
|
{database.DeliveryStatusFailed, true, 0, 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.status), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := testWebhookDB(t)
|
|
e := testEngine(t, 1)
|
|
event := seedEvent(t, db, `{}`)
|
|
targetID := uuid.New().String()
|
|
d := seedDelivery(
|
|
t, db, event.ID, targetID,
|
|
database.DeliveryStatusPending,
|
|
)
|
|
|
|
before := time.Now()
|
|
|
|
require.NoError(t, e.ExportUpdateDeliveryStatus(
|
|
db, &d, tt.status,
|
|
))
|
|
|
|
var stored database.Delivery
|
|
|
|
require.NoError(t, db.First(&stored, "id = ?", d.ID).Error)
|
|
assert.Equal(t, tt.status, stored.Status)
|
|
|
|
if tt.finished {
|
|
require.NotNil(t, stored.FinishedAt)
|
|
assert.False(t, stored.FinishedAt.Before(before))
|
|
} else {
|
|
assert.Nil(t, stored.FinishedAt)
|
|
}
|
|
|
|
assert.Equal(t, database.TargetTotals{
|
|
TargetID: targetID,
|
|
Delivered: tt.delivered,
|
|
Failed: tt.failed,
|
|
}, targetTotals(t, db, targetID))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestUpdateDeliveryStatus_DeletedDeliveryIsNotCounted covers a
|
|
// delivery retention deleted while the engine still held it. Failing
|
|
// it afterwards writes no row, so it adds no failure either: retention
|
|
// has already counted what it removed.
|
|
func TestUpdateDeliveryStatus_DeletedDeliveryIsNotCounted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := testWebhookDB(t)
|
|
e := testEngine(t, 1)
|
|
event := seedEvent(t, db, `{}`)
|
|
targetID := uuid.New().String()
|
|
d := seedDelivery(
|
|
t, db, event.ID, targetID,
|
|
database.DeliveryStatusRetrying,
|
|
)
|
|
|
|
require.NoError(t, db.Unscoped().
|
|
Delete(&database.Delivery{}, "id = ?", d.ID).Error)
|
|
|
|
require.NoError(t, e.ExportUpdateDeliveryStatus(
|
|
db, &d, database.DeliveryStatusFailed,
|
|
))
|
|
|
|
assert.Equal(t, database.TargetTotals{TargetID: targetID},
|
|
targetTotals(t, db, targetID))
|
|
}
|
|
|
|
// TestUpdateDeliveryStatus_FinishedDeliveryIsNotSettledAgain covers a
|
|
// delivery settled a second time, as recovery can do when a worker has
|
|
// settled it since recovery read it. Neither status writes over the
|
|
// first, and the totals do not move.
|
|
func TestUpdateDeliveryStatus_FinishedDeliveryIsNotSettledAgain(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
finished := []database.DeliveryStatus{
|
|
database.DeliveryStatusDelivered,
|
|
database.DeliveryStatusFailed,
|
|
}
|
|
|
|
for _, first := range finished {
|
|
t.Run(string(first), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := testWebhookDB(t)
|
|
e := testEngine(t, 1)
|
|
event := seedEvent(t, db, `{}`)
|
|
targetID := uuid.New().String()
|
|
d := seedDelivery(
|
|
t, db, event.ID, targetID,
|
|
database.DeliveryStatusRetrying,
|
|
)
|
|
|
|
// The delivery as recovery read it, before the worker
|
|
// settled it.
|
|
readBefore := d
|
|
|
|
require.NoError(t, e.ExportUpdateDeliveryStatus(
|
|
db, &d, first,
|
|
))
|
|
|
|
var settled database.Delivery
|
|
|
|
require.NoError(t, db.First(&settled, "id = ?", d.ID).Error)
|
|
require.NotNil(t, settled.FinishedAt)
|
|
|
|
totals := targetTotals(t, db, targetID)
|
|
|
|
for _, again := range finished {
|
|
stale := readBefore
|
|
|
|
require.NoError(t, e.ExportUpdateDeliveryStatus(
|
|
db, &stale, again,
|
|
))
|
|
}
|
|
|
|
var stored database.Delivery
|
|
|
|
require.NoError(t, db.First(&stored, "id = ?", d.ID).Error)
|
|
assert.Equal(t, first, stored.Status)
|
|
require.NotNil(t, stored.FinishedAt)
|
|
assert.True(t, settled.FinishedAt.Equal(*stored.FinishedAt))
|
|
assert.Equal(t, totals, targetTotals(t, db, targetID))
|
|
})
|
|
}
|
|
}
|