github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/local/hook_state.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package local
     5  
     6  import (
     7  	"log"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/terramate-io/tf/states"
    12  	"github.com/terramate-io/tf/states/statemgr"
    13  	"github.com/terramate-io/tf/terraform"
    14  )
    15  
    16  // StateHook is a hook that continuously updates the state by calling
    17  // WriteState on a statemgr.Full.
    18  type StateHook struct {
    19  	terraform.NilHook
    20  	sync.Mutex
    21  
    22  	StateMgr statemgr.Writer
    23  
    24  	// If PersistInterval is nonzero then for any new state update after
    25  	// the duration has elapsed we'll try to persist a state snapshot
    26  	// to the persistent backend too.
    27  	// That's only possible if field Schemas is valid, because the
    28  	// StateMgr.PersistState function for some backends needs schemas.
    29  	PersistInterval time.Duration
    30  
    31  	// Schemas are the schemas to use when persisting state due to
    32  	// PersistInterval. This is ignored if PersistInterval is zero,
    33  	// and PersistInterval is ignored if this is nil.
    34  	Schemas *terraform.Schemas
    35  
    36  	intermediatePersist IntermediateStatePersistInfo
    37  }
    38  
    39  type IntermediateStatePersistInfo struct {
    40  	// RequestedPersistInterval is the persist interval requested by whatever
    41  	// instantiated the StateHook.
    42  	//
    43  	// Implementations of [IntermediateStateConditionalPersister] should ideally
    44  	// respect this, but may ignore it if they use something other than the
    45  	// passage of time to make their decision.
    46  	RequestedPersistInterval time.Duration
    47  
    48  	// LastPersist is the time when the last intermediate state snapshot was
    49  	// persisted, or the time of the first report for Terraform Core if there
    50  	// hasn't yet been a persisted snapshot.
    51  	LastPersist time.Time
    52  
    53  	// ForcePersist is true when Terraform CLI has receieved an interrupt
    54  	// signal and is therefore trying to create snapshots more aggressively
    55  	// in anticipation of possibly being terminated ungracefully.
    56  	// [IntermediateStateConditionalPersister] implementations should ideally
    57  	// persist every snapshot they get when this flag is set, unless they have
    58  	// some external information that implies this shouldn't be necessary.
    59  	ForcePersist bool
    60  }
    61  
    62  var _ terraform.Hook = (*StateHook)(nil)
    63  
    64  func (h *StateHook) PostStateUpdate(new *states.State) (terraform.HookAction, error) {
    65  	h.Lock()
    66  	defer h.Unlock()
    67  
    68  	h.intermediatePersist.RequestedPersistInterval = h.PersistInterval
    69  
    70  	if h.intermediatePersist.LastPersist.IsZero() {
    71  		// The first PostStateUpdate starts the clock for intermediate
    72  		// calls to PersistState.
    73  		h.intermediatePersist.LastPersist = time.Now()
    74  	}
    75  
    76  	if h.StateMgr != nil {
    77  		if err := h.StateMgr.WriteState(new); err != nil {
    78  			return terraform.HookActionHalt, err
    79  		}
    80  		if mgrPersist, ok := h.StateMgr.(statemgr.Persister); ok && h.PersistInterval != 0 && h.Schemas != nil {
    81  			if h.shouldPersist() {
    82  				err := mgrPersist.PersistState(h.Schemas)
    83  				if err != nil {
    84  					return terraform.HookActionHalt, err
    85  				}
    86  				h.intermediatePersist.LastPersist = time.Now()
    87  			} else {
    88  				log.Printf("[DEBUG] State storage %T declined to persist a state snapshot", h.StateMgr)
    89  			}
    90  		}
    91  	}
    92  
    93  	return terraform.HookActionContinue, nil
    94  }
    95  
    96  func (h *StateHook) Stopping() {
    97  	h.Lock()
    98  	defer h.Unlock()
    99  
   100  	// If Terraform has been asked to stop then that might mean that a hard
   101  	// kill signal will follow shortly in case Terraform doesn't stop
   102  	// quickly enough, and so we'll try to persist the latest state
   103  	// snapshot in the hope that it'll give the user less recovery work to
   104  	// do if they _do_ subsequently hard-kill Terraform during an apply.
   105  
   106  	if mgrPersist, ok := h.StateMgr.(statemgr.Persister); ok && h.Schemas != nil {
   107  		// While we're in the stopping phase we'll try to persist every
   108  		// new state update to maximize every opportunity we get to avoid
   109  		// losing track of objects that have been created or updated.
   110  		// Terraform Core won't start any new operations after it's been
   111  		// stopped, so at most we should see one more PostStateUpdate
   112  		// call per already-active request.
   113  		h.intermediatePersist.ForcePersist = true
   114  
   115  		if h.shouldPersist() {
   116  			err := mgrPersist.PersistState(h.Schemas)
   117  			if err != nil {
   118  				// This hook can't affect Terraform Core's ongoing behavior,
   119  				// but it's a best effort thing anyway so we'll just emit a
   120  				// log to aid with debugging.
   121  				log.Printf("[ERROR] Failed to persist state after interruption: %s", err)
   122  			}
   123  		} else {
   124  			log.Printf("[DEBUG] State storage %T declined to persist a state snapshot", h.StateMgr)
   125  		}
   126  	}
   127  
   128  }
   129  
   130  func (h *StateHook) shouldPersist() bool {
   131  	if m, ok := h.StateMgr.(IntermediateStateConditionalPersister); ok {
   132  		return m.ShouldPersistIntermediateState(&h.intermediatePersist)
   133  	}
   134  	return DefaultIntermediateStatePersistRule(&h.intermediatePersist)
   135  }
   136  
   137  // DefaultIntermediateStatePersistRule is the default implementation of
   138  // [IntermediateStateConditionalPersister.ShouldPersistIntermediateState] used
   139  // when the selected state manager doesn't implement that interface.
   140  //
   141  // Implementers of that interface can optionally wrap a call to this function
   142  // if they want to combine the default behavior with some logic of their own.
   143  func DefaultIntermediateStatePersistRule(info *IntermediateStatePersistInfo) bool {
   144  	return info.ForcePersist || time.Since(info.LastPersist) >= info.RequestedPersistInterval
   145  }
   146  
   147  // IntermediateStateConditionalPersister is an optional extension of
   148  // [statemgr.Persister] that allows an implementation to tailor the rules for
   149  // whether to create intermediate state snapshots when Terraform Core emits
   150  // events reporting that the state might have changed.
   151  //
   152  // For state managers that don't implement this interface, [StateHook] uses
   153  // a default set of rules that aim to be a good compromise between how long
   154  // a state change can be active before it gets committed as a snapshot vs.
   155  // how many intermediate snapshots will get created. That compromise is subject
   156  // to change over time, but a state manager can implement this interface to
   157  // exert full control over those rules.
   158  type IntermediateStateConditionalPersister interface {
   159  	// ShouldPersistIntermediateState will be called each time Terraform Core
   160  	// emits an intermediate state event that is potentially eligible to be
   161  	// persisted.
   162  	//
   163  	// The implemention should return true to signal that the state snapshot
   164  	// most recently provided to the object's WriteState should be persisted,
   165  	// or false if it should not be persisted. If this function returns true
   166  	// then the receiver will see a subsequent call to
   167  	// [statemgr.Persister.PersistState] to request persistence.
   168  	//
   169  	// The implementation must not modify anything reachable through the
   170  	// arguments, and must not retain pointers to anything reachable through
   171  	// them after the function returns. However, implementers can assume that
   172  	// nothing will write to anything reachable through the arguments while
   173  	// this function is active.
   174  	ShouldPersistIntermediateState(info *IntermediateStatePersistInfo) bool
   175  }