github.com/thy00/storage@v1.12.8/lockfile_unix.go (about)

     1  // +build linux solaris darwin freebsd
     2  
     3  package storage
     4  
     5  import (
     6  	"fmt"
     7  	"os"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/containers/storage/pkg/stringid"
    12  	"github.com/pkg/errors"
    13  	"golang.org/x/sys/unix"
    14  )
    15  
    16  type lockfile struct {
    17  	// rwMutex serializes concurrent reader-writer acquisitions in the same process space
    18  	rwMutex *sync.RWMutex
    19  	// stateMutex is used to synchronize concurrent accesses to the state below
    20  	stateMutex *sync.Mutex
    21  	counter    int64
    22  	file       string
    23  	fd         uintptr
    24  	lw         string
    25  	locktype   int16
    26  	locked     bool
    27  	ro         bool
    28  	recursive  bool
    29  }
    30  
    31  // openLock opens the file at path and returns the corresponding file
    32  // descriptor.  Note that the path is opened read-only when ro is set.  If ro
    33  // is unset, openLock will open the path read-write and create the file if
    34  // necessary.
    35  func openLock(path string, ro bool) (int, error) {
    36  	if ro {
    37  		return unix.Open(path, os.O_RDONLY, 0)
    38  	}
    39  	return unix.Open(path, os.O_RDWR|os.O_CREATE, unix.S_IRUSR|unix.S_IWUSR)
    40  }
    41  
    42  // createLockerForPath returns a Locker object, possibly (depending on the platform)
    43  // working inter-process and associated with the specified path.
    44  //
    45  // This function will be called at most once for each path value within a single process.
    46  //
    47  // If ro, the lock is a read-write lock and the returned Locker should correspond to the
    48  // “lock for reading” (shared) operation; otherwise, the lock is either an exclusive lock,
    49  // or a read-write lock and Locker should correspond to the “lock for writing” (exclusive) operation.
    50  //
    51  // WARNING:
    52  // - The lock may or MAY NOT be inter-process.
    53  // - There may or MAY NOT be an actual object on the filesystem created for the specified path.
    54  // - Even if ro, the lock MAY be exclusive.
    55  func createLockerForPath(path string, ro bool) (Locker, error) {
    56  	// Check if we can open the lock.
    57  	fd, err := openLock(path, ro)
    58  	if err != nil {
    59  		return nil, errors.Wrapf(err, "error opening %q", path)
    60  	}
    61  	unix.Close(fd)
    62  
    63  	locktype := unix.F_WRLCK
    64  	if ro {
    65  		locktype = unix.F_RDLCK
    66  	}
    67  	return &lockfile{
    68  		stateMutex: &sync.Mutex{},
    69  		rwMutex:    &sync.RWMutex{},
    70  		file:       path,
    71  		lw:         stringid.GenerateRandomID(),
    72  		locktype:   int16(locktype),
    73  		locked:     false,
    74  		ro:         ro}, nil
    75  }
    76  
    77  // lock locks the lockfile via FCTNL(2) based on the specified type and
    78  // command.
    79  func (l *lockfile) lock(l_type int16, recursive bool) {
    80  	lk := unix.Flock_t{
    81  		Type:   l_type,
    82  		Whence: int16(os.SEEK_SET),
    83  		Start:  0,
    84  		Len:    0,
    85  	}
    86  	switch l_type {
    87  	case unix.F_RDLCK:
    88  		l.rwMutex.RLock()
    89  	case unix.F_WRLCK:
    90  		if recursive {
    91  			// NOTE: that's okay as recursive is only set in RecursiveLock(), so
    92  			// there's no need to protect against hypothetical RDLCK cases.
    93  			l.rwMutex.RLock()
    94  		} else {
    95  			l.rwMutex.Lock()
    96  		}
    97  	default:
    98  		panic(fmt.Sprintf("attempted to acquire a file lock of unrecognized type %d", l_type))
    99  	}
   100  	l.stateMutex.Lock()
   101  	defer l.stateMutex.Unlock()
   102  	if l.counter == 0 {
   103  		// If we're the first reference on the lock, we need to open the file again.
   104  		fd, err := openLock(l.file, l.ro)
   105  		if err != nil {
   106  			panic(fmt.Sprintf("error opening %q", l.file))
   107  		}
   108  		unix.CloseOnExec(fd)
   109  		l.fd = uintptr(fd)
   110  
   111  		// Optimization: only use the (expensive) fcntl syscall when
   112  		// the counter is 0.  In this case, we're either the first
   113  		// reader lock or a writer lock.
   114  		for unix.FcntlFlock(l.fd, unix.F_SETLKW, &lk) != nil {
   115  			time.Sleep(10 * time.Millisecond)
   116  		}
   117  	}
   118  	l.locktype = l_type
   119  	l.locked = true
   120  	l.recursive = recursive
   121  	l.counter++
   122  }
   123  
   124  // Lock locks the lockfile as a writer.  Note that RLock() will be called if
   125  // the lock is a read-only one.
   126  func (l *lockfile) Lock() {
   127  	if l.ro {
   128  		l.RLock()
   129  	} else {
   130  		l.lock(unix.F_WRLCK, false)
   131  	}
   132  }
   133  
   134  // RecursiveLock locks the lockfile as a writer but allows for recursive
   135  // acquisitions within the same process space.  Note that RLock() will be called
   136  // if it's a lockTypReader lock.
   137  func (l *lockfile) RecursiveLock() {
   138  	if l.ro {
   139  		l.RLock()
   140  	} else {
   141  		l.lock(unix.F_WRLCK, true)
   142  	}
   143  }
   144  
   145  // LockRead locks the lockfile as a reader.
   146  func (l *lockfile) RLock() {
   147  	l.lock(unix.F_RDLCK, false)
   148  }
   149  
   150  // Unlock unlocks the lockfile.
   151  func (l *lockfile) Unlock() {
   152  	lk := unix.Flock_t{
   153  		Type:   unix.F_UNLCK,
   154  		Whence: int16(os.SEEK_SET),
   155  		Start:  0,
   156  		Len:    0,
   157  		Pid:    int32(os.Getpid()),
   158  	}
   159  	l.stateMutex.Lock()
   160  	if l.locked == false {
   161  		// Panic when unlocking an unlocked lock.  That's a violation
   162  		// of the lock semantics and will reveal such.
   163  		panic("calling Unlock on unlocked lock")
   164  	}
   165  	l.counter--
   166  	if l.counter < 0 {
   167  		// Panic when the counter is negative.  There is no way we can
   168  		// recover from a corrupted lock and we need to protect the
   169  		// storage from corruption.
   170  		panic(fmt.Sprintf("lock %q has been unlocked too often", l.file))
   171  	}
   172  	if l.counter == 0 {
   173  		// We should only release the lock when the counter is 0 to
   174  		// avoid releasing read-locks too early; a given process may
   175  		// acquire a read lock multiple times.
   176  		l.locked = false
   177  		for unix.FcntlFlock(l.fd, unix.F_SETLKW, &lk) != nil {
   178  			time.Sleep(10 * time.Millisecond)
   179  		}
   180  		// Close the file descriptor on the last unlock.
   181  		unix.Close(int(l.fd))
   182  	}
   183  	if l.locktype == unix.F_RDLCK || l.recursive {
   184  		l.rwMutex.RUnlock()
   185  	} else {
   186  		l.rwMutex.Unlock()
   187  	}
   188  	l.stateMutex.Unlock()
   189  }
   190  
   191  // Locked checks if lockfile is locked for writing by a thread in this process.
   192  func (l *lockfile) Locked() bool {
   193  	l.stateMutex.Lock()
   194  	defer l.stateMutex.Unlock()
   195  	return l.locked && (l.locktype == unix.F_WRLCK)
   196  }
   197  
   198  // Touch updates the lock file with the UID of the user.
   199  func (l *lockfile) Touch() error {
   200  	l.stateMutex.Lock()
   201  	if !l.locked || (l.locktype != unix.F_WRLCK) {
   202  		panic("attempted to update last-writer in lockfile without the write lock")
   203  	}
   204  	l.stateMutex.Unlock()
   205  	l.lw = stringid.GenerateRandomID()
   206  	id := []byte(l.lw)
   207  	_, err := unix.Seek(int(l.fd), 0, os.SEEK_SET)
   208  	if err != nil {
   209  		return err
   210  	}
   211  	n, err := unix.Write(int(l.fd), id)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	if n != len(id) {
   216  		return unix.ENOSPC
   217  	}
   218  	err = unix.Fsync(int(l.fd))
   219  	if err != nil {
   220  		return err
   221  	}
   222  	return nil
   223  }
   224  
   225  // Modified indicates if the lockfile has been updated since the last time it
   226  // was loaded.
   227  func (l *lockfile) Modified() (bool, error) {
   228  	id := []byte(l.lw)
   229  	l.stateMutex.Lock()
   230  	if !l.locked {
   231  		panic("attempted to check last-writer in lockfile without locking it first")
   232  	}
   233  	l.stateMutex.Unlock()
   234  	_, err := unix.Seek(int(l.fd), 0, os.SEEK_SET)
   235  	if err != nil {
   236  		return true, err
   237  	}
   238  	n, err := unix.Read(int(l.fd), id)
   239  	if err != nil {
   240  		return true, err
   241  	}
   242  	if n != len(id) {
   243  		return true, nil
   244  	}
   245  	lw := l.lw
   246  	l.lw = string(id)
   247  	return l.lw != lw, nil
   248  }
   249  
   250  // IsReadWriteLock indicates if the lock file is a read-write lock.
   251  func (l *lockfile) IsReadWrite() bool {
   252  	return !l.ro
   253  }