-
Notifications
You must be signed in to change notification settings - Fork 10
/
locker.go
224 lines (195 loc) · 6.34 KB
/
locker.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
package statemgr
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"os"
"os/user"
"strings"
"text/template"
"time"
uuid "github.com/hashicorp/go-uuid"
"github.com/snyk/policy-engine/pkg/internal/terraform/version"
)
var rngSource = rand.New(rand.NewSource(time.Now().UnixNano()))
// Locker is the interface for state managers that are able to manage
// mutual-exclusion locks for state.
//
// Implementing Locker alongside Persistent relaxes some of the usual
// implementation constraints for implementations of Refresher and Persister,
// under the assumption that the locking mechanism effectively prevents
// multiple Terraform processes from reading and writing state concurrently.
// In particular, a type that implements both Locker and Persistent is only
// required to that the Persistent implementation is concurrency-safe within
// a single Terraform process.
//
// A Locker implementation must ensure that another processes with a
// similarly-configured state manager cannot successfully obtain a lock while
// the current process is holding it, or vice-versa, assuming that both
// processes agree on the locking mechanism.
//
// A Locker is not required to prevent non-cooperating processes from
// concurrently modifying the state, but is free to do so as an extra
// protection. If a mandatory locking mechanism of this sort is implemented,
// the state manager must ensure that RefreshState and PersistState calls
// can succeed if made through the same manager instance that is holding the
// lock, such has by retaining some sort of lock token that the Persistent
// methods can then use.
type Locker interface {
// Lock attempts to obtain a lock, using the given lock information.
//
// The result is an opaque id that can be passed to Unlock to release
// the lock, or an error if the lock cannot be acquired. Lock returns
// an instance of LockError immediately if the lock is already held,
// and the helper function LockWithContext uses this to automatically
// retry lock acquisition periodically until a timeout is reached.
Lock(info *LockInfo) (string, error)
// Unlock releases a lock previously acquired by Lock.
//
// If the lock cannot be released -- for example, if it was stolen by
// another user with some sort of administrative override privilege --
// then an error is returned explaining the situation in a way that
// is suitable for returning to an end-user.
Unlock(id string) error
}
// test hook to verify that LockWithContext has attempted a lock
var postLockHook func()
// LockWithContext locks the given state manager using the provided context
// for both timeout and cancellation.
//
// This method has a built-in retry/backoff behavior up to the context's
// timeout.
func LockWithContext(ctx context.Context, s Locker, info *LockInfo) (string, error) {
delay := time.Second
maxDelay := 16 * time.Second
for {
id, err := s.Lock(info)
if err == nil {
return id, nil
}
le, ok := err.(*LockError)
if !ok {
// not a lock error, so we can't retry
return "", err
}
if le == nil || le.Info == nil || le.Info.ID == "" {
// If we don't have a complete LockError then there's something
// wrong with the lock.
return "", err
}
if postLockHook != nil {
postLockHook()
}
// there's an existing lock, wait and try again
select {
case <-ctx.Done():
// return the last lock error with the info
return "", err
case <-time.After(delay):
if delay < maxDelay {
delay *= 2
}
}
}
}
// LockInfo stores lock metadata.
//
// Only Operation and Info are required to be set by the caller of Lock.
// Most callers should use NewLockInfo to create a LockInfo value with many
// of the fields populated with suitable default values.
type LockInfo struct {
// Unique ID for the lock. NewLockInfo provides a random ID, but this may
// be overridden by the lock implementation. The final value of ID will be
// returned by the call to Lock.
ID string
// Terraform operation, provided by the caller.
Operation string
// Extra information to store with the lock, provided by the caller.
Info string
// user@hostname when available
Who string
// Terraform version
Version string
// Time that the lock was taken.
Created time.Time
// Path to the state file when applicable. Set by the Lock implementation.
Path string
}
// NewLockInfo creates a LockInfo object and populates many of its fields
// with suitable default values.
func NewLockInfo() *LockInfo {
// this doesn't need to be cryptographically secure, just unique.
// Using math/rand alleviates the need to check handle the read error.
// Use a uuid format to match other IDs used throughout Terraform.
buf := make([]byte, 16)
rngSource.Read(buf)
id, err := uuid.FormatUUID(buf)
if err != nil {
// this of course shouldn't happen
panic(err)
}
// don't error out on user and hostname, as we don't require them
userName := ""
if userInfo, err := user.Current(); err == nil {
userName = userInfo.Username
}
host, _ := os.Hostname()
info := &LockInfo{
ID: id,
Who: fmt.Sprintf("%s@%s", userName, host),
Version: version.Version,
Created: time.Now().UTC(),
}
return info
}
// Err returns the lock info formatted in an error
func (l *LockInfo) Err() error {
return errors.New(l.String())
}
// Marshal returns a string json representation of the LockInfo
func (l *LockInfo) Marshal() []byte {
js, err := json.Marshal(l)
if err != nil {
panic(err)
}
return js
}
// String return a multi-line string representation of LockInfo
func (l *LockInfo) String() string {
tmpl := `Lock Info:
ID: {{.ID}}
Path: {{.Path}}
Operation: {{.Operation}}
Who: {{.Who}}
Version: {{.Version}}
Created: {{.Created}}
Info: {{.Info}}
`
t := template.Must(template.New("LockInfo").Parse(tmpl))
var out bytes.Buffer
if err := t.Execute(&out, l); err != nil {
panic(err)
}
return out.String()
}
// LockError is a specialization of type error that is returned by Locker.Lock
// to indicate that the lock is already held by another process and that
// retrying may be productive to take the lock once the other process releases
// it.
type LockError struct {
Info *LockInfo
Err error
}
func (e *LockError) Error() string {
var out []string
if e.Err != nil {
out = append(out, e.Err.Error())
}
if e.Info != nil {
out = append(out, e.Info.String())
}
return strings.Join(out, "\n")
}