Skip to content

Commit fd3c0f4

Browse files
committed
feat(feed): use exponential backoff for broken feeds
Add an exponential backoff for broken feeds, with an upper-limit of one week. See #3334 for context/discussions.
1 parent d2c783b commit fd3c0f4

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

internal/model/feed.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package model // import "miniflux.app/v2/internal/model"
66
import (
77
"fmt"
88
"io"
9+
"math"
910
"time"
1011

1112
"miniflux.app/v2/internal/config"
@@ -20,6 +21,9 @@ const (
2021
DefaultFeedSortingDirection = "desc"
2122
)
2223

24+
// A feed with parsing errors will be checked at least every week.
25+
var backoffMax = time.Duration(time.Hour * 24 * 7)
26+
2327
// Feed represents a feed in the application.
2428
type Feed struct {
2529
ID int64 `json:"id"`
@@ -143,10 +147,24 @@ func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelay time.Duration) ti
143147
interval = min(interval, config.Opts.SchedulerEntryFrequencyMaxInterval())
144148
}
145149

150+
interval += backoff(f.ParsingErrorCount)
151+
146152
f.NextCheckAt = time.Now().Add(interval)
147153
return interval
148154
}
149155

156+
func backoff(count int) time.Duration {
157+
if count == 0 {
158+
return 0
159+
}
160+
// https://en.wikipedia.org/wiki/Exponential_backoff
161+
backoff := time.Duration(math.Pow(2, float64(count))) * time.Hour
162+
if backoff > backoffMax {
163+
return backoffMax
164+
}
165+
return backoff
166+
}
167+
150168
// FeedCreationRequest represents the request to create a feed.
151169
type FeedCreationRequest struct {
152170
FeedURL string `json:"feed_url"`

internal/model/feed_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,47 @@ func TestFeedScheduleNextCheckEntryFrequencyLargeNewTTL(t *testing.T) {
376376
t.Error(`The next_check_at should be after timeBefore + entry frequency min interval`)
377377
}
378378
}
379+
380+
func TestFeedScheduleNextCheckParsingErrorsBackoff(t *testing.T) {
381+
for count := range 10 {
382+
f1 := &Feed{ParsingErrorCount: 0}
383+
f2 := &Feed{ParsingErrorCount: count}
384+
newTTL := time.Duration(1) * time.Minute * 2
385+
f1.ScheduleNextCheck(0, newTTL)
386+
f2.ScheduleNextCheck(0, newTTL)
387+
388+
if f1.NextCheckAt.IsZero() {
389+
t.Error(`The next_check_at for f1 must be set`)
390+
}
391+
if f2.NextCheckAt.IsZero() {
392+
t.Error(`The next_check_at for f2 must be set`)
393+
}
394+
395+
if f1.NextCheckAt.Add(backoff(count)).Sub(f2.NextCheckAt).Minutes() > 10 {
396+
t.Error("The next_check_at should have been using the exponential backoff.")
397+
}
398+
}
399+
}
400+
401+
func TestFeedScheduleNextCheckParsingErrorsBackoffMax(t *testing.T) {
402+
f1 := &Feed{ParsingErrorCount: 0}
403+
newTTL := time.Duration(1) * time.Minute * 2
404+
f1.ScheduleNextCheck(0, newTTL)
405+
if f1.NextCheckAt.IsZero() {
406+
t.Error(`The next_check_at for f1 must be set`)
407+
}
408+
409+
for count := range 128 {
410+
f2 := &Feed{ParsingErrorCount: count}
411+
f2.ScheduleNextCheck(0, newTTL)
412+
413+
if f2.NextCheckAt.IsZero() {
414+
t.Error(`The next_check_at for f1 must be set`)
415+
}
416+
417+
offset := f2.NextCheckAt.Sub(f1.NextCheckAt)
418+
if offset > backoffMax+time.Minute*1 {
419+
t.Errorf("The next_check_at's offset for errors (%q) for %d errors should never be bigger than %q.", offset, count, backoffMax)
420+
}
421+
}
422+
}

0 commit comments

Comments
 (0)