Files
3x-ui/internal/web/job/check_cpu_usage_test.go
T
PluS98 121123eaaa fix: keep panel restarts from hanging on the CPU job and Telegram (#7029)
Restarting the panel left it unreachable for up to a minute. CheckCpuJob sampled cpu.Percent over its whole one-minute period, so cron.Stop nearly always waited on it; it now diffs cpu.Times against its previous run (iowait counted as idle, as before). With Telegram unreachable, bus.Stop waited on an in-flight send and tgbot.Start ran getMe/setMyCommands inline; each bot call now returns once StopBot closes the bot's stop channel, the bot stops before the bus, and the artwork/commands warm-up runs in the background.
2026-11-02 15:34:22 +01:00

105 lines
2.8 KiB
Go

package job
import (
"testing"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/eventbus"
"github.com/shirou/gopsutil/v4/cpu"
)
// The alarm must report usage over the minute between runs, not since boot:
// cumulative counters alone would hide a spike behind days of idle time.
func TestCheckCpuJobPublishesUsageSinceThePreviousRun(t *testing.T) {
samples := []cpu.TimesStat{
{User: 1000, System: 500, Idle: 98500},
{User: 1060, System: 515, Idle: 98525}, // 60+15 busy, 25 idle: 75%
}
j := &CheckCpuJob{readTimes: func(bool) ([]cpu.TimesStat, error) {
s := samples[0]
samples = samples[1:]
return []cpu.TimesStat{s}, nil
}}
bus := eventbus.New(0)
t.Cleanup(bus.Stop)
got := make(chan float64, 2)
bus.Subscribe("test", func(e eventbus.Event) {
got <- e.Data.(*eventbus.SystemMetricData).Percent
})
prevBus := EventBus
EventBus = bus
t.Cleanup(func() { EventBus = prevBus })
j.Run()
j.Run()
select {
case p := <-got:
if p != 75 {
t.Fatalf("published %v%%, want 75%% (busy share between the two runs)", p)
}
case <-time.After(2 * time.Second):
t.Fatal("no cpu.high event published by the second run")
}
select {
case p := <-got:
t.Fatalf("first run has no baseline yet but published %v%%", p)
case <-time.After(100 * time.Millisecond):
}
}
// gopsutil's cpu.Percent, which the alarm used before, counts iowait as idle; counting it busy
// would page operators every minute of a backup on a slow-disk host.
func TestCheckCpuJobDoesNotCountIowaitAsBusy(t *testing.T) {
samples := []cpu.TimesStat{
{User: 1000, Iowait: 2000, Idle: 97000},
{User: 1030, Iowait: 2060, Idle: 97010}, // 30 user, 60 iowait, 10 idle: 30%
}
j := &CheckCpuJob{readTimes: func(bool) ([]cpu.TimesStat, error) {
s := samples[0]
samples = samples[1:]
return []cpu.TimesStat{s}, nil
}}
bus := eventbus.New(0)
t.Cleanup(bus.Stop)
got := make(chan float64, 2)
bus.Subscribe("test", func(e eventbus.Event) {
got <- e.Data.(*eventbus.SystemMetricData).Percent
})
prevBus := EventBus
EventBus = bus
t.Cleanup(func() { EventBus = prevBus })
j.Run()
j.Run()
select {
case p := <-got:
if p != 30 {
t.Fatalf("published %v%%, want 30%% (iowait is waiting, not CPU work)", p)
}
case <-time.After(2 * time.Second):
t.Fatal("no cpu.high event published by the second run")
}
}
// A Run that samples for its whole period keeps cron.Stop waiting, and with it
// every panel restart, for up to a minute behind a closed listener.
func TestCheckCpuJobRunDoesNotBlockForTheSamplePeriod(t *testing.T) {
j := NewCheckCpuJob()
done := make(chan struct{})
go func() {
j.Run()
j.Run()
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("CheckCpuJob.Run still blocking after 5s; it must read CPU time since its previous run")
}
}