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
On error callback #89
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I'd considered this previously, but found that I never actually had a real use case for it. Do you have an example of how you'd want to use this?
if p.errorCallBack != nil { | ||
p.errorCallBack(err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be behind the mutex? It seems useful to be able to guarantee that the callback will never be called concurrently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have also thought about that but if the callback is doing something time intensive it would block the whole pool.
I don't really have a strong opinion about this, it's a matter communication to the user so they know how it behaves either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, makes sense. I also don't feel very strongly. I think it's fine the way it is, but we should document that the callback must be safe to call from multiple goroutines.
My current use case for this I want to log the errors as they happen but I don't want to necessarily Stop the pool. Example would be: func errorCB(ctx context.Context, err error) {
if err != nil {
log.Print(err)
}
}
type handler struct {
pool *pool.ContextPool
}
func (h *handler) hello(w http.ResponseWriter, req *http.Request) {
h.pool.Go(func(ctx context.Context) error {
time.Sleep(time.Second)
return errors.New("oh no!")
})
}
func (h *handler) close() error {
return h.pool.Wait()
}
func main() {
h := &handler{
pool: pool.New().ContextPool().WithErrorCallback(errorCB),
}
defer func() {
if err := h.close(); err != nil {
log.Print(err)
}
}()
http.HandleFunc("/hello", h.hello)
http.ListenAndServe(":8090", nil)
} |
Codecov Report
@@ Coverage Diff @@
## main #89 +/- ##
==========================================
- Coverage 99.28% 98.37% -0.91%
==========================================
Files 12 12
Lines 419 431 +12
==========================================
+ Hits 416 424 +8
- Misses 3 7 +4
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
// WithErrorCallback configures the pool to call f everytime a task returns error. | ||
func (p *ContextPool) WithErrorCallback(f func(err error)) *ContextPool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we expose an equivalent on ResultErrorPool
and ResultContextPool
?
Thinking out loud a bit here. Apologies if my comments below aren't clear -- I'm struggling to put words to my thoughts. In general, I am a little concerned about this change because it encourages a pattern of long-lived pools. The problem with long-lived pools is that 1) errors are not propagated until the pool completes, and 2) panics are not propagated until the pool completes. This PR covers case 1, but it does not cover case 2. In your example, you spawn a goroutine per request. However, the HTTP response writer is never actually used and the goroutine does not complete before the request completes, so the client will never see result or error. In practice, a HTTP server will parallelize requests, so a pool is not needed here. Additionally, handling requests should almost be done synchronously because you still need to write the response to the I know this probably sounds a bit pedantic, but I think adding streaming capabilities to |
Excellent points, For the case of handling panics I think it wouldn't be a bad idea to add a func (p *Catcher) tryRecover() {
if val := recover(); val != nil {
rp := NewRecoveredPanic(1, val)
if p.onPanicCallback == nil {
p.recovered.CompareAndSwap(nil, &rp)
} else {
p.onPanicCallback(&rp)
}
}
} In my example I've kept code short for the sake of brevity, think of the goroutine invocation within the HTTP request would just initiate a background process which shouldn't interrupt other invocations of the same task in case of error but the error should be dealt with in some way (reported/logged, etc.). Could you please elaborate a bit more on "I think adding streaming capabilities to pool might be a mistake", since I believe the Error/Panic Callbacks would/should not really promote any streaming patterns (and why streaming patterns are bad in this case?)? Moreover, I have been thinking what if we extend these callbacks to Pre/Post hooks which would be respectively invoked before and after the each task. Let me know what you think about it? |
@camdencheek chiming in here on behalf of Cloud - we recently had a similar use case, where specifically we needed to do error handling after the pool becomes aware of the error (adds it to the underling
+1 on this - IMO if you want to expose "job lifecycle" concepts, it probably should live in (aside - now that I think of it, I think I like the old naming of |
I think a callback that happens as soon as something happens is what Camden means by "streaming", and as I note in my previous comment I think the intended model is the |
Register a callback that is called every time a task returns an error.