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 }