forked from edulinq/autograder-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added centralized contextual locking functionalities (edulinq#75).
- Loading branch information
Showing
4 changed files
with
673 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
package common | ||
|
||
import ( | ||
"fmt" | ||
"sync" | ||
"time" | ||
|
||
"github.com/edulinq/autograder/config" | ||
"github.com/edulinq/autograder/log" | ||
) | ||
|
||
type lockData struct { | ||
timestamp time.Time; | ||
mutex sync.RWMutex; | ||
lockCount int; | ||
} | ||
|
||
const TICKER_DURATION_HOUR = 1.0; | ||
|
||
var ( | ||
lockManagerMutex sync.Mutex; | ||
lockMap sync.Map; | ||
ticker *time.Ticker; | ||
) | ||
|
||
func init() { | ||
ticker = time.NewTicker(TICKER_DURATION_HOUR * time.Hour); | ||
go removeStaleLocks(); | ||
} | ||
|
||
func Lock(key string) { | ||
lock(key, false); | ||
} | ||
|
||
func Unlock(key string) error { | ||
return unlock(key, false); | ||
} | ||
|
||
func ReadLock(key string) { | ||
lock(key, true); | ||
} | ||
|
||
func ReadUnlock(key string) error { | ||
return unlock(key, true); | ||
} | ||
|
||
func lock(key string, read bool) { | ||
lockManagerMutex.Lock(); | ||
|
||
val, _ := lockMap.LoadOrStore(key, &lockData{}); | ||
lock := val.(*lockData); | ||
|
||
// Unlock the lockManagerMutex before acquiring the lock to avoid a deadlock. | ||
lockManagerMutex.Unlock(); | ||
|
||
if (read) { | ||
lock.mutex.RLock(); | ||
} else { | ||
lock.mutex.Lock(); | ||
} | ||
|
||
lock.lockCount++; | ||
|
||
lock.timestamp = time.Now(); | ||
} | ||
|
||
func unlock(key string, read bool) error { | ||
lockManagerMutex.Lock(); | ||
defer lockManagerMutex.Unlock(); | ||
|
||
val, exists := lockMap.Load(key); | ||
if (!exists) { | ||
log.Error("Key does not exist.", log.NewAttr("key", key)); | ||
return fmt.Errorf("Lock key not found: '%s'.", key); | ||
} | ||
|
||
lock := val.(*lockData); | ||
if (!read && lock.lockCount == 0) { | ||
log.Error("Tried to unlock a lock that is already unlocked: %s\n", key); | ||
return fmt.Errorf("Tried to unlock a lock that is already unlocked with key '%s'", key); | ||
} | ||
|
||
if (read) { | ||
lock.mutex.RUnlock(); | ||
} else { | ||
lock.mutex.Unlock(); | ||
} | ||
|
||
lock.lockCount--; | ||
|
||
lock.timestamp = time.Now(); | ||
|
||
return nil; | ||
} | ||
|
||
func removeStaleLocks() { | ||
for range ticker.C { | ||
RemoveStaleLocksOnce(); | ||
} | ||
} | ||
|
||
func RemoveStaleLocksOnce() { | ||
staleDuration := time.Duration(time.Duration(config.STALELOCK_DURATION_SECS.Get()) * time.Second); | ||
|
||
lockMap.Range(func(key, val any) bool { | ||
lock := val.(*lockData); | ||
|
||
// First check: If the lock isn't stale or is locked, return early. | ||
if (time.Since(lock.timestamp) < staleDuration || lock.lockCount > 0) { | ||
return true; | ||
} | ||
|
||
// Lock the lock manager in case another thread is trying to lock/unlock. | ||
lockManagerMutex.Lock(); | ||
defer lockManagerMutex.Unlock(); | ||
|
||
// Second check: If the lock is stale and and is able to be locked, delete it. | ||
if (time.Since(lock.timestamp) > staleDuration && lock.mutex.TryLock()) { | ||
lockMap.Delete(key); | ||
} | ||
|
||
return true; | ||
}) | ||
} |
Oops, something went wrong.