launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/utils/fslock/fslock.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // On-disk mutex protecting a resource 5 // 6 // A lock is represented on disk by a directory of a particular name, 7 // containing an information file. Taking a lock is done by renaming a 8 // temporary directory into place. We use temporary directories because for 9 // all filesystems we believe that exactly one attempt to claim the lock will 10 // succeed and the others will fail. 11 package fslock 12 13 import ( 14 "bytes" 15 "fmt" 16 "io/ioutil" 17 "launchpad.net/errgo/errors" 18 "os" 19 "path" 20 "regexp" 21 "time" 22 23 "github.com/loggo/loggo" 24 25 "launchpad.net/juju-core/utils" 26 ) 27 28 const ( 29 // NameRegexp specifies the regular expression used to identify valid lock names. 30 NameRegexp = "^[a-z]+[a-z0-9.-]*$" 31 heldFilename = "held" 32 messageFilename = "message" 33 ) 34 35 var ( 36 mask = errors.Mask 37 logger = loggo.GetLogger("juju.utils.fslock") 38 ErrLockNotHeld = errors.New("lock not held") 39 ErrTimeout = errors.New("lock timeout exceeded") 40 41 validName = regexp.MustCompile(NameRegexp) 42 43 LockWaitDelay = 1 * time.Second 44 ) 45 46 type Lock struct { 47 name string 48 parent string 49 nonce []byte 50 } 51 52 // NewLock returns a new lock with the given name within the given lock 53 // directory, without acquiring it. The lock name must match the regular 54 // expression defined by NameRegexp. 55 func NewLock(lockDir, name string) (*Lock, error) { 56 if !validName.MatchString(name) { 57 return nil, errors.Newf("Invalid lock name %q. Names must match %q", name, NameRegexp) 58 } 59 nonce, err := utils.NewUUID() 60 if err != nil { 61 return nil, mask(err) 62 } 63 lock := &Lock{ 64 name: name, 65 parent: lockDir, 66 nonce: nonce[:], 67 } 68 // Ensure the parent exists. 69 if err := os.MkdirAll(lock.parent, 0755); err != nil { 70 return nil, mask(err) 71 } 72 return lock, nil 73 } 74 75 func (lock *Lock) lockDir() string { 76 return path.Join(lock.parent, lock.name) 77 } 78 79 func (lock *Lock) heldFile() string { 80 return path.Join(lock.lockDir(), "held") 81 } 82 83 func (lock *Lock) messageFile() string { 84 return path.Join(lock.lockDir(), "message") 85 } 86 87 // If message is set, it will write the message to the lock directory as the 88 // lock is taken. 89 func (lock *Lock) acquire(message string) (bool, error) { 90 // If the lockDir exists, then the lock is held by someone else. 91 _, err := os.Stat(lock.lockDir()) 92 if err == nil { 93 return false, nil 94 } 95 if !os.IsNotExist(err) { 96 return false, err 97 } 98 // Create a temporary directory (in the parent dir), and then move it to 99 // the right name. Using the same directory to make sure the directories 100 // are on the same filesystem. Use a directory name starting with "." as 101 // it isn't a valid lock name. 102 tempLockName := fmt.Sprintf(".%x", lock.nonce) 103 tempDirName, err := ioutil.TempDir(lock.parent, tempLockName) 104 if err != nil { 105 return false, mask(err) // this shouldn't really fail... 106 107 } 108 // write nonce into the temp dir 109 err = ioutil.WriteFile(path.Join(tempDirName, heldFilename), lock.nonce, 0755) 110 if err != nil { 111 return false, mask(err) 112 } 113 if message != "" { 114 err = ioutil.WriteFile(path.Join(tempDirName, messageFilename), []byte(message), 0755) 115 if err != nil { 116 return false, mask(err) 117 } 118 } 119 // Now move the temp directory to the lock directory. 120 err = utils.ReplaceFile(tempDirName, lock.lockDir()) 121 if err != nil { 122 // Any error on rename means we failed. 123 // Beaten to it, clean up temporary directory. 124 os.RemoveAll(tempDirName) 125 return false, nil 126 } 127 // We now have the lock. 128 return true, nil 129 } 130 131 // lockLoop tries to acquire the lock. If the acquisition fails, the 132 // continueFunc is run to see if the function should continue waiting. 133 func (lock *Lock) lockLoop(message string, continueFunc func() error) error { 134 var heldMessage = "" 135 for { 136 acquired, err := lock.acquire(message) 137 if err != nil { 138 return mask(err) 139 } 140 if acquired { 141 return nil 142 } 143 if err = continueFunc(); err != nil { 144 return mask(err, errors.Any) 145 } 146 currMessage := lock.Message() 147 if currMessage != heldMessage { 148 logger.Infof("attempted lock failed %q, %s, currently held: %s", lock.name, message, currMessage) 149 heldMessage = currMessage 150 } 151 time.Sleep(LockWaitDelay) 152 } 153 } 154 155 // Lock blocks until it is able to acquire the lock. Since we are dealing 156 // with sharing and locking using the filesystem, it is good behaviour to 157 // provide a message that is saved with the lock. This is output in debugging 158 // information, and can be queried by any other Lock dealing with the same 159 // lock name and lock directory. 160 func (lock *Lock) Lock(message string) error { 161 // The continueFunc is effectively a no-op, causing continual looping 162 // until the lock is acquired. 163 continueFunc := func() error { return nil } 164 return lock.lockLoop(message, continueFunc) 165 } 166 167 // LockWithTimeout tries to acquire the lock. If it cannot acquire the lock 168 // within the given duration, it returns ErrTimeout. See `Lock` for 169 // information about the message. 170 func (lock *Lock) LockWithTimeout(duration time.Duration, message string) error { 171 deadline := time.Now().Add(duration) 172 continueFunc := func() error { 173 if time.Now().After(deadline) { 174 return ErrTimeout 175 } 176 return nil 177 } 178 return lock.lockLoop(message, continueFunc) 179 } 180 181 // Lock blocks until it is able to acquire the lock. If the lock is failed to 182 // be acquired, the continueFunc is called prior to the sleeping. If the 183 // continueFunc returns an error, that error is returned from LockWithFunc. 184 func (lock *Lock) LockWithFunc(message string, continueFunc func() error) error { 185 return lock.lockLoop(message, continueFunc) 186 } 187 188 // IsHeld returns whether the lock is currently held by the receiver. 189 func (lock *Lock) IsLockHeld() bool { 190 heldNonce, err := ioutil.ReadFile(lock.heldFile()) 191 if err != nil { 192 return false 193 } 194 return bytes.Equal(heldNonce, lock.nonce) 195 } 196 197 // Unlock releases a held lock. If the lock is not held ErrLockNotHeld is 198 // returned. 199 func (lock *Lock) Unlock() error { 200 if !lock.IsLockHeld() { 201 return ErrLockNotHeld 202 } 203 // To ensure reasonable unlocking, we should rename to a temp name, and delete that. 204 tempLockName := fmt.Sprintf(".%s.%x", lock.name, lock.nonce) 205 tempDirName := path.Join(lock.parent, tempLockName) 206 // Now move the lock directory to the temp directory to release the lock. 207 if err := utils.ReplaceFile(lock.lockDir(), tempDirName); err != nil { 208 return mask(err) 209 } 210 211 // And now cleanup. 212 return os.RemoveAll(tempDirName) 213 } 214 215 // IsLocked returns true if the lock is currently held by anyone. 216 func (lock *Lock) IsLocked() bool { 217 _, err := os.Stat(lock.heldFile()) 218 return err == nil 219 } 220 221 // BreakLock forcably breaks the lock that is currently being held. 222 func (lock *Lock) BreakLock() error { 223 return os.RemoveAll(lock.lockDir()) 224 } 225 226 // Message returns the saved message, or the empty string if there is no 227 // saved message. 228 func (lock *Lock) Message() string { 229 message, err := ioutil.ReadFile(lock.messageFile()) 230 if err != nil { 231 return "" 232 } 233 return string(message) 234 }