github.com/opentofu/opentofu@v1.7.1/internal/states/statemgr/locker.go (about)

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