1
// Copyright 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
// On-disk mutex protecting a resource
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.
23
"launchpad.net/juju-core/log"
24
"launchpad.net/juju-core/utils"
28
// NameRegexp specifies the regular expression used to itentify valid lock names.
29
NameRegexp = "^[a-z]+[a-z0-9.-]*$"
31
messageFilename = "message"
35
ErrLockNotHeld = errors.New("lock not held")
36
ErrTimeout = errors.New("lock timeout exceeded")
38
validName = regexp.MustCompile(NameRegexp)
40
lockWaitDelay = 1 * time.Second
49
// NewLock returns a new lock with the given name within the given lock
50
// directory, without acquiring it. The lock name must match the regular
51
// expression defined by NameRegexp.
52
func NewLock(lockDir, name string) (*Lock, error) {
53
if !validName.MatchString(name) {
54
return nil, fmt.Errorf("Invalid lock name %q. Names must match %q", name, NameRegexp)
56
nonce, err := utils.NewUUID()
65
// Ensure the parent exists.
66
if err := os.MkdirAll(lock.parent, 0755); err != nil {
72
func (lock *Lock) lockDir() string {
73
return path.Join(lock.parent, lock.name)
76
func (lock *Lock) heldFile() string {
77
return path.Join(lock.lockDir(), "held")
80
func (lock *Lock) messageFile() string {
81
return path.Join(lock.lockDir(), "message")
84
// If message is set, it will write the message to the lock directory as the
86
func (lock *Lock) acquire(message string) (bool, error) {
87
// If the lockDir exists, then the lock is held by someone else.
88
_, err := os.Stat(lock.lockDir())
92
if !os.IsNotExist(err) {
95
// Create a temporary directory (in the parent dir), and then move it to
96
// the right name. Using the same directory to make sure the directories
97
// are on the same filesystem. Use a directory name starting with "." as
98
// it isn't a valid lock name.
99
tempLockName := fmt.Sprintf(".%x", lock.nonce)
100
tempDirName, err := ioutil.TempDir(lock.parent, tempLockName)
102
return false, err // this shouldn't really fail...
104
// write nonce into the temp dir
105
err = ioutil.WriteFile(path.Join(tempDirName, heldFilename), lock.nonce, 0755)
110
err = ioutil.WriteFile(path.Join(tempDirName, messageFilename), []byte(message), 0755)
115
// Now move the temp directory to the lock directory.
116
err = os.Rename(tempDirName, lock.lockDir())
118
// Any error on rename means we failed.
119
// Beaten to it, clean up temporary directory.
120
os.RemoveAll(tempDirName)
123
// We now have the lock.
127
// lockLoop tries to acquire the lock. If the acquisition fails, the
128
// continueFunc is run to see if the function should continue waiting.
129
func (lock *Lock) lockLoop(message string, continueFunc func() error) error {
132
acquired, err := lock.acquire(message)
139
if err = continueFunc(); err != nil {
142
currMessage := lock.Message()
143
if currMessage != heldMessage {
144
log.Infof("attempted lock failed %q, %s, currently held: %s", lock.name, message, currMessage)
145
heldMessage = currMessage
147
time.Sleep(lockWaitDelay)
152
// Lock blocks until it is able to acquire the lock. Since we are dealing
153
// with sharing and locking using the filesystem, it is good behaviour to
154
// provide a message that is saved with the lock. This is output in debugging
155
// information, and can be queried by any other Lock dealing with the same
156
// lock name and lock directory.
157
func (lock *Lock) Lock(message string) error {
158
// The continueFunc is effectively a no-op, causing continual looping
159
// until the lock is acquired.
160
continueFunc := func() error { return nil }
161
return lock.lockLoop(message, continueFunc)
164
// LockWithTimeout tries to acquire the lock. If it cannot acquire the lock
165
// within the given duration, it returns ErrTimeout. See `Lock` for
166
// information about the message.
167
func (lock *Lock) LockWithTimeout(duration time.Duration, message string) error {
168
deadline := time.Now().Add(duration)
169
continueFunc := func() error {
170
if time.Now().After(deadline) {
175
return lock.lockLoop(message, continueFunc)
178
// Lock blocks until it is able to acquire the lock. If the lock is failed to
179
// be acquired, the continueFunc is called prior to the sleeping. If the
180
// continueFunc returns an error, that error is returned from LockWithFunc.
181
func (lock *Lock) LockWithFunc(message string, continueFunc func() error) error {
182
return lock.lockLoop(message, continueFunc)
185
// IsHeld returns whether the lock is currently held by the receiver.
186
func (lock *Lock) IsLockHeld() bool {
187
heldNonce, err := ioutil.ReadFile(lock.heldFile())
191
return bytes.Equal(heldNonce, lock.nonce)
194
// Unlock releases a held lock. If the lock is not held ErrLockNotHeld is
196
func (lock *Lock) Unlock() error {
197
if !lock.IsLockHeld() {
198
return ErrLockNotHeld
200
// To ensure reasonable unlocking, we should rename to a temp name, and delete that.
201
tempLockName := fmt.Sprintf(".%s.%x", lock.name, lock.nonce)
202
tempDirName := path.Join(lock.parent, tempLockName)
203
// Now move the lock directory to the temp directory to release the lock.
204
if err := os.Rename(lock.lockDir(), tempDirName); err != nil {
208
return os.RemoveAll(tempDirName)
211
// IsLocked returns true if the lock is currently held by anyone.
212
func (lock *Lock) IsLocked() bool {
213
_, err := os.Stat(lock.heldFile())
217
// BreakLock forcably breaks the lock that is currently being held.
218
func (lock *Lock) BreakLock() error {
219
return os.RemoveAll(lock.lockDir())
222
// Message returns the saved message, or the empty string if there is no
224
func (lock *Lock) Message() string {
225
message, err := ioutil.ReadFile(lock.messageFile())
229
return string(message)