Skip to content

Commit 7899e20

Browse files
authored
Add stack trace to error handler's HandlePanicFunc (#423)
This one in response to #418 in which although we persist a stack trace to a job row's errors property, we don't reveal it in a panic handler, which is quite inconvenient for purposes of logging or other telemetry (e.g. sending to Sentry). Here, `HandlePanic`'s signature changes to (`trace` is added): HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult A couple notes on choices: * The naming of `trace` is reused from `AttemptError`. * The value type is `string`. This is a little non-obvious because Go exposes it as a `[]byte`, something I've never quite understood as to why, but I did `string` because for one it's more convenient to use, but more importantly, it's the same type on `AttemptError`. This is a breaking change, but it seems like being able to get a stack trace during panic is important enough that it's worth it, and with any luck there's few enough people using this feature that it won't break that many people. The fix is quite easy regardless and will easily be caught by the compiler. Fixes #418.
1 parent 7e1e269 commit 7899e20

6 files changed

Lines changed: 32 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- `Config.TestOnly` has been added. It disables various features in the River client like staggered maintenance service start that are useful in production, but may be somewhat harmful in tests because they make start/stop slower. [PR #414](https://github.com/riverqueue/river/pull/414).
1313

14+
### Changed
15+
16+
⚠️ Version 0.9.0 has a small breaking change in `ErrorHandler`. As before, we try never to make breaking changes, but this one was deemed quite important because `ErrorHandler` was fundamentally lacking important functionality.
17+
18+
- **Breaking change:** Add stack trace to `ErrorHandler.HandlePanicFunc`. Fixing code only requires adding a new `trace string` argument to `HandlePanicFunc`. [PR #423](https://github.com/riverqueue/river/pull/423).
19+
20+
``` go
21+
# before
22+
HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any) *ErrorHandlerResult
23+
24+
# after
25+
HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult
26+
```
27+
1428
### Fixed
1529

1630
- Pausing or resuming a queue that was already paused or not paused respectively no longer returns `rivertype.ErrNotFound`. The same goes for pausing or resuming using the all queues string (`*`) when no queues are in the database (previously that also returned `rivertype.ErrNotFound`). [PR #408](https://github.com/riverqueue/river/pull/408).

client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2300,7 +2300,7 @@ func Test_Client_ErrorHandler(t *testing.T) {
23002300

23012301
var panicHandlerCalled bool
23022302
config.ErrorHandler = &testErrorHandler{
2303-
HandlePanicFunc: func(ctx context.Context, job *rivertype.JobRow, panicVal any) *ErrorHandlerResult {
2303+
HandlePanicFunc: func(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult {
23042304
require.Equal(t, "panic val", panicVal)
23052305
panicHandlerCalled = true
23062306
return &ErrorHandlerResult{}

error_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type ErrorHandler interface {
2020
//
2121
// Context is descended from the one used to start the River client that
2222
// worked the job.
23-
HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any) *ErrorHandlerResult
23+
HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult
2424
}
2525

2626
type ErrorHandlerResult struct {

example_error_handler_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func (*CustomErrorHandler) HandleError(ctx context.Context, job *rivertype.JobRo
2222
return nil
2323
}
2424

25-
func (*CustomErrorHandler) HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any) *river.ErrorHandlerResult {
25+
func (*CustomErrorHandler) HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *river.ErrorHandlerResult {
2626
fmt.Printf("Job panicked with: %v\n", panicVal)
2727

2828
// Either function can also set the job to be immediately cancelled.

job_executor.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ var ErrJobCancelledRemotely = JobCancel(errors.New("job cancelled remotely"))
100100
type jobExecutorResult struct {
101101
Err error
102102
NextRetry time.Time
103-
PanicTrace []byte
103+
PanicTrace string
104104
PanicVal any
105105
}
106106

@@ -175,7 +175,7 @@ func (e *jobExecutor) execute(ctx context.Context) (res *jobExecutorResult) {
175175
)
176176

177177
res = &jobExecutorResult{
178-
PanicTrace: debug.Stack(),
178+
PanicTrace: string(debug.Stack()),
179179
PanicVal: recovery,
180180
}
181181
}
@@ -234,7 +234,7 @@ func (e *jobExecutor) invokeErrorHandler(ctx context.Context, res *jobExecutorRe
234234

235235
case res.PanicVal != nil:
236236
errorHandlerRes = invokeAndHandlePanic("HandlePanic", func() *ErrorHandlerResult {
237-
return e.ErrorHandler.HandlePanic(ctx, e.JobRow, res.PanicVal)
237+
return e.ErrorHandler.HandlePanic(ctx, e.JobRow, res.PanicVal, res.PanicTrace)
238238
})
239239
}
240240

@@ -316,7 +316,7 @@ func (e *jobExecutor) reportError(ctx context.Context, res *jobExecutorResult) {
316316
At: e.start,
317317
Attempt: e.JobRow.Attempt,
318318
Error: res.ErrorStr(),
319-
Trace: string(res.PanicTrace),
319+
Trace: res.PanicTrace,
320320
}
321321

322322
errData, err := json.Marshal(attemptErr)

job_executor_test.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,16 @@ type testErrorHandler struct {
8989
HandleErrorFunc func(ctx context.Context, job *rivertype.JobRow, err error) *ErrorHandlerResult
9090

9191
HandlePanicCalled bool
92-
HandlePanicFunc func(ctx context.Context, job *rivertype.JobRow, panicVal any) *ErrorHandlerResult
92+
HandlePanicFunc func(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult
9393
}
9494

9595
// Test handler with no-ops for both error handling functions.
9696
func newTestErrorHandler() *testErrorHandler {
9797
return &testErrorHandler{
9898
HandleErrorFunc: func(ctx context.Context, job *rivertype.JobRow, err error) *ErrorHandlerResult { return nil },
99-
HandlePanicFunc: func(ctx context.Context, job *rivertype.JobRow, panicVal any) *ErrorHandlerResult { return nil },
99+
HandlePanicFunc: func(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult {
100+
return nil
101+
},
100102
}
101103
}
102104

@@ -105,9 +107,9 @@ func (h *testErrorHandler) HandleError(ctx context.Context, job *rivertype.JobRo
105107
return h.HandleErrorFunc(ctx, job, err)
106108
}
107109

108-
func (h *testErrorHandler) HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any) *ErrorHandlerResult {
110+
func (h *testErrorHandler) HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult {
109111
h.HandlePanicCalled = true
110-
return h.HandlePanicFunc(ctx, job, panicVal)
112+
return h.HandlePanicFunc(ctx, job, panicVal, trace)
111113
}
112114

113115
func TestJobExecutor_Execute(t *testing.T) {
@@ -565,8 +567,10 @@ func TestJobExecutor_Execute(t *testing.T) {
565567
executor, bundle := setup(t)
566568

567569
executor.WorkUnit = newWorkUnitFactoryWithCustomRetry(func() error { panic("panic val") }, nil).MakeUnit(bundle.jobRow)
568-
bundle.errorHandler.HandlePanicFunc = func(ctx context.Context, job *rivertype.JobRow, panicVal any) *ErrorHandlerResult {
570+
bundle.errorHandler.HandlePanicFunc = func(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult {
569571
require.Equal(t, "panic val", panicVal)
572+
require.Contains(t, trace, "runtime/debug.Stack()\n")
573+
570574
return nil
571575
}
572576

@@ -586,7 +590,7 @@ func TestJobExecutor_Execute(t *testing.T) {
586590
executor, bundle := setup(t)
587591

588592
executor.WorkUnit = newWorkUnitFactoryWithCustomRetry(func() error { panic("panic val") }, nil).MakeUnit(bundle.jobRow)
589-
bundle.errorHandler.HandlePanicFunc = func(ctx context.Context, job *rivertype.JobRow, panicVal any) *ErrorHandlerResult {
593+
bundle.errorHandler.HandlePanicFunc = func(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult {
590594
return &ErrorHandlerResult{SetCancelled: true}
591595
}
592596

@@ -606,7 +610,7 @@ func TestJobExecutor_Execute(t *testing.T) {
606610
executor, bundle := setup(t)
607611

608612
executor.WorkUnit = newWorkUnitFactoryWithCustomRetry(func() error { panic("panic val") }, nil).MakeUnit(bundle.jobRow)
609-
bundle.errorHandler.HandlePanicFunc = func(ctx context.Context, job *rivertype.JobRow, panicVal any) *ErrorHandlerResult {
613+
bundle.errorHandler.HandlePanicFunc = func(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *ErrorHandlerResult {
610614
panic("panic handler panicked!")
611615
}
612616

0 commit comments

Comments
 (0)