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 }