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  }