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.

132 lines
3.2 KiB

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