Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add signals.RegisterStackTraceHandlerOnSignals for custom handling logic #129

Merged
merged 3 commits into from
Dec 11, 2018

Conversation

bmoylan
Copy link
Contributor

@bmoylan bmoylan commented Dec 10, 2018

This change is Reviewable

Copy link
Contributor

@nmiyake nmiyake left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 1 files at r1.
Reviewable status: all files reviewed, 8 unresolved discussions (waiting on @bmoylan)

a discussion (no related file):
Request that the API be modified so the handler returns an error -- that will prevent clients from having to use the error handler they define themselves in a provided closure (which introduces some awkwardness in requiring parameters to be declared before providing them to the function and mixes some of the concepts)



signals/signals.go, line 64 at r1 (raw file):

// listener when called.
func RegisterStackTraceWriterOnSignals(out io.Writer, errHandler func(error), sig ...os.Signal) (unregister func()) {
	handler := func(body []byte) {

body doesn't seem semantically correct as the variable name in this scenario.


signals/signals.go, line 65 at r1 (raw file):

func RegisterStackTraceWriterOnSignals(out io.Writer, errHandler func(error), sig ...os.Signal) (unregister func()) {
	handler := func(body []byte) {
		if _, err := out.Write(body); err != nil && errHandler != nil {

With proposed API, the function can become:

_, err := out.Write(snapshot)
return err

This makes the API a bit cleaner -- as it stands, is a bit confusing that the error handler passed to the other function is used in the closure here.


signals/signals.go, line 73 at r1 (raw file):

// RegisterStackTraceHandlerOnSignals starts a goroutine that listens for the specified signals and calls outHandler with a
// pprof-formatted snapshot of all running goroutines when any of the provided signals are received. If writing to out returns an

The section on "If writing to out returns an error" should be updated/re-worded.


signals/signals.go, line 76 at r1 (raw file):

// error, that error is provided to the errHandler function if one is provided. Returns a function that unregisters the
// listener when called.
func RegisterStackTraceHandlerOnSignals(outHandler func([]byte), errHandler func(error), sig ...os.Signal) (unregister func()) {

I think the more direct translation of the previous API is to have outHandler be a func([]byte) error and to pass that returned error to the error handler.


signals/signals.go, line 76 at r1 (raw file):

// error, that error is provided to the errHandler function if one is provided. Returns a function that unregisters the
// listener when called.
func RegisterStackTraceHandlerOnSignals(outHandler func([]byte), errHandler func(error), sig ...os.Signal) (unregister func()) {

out probably wasn't the best variable name initially, but in this context I think it makes less sense -- the handler may not actually be writing output. Prefer something more along the lines of stackTraceHandler, pprofHandler, pprofBytesHandler etc. (doesn't have to be any of these specifically)


signals/signals.go, line 90 at r1 (raw file):

				err := pprof.Lookup("goroutine").WriteTo(&buf, 2)
				if err != nil && errHandler != nil {
					errHandler(err)

We can keep this fore completeness, but as written this is actually not reachable -- bytes.Buffer never returns a non-nil error in its Write function, and WriteTo only returns the error of the provided writer.

I think it would be acceptable to also do an underscore assignment for the error and to explain that the error cannot be reached based on logic (leaving is also fine, but may be a bit confusing to see the error handling code twice)


signals/signals.go, line 93 at r1 (raw file):

				}
				if outHandler != nil {
					outHandler(buf.Bytes())

Returned error should be stored and provided to the errHandler if non-nil

Copy link
Contributor Author

@bmoylan bmoylan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: all files reviewed, 8 unresolved discussions (waiting on @nmiyake)

a discussion (no related file):

Previously, nmiyake (Nick Miyake) wrote…

Request that the API be modified so the handler returns an error -- that will prevent clients from having to use the error handler they define themselves in a provided closure (which introduces some awkwardness in requiring parameters to be declared before providing them to the function and mixes some of the concepts)

Agreed and done, thanks for the review!



signals/signals.go, line 64 at r1 (raw file):

Previously, nmiyake (Nick Miyake) wrote…

body doesn't seem semantically correct as the variable name in this scenario.

changed to stackTraceOutput


signals/signals.go, line 65 at r1 (raw file):

Previously, nmiyake (Nick Miyake) wrote…

With proposed API, the function can become:

_, err := out.Write(snapshot)
return err

This makes the API a bit cleaner -- as it stands, is a bit confusing that the error handler passed to the other function is used in the closure here.

Done.


signals/signals.go, line 73 at r1 (raw file):

Previously, nmiyake (Nick Miyake) wrote…

The section on "If writing to out returns an error" should be updated/re-worded.

Done.


signals/signals.go, line 76 at r1 (raw file):

Previously, nmiyake (Nick Miyake) wrote…

I think the more direct translation of the previous API is to have outHandler be a func([]byte) error and to pass that returned error to the error handler.

Done.


signals/signals.go, line 76 at r1 (raw file):

Previously, nmiyake (Nick Miyake) wrote…

out probably wasn't the best variable name initially, but in this context I think it makes less sense -- the handler may not actually be writing output. Prefer something more along the lines of stackTraceHandler, pprofHandler, pprofBytesHandler etc. (doesn't have to be any of these specifically)

Done.


signals/signals.go, line 90 at r1 (raw file):

Previously, nmiyake (Nick Miyake) wrote…

We can keep this fore completeness, but as written this is actually not reachable -- bytes.Buffer never returns a non-nil error in its Write function, and WriteTo only returns the error of the provided writer.

I think it would be acceptable to also do an underscore assignment for the error and to explain that the error cannot be reached based on logic (leaving is also fine, but may be a bit confusing to see the error handling code twice)

Done.


signals/signals.go, line 93 at r1 (raw file):

Previously, nmiyake (Nick Miyake) wrote…

Returned error should be stored and provided to the errHandler if non-nil

Done.

Copy link
Contributor

@nmiyake nmiyake left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 1 files at r2.
Reviewable status: all files reviewed, 3 unresolved discussions (waiting on @bmoylan)

a discussion (no related file):

Previously, bmoylan (Brad Moylan) wrote…

Agreed and done, thanks for the review!

Thanks!

In looking at it again, noticed a few more high-level things that I think will improve this:

  • Bail out early as a no-op if handler is nil or sig is empty
  • Use context to control lifecycle of goroutine instead of a cancel function

See feedback for details.



signals/signals.go, line 74 at r2 (raw file):

// pprof-formatted snapshot of all running goroutines when any of the provided signals are received. If stackTraceHandler returns
// an error, that error is provided to the errHandler function if one is provided. Returns a function that unregisters the listener when called.
func RegisterStackTraceHandlerOnSignals(stackTraceHandler func(stackTraceOutput []byte) error, errHandler func(error), sig ...os.Signal) (unregister func()) {

Since we're adding this as a new function, let's handle cancellation using a context rather than returning a cancel function -- I've found that it's a much better pattern because it allows the lifecycle of the goroutine to be tied to a larger context, but we can still manually return the cancel function if we want to do so.

So, signature changes to:

func RegisterStackTraceHandlerOnSignals(ctx context.Context, ...) {
}

Then we no longer declare/return a cancel function, and goroutine select adds a case for <-ctx.Done().

To keep back-compat for the previous function:

ctx, cancel := context.WithCancel(context.Background())
RegisterStackTraceHandlerOnSignals(ctx, ...)
return cancel

This should make the usage at the call site easier as well (can pass in the "real" context and don't need to worry about deferring the cancel).


signals/signals.go, line 87 at r2 (raw file):

				var buf bytes.Buffer
				_ = pprof.Lookup("goroutine").WriteTo(&buf, 2) // bytes.Buffer's Write never returns an error, so we swallow it
				if stackTraceHandler != nil {

Just noticed this when looking at this again, but if stackTraceHandler is nil, this entire function is basically a no-op (we will write the pprof to bytes.Buffer, but never do anything with it).

I think it makes sense to just check on the no-op cases (len(sig) == 0 being the other one) at the beginning and bail out if it will be a noop:

if stackTraceHandler == nil || len(sig) == 0 {
     return fun(){}
}

Once that's done, we don't need to bother with the nil check.

We should just add to the function documentation that the function is a no-op/no goroutine is started if the handler is nil or no signals are specified.

Copy link
Contributor Author

@bmoylan bmoylan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 0 of 1 files reviewed, 3 unresolved discussions (waiting on @nmiyake and @bmoylan)


signals/signals.go, line 74 at r2 (raw file):

Previously, nmiyake (Nick Miyake) wrote…

Since we're adding this as a new function, let's handle cancellation using a context rather than returning a cancel function -- I've found that it's a much better pattern because it allows the lifecycle of the goroutine to be tied to a larger context, but we can still manually return the cancel function if we want to do so.

So, signature changes to:

func RegisterStackTraceHandlerOnSignals(ctx context.Context, ...) {
}

Then we no longer declare/return a cancel function, and goroutine select adds a case for <-ctx.Done().

To keep back-compat for the previous function:

ctx, cancel := context.WithCancel(context.Background())
RegisterStackTraceHandlerOnSignals(ctx, ...)
return cancel

This should make the usage at the call site easier as well (can pass in the "real" context and don't need to worry about deferring the cancel).

Done.


signals/signals.go, line 87 at r2 (raw file):

Previously, nmiyake (Nick Miyake) wrote…

Just noticed this when looking at this again, but if stackTraceHandler is nil, this entire function is basically a no-op (we will write the pprof to bytes.Buffer, but never do anything with it).

I think it makes sense to just check on the no-op cases (len(sig) == 0 being the other one) at the beginning and bail out if it will be a noop:

if stackTraceHandler == nil || len(sig) == 0 {
     return fun(){}
}

Once that's done, we don't need to bother with the nil check.

We should just add to the function documentation that the function is a no-op/no goroutine is started if the handler is nil or no signals are specified.

Done. As discussed in person, we'll match os/signal's API for when no signals are provided.

Copy link
Contributor

@nmiyake nmiyake left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 1 files at r3.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on @bmoylan)

a discussion (no related file):

Previously, nmiyake (Nick Miyake) wrote…

Thanks!

In looking at it again, noticed a few more high-level things that I think will improve this:

  • Bail out early as a no-op if handler is nil or sig is empty
  • Use context to control lifecycle of goroutine instead of a cancel function

See feedback for details.

Great, :lgtm: -- thanks!


@bmoylan bmoylan merged commit 97c8883 into master Dec 11, 2018
@bmoylan bmoylan deleted the bm/stacktrace-handler branch December 11, 2018 00:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants