-
Notifications
You must be signed in to change notification settings - Fork 11
Add lock to user api #437
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
base: bl4ze/dev
Are you sure you want to change the base?
Add lock to user api #437
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 | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||||||
| package api | ||||||||||||||||||
|
|
||||||||||||||||||
| import ( | ||||||||||||||||||
| "sync" | ||||||||||||||||||
| log "github.com/sirupsen/logrus" | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| var ( | ||||||||||||||||||
| lockManagerInstance *UserLockManager | ||||||||||||||||||
| once sync.Once | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| type UserLockManager struct { | ||||||||||||||||||
| locks sync.Map | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func GetUserLockManager() *UserLockManager { | ||||||||||||||||||
| once.Do(func() { | ||||||||||||||||||
| lockManagerInstance = &UserLockManager{} | ||||||||||||||||||
| }) | ||||||||||||||||||
| return lockManagerInstance | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (m *UserLockManager) getLock(lockId string) *sync.Mutex { | ||||||||||||||||||
| lock, _ := m.locks.LoadOrStore(lockId, &sync.Mutex{}) | ||||||||||||||||||
| return lock.(*sync.Mutex) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (m *UserLockManager) TryLock(lockId string) bool { | ||||||||||||||||||
| lock := m.getLock(lockId) | ||||||||||||||||||
| return lock.TryLock() | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (m *UserLockManager) Unlock(userID string) { | ||||||||||||||||||
| lockVal, exists := m.locks.Load(userID) | ||||||||||||||||||
| if !exists { | ||||||||||||||||||
| log.Errorf("unlock called for non-existent lock, user_id: %s", userID) | ||||||||||||||||||
|
Comment on lines
+35
to
+37
|
||||||||||||||||||
| return | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| lock := lockVal.(*sync.Mutex) | ||||||||||||||||||
|
|
||||||||||||||||||
| // Catch panic from double-unlock | ||||||||||||||||||
| defer func() { | ||||||||||||||||||
| if r := recover(); r != nil { | ||||||||||||||||||
| log.Errorf("double unlock detected for user_id: %s", userID) | ||||||||||||||||||
|
||||||||||||||||||
| } | ||||||||||||||||||
| }() | ||||||||||||||||||
|
|
||||||||||||||||||
|
Comment on lines
+43
to
+49
|
||||||||||||||||||
| // Catch panic from double-unlock | |
| defer func() { | |
| if r := recover(); r != nil { | |
| log.Errorf("double unlock detected for user_id: %s", userID) | |
| } | |
| }() | |
| // Unlock the mutex; let any panic propagate to surface programming errors |
Copilot
AI
Dec 9, 2025
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.
The Cleanup function has a logical issue: it attempts to TryLock to check if the mutex is unlocked, then immediately unlocks it, and then deletes it from the map. However, if the mutex is currently locked by another goroutine (TryLock returns false), the mutex won't be deleted, which is correct. But if TryLock succeeds (returns true), you're essentially stealing the lock from any goroutine that might be about to use it between TryLock and Delete. A safer approach would be to track lock metadata (last used timestamp) separately and clean up locks that haven't been used for a certain period, rather than trying to determine lock state through TryLock.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||
| package api | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||
| "math" | ||||||||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||||||||
| "strconv" | ||||||||||||||||||||||||||||||||||
|
|
@@ -96,7 +97,6 @@ func submitFlagHandler(c *gin.Context) { | |||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| chall, err := database.QueryChallengeEntries("id", strconv.Itoa(int(parsedChallId))) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusInternalServerError, HTTPErrorResp{ | ||||||||||||||||||||||||||||||||||
|
|
@@ -105,6 +105,24 @@ func submitFlagHandler(c *gin.Context) { | |||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if len(chall) == 0 { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusBadRequest, HTTPErrorResp{ | ||||||||||||||||||||||||||||||||||
| Error: "Challenge not found.", | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| lockManager := GetUserLockManager() | ||||||||||||||||||||||||||||||||||
| lockId := fmt.Sprintf("%d_%s", user.ID, challId) | ||||||||||||||||||||||||||||||||||
|
Collaborator
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. Can we have a non-string approach for the key?
Collaborator
Author
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. Yeswe can use hb fir userid and lb for challid in that case |
||||||||||||||||||||||||||||||||||
| if !lockManager.TryLock(lockId) { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusOK, FlagSubmitResp{ | ||||||||||||||||||||||||||||||||||
| Message: "Request already in process", | ||||||||||||||||||||||||||||||||||
| Success: false, | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| defer lockManager.Unlock(lockId) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+115
to
+124
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| challenge := chall[0] | ||||||||||||||||||||||||||||||||||
| if challenge.Status != core.DEPLOY_STATUS["deployed"] { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusOK, FlagSubmitResp{ | ||||||||||||||||||||||||||||||||||
|
|
@@ -116,14 +134,12 @@ func submitFlagHandler(c *gin.Context) { | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if challenge.PreReqs != "" { | ||||||||||||||||||||||||||||||||||
| preReqsStatus, err := database.CheckPreReqsStatus(challenge, user.ID) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusInternalServerError, HTTPErrorResp{ | ||||||||||||||||||||||||||||||||||
| Error: "DATABASE ERROR while processing the request.", | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if !preReqsStatus { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusOK, FlagSubmitResp{ | ||||||||||||||||||||||||||||||||||
| Message: "You have not solved the prerequisites of this challenge.", | ||||||||||||||||||||||||||||||||||
|
|
@@ -133,40 +149,13 @@ func submitFlagHandler(c *gin.Context) { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if challenge.MaxAttemptLimit > 0 { | ||||||||||||||||||||||||||||||||||
| previousTries, err := database.GetUserPreviousTries(user.ID, challenge.ID) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusInternalServerError, HTTPErrorResp{ | ||||||||||||||||||||||||||||||||||
| Error: "DATABASE ERROR while processing the request."}) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if previousTries >= challenge.MaxAttemptLimit { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusOK, FlagSubmitResp{ | ||||||||||||||||||||||||||||||||||
| Message: "You have reached the maximum number of tries for this challenge.", | ||||||||||||||||||||||||||||||||||
| Success: false, | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Increase user tries by 1 | ||||||||||||||||||||||||||||||||||
| err = database.UpdateUserChallengeTries(user.ID, challenge.ID) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusInternalServerError, HTTPErrorResp{ | ||||||||||||||||||||||||||||||||||
| Error: "DATABASE ERROR while processing the request.", | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| solved, err := database.CheckPreviousSubmissions(user.ID, challenge.ID) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusInternalServerError, HTTPErrorResp{ | ||||||||||||||||||||||||||||||||||
| Error: "DATABASE ERROR while processing the request.", | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if solved { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusOK, FlagSubmitResp{ | ||||||||||||||||||||||||||||||||||
| Message: "Challenge has already been solved.", | ||||||||||||||||||||||||||||||||||
|
|
@@ -175,6 +164,29 @@ func submitFlagHandler(c *gin.Context) { | |||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| var previousTries int | ||||||||||||||||||||||||||||||||||
| if challenge.MaxAttemptLimit > 0 { | ||||||||||||||||||||||||||||||||||
| previousTries, err = database.GetUserPreviousTries(user.ID, challenge.ID) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusInternalServerError, HTTPErrorResp{ | ||||||||||||||||||||||||||||||||||
| Error: "DATABASE ERROR while processing the request."}) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if previousTries >= challenge.MaxAttemptLimit { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusOK, FlagSubmitResp{ | ||||||||||||||||||||||||||||||||||
| Message: "You have reached the maximum number of tries for this challenge.", | ||||||||||||||||||||||||||||||||||
| Success: false, | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| err = database.UpdateUserChallengeTries(user.ID, challenge.ID) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| c.JSON(http.StatusInternalServerError, HTTPErrorResp{ | ||||||||||||||||||||||||||||||||||
| Error: "DATABASE ERROR while processing the request.", | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+182
to
+189
|
||||||||||||||||||||||||||||||||||
| err = database.UpdateUserChallengeTries(user.ID, challenge.ID) | |
| if err != nil { | |
| c.JSON(http.StatusInternalServerError, HTTPErrorResp{ | |
| Error: "DATABASE ERROR while processing the request.", | |
| }) | |
| return | |
| } | |
| } | |
| } | |
| err = database.UpdateUserChallengeTries(user.ID, challenge.ID) | |
| if err != nil { | |
| c.JSON(http.StatusInternalServerError, HTTPErrorResp{ | |
| Error: "DATABASE ERROR while processing the request.", | |
| }) | |
| return | |
| } |
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.
Parameter name is misleading and inconsistent. The function receives
userIDbut should belockIdto match the usage in the calling code (line 124 of submit.go wherelockIdis passed) and to be consistent with theTryLockfunction which useslockId.