blob: d591d3644fee00b3b934bc76c70f1a6caf350403 [file] [log] [blame]
// 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
}