Skip to content
This repository was archived by the owner on Jan 21, 2020. It is now read-only.

Commit 5e53234

Browse files
author
David Chung
committed
Max resource lifetime
Signed-off-by: David Chung <[email protected]>
1 parent fd983ac commit 5e53234

File tree

5 files changed

+271
-10
lines changed

5 files changed

+271
-10
lines changed

cmd/infrakit/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
_ "github.com/docker/infrakit/cmd/infrakit/remote"
3333
_ "github.com/docker/infrakit/cmd/infrakit/template"
3434
_ "github.com/docker/infrakit/cmd/infrakit/util"
35+
_ "github.com/docker/infrakit/cmd/infrakit/x"
3536
)
3637

3738
func init() {

cmd/infrakit/util/util.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ func Command(plugins func() discovery.Plugins) *cobra.Command {
2121
Short: "Utilties",
2222
}
2323

24-
util.AddCommand(muxCommand(plugins), fileServerCommand(plugins), trackCommand(plugins))
24+
util.AddCommand(
25+
muxCommand(plugins),
26+
fileServerCommand(plugins),
27+
trackCommand(plugins),
28+
)
2529

2630
return util
2731
}

cmd/infrakit/x/maxlife.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package x
2+
3+
import (
4+
"os"
5+
"strings"
6+
"time"
7+
8+
"github.com/docker/infrakit/pkg/discovery"
9+
"github.com/docker/infrakit/pkg/plugin"
10+
instance_rpc "github.com/docker/infrakit/pkg/rpc/instance"
11+
"github.com/docker/infrakit/pkg/spi/instance"
12+
"github.com/docker/infrakit/pkg/types"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
func maxlifeCommand(plugins func() discovery.Plugins) *cobra.Command {
17+
18+
cmd := &cobra.Command{
19+
Use: "maxlife <instance plugin name>...",
20+
Short: "Sets max life on the given instances",
21+
}
22+
23+
//name := cmd.Flags().String("name", "", "Name to use as name of this plugin")
24+
poll := cmd.Flags().DurationP("poll", "i", 10*time.Second, "Polling interval")
25+
maxlife := cmd.Flags().DurationP("maxlife", "m", 10*time.Minute, "Max lifetime of the resource")
26+
flagTags := cmd.Flags().StringSliceP("tag", "t", []string{}, "Tags to filter instance by")
27+
28+
cmd.RunE = func(c *cobra.Command, args []string) error {
29+
30+
if len(args) == 0 {
31+
cmd.Usage()
32+
os.Exit(-1)
33+
}
34+
35+
tags := toTags(*flagTags)
36+
37+
// Now we have a list of instance plugins to maxlife
38+
plugins, err := getInstancePlugins(plugins, args)
39+
if err != nil {
40+
return err
41+
}
42+
43+
// For each we start a goroutine to poll and kill instances
44+
stops := []chan struct{}{}
45+
46+
retry := false
47+
48+
loop:
49+
for {
50+
for name, plugin := range plugins {
51+
52+
stop := make(chan struct{})
53+
stops = append(stops, stop)
54+
55+
described, err := plugin.DescribeInstances(tags, false)
56+
if err != nil {
57+
log.Warn("cannot get initial count", "name", name, "err", err)
58+
retry = true
59+
}
60+
61+
go ensureMaxlife(name, plugin, stop, *poll, *maxlife, tags, len(described))
62+
}
63+
64+
if !retry {
65+
break loop
66+
}
67+
68+
// Wait a little bit before trying again -- use the same poll interval
69+
<-time.After(*poll)
70+
}
71+
72+
return nil
73+
}
74+
75+
return cmd
76+
}
77+
78+
func age(instance instance.Description, now time.Time) (age time.Duration) {
79+
link := types.NewLinkFromMap(instance.Tags)
80+
if link.Valid() {
81+
age = now.Sub(link.Created())
82+
}
83+
return
84+
}
85+
86+
func maxAge(instances []instance.Description, now time.Time) instance.Description {
87+
// check to see if the tags of the instances have links. Links have a creation date and
88+
// we can use it to compute the age
89+
var max time.Duration
90+
var found = 0
91+
for i, instance := range instances {
92+
age := age(instance, now)
93+
if age > max {
94+
max = age
95+
found = i
96+
}
97+
}
98+
return instances[found]
99+
}
100+
101+
func ensureMaxlife(name string, plugin instance.Plugin, stop chan struct{}, poll, maxlife time.Duration,
102+
tags map[string]string, initialCount int) {
103+
104+
// Count is used to track the steady state... we don't want to keep killing instances
105+
// if the counts are steadily decreasing. The idea here is that once we terminate a resource
106+
// another one will be resurrected so we will be back to steady state.
107+
// Of course it's possible that the size of the cluster actually is decreased. So we'd
108+
// wait for a few samples to get to steady state before we terminate another instance.
109+
// Currently we assume damping == 1 or 1 successive samples of delta >= 0 is sufficient to terminate
110+
// another instance.
111+
112+
last := initialCount
113+
tick := time.Tick(poll)
114+
loop:
115+
for {
116+
117+
select {
118+
119+
case now := <-tick:
120+
121+
described, err := plugin.DescribeInstances(tags, false)
122+
if err != nil {
123+
// Transient error?
124+
log.Warn("error describing instances", "name", name, "err", err)
125+
continue
126+
}
127+
128+
// TODO -- we should compute the 2nd derivative wrt time to make sure we
129+
// are truly in a steady state...
130+
131+
current := len(described)
132+
delta := current - last
133+
last = current
134+
135+
if current < 2 {
136+
log.Info("there are less than 2 instances. No actions.", "name", name)
137+
continue
138+
}
139+
140+
if delta < 0 {
141+
// Don't do anything if there are fewer instances at this iteration
142+
// than the last. We want to wait until steady state
143+
log.Info("fewer instances in this round. No actions taken", "name", name)
144+
continue
145+
}
146+
147+
// Just pick a single oldest instance per polling cycle. This is so
148+
// that we don't end up destroying the cluster by taking down too many instances
149+
// all at once.
150+
oldest := maxAge(described, now)
151+
152+
// check to make sure the age is over the maxlife
153+
if age(oldest, now) > maxlife {
154+
// terminate it and hope the group controller restores with a new intance
155+
err = plugin.Destroy(oldest.ID)
156+
if err != nil {
157+
log.Warn("cannot destroy instance", "name", name, "id", oldest.ID, "err", err)
158+
continue
159+
}
160+
}
161+
162+
case <-stop:
163+
log.Info("stop requested", "name", name)
164+
break loop
165+
}
166+
}
167+
168+
log.Info("maxlife stopped", "name", name)
169+
return
170+
}
171+
172+
func toTags(slice []string) map[string]string {
173+
tags := map[string]string{}
174+
175+
for _, tag := range slice {
176+
kv := strings.SplitN(tag, "=", 2)
177+
if len(kv) != 2 {
178+
log.Warn("bad format tag", "input", tag)
179+
continue
180+
}
181+
key := strings.TrimSpace(kv[0])
182+
val := strings.TrimSpace(kv[1])
183+
if key != "" && val != "" {
184+
tags[key] = val
185+
}
186+
}
187+
return tags
188+
}
189+
190+
func getInstancePlugins(plugins func() discovery.Plugins, names []string) (map[string]instance.Plugin, error) {
191+
targets := map[string]instance.Plugin{}
192+
for _, target := range names {
193+
endpoint, err := plugins().Find(plugin.Name(target))
194+
if err != nil {
195+
return nil, err
196+
}
197+
if p, err := instance_rpc.NewClient(plugin.Name(target), endpoint.Address); err == nil {
198+
targets[target] = p
199+
} else {
200+
return nil, err
201+
}
202+
}
203+
return targets, nil
204+
}

cmd/infrakit/x/x.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package x
2+
3+
import (
4+
"github.com/docker/infrakit/cmd/infrakit/base"
5+
"github.com/docker/infrakit/pkg/discovery"
6+
logutil "github.com/docker/infrakit/pkg/log"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var log = logutil.New("module", "cli/x")
11+
12+
func init() {
13+
base.Register(Command)
14+
}
15+
16+
// Command is the head of this module
17+
func Command(plugins func() discovery.Plugins) *cobra.Command {
18+
19+
experimental := &cobra.Command{
20+
Use: "x",
21+
Short: "Experimental features",
22+
}
23+
24+
experimental.AddCommand(
25+
maxlifeCommand(plugins),
26+
)
27+
28+
return experimental
29+
}

pkg/types/link.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,42 @@ func init() {
2020
type Link struct {
2121
value string
2222
context string
23+
created time.Time
2324
}
2425

2526
// NewLink creates a link
2627
func NewLink() *Link {
2728
return &Link{
28-
value: randomAlphaNumericString(16),
29+
value: randomAlphaNumericString(16),
30+
created: time.Now(),
2931
}
3032
}
3133

32-
// NewLinkFromMap constructs a link from data in the map
34+
// Link related labels
35+
const (
36+
LinkLabel = "infrakit-link"
37+
LinkContextLabel = "infrakit-link-context"
38+
LinkCreatedLabel = "infrakit-link-created"
39+
)
40+
41+
// NewLinkFromMap constructs a link from data in the map. The link will have missing data
42+
// if the input does not contain the attribute labels.
3343
func NewLinkFromMap(m map[string]string) *Link {
3444
l := &Link{}
35-
if v, has := m["infrakit-link"]; has {
45+
if v, has := m[LinkLabel]; has {
3646
l.value = v
3747
}
3848

39-
if v, has := m["infrakit-link-context"]; has {
49+
if v, has := m[LinkContextLabel]; has {
4050
l.context = v
4151
}
52+
53+
if v, has := m[LinkCreatedLabel]; has {
54+
t, err := time.Parse(time.RFC3339, v)
55+
if err == nil {
56+
l.created = t
57+
}
58+
}
4259
return l
4360
}
4461

@@ -52,9 +69,14 @@ func (l Link) Value() string {
5269
return l.value
5370
}
5471

72+
// Created returns the creation time of the link
73+
func (l Link) Created() time.Time {
74+
return l.created
75+
}
76+
5577
// Label returns the label to look for the link
5678
func (l Link) Label() string {
57-
return "infrakit-link"
79+
return LinkLabel
5880
}
5981

6082
// Context returns the context of the link
@@ -80,8 +102,9 @@ func (l *Link) KVPairs() []string {
80102
// Map returns a representation that is easily converted to JSON or YAML
81103
func (l *Link) Map() map[string]string {
82104
return map[string]string{
83-
"infrakit-link": l.value,
84-
"infrakit-link-context": l.context,
105+
LinkLabel: l.value,
106+
LinkContextLabel: l.context,
107+
LinkCreatedLabel: l.created.Format(time.RFC3339),
85108
}
86109
}
87110

@@ -94,15 +117,15 @@ func (l *Link) WriteMap(target map[string]string) {
94117

95118
// InMap returns true if the link is contained in the map
96119
func (l *Link) InMap(m map[string]string) bool {
97-
c, has := m["infrakit-link-context"]
120+
c, has := m[LinkContextLabel]
98121
if !has {
99122
return false
100123
}
101124
if c != l.context {
102125
return false
103126
}
104127

105-
v, has := m["infrakit-link"]
128+
v, has := m[LinkLabel]
106129
if !has {
107130
return false
108131
}

0 commit comments

Comments
 (0)