enhance: serialize user update methods - #1042
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1042 +/- ##
==========================================
- Coverage 75.77% 75.76% -0.02%
==========================================
Files 66 66
Lines 3620 3647 +27
==========================================
+ Hits 2743 2763 +20
- Misses 666 671 +5
- Partials 211 213 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| type UserAPI struct { | ||
| DB UserDatabase | ||
| type UserAPI[T UserDatabase[T]] struct { | ||
| DB T |
There was a problem hiding this comment.
Can we use database.GormDatabase directly here? I don't think we need the interface here. I think I previously added this when the DB was mocked, but this isn't done anymore.
| if success := successOrAbort(ctx, 500, txdb.DeleteUserByID(id)); !success { | ||
| return err | ||
| } |
There was a problem hiding this comment.
After the transaction commits/aborts there is another ctx.AbortWithError(500, err). successOrAbort already Aborts the ctx. The context should only be aborted once, as I think otherwise multiple errors are printed to the body.
| if !commitError || err == nil { | ||
| break | ||
| } |
There was a problem hiding this comment.
It took me a while to understand this case, maybe it could be done like this with explicit definition what the boolean is used for?
func (a *UserAPI[T]) DeleteUserByID(ctx *gin.Context) {
withID(ctx, "id", func(id uint) {
user, err := a.DB.GetUserByID(id)
if success := successOrAbort(ctx, 500, err); !success {
return
}
if user == nil {
ctx.AbortWithError(404, errors.New("user does not exist"))
return
}
for range 3 {
retryable := true
err = a.DB.Txn(func(txdb T) error {
if err := txdb.DeleteUserByID(id); err != nil {
return err
}
anotherAdmin, err := txdb.GetUsers(&model.User{Admin: true})
if err != nil {
return err
}
if user.Admin && len(anotherAdmin) == 0 {
retryable = false
return errCannotDeleteLastAdmin
}
if err := a.UserChangeNotifier.fireUserDeleted(id); err != nil {
retryable = false
return err
}
return nil
})
if err == nil {
// user deleted successfully
ctx.Status(200)
return
}
if retryable {
continue
}
if err != nil {
status := 500
if errors.Is(err, errCannotDeleteLastAdmin) {
status = 400
}
ctx.AbortWithError(status, err)
return
}
}
})
}There was a problem hiding this comment.
I think only commit errors should be retryable (temporary serialization failures). If regular statements returned errors it means there is something wrong with the database connection or underlying data, we should just return immediately.
There was a problem hiding this comment.
Okay, then only retry then. Can it use the adjusted statements so than err == null is a separate statement and commitError == true does an explicit continue? (I think I still prefer calling it retryable) I had trouble understanding the retry condition.
Serializes user update actions to prevent race conditions leading to unexpected results.
I removed the 'Test_UpdateUserByID_EmptyPassword_Expect400' test as it seemed to be a mistake - it should return 200, it returned 400 in the test because there wasn't a second admin.