github.com/blend/go-sdk@v1.20220411.3/filelock/mutex.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package filelock
     9  
    10  import (
    11  	"fmt"
    12  	"io/fs"
    13  	"os"
    14  	"sync"
    15  )
    16  
    17  // MutexAt returns a file based mutex at a given path.
    18  func MutexAt(path string) *Mutex {
    19  	futex := &Mutex{
    20  		Path: path,
    21  	}
    22  	return futex
    23  }
    24  
    25  // Mutex manages filehandle based locks that are scoped to the lifetime
    26  // of a parent process. If you need semi-durable locks that can span for the duration
    27  // of an action, use `fslock.FSLock` in the github-actions project.
    28  //
    29  // It does not implement `sync.Locker` because file based mutexes can fail to Lock().
    30  type Mutex struct {
    31  	// we still have a sync.Mutex on this to guard against race-prone use cases
    32  	// within the same process.
    33  	mu sync.Mutex
    34  
    35  	// Path is the file path to use as the lock.
    36  	Path string
    37  }
    38  
    39  // RLock creates a reader lock and returns an unlock function.
    40  func (mu *Mutex) RLock() (runlock func(), err error) {
    41  	if mu.Path == "" {
    42  		err = fmt.Errorf("mutex; path unset")
    43  		return
    44  	}
    45  
    46  	f, err := mu.openFile(os.O_RDONLY|os.O_CREATE, 0666)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  	mu.mu.Lock()
    51  	return func() {
    52  		mu.mu.Unlock()
    53  		mu.closeFile(f)
    54  	}, nil
    55  }
    56  
    57  // Lock attempts to lock the Mutex.
    58  //
    59  // If successful, Lock returns a non-nil unlock function: it is provided as a
    60  // return-value instead of a separate method to remind the caller to check the
    61  // accompanying error. (See https://golang.org/issue/20803.)
    62  func (mu *Mutex) Lock() (unlock func(), err error) {
    63  	if mu.Path == "" {
    64  		err = fmt.Errorf("mutex; path unset")
    65  		return
    66  	}
    67  	// We could use either O_RDWR or O_WRONLY here. If we choose O_RDWR and the
    68  	// file at mu.Path is write-only, the call to OpenFile will fail with a
    69  	// permission error. That's actually what we want: if we add an RLock method
    70  	// in the future, it should call OpenFile with O_RDONLY and will require the
    71  	// files must be readable, so we should not let the caller make any
    72  	// assumptions about Mutex working with write-only files.
    73  	f, err := mu.openFile(os.O_RDWR|os.O_CREATE, 0666)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	mu.mu.Lock()
    78  	return func() {
    79  		mu.mu.Unlock()
    80  		mu.closeFile(f)
    81  	}, nil
    82  }
    83  
    84  func (mu *Mutex) openFile(flag int, perm fs.FileMode) (*os.File, error) {
    85  	// On BSD systems, we could add the O_SHLOCK or O_EXLOCK flag to the OpenFile
    86  	// call instead of locking separately, but we have to support separate locking
    87  	// calls for Linux and Windows anyway, so it's simpler to use that approach
    88  	// consistently.
    89  	f, err := os.OpenFile(mu.Path, flag&^os.O_TRUNC, perm)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
    95  	case os.O_WRONLY, os.O_RDWR:
    96  		err = Lock(f)
    97  	default:
    98  		err = RLock(f)
    99  	}
   100  	if err != nil {
   101  		f.Close()
   102  		return nil, err
   103  	}
   104  
   105  	if flag&os.O_TRUNC == os.O_TRUNC {
   106  		if err := f.Truncate(0); err != nil {
   107  			// The documentation for os.O_TRUNC says “if possible, truncate file when
   108  			// opened”, but doesn't define “possible” (golang.org/issue/28699).
   109  			// We'll treat regular files (and symlinks to regular files) as “possible”
   110  			// and ignore errors for the rest.
   111  			if fi, statErr := f.Stat(); statErr != nil || fi.Mode().IsRegular() {
   112  				Unlock(f)
   113  				f.Close()
   114  				return nil, err
   115  			}
   116  		}
   117  	}
   118  	return f, nil
   119  }
   120  
   121  func (mu *Mutex) closeFile(f *os.File) error {
   122  	// Since locking syscalls operate on file descriptors, we must unlock the file
   123  	// while the descriptor is still valid — that is, before the file is closed —
   124  	// and avoid unlocking files that are already closed.
   125  	err := Unlock(f)
   126  	if closeErr := f.Close(); err == nil {
   127  		err = closeErr
   128  	}
   129  	return err
   130  }