From 373d82416dfa9ee3b066eec7a45c8a542f361643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Guard=C3=A3o=20Gandarez?= Date: Mon, 7 Aug 2023 16:20:30 -0300 Subject: [PATCH] Catch panic from bbolt open connection --- pkg/offline/error.go | 20 ++++++++++ pkg/offline/offline.go | 83 +++++++++++++++++++++++++----------------- 2 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 pkg/offline/error.go diff --git a/pkg/offline/error.go b/pkg/offline/error.go new file mode 100644 index 00000000..7cd06d6e --- /dev/null +++ b/pkg/offline/error.go @@ -0,0 +1,20 @@ +package offline + +import ( + "fmt" +) + +// ErrOpenDB is an error returned when the database cannot be opened. +type ErrOpenDB struct { + Err error +} + +// Error method to implement error interface. +func (e ErrOpenDB) Error() string { + return e.Err.Error() +} + +// Message method to implement wakaerror.Error interface. +func (e ErrOpenDB) Message() string { + return fmt.Sprintf("failed to open db connection: %s", e.Err) +} diff --git a/pkg/offline/offline.go b/pkg/offline/offline.go index cef87f0c..eeed7ce9 100644 --- a/pkg/offline/offline.go +++ b/pkg/offline/offline.go @@ -7,6 +7,7 @@ import ( "math" "net/http" "path/filepath" + "runtime/debug" "time" "github.com/wakatime/wakatime-cli/pkg/api" @@ -66,10 +67,17 @@ func WithQueue(filepath string) heartbeat.HandleOption { requeueErr := pushHeartbeatsWithRetry(filepath, hh) if requeueErr != nil { + if errors.Is(requeueErr, &ErrOpenDB{}) { + log.Errorf("failed to push heartbeats to queue: %s", requeueErr.(ErrOpenDB).Message()) + + return nil, nil + } + return nil, fmt.Errorf( "failed to push heartbeats to queue after api error: %s. error: %s", requeueErr, - err) + err, + ) } return nil, err @@ -158,7 +166,7 @@ func Sync(filepath string, syncLimit int) func(next heartbeat.Handle) error { if err != nil { requeueErr := pushHeartbeatsWithRetry(filepath, hh) if requeueErr != nil { - log.Warnf("failed to push heatbeats to queue after api error: %s", requeueErr) + log.Warnf("failed to push heartbeats to queue after api error: %s", requeueErr) } return err @@ -166,7 +174,7 @@ func Sync(filepath string, syncLimit int) func(next heartbeat.Handle) error { err = handleResults(filepath, results, hh) if err != nil { - return fmt.Errorf("failed to handle heatbeats api results: %s", err) + return fmt.Errorf("failed to handle heartbeats api results: %s", err) } } @@ -213,7 +221,7 @@ func handleResults(filepath string, results []heartbeat.Result, hh []heartbeat.H err = pushHeartbeatsWithRetry(filepath, withInvalidStatus) if err != nil { - log.Warnf("failed to push heatbeats with invalid status to queue: %s", err) + log.Warnf("failed to push heartbeats with invalid status to queue: %s", err) } } @@ -226,7 +234,7 @@ func handleResults(filepath string, results []heartbeat.Result, hh []heartbeat.H err = pushHeartbeatsWithRetry(filepath, hh[start:]) if err != nil { - log.Warnf("failed to push leftover heatbeats to queue: %s", err) + log.Warnf("failed to push leftover heartbeats to queue: %s", err) } } @@ -234,16 +242,12 @@ func handleResults(filepath string, results []heartbeat.Result, hh []heartbeat.H } func popHeartbeats(filepath string, limit int) ([]heartbeat.Heartbeat, error) { - db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 10 * time.Minute}) + db, close, err := openDB(filepath) if err != nil { - return nil, fmt.Errorf("failed to open db connection: %s", err) + return nil, err } - defer func() { - if err := db.Close(); err != nil { - log.Debugf("failed to close db file: %s", err) - } - }() + defer close() tx, err := db.Begin(true) if err != nil { @@ -308,16 +312,12 @@ func pushHeartbeatsWithRetry(filepath string, hh []heartbeat.Heartbeat) error { } func pushHeartbeats(filepath string, hh []heartbeat.Heartbeat) error { - db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 10 * time.Minute}) + db, close, err := openDB(filepath) if err != nil { - return fmt.Errorf("failed to open db connection: %s", err) + return err } - defer func() { - if err := db.Close(); err != nil { - log.Debugf("failed to close db file: %s", err) - } - }() + defer close() tx, err := db.Begin(true) if err != nil { @@ -340,16 +340,12 @@ func pushHeartbeats(filepath string, hh []heartbeat.Heartbeat) error { // CountHeartbeats returns the total number of heartbeats in the offline db. func CountHeartbeats(filepath string) (int, error) { - db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 30 * time.Second}) + db, close, err := openDB(filepath) if err != nil { - return 0, fmt.Errorf("failed to open db connection: %s", err) + return 0, err } - defer func() { - if err := db.Close(); err != nil { - log.Debugf("failed to close db file: %s", err) - } - }() + defer close() tx, err := db.Begin(true) if err != nil { @@ -375,16 +371,12 @@ func CountHeartbeats(filepath string) (int, error) { // ReadHeartbeats reads the informed heartbeats in the offline db. func ReadHeartbeats(filepath string, limit int) ([]heartbeat.Heartbeat, error) { - db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 30 * time.Second}) + db, close, err := openDB(filepath) if err != nil { - return nil, fmt.Errorf("failed to open db connection: %s", err) + return nil, err } - defer func() { - if err := db.Close(); err != nil { - log.Debugf("failed to close db file: %s", err) - } - }() + defer close() tx, err := db.Begin(true) if err != nil { @@ -410,6 +402,31 @@ func ReadHeartbeats(filepath string, limit int) ([]heartbeat.Heartbeat, error) { return hh, nil } +// openDB opens a connection to the offline db. +// It returns the pointer to bolt.DB, a function to close the connection and an error. +func openDB(filepath string) (*bolt.DB, func(), error) { + var err error + + defer func() { + if r := recover(); r != nil { + err = ErrOpenDB{Err: fmt.Errorf("panicked: %v", r)} + + log.Errorf("%s. Stack: %s", err, string(debug.Stack())) + } + }() + + db, err := bolt.Open(filepath, 0600, &bolt.Options{Timeout: 30 * time.Second}) + if err != nil { + return nil, nil, fmt.Errorf("failed to open db connection: %s", err) + } + + return db, func() { + if err := db.Close(); err != nil { + log.Debugf("failed to close db file: %s", err) + } + }, err +} + // Queue is a db client to temporarily store heartbeats in bolt db, in case heartbeat // sending to wakatime api is not possible. Transaction handling is left to the user // via the passed in transaction.