github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/local-locker.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  //go:generate msgp -file=$GOFILE -unexported
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"strconv"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/minio/minio/internal/dsync"
    30  )
    31  
    32  // lockRequesterInfo stores various info from the client for each lock that is requested.
    33  type lockRequesterInfo struct {
    34  	Name            string    // name of the resource lock was requested for
    35  	Writer          bool      // Bool whether write or read lock.
    36  	UID             string    // UID to uniquely identify request of client.
    37  	Timestamp       time.Time // Timestamp set at the time of initialization.
    38  	TimeLastRefresh time.Time // Timestamp for last lock refresh.
    39  	Source          string    // Contains line, function and filename requesting the lock.
    40  	Group           bool      // indicates if it was a group lock.
    41  	Owner           string    // Owner represents the UUID of the owner who originally requested the lock.
    42  	Quorum          int       // Quorum represents the quorum required for this lock to be active.
    43  	idx             int       `msg:"-"` // index of the lock in the lockMap.
    44  }
    45  
    46  // isWriteLock returns whether the lock is a write or read lock.
    47  func isWriteLock(lri []lockRequesterInfo) bool {
    48  	return len(lri) == 1 && lri[0].Writer
    49  }
    50  
    51  // localLocker implements Dsync.NetLocker
    52  //
    53  //msgp:ignore localLocker
    54  type localLocker struct {
    55  	mutex   sync.Mutex
    56  	lockMap map[string][]lockRequesterInfo
    57  	lockUID map[string]string // UUID -> resource map.
    58  }
    59  
    60  func (l *localLocker) String() string {
    61  	return globalEndpoints.Localhost()
    62  }
    63  
    64  func (l *localLocker) canTakeLock(resources ...string) bool {
    65  	for _, resource := range resources {
    66  		_, lockTaken := l.lockMap[resource]
    67  		if lockTaken {
    68  			return false
    69  		}
    70  	}
    71  	return true
    72  }
    73  
    74  func (l *localLocker) Lock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
    75  	if len(args.Resources) > maxDeleteList {
    76  		return false, fmt.Errorf("internal error: localLocker.Lock called with more than %d resources", maxDeleteList)
    77  	}
    78  
    79  	l.mutex.Lock()
    80  	defer l.mutex.Unlock()
    81  
    82  	if !l.canTakeLock(args.Resources...) {
    83  		// Not all locks can be taken on resources,
    84  		// reject it completely.
    85  		return false, nil
    86  	}
    87  
    88  	// No locks held on the all resources, so claim write
    89  	// lock on all resources at once.
    90  	for i, resource := range args.Resources {
    91  		l.lockMap[resource] = []lockRequesterInfo{
    92  			{
    93  				Name:            resource,
    94  				Writer:          true,
    95  				Source:          args.Source,
    96  				Owner:           args.Owner,
    97  				UID:             args.UID,
    98  				Timestamp:       UTCNow(),
    99  				TimeLastRefresh: UTCNow(),
   100  				Group:           len(args.Resources) > 1,
   101  				Quorum:          args.Quorum,
   102  				idx:             i,
   103  			},
   104  		}
   105  		l.lockUID[formatUUID(args.UID, i)] = resource
   106  	}
   107  	return true, nil
   108  }
   109  
   110  func formatUUID(s string, idx int) string {
   111  	return s + strconv.Itoa(idx)
   112  }
   113  
   114  func (l *localLocker) Unlock(_ context.Context, args dsync.LockArgs) (reply bool, err error) {
   115  	if len(args.Resources) > maxDeleteList {
   116  		return false, fmt.Errorf("internal error: localLocker.Unlock called with more than %d resources", maxDeleteList)
   117  	}
   118  
   119  	l.mutex.Lock()
   120  	defer l.mutex.Unlock()
   121  	err = nil
   122  
   123  	for _, resource := range args.Resources {
   124  		lri, ok := l.lockMap[resource]
   125  		if ok && !isWriteLock(lri) {
   126  			// Unless it is a write lock reject it.
   127  			err = fmt.Errorf("unlock attempted on a read locked entity: %s", resource)
   128  			continue
   129  		}
   130  		if ok {
   131  			reply = l.removeEntry(resource, args, &lri) || reply
   132  		}
   133  	}
   134  	return
   135  }
   136  
   137  // removeEntry based on the uid of the lock message, removes a single entry from the
   138  // lockRequesterInfo array or the whole array from the map (in case of a write lock
   139  // or last read lock)
   140  // UID and optionally owner must match for entries to be deleted.
   141  func (l *localLocker) removeEntry(name string, args dsync.LockArgs, lri *[]lockRequesterInfo) bool {
   142  	// Find correct entry to remove based on uid.
   143  	for index, entry := range *lri {
   144  		if entry.UID == args.UID && (args.Owner == "" || entry.Owner == args.Owner) {
   145  			if len(*lri) == 1 {
   146  				// Remove the write lock.
   147  				delete(l.lockMap, name)
   148  			} else {
   149  				// Remove the appropriate read lock.
   150  				*lri = append((*lri)[:index], (*lri)[index+1:]...)
   151  				l.lockMap[name] = *lri
   152  			}
   153  			delete(l.lockUID, formatUUID(args.UID, entry.idx))
   154  			return true
   155  		}
   156  	}
   157  
   158  	// None found return false, perhaps entry removed in previous run.
   159  	return false
   160  }
   161  
   162  func (l *localLocker) RLock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
   163  	if len(args.Resources) != 1 {
   164  		return false, fmt.Errorf("internal error: localLocker.RLock called with more than one resource")
   165  	}
   166  
   167  	l.mutex.Lock()
   168  	defer l.mutex.Unlock()
   169  	resource := args.Resources[0]
   170  	lrInfo := lockRequesterInfo{
   171  		Name:            resource,
   172  		Writer:          false,
   173  		Source:          args.Source,
   174  		Owner:           args.Owner,
   175  		UID:             args.UID,
   176  		Timestamp:       UTCNow(),
   177  		TimeLastRefresh: UTCNow(),
   178  		Quorum:          args.Quorum,
   179  	}
   180  	if lri, ok := l.lockMap[resource]; ok {
   181  		if reply = !isWriteLock(lri); reply {
   182  			// Unless there is a write lock
   183  			l.lockMap[resource] = append(l.lockMap[resource], lrInfo)
   184  			l.lockUID[formatUUID(args.UID, 0)] = resource
   185  		}
   186  	} else {
   187  		// No locks held on the given name, so claim (first) read lock
   188  		l.lockMap[resource] = []lockRequesterInfo{lrInfo}
   189  		l.lockUID[formatUUID(args.UID, 0)] = resource
   190  		reply = true
   191  	}
   192  	return reply, nil
   193  }
   194  
   195  func (l *localLocker) RUnlock(_ context.Context, args dsync.LockArgs) (reply bool, err error) {
   196  	if len(args.Resources) > 1 {
   197  		return false, fmt.Errorf("internal error: localLocker.RUnlock called with more than one resource")
   198  	}
   199  
   200  	l.mutex.Lock()
   201  	defer l.mutex.Unlock()
   202  	var lri []lockRequesterInfo
   203  
   204  	resource := args.Resources[0]
   205  	if lri, reply = l.lockMap[resource]; !reply {
   206  		// No lock is held on the given name
   207  		return true, nil
   208  	}
   209  	if isWriteLock(lri) {
   210  		// A write-lock is held, cannot release a read lock
   211  		return false, fmt.Errorf("RUnlock attempted on a write locked entity: %s", resource)
   212  	}
   213  	l.removeEntry(resource, args, &lri)
   214  	return reply, nil
   215  }
   216  
   217  type lockStats struct {
   218  	Total  int
   219  	Writes int
   220  	Reads  int
   221  }
   222  
   223  func (l *localLocker) stats() lockStats {
   224  	l.mutex.Lock()
   225  	defer l.mutex.Unlock()
   226  
   227  	st := lockStats{Total: len(l.lockMap)}
   228  	for _, v := range l.lockMap {
   229  		if len(v) == 0 {
   230  			continue
   231  		}
   232  		entry := v[0]
   233  		if entry.Writer {
   234  			st.Writes++
   235  		} else {
   236  			st.Reads += len(v)
   237  		}
   238  	}
   239  	return st
   240  }
   241  
   242  type localLockMap map[string][]lockRequesterInfo
   243  
   244  func (l *localLocker) DupLockMap() localLockMap {
   245  	l.mutex.Lock()
   246  	defer l.mutex.Unlock()
   247  
   248  	lockCopy := make(map[string][]lockRequesterInfo, len(l.lockMap))
   249  	for k, v := range l.lockMap {
   250  		if len(v) == 0 {
   251  			delete(l.lockMap, k)
   252  			continue
   253  		}
   254  		lockCopy[k] = append(make([]lockRequesterInfo, 0, len(v)), v...)
   255  	}
   256  	return lockCopy
   257  }
   258  
   259  func (l *localLocker) Close() error {
   260  	return nil
   261  }
   262  
   263  // IsOnline - local locker is always online.
   264  func (l *localLocker) IsOnline() bool {
   265  	return true
   266  }
   267  
   268  // IsLocal - local locker returns true.
   269  func (l *localLocker) IsLocal() bool {
   270  	return true
   271  }
   272  
   273  func (l *localLocker) ForceUnlock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
   274  	select {
   275  	case <-ctx.Done():
   276  		return false, ctx.Err()
   277  	default:
   278  		l.mutex.Lock()
   279  		defer l.mutex.Unlock()
   280  		if len(args.UID) == 0 {
   281  			for _, resource := range args.Resources {
   282  				lris, ok := l.lockMap[resource]
   283  				if !ok {
   284  					continue
   285  				}
   286  				// Collect uids, so we don't mutate while we delete
   287  				uids := make([]string, 0, len(lris))
   288  				for _, lri := range lris {
   289  					uids = append(uids, lri.UID)
   290  				}
   291  
   292  				// Delete collected uids:
   293  				for _, uid := range uids {
   294  					lris, ok := l.lockMap[resource]
   295  					if !ok {
   296  						// Just to be safe, delete uuids.
   297  						for idx := 0; idx < maxDeleteList; idx++ {
   298  							mapID := formatUUID(uid, idx)
   299  							if _, ok := l.lockUID[mapID]; !ok {
   300  								break
   301  							}
   302  							delete(l.lockUID, mapID)
   303  						}
   304  						continue
   305  					}
   306  					l.removeEntry(resource, dsync.LockArgs{UID: uid}, &lris)
   307  				}
   308  			}
   309  			return true, nil
   310  		}
   311  
   312  		idx := 0
   313  		for {
   314  			mapID := formatUUID(args.UID, idx)
   315  			resource, ok := l.lockUID[mapID]
   316  			if !ok {
   317  				return idx > 0, nil
   318  			}
   319  			lris, ok := l.lockMap[resource]
   320  			if !ok {
   321  				// Unexpected  inconsistency, delete.
   322  				delete(l.lockUID, mapID)
   323  				idx++
   324  				continue
   325  			}
   326  			reply = true
   327  			l.removeEntry(resource, dsync.LockArgs{UID: args.UID}, &lris)
   328  			idx++
   329  		}
   330  	}
   331  }
   332  
   333  func (l *localLocker) Refresh(ctx context.Context, args dsync.LockArgs) (refreshed bool, err error) {
   334  	select {
   335  	case <-ctx.Done():
   336  		return false, ctx.Err()
   337  	default:
   338  		l.mutex.Lock()
   339  		defer l.mutex.Unlock()
   340  
   341  		// Check whether uid is still active.
   342  		resource, ok := l.lockUID[formatUUID(args.UID, 0)]
   343  		if !ok {
   344  			return false, nil
   345  		}
   346  		idx := 0
   347  		for {
   348  			lris, ok := l.lockMap[resource]
   349  			if !ok {
   350  				// Inconsistent. Delete UID.
   351  				delete(l.lockUID, formatUUID(args.UID, idx))
   352  				return idx > 0, nil
   353  			}
   354  			for i := range lris {
   355  				if lris[i].UID == args.UID {
   356  					lris[i].TimeLastRefresh = UTCNow()
   357  				}
   358  			}
   359  			idx++
   360  			resource, ok = l.lockUID[formatUUID(args.UID, idx)]
   361  			if !ok {
   362  				// No more resources for UID, but we did update at least one.
   363  				return true, nil
   364  			}
   365  		}
   366  	}
   367  }
   368  
   369  // Similar to removeEntry but only removes an entry only if the lock entry exists in map.
   370  // Caller must hold 'l.mutex' lock.
   371  func (l *localLocker) expireOldLocks(interval time.Duration) {
   372  	l.mutex.Lock()
   373  	defer l.mutex.Unlock()
   374  
   375  	for k, lris := range l.lockMap {
   376  		modified := false
   377  		for i := 0; i < len(lris); {
   378  			lri := &lris[i]
   379  			if time.Since(lri.TimeLastRefresh) > interval {
   380  				delete(l.lockUID, formatUUID(lri.UID, lri.idx))
   381  				if len(lris) == 1 {
   382  					// Remove the write lock.
   383  					delete(l.lockMap, lri.Name)
   384  					modified = false
   385  					break
   386  				}
   387  				modified = true
   388  				// Remove the appropriate lock.
   389  				lris = append(lris[:i], lris[i+1:]...)
   390  				// Check same i
   391  			} else {
   392  				// Move to next
   393  				i++
   394  			}
   395  		}
   396  		if modified {
   397  			l.lockMap[k] = lris
   398  		}
   399  	}
   400  }
   401  
   402  func newLocker() *localLocker {
   403  	return &localLocker{
   404  		lockMap: make(map[string][]lockRequesterInfo, 1000),
   405  		lockUID: make(map[string]string, 1000),
   406  	}
   407  }