github.com/cilium/cilium@v1.16.2/pkg/kvstore/lock.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package kvstore 5 6 import ( 7 "context" 8 "fmt" 9 10 "github.com/davecgh/go-spew/spew" 11 "github.com/google/uuid" 12 "github.com/sirupsen/logrus" 13 14 "github.com/cilium/cilium/pkg/debug" 15 "github.com/cilium/cilium/pkg/defaults" 16 "github.com/cilium/cilium/pkg/inctimer" 17 "github.com/cilium/cilium/pkg/lock" 18 "github.com/cilium/cilium/pkg/time" 19 ) 20 21 var ( 22 kvstoreLocks = pathLocks{lockPaths: map[string]lockOwner{}} 23 24 // staleLockTimeout is the timeout after which waiting for a believed 25 // other local lock user for the same key is given up on and etcd is 26 // asked directly. It is still highly unlikely that concurrent access 27 // occurs as only one consumer will manage to acquire the newly 28 // released lock. The only possibility of concurrent access is if a 29 // consumer is *still* holding the lock but this is highly unlikely 30 // given the duration of this timeout. 31 staleLockTimeout = defaults.KVStoreStaleLockTimeout 32 ) 33 34 type KVLocker interface { 35 Unlock(ctx context.Context) error 36 // Comparator returns an object that should be used by the KVStore to make 37 // sure if the lock is still valid for its client or nil if no such 38 // verification exists. 39 Comparator() interface{} 40 } 41 42 // getLockPath returns the lock path representation of the given path. 43 func getLockPath(path string) string { 44 return path + ".lock" 45 } 46 47 type lockOwner struct { 48 created time.Time 49 id uuid.UUID 50 } 51 52 type pathLocks struct { 53 mutex lock.RWMutex 54 lockPaths map[string]lockOwner 55 } 56 57 func init() { 58 debug.RegisterStatusObject("kvstore-locks", &kvstoreLocks) 59 } 60 61 // DebugStatus implements debug.StatusObject to provide debug status collection 62 // ability 63 func (pl *pathLocks) DebugStatus() string { 64 pl.mutex.RLock() 65 str := spew.Sdump(pl.lockPaths) 66 pl.mutex.RUnlock() 67 return str 68 } 69 70 func (pl *pathLocks) runGC() { 71 pl.mutex.Lock() 72 for path, owner := range pl.lockPaths { 73 if time.Since(owner.created) > staleLockTimeout { 74 log.WithField("path", path).Error("Forcefully unlocking local kvstore lock") 75 delete(pl.lockPaths, path) 76 } 77 } 78 pl.mutex.Unlock() 79 } 80 81 func (pl *pathLocks) lock(ctx context.Context, path string) (id uuid.UUID, err error) { 82 lockTimer, lockTimerDone := inctimer.New() 83 defer lockTimerDone() 84 for { 85 pl.mutex.Lock() 86 if _, ok := pl.lockPaths[path]; !ok { 87 id = uuid.New() 88 pl.lockPaths[path] = lockOwner{ 89 created: time.Now(), 90 id: id, 91 } 92 pl.mutex.Unlock() 93 return 94 } 95 pl.mutex.Unlock() 96 97 select { 98 case <-lockTimer.After(time.Duration(10) * time.Millisecond): 99 case <-ctx.Done(): 100 err = fmt.Errorf("lock was cancelled: %w", ctx.Err()) 101 return 102 } 103 } 104 } 105 106 func (pl *pathLocks) unlock(path string, id uuid.UUID) { 107 pl.mutex.Lock() 108 if owner, ok := pl.lockPaths[path]; ok && owner.id == id { 109 delete(pl.lockPaths, path) 110 } 111 pl.mutex.Unlock() 112 } 113 114 // Lock is a lock return by LockPath 115 type Lock struct { 116 path string 117 id uuid.UUID 118 kvLock KVLocker 119 } 120 121 // LockPath locks the specified path. The key for the lock is not the path 122 // provided itself but the path with a suffix of ".lock" appended. The lock 123 // returned also contains a patch specific local Mutex which will be held. 124 // 125 // It is required to call Unlock() on the returned Lock to unlock 126 func LockPath(ctx context.Context, backend BackendOperations, path string) (l *Lock, err error) { 127 id, err := kvstoreLocks.lock(ctx, path) 128 if err != nil { 129 return nil, err 130 } 131 132 lock, err := backend.LockPath(ctx, path) 133 if err != nil { 134 kvstoreLocks.unlock(path, id) 135 Trace("Failed to lock", err, logrus.Fields{fieldKey: path}) 136 err = fmt.Errorf("error while locking path %s: %w", path, err) 137 return nil, err 138 } 139 140 Trace("Successful lock", err, logrus.Fields{fieldKey: path}) 141 return &Lock{kvLock: lock, path: path, id: id}, err 142 } 143 144 // RunLockGC inspects all local kvstore locks to determine whether they have 145 // been held longer than the stale lock timeout, and if so, unlocks them 146 // forceably. 147 func RunLockGC() { 148 kvstoreLocks.runGC() 149 } 150 151 // Unlock unlocks a lock 152 func (l *Lock) Unlock(ctx context.Context) error { 153 if l == nil { 154 return nil 155 } 156 157 // Unlock kvstore mutex first 158 err := l.kvLock.Unlock(ctx) 159 if err != nil { 160 log.WithError(err).WithField("path", l.path).Error("Unable to unlock kvstore lock") 161 } 162 163 // unlock local lock even if kvstore cannot be unlocked 164 kvstoreLocks.unlock(l.path, l.id) 165 Trace("Unlocked", nil, logrus.Fields{fieldKey: l.path}) 166 167 return err 168 } 169 170 func (l *Lock) Comparator() interface{} { 171 return l.kvLock.Comparator() 172 }