From 65ecb6aa121288dc2d838affe8dd79b7c64906b1 Mon Sep 17 00:00:00 2001 From: Ping Qiu Date: Sat, 14 Nov 2026 10:46:57 -0800 Subject: [PATCH] fix(mount): prevent negative activity-score shifts (#12631) fix(mount): bound activity decay shifts across time regressions --- weed/mount/page_writer/activity_score.go | 7 ++-- weed/mount/page_writer/activity_score_test.go | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 weed/mount/page_writer/activity_score_test.go diff --git a/weed/mount/page_writer/activity_score.go b/weed/mount/page_writer/activity_score.go index cec9a791ae..b8ccfa9b42 100644 --- a/weed/mount/page_writer/activity_score.go +++ b/weed/mount/page_writer/activity_score.go @@ -17,7 +17,8 @@ func NewActivityScore() *ActivityScore { func (as *ActivityScore) MarkRead() { now := time.Now().UnixNano() last := atomic.SwapInt64(&as.lastActiveTsNs, now) - deltaTime := (now - last) >> 30 // about number of seconds + // Concurrent timestamp swaps or a clock adjustment can make last newer than now. + deltaTime := max(0, min(63, (now-last)>>30)) // about number of seconds for { old := atomic.LoadInt64(&as.decayedActivenessScore) score := old>>deltaTime + 256 @@ -33,7 +34,7 @@ func (as *ActivityScore) MarkRead() { func (as *ActivityScore) MarkWrite() { now := time.Now().UnixNano() last := atomic.SwapInt64(&as.lastActiveTsNs, now) - deltaTime := (now - last) >> 30 // about number of seconds + deltaTime := max(0, min(63, (now-last)>>30)) // about number of seconds for { old := atomic.LoadInt64(&as.decayedActivenessScore) score := old>>deltaTime + 1024 @@ -47,6 +48,6 @@ func (as *ActivityScore) MarkWrite() { } func (as *ActivityScore) ActivityScore() int64 { - deltaTime := (time.Now().UnixNano() - atomic.LoadInt64(&as.lastActiveTsNs)) >> 30 + deltaTime := max(0, min(63, (time.Now().UnixNano()-atomic.LoadInt64(&as.lastActiveTsNs))>>30)) return atomic.LoadInt64(&as.decayedActivenessScore) >> deltaTime } diff --git a/weed/mount/page_writer/activity_score_test.go b/weed/mount/page_writer/activity_score_test.go new file mode 100644 index 0000000000..b3c9a13915 --- /dev/null +++ b/weed/mount/page_writer/activity_score_test.go @@ -0,0 +1,39 @@ +package page_writer + +import ( + "sync/atomic" + "testing" + "time" +) + +func TestActivityScoreHandlesFutureTimestamp(t *testing.T) { + for _, test := range []struct { + name string + call func(*ActivityScore) int64 + want int64 + }{ + {"MarkRead", func(score *ActivityScore) int64 { + score.MarkRead() + return score.ActivityScore() + }, 4352}, + {"MarkWrite", func(score *ActivityScore) int64 { + score.MarkWrite() + return score.ActivityScore() + }, 5120}, + {"ActivityScore", (*ActivityScore).ActivityScore, 4096}, + } { + t.Run(test.name, func(t *testing.T) { + score := NewActivityScore() + atomic.StoreInt64(&score.lastActiveTsNs, time.Now().Add(time.Hour).UnixNano()) + atomic.StoreInt64(&score.decayedActivenessScore, 4096) + defer func() { + if caught := recover(); caught != nil { + t.Fatalf("future last-active timestamp must not panic: %v", caught) + } + }() + if got := test.call(score); got != test.want { + t.Fatalf("score=%d, want %d without backwards-time decay", got, test.want) + } + }) + } +}