1
0
Fork 0
forked from 0x2E/fusion
fusion/service/pull/handle_test.go
Michael Lynch df412f17d3
Recover after feed fetch failure with exponential backoff (#108)
* Recover after feed fetch failure with exponential backoff

The current implementation stops attempting to fetch a feed if fusion encounters any error fetching it. The only way to continue fetching the feed is if the user manually forces a refresh.

This allows fusion to recover from feed fetch errors by tracking the number of consecutive failures and slowing down requests for consistent failure. If a feed always fails, we eventually slow to only checking it once per week.

Fixes #67

* Add comment
2025-03-24 11:11:18 +08:00

154 lines
5.1 KiB
Go

package pull_test
import (
"math"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/0x2e/fusion/model"
"github.com/0x2e/fusion/pkg/ptr"
"github.com/0x2e/fusion/service/pull"
)
func TestDecideFeedUpdateAction(t *testing.T) {
// Helper function to parse ISO8601 string to time.Time.
parseTime := func(iso8601 string) time.Time {
t, err := time.Parse(time.RFC3339, iso8601)
if err != nil {
panic(err)
}
return t
}
for _, tt := range []struct {
description string
currentTime time.Time
feed model.Feed
expectedAction pull.FeedUpdateAction
expectedSkipReason *pull.FeedSkipReason
}{
{
description: "suspended feed should skip update",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Suspended: ptr.To(true),
UpdatedAt: parseTime("2025-01-01T12:00:00Z"),
},
expectedAction: pull.ActionSkipUpdate,
expectedSkipReason: &pull.SkipReasonSuspended,
},
{
description: "feed should be updated when conditions are met",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Failure: ptr.To(""),
Suspended: ptr.To(false),
UpdatedAt: parseTime("2025-01-01T11:15:00Z"), // 45 minutes before current time
},
expectedAction: pull.ActionFetchUpdate,
expectedSkipReason: nil,
},
{
description: "feed with nil failure should be updated",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Failure: nil,
Suspended: ptr.To(false),
UpdatedAt: parseTime("2025-01-01T11:15:00Z"), // 45 minutes before current time
},
expectedAction: pull.ActionFetchUpdate,
expectedSkipReason: nil,
},
{
description: "feed with nil suspended should be updated",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Failure: ptr.To(""),
Suspended: nil,
UpdatedAt: parseTime("2025-01-01T11:15:00Z"), // 45 minutes before current time
},
expectedAction: pull.ActionFetchUpdate,
expectedSkipReason: nil,
},
{
description: "failed feed with 1 consecutive failure should skip update before 54 minutes",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Failure: ptr.To("dummy previous error"),
Suspended: ptr.To(false),
UpdatedAt: parseTime("2025-01-01T11:15:00Z"), // 45 minutes before current time
ConsecutiveFailures: 1,
},
expectedAction: pull.ActionSkipUpdate,
expectedSkipReason: &pull.SkipReasonCoolingOff,
},
{
description: "failed feed with 1 consecutive failure should be updated after 54 minutes",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Failure: ptr.To("dummy previous error"),
Suspended: ptr.To(false),
UpdatedAt: parseTime("2025-01-01T11:06:00Z"), // 54 minutes before current time
ConsecutiveFailures: 1,
},
expectedAction: pull.ActionFetchUpdate,
expectedSkipReason: nil,
},
{
description: "failed feed with 3 consecutive failures should skip update for 174 minutes",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Failure: ptr.To("dummy previous error"),
Suspended: ptr.To(false),
UpdatedAt: parseTime("2025-01-01T09:10:00Z"), // 170 minutes before current time
ConsecutiveFailures: 3,
},
expectedAction: pull.ActionSkipUpdate,
expectedSkipReason: &pull.SkipReasonCoolingOff,
},
{
description: "failed feed with 3 consecutive failures should be updated after 174 minutes",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Failure: ptr.To("dummy previous error"),
Suspended: ptr.To(false),
UpdatedAt: parseTime("2025-01-01T09:06:00Z"), // 174 minutes before current time
ConsecutiveFailures: 3,
},
expectedAction: pull.ActionFetchUpdate,
expectedSkipReason: nil,
},
{
description: "failed feed with many consecutive failures should not exceed maximum wait time of 7 days",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Failure: ptr.To("dummy previous error"),
Suspended: ptr.To(false),
UpdatedAt: parseTime("2024-12-30T12:00:00Z"), // 2 days before current time
ConsecutiveFailures: 10,
},
expectedAction: pull.ActionSkipUpdate,
expectedSkipReason: &pull.SkipReasonCoolingOff,
},
{
description: "failed feed with many consecutive failures should be updated after maximum wait time of 7 days",
currentTime: parseTime("2025-01-01T12:00:00Z"),
feed: model.Feed{
Failure: ptr.To("dummy previous error"),
Suspended: ptr.To(false),
UpdatedAt: parseTime("2024-12-25T12:00:00Z"), // 7 days before current time
ConsecutiveFailures: math.MaxUint,
},
expectedAction: pull.ActionFetchUpdate,
expectedSkipReason: nil,
},
} {
t.Run(tt.description, func(t *testing.T) {
action, skipReason := pull.DecideFeedUpdateAction(&tt.feed, tt.currentTime)
assert.Equal(t, tt.expectedAction, action)
assert.Equal(t, tt.expectedSkipReason, skipReason)
})
}
}