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