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.

128 lines
3.1 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. return &Lock{Token: token, ExpiredAtNs: expiredAtNs}, false
  42. } else {
  43. err = fmt.Errorf("lock: token mismatch")
  44. return oldValue, false
  45. }
  46. } else {
  47. if token == "" {
  48. // new lock
  49. renewToken = uuid.New().String()
  50. return &Lock{Token: renewToken, ExpiredAtNs: expiredAtNs}, false
  51. } else {
  52. err = fmt.Errorf("lock: non-empty token on a new lock")
  53. return nil, false
  54. }
  55. }
  56. })
  57. return
  58. }
  59. func (lm *LockManager) Unlock(path string, token string) (isUnlocked bool, err error) {
  60. lm.locks.Compute(path, func(oldValue *Lock, loaded bool) (newValue *Lock, delete bool) {
  61. if oldValue != nil {
  62. now := time.Now()
  63. if oldValue.ExpiredAtNs > 0 && oldValue.ExpiredAtNs < now.UnixNano() {
  64. // lock is expired, delete it
  65. isUnlocked = true
  66. return nil, true
  67. }
  68. if oldValue.Token == token {
  69. if oldValue.ExpiredAtNs <= now.UnixNano() {
  70. isUnlocked = true
  71. return nil, true
  72. }
  73. return oldValue, false
  74. } else {
  75. isUnlocked = false
  76. err = fmt.Errorf("unlock: token mismatch")
  77. return oldValue, false
  78. }
  79. } else {
  80. isUnlocked = true
  81. return nil, true
  82. }
  83. })
  84. return
  85. }
  86. func (lm *LockManager) CleanUp() {
  87. for {
  88. time.Sleep(1 * time.Minute)
  89. now := time.Now().UnixNano()
  90. lm.locks.Range(func(key string, value *Lock) bool {
  91. if now > value.ExpiredAtNs {
  92. lm.locks.Delete(key)
  93. return true
  94. }
  95. return true
  96. })
  97. }
  98. }
  99. // SelectLocks takes out locks by key
  100. // if keyFn return true, the lock will be taken out
  101. func (lm *LockManager) SelectLocks(selectFn func(key string) bool) (locks []*Lock) {
  102. now := time.Now().UnixNano()
  103. lm.locks.Range(func(key string, lock *Lock) bool {
  104. if now > lock.ExpiredAtNs {
  105. lm.locks.Delete(key)
  106. return true
  107. }
  108. if selectFn(key) {
  109. lm.locks.Delete(key)
  110. lock.Key = key
  111. locks = append(locks, lock)
  112. }
  113. return true
  114. })
  115. return
  116. }
  117. // InsertLock inserts a lock unconditionally
  118. func (lm *LockManager) InsertLock(path string, expiredAtNs int64, token string) {
  119. lm.locks.Store(path, &Lock{Token: token, ExpiredAtNs: expiredAtNs})
  120. }