opentofu/states/statemgr/locker.go
Martin Atkins 53cafc542b statemgr: New package for state managers
This idea of a "state manager" was previously modelled via the
confusingly-named state.State interface, which we've been calling a "state
manager" only in some local variable names in situations where there were
also *terraform.State variables.

As part of reworking our state models to make room for the new type
system, we also need to change what was previously the state.StateReader
interface. Since we've found the previous organization confusing anyway,
here we just copy all of those interfaces over into statemgr where we can
make the relationship to states.State hopefully a little clearer.

This is not yet a complete move of the functionality from "state", since
we're not yet ready to break existing callers. In a future commit we'll
turn the interfaces in the old "state" package into aliases of the
interfaces in this package, and update all the implementers of what will
by then be statemgr.Reader to use *states.State instead of
*terraform.State.

This also includes an adaptation of what was previously state.LocalState
into statemgr.FileSystem, using the new state serialization functionality
from package statefile instead of the old terraform.ReadState and
terraform.WriteState.
2018-10-16 18:49:20 -07:00

225 lines
6.3 KiB
Go

package statemgr
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"html/template"
"math/rand"
"os"
"os/user"
"strings"
"time"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/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
// implemention 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 if 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")
}