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 }