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