Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: close provider/provisioner connections when not used anymore #2406

Merged
merged 2 commits into from
Jun 23, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions rpc/mux_broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package rpc
import (
"encoding/binary"
"fmt"
"math/rand"
"net"
"sync"
"sync/atomic"
"time"

"github.com/hashicorp/yamux"
Expand All @@ -17,7 +17,6 @@ import (
// or accept a connection from, and the broker handles the details of
// holding these channels open while they're being negotiated.
type muxBroker struct {
nextId uint32
session *yamux.Session
streams map[uint32]*muxBrokerPending

Expand Down Expand Up @@ -95,9 +94,12 @@ func (m *muxBroker) Dial(id uint32) (net.Conn, error) {
return stream, nil
}

// NextId returns a unique ID to use next.
// NextId returns a unique ID to use next. There is no need for seeding the
// rand package as the returned ID's aren't stored or used anywhere outside
// the current runtime. So it's perfectly fine to get the same pseudo-random
// numbers each time terraform is running.
func (m *muxBroker) NextId() uint32 {
return atomic.AddUint32(&m.nextId, 1)
return rand.Uint32()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces a (very low) possibility of the ID repeating, no? Which would be problematic for an identifier?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It are pseudo-random numbers and there are 4.294.967.295 of numbers to hand out. That means it would be extremely unlikely to get the same number within just a few numbers of each other.

And an ID may very wel be reused as soon as the previous command that was using the ID is closed by the new close provider. Having just a counter going up would mean there is hard limit of the maximum amount of commands that can be executed. And even through 4.294.967.295 is quite a lot, if you use TF in an always on setup it will hit that limit sooner or later.

But if we think this could be an issue, the only additional thing I can think of is to make a map with generated numbers and a timestamp (something like a map[uint32]Time) and then whenever a new number is generated we first check if the number was already handed out. But then additionally it should also delete entries that are older then x time, where x would be something we would agree on. I would say if you would like to make it extremely safe make it 60 minutes. There is no single apply/plan/refresh that takes 60 minutes right? Doing the cleanup could then be triggered only when NextId() is called, so it doesn't require a scheduled thing of it's own to periodically check and cleanup.

Sounds like maybe a good add-on, let me know what you think and if you think it's needed...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked about this on IRC, but mirroring here - I think we're fine to punt on this for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will have a look tomorrow at how much impact a solution as discussed would have. The more I think about it the more I believe it wouldn't hurt and would make 99,99% a 100% which is always better of course 😉

But if so, I'll open a new PR for that one...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would feel better about reinstating the atomic increment. Packer/Terraform (both use this system, but not the same code) open a LOT of channels. Hundreds. While the chance is low, if it did conflict, it would be disastrous. LIkely an immediate panic at some point.

The atomic increment isn't a hard limit, we just expect it to wrap at some point (being a signed integer).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point and worth potentially opening in Yamux. But I think on our side we use it for other reasons.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll open an issue there to discuss this, but I can imagine use cases where this check would actually make sense. For Terraform it doesn't as the channels aren't usually used for more then a couple of minutes...

So would something like this be a workaround until (if) the logic in yamux can be changed?

func (m *muxBroker) NextId() uint32 {
  if atomic.LoadUint32(&m.nextid) == math.MaxUint32-10 {
    atomic.StoreUint32(&m.nextid, 0)
  }
  return atomic.AddUint32(&m.nextId, 1)
}

Maybe still a little dirty, but this would do the trick right?

Edit: With some comments explaining why that is done of course.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 This seems like a good compromise for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for not picking this up today. Had a horrable (non productive) day with a broken macbook.

Will do some tests and make a PR for this tomorrow so we can get it in (if all is well of course) before shipping 0.6.0...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yeah, that would be ideal!

}

// Run starts the brokering and should be executed in a goroutine, since it
Expand Down
4 changes: 4 additions & 0 deletions rpc/resource_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ func (p *ResourceProvider) Resources() []terraform.ResourceType {
return result
}

func (p *ResourceProvider) Close() error {
return p.Client.Close()
}

// ResourceProviderServer is a net/rpc compatible structure for serving
// a ResourceProvider. This should not be used directly.
type ResourceProviderServer struct {
Expand Down
28 changes: 28 additions & 0 deletions rpc/resource_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,31 @@ func TestResourceProvider_validateResource_warns(t *testing.T) {
t.Fatalf("bad: %#v", w)
}
}

func TestResourceProvider_close(t *testing.T) {
client, _ := testNewClientServer(t)
defer client.Close()

provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}

var p interface{}
p = provider
pCloser, ok := p.(terraform.ResourceProviderCloser)
if !ok {
t.Fatal("should be a ResourceProviderCloser")
}

if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provider: %s", err)
}

// The connection should be closed now, so if we to make a
// new call we should get an error.
err = provider.Configure(&terraform.ResourceConfig{})
if err == nil {
t.Fatal("should have error")
}
}
4 changes: 4 additions & 0 deletions rpc/resource_provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ func (p *ResourceProvisioner) Apply(
return err
}

func (p *ResourceProvisioner) Close() error {
return p.Client.Close()
}

type ResourceProvisionerValidateArgs struct {
Config *terraform.ResourceConfig
}
Expand Down
31 changes: 31 additions & 0 deletions rpc/resource_provisioner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,34 @@ func TestResourceProvisioner_validate_warns(t *testing.T) {
t.Fatalf("bad: %#v", w)
}
}

func TestResourceProvisioner_close(t *testing.T) {
client, _ := testNewClientServer(t)
defer client.Close()

provisioner, err := client.ResourceProvisioner()
if err != nil {
t.Fatalf("err: %s", err)
}

var p interface{}
p = provisioner
pCloser, ok := p.(terraform.ResourceProvisionerCloser)
if !ok {
t.Fatal("should be a ResourceProvisionerCloser")
}

if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provisioner: %s", err)
}

// The connection should be closed now, so if we to make a
// new call we should get an error.
o := &terraform.MockUIOutput{}
s := &terraform.InstanceState{}
c := &terraform.ResourceConfig{}
err = provisioner.Apply(o, s, c)
if err == nil {
t.Fatal("should have error")
}
}
7 changes: 7 additions & 0 deletions terraform/eval_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type EvalContext interface {
// initialized) or returns nil if the provider isn't initialized.
Provider(string) ResourceProvider

// CloseProvider closes provider connections that aren't needed anymore.
CloseProvider(string) error

// ConfigureProvider configures the provider with the given
// configuration. This is a separate context call because this call
// is used to store the provider configuration for inheritance lookups
Expand All @@ -51,6 +54,10 @@ type EvalContext interface {
// initialized) or returns nil if the provisioner isn't initialized.
Provisioner(string) ResourceProvisioner

// CloseProvisioner closes provisioner connections that aren't needed
// anymore.
CloseProvisioner(string) error

// Interpolate takes the given raw configuration and completes
// the interpolations, returning the processed ResourceConfig.
//
Expand Down
44 changes: 44 additions & 0 deletions terraform/eval_context_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,28 @@ func (ctx *BuiltinEvalContext) Provider(n string) ResourceProvider {
return ctx.ProviderCache[PathCacheKey(providerPath)]
}

func (ctx *BuiltinEvalContext) CloseProvider(n string) error {
ctx.once.Do(ctx.init)

ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()

providerPath := make([]string, len(ctx.Path())+1)
copy(providerPath, ctx.Path())
providerPath[len(providerPath)-1] = n

var provider interface{}
provider = ctx.ProviderCache[PathCacheKey(providerPath)]
if provider != nil {
if p, ok := provider.(ResourceProviderCloser); ok {
delete(ctx.ProviderCache, PathCacheKey(providerPath))
return p.Close()
}
}

return nil
}

func (ctx *BuiltinEvalContext) ConfigureProvider(
n string, cfg *ResourceConfig) error {
p := ctx.Provider(n)
Expand Down Expand Up @@ -222,6 +244,28 @@ func (ctx *BuiltinEvalContext) Provisioner(n string) ResourceProvisioner {
return ctx.ProvisionerCache[PathCacheKey(provPath)]
}

func (ctx *BuiltinEvalContext) CloseProvisioner(n string) error {
ctx.once.Do(ctx.init)

ctx.ProvisionerLock.Lock()
defer ctx.ProvisionerLock.Unlock()

provPath := make([]string, len(ctx.Path())+1)
copy(provPath, ctx.Path())
provPath[len(provPath)-1] = n

var prov interface{}
prov = ctx.ProvisionerCache[PathCacheKey(provPath)]
if prov != nil {
if p, ok := prov.(ResourceProvisionerCloser); ok {
delete(ctx.ProvisionerCache, PathCacheKey(provPath))
return p.Close()
}
}

return nil
}

func (ctx *BuiltinEvalContext) Interpolate(
cfg *config.RawConfig, r *Resource) (*ResourceConfig, error) {
if cfg != nil {
Expand Down
20 changes: 20 additions & 0 deletions terraform/eval_context_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type MockEvalContext struct {
ProviderName string
ProviderProvider ResourceProvider

CloseProviderCalled bool
CloseProviderName string
CloseProviderProvider ResourceProvider

ProviderInputCalled bool
ProviderInputName string
ProviderInputConfig map[string]interface{}
Expand Down Expand Up @@ -55,6 +59,10 @@ type MockEvalContext struct {
ProvisionerName string
ProvisionerProvisioner ResourceProvisioner

CloseProvisionerCalled bool
CloseProvisionerName string
CloseProvisionerProvisioner ResourceProvisioner

InterpolateCalled bool
InterpolateConfig *config.RawConfig
InterpolateResource *Resource
Expand Down Expand Up @@ -105,6 +113,12 @@ func (c *MockEvalContext) Provider(n string) ResourceProvider {
return c.ProviderProvider
}

func (c *MockEvalContext) CloseProvider(n string) error {
c.CloseProviderCalled = true
c.CloseProviderName = n
return nil
}

func (c *MockEvalContext) ConfigureProvider(n string, cfg *ResourceConfig) error {
c.ConfigureProviderCalled = true
c.ConfigureProviderName = n
Expand Down Expand Up @@ -150,6 +164,12 @@ func (c *MockEvalContext) Provisioner(n string) ResourceProvisioner {
return c.ProvisionerProvisioner
}

func (c *MockEvalContext) CloseProvisioner(n string) error {
c.CloseProvisionerCalled = true
c.CloseProvisionerName = n
return nil
}

func (c *MockEvalContext) Interpolate(
config *config.RawConfig, resource *Resource) (*ResourceConfig, error) {
c.InterpolateCalled = true
Expand Down
11 changes: 11 additions & 0 deletions terraform/eval_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ func (n *EvalInitProvider) Eval(ctx EvalContext) (interface{}, error) {
return ctx.InitProvider(n.Name)
}

// EvalCloseProvider is an EvalNode implementation that closes provider
// connections that aren't needed anymore.
type EvalCloseProvider struct {
Name string
}

func (n *EvalCloseProvider) Eval(ctx EvalContext) (interface{}, error) {
ctx.CloseProvider(n.Name)
return nil, nil
}

// EvalGetProvider is an EvalNode implementation that retrieves an already
// initialized provider instance for the given name.
type EvalGetProvider struct {
Expand Down
16 changes: 16 additions & 0 deletions terraform/eval_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,22 @@ func TestEvalInitProvider(t *testing.T) {
}
}

func TestEvalCloseProvider(t *testing.T) {
n := &EvalCloseProvider{Name: "foo"}
provider := &MockResourceProvider{}
ctx := &MockEvalContext{CloseProviderProvider: provider}
if _, err := n.Eval(ctx); err != nil {
t.Fatalf("err: %s", err)
}

if !ctx.CloseProviderCalled {
t.Fatal("should be called")
}
if ctx.CloseProviderName != "foo" {
t.Fatalf("bad: %#v", ctx.CloseProviderName)
}
}

func TestEvalGetProvider_impl(t *testing.T) {
var _ EvalNode = new(EvalGetProvider)
}
Expand Down
11 changes: 11 additions & 0 deletions terraform/eval_provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ func (n *EvalInitProvisioner) Eval(ctx EvalContext) (interface{}, error) {
return ctx.InitProvisioner(n.Name)
}

// EvalCloseProvisioner is an EvalNode implementation that closes provisioner
// connections that aren't needed anymore.
type EvalCloseProvisioner struct {
Name string
}

func (n *EvalCloseProvisioner) Eval(ctx EvalContext) (interface{}, error) {
ctx.CloseProvisioner(n.Name)
return nil, nil
}

// EvalGetProvisioner is an EvalNode implementation that retrieves an already
// initialized provisioner instance for the given name.
type EvalGetProvisioner struct {
Expand Down
16 changes: 16 additions & 0 deletions terraform/eval_provisioner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ func TestEvalInitProvisioner(t *testing.T) {
}
}

func TestEvalCloseProvisioner(t *testing.T) {
n := &EvalCloseProvisioner{Name: "foo"}
provisioner := &MockResourceProvisioner{}
ctx := &MockEvalContext{CloseProvisionerProvisioner: provisioner}
if _, err := n.Eval(ctx); err != nil {
t.Fatalf("err: %s", err)
}

if !ctx.CloseProvisionerCalled {
t.Fatal("should be called")
}
if ctx.CloseProvisionerName != "foo" {
t.Fatalf("bad: %#v", ctx.CloseProvisionerName)
}
}

func TestEvalGetProvisioner_impl(t *testing.T) {
var _ EvalNode = new(EvalGetProvisioner)
}
Expand Down
6 changes: 6 additions & 0 deletions terraform/evaltree_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,9 @@ func ProviderEvalTree(n string, config *config.RawConfig) EvalNode {

return &EvalSequence{Nodes: seq}
}

// CloseProviderEvalTree returns the evaluation tree for closing
// provider connections that aren't needed anymore.
func CloseProviderEvalTree(n string) EvalNode {
return &EvalCloseProvider{Name: n}
}
2 changes: 2 additions & 0 deletions terraform/graph_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,14 @@ func (b *BuiltinGraphBuilder) Steps(path []string) []GraphTransformer {
// Provider-related transformations
&MissingProviderTransformer{Providers: b.Providers},
&ProviderTransformer{},
&CloseProviderTransformer{},
&PruneProviderTransformer{},
&DisableProviderTransformer{},

// Provisioner-related transformations
&MissingProvisionerTransformer{Provisioners: b.Provisioners},
&ProvisionerTransformer{},
&CloseProvisionerTransformer{},
&PruneProvisionerTransformer{},

// Run our vertex-level transforms
Expand Down
Loading