// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package local import ( "log" "sync" "time" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" ) // StateHook is a hook that continuously updates the state by calling // WriteState on a statemgr.Full. type StateHook struct { terraform.NilHook sync.Mutex StateMgr statemgr.Writer // If PersistInterval is nonzero then for any new state update after // the duration has elapsed we'll try to persist a state snapshot // to the persistent backend too. // That's only possible if field Schemas is valid, because the // StateMgr.PersistState function for some backends needs schemas. PersistInterval time.Duration // Schemas are the schemas to use when persisting state due to // PersistInterval. This is ignored if PersistInterval is zero, // and PersistInterval is ignored if this is nil. Schemas *terraform.Schemas intermediatePersist IntermediateStatePersistInfo } type IntermediateStatePersistInfo struct { // RequestedPersistInterval is the persist interval requested by whatever // instantiated the StateHook. // // Implementations of [IntermediateStateConditionalPersister] should ideally // respect this, but may ignore it if they use something other than the // passage of time to make their decision. RequestedPersistInterval time.Duration // LastPersist is the time when the last intermediate state snapshot was // persisted, or the time of the first report for Terraform Core if there // hasn't yet been a persisted snapshot. LastPersist time.Time // ForcePersist is true when Terraform CLI has receieved an interrupt // signal and is therefore trying to create snapshots more aggressively // in anticipation of possibly being terminated ungracefully. // [IntermediateStateConditionalPersister] implementations should ideally // persist every snapshot they get when this flag is set, unless they have // some external information that implies this shouldn't be necessary. ForcePersist bool } var _ terraform.Hook = (*StateHook)(nil) func (h *StateHook) PostStateUpdate(new *states.State) (terraform.HookAction, error) { h.Lock() defer h.Unlock() h.intermediatePersist.RequestedPersistInterval = h.PersistInterval if h.intermediatePersist.LastPersist.IsZero() { // The first PostStateUpdate starts the clock for intermediate // calls to PersistState. h.intermediatePersist.LastPersist = time.Now() } if h.StateMgr != nil { if err := h.StateMgr.WriteState(new); err != nil { return terraform.HookActionHalt, err } if mgrPersist, ok := h.StateMgr.(statemgr.Persister); ok && h.PersistInterval != 0 && h.Schemas != nil { if h.shouldPersist() { err := mgrPersist.PersistState(h.Schemas) if err != nil { return terraform.HookActionHalt, err } h.intermediatePersist.LastPersist = time.Now() } else { log.Printf("[DEBUG] State storage %T declined to persist a state snapshot", h.StateMgr) } } } return terraform.HookActionContinue, nil } func (h *StateHook) Stopping() { h.Lock() defer h.Unlock() // If Terraform has been asked to stop then that might mean that a hard // kill signal will follow shortly in case Terraform doesn't stop // quickly enough, and so we'll try to persist the latest state // snapshot in the hope that it'll give the user less recovery work to // do if they _do_ subsequently hard-kill Terraform during an apply. if mgrPersist, ok := h.StateMgr.(statemgr.Persister); ok && h.Schemas != nil { // While we're in the stopping phase we'll try to persist every // new state update to maximize every opportunity we get to avoid // losing track of objects that have been created or updated. // Terraform Core won't start any new operations after it's been // stopped, so at most we should see one more PostStateUpdate // call per already-active request. h.intermediatePersist.ForcePersist = true if h.shouldPersist() { err := mgrPersist.PersistState(h.Schemas) if err != nil { // This hook can't affect Terraform Core's ongoing behavior, // but it's a best effort thing anyway so we'll just emit a // log to aid with debugging. log.Printf("[ERROR] Failed to persist state after interruption: %s", err) } } else { log.Printf("[DEBUG] State storage %T declined to persist a state snapshot", h.StateMgr) } } } func (h *StateHook) shouldPersist() bool { if m, ok := h.StateMgr.(IntermediateStateConditionalPersister); ok { return m.ShouldPersistIntermediateState(&h.intermediatePersist) } return DefaultIntermediateStatePersistRule(&h.intermediatePersist) } // DefaultIntermediateStatePersistRule is the default implementation of // [IntermediateStateConditionalPersister.ShouldPersistIntermediateState] used // when the selected state manager doesn't implement that interface. // // Implementers of that interface can optionally wrap a call to this function // if they want to combine the default behavior with some logic of their own. func DefaultIntermediateStatePersistRule(info *IntermediateStatePersistInfo) bool { return info.ForcePersist || time.Since(info.LastPersist) >= info.RequestedPersistInterval } // IntermediateStateConditionalPersister is an optional extension of // [statemgr.Persister] that allows an implementation to tailor the rules for // whether to create intermediate state snapshots when Terraform Core emits // events reporting that the state might have changed. // // For state managers that don't implement this interface, [StateHook] uses // a default set of rules that aim to be a good compromise between how long // a state change can be active before it gets committed as a snapshot vs. // how many intermediate snapshots will get created. That compromise is subject // to change over time, but a state manager can implement this interface to // exert full control over those rules. type IntermediateStateConditionalPersister interface { // ShouldPersistIntermediateState will be called each time Terraform Core // emits an intermediate state event that is potentially eligible to be // persisted. // // The implemention should return true to signal that the state snapshot // most recently provided to the object's WriteState should be persisted, // or false if it should not be persisted. If this function returns true // then the receiver will see a subsequent call to // [statemgr.Persister.PersistState] to request persistence. // // The implementation must not modify anything reachable through the // arguments, and must not retain pointers to anything reachable through // them after the function returns. However, implementers can assume that // nothing will write to anything reachable through the arguments while // this function is active. ShouldPersistIntermediateState(info *IntermediateStatePersistInfo) bool }