github.com/cilium/cilium@v1.16.2/pkg/lock/lockfile/lockfile_linux.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  package lockfile
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"os"
     9  	"syscall"
    10  )
    11  
    12  // Lockfile is a simple wrapper around POSIX file locking
    13  // but it uses Linux's per-fd locks, which makes it safe to
    14  // use within the same process
    15  type Lockfile struct {
    16  	fp *os.File
    17  }
    18  
    19  // Linux supports per-file-descriptor locks, which are safer
    20  // and, more importantly, testable
    21  
    22  const (
    23  	SETLK  = 37 // F_OFD_SETLK
    24  	SETLKW = 38 // F_OFD_SETLKW
    25  )
    26  
    27  // NewLockfile creates and opens a lockfile, but does not acquire
    28  // a lock.
    29  func NewLockfile(path string) (*Lockfile, error) {
    30  	fp, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
    31  	if err != nil {
    32  		return nil, fmt.Errorf("failed to create lockfile %s: %w", path, err)
    33  	}
    34  
    35  	return &Lockfile{
    36  		fp: fp,
    37  	}, nil
    38  }
    39  
    40  // Close will close the file, which implicitly removes all locks held.
    41  // It is an error to re-use a closed Lockfile.
    42  func (l *Lockfile) Close() error {
    43  	fp := l.fp
    44  	l.fp = nil
    45  	return fp.Close()
    46  }
    47  
    48  // TryLock will attempt to take a lock, returining error if it is not
    49  // possible to acquire the lock.
    50  // If exclusive is true, then it will attempt to obtain a write, or exclusive, lock
    51  func (l *Lockfile) TryLock(exclusive bool) error {
    52  	return l.flock(context.Background(), false, exclusive, false)
    53  }
    54  
    55  // Lock will attempt to take a lock, blocking until it is able to do so.
    56  // If exclusive is true, then it will obtain a write, or exclusive, lock
    57  func (l *Lockfile) Lock(ctx context.Context, exclusive bool) error {
    58  	return l.flock(ctx, false, exclusive, true)
    59  }
    60  
    61  // Unlock removes the lock, but keeps the file open.
    62  func (l *Lockfile) Unlock() error {
    63  	return l.flock(context.Background(), true, false, false)
    64  }
    65  
    66  // flock will perform the lock operation
    67  // - unlock: if true, remove the lock
    68  // - exclusive: if true, then obtain a write lock, else a read lock
    69  // - wait: if true, then block until the lock is obtained. Ignored when unlocking
    70  func (l *Lockfile) flock(ctx context.Context, unlock, exclusive, wait bool) error {
    71  	var lockType int16 = syscall.F_RDLCK
    72  	if unlock {
    73  		lockType = syscall.F_UNLCK // unlock is a lockType!? What an API.
    74  	} else if exclusive {
    75  		lockType = syscall.F_WRLCK
    76  	}
    77  
    78  	command := SETLK
    79  	if !unlock && wait {
    80  		command = SETLKW
    81  	}
    82  
    83  	flockT := syscall.Flock_t{
    84  		Type:   lockType,
    85  		Whence: 0,
    86  		Start:  0,
    87  		Len:    0,
    88  	}
    89  
    90  	// if no context is supplied, or the context is non-cancellable,
    91  	// then don't do the goroutine dance
    92  	if ctx.Done() == nil {
    93  		return syscall.FcntlFlock(l.fp.Fd(), command, &flockT)
    94  	}
    95  
    96  	// syscalls can't be cancelled, so just wrap in a goroutine so we can
    97  	// return early if the context closes
    98  	lockCh := make(chan error, 1)
    99  	go func() {
   100  		lockCh <- syscall.FcntlFlock(l.fp.Fd(), command, &flockT)
   101  	}()
   102  	select {
   103  	case err := <-lockCh:
   104  		return err
   105  	case <-ctx.Done():
   106  		// oops, we cancelled
   107  		// spin up a goroutine to drop the lock when we get it,
   108  		// since syscalls can't actually be cancelled
   109  		go func() {
   110  			err := <-lockCh
   111  			if err == nil {
   112  				l.flock(context.Background(), true, false, false)
   113  			}
   114  		}()
   115  		return ctx.Err()
   116  	}
   117  }