diff --git a/actor/engine_test.go b/actor/engine_test.go index 6eed2d0..563b20e 100644 --- a/actor/engine_test.go +++ b/actor/engine_test.go @@ -124,7 +124,7 @@ func TestRestarts(t *testing.T) { if msg.data != 10 { panic("I failed to process this message") } else { - fmt.Println("finally processed all my messsages after borking.", msg.data) + fmt.Println("finally processed all my messages after borking", msg.data) wg.Done() } } diff --git a/actor/process.go b/actor/process.go index 94f5730..ad10d2f 100644 --- a/actor/process.go +++ b/actor/process.go @@ -1,7 +1,9 @@ package actor import ( + "bytes" "fmt" + "github.com/DataDog/gostackparse" "log/slog" "runtime/debug" "sync" @@ -149,8 +151,7 @@ func (p *process) tryRestart(v any) { p.Start() return } - stackTrace := debug.Stack() - fmt.Println(string(stackTrace)) + stackTrace := cleanTrace(debug.Stack()) // If we reach the max restarts, we shutdown the inbox and clean // everything up. if p.restarts == p.MaxRestarts { @@ -210,3 +211,24 @@ func (p *process) Send(_ *PID, msg any, sender *PID) { p.inbox.Send(Envelope{Msg: msg, Sender: sender}) } func (p *process) Shutdown(wg *sync.WaitGroup) { p.cleanup(wg) } + +func cleanTrace(stack []byte) []byte { + goros, err := gostackparse.Parse(bytes.NewReader(stack)) + if err != nil { + slog.Error("failed to parse stacktrace", "err", err) + return stack + } + if len(goros) != 1 { + slog.Error("expected only one goroutine", "goroutines", len(goros)) + return stack + } + // skip the first frames: + goros[0].Stack = goros[0].Stack[4:] + buf := bytes.NewBuffer(nil) + _, _ = fmt.Fprintf(buf, "goroutine %d [%s]\n", goros[0].ID, goros[0].State) + for _, frame := range goros[0].Stack { + _, _ = fmt.Fprintf(buf, "%s\n", frame.Func) + _, _ = fmt.Fprint(buf, "\t", frame.File, ":", frame.Line, "\n") + } + return buf.Bytes() +} diff --git a/actor/process_test.go b/actor/process_test.go new file mode 100644 index 0000000..b0e7dc9 --- /dev/null +++ b/actor/process_test.go @@ -0,0 +1,49 @@ +package actor + +import ( + "bytes" + "fmt" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +// Test_CleanTrace tests that the stack trace is cleaned up correctly and that the function +// which triggers the panic is at the top of the stack trace. +func Test_CleanTrace(t *testing.T) { + e, err := NewEngine(nil) + require.NoError(t, err) + type triggerPanic struct { + data int + } + stopCh := make(chan struct{}) + pid := e.SpawnFunc(func(c *Context) { + fmt.Printf("Got message type %T\n", c.Message()) + switch c.Message().(type) { + case Started: + c.Engine().Subscribe(c.pid) + case triggerPanic: + panicWrapper() + case ActorRestartedEvent: + m := c.Message().(ActorRestartedEvent) + // split the panic into lines: + lines := bytes.Split(m.Stacktrace, []byte("\n")) + // check that the second line is the panicWrapper function: + if bytes.Contains(lines[1], []byte("panicWrapper")) { + fmt.Println("stack trace contains panicWrapper at the right line") + stopCh <- struct{}{} + } + } + }, "foo", WithMaxRestarts(1)) + e.Send(pid, triggerPanic{1}) + select { + case <-stopCh: + fmt.Println("test passed") + case <-time.After(time.Second): + t.Error("test timed out. stack trace likely did not contain panicWrapper at the right line") + } +} + +func panicWrapper() { + panic("foo") +} diff --git a/go.mod b/go.mod index 6676ec7..0e695b2 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/anthdm/hollywood go 1.21 require ( + github.com/DataDog/gostackparse v0.7.0 github.com/grandcat/zeroconf v1.0.0 github.com/planetscale/vtprotobuf v0.4.0 github.com/prometheus/client_golang v1.15.0 diff --git a/go.sum b/go.sum index 41a2843..88f29ba 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= +github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= @@ -57,6 +59,7 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=