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