~juju-qa/ubuntu/yakkety/juju/juju-1.25.8

« back to all changes in this revision

Viewing changes to src/github.com/juju/utils/fslock/fslock.go

  • Committer: Nicholas Skaggs
  • Date: 2016-12-02 17:28:37 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161202172837-jkrbdlyjcxtrii2n
Initial commit of 1.25.6

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2013 Canonical Ltd.
 
2
// Licensed under the LGPLv3, 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
        "github.com/juju/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
// 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)
 
184
}
 
185
 
 
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())
 
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
}