Use one recovery routine at start and on every sweep tick (closes #542)
check / check (push) Canceled after 0s

Start-up recovery and the 60-second sweep were two parallel sets of
functions doing the same job. recoverDeliveries now does it, run once at
start and then on every tick from one goroutine. A retrying delivery is
scheduled after what is left of its backoff, now if nothing is left, so
backoffElapsed is gone. The run at start still takes every pending
delivery and opens a webhook database whose file is missing; a tick
still takes only pending deliveries untouched for 15 minutes, at most
500, and skips a missing file. A retrying delivery is taken before its
event is read. The README describes one routine.

Model: opus-5-5
This commit is contained in:
2026-10-15 18:27:03 +00:00
parent f10440b7cc
commit 46aefde3cc
13 changed files with 622 additions and 524 deletions
+26 -26
View File
@@ -659,7 +659,7 @@ func TestRecoverPendingDeliveries(t *testing.T) {
s.Engine.ExportRecoverPendingDeliveries(
context.Background(), s.WebhookDB,
s.WebhookID,
s.WebhookID, atStart,
)
for i := range 3 {
@@ -756,7 +756,7 @@ func TestRecoverWebhookDeliveries_RetryingDeliveries(
)
s.Engine.ExportRecoverWebhookDeliveries(
context.Background(), s.WebhookID,
context.Background(), s.WebhookID, atStart,
)
select {
@@ -880,7 +880,7 @@ func TestRecoverSingleRetry_TypeNoLongerRetries(
)
s.Engine.ExportRecoverWebhookDeliveries(
context.Background(), s.WebhookID,
context.Background(), s.WebhookID, atStart,
)
iAssertTerminallyFailed(
@@ -903,8 +903,8 @@ func TestSweepSingleRetry_TypeNoLongerRetries(
t, s, database.TargetTypeDatabase,
)
s.Engine.ExportSweepWebhookRetries(
context.Background(), s.WebhookID,
s.Engine.ExportRecoverWebhookDeliveries(
context.Background(), s.WebhookID, onTick,
)
iAssertTerminallyFailed(
@@ -955,8 +955,8 @@ func TestFailUnretryableRetry_WritesNoTargetRow(
iSeedFailedResult(t, s.WebhookDB, d.ID)
s.Engine.ExportSweepWebhookRetries(
context.Background(), s.WebhookID,
s.Engine.ExportRecoverWebhookDeliveries(
context.Background(), s.WebhookID, onTick,
)
iAssertStatus(t, s.WebhookDB, d.ID,
@@ -987,7 +987,7 @@ func TestRecoverSingleRetry_UnknownTargetType(
deliveryID := iSeedRetryingWithType(t, s, unknown)
s.Engine.ExportRecoverWebhookDeliveries(
context.Background(), s.WebhookID,
context.Background(), s.WebhookID, atStart,
)
iAssertTerminallyFailed(t, s, deliveryID, unknown)
@@ -1008,8 +1008,8 @@ func TestSweepSingleRetry_UnknownTargetType(
deliveryID := iSeedRetryingWithType(t, s, unknown)
s.Engine.ExportSweepWebhookRetries(
context.Background(), s.WebhookID,
s.Engine.ExportRecoverWebhookDeliveries(
context.Background(), s.WebhookID, onTick,
)
iAssertTerminallyFailed(t, s, deliveryID, unknown)
@@ -1051,19 +1051,19 @@ func iCreateWebhook(
require.NoError(t, mainDB.Create(&webhook).Error)
}
// --- recoverInFlight Tests ---
// --- recoverDeliveries Tests ---
func TestRecoverInFlight_NoWebhooks(t *testing.T) {
func TestRecoverDeliveries_NoWebhooks(t *testing.T) {
t.Parallel()
s := newISetup(t)
s.Engine.ExportRecoverInFlight(
context.Background(),
s.Engine.ExportRecoverDeliveries(
context.Background(), atStart,
)
}
func TestRecoverInFlight_WithPendingDeliveries(
func TestRecoverDeliveries_WithPendingDeliveries(
t *testing.T,
) {
t.Parallel()
@@ -1090,8 +1090,8 @@ func TestRecoverInFlight_WithPendingDeliveries(
database.DeliveryStatusPending,
)
s.Engine.ExportRecoverInFlight(
context.Background(),
s.Engine.ExportRecoverDeliveries(
context.Background(), atStart,
)
select {
@@ -1102,16 +1102,16 @@ func TestRecoverInFlight_WithPendingDeliveries(
database.TargetTypeLog, task.TargetType,
)
case <-time.After(2 * time.Second):
t.Fatal("expected task from recoverInFlight")
t.Fatal("expected task from recovery at start")
}
}
// TestRecoverInFlight_ReportsAMissingWebhookDatabase covers a webhook
// whose database file is gone, after a partial restore say. Restart
// recovery opens every webhook's database, so the empty one made in its
// TestRecoverDeliveries_ReportsAMissingWebhookDatabase covers a webhook
// whose database file is gone, after a partial restore say. Recovery at
// start opens every webhook's database, so the empty one made in its
// place is reported at start, naming the file
// (https://git.eeqj.de/sneak/webhooker/issues/290).
func TestRecoverInFlight_ReportsAMissingWebhookDatabase(t *testing.T) {
func TestRecoverDeliveries_ReportsAMissingWebhookDatabase(t *testing.T) {
t.Parallel()
mainDB := iMainDB(t)
@@ -1131,7 +1131,7 @@ func TestRecoverInFlight_ReportsAMissingWebhookDatabase(t *testing.T) {
&http.Client{Timeout: 5 * time.Second}, 1,
)
engine.ExportRecoverInFlight(context.Background())
engine.ExportRecoverDeliveries(context.Background(), atStart)
assert.Contains(
t, logs.String(),
@@ -1140,10 +1140,10 @@ func TestRecoverInFlight_ReportsAMissingWebhookDatabase(t *testing.T) {
)
}
// TestRecoverInFlight_SkipsAWebhookDeletedAfterTheListIsRead covers a
// TestRecoverDeliveries_SkipsAWebhookDeletedAfterTheListIsRead covers a
// webhook deleted from the web interface while restart recovery runs.
// Its database file is gone, and recovery must not create it again.
func TestRecoverInFlight_SkipsAWebhookDeletedAfterTheListIsRead(
func TestRecoverDeliveries_SkipsAWebhookDeletedAfterTheListIsRead(
t *testing.T,
) {
t.Parallel()
@@ -1179,7 +1179,7 @@ func TestRecoverInFlight_SkipsAWebhookDeletedAfterTheListIsRead(
&http.Client{Timeout: 5 * time.Second}, 1,
)
engine.ExportRecoverInFlight(context.Background())
engine.ExportRecoverDeliveries(context.Background(), atStart)
require.True(t, deleted)
assert.False(t, dbMgr.DBExists(webhookID))