From 2a150d6aa190a4be0d1fc4f4b8e16056fc81d666 Mon Sep 17 00:00:00 2001 From: Angelo Marletta Date: Wed, 8 May 2024 20:23:00 -0700 Subject: [PATCH] Deliver async reports from a goroutine pool --- v2/bugsnag.go | 10 ++++++++++ v2/configuration.go | 23 +++++++++++++++++++++++ v2/go.mod | 1 + v2/go.sum | 2 ++ v2/report_publisher.go | 6 ++++-- 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/v2/bugsnag.go b/v2/bugsnag.go index 631d2e93..45575309 100644 --- a/v2/bugsnag.go +++ b/v2/bugsnag.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/alitto/pond" "github.com/bugsnag/bugsnag-go/v2/device" "github.com/bugsnag/bugsnag-go/v2/errors" "github.com/bugsnag/bugsnag-go/v2/sessions" @@ -39,6 +40,8 @@ var DefaultSessionPublishInterval = 60 * time.Second var defaultNotifier = Notifier{&Config, nil} var sessionTracker sessions.SessionTracker +var asyncPool *pond.WorkerPool + // Configure Bugsnag. The only required setting is the APIKey, which can be // obtained by clicking on "Settings" in your Bugsnag dashboard. This function // is also responsible for installing the global panic handler, so it should be @@ -51,6 +54,7 @@ func Configure(config Configuration) { // Only do once in case the user overrides the default panichandler, and // configures multiple times. panicHandlerOnce.Do(Config.PanicHandler) + setupAsyncPool() } // StartSession creates new context from the context.Context instance with @@ -253,6 +257,8 @@ func init() { Logger: log.New(os.Stdout, log.Prefix(), log.Flags()), PanicHandler: defaultPanicHandler, Transport: http.DefaultTransport, + NumGoroutines: 10, + MaxPendingReports: 1000, flushSessionsOnRepanic: true, }) @@ -282,3 +288,7 @@ func updateSessionConfig() { Logger: Config.Logger, }) } + +func setupAsyncPool() { + asyncPool = pond.New(Config.NumGoroutines, Config.MaxPendingReports) +} diff --git a/v2/configuration.go b/v2/configuration.go index 48680bb3..59c0ce30 100644 --- a/v2/configuration.go +++ b/v2/configuration.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" ) @@ -98,9 +99,19 @@ type Configuration struct { // can be configured if you are in an environment // that has stringent conditions on making http requests. Transport http.RoundTripper + // Whether bugsnag should notify synchronously. This defaults to false which // causes bugsnag-go to spawn a new goroutine for each notification. Synchronous bool + + // Number of goroutines to use for sending notifications in asynchronous + // mode. This defaults to 10. + NumGoroutines int + + // The maximum number of reports that can be pending in asynchronous mode. + // This defaults to 1000. + MaxPendingReports int + // Whether the notifier should send all sessions recorded so far to Bugsnag // when repanicking to ensure that no session information is lost in a // fatal crash. @@ -307,6 +318,18 @@ func (config *Configuration) loadEnv() { if synchronous := os.Getenv("BUGSNAG_SYNCHRONOUS"); synchronous != "" { envConfig.Synchronous = synchronous == "1" } + if numGoroutines := os.Getenv("BUGSNAG_NUM_GOROUTINES"); numGoroutines != "" { + num, err := strconv.Atoi(numGoroutines) + if err != nil { + envConfig.NumGoroutines = num + } + } + if maxPendingReports := os.Getenv("BUGSNAG_MAX_PENDING_REPORTS"); maxPendingReports != "" { + max, err := strconv.Atoi(maxPendingReports) + if err != nil { + envConfig.MaxPendingReports = max + } + } if disablePanics := os.Getenv("BUGSNAG_DISABLE_PANIC_HANDLER"); disablePanics == "1" { envConfig.PanicHandler = func() {} } diff --git a/v2/go.mod b/v2/go.mod index d891c127..412d1d65 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,6 +3,7 @@ module github.com/bugsnag/bugsnag-go/v2 go 1.15 require ( + github.com/alitto/pond v1.8.3 github.com/bitly/go-simplejson v0.5.1 github.com/bugsnag/panicwrap v1.3.4 github.com/google/uuid v1.6.0 diff --git a/v2/go.sum b/v2/go.sum index 83d02484..3556764e 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,3 +1,5 @@ +github.com/alitto/pond v1.8.3 h1:ydIqygCLVPqIX/USe5EaV/aSRXTRXDEI9JwuDdu+/xs= +github.com/alitto/pond v1.8.3/go.mod h1:CmvIIGd5jKLasGI3D87qDkQxjzChdKMmnXMg3fG6M6Q= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/bugsnag/panicwrap v1.3.4 h1:A6sXFtDGsgU/4BLf5JT0o5uYg3EeKgGx3Sfs+/uk3pU= diff --git a/v2/report_publisher.go b/v2/report_publisher.go index 87d228fc..a4d0e53c 100644 --- a/v2/report_publisher.go +++ b/v2/report_publisher.go @@ -17,11 +17,13 @@ func (*defaultReportPublisher) publishReport(p *payload) error { return p.deliver() } - go func(p *payload) { + if !asyncPool.TrySubmit(func() { if err := p.deliver(); err != nil { // Ensure that any errors are logged if they occur in a goroutine. p.logf("bugsnag/defaultReportPublisher.publishReport: %v", err) } - }(p) + }) { + return fmt.Errorf("failed to submit report to async pool") + } return nil }