mirror of
https://github.com/miniflux/v2.git
synced 2025-08-26 18:21:01 +00:00
refactor(reader): use time.Duration instead of minutes count
In general, duration is used as time unit representation. At some places when int is returned, there's no documentation which unit is used. So just convert to time.Duration ASAP.
This commit is contained in:
parent
03021af53c
commit
ed3bf59356
10 changed files with 144 additions and 104 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue