github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/core/namelocker.go (about) 1 // Package core provides core metadata and in-cluster API 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package core 6 7 import ( 8 "sync" 9 "time" 10 11 "github.com/NVIDIA/aistore/cmn/cos" 12 "github.com/NVIDIA/aistore/cmn/debug" 13 "github.com/OneOfOne/xxhash" 14 ) 15 16 const nlpTryDefault = time.Second // nlp.TryLock default duration 17 18 // nameLocker is a 2-level structure utilized to lock objects of *any* kind, 19 // as long as the object in question has a (string) name and an (int) digest. 20 // Digest (plus a certain fixed mask) is used to select one of the specific `nlc` maps; 21 // in most cases, digest will be some sort a hash of the name itself and does not 22 // need to be cryptographic. 23 // The lock can be exclusive (write) or shared (read). 24 25 type ( 26 nameLocker []nlc 27 nlc struct { 28 m map[string]*lockInfo 29 mu sync.Mutex 30 } 31 lockInfo struct { 32 wcond *sync.Cond // to synchronize "waiting room" upgrade logic 33 rc int32 // read-lock refcount 34 waiting int32 // waiting room count 35 exclusive bool // write-lock 36 upgraded bool // indication for the waiters that upgrade's done 37 } 38 ) 39 40 // pair 41 type ( 42 NLP interface { 43 Lock() 44 TryLock(timeout time.Duration) bool 45 TryRLock(timeout time.Duration) bool 46 Unlock() 47 } 48 49 noCopy struct{} 50 nlp struct { 51 _ noCopy 52 nlc *nlc 53 uname string 54 exclusive bool 55 } 56 ) 57 58 const ( 59 initPollInterval = 10 * time.Microsecond 60 maxPollInterval = 100 * time.Millisecond 61 initCapacity = 32 62 ) 63 64 //////////////// 65 // namelocker // 66 //////////////// 67 68 func newNameLocker() (nl nameLocker) { 69 nl = make(nameLocker, cos.MultiSyncMapCount) 70 for idx := range len(nl) { 71 nl[idx].init() 72 } 73 return 74 } 75 76 ////////////// 77 // lockInfo // 78 ////////////// 79 80 func (li *lockInfo) notify() { 81 if li.wcond == nil || li.waiting == 0 { 82 return 83 } 84 debug.Assert(li.rc >= li.waiting) 85 if li.upgraded { 86 // has been upgraded - wake up all waiters 87 li.wcond.Broadcast() 88 } else { 89 // wake up only the owner 90 li.wcond.Signal() 91 } 92 } 93 94 func (li *lockInfo) decWaiting() { 95 li.waiting-- 96 if li.waiting == 0 { 97 li.wcond = nil 98 } 99 } 100 101 ///////// 102 // nlc // 103 ///////// 104 105 func (nlc *nlc) init() { 106 nlc.m = make(map[string]*lockInfo, initCapacity) 107 } 108 109 func (nlc *nlc) IsLocked(uname string) (rc int, exclusive bool) { 110 nlc.mu.Lock() 111 defer nlc.mu.Unlock() 112 113 lockInfo, found := nlc.m[uname] 114 if !found { 115 return 116 } 117 return int(lockInfo.rc), lockInfo.exclusive 118 } 119 120 func (nlc *nlc) TryLock(uname string, exclusive bool) (locked bool) { 121 nlc.mu.Lock() 122 locked = nlc.try(uname, exclusive) 123 nlc.mu.Unlock() 124 return 125 } 126 127 func (nlc *nlc) try(uname string, exclusive bool) bool { 128 li, found := nlc.m[uname] 129 // wlock 130 if exclusive { 131 if found { 132 return false 133 } 134 nlc.m[uname] = &lockInfo{exclusive: true} 135 return true 136 } 137 // rlock 138 if !found { 139 nlc.m[uname] = &lockInfo{rc: 1} 140 return true 141 } 142 if li.exclusive { 143 return false 144 } 145 // can't rlock if there's someone trying to upgrade 146 if li.waiting > 0 { 147 return false 148 } 149 li.rc++ 150 return true 151 } 152 153 // NOTE: Lock() stays in the loop for as long as needed to acquire the lock. 154 // The implementation is intentionally simple as we currently don't need 155 // cancellation (e.g., via context.Context), timeout, etc. semantics 156 func (nlc *nlc) Lock(uname string, exclusive bool) { 157 if nlc.TryLock(uname, exclusive) { 158 return 159 } 160 sleep := initPollInterval 161 for { 162 time.Sleep(sleep) 163 if nlc.TryLock(uname, exclusive) { 164 return 165 } 166 sleep = min(sleep*2, maxPollInterval) 167 } 168 } 169 170 // upgrade rlock -> wlock 171 // e.g. usage: simultaneous cold GET 172 // returns true if exclusively locked by _another_ thread 173 func (nlc *nlc) UpgradeLock(uname string) bool { 174 nlc.mu.Lock() 175 li, found := nlc.m[uname] 176 debug.Assert(found && !li.exclusive && li.rc > 0) 177 if li.rc == 1 { 178 li.rc = 0 179 li.exclusive = true 180 nlc.mu.Unlock() 181 return false 182 } 183 if li.wcond == nil { 184 li.wcond = sync.NewCond(&nlc.mu) 185 } 186 li.waiting++ 187 // Wait here until all readers get in line 188 for li.rc != li.waiting { 189 li.wcond.Wait() 190 191 // Has been upgraded by smbd. else 192 if li.upgraded { 193 li.decWaiting() 194 nlc.mu.Unlock() 195 return true 196 } 197 } 198 // Upgrading 199 li.upgraded = true 200 li.rc-- 201 li.decWaiting() 202 li.exclusive = true 203 nlc.mu.Unlock() 204 return false 205 } 206 207 func (nlc *nlc) DowngradeLock(uname string) { 208 nlc.mu.Lock() 209 li, found := nlc.m[uname] 210 debug.Assert(found && li.exclusive) 211 li.rc++ 212 li.exclusive = false 213 li.notify() 214 nlc.mu.Unlock() 215 } 216 217 func (nlc *nlc) Unlock(uname string, exclusive bool) { 218 nlc.mu.Lock() 219 li, found := nlc.m[uname] 220 debug.Assert(found) 221 if exclusive { 222 debug.Assert(li.exclusive) 223 if li.waiting > 0 { 224 li.exclusive = false 225 li.notify() 226 } else { 227 delete(nlc.m, uname) 228 } 229 nlc.mu.Unlock() 230 return 231 } 232 li.rc-- 233 if li.rc == 0 { 234 delete(nlc.m, uname) 235 } 236 li.notify() 237 nlc.mu.Unlock() 238 } 239 240 ///////// 241 // nlp // 242 ///////// 243 244 // interface guard 245 var _ NLP = (*nlp)(nil) 246 247 // NOTE: currently, is only used to lock buckets 248 func NewNLP(name string) NLP { 249 var ( 250 nlp = &nlp{uname: name} 251 hash = xxhash.Checksum64S(cos.UnsafeB(name), cos.MLCG32) 252 idx = int(hash & cos.MultiSyncMapMask) 253 ) 254 nlp.nlc = &bckLocker[idx] // NOTE: bckLocker 255 return nlp 256 } 257 258 func (nlp *nlp) Lock() { 259 nlp.nlc.Lock(nlp.uname, true) 260 nlp.exclusive = true 261 } 262 263 func (nlp *nlp) TryLock(timeout time.Duration) (ok bool) { 264 if timeout == 0 { 265 timeout = nlpTryDefault 266 } 267 ok = nlp.withRetry(timeout, true) 268 nlp.exclusive = ok 269 return 270 } 271 272 // NOTE: ensure single-time usage (no ref counting!) 273 func (nlp *nlp) TryRLock(timeout time.Duration) (ok bool) { 274 if timeout == 0 { 275 timeout = nlpTryDefault 276 } 277 ok = nlp.withRetry(timeout, false) 278 debug.Assert(!nlp.exclusive) 279 return 280 } 281 282 func (nlp *nlp) Unlock() { 283 nlp.nlc.Unlock(nlp.uname, nlp.exclusive) 284 } 285 286 func (nlp *nlp) withRetry(d time.Duration, exclusive bool) bool { 287 if nlp.nlc.TryLock(nlp.uname, exclusive) { 288 return true 289 } 290 i := d / 10 291 for j := i; j < d; j += i { 292 time.Sleep(i) 293 if nlp.nlc.TryLock(nlp.uname, exclusive) { 294 return true 295 } 296 } 297 return false 298 } 299 300 func (*noCopy) Lock() {} 301 func (*noCopy) Unlock() {}