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() {}