Skip to content
This repository was archived by the owner on Aug 17, 2020. It is now read-only.

Commit 5d294f6

Browse files
authored
Goroutines global panic handler (#188)
* New global panic handler allowing to fail running tests and flush the recorder buffer before process exit * Panic handler test * Test race condition fix * Panic handler test in scope
1 parent 75ca3f8 commit 5d294f6

File tree

7 files changed

+175
-0
lines changed

7 files changed

+175
-0
lines changed

agent/agent.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,15 @@ func (a *Agent) Stop() {
396396
a.PrintReport()
397397
}
398398

399+
// Flush agent buffer
400+
func (a *Agent) Flush() {
401+
a.logger.Println("Flushing agent buffer...")
402+
err := a.recorder.Flush()
403+
if err != nil {
404+
a.logger.Println(err)
405+
}
406+
}
407+
399408
func generateAgentID() string {
400409
agentId, err := uuid.NewRandom()
401410
if err != nil {

agent/recorder.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,15 @@ func (r *SpanRecorder) Stop() {
203203
}
204204
}
205205

206+
// Flush recorder
207+
func (r *SpanRecorder) Flush() error {
208+
if r.debugMode {
209+
r.logger.Println("Flushing recorder buffer...")
210+
}
211+
err, _ := r.sendSpans()
212+
return err
213+
}
214+
206215
// Write statistics
207216
func (r *SpanRecorder) writeStats() {
208217
r.statsOnce.Do(func() {

init.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"go.undefinedlabs.com/scopeagent/instrumentation"
1515
"go.undefinedlabs.com/scopeagent/instrumentation/logging"
1616
scopetesting "go.undefinedlabs.com/scopeagent/instrumentation/testing"
17+
"go.undefinedlabs.com/scopeagent/reflection"
1718
)
1819

1920
var (
@@ -50,6 +51,15 @@ func Run(m *testing.M, opts ...agent.Option) int {
5051
newAgent.Stop()
5152
os.Exit(1)
5253
}()
54+
reflection.AddPanicHandler(func(e interface{}) {
55+
instrumentation.Logger().Printf("Panic handler triggered by: %v,\nFlushing agent, sending partial results...", e)
56+
newAgent.Flush()
57+
})
58+
reflection.AddOnPanicExitHandler(func(e interface{}) {
59+
instrumentation.Logger().Printf("Process is going to end by: %v,\nStopping agent...", e)
60+
scopetesting.PanicAllRunningTests(e, 3)
61+
newAgent.Stop()
62+
})
5363

5464
defaultAgent = newAgent
5565
return newAgent.Run(m)

instrumentation/testing/testing.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,29 @@ func GetTest(t *testing.T) *Test {
245245
}
246246
}
247247

248+
// Fails and write panic on running tests
249+
// Use this only if the process is going to crash
250+
func PanicAllRunningTests(e interface{}, skip int) {
251+
autoInstrumentedTestsMutex.Lock()
252+
defer autoInstrumentedTestsMutex.Unlock()
253+
254+
// We copy the testMap because v.end() locks
255+
testMapMutex.RLock()
256+
tmp := map[*testing.T]*Test{}
257+
for k, v := range testMap {
258+
tmp[k] = v
259+
}
260+
testMapMutex.RUnlock()
261+
262+
for _, v := range tmp {
263+
delete(autoInstrumentedTests, v.t)
264+
v.t.Fail()
265+
v.span.SetTag("error", true)
266+
errors.LogError(v.span, e, 1+skip)
267+
v.end()
268+
}
269+
}
270+
248271
// Adds an auto instrumented test to the map
249272
func addAutoInstrumentedTest(t *testing.T) {
250273
autoInstrumentedTestsMutex.Lock()

reflection/panic_handler.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package reflection
2+
3+
import (
4+
"reflect"
5+
"sync"
6+
"unsafe"
7+
_ "unsafe"
8+
9+
"github.com/undefinedlabs/go-mpatch"
10+
)
11+
12+
var (
13+
patchOnPanic *mpatch.Patch
14+
mOnPanic sync.Mutex
15+
onPanicHandlers []func(e interface{})
16+
17+
patchOnExit *mpatch.Patch
18+
mOnExit sync.Mutex
19+
onExitHandler []func(e interface{})
20+
)
21+
22+
// Adds a global panic handler (this handler will be executed before any recover call)
23+
func AddPanicHandler(fn func(interface{})) {
24+
mOnPanic.Lock()
25+
defer mOnPanic.Unlock()
26+
if patchOnPanic == nil {
27+
gp := lgopanic
28+
np, err := mpatch.PatchMethodByReflect(reflect.Method{Func: reflect.ValueOf(gp)}, gopanic)
29+
if err == nil {
30+
patchOnPanic = np
31+
}
32+
}
33+
onPanicHandlers = append(onPanicHandlers, fn)
34+
}
35+
36+
// Adds a global panic handler before process kill (this handler will be executed if not recover is set before the process exits)
37+
func AddOnPanicExitHandler(fn func(interface{})) {
38+
mOnExit.Lock()
39+
defer mOnExit.Unlock()
40+
if patchOnExit == nil {
41+
gp := lpreprintpanics
42+
np, err := mpatch.PatchMethodByReflect(reflect.Method{Func: reflect.ValueOf(gp)}, preprintpanics)
43+
if err == nil {
44+
patchOnExit = np
45+
}
46+
}
47+
onExitHandler = append(onExitHandler, fn)
48+
}
49+
50+
func gopanic(e interface{}) {
51+
mOnPanic.Lock()
52+
defer mOnPanic.Unlock()
53+
for _, fn := range onPanicHandlers {
54+
fn(e)
55+
}
56+
patchOnPanic.Unpatch()
57+
defer patchOnPanic.Patch()
58+
lgopanic(e)
59+
}
60+
61+
func preprintpanics(p *_panic) {
62+
mOnExit.Lock()
63+
defer mOnExit.Unlock()
64+
for _, fn := range onExitHandler {
65+
fn(p.arg)
66+
}
67+
patchOnExit.Unpatch()
68+
defer patchOnExit.Patch()
69+
lpreprintpanics(p)
70+
}
71+
72+
//go:linkname lgopanic runtime.gopanic
73+
func lgopanic(e interface{})
74+
75+
//go:linkname lpreprintpanics runtime.preprintpanics
76+
func lpreprintpanics(p *_panic)
77+
78+
type _panic struct {
79+
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
80+
arg interface{} // argument to panic
81+
link *_panic // link to earlier panic
82+
recovered bool // whether this panic is over
83+
aborted bool // the panic was aborted
84+
}

reflection/panic_handler.s

Whitespace-only changes.

reflection/panic_handler_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package reflection_test
2+
3+
import (
4+
"sync/atomic"
5+
"testing"
6+
"time"
7+
8+
_ "go.undefinedlabs.com/scopeagent/autoinstrument"
9+
"go.undefinedlabs.com/scopeagent/reflection"
10+
)
11+
12+
func TestPanicHandler(t *testing.T) {
13+
var panicHandlerVisit int32
14+
15+
reflection.AddPanicHandler(func(e interface{}) {
16+
t.Log("PANIC HANDLER FOR:", e)
17+
atomic.AddInt32(&panicHandlerVisit, 1)
18+
})
19+
20+
t.Run("OnPanic", func(t2 *testing.T) {
21+
go func() {
22+
23+
defer func() {
24+
if r := recover(); r != nil {
25+
t.Log("PANIC RECOVERED")
26+
}
27+
}()
28+
29+
t.Log("PANICKING!")
30+
panic("Panic error")
31+
32+
}()
33+
34+
time.Sleep(1 * time.Second)
35+
})
36+
37+
if atomic.LoadInt32(&panicHandlerVisit) != 1 {
38+
t.Fatalf("panic handler should be executed once.")
39+
}
40+
}

0 commit comments

Comments
 (0)