Add a statistics pane to the webhook page (closes #368)
check / check (push) Successful in 3m58s
check / check (push) Successful in 3m58s
Each webhook's event database keeps one row of running totals: events, deliveries and failures, and how many of each retention removed. Storing an event, creating a delivery, a delivery becoming failed and the retention sweep each update it in the transaction that writes or deletes the rows it counts. Deliveries get a finished_at column, the last column of the status index, so the last-10-minutes and last-24-hours figures are index-range counts. The pane is its own template, included at the top of the page. The schema changes in place with nothing back-filled, so an existing database must be recreated. Model: opus-5-5
This commit is contained in:
@@ -267,55 +267,101 @@ func retentionCutoff(
|
||||
|
||||
// reapExpired hard-deletes, in foreign-key-safe order, the delivery
|
||||
// results, deliveries, and events associated with events older than
|
||||
// cutoff. Deletes are unscoped so rows are physically removed rather
|
||||
// than soft-deleted, reclaiming disk. It returns the number of events
|
||||
// deleted.
|
||||
// cutoff, and adds what it deleted to the running totals, all in one
|
||||
// transaction. Deletes are unscoped so rows are physically removed
|
||||
// rather than soft-deleted, reclaiming disk. It returns the number of
|
||||
// events deleted.
|
||||
func reapExpired(db *gorm.DB, cutoff time.Time) (int64, error) {
|
||||
var removed Totals
|
||||
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
var err error
|
||||
|
||||
removed, err = deleteExpired(tx, cutoff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return AddTotals(tx, removed)
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return removed.EventsRemoved, nil
|
||||
}
|
||||
|
||||
// deleteExpired runs reapExpired's deletes and returns how many
|
||||
// events, deliveries and failed deliveries they removed.
|
||||
func deleteExpired(tx *gorm.DB, cutoff time.Time) (Totals, error) {
|
||||
var removed Totals
|
||||
|
||||
// Fresh subqueries are built per statement to avoid reusing a
|
||||
// mutated builder across executions.
|
||||
expiredEventIDs := func() *gorm.DB {
|
||||
return db.Model(&Event{}).
|
||||
return tx.Model(&Event{}).
|
||||
Select("id").
|
||||
Where("created_at < ?", cutoff)
|
||||
}
|
||||
expiredDeliveryIDs := func() *gorm.DB {
|
||||
return db.Model(&Delivery{}).
|
||||
return tx.Model(&Delivery{}).
|
||||
Select("id").
|
||||
Where("event_id IN (?)", expiredEventIDs())
|
||||
}
|
||||
|
||||
// 1. Delivery results whose delivery belongs to an expired event.
|
||||
res := db.Unscoped().
|
||||
res := tx.Unscoped().
|
||||
Where("delivery_id IN (?)", expiredDeliveryIDs()).
|
||||
Delete(&DeliveryResult{})
|
||||
if res.Error != nil {
|
||||
return 0, fmt.Errorf(
|
||||
return removed, fmt.Errorf(
|
||||
"deleting expired delivery results: %w",
|
||||
res.Error,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Deliveries belonging to an expired event.
|
||||
del := db.Unscoped().
|
||||
// 2. Deliveries belonging to an expired event, after counting the
|
||||
// failed ones among them. The status is tested in the select list
|
||||
// rather than the WHERE clause: there, SQLite would read every
|
||||
// failed delivery the webhook has through the status index,
|
||||
// instead of only the expired ones through the event_id index.
|
||||
var failed struct{ Count int64 }
|
||||
|
||||
err := tx.Unscoped().Model(&Delivery{}).
|
||||
Select("count(CASE WHEN status = ? THEN 1 END) AS count",
|
||||
DeliveryStatusFailed).
|
||||
Where("event_id IN (?)", expiredEventIDs()).
|
||||
Take(&failed).Error
|
||||
if err != nil {
|
||||
return removed, fmt.Errorf(
|
||||
"counting expired failed deliveries: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
del := tx.Unscoped().
|
||||
Where("event_id IN (?)", expiredEventIDs()).
|
||||
Delete(&Delivery{})
|
||||
if del.Error != nil {
|
||||
return 0, fmt.Errorf(
|
||||
return removed, fmt.Errorf(
|
||||
"deleting expired deliveries: %w",
|
||||
del.Error,
|
||||
)
|
||||
}
|
||||
|
||||
// 3. The expired events themselves.
|
||||
ev := db.Unscoped().
|
||||
ev := tx.Unscoped().
|
||||
Where("created_at < ?", cutoff).
|
||||
Delete(&Event{})
|
||||
if ev.Error != nil {
|
||||
return 0, fmt.Errorf(
|
||||
return removed, fmt.Errorf(
|
||||
"deleting expired events: %w",
|
||||
ev.Error,
|
||||
)
|
||||
}
|
||||
|
||||
return ev.RowsAffected, nil
|
||||
removed.EventsRemoved = ev.RowsAffected
|
||||
removed.DeliveriesRemoved = del.RowsAffected
|
||||
removed.FailuresRemoved = failed.Count
|
||||
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user