Skip to content

Commit 8deb634

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 8deb634

File tree

2 files changed

+65
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)