-
Notifications
You must be signed in to change notification settings - Fork 4
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 TxWithRetries and test cases #109
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,12 @@ | ||
package db | ||
|
||
import ( | ||
"errors" | ||
"log" | ||
|
||
"go.mongodb.org/mongo-driver/mongo" | ||
) | ||
|
||
// DuplicateKeyError is an error type for duplicate key errors | ||
type DuplicateKeyError struct { | ||
Key string | ||
|
@@ -43,3 +50,44 @@ func IsNotFoundError(err error) bool { | |
_, ok := err.(*NotFoundError) | ||
return ok | ||
} | ||
|
||
// Error code references: https://www.mongodb.com/docs/manual/reference/error-codes/ | ||
func IsWriteConflictError(err error) bool { | ||
if err == nil { | ||
log.Println("Error is nil, cannot be a write conflict") | ||
return false | ||
} | ||
|
||
var cmdErr *mongo.CommandError | ||
if errors.As(err, &cmdErr) { | ||
if cmdErr == nil { | ||
log.Println("Error is not a CommandError, cannot be a write conflict") | ||
return false | ||
} | ||
log.Println("Checking for write conflict error, code received:", cmdErr.Code) | ||
return cmdErr.Code == 112 | ||
} | ||
|
||
log.Println("Error does not conform to CommandError") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just adding on top of this. i don't think we actually need any logs in this file. it's the calling methods responsibility to log errors. |
||
return false | ||
} | ||
|
||
func IsTransactionAbortedError(err error) bool { | ||
if err == nil { | ||
log.Println("Error is nil, cannot be a transaction aborted") | ||
return false | ||
} | ||
|
||
var cmdErr *mongo.CommandError | ||
if errors.As(err, &cmdErr) { | ||
if cmdErr == nil { | ||
log.Println("Error is not a CommandError, cannot be a transaction aborted") | ||
return false | ||
} | ||
log.Println("Checking for transaction aborted error, code received:", cmdErr.Code) | ||
return cmdErr.Code == 251 | ||
} | ||
|
||
log.Println("Error does not conform to CommandError") | ||
return false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ import ( | |
|
||
"github.com/babylonchain/staking-api-service/internal/db/model" | ||
"github.com/babylonchain/staking-api-service/internal/types" | ||
"go.mongodb.org/mongo-driver/mongo" | ||
"go.mongodb.org/mongo-driver/mongo/options" | ||
) | ||
|
||
type DBClient interface { | ||
|
@@ -54,3 +56,10 @@ type DBClient interface { | |
) error | ||
FindTopStakersByTvl(ctx context.Context, paginationToken string) (*DbResultMap[model.StakerStatsDocument], error) | ||
} | ||
type DBSession interface { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you remind me why we need to define this here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. was trying to make it an interface so that the function could consume both test db and mongo db |
||
EndSession(ctx context.Context) | ||
WithTransaction(ctx context.Context, fn func(sessCtx mongo.SessionContext) (interface{}, error), opts ...*options.TransactionOptions) (interface{}, error) | ||
} | ||
type DBTransactionClient interface { | ||
StartSession(opts ...*options.SessionOptions) (DBSession, error) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package db | ||
|
||
import ( | ||
"context" | ||
"log" | ||
"time" | ||
|
||
utils "github.com/babylonchain/staking-api-service/internal/utils" | ||
"go.mongodb.org/mongo-driver/mongo" | ||
"go.mongodb.org/mongo-driver/mongo/options" | ||
) | ||
|
||
const ( | ||
DefaultMaxAttempts = 4 // max attempt INCLUDES the first execution | ||
DefaultInitialBackoff = 100 * time.Millisecond | ||
DefaultBackoffFactor = 2.0 | ||
) | ||
|
||
type dbTransactionClient struct { | ||
*mongo.Client | ||
} | ||
|
||
type dbSessionWrapper struct { | ||
mongo.Session | ||
} | ||
|
||
func (c *dbTransactionClient) StartSession(opts...*options.SessionOptions) (DBSession, error) { | ||
session, err := c.Client.StartSession(opts...) | ||
if err!= nil { | ||
return nil, err | ||
} | ||
return &dbSessionWrapper{session}, nil | ||
} | ||
|
||
|
||
func (s *dbSessionWrapper) EndSession(ctx context.Context) { | ||
s.Session.EndSession(ctx) | ||
} | ||
|
||
func (s *dbSessionWrapper) WithTransaction(ctx context.Context, fn func(sessCtx mongo.SessionContext) (interface{}, error), opts...*options.TransactionOptions) (interface{}, error) { | ||
return s.Session.WithTransaction(ctx, fn, opts...) | ||
} | ||
|
||
func TxWithRetries( | ||
ctx context.Context, | ||
dbTransactionClient DBTransactionClient, | ||
txnFunc func(sessCtx mongo.SessionContext) (interface{}, error), | ||
) (interface{}, error) { | ||
maxAttempts := DefaultMaxAttempts | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes I can remove that |
||
initialBackoff := DefaultInitialBackoff | ||
backoffFactor := DefaultBackoffFactor | ||
|
||
var ( | ||
result interface{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there might be too many tabs here |
||
err error | ||
backoff = initialBackoff | ||
) | ||
|
||
for attempt := 1; attempt <= maxAttempts; attempt++ { | ||
session, sessionErr := dbTransactionClient.StartSession(); | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extra space |
||
if sessionErr != nil { | ||
return nil, sessionErr | ||
} | ||
|
||
|
||
result, err = session.WithTransaction(ctx, txnFunc) | ||
session.EndSession(ctx) | ||
|
||
if err != nil { | ||
if shouldRetry(err) && attempt < maxAttempts { | ||
log.Printf("Attempt %d failed with retryable error: %v. Retrying after %v...", attempt, err, backoff) | ||
utils.Sleep(backoff) | ||
backoff *= time.Duration(backoffFactor) | ||
continue | ||
} | ||
log.Printf("Attempt %d failed with non-retryable error: %v", attempt, err) | ||
return nil, err | ||
} | ||
break | ||
} | ||
return result, nil | ||
} | ||
|
||
// Check for network-related, timeout errors, write conflicts or transaction aborted, which are generally transient should retry. Other errors such as duplicated keys or other non-specified errors should be considered non-retryable. | ||
func shouldRetry(err error) bool { | ||
if mongo.IsNetworkError(err) { | ||
return true | ||
} | ||
if mongo.IsTimeout(err) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think the network error and timeout error from mongo is already retry handled by the mongo driver implementation of |
||
return true | ||
} | ||
|
||
if IsWriteConflictError(err) { | ||
return true | ||
} | ||
|
||
if IsTransactionAbortedError(err) { | ||
return true | ||
} | ||
|
||
return false | ||
} |
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.
all the methods here looks very similar, they all try to check the cmdErr code.
shall we have a private generic method that does that and compare the code instead? then this generic method can be called by IsWriteConflictError etc by passing in the
122
code into the method