aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'backend/cmd')
-rw-r--r--backend/cmd/serve.go68
-rw-r--r--backend/cmd/serve_test.go30
2 files changed, 72 insertions, 26 deletions
diff --git a/backend/cmd/serve.go b/backend/cmd/serve.go
index 4b32868..75bcb53 100644
--- a/backend/cmd/serve.go
+++ b/backend/cmd/serve.go
@@ -23,46 +23,62 @@ import (
"undef.ninja/x/feedaka/feed"
)
-func fetchOneFeed(feedID int64, url string, ctx context.Context, queries *db.Queries) error {
- log.Printf("Fetching %s...\n", url)
- result, err := feed.Fetch(ctx, url)
- if err != nil {
- return err
+const (
+ minFetchIntervalSeconds = 60 * 60 // 1 hour
+ maxFetchIntervalSeconds = 24 * 60 * 60 // 1 day
+)
+
+// nextFetchInterval adapts the per-feed poll interval based on whether the
+// last fetch yielded new articles. Active feeds converge toward the floor
+// quickly (halving) while quiet feeds back off gently (1.5x) so a brief lull
+// doesn't push the interval to the ceiling.
+func nextFetchInterval(current int64, hadNewArticles bool) int64 {
+ var next int64
+ if hadNewArticles {
+ next = current / 2
+ } else {
+ next = current * 3 / 2
}
- return feed.Sync(ctx, queries, feedID, result.Feed)
+ if next < minFetchIntervalSeconds {
+ next = minFetchIntervalSeconds
+ }
+ if next > maxFetchIntervalSeconds {
+ next = maxFetchIntervalSeconds
+ }
+ return next
}
-func listFeedsToBeFetched(ctx context.Context, queries *db.Queries) (map[int64]string, error) {
- feeds, err := queries.GetFeedsToFetch(ctx)
+func fetchOneFeed(ctx context.Context, queries *db.Queries, f db.GetFeedsToFetchRow) error {
+ log.Printf("Fetching %s...\n", f.Url)
+ result, err := feed.Fetch(ctx, f.Url)
+ if err != nil {
+ return err
+ }
+ newCount, err := feed.Sync(ctx, queries, f.ID, result.Feed)
if err != nil {
- return nil, err
+ return err
}
-
- result := make(map[int64]string)
- for _, feed := range feeds {
- fetchedAtTime, err := time.Parse(time.RFC3339, feed.FetchedAt)
- if err != nil {
- log.Fatal(err)
- }
- now := time.Now().UTC()
- if now.Sub(fetchedAtTime).Minutes() <= 10 {
- continue
+ next := nextFetchInterval(f.FetchIntervalSeconds, newCount > 0)
+ if next != f.FetchIntervalSeconds {
+ if err := queries.UpdateFeedFetchInterval(ctx, db.UpdateFeedFetchIntervalParams{
+ FetchIntervalSeconds: next,
+ ID: f.ID,
+ }); err != nil {
+ return err
}
- result[feed.ID] = feed.Url
}
- return result, nil
+ return nil
}
func fetchAllFeeds(ctx context.Context, queries *db.Queries) error {
- feeds, err := listFeedsToBeFetched(ctx, queries)
+ feeds, err := queries.GetFeedsToFetch(ctx)
if err != nil {
return err
}
var result *multierror.Error
- for feedID, url := range feeds {
- err := fetchOneFeed(feedID, url, ctx, queries)
- if err != nil {
+ for _, f := range feeds {
+ if err := fetchOneFeed(ctx, queries, f); err != nil {
result = multierror.Append(result, err)
}
time.Sleep(5 * time.Second)
@@ -121,7 +137,7 @@ func RunServe(database *sql.DB, cfg *config.Config, publicFS embed.FS) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- scheduled(ctx, 1*time.Hour, func() {
+ scheduled(ctx, 30*time.Minute, func() {
err := fetchAllFeeds(ctx, queries)
if err != nil {
log.Printf("Failed to fetch feeds: %v\n", err)
diff --git a/backend/cmd/serve_test.go b/backend/cmd/serve_test.go
new file mode 100644
index 0000000..82b7d53
--- /dev/null
+++ b/backend/cmd/serve_test.go
@@ -0,0 +1,30 @@
+package cmd
+
+import "testing"
+
+func TestNextFetchInterval(t *testing.T) {
+ tests := []struct {
+ name string
+ current int64
+ hadNewArticles bool
+ want int64
+ }{
+ {"new articles halve from mid-range", 14400, true, 7200},
+ {"new articles halve from max", maxFetchIntervalSeconds, true, 43200},
+ {"new articles clamp at min when halving below", 3600, true, minFetchIntervalSeconds},
+ {"new articles clamp at min from slightly above", 4000, true, minFetchIntervalSeconds},
+ {"no articles grow by 1.5x from min", minFetchIntervalSeconds, false, 5400},
+ {"no articles grow by 1.5x from mid-range", 14400, false, 21600},
+ {"no articles clamp at max", maxFetchIntervalSeconds, false, maxFetchIntervalSeconds},
+ {"no articles clamp at max from slightly below", 60000, false, maxFetchIntervalSeconds},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := nextFetchInterval(tt.current, tt.hadNewArticles)
+ if got != tt.want {
+ t.Errorf("nextFetchInterval(%d, %v) = %d, want %d", tt.current, tt.hadNewArticles, got, tt.want)
+ }
+ })
+ }
+}