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  }