1
// Copyright 2013 Canonical Ltd.
2
// Licensed under the LGPLv3, 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
"github.com/juju/loggo"
25
"github.com/juju/utils"
29
// NameRegexp specifies the regular expression used to identify valid lock names.
30
NameRegexp = "^[a-z]+[a-z0-9.-]*$"
32
messageFilename = "message"
36
logger = loggo.GetLogger("juju.utils.fslock")
37
ErrLockNotHeld = errors.New("lock not held")
38
ErrTimeout = errors.New("lock timeout exceeded")
40
validName = regexp.MustCompile(NameRegexp)
42
LockWaitDelay = 1 * time.Second
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)
58
nonce, err := utils.NewUUID()
67
// Ensure the parent exists.
68
if err := os.MkdirAll(lock.parent, 0755); err != nil {
74
func (lock *Lock) lockDir() string {
75
return path.Join(lock.parent, lock.name)
78
func (lock *Lock) heldFile() string {
79
return path.Join(lock.lockDir(), "held")
82
func (lock *Lock) messageFile() string {
83
return path.Join(lock.lockDir(), "message")
86
// If message is set, it will write the message to the lock directory as the
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())
94
if !os.IsNotExist(err) {
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)
104
return false, err // this shouldn't really fail...
106
// write nonce into the temp dir
107
err = ioutil.WriteFile(path.Join(tempDirName, heldFilename), lock.nonce, 0755)
112
err = ioutil.WriteFile(path.Join(tempDirName, messageFilename), []byte(message), 0755)
117
// Now move the temp directory to the lock directory.
118
err = utils.ReplaceFile(tempDirName, lock.lockDir())
120
// Any error on rename means we failed.
121
// Beaten to it, clean up temporary directory.
122
os.RemoveAll(tempDirName)
125
// We now have the lock.
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 {
134
acquired, err := lock.acquire(message)
141
if err = continueFunc(); err != nil {
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
149
time.Sleep(LockWaitDelay)
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)
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) {
176
return lock.lockLoop(message, continueFunc)
179
// LockWithFunc 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)
186
// IsLockHeld returns whether the lock is currently held by the receiver.
187
func (lock *Lock) IsLockHeld() bool {
188
heldNonce, err := ioutil.ReadFile(lock.heldFile())
192
return bytes.Equal(heldNonce, lock.nonce)
195
// Unlock releases a held lock. If the lock is not held ErrLockNotHeld is
197
func (lock *Lock) Unlock() error {
198
if !lock.IsLockHeld() {
199
return ErrLockNotHeld
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 {
209
return os.RemoveAll(tempDirName)
212
// IsLocked returns true if the lock is currently held by anyone.
213
func (lock *Lock) IsLocked() bool {
214
_, err := os.Stat(lock.heldFile())
218
// BreakLock forcably breaks the lock that is currently being held.
219
func (lock *Lock) BreakLock() error {
220
return os.RemoveAll(lock.lockDir())
223
// Message returns the saved message, or the empty string if there is no
225
func (lock *Lock) Message() string {
226
message, err := ioutil.ReadFile(lock.messageFile())
230
return string(message)