mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
Add TestCheckStatus_SampleMemoryBounded asserting that, across 100k ticks, the per-(alert,server) sample slice length and capacity stay bounded by the rule's retention window and never grow with elapsed time.
120 lines
4.8 KiB
Go
120 lines
4.8 KiB
Go
package singleton
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/nezhahq/nezha/model"
|
|
)
|
|
|
|
// notifyDecision replays the exact send-gate from checkStatus (lines 164-186)
|
|
// for a single (alert, server) pair: given the current Check verdict and the
|
|
// previous stored state, it reports whether an incident or recovery notification
|
|
// would be dispatched and what the next stored state becomes. Kept in lockstep
|
|
// with checkStatus so the end-to-end "does it actually notify" path is testable
|
|
// without the DB/global singletons checkStatus pulls in.
|
|
func notifyDecision(triggerMode uint8, passed bool, prev uint8) (incident, recover bool, next uint8) {
|
|
if !passed {
|
|
if triggerMode == model.ModeAlwaysTrigger || prev != _RuleCheckFail {
|
|
return true, false, _RuleCheckFail
|
|
}
|
|
return false, false, _RuleCheckFail
|
|
}
|
|
if prev == _RuleCheckFail {
|
|
return false, true, _RuleCheckPass
|
|
}
|
|
return false, false, _RuleCheckPass
|
|
}
|
|
|
|
// driveCheckStatus simulates the checkStatus tick loop end-to-end: each tick
|
|
// appends one sample, runs the real Check, applies the real RetentionWindow
|
|
// trim, then runs the send-gate. It returns how many incident notifications
|
|
// would have been dispatched across all ticks and the final sample window size.
|
|
func driveCheckStatus(rule *model.AlertRule, triggerMode uint8, ticks int, sample []bool) (incidents int, finalWindow int) {
|
|
incidents, finalWindow, _ = driveCheckStatusCap(rule, triggerMode, ticks, sample)
|
|
return incidents, finalWindow
|
|
}
|
|
|
|
// driveCheckStatusCap additionally reports the peak length and capacity the
|
|
// sample slice ever reached, so tests can assert memory stays bounded.
|
|
func driveCheckStatusCap(rule *model.AlertRule, triggerMode uint8, ticks int, sample []bool) (incidents, finalLen, peakCap int) {
|
|
var samples [][]bool
|
|
prev := uint8(_RuleCheckNoData)
|
|
for i := 0; i < ticks; i++ {
|
|
samples = append(samples, append([]bool(nil), sample...))
|
|
_, passed := rule.Check(samples)
|
|
w := rule.RetentionWindow()
|
|
if w <= 0 {
|
|
samples = samples[:0]
|
|
} else if w < len(samples) {
|
|
samples = samples[len(samples)-w:]
|
|
}
|
|
if cap(samples) > peakCap {
|
|
peakCap = cap(samples)
|
|
}
|
|
incident, _, next := notifyDecision(triggerMode, passed, prev)
|
|
if incident {
|
|
incidents++
|
|
}
|
|
prev = next
|
|
}
|
|
return incidents, len(samples), peakCap
|
|
}
|
|
|
|
// TestCheckStatus_GeneralRuleFiresIncident is the end-to-end guard for the
|
|
// regression: a Duration:10 rule on a server that fails every tick must,
|
|
// after the window fills, reach passed=false and actually dispatch an incident
|
|
// notification. Before the fix the window was wiped each tick, so passed never
|
|
// became false and zero notifications were sent.
|
|
func TestCheckStatus_GeneralRuleFiresIncident(t *testing.T) {
|
|
rule := &model.AlertRule{Rules: []*model.Rule{{Type: "cpu", Duration: 10}}}
|
|
|
|
t.Run("AlwaysTrigger fires repeatedly once window fills", func(t *testing.T) {
|
|
incidents, window := driveCheckStatus(rule, model.ModeAlwaysTrigger, 30, []bool{false})
|
|
if window < 10 {
|
|
t.Fatalf("window never filled: got %d want >= 10", window)
|
|
}
|
|
if incidents == 0 {
|
|
t.Fatalf("AlwaysTrigger rule never dispatched an incident notification")
|
|
}
|
|
})
|
|
|
|
t.Run("OnetimeTrigger fires exactly once", func(t *testing.T) {
|
|
incidents, _ := driveCheckStatus(rule, model.ModeOnetimeTrigger, 30, []bool{false})
|
|
if incidents != 1 {
|
|
t.Fatalf("OnetimeTrigger must dispatch exactly one incident, got %d", incidents)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCheckStatus_HealthyServerStaysSilent guards the other direction: a server
|
|
// passing every tick must never dispatch an incident.
|
|
func TestCheckStatus_HealthyServerStaysSilent(t *testing.T) {
|
|
rule := &model.AlertRule{Rules: []*model.Rule{{Type: "cpu", Duration: 10}}}
|
|
incidents, _ := driveCheckStatus(rule, model.ModeAlwaysTrigger, 30, []bool{true})
|
|
if incidents != 0 {
|
|
t.Fatalf("a healthy server must never trigger an incident, got %d", incidents)
|
|
}
|
|
}
|
|
|
|
// TestCheckStatus_SampleMemoryBounded pins the no-memory-leak invariant: no
|
|
// matter how many ticks run, the per-(alert,server) sample slice length and
|
|
// capacity stay bounded by the rule's retention window, never growing with
|
|
// elapsed time. Runs far more ticks than the window to expose any unbounded
|
|
// growth.
|
|
func TestCheckStatus_SampleMemoryBounded(t *testing.T) {
|
|
const duration = 10
|
|
rule := &model.AlertRule{Rules: []*model.Rule{{Type: "cpu", Duration: duration}}}
|
|
|
|
_, finalLen, peakCap := driveCheckStatusCap(rule, model.ModeAlwaysTrigger, 100000, []bool{false})
|
|
|
|
if finalLen > duration {
|
|
t.Fatalf("sample length exceeded retention window after many ticks: got %d want <= %d", finalLen, duration)
|
|
}
|
|
// append grows capacity geometrically; with length pinned at window+1 the
|
|
// backing array stabilises at a small constant. A generous 4x window bound
|
|
// catches any reintroduced unbounded growth without being flaky.
|
|
if peakCap > duration*4 {
|
|
t.Fatalf("sample capacity grew unbounded: peak cap %d exceeds 4x window %d", peakCap, duration*4)
|
|
}
|
|
}
|