You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
3.4 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. package lock_manager
  2. import (
  3. "fmt"
  4. "github.com/google/uuid"
  5. "github.com/puzpuzpuz/xsync/v2"
  6. "time"
  7. )
  8. var LockErrorNonEmptyTokenOnNewLock = fmt.Errorf("lock: non-empty token on a new lock")
  9. var LockErrorNonEmptyTokenOnExpiredLock = fmt.Errorf("lock: non-empty token on an expired lock")
  10. var LockErrorTokenMismatch = fmt.Errorf("lock: token mismatch")
  11. var UnlockErrorTokenMismatch = fmt.Errorf("unlock: token mismatch")
  12. // LockManager lock manager
  13. type LockManager struct {
  14. locks *xsync.MapOf[string, *Lock]
  15. }
  16. type Lock struct {
  17. Token string
  18. ExpiredAtNs int64
  19. Key string // only used for moving locks
  20. }
  21. func NewLockManager() *LockManager {
  22. t := &LockManager{
  23. locks: xsync.NewMapOf[*Lock](),
  24. }
  25. go t.CleanUp()
  26. return t
  27. }
  28. func (lm *LockManager) Lock(path string, expiredAtNs int64, token string) (renewToken string, err error) {
  29. lm.locks.Compute(path, func(oldValue *Lock, loaded bool) (newValue *Lock, delete bool) {
  30. if oldValue != nil {
  31. if oldValue.ExpiredAtNs > 0 && oldValue.ExpiredAtNs < time.Now().UnixNano() {
  32. // lock is expired, set to a new lock
  33. if token != "" {
  34. err = LockErrorNonEmptyTokenOnExpiredLock
  35. return nil, false
  36. } else {
  37. // new lock
  38. renewToken = uuid.New().String()
  39. return &Lock{Token: renewToken, ExpiredAtNs: expiredAtNs}, false
  40. }
  41. }
  42. // not expired
  43. if oldValue.Token == token {
  44. // token matches, renew the lock
  45. renewToken = uuid.New().String()
  46. return &Lock{Token: renewToken, ExpiredAtNs: expiredAtNs}, false
  47. } else {
  48. err = LockErrorTokenMismatch
  49. return oldValue, false
  50. }
  51. } else {
  52. if token == "" {
  53. // new lock
  54. renewToken = uuid.New().String()
  55. return &Lock{Token: renewToken, ExpiredAtNs: expiredAtNs}, false
  56. } else {
  57. err = LockErrorNonEmptyTokenOnNewLock
  58. return nil, false
  59. }
  60. }
  61. })
  62. return
  63. }
  64. func (lm *LockManager) Unlock(path string, token string) (isUnlocked bool, err error) {
  65. lm.locks.Compute(path, func(oldValue *Lock, loaded bool) (newValue *Lock, delete bool) {
  66. if oldValue != nil {
  67. now := time.Now()
  68. if oldValue.ExpiredAtNs > 0 && oldValue.ExpiredAtNs < now.UnixNano() {
  69. // lock is expired, delete it
  70. isUnlocked = true
  71. return nil, true
  72. }
  73. if oldValue.Token == token {
  74. if oldValue.ExpiredAtNs <= now.UnixNano() {
  75. isUnlocked = true
  76. return nil, true
  77. }
  78. return oldValue, false
  79. } else {
  80. isUnlocked = false
  81. err = UnlockErrorTokenMismatch
  82. return oldValue, false
  83. }
  84. } else {
  85. isUnlocked = true
  86. return nil, true
  87. }
  88. })
  89. return
  90. }
  91. func (lm *LockManager) CleanUp() {
  92. for {
  93. time.Sleep(1 * time.Minute)
  94. now := time.Now().UnixNano()
  95. lm.locks.Range(func(key string, value *Lock) bool {
  96. if value == nil {
  97. return true
  98. }
  99. if now > value.ExpiredAtNs {
  100. lm.locks.Delete(key)
  101. return true
  102. }
  103. return true
  104. })
  105. }
  106. }
  107. // SelectLocks takes out locks by key
  108. // if keyFn return true, the lock will be taken out
  109. func (lm *LockManager) SelectLocks(selectFn func(key string) bool) (locks []*Lock) {
  110. now := time.Now().UnixNano()
  111. lm.locks.Range(func(key string, lock *Lock) bool {
  112. if now > lock.ExpiredAtNs {
  113. lm.locks.Delete(key)
  114. return true
  115. }
  116. if selectFn(key) {
  117. lm.locks.Delete(key)
  118. lock.Key = key
  119. locks = append(locks, lock)
  120. }
  121. return true
  122. })
  123. return
  124. }
  125. // InsertLock inserts a lock unconditionally
  126. func (lm *LockManager) InsertLock(path string, expiredAtNs int64, token string) {
  127. lm.locks.Store(path, &Lock{Token: token, ExpiredAtNs: expiredAtNs})
  128. }