From 63b1279792ad8c5084568206195de58346c42941 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Tue, 30 Sep 2025 16:44:36 +0200 Subject: [PATCH] feat(feed): use exponential backoff for broken feeds Instead of having a hard limit of 3 (regular) tries for feeds with errors, use an exponential backoff by default, with an upper-limit of one week. See https://github.com/miniflux/v2/issues/3334 for context/discussions. --- internal/config/options.go | 4 ++-- internal/config/options_parsing_test.go | 4 ++-- internal/model/feed.go | 11 +++++++++++ miniflux.1 | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/internal/config/options.go b/internal/config/options.go index be8768a6..7007b42a 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -489,8 +489,8 @@ func NewConfigOptions() *configOptions { }, }, "POLLING_PARSING_ERROR_LIMIT": { - ParsedIntValue: 3, - RawValue: "3", + ParsedIntValue: 0, + RawValue: "0", ValueType: intType, Validator: func(rawValue string) error { return validateGreaterOrEqualThan(rawValue, 0) diff --git a/internal/config/options_parsing_test.go b/internal/config/options_parsing_test.go index cf965644..01270b23 100644 --- a/internal/config/options_parsing_test.go +++ b/internal/config/options_parsing_test.go @@ -1058,8 +1058,8 @@ func TestPollingLimitPerHostOptionParsing(t *testing.T) { func TestPollingParsingErrorLimitOptionParsing(t *testing.T) { configParser := NewConfigParser() - if configParser.options.PollingParsingErrorLimit() != 3 { - t.Fatalf("Expected POLLING_PARSING_ERROR_LIMIT to be 3 by default") + if configParser.options.PollingParsingErrorLimit() != 0 { + t.Fatalf("Expected POLLING_PARSING_ERROR_LIMIT to be 0 by default") } if err := configParser.parseLines([]string{"POLLING_PARSING_ERROR_LIMIT=5"}); err != nil { diff --git a/internal/model/feed.go b/internal/model/feed.go index 5e8a7aaa..bd22e2da 100644 --- a/internal/model/feed.go +++ b/internal/model/feed.go @@ -6,6 +6,7 @@ package model // import "miniflux.app/v2/internal/model" import ( "fmt" "io" + "math" "time" "miniflux.app/v2/internal/config" @@ -135,6 +136,16 @@ func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelay time.Duration) ti // Use the RSS TTL field, Retry-After, Cache-Control or Expires HTTP headers if defined. interval = max(interval, refreshDelay) + // Use a binary exponential backoff for feeds with parsing errors. + // https://en.wikipedia.org/wiki/Exponential_backoff + if f.ParsingErrorCount > 0 { + backoff := time.Duration(math.Pow(2, float64(f.ParsingErrorCount))) * time.Hour + // Set a hard limit at one week + if backoff.Hours() < time.Hour.Hours()*24*7 { + interval += backoff + } + } + // Limit the max interval value for misconfigured feeds. switch config.Opts.PollingScheduler() { case SchedulerRoundRobin: diff --git a/miniflux.1 b/miniflux.1 index b0ba0bbf..8bae5536 100644 --- a/miniflux.1 +++ b/miniflux.1 @@ -504,7 +504,7 @@ The maximum number of parsing errors that the program will try before stopping p .br Once the limit is reached, the user must refresh the feed manually. Set to 0 for unlimited. .br -Default is 3\&. +Default is 0\&. .TP .B POLLING_SCHEDULER Determines the strategy used to schedule feed polling.