Skip to content

Commit

Permalink
states: New SyncState type
Browse files Browse the repository at this point in the history
This is a wrapper around State that is able to perform higher-level
manipulations (at the granularity of the entire state) in a
concurrency-safe manner, using the lower-level APIs exposed by State and
all of the types it contains.

The granularity of a SyncState operation roughly matches the granularity
off a state-related EvalNode in the "terraform" package, performing a
sequence of more primitive operations while guaranteeing atomicity of the
entire change.

As a compromise for convenience of usage, it's still possible to access
the individual state data objects via this API, but they are always copied
before returning to ensure that two distinct callers cannot have data
races. Callers should access the most granular object possible for their
operation.
  • Loading branch information
apparentlymart committed Oct 17, 2018
1 parent 53cafc5 commit a33f941
Show file tree
Hide file tree
Showing 5 changed files with 378 additions and 18 deletions.
31 changes: 23 additions & 8 deletions states/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,14 @@ func (ms *Module) SetResourceInstanceDeposed(addr addrs.ResourceInstance, key De
}
}

// DeposeResourceInstanceObject moves the current instance object for the
// given resource instance address into the deposed set, leaving the instance
// without a current object.
//
// The return value is the newly-allocated deposed key, or NotDeposed if the
// given instance is already lacking a current object.
func (ms *Module) DeposeResourceInstanceObject(addr addrs.ResourceInstance) DeposedKey {
// deposeResourceInstanceObject is the real implementation of
// SyncState.DeposeResourceInstanceObject.
func (ms *Module) deposeResourceInstanceObject(addr addrs.ResourceInstance) DeposedKey {
is := ms.ResourceInstance(addr)
if is == nil {
return NotDeposed
}
return is.DeposeCurrentObject()
return is.deposeCurrentObject()
}

// SetOutputValue writes an output value into the state, overwriting any
Expand Down Expand Up @@ -190,3 +186,22 @@ func (ms *Module) SetLocalValue(name string, value cty.Value) {
func (ms *Module) RemoveLocalValue(name string) {
delete(ms.LocalValues, name)
}

// empty returns true if the receving module state is contributing nothing
// to the state. In other words, it returns true if the module could be
// removed from the state altogether without changing the meaning of the state.
//
// In practice a module containing no objects is the same as a non-existent
// module, and so we can opportunistically clean up once a module becomes
// empty on the assumption that it will be re-added if needed later.
func (ms *Module) empty() bool {
if ms == nil {
return true
}

// This must be updated to cover any new collections added to Module
// in future.
return (len(ms.Resources) == 0 &&
len(ms.OutputValues) == 0 &&
len(ms.LocalValues) == 0)
}
12 changes: 5 additions & 7 deletions states/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,11 @@ func (i *ResourceInstance) HasObjects() bool {
return i.Current != nil || len(i.Deposed) != 0
}

// DeposeCurrentObject moves the current generation object, if present, into
// the deposed set. After this method returns, the instance has no current
// object.
//
// The return value is either the newly-allocated deposed key, or NotDeposed
// if the instance is already lacking a current instance object.
func (i *ResourceInstance) DeposeCurrentObject() DeposedKey {
// deposeCurrentObject is part of the real implementation of
// SyncState.DeposeResourceInstanceObject. The exported method uses a lock
// to ensure that we can safely allocate an unused deposed key without
// collision.
func (i *ResourceInstance) deposeCurrentObject() DeposedKey {
if !i.HasCurrent() {
return NotDeposed
}
Expand Down
4 changes: 2 additions & 2 deletions states/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestResourceInstanceDeposeCurrentObject(t *testing.T) {
var dk DeposedKey

t.Run("first depose", func(t *testing.T) {
dk = is.DeposeCurrentObject() // dk is randomly-generated but should be eight characters long
dk = is.deposeCurrentObject() // dk is randomly-generated but should be eight characters long
t.Logf("deposedKey is %q", dk)

if got := is.Current; got != nil {
Expand All @@ -33,7 +33,7 @@ func TestResourceInstanceDeposeCurrentObject(t *testing.T) {
})

t.Run("second depose", func(t *testing.T) {
notDK := is.DeposeCurrentObject()
notDK := is.deposeCurrentObject()
if notDK != NotDeposed {
t.Errorf("got deposedKey %q; want NotDeposed", notDK)
}
Expand Down
28 changes: 27 additions & 1 deletion states/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
// Access to State and the nested values within it is not concurrency-safe,
// so when accessing a State object concurrently it is the caller's
// responsibility to ensure that only one write is in progress at a time
// and that reads only occur when no write is in progress.
// and that reads only occur when no write is in progress. The most common
// way to acheive this is to wrap the State in a SyncState and use the
// higher-level atomic operations supported by that type.
type State struct {
// Modules contains the state for each module. The keys in this map are
// an implementation detail and must not be used by outside callers.
Expand All @@ -36,6 +38,23 @@ func (s *State) Module(addr addrs.ModuleInstance) *Module {
return s.Modules[addr.String()]
}

// RemoveModule removes the module with the given address from the state,
// unless it is the root module. The root module cannot be deleted, and so
// this method will panic if that is attempted.
//
// Removing a module implicitly discards all of the resources, outputs and
// local values within it, and so this should usually be done only for empty
// modules. For callers accessing the state through a SyncState wrapper, modules
// are automatically pruned if they are empty after one of their contained
// elements is removed.
func (s *State) RemoveModule(addr addrs.ModuleInstance) {
if addr.IsRoot() {
panic("attempted to remote root module")
}

delete(s.Modules, addr.String())
}

// RootModule is a convenient alias for Module(addrs.RootModuleInstance).
func (s *State) RootModule() *Module {
return s.Modules[addrs.RootModuleInstance.String()]
Expand Down Expand Up @@ -94,3 +113,10 @@ func (s *State) LocalValue(addr addrs.AbsLocalValue) cty.Value {
}
return ms.LocalValues[addr.LocalValue.Name]
}

// SyncWrapper returns a SyncState object wrapping the receiver.
func (s *State) SyncWrapper() *SyncState {
return &SyncState{
state: s,
}
}
Loading

0 comments on commit a33f941

Please sign in to comment.