github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/lockedfile/internal/filelock/filelock_fcntl.go (about)

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build aix || (solaris && !illumos)
     6  
     7  // This code implements the filelock API using POSIX 'fcntl' locks, which attach
     8  // to an (inode, process) pair rather than a file descriptor. To avoid unlocking
     9  // files prematurely when the same file is opened through different descriptors,
    10  // we allow only one read-lock at a time.
    11  //
    12  // Most platforms provide some alternative API, such as an 'flock' system call
    13  // or an F_OFD_SETLK command for 'fcntl', that allows for better concurrency and
    14  // does not require per-inode bookkeeping in the application.
    15  
    16  package filelock
    17  
    18  import (
    19  	"errors"
    20  	"io"
    21  	"io/fs"
    22  	"math/rand"
    23  	"sync"
    24  	"syscall"
    25  	"time"
    26  )
    27  
    28  type lockType int16
    29  
    30  const (
    31  	readLock  lockType = syscall.F_RDLCK
    32  	writeLock lockType = syscall.F_WRLCK
    33  )
    34  
    35  type inode = uint64 // type of syscall.Stat_t.Ino
    36  
    37  type inodeLock struct {
    38  	owner File
    39  	queue []<-chan File
    40  }
    41  
    42  var (
    43  	mu     sync.Mutex
    44  	inodes = map[File]inode{}
    45  	locks  = map[inode]inodeLock{}
    46  )
    47  
    48  func lock(f File, lt lockType) (err error) {
    49  	// POSIX locks apply per inode and process, and the lock for an inode is
    50  	// released when *any* descriptor for that inode is closed. So we need to
    51  	// synchronize access to each inode internally, and must serialize lock and
    52  	// unlock calls that refer to the same inode through different descriptors.
    53  	fi, err := f.Stat()
    54  	if err != nil {
    55  		return err
    56  	}
    57  	ino := fi.Sys().(*syscall.Stat_t).Ino
    58  
    59  	mu.Lock()
    60  	if i, dup := inodes[f]; dup && i != ino {
    61  		mu.Unlock()
    62  		return &fs.PathError{
    63  			Op:   lt.String(),
    64  			Path: f.Name(),
    65  			Err:  errors.New("inode for file changed since last Lock or RLock"),
    66  		}
    67  	}
    68  	inodes[f] = ino
    69  
    70  	var wait chan File
    71  	l := locks[ino]
    72  	if l.owner == f {
    73  		// This file already owns the lock, but the call may change its lock type.
    74  	} else if l.owner == nil {
    75  		// No owner: it's ours now.
    76  		l.owner = f
    77  	} else {
    78  		// Already owned: add a channel to wait on.
    79  		wait = make(chan File)
    80  		l.queue = append(l.queue, wait)
    81  	}
    82  	locks[ino] = l
    83  	mu.Unlock()
    84  
    85  	if wait != nil {
    86  		wait <- f
    87  	}
    88  
    89  	// Spurious EDEADLK errors arise on platforms that compute deadlock graphs at
    90  	// the process, rather than thread, level. Consider processes P and Q, with
    91  	// threads P.1, P.2, and Q.3. The following trace is NOT a deadlock, but will be
    92  	// reported as a deadlock on systems that consider only process granularity:
    93  	//
    94  	// 	P.1 locks file A.
    95  	// 	Q.3 locks file B.
    96  	// 	Q.3 blocks on file A.
    97  	// 	P.2 blocks on file B. (This is erroneously reported as a deadlock.)
    98  	// 	P.1 unlocks file A.
    99  	// 	Q.3 unblocks and locks file A.
   100  	// 	Q.3 unlocks files A and B.
   101  	// 	P.2 unblocks and locks file B.
   102  	// 	P.2 unlocks file B.
   103  	//
   104  	// These spurious errors were observed in practice on AIX and Solaris in
   105  	// cmd/go: see https://golang.org/issue/32817.
   106  	//
   107  	// We work around this bug by treating EDEADLK as always spurious. If there
   108  	// really is a lock-ordering bug between the interacting processes, it will
   109  	// become a livelock instead, but that's not appreciably worse than if we had
   110  	// a proper flock implementation (which generally does not even attempt to
   111  	// diagnose deadlocks).
   112  	//
   113  	// In the above example, that changes the trace to:
   114  	//
   115  	// 	P.1 locks file A.
   116  	// 	Q.3 locks file B.
   117  	// 	Q.3 blocks on file A.
   118  	// 	P.2 spuriously fails to lock file B and goes to sleep.
   119  	// 	P.1 unlocks file A.
   120  	// 	Q.3 unblocks and locks file A.
   121  	// 	Q.3 unlocks files A and B.
   122  	// 	P.2 wakes up and locks file B.
   123  	// 	P.2 unlocks file B.
   124  	//
   125  	// We know that the retry loop will not introduce a *spurious* livelock
   126  	// because, according to the POSIX specification, EDEADLK is only to be
   127  	// returned when “the lock is blocked by a lock from another process”.
   128  	// If that process is blocked on some lock that we are holding, then the
   129  	// resulting livelock is due to a real deadlock (and would manifest as such
   130  	// when using, for example, the flock implementation of this package).
   131  	// If the other process is *not* blocked on some other lock that we are
   132  	// holding, then it will eventually release the requested lock.
   133  
   134  	nextSleep := 1 * time.Millisecond
   135  	const maxSleep = 500 * time.Millisecond
   136  	for {
   137  		err = setlkw(f.Fd(), lt)
   138  		if err != syscall.EDEADLK {
   139  			break
   140  		}
   141  		time.Sleep(nextSleep)
   142  
   143  		nextSleep += nextSleep
   144  		if nextSleep > maxSleep {
   145  			nextSleep = maxSleep
   146  		}
   147  		// Apply 10% jitter to avoid synchronizing collisions when we finally unblock.
   148  		nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
   149  	}
   150  
   151  	if err != nil {
   152  		unlock(f)
   153  		return &fs.PathError{
   154  			Op:   lt.String(),
   155  			Path: f.Name(),
   156  			Err:  err,
   157  		}
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  func unlock(f File) error {
   164  	var owner File
   165  
   166  	mu.Lock()
   167  	ino, ok := inodes[f]
   168  	if ok {
   169  		owner = locks[ino].owner
   170  	}
   171  	mu.Unlock()
   172  
   173  	if owner != f {
   174  		panic("unlock called on a file that is not locked")
   175  	}
   176  
   177  	err := setlkw(f.Fd(), syscall.F_UNLCK)
   178  
   179  	mu.Lock()
   180  	l := locks[ino]
   181  	if len(l.queue) == 0 {
   182  		// No waiters: remove the map entry.
   183  		delete(locks, ino)
   184  	} else {
   185  		// The first waiter is sending us their file now.
   186  		// Receive it and update the queue.
   187  		l.owner = <-l.queue[0]
   188  		l.queue = l.queue[1:]
   189  		locks[ino] = l
   190  	}
   191  	delete(inodes, f)
   192  	mu.Unlock()
   193  
   194  	return err
   195  }
   196  
   197  // setlkw calls FcntlFlock with F_SETLKW for the entire file indicated by fd.
   198  func setlkw(fd uintptr, lt lockType) error {
   199  	for {
   200  		err := syscall.FcntlFlock(fd, syscall.F_SETLKW, &syscall.Flock_t{
   201  			Type:   int16(lt),
   202  			Whence: io.SeekStart,
   203  			Start:  0,
   204  			Len:    0, // All bytes.
   205  		})
   206  		if err != syscall.EINTR {
   207  			return err
   208  		}
   209  	}
   210  }