aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/cmd
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-04-27 21:20:10 +0900
committernsfisis <nsfisis@gmail.com>2026-04-27 21:20:10 +0900
commitcb00405041ee4714b6e817e9570cfa10ae972840 (patch)
treeff98728b3e5e099eb9ac5556eeb407c68e0fc208 /backend/cmd
parent938863425bf8ad6c17e43b3da128f92cf6d6ab63 (diff)
downloadfeedaka-cb00405041ee4714b6e817e9570cfa10ae972840.tar.gz
feedaka-cb00405041ee4714b6e817e9570cfa10ae972840.tar.zst
feedaka-cb00405041ee4714b6e817e9570cfa10ae972840.zip
feat(backend): adapt feed fetch interval to update frequencyHEADmain
Add per-feed fetch_interval_seconds (clamped to [1h, 24h]) that halves on new articles and grows 1.5x when a fetch yields nothing, replacing the fixed 1h schedule with the 10min cooldown filter. Scheduler tick shortened to 30min so the 1h floor is honored with reasonable precision.
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)
+ }
+ })
+ }
+}