diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 50fa8b4c..cc4a24a4 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -7,7 +7,9 @@ import ( "bytes" "os" "reflect" + "slices" "testing" + "time" ) func TestLogFileDefaultValue(t *testing.T) { @@ -887,12 +889,22 @@ func TestSchedulerEntryFrequencyMaxInterval(t *testing.T) { t.Fatalf(`Parsing failure: %v`, err) } - expected := 30 + expected := 30 * time.Minute result := opts.SchedulerEntryFrequencyMaxInterval() if result != expected { t.Fatalf(`Unexpected SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL value, got %v instead of %v`, result, expected) } + + sorted := opts.SortedOptions(false) + i := slices.IndexFunc(sorted, func(opt *option) bool { + return opt.Key == "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL" + }) + + expectedSerialized := 30 + if got := sorted[i].Value; got != expectedSerialized { + t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) + } } func TestDefaultSchedulerEntryFrequencyMinIntervalValue(t *testing.T) { @@ -922,12 +934,22 @@ func TestSchedulerEntryFrequencyMinInterval(t *testing.T) { t.Fatalf(`Parsing failure: %v`, err) } - expected := 30 + expected := 30 * time.Minute result := opts.SchedulerEntryFrequencyMinInterval() if result != expected { t.Fatalf(`Unexpected SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL value, got %v instead of %v`, result, expected) } + + sorted := opts.SortedOptions(false) + i := slices.IndexFunc(sorted, func(opt *option) bool { + return opt.Key == "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL" + }) + + expectedSerialized := 30 + if got := sorted[i].Value; got != expectedSerialized { + t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) + } } func TestDefaultSchedulerEntryFrequencyFactorValue(t *testing.T) { @@ -992,12 +1014,22 @@ func TestSchedulerRoundRobin(t *testing.T) { t.Fatalf(`Parsing failure: %v`, err) } - expected := 15 + expected := 15 * time.Minute result := opts.SchedulerRoundRobinMinInterval() if result != expected { t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MIN_INTERVAL value, got %v instead of %v`, result, expected) } + + sorted := opts.SortedOptions(false) + i := slices.IndexFunc(sorted, func(opt *option) bool { + return opt.Key == "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL" + }) + + expectedSerialized := 15 + if got := sorted[i].Value; got != expectedSerialized { + t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) + } } func TestDefaultSchedulerRoundRobinMaxIntervalValue(t *testing.T) { @@ -1027,12 +1059,22 @@ func TestSchedulerRoundRobinMaxInterval(t *testing.T) { t.Fatalf(`Parsing failure: %v`, err) } - expected := 150 + expected := 150 * time.Minute result := opts.SchedulerRoundRobinMaxInterval() if result != expected { t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected) } + + sorted := opts.SortedOptions(false) + i := slices.IndexFunc(sorted, func(opt *option) bool { + return opt.Key == "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL" + }) + + expectedSerialized := 150 + if got := sorted[i].Value; got != expectedSerialized { + t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) + } } func TestPollingParsingErrorLimit(t *testing.T) { diff --git a/internal/config/options.go b/internal/config/options.go index a64e4530..10b6cfe5 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -32,11 +32,11 @@ const ( defaultForceRefreshInterval = 30 defaultBatchSize = 100 defaultPollingScheduler = "round_robin" - defaultSchedulerEntryFrequencyMinInterval = 5 - defaultSchedulerEntryFrequencyMaxInterval = 24 * 60 + defaultSchedulerEntryFrequencyMinInterval = 5 * time.Minute + defaultSchedulerEntryFrequencyMaxInterval = 24 * time.Hour defaultSchedulerEntryFrequencyFactor = 1 - defaultSchedulerRoundRobinMinInterval = 60 - defaultSchedulerRoundRobinMaxInterval = 1440 + defaultSchedulerRoundRobinMinInterval = 1 * time.Hour + defaultSchedulerRoundRobinMaxInterval = 24 * time.Hour defaultPollingParsingErrorLimit = 3 defaultRunMigrations = false defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable" @@ -132,11 +132,11 @@ type options struct { cleanupRemoveSessionsDays int forceRefreshInterval int batchSize int - schedulerEntryFrequencyMinInterval int - schedulerEntryFrequencyMaxInterval int + schedulerEntryFrequencyMinInterval time.Duration + schedulerEntryFrequencyMaxInterval time.Duration schedulerEntryFrequencyFactor int - schedulerRoundRobinMinInterval int - schedulerRoundRobinMaxInterval int + schedulerRoundRobinMinInterval time.Duration + schedulerRoundRobinMaxInterval time.Duration pollingFrequency int pollingLimitPerHost int pollingParsingErrorLimit int @@ -422,13 +422,13 @@ func (o *options) PollingScheduler() string { return o.pollingScheduler } -// SchedulerEntryFrequencyMaxInterval returns the maximum interval in minutes for the entry frequency scheduler. -func (o *options) SchedulerEntryFrequencyMaxInterval() int { +// SchedulerEntryFrequencyMaxInterval returns the maximum interval for the entry frequency scheduler. +func (o *options) SchedulerEntryFrequencyMaxInterval() time.Duration { return o.schedulerEntryFrequencyMaxInterval } -// SchedulerEntryFrequencyMinInterval returns the minimum interval in minutes for the entry frequency scheduler. -func (o *options) SchedulerEntryFrequencyMinInterval() int { +// SchedulerEntryFrequencyMinInterval returns the minimum interval for the entry frequency scheduler. +func (o *options) SchedulerEntryFrequencyMinInterval() time.Duration { return o.schedulerEntryFrequencyMinInterval } @@ -437,11 +437,11 @@ func (o *options) SchedulerEntryFrequencyFactor() int { return o.schedulerEntryFrequencyFactor } -func (o *options) SchedulerRoundRobinMinInterval() int { +func (o *options) SchedulerRoundRobinMinInterval() time.Duration { return o.schedulerRoundRobinMinInterval } -func (o *options) SchedulerRoundRobinMaxInterval() int { +func (o *options) SchedulerRoundRobinMaxInterval() time.Duration { return o.schedulerRoundRobinMaxInterval } @@ -781,11 +781,11 @@ func (o *options) SortedOptions(redactSecret bool) []*option { "MEDIA_PROXY_CUSTOM_URL": o.mediaProxyCustomURL, "ROOT_URL": o.rootURL, "RUN_MIGRATIONS": o.runMigrations, - "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval, - "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval, + "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": int(o.schedulerEntryFrequencyMaxInterval.Minutes()), + "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": int(o.schedulerEntryFrequencyMinInterval.Minutes()), "SCHEDULER_ENTRY_FREQUENCY_FACTOR": o.schedulerEntryFrequencyFactor, - "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": o.schedulerRoundRobinMinInterval, - "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": o.schedulerRoundRobinMaxInterval, + "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": int(o.schedulerRoundRobinMinInterval.Minutes()), + "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": int(o.schedulerRoundRobinMaxInterval.Minutes()), "SCHEDULER_SERVICE": o.schedulerService, "WATCHDOG": o.watchdog, "WORKER_POOL_SIZE": o.workerPoolSize, diff --git a/internal/config/parser.go b/internal/config/parser.go index 836c7d65..50b506d4 100644 --- a/internal/config/parser.go +++ b/internal/config/parser.go @@ -14,6 +14,7 @@ import ( "os" "strconv" "strings" + "time" ) // parser handles configuration parsing. @@ -150,15 +151,15 @@ func (p *parser) parseLines(lines []string) (err error) { case "POLLING_SCHEDULER": p.opts.pollingScheduler = strings.ToLower(parseString(value, defaultPollingScheduler)) case "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": - p.opts.schedulerEntryFrequencyMaxInterval = parseInt(value, defaultSchedulerEntryFrequencyMaxInterval) + p.opts.schedulerEntryFrequencyMaxInterval = parseInterval(value, time.Minute, defaultSchedulerEntryFrequencyMaxInterval) case "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": - p.opts.schedulerEntryFrequencyMinInterval = parseInt(value, defaultSchedulerEntryFrequencyMinInterval) + p.opts.schedulerEntryFrequencyMinInterval = parseInterval(value, time.Minute, defaultSchedulerEntryFrequencyMinInterval) case "SCHEDULER_ENTRY_FREQUENCY_FACTOR": p.opts.schedulerEntryFrequencyFactor = parseInt(value, defaultSchedulerEntryFrequencyFactor) case "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": - p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval) + p.opts.schedulerRoundRobinMinInterval = parseInterval(value, time.Minute, defaultSchedulerRoundRobinMinInterval) case "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": - p.opts.schedulerRoundRobinMaxInterval = parseInt(value, defaultSchedulerRoundRobinMaxInterval) + p.opts.schedulerRoundRobinMaxInterval = parseInterval(value, time.Minute, defaultSchedulerRoundRobinMaxInterval) case "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": p.opts.mediaProxyHTTPClientTimeout = parseInt(value, defaultMediaProxyHTTPClientTimeout) case "MEDIA_PROXY_MODE": diff --git a/internal/model/feed.go b/internal/model/feed.go index a25ebbe5..5e8a7aaa 100644 --- a/internal/model/feed.go +++ b/internal/model/feed.go @@ -6,7 +6,6 @@ package model // import "miniflux.app/v2/internal/model" import ( "fmt" "io" - "math" "time" "miniflux.app/v2/internal/config" @@ -69,11 +68,11 @@ type Feed struct { Entries Entries `json:"entries,omitempty"` // Internal attributes (not exposed in the API and not persisted in the database) - TTL int `json:"-"` - IconURL string `json:"-"` - UnreadCount int `json:"-"` - ReadCount int `json:"-"` - NumberOfVisibleEntries int `json:"-"` + TTL time.Duration `json:"-"` + IconURL string `json:"-"` + UnreadCount int `json:"-"` + ReadCount int `json:"-"` + NumberOfVisibleEntries int `json:"-"` } type FeedCounters struct { @@ -119,35 +118,33 @@ func (f *Feed) CheckedNow() { } // ScheduleNextCheck set "next_check_at" of a feed based on the scheduler selected from the configuration. -func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) int { +func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelay time.Duration) time.Duration { // Default to the global config Polling Frequency. - intervalMinutes := config.Opts.SchedulerRoundRobinMinInterval() + interval := config.Opts.SchedulerRoundRobinMinInterval() if config.Opts.PollingScheduler() == SchedulerEntryFrequency { if weeklyCount <= 0 { - intervalMinutes = config.Opts.SchedulerEntryFrequencyMaxInterval() + interval = config.Opts.SchedulerEntryFrequencyMaxInterval() } else { - intervalMinutes = int(math.Round(float64(7*24*60) / float64(weeklyCount*config.Opts.SchedulerEntryFrequencyFactor()))) - intervalMinutes = min(intervalMinutes, config.Opts.SchedulerEntryFrequencyMaxInterval()) - intervalMinutes = max(intervalMinutes, config.Opts.SchedulerEntryFrequencyMinInterval()) + interval = (7 * 24 * time.Hour) / time.Duration(weeklyCount*config.Opts.SchedulerEntryFrequencyFactor()) + interval = min(interval, config.Opts.SchedulerEntryFrequencyMaxInterval()) + interval = max(interval, config.Opts.SchedulerEntryFrequencyMinInterval()) } } // Use the RSS TTL field, Retry-After, Cache-Control or Expires HTTP headers if defined. - if refreshDelayInMinutes > 0 && refreshDelayInMinutes > intervalMinutes { - intervalMinutes = refreshDelayInMinutes - } + interval = max(interval, refreshDelay) // Limit the max interval value for misconfigured feeds. switch config.Opts.PollingScheduler() { case SchedulerRoundRobin: - intervalMinutes = min(intervalMinutes, config.Opts.SchedulerRoundRobinMaxInterval()) + interval = min(interval, config.Opts.SchedulerRoundRobinMaxInterval()) case SchedulerEntryFrequency: - intervalMinutes = min(intervalMinutes, config.Opts.SchedulerEntryFrequencyMaxInterval()) + interval = min(interval, config.Opts.SchedulerEntryFrequencyMaxInterval()) } - f.NextCheckAt = time.Now().Add(time.Minute * time.Duration(intervalMinutes)) - return intervalMinutes + f.NextCheckAt = time.Now().Add(interval) + return interval } // FeedCreationRequest represents the request to create a feed. diff --git a/internal/model/feed_test.go b/internal/model/feed_test.go index 43e13327..6cc7bc59 100644 --- a/internal/model/feed_test.go +++ b/internal/model/feed_test.go @@ -67,11 +67,11 @@ func TestFeedCheckedNow(t *testing.T) { } } -func checkTargetInterval(t *testing.T, feed *Feed, targetInterval int, timeBefore time.Time, message string) { - if feed.NextCheckAt.Before(timeBefore.Add(time.Minute * time.Duration(targetInterval))) { +func checkTargetInterval(t *testing.T, feed *Feed, targetInterval time.Duration, timeBefore time.Time, message string) { + if feed.NextCheckAt.Before(timeBefore.Add(targetInterval)) { t.Errorf(`The next_check_at should be after timeBefore + %s`, message) } - if feed.NextCheckAt.After(time.Now().Add(time.Minute * time.Duration(targetInterval))) { + if feed.NextCheckAt.After(time.Now().Add(targetInterval)) { t.Errorf(`The next_check_at should be before now + %s`, message) } } @@ -188,7 +188,7 @@ func TestFeedScheduleNextCheckRoundRobinMinInterval(t *testing.T) { t.Error(`The next_check_at must be set`) } - expectedInterval := minInterval + expectedInterval := time.Duration(minInterval) * time.Minute checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinMinInterval") } @@ -217,7 +217,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMaxInterval(t *testing.T) { t.Error(`The next_check_at must be set`) } - targetInterval := maxInterval + targetInterval := time.Duration(maxInterval) * time.Minute checkTargetInterval(t, feed, targetInterval, timeBefore, "entry frequency max interval") } @@ -246,7 +246,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMaxIntervalZeroWeeklyCount(t *testin t.Error(`The next_check_at must be set`) } - targetInterval := maxInterval + targetInterval := time.Duration(maxInterval) * time.Minute checkTargetInterval(t, feed, targetInterval, timeBefore, "entry frequency max interval") } @@ -275,7 +275,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMinInterval(t *testing.T) { t.Error(`The next_check_at must be set`) } - targetInterval := minInterval + targetInterval := time.Duration(minInterval) * time.Minute checkTargetInterval(t, feed, targetInterval, timeBefore, "entry frequency min interval") } @@ -301,7 +301,7 @@ func TestFeedScheduleNextCheckEntryFrequencyFactor(t *testing.T) { t.Error(`The next_check_at must be set`) } - targetInterval := config.Opts.SchedulerEntryFrequencyMaxInterval() / factor + targetInterval := config.Opts.SchedulerEntryFrequencyMaxInterval() / time.Duration(factor) checkTargetInterval(t, feed, targetInterval, timeBefore, "factor * count") } @@ -326,17 +326,17 @@ func TestFeedScheduleNextCheckEntryFrequencySmallNewTTL(t *testing.T) { // Use a very large weekly count to trigger the min interval weeklyCount := largeWeeklyCount // TTL is smaller than minInterval. - newTTL := minInterval / 2 + newTTL := time.Duration(minInterval) * time.Minute / 2 feed.ScheduleNextCheck(weeklyCount, newTTL) if feed.NextCheckAt.IsZero() { t.Error(`The next_check_at must be set`) } - targetInterval := minInterval + targetInterval := time.Duration(minInterval) * time.Minute checkTargetInterval(t, feed, targetInterval, timeBefore, "entry frequency min interval") - if feed.NextCheckAt.Before(timeBefore.Add(time.Minute * time.Duration(newTTL))) { + if feed.NextCheckAt.Before(timeBefore.Add(newTTL)) { t.Error(`The next_check_at should be after timeBefore + TTL`) } } @@ -362,7 +362,7 @@ func TestFeedScheduleNextCheckEntryFrequencyLargeNewTTL(t *testing.T) { // Use a very large weekly count to trigger the min interval weeklyCount := largeWeeklyCount // TTL is larger than minInterval. - newTTL := minInterval * 2 + newTTL := time.Duration(minInterval) * time.Minute * 2 feed.ScheduleNextCheck(weeklyCount, newTTL) if feed.NextCheckAt.IsZero() { diff --git a/internal/reader/fetcher/response_handler.go b/internal/reader/fetcher/response_handler.go index c7ce0404..6709ee56 100644 --- a/internal/reader/fetcher/response_handler.go +++ b/internal/reader/fetcher/response_handler.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "log/slog" - "math" "net" "net/http" "net/url" @@ -54,18 +53,19 @@ func (r *ResponseHandler) ETag() string { return r.httpResponse.Header.Get("ETag") } -func (r *ResponseHandler) ExpiresInMinutes() int { +func (r *ResponseHandler) Expires() time.Duration { expiresHeaderValue := r.httpResponse.Header.Get("Expires") if expiresHeaderValue != "" { t, err := time.Parse(time.RFC1123, expiresHeaderValue) if err == nil { - return int(math.Ceil(time.Until(t).Minutes())) + // This rounds up to the next minute by rounding down and just adding a minute. + return time.Until(t).Truncate(time.Minute) + time.Minute } } return 0 } -func (r *ResponseHandler) CacheControlMaxAgeInMinutes() int { +func (r *ResponseHandler) CacheControlMaxAge() time.Duration { cacheControlHeaderValue := r.httpResponse.Header.Get("Cache-Control") if cacheControlHeaderValue != "" { for _, directive := range strings.Split(cacheControlHeaderValue, ",") { @@ -73,7 +73,7 @@ func (r *ResponseHandler) CacheControlMaxAgeInMinutes() int { if strings.HasPrefix(directive, "max-age=") { maxAge, err := strconv.Atoi(strings.TrimPrefix(directive, "max-age=")) if err == nil { - return int(math.Ceil(float64(maxAge) / 60)) + return time.Duration(maxAge) * time.Second } } } @@ -81,17 +81,17 @@ func (r *ResponseHandler) CacheControlMaxAgeInMinutes() int { return 0 } -func (r *ResponseHandler) ParseRetryDelay() int { +func (r *ResponseHandler) ParseRetryDelay() time.Duration { retryAfterHeaderValue := r.httpResponse.Header.Get("Retry-After") if retryAfterHeaderValue != "" { // First, try to parse as an integer (number of seconds) if seconds, err := strconv.Atoi(retryAfterHeaderValue); err == nil { - return seconds + return time.Duration(seconds) * time.Second } // If not an integer, try to parse as an HTTP-date if t, err := time.Parse(time.RFC1123, retryAfterHeaderValue); err == nil { - return int(time.Until(t).Seconds()) + return time.Until(t).Truncate(time.Second) } } return 0 diff --git a/internal/reader/fetcher/response_handler_test.go b/internal/reader/fetcher/response_handler_test.go index cc675506..5934eab2 100644 --- a/internal/reader/fetcher/response_handler_test.go +++ b/internal/reader/fetcher/response_handler_test.go @@ -72,7 +72,7 @@ func TestIsModified(t *testing.T) { func TestRetryDelay(t *testing.T) { var testCases = map[string]struct { RetryAfterHeader string - ExpectedDelay int + ExpectedDelay time.Duration }{ "Empty header": { RetryAfterHeader: "", @@ -80,11 +80,11 @@ func TestRetryDelay(t *testing.T) { }, "Integer value": { RetryAfterHeader: "42", - ExpectedDelay: 42, + ExpectedDelay: 42 * time.Second, }, "HTTP-date": { RetryAfterHeader: time.Now().Add(42 * time.Second).Format(time.RFC1123), - ExpectedDelay: 41, + ExpectedDelay: 41 * time.Second, }, } for name, tc := range testCases { @@ -105,20 +105,20 @@ func TestRetryDelay(t *testing.T) { func TestExpiresInMinutes(t *testing.T) { var testCases = map[string]struct { - ExpiresHeader string - ExpectedMinutes int + ExpiresHeader string + Expected time.Duration }{ "Empty header": { - ExpiresHeader: "", - ExpectedMinutes: 0, + ExpiresHeader: "", + Expected: 0, }, "Valid Expires header": { - ExpiresHeader: time.Now().Add(10 * time.Minute).Format(time.RFC1123), - ExpectedMinutes: 10, + ExpiresHeader: time.Now().Add(10 * time.Minute).Format(time.RFC1123), + Expected: 10 * time.Minute, }, "Invalid Expires header": { - ExpiresHeader: "invalid-date", - ExpectedMinutes: 0, + ExpiresHeader: "invalid-date", + Expected: 0, }, } for name, tc := range testCases { @@ -130,8 +130,8 @@ func TestExpiresInMinutes(t *testing.T) { Header: header, }, } - if tc.ExpectedMinutes != rh.ExpiresInMinutes() { - t.Errorf("Expected %d, got %d for scenario %q", tc.ExpectedMinutes, rh.ExpiresInMinutes(), name) + if tc.Expected != rh.Expires() { + t.Errorf("Expected %d, got %d for scenario %q", tc.Expected, rh.Expires(), name) } }) } @@ -140,23 +140,23 @@ func TestExpiresInMinutes(t *testing.T) { func TestCacheControlMaxAgeInMinutes(t *testing.T) { var testCases = map[string]struct { CacheControlHeader string - ExpectedMinutes int + Expected time.Duration }{ "Empty header": { CacheControlHeader: "", - ExpectedMinutes: 0, + Expected: 0, }, "Valid max-age": { CacheControlHeader: "max-age=600", - ExpectedMinutes: 10, + Expected: 10 * time.Minute, }, "Invalid max-age": { CacheControlHeader: "max-age=invalid", - ExpectedMinutes: 0, + Expected: 0, }, "Multiple directives": { CacheControlHeader: "no-cache, max-age=300", - ExpectedMinutes: 5, + Expected: 5 * time.Minute, }, } for name, tc := range testCases { @@ -168,8 +168,8 @@ func TestCacheControlMaxAgeInMinutes(t *testing.T) { Header: header, }, } - if tc.ExpectedMinutes != rh.CacheControlMaxAgeInMinutes() { - t.Errorf("Expected %d, got %d for scenario %q", tc.ExpectedMinutes, rh.CacheControlMaxAgeInMinutes(), name) + if tc.Expected != rh.CacheControlMaxAge() { + t.Errorf("Expected %d, got %d for scenario %q", tc.Expected, rh.CacheControlMaxAge(), name) } }) } diff --git a/internal/reader/handler/handler.go b/internal/reader/handler/handler.go index c193b31d..6d8da5b8 100644 --- a/internal/reader/handler/handler.go +++ b/internal/reader/handler/handler.go @@ -7,6 +7,7 @@ import ( "bytes" "errors" "log/slog" + "time" "miniflux.app/v2/internal/config" "miniflux.app/v2/internal/integration" @@ -208,7 +209,7 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool } weeklyEntryCount := 0 - refreshDelayInMinutes := 0 + var refreshDelay time.Duration if config.Opts.PollingScheduler() == model.SchedulerEntryFrequency { var weeklyCountErr error weeklyEntryCount, weeklyCountErr = store.WeeklyFeedEntryCount(userID, feedID) @@ -218,7 +219,7 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool } originalFeed.CheckedNow() - originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes) + originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelay) requestBuilder := fetcher.NewRequestBuilder() requestBuilder.WithUsernameAndPassword(originalFeed.Username, originalFeed.Password) @@ -242,15 +243,14 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool defer responseHandler.Close() if responseHandler.IsRateLimited() { - retryDelayInSeconds := responseHandler.ParseRetryDelay() - refreshDelayInMinutes = retryDelayInSeconds / 60 - calculatedNextCheckIntervalInMinutes := originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes) + retryDelay := responseHandler.ParseRetryDelay() + calculatedNextCheckInterval := originalFeed.ScheduleNextCheck(weeklyEntryCount, retryDelay) slog.Warn("Feed is rate limited", slog.String("feed_url", originalFeed.FeedURL), - slog.Int("retry_delay_in_seconds", retryDelayInSeconds), - slog.Int("refresh_delay_in_minutes", refreshDelayInMinutes), - slog.Int("calculated_next_check_interval_in_minutes", calculatedNextCheckIntervalInMinutes), + slog.Int("retry_delay_in_seconds", int(retryDelay.Seconds())), + slog.Int("refresh_delay_in_minutes", int(refreshDelay.Minutes())), + slog.Int("calculated_next_check_interval_in_minutes", int(calculatedNextCheckInterval.Minutes())), slog.Time("new_next_check_at", originalFeed.NextCheckAt), ) } @@ -316,22 +316,22 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool // Use the RSS TTL value, or the Cache-Control or Expires HTTP headers if available. // Otherwise, we use the default value from the configuration (min interval parameter). feedTTLValue := updatedFeed.TTL - cacheControlMaxAgeValue := responseHandler.CacheControlMaxAgeInMinutes() - expiresValue := responseHandler.ExpiresInMinutes() - refreshDelayInMinutes = max(feedTTLValue, cacheControlMaxAgeValue, expiresValue) + cacheControlMaxAgeValue := responseHandler.CacheControlMaxAge() + expiresValue := responseHandler.Expires() + refreshDelay = max(feedTTLValue, cacheControlMaxAgeValue, expiresValue) // Set the next check at with updated arguments. - calculatedNextCheckIntervalInMinutes := originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes) + calculatedNextCheckInterval := originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelay) slog.Debug("Updated next check date", slog.Int64("user_id", userID), slog.Int64("feed_id", feedID), slog.String("feed_url", originalFeed.FeedURL), - slog.Int("feed_ttl_minutes", feedTTLValue), - slog.Int("cache_control_max_age_in_minutes", cacheControlMaxAgeValue), - slog.Int("expires_in_minutes", expiresValue), - slog.Int("refresh_delay_in_minutes", refreshDelayInMinutes), - slog.Int("calculated_next_check_interval_in_minutes", calculatedNextCheckIntervalInMinutes), + slog.Int("feed_ttl_minutes", int(feedTTLValue.Minutes())), + slog.Int("cache_control_max_age_in_minutes", int(cacheControlMaxAgeValue.Minutes())), + slog.Int("expires_in_minutes", int(expiresValue.Minutes())), + slog.Int("refresh_delay_in_minutes", int(refreshDelay.Minutes())), + slog.Int("calculated_next_check_interval_in_minutes", int(calculatedNextCheckInterval.Minutes())), slog.Time("new_next_check_at", originalFeed.NextCheckAt), ) diff --git a/internal/reader/rss/adapter.go b/internal/reader/rss/adapter.go index 2e1f386b..95c86484 100644 --- a/internal/reader/rss/adapter.go +++ b/internal/reader/rss/adapter.go @@ -55,7 +55,7 @@ func (r *rssAdapter) buildFeed(baseURL string) *model.Feed { // Get TTL if defined. if r.rss.Channel.TTL != "" { if ttl, err := strconv.Atoi(r.rss.Channel.TTL); err == nil { - feed.TTL = ttl + feed.TTL = time.Duration(ttl) * time.Minute } } diff --git a/internal/reader/rss/parser_test.go b/internal/reader/rss/parser_test.go index 422073fe..63b70f7d 100644 --- a/internal/reader/rss/parser_test.go +++ b/internal/reader/rss/parser_test.go @@ -2151,7 +2151,7 @@ func TestParseFeedWithTTLField(t *testing.T) { t.Fatal(err) } - if feed.TTL != 60 { + if feed.TTL != 60*time.Minute { t.Errorf("Incorrect TTL, got: %d", feed.TTL) } }