mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-11-05 00:50:21 +00:00
feat(server): online session history with configurable stats window (#1243)
Add persisted online sessions, configurable retention and online history statistics with lifecycle, recovery, timezone and concurrency regression coverage. Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
1 parent
9881c47f0c
commit
8721a15b12
22 files changed
+2242
-20
No files matched your search
@@ -0,0 +1,220 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var testZone = time.FixedZone("TEST", 8*3600)
|
||||
|
||||
func ts(y int, mo time.Month, d, h, mi, s int) time.Time {
|
||||
return time.Date(y, mo, d, h, mi, s, 0, testZone)
|
||||
}
|
||||
|
||||
func TestClipOnlineSessions(t *testing.T) {
|
||||
windowStart := ts(2026, 10, 1, 0, 0, 0)
|
||||
windowEnd := ts(2026, 10, 8, 0, 0, 0)
|
||||
|
||||
t.Run("session fully inside window", func(t *testing.T) {
|
||||
sessions := []OnlineSession{
|
||||
{ServerID: 1, StartedAt: ts(2026, 10, 2, 8, 0, 0), EndedAt: ts(2026, 10, 2, 9, 30, 0)},
|
||||
}
|
||||
periods := ClipOnlineSessions(sessions, windowStart, windowEnd)
|
||||
require.Len(t, periods, 1)
|
||||
require.Equal(t, ts(2026, 10, 2, 8, 0, 0), periods[0].Start)
|
||||
require.Equal(t, ts(2026, 10, 2, 9, 30, 0), periods[0].End)
|
||||
require.False(t, periods[0].Ongoing)
|
||||
})
|
||||
|
||||
t.Run("session started before window is clipped", func(t *testing.T) {
|
||||
sessions := []OnlineSession{
|
||||
{ServerID: 1, StartedAt: ts(2026, 9, 28, 12, 0, 0), EndedAt: ts(2026, 10, 1, 5, 0, 0)},
|
||||
}
|
||||
periods := ClipOnlineSessions(sessions, windowStart, windowEnd)
|
||||
require.Len(t, periods, 1)
|
||||
require.Equal(t, windowStart, periods[0].Start)
|
||||
require.Equal(t, ts(2026, 10, 1, 5, 0, 0), periods[0].End)
|
||||
})
|
||||
|
||||
t.Run("ongoing session ends at window end", func(t *testing.T) {
|
||||
sessions := []OnlineSession{
|
||||
{ServerID: 1, StartedAt: ts(2026, 10, 7, 10, 0, 0), Ongoing: true},
|
||||
}
|
||||
periods := ClipOnlineSessions(sessions, windowStart, windowEnd)
|
||||
require.Len(t, periods, 1)
|
||||
require.Equal(t, windowEnd, periods[0].End)
|
||||
require.True(t, periods[0].Ongoing)
|
||||
})
|
||||
|
||||
t.Run("zero ended_at treated as ongoing", func(t *testing.T) {
|
||||
sessions := []OnlineSession{
|
||||
{ServerID: 1, StartedAt: ts(2026, 10, 7, 10, 0, 0)},
|
||||
}
|
||||
periods := ClipOnlineSessions(sessions, windowStart, windowEnd)
|
||||
require.Len(t, periods, 1)
|
||||
require.True(t, periods[0].Ongoing)
|
||||
require.Equal(t, windowEnd, periods[0].End)
|
||||
})
|
||||
|
||||
t.Run("session outside window yields no period", func(t *testing.T) {
|
||||
sessions := []OnlineSession{
|
||||
{ServerID: 1, StartedAt: ts(2026, 9, 1, 0, 0, 0), EndedAt: ts(2026, 9, 2, 0, 0, 0)},
|
||||
}
|
||||
require.Empty(t, ClipOnlineSessions(sessions, windowStart, windowEnd))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDailyOnlineBreakdown(t *testing.T) {
|
||||
loc := time.FixedZone("UTC+8", 8*3600)
|
||||
windowStart := time.Date(2026, 10, 1, 10, 0, 0, 0, loc)
|
||||
windowEnd := time.Date(2026, 10, 3, 14, 0, 0, 0, loc)
|
||||
|
||||
periods := []OnlinePeriod{
|
||||
// 跨午夜:2 日 23:00 ~ 3 日 01:00
|
||||
{Start: time.Date(2026, 10, 2, 23, 0, 0, 0, loc), End: time.Date(2026, 10, 3, 1, 0, 0, 0, loc)},
|
||||
// 窗口前就开始的会话,裁剪后从窗口起点开始
|
||||
{Start: windowStart, End: time.Date(2026, 10, 1, 12, 0, 0, 0, loc)},
|
||||
}
|
||||
|
||||
daily := DailyOnlineBreakdown(periods, windowStart, windowEnd, loc)
|
||||
require.Len(t, daily, 3)
|
||||
require.Equal(t, "2026-10-01", daily[0].Date)
|
||||
require.Equal(t, "2026-10-02", daily[1].Date)
|
||||
require.Equal(t, "2026-10-03", daily[2].Date)
|
||||
|
||||
// 1 日:12:00-10:00 = 2h;2 日:23:00-24:00 = 1h;3 日:0:00-1:00 + 窗口外无 = 1h
|
||||
require.Equal(t, uint64(7200), daily[0].OnlineSeconds)
|
||||
require.Equal(t, uint64(3600), daily[1].OnlineSeconds)
|
||||
require.Equal(t, uint64(3600), daily[2].OnlineSeconds)
|
||||
|
||||
// 每日合计即总时长(口径:每日累计原始时长后统一取整)
|
||||
var sum uint64
|
||||
for _, d := range daily {
|
||||
sum += d.OnlineSeconds
|
||||
}
|
||||
require.Equal(t, uint64(7200+3600+3600), sum)
|
||||
}
|
||||
|
||||
// 评审回归:取整必须按日累计原始时长后统一转换。逐段截断再累加会让亚秒
|
||||
// 会话系统性偏小——同一天两个 1.5s 的会话应得 3s,而不是 1+1=2s
|
||||
func TestDailyOnlineBreakdownSubSecond(t *testing.T) {
|
||||
loc := testZone
|
||||
windowStart := ts(2026, 10, 1, 0, 0, 0)
|
||||
windowEnd := ts(2026, 10, 2, 0, 0, 0)
|
||||
periods := []OnlinePeriod{
|
||||
{Start: windowStart.Add(100 * time.Millisecond), End: windowStart.Add(1600 * time.Millisecond)},
|
||||
{Start: windowStart.Add(2 * time.Second), End: windowStart.Add(3500 * time.Millisecond)},
|
||||
}
|
||||
daily := DailyOnlineBreakdown(periods, windowStart, windowEnd, loc)
|
||||
require.Len(t, daily, 1)
|
||||
require.Equal(t, uint64(3), daily[0].OnlineSeconds)
|
||||
}
|
||||
|
||||
// 跨日时段按日界切分不损失精度:各日分别累计原始时长后各自取整
|
||||
func TestDailyOnlineBreakdownCrossDayFraction(t *testing.T) {
|
||||
loc := testZone
|
||||
windowStart := ts(2026, 10, 1, 0, 0, 0)
|
||||
windowEnd := ts(2026, 10, 3, 0, 0, 0)
|
||||
periods := []OnlinePeriod{{
|
||||
Start: time.Date(2026, 10, 1, 23, 59, 59, 250000000, loc),
|
||||
End: time.Date(2026, 10, 2, 0, 0, 2, 750000000, loc),
|
||||
}}
|
||||
daily := DailyOnlineBreakdown(periods, windowStart, windowEnd, loc)
|
||||
require.Len(t, daily, 2)
|
||||
require.Equal(t, uint64(0), daily[0].OnlineSeconds) // 0.75s → 0
|
||||
require.Equal(t, uint64(2), daily[1].OnlineSeconds) // 2.75s → 2
|
||||
}
|
||||
|
||||
func TestDailyOnlineBreakdownZeroOnline(t *testing.T) {
|
||||
loc := time.FixedZone("UTC+8", 8*3600)
|
||||
windowStart := time.Date(2026, 10, 1, 0, 0, 0, 0, loc)
|
||||
windowEnd := time.Date(2026, 10, 4, 0, 0, 0, 0, loc)
|
||||
daily := DailyOnlineBreakdown(nil, windowStart, windowEnd, loc)
|
||||
require.Len(t, daily, 3)
|
||||
for _, d := range daily {
|
||||
require.Zero(t, d.OnlineSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlineStatsDaysBounds(t *testing.T) {
|
||||
require.Equal(t, 7, DefaultOnlineStatsDays)
|
||||
require.Equal(t, 7, MinOnlineStatsDays)
|
||||
require.Equal(t, 3650, MaxOnlineStatsDays)
|
||||
}
|
||||
|
||||
// F1 回归保护:指针推进实现必须与逐日朴素实现对任意不相交时段集合等价
|
||||
// (单服务器会话互不重叠的约束一致),长窗口大量时段下不再随时段数平方放大。
|
||||
func TestDailyOnlineBreakdownEquivalence(t *testing.T) {
|
||||
loc := testZone
|
||||
windowStart := ts(2026, 1, 1, 0, 0, 0)
|
||||
windowEnd := windowStart.AddDate(0, 0, 90)
|
||||
|
||||
rng := rand.New(rand.NewSource(42))
|
||||
var periods []OnlinePeriod
|
||||
cursor := windowStart
|
||||
for cursor.Before(windowEnd) {
|
||||
// 起止带亚秒抖动:让逐段截断与按日累计两种口径可区分
|
||||
start := cursor.Add(time.Duration(rng.Intn(4*3600))*time.Second + time.Duration(rng.Int63n(int64(time.Second))))
|
||||
end := start.Add(time.Duration(1+rng.Intn(6*3600))*time.Second + time.Duration(rng.Int63n(int64(time.Second))))
|
||||
if end.After(windowEnd) {
|
||||
end = windowEnd
|
||||
}
|
||||
periods = append(periods, OnlinePeriod{Start: start, End: end})
|
||||
cursor = end.Add(time.Duration(1+rng.Intn(3600)) * time.Second)
|
||||
}
|
||||
|
||||
days := DailyOnlineBreakdown(periods, windowStart, windowEnd, loc)
|
||||
|
||||
// 参考实现与生产同一口径:每日累计原始时长、汇总时向下取整一次
|
||||
reference := map[string]time.Duration{}
|
||||
for _, p := range periods {
|
||||
for d := windowStart; d.Before(windowEnd); d = d.AddDate(0, 0, 1) {
|
||||
overlapStart := maxTime(p.Start, d)
|
||||
overlapEnd := minTime(p.End, d.AddDate(0, 0, 1))
|
||||
if overlapStart.Before(overlapEnd) {
|
||||
reference[d.Format("2006-01-02")] += overlapEnd.Sub(overlapStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
require.Len(t, days, 90)
|
||||
for _, day := range days {
|
||||
require.Equal(t, uint64(reference[day.Date].Seconds()), day.OnlineSeconds, "date %s", day.Date)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyOnlineBreakdownMidnightDSTTransition(t *testing.T) {
|
||||
loc, err := time.LoadLocation("America/Sao_Paulo")
|
||||
require.NoError(t, err)
|
||||
start := time.Date(2018, 11, 3, 0, 0, 0, 0, loc)
|
||||
end := time.Date(2018, 11, 6, 0, 0, 0, 0, loc)
|
||||
days := DailyOnlineBreakdown([]OnlinePeriod{{Start: start, End: end}}, start, end, loc)
|
||||
require.Equal(t, []DailyOnlineStat{{Date: "2018-11-03", OnlineSeconds: 86400}, {Date: "2018-11-04", OnlineSeconds: 82800}, {Date: "2018-11-05", OnlineSeconds: 86400}}, days)
|
||||
}
|
||||
|
||||
func TestDailyOnlineBreakdownRepeatedMidnight(t *testing.T) {
|
||||
loc, err := time.LoadLocation("Asia/Amman")
|
||||
require.NoError(t, err)
|
||||
start := time.Date(2021, 10, 28, 21, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2021, 10, 29, 22, 0, 0, 0, time.UTC)
|
||||
days := DailyOnlineBreakdown([]OnlinePeriod{{Start: start, End: end}}, start, end, loc)
|
||||
require.Equal(t, []DailyOnlineStat{{Date: "2021-10-29", OnlineSeconds: 90000}}, days)
|
||||
}
|
||||
|
||||
func TestOnlineHistoryWindowUsesDashboardCalendar(t *testing.T) {
|
||||
loc, err := time.LoadLocation("America/New_York")
|
||||
require.NoError(t, err)
|
||||
for _, test := range []struct {
|
||||
month time.Month
|
||||
day, hours int
|
||||
}{{time.March, 9, 23}, {time.November, 2, 25}} {
|
||||
end := time.Date(2026, test.month, test.day, 0, 30, 0, 0, loc)
|
||||
start := OnlineHistoryWindowStart(end.UTC(), 1, loc)
|
||||
require.True(t, start.Equal(time.Date(2026, test.month, test.day-1, 0, 30, 0, 0, loc)))
|
||||
require.Equal(t, time.Duration(test.hours)*time.Hour, end.Sub(start))
|
||||
require.Len(t, DailyOnlineBreakdown(nil, start, end, loc), 2)
|
||||
require.True(t, OnlineHistoryWindowStart(end.UTC(), 7, loc).Equal(end.In(loc).AddDate(0, 0, -7)))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user