opentofu/internal/terraform/hook_test.go
Martin Atkins f0de9b60c1 backend/local: Periodically persist intermediate state snapshots
Terraform Core emits a hook event every time it writes a change into the
in-memory state. Previously the local backend would just copy that into
the transient storage of the state manager, but for most state storage
implementations that doesn't really do anything useful because it just
makes another copy of the state in memory.

We originally added this hook mechanism with the intent of making
Terraform _persist_ the state each time, but we backed that out after
finding that it was a bit too aggressive and was making the state snapshot
history much harder to use in storage systems that can preserve historical
snapshots.

However, sometimes Terraform gets killed mid-apply for whatever reason and
in our previous implementation that meant always losing that transient
state, forcing the user to edit the state manually (or use "import") to
recover a useful state.

In an attempt at finding a sweet spot between these extremes, here we
change the rule so that if an apply runs for longer than 20 seconds then
we'll try to persist the state to the backend in an update that arrives
at least 20 seconds after the first update, and then again for each
additional 20 second period as long as Terraform keeps announcing new
state snapshots.

This also introduces a special interruption mode where if the apply phase
gets interrupted by SIGINT (or equivalent) then the local backend will
try to persist the state immediately in anticipation of a
possibly-imminent SIGKILL, and will then immediately persist any
subsequent state update that arrives until the apply phase is complete.
After interruption Terraform will not start any new operations and will
instead just let any already-running operations run to completion, and so
this will persist the state once per resource instance that is able to
complete before being killed.

This does mean that now long-running applies will generate intermediate
state snapshots where they wouldn't before, but there should still be
considerably fewer snapshots than were created when we were persisting
for each individual state change. We can adjust the 20 second interval
in future commits if we find that this spot isn't as sweet as first
assumed.
2023-02-14 15:17:56 -08:00

139 lines
4.6 KiB
Go

package terraform
import (
"sync"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
)
func TestNilHook_impl(t *testing.T) {
var _ Hook = new(NilHook)
}
// testHook is a Hook implementation that logs the calls it receives.
// It is intended for testing that core code is emitting the correct hooks
// for a given situation.
type testHook struct {
mu sync.Mutex
Calls []*testHookCall
}
var _ Hook = (*testHook)(nil)
// testHookCall represents a single call in testHook.
// This hook just logs string names to make it easy to write "want" expressions
// in tests that can DeepEqual against the real calls.
type testHookCall struct {
Action string
InstanceID string
}
func (h *testHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PreApply", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PostApply", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PreDiff", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PostDiff", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PreProvisionInstance", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PostProvisionInstance", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PreProvisionInstanceStep", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PostProvisionInstanceStep", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"ProvisionOutput", addr.String()})
}
func (h *testHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PreRefresh", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PostRefresh", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PreImportState", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PostImportState", addr.String()})
return HookActionContinue, nil
}
func (h *testHook) Stopping() {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"Stopping", ""})
}
func (h *testHook) PostStateUpdate(new *states.State) (HookAction, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.Calls = append(h.Calls, &testHookCall{"PostStateUpdate", ""})
return HookActionContinue, nil
}