Skip to content

enhance: serialize user update methods - #1042

Open
eternal-flame-AD wants to merge 1 commit into
masterfrom
api-user-txn
Open

enhance: serialize user update methods#1042
eternal-flame-AD wants to merge 1 commit into
masterfrom
api-user-txn

Conversation

@eternal-flame-AD

Copy link
Copy Markdown
Member

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.

@eternal-flame-AD
eternal-flame-AD requested a review from a team as a code owner September 2, 2026 09:00
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.40984% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.76%. Comparing base (14bfc25) to head (ddfa0cc).

Files with missing lines Patch % Lines
api/user.go 75.55% 7 Missing and 4 partials ⚠️
database/database.go 66.66% 1 Missing and 1 partial ⚠️
database/user.go 60.00% 1 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread api/user.go
type UserAPI struct {
DB UserDatabase
type UserAPI[T UserDatabase[T]] struct {
DB T

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/user.go
Comment on lines +351 to +353
if success := successOrAbort(ctx, 500, txdb.DeleteUserByID(id)); !success {
return err
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/user.go
Comment on lines +368 to +370
if !commitError || err == nil {
break
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
			}
		}
	})
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants