Skip to content

Commit 6b7610f

Browse files
mvicknrGithub Copilot
andcommitted
test: Add OOM monitor tests and fix issues found with tests (#1075)
* test: Add tests and fix issues found with tests Co-authored-by: Github Copilot <[email protected]> * Chore: Cleanup and simplify Co-authored-by: Github Copilot <[email protected]> * chore: Naming fix * test: Check reset with new duration Co-authored-by: Github Copilot <[email protected]> * test: Fix test to precisely count ticks Co-authored-by: Github Copilot <[email protected]> --------- Co-authored-by: Github Copilot <[email protected]>
1 parent f64e57e commit 6b7610f

File tree

2 files changed

+320
-3
lines changed

2 files changed

+320
-3
lines changed

v3/newrelic/oom_monitor.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,30 +59,33 @@ func (as *heapHighWaterMarkAlarmSet) monitor() {
5959
var m runtime.MemStats
6060
runtime.ReadMemStats(&m)
6161
as.lock.RLock()
62-
defer as.lock.RUnlock()
6362
if as.alarms != nil {
6463
for limit, callback := range as.alarms {
6564
if m.HeapAlloc >= limit {
6665
callback(limit, &m)
6766
}
6867
}
6968
}
69+
as.lock.RUnlock()
7070
case <-as.done:
7171
return
7272
}
7373
}
7474
}
7575

7676
// HeapHighWaterMarkAlarmShutdown stops the monitoring goroutine and deallocates the entire
77-
// monitoring completely. All alarms are calcelled and disabled.
77+
// monitoring completely. All alarms are canceled and disabled.
7878
func (a *Application) HeapHighWaterMarkAlarmShutdown() {
7979
if a == nil || a.app == nil {
8080
return
8181
}
8282

8383
a.app.heapHighWaterMarkAlarms.lock.Lock()
8484
defer a.app.heapHighWaterMarkAlarms.lock.Unlock()
85-
a.app.heapHighWaterMarkAlarms.sampleTicker.Stop()
85+
86+
if a.app.heapHighWaterMarkAlarms.sampleTicker != nil {
87+
a.app.heapHighWaterMarkAlarms.sampleTicker.Stop()
88+
}
8689
if a.app.heapHighWaterMarkAlarms.done != nil {
8790
a.app.heapHighWaterMarkAlarms.done <- 0
8891
}

v3/newrelic/oom_monitor_test.go

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
package newrelic
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestHeapHighWaterMarkAlarmNilAppSafety(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
fn func(*Application)
13+
}{
14+
{"Enable", func(app *Application) { app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond) }},
15+
{"Disable", func(app *Application) { app.HeapHighWaterMarkAlarmDisable() }},
16+
{"Shutdown", func(app *Application) { app.HeapHighWaterMarkAlarmShutdown() }},
17+
{"Set", func(app *Application) { app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {}) }},
18+
{"ClearAll", func(app *Application) { app.HeapHighWaterMarkAlarmClearAll() }},
19+
}
20+
21+
appCases := []struct {
22+
name string
23+
app *Application
24+
}{
25+
{"nil app", nil},
26+
{"nil app.app", &Application{app: nil}},
27+
}
28+
29+
for _, appCase := range appCases {
30+
for _, tt := range tests {
31+
t.Run(appCase.name+"_"+tt.name, func(t *testing.T) {
32+
defer func() {
33+
if r := recover(); r != nil {
34+
t.Errorf("%s panicked on %s: %v", tt.name, appCase.name, r)
35+
}
36+
}()
37+
38+
tt.fn(appCase.app)
39+
})
40+
}
41+
}
42+
}
43+
44+
func TestHeapHighWaterMarkAlarmEnable(t *testing.T) {
45+
app := testApp(nil, ConfigEnabled(true), t)
46+
defer app.Shutdown(10 * time.Second)
47+
48+
app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond)
49+
defer app.HeapHighWaterMarkAlarmShutdown()
50+
51+
app.app.heapHighWaterMarkAlarms.lock.RLock()
52+
ticker := app.app.heapHighWaterMarkAlarms.sampleTicker
53+
done := app.app.heapHighWaterMarkAlarms.done
54+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
55+
56+
if ticker == nil || done == nil {
57+
t.Error("Expected ticker and done channel to be created")
58+
}
59+
}
60+
61+
func TestHeapHighWaterMarkAlarmEnableResetTicker(t *testing.T) {
62+
app := testApp(nil, ConfigEnabled(true), t)
63+
defer app.Shutdown(10 * time.Second)
64+
65+
// Enable with initial interval
66+
app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond)
67+
defer app.HeapHighWaterMarkAlarmShutdown()
68+
69+
app.app.heapHighWaterMarkAlarms.lock.RLock()
70+
oldTicker := app.app.heapHighWaterMarkAlarms.sampleTicker
71+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
72+
73+
if oldTicker == nil {
74+
t.Fatal("Expected ticker to be created on first enable")
75+
}
76+
77+
// Enable with new interval - should reset existing ticker
78+
app.HeapHighWaterMarkAlarmEnable(400 * time.Millisecond)
79+
80+
app.app.heapHighWaterMarkAlarms.lock.RLock()
81+
newTicker := app.app.heapHighWaterMarkAlarms.sampleTicker
82+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
83+
84+
if newTicker == nil {
85+
t.Fatal("Expected ticker to exist after reset")
86+
}
87+
88+
if oldTicker != newTicker {
89+
t.Error("Expected ticker to be the same instance after reset")
90+
}
91+
92+
// Verify ticker is functional and doesn't tick too frequently
93+
tickCount := 0
94+
timeout := time.After(1200 * time.Millisecond) // Give enough time for 2+ ticks
95+
96+
for {
97+
select {
98+
case <-newTicker.C:
99+
tickCount++
100+
if tickCount == 2 { // Stop after getting expected ticks
101+
return
102+
} else if tickCount > 2 {
103+
t.Errorf("Ticker ticked more than expected: %d times", tickCount)
104+
return
105+
}
106+
case <-timeout:
107+
if tickCount == 0 {
108+
t.Error("Ticker should have ticked at least once within timeout")
109+
}
110+
return
111+
}
112+
}
113+
}
114+
115+
func TestHeapHighWaterMarkAlarmCallbackTriggered(t *testing.T) {
116+
app := testApp(nil, ConfigEnabled(true), t)
117+
defer app.Shutdown(10 * time.Second)
118+
119+
app.HeapHighWaterMarkAlarmEnable(10 * time.Millisecond)
120+
defer app.HeapHighWaterMarkAlarmShutdown()
121+
122+
callbackCh := make(chan struct{}, 1)
123+
var calledLimit uint64
124+
var calledMemStats *runtime.MemStats
125+
126+
app.HeapHighWaterMarkAlarmSet(1, func(limit uint64, memStats *runtime.MemStats) {
127+
calledLimit = limit
128+
calledMemStats = memStats
129+
select {
130+
case callbackCh <- struct{}{}:
131+
default:
132+
}
133+
})
134+
135+
select {
136+
case <-callbackCh:
137+
// Callback triggered
138+
case <-time.After(200 * time.Millisecond):
139+
t.Fatal("Timeout: callback was not called when heap allocation exceeds limit")
140+
}
141+
142+
if calledLimit != 1 {
143+
t.Errorf("Expected callback to be called with limit 1, got %d", calledLimit)
144+
}
145+
146+
if calledMemStats == nil {
147+
t.Error("Expected callback to be called with valid MemStats")
148+
}
149+
150+
if calledMemStats != nil && calledMemStats.HeapAlloc < 1 {
151+
t.Errorf("Expected HeapAlloc to be >= 1, got %d", calledMemStats.HeapAlloc)
152+
}
153+
}
154+
155+
func TestHeapHighWaterMarkWithInvalidInterval(t *testing.T) {
156+
tests := []struct {
157+
name string
158+
interval time.Duration
159+
}{
160+
{"zero interval", 0},
161+
{"negative interval", -1 * time.Second},
162+
}
163+
164+
for _, tt := range tests {
165+
t.Run(tt.name, func(t *testing.T) {
166+
app := testApp(nil, ConfigEnabled(true), t)
167+
defer app.Shutdown(10 * time.Second)
168+
169+
app.HeapHighWaterMarkAlarmEnable(tt.interval)
170+
defer app.HeapHighWaterMarkAlarmShutdown()
171+
172+
app.app.heapHighWaterMarkAlarms.lock.RLock()
173+
ticker := app.app.heapHighWaterMarkAlarms.sampleTicker
174+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
175+
176+
if ticker != nil {
177+
t.Error("Expected ticker to be nil with invalid interval")
178+
}
179+
})
180+
}
181+
}
182+
183+
func TestHeapHighWaterMarkAlarmDisable(t *testing.T) {
184+
app := testApp(nil, ConfigEnabled(true), t)
185+
defer app.Shutdown(10 * time.Second)
186+
187+
app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond)
188+
defer app.HeapHighWaterMarkAlarmShutdown()
189+
190+
app.HeapHighWaterMarkAlarmDisable()
191+
192+
app.app.heapHighWaterMarkAlarms.lock.RLock()
193+
ticker := app.app.heapHighWaterMarkAlarms.sampleTicker
194+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
195+
196+
if ticker == nil {
197+
t.Error("Expected ticker to still exist after disable")
198+
}
199+
}
200+
201+
func TestHeapHighWaterMarkAlarmShutdown(t *testing.T) {
202+
app := testApp(nil, ConfigEnabled(true), t)
203+
defer app.Shutdown(10 * time.Second)
204+
205+
app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond)
206+
app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {})
207+
app.HeapHighWaterMarkAlarmShutdown()
208+
209+
app.app.heapHighWaterMarkAlarms.lock.RLock()
210+
ticker := app.app.heapHighWaterMarkAlarms.sampleTicker
211+
alarms := app.app.heapHighWaterMarkAlarms.alarms
212+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
213+
214+
if ticker != nil || alarms != nil {
215+
t.Error("Expected ticker and alarms to be nil after shutdown")
216+
}
217+
}
218+
219+
func TestHeapHighWaterMarkAlarmSet(t *testing.T) {
220+
app := testApp(nil, ConfigEnabled(true), t)
221+
defer app.Shutdown(10 * time.Second)
222+
223+
app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond)
224+
defer app.HeapHighWaterMarkAlarmShutdown()
225+
226+
app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {})
227+
228+
if app.app == nil {
229+
t.Error("app.app is nil")
230+
return
231+
}
232+
233+
app.app.heapHighWaterMarkAlarms.lock.RLock()
234+
alarms := app.app.heapHighWaterMarkAlarms.alarms
235+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
236+
237+
if alarms == nil || len(alarms) != 1 || alarms[1024] == nil {
238+
t.Error("Expected one alarm with a valid callback")
239+
}
240+
}
241+
242+
func TestHeapHighWaterMarkAlarmSetReplaceCallback(t *testing.T) {
243+
app := testApp(nil, ConfigEnabled(true), t)
244+
defer app.Shutdown(10 * time.Second)
245+
246+
app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond)
247+
defer app.HeapHighWaterMarkAlarmShutdown()
248+
249+
app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {})
250+
app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {})
251+
252+
app.app.heapHighWaterMarkAlarms.lock.RLock()
253+
alarms := app.app.heapHighWaterMarkAlarms.alarms
254+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
255+
256+
if len(alarms) != 1 {
257+
t.Error("Expected one alarm after replacing callback")
258+
}
259+
}
260+
261+
func TestHeapHighWaterMarkAlarmSetRemoveWithNil(t *testing.T) {
262+
app := testApp(nil, ConfigEnabled(true), t)
263+
defer app.Shutdown(10 * time.Second)
264+
265+
app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond)
266+
defer app.HeapHighWaterMarkAlarmShutdown()
267+
268+
app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {})
269+
app.HeapHighWaterMarkAlarmSet(1024, nil)
270+
271+
app.app.heapHighWaterMarkAlarms.lock.RLock()
272+
alarms := app.app.heapHighWaterMarkAlarms.alarms
273+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
274+
275+
if len(alarms) != 0 {
276+
t.Error("Expected no alarms after removing with nil")
277+
}
278+
}
279+
280+
func TestHeapHighWaterMarkAlarmClearAll(t *testing.T) {
281+
app := testApp(nil, ConfigEnabled(true), t)
282+
defer app.Shutdown(10 * time.Second)
283+
284+
app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond)
285+
defer app.HeapHighWaterMarkAlarmShutdown()
286+
287+
app.HeapHighWaterMarkAlarmSet(1024, func(uint64, *runtime.MemStats) {})
288+
app.HeapHighWaterMarkAlarmSet(2048, func(uint64, *runtime.MemStats) {})
289+
app.HeapHighWaterMarkAlarmClearAll()
290+
291+
app.app.heapHighWaterMarkAlarms.lock.RLock()
292+
alarms := app.app.heapHighWaterMarkAlarms.alarms
293+
app.app.heapHighWaterMarkAlarms.lock.RUnlock()
294+
295+
if len(alarms) != 0 {
296+
t.Error("Expected no alarms after clearing all")
297+
}
298+
}
299+
300+
func TestHeapHighWaterMarkAlarmClearAllWithNilAlarms(t *testing.T) {
301+
app := testApp(nil, ConfigEnabled(true), t)
302+
defer app.Shutdown(10 * time.Second)
303+
304+
app.HeapHighWaterMarkAlarmEnable(100 * time.Millisecond)
305+
defer app.HeapHighWaterMarkAlarmShutdown()
306+
307+
// Ensure alarms is nil
308+
app.app.heapHighWaterMarkAlarms.lock.Lock()
309+
app.app.heapHighWaterMarkAlarms.alarms = nil
310+
app.app.heapHighWaterMarkAlarms.lock.Unlock()
311+
312+
// Should not panic or error
313+
app.HeapHighWaterMarkAlarmClearAll()
314+
}

0 commit comments

Comments
 (0)