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