Skip to content

Commit 51f3abd

Browse files
initial implementation of oom callback trigger mechanism
1 parent dea6291 commit 51f3abd

File tree

4 files changed

+220
-0
lines changed

4 files changed

+220
-0
lines changed

v3/examples/oom/main.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2020 New Relic Corporation. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"runtime"
10+
"time"
11+
12+
"github.com/newrelic/go-agent/v3/newrelic"
13+
)
14+
15+
const MB = 1024 * 1024
16+
17+
func main() {
18+
app, err := newrelic.NewApplication(
19+
newrelic.ConfigAppName("OOM Response High Water Mark App"),
20+
newrelic.ConfigFromEnvironment(),
21+
newrelic.ConfigDebugLogger(os.Stdout),
22+
)
23+
if err != nil {
24+
fmt.Println(err)
25+
os.Exit(1)
26+
}
27+
28+
// Wait for the application to connect.
29+
if err := app.WaitForConnection(5 * time.Second); nil != err {
30+
fmt.Println(err)
31+
}
32+
33+
app.HeapHighWaterMarkAlarmSet(1*MB, megabyte)
34+
app.HeapHighWaterMarkAlarmSet(10*MB, tenMegabyte)
35+
app.HeapHighWaterMarkAlarmSet(100*MB, hundredMegabyte)
36+
app.HeapHighWaterMarkAlarmEnable(2 * time.Second)
37+
38+
var a [][]byte
39+
for _ = range 100 {
40+
a = append(a, make([]byte, MB, MB))
41+
time.Sleep(1 * time.Second)
42+
}
43+
44+
// Shut down the application to flush data to New Relic.
45+
app.Shutdown(10 * time.Second)
46+
}
47+
48+
func megabyte(limit uint64, stats *runtime.MemStats) {
49+
fmt.Printf("*** 1M *** threshold %v alloc %v (%v)\n", limit, stats.Alloc, stats.TotalAlloc)
50+
}
51+
func tenMegabyte(limit uint64, stats *runtime.MemStats) {
52+
fmt.Printf("*** 10M *** threshold %v alloc %v (%v)\n", limit, stats.Alloc, stats.TotalAlloc)
53+
}
54+
func hundredMegabyte(limit uint64, stats *runtime.MemStats) {
55+
fmt.Printf("*** 100M *** threshold %v alloc %v (%v)\n", limit, stats.Alloc, stats.TotalAlloc)
56+
}

v3/go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ require (
77
google.golang.org/protobuf v1.34.2
88
)
99

10+
require (
11+
golang.org/x/net v0.25.0 // indirect
12+
golang.org/x/sys v0.20.0 // indirect
13+
golang.org/x/text v0.15.0 // indirect
14+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
15+
)
1016

1117
retract v3.22.0 // release process error corrected in v3.22.1
1218

v3/newrelic/internal_app.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ type app struct {
6666
// registered callback functions
6767
llmTokenCountCallback func(string, string) int
6868

69+
// high water mark alarms
70+
heapHighWaterMarkAlarms heapHighWaterMarkAlarmSet
71+
6972
serverless *serverlessHarvest
7073
}
7174

v3/newrelic/oom_monitor.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright 2022 New Relic Corporation. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package newrelic
5+
6+
import (
7+
"runtime"
8+
"sync"
9+
"time"
10+
)
11+
12+
type heapHighWaterMarkAlarmSet struct {
13+
lock sync.RWMutex // protects creation of the ticker and access to map
14+
sampleTicker *time.Ticker // once made, only read by monitor goroutine
15+
alarms map[uint64]func(uint64, *runtime.MemStats)
16+
done chan byte
17+
}
18+
19+
// This is a gross, high-level whole-heap memory monitor which can be used to monitor, track,
20+
// and trigger an application's response to running out of memory as an initial step or when
21+
// more expensive or sophisticated analysis such as per-routine memory usage tracking is not
22+
// needed.
23+
//
24+
// For this, we simply configure one or more heap memory limits and for each, register a callback
25+
// function to be called any time we notice that the total heap allocation reaches or exceeds that
26+
// limit. Note that this means if the allocation size crosses multiple limits, then multiple
27+
// callbacks will be triggered since each of their criteria will be met.
28+
//
29+
// HeapHighWaterMarkAlarmEnable starts the periodic sampling of the runtime heap allocation
30+
// of the application, at the user-provided sampling interval. Calling HeapHighWaterMarkAlarmEnable
31+
// with an interval less than or equal to 0 is equivalent to calling HeapHighWaterMarkAlarmDisable.
32+
//
33+
// If there was already a running heap monitor, this merely changes its sample interval time.
34+
func (a *Application) HeapHighWaterMarkAlarmEnable(interval time.Duration) {
35+
if a == nil || a.app == nil {
36+
return
37+
}
38+
39+
if interval <= 0 {
40+
a.HeapHighWaterMarkAlarmDisable()
41+
return
42+
}
43+
44+
a.app.heapHighWaterMarkAlarms.lock.Lock()
45+
defer a.app.heapHighWaterMarkAlarms.lock.Unlock()
46+
if a.app.heapHighWaterMarkAlarms.sampleTicker == nil {
47+
a.app.heapHighWaterMarkAlarms.sampleTicker = time.NewTicker(interval)
48+
a.app.heapHighWaterMarkAlarms.done = make(chan byte)
49+
go a.app.heapHighWaterMarkAlarms.monitor()
50+
} else {
51+
a.app.heapHighWaterMarkAlarms.sampleTicker.Reset(interval)
52+
}
53+
}
54+
55+
func (as *heapHighWaterMarkAlarmSet) monitor() {
56+
for {
57+
select {
58+
case <-as.sampleTicker.C:
59+
var m runtime.MemStats
60+
runtime.ReadMemStats(&m)
61+
as.lock.RLock()
62+
defer as.lock.RUnlock()
63+
if as.alarms != nil {
64+
for limit, callback := range as.alarms {
65+
if m.HeapAlloc >= limit {
66+
callback(limit, &m)
67+
}
68+
}
69+
}
70+
case <-as.done:
71+
return
72+
}
73+
}
74+
}
75+
76+
// HeapHighWaterMarkAlarmShutdown stops the monitoring goroutine and deallocates the entire
77+
// monitoring completely. All alarms are calcelled and disabled.
78+
func (a *Application) HeapHighWaterMarkAlarmShutdown() {
79+
if a == nil || a.app == nil {
80+
return
81+
}
82+
83+
a.app.heapHighWaterMarkAlarms.lock.Lock()
84+
defer a.app.heapHighWaterMarkAlarms.lock.Unlock()
85+
a.app.heapHighWaterMarkAlarms.sampleTicker.Stop()
86+
if a.app.heapHighWaterMarkAlarms.done != nil {
87+
a.app.heapHighWaterMarkAlarms.done <- 0
88+
}
89+
if a.app.heapHighWaterMarkAlarms.alarms != nil {
90+
clear(a.app.heapHighWaterMarkAlarms.alarms)
91+
a.app.heapHighWaterMarkAlarms.alarms = nil
92+
}
93+
}
94+
95+
// HeapHighWaterMarkAlarmDisable stops sampling the heap memory allocation started by
96+
// HeapHighWaterMarkAlarmEnable. It is safe to call even if HeapHighWaterMarkAlarmEnable was
97+
// never called or the alarms were already disabled.
98+
func (a *Application) HeapHighWaterMarkAlarmDisable() {
99+
if a == nil || a.app == nil {
100+
return
101+
}
102+
103+
a.app.heapHighWaterMarkAlarms.lock.Lock()
104+
defer a.app.heapHighWaterMarkAlarms.lock.Unlock()
105+
if a.app.heapHighWaterMarkAlarms.sampleTicker != nil {
106+
a.app.heapHighWaterMarkAlarms.sampleTicker.Stop()
107+
}
108+
}
109+
110+
// HeapHighWaterMarkAlarmSet adds a heap memory high water mark alarm to the set of alarms
111+
// being tracked by the running heap monitor. Memory is checked on the interval specified to
112+
// the last call to HeapHighWaterMarkAlarmEnable, and if at that point the globally allocated heap
113+
// memory is at least the specified size, the provided callback function will be invoked. This
114+
// method may be called multiple times to register any number of callback functions to respond
115+
// to different memory thresholds. For example, you may wish to make measurements or warnings
116+
// of various urgency levels before finally taking action.
117+
//
118+
// If HeapHighWaterMarkAlarmSet is called with the same memory limit as a previous call, the
119+
// supplied callback function will replace the one previously registered for that limit. If
120+
// the function is given as nil, then that memory limit alarm is removed from the list.
121+
func (a *Application) HeapHighWaterMarkAlarmSet(limit uint64, f func(uint64, *runtime.MemStats)) {
122+
if a == nil || a.app == nil {
123+
return
124+
}
125+
126+
a.app.heapHighWaterMarkAlarms.lock.Lock()
127+
defer a.app.heapHighWaterMarkAlarms.lock.Unlock()
128+
129+
if a.app.heapHighWaterMarkAlarms.alarms == nil {
130+
a.app.heapHighWaterMarkAlarms.alarms = make(map[uint64]func(uint64, *runtime.MemStats))
131+
}
132+
133+
if f == nil {
134+
delete(a.app.heapHighWaterMarkAlarms.alarms, limit)
135+
} else {
136+
a.app.heapHighWaterMarkAlarms.alarms[limit] = f
137+
}
138+
}
139+
140+
// HeapHighWaterMarkAlarmClearAll removes all high water mark alarms from the memory monitor
141+
// set.
142+
func (a *Application) HeapHighWaterMarkAlarmClearAll() {
143+
if a == nil || a.app == nil {
144+
return
145+
}
146+
147+
a.app.heapHighWaterMarkAlarms.lock.Lock()
148+
defer a.app.heapHighWaterMarkAlarms.lock.Unlock()
149+
150+
if a.app.heapHighWaterMarkAlarms.alarms == nil {
151+
return
152+
}
153+
154+
clear(a.app.heapHighWaterMarkAlarms.alarms)
155+
}

0 commit comments

Comments
 (0)