"fmt"
"math/big"
"sync"
+ "sync/atomic"
+ "time"
"git.arvados.org/arvados.git/lib/cloud"
"git.arvados.org/arvados.git/sdk/go/arvados"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
+ "github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/sirupsen/logrus"
// Driver is the ec2 implementation of the cloud.Driver interface.
var Driver = cloud.DriverFunc(newEC2InstanceSet)
+const (
+ throttleDelayMin = time.Second
+ throttleDelayMax = time.Minute
+)
+
type ec2InstanceSetConfig struct {
AccessKeyID string
SecretAccessKey string
}
type ec2InstanceSet struct {
- ec2config ec2InstanceSetConfig
- instanceSetID cloud.InstanceSetID
- logger logrus.FieldLogger
- client ec2Interface
- keysMtx sync.Mutex
- keys map[string]string
+ ec2config ec2InstanceSetConfig
+ instanceSetID cloud.InstanceSetID
+ logger logrus.FieldLogger
+ client ec2Interface
+ keysMtx sync.Mutex
+ keys map[string]string
+ throttleDelayCreate atomic.Value
+ throttleDelayInstances atomic.Value
}
func newEC2InstanceSet(config json.RawMessage, instanceSetID cloud.InstanceSetID, _ cloud.SharedResourceTags, logger logrus.FieldLogger) (prv cloud.InstanceSet, err error) {
}
rsv, err := instanceSet.client.RunInstances(&rii)
-
+ err = wrapError(err, &instanceSet.throttleDelayCreate)
if err != nil {
return nil, err
}
dii := &ec2.DescribeInstancesInput{Filters: filters}
for {
dio, err := instanceSet.client.DescribeInstances(dii)
+ err = wrapError(err, &instanceSet.throttleDelayInstances)
if err != nil {
return nil, err
}
func (inst *ec2Instance) VerifyHostKey(ssh.PublicKey, *ssh.Client) error {
return cloud.ErrNotImplemented
}
+
+type rateLimitError struct {
+ error
+ earliestRetry time.Time
+}
+
+func (err rateLimitError) EarliestRetry() time.Time {
+ return err.earliestRetry
+}
+
+func wrapError(err error, throttleValue *atomic.Value) error {
+ if request.IsErrorThrottle(err) {
+ // Back off exponentially until an upstream call
+ // either succeeds or returns a non-throttle error.
+ d, _ := throttleValue.Load().(time.Duration)
+ d = d*3/2 + time.Second
+ if d < throttleDelayMin {
+ d = throttleDelayMin
+ } else if d > throttleDelayMax {
+ d = throttleDelayMax
+ }
+ throttleValue.Store(d)
+ return rateLimitError{error: err, earliestRetry: time.Now().Add(d)}
+ } else if err != nil {
+ throttleValue.Store(time.Duration(0))
+ return err
+ }
+ throttleValue.Store(time.Duration(0))
+ return nil
+}
ticker *time.Ticker
}
+func (is rateLimitedInstanceSet) Instances(tags cloud.InstanceTags) ([]cloud.Instance, error) {
+ <-is.ticker.C
+ insts, err := is.InstanceSet.Instances(tags)
+ for i, inst := range insts {
+ insts[i] = &rateLimitedInstance{inst, is.ticker}
+ }
+ return insts, err
+}
+
func (is rateLimitedInstanceSet) Create(it arvados.InstanceType, image cloud.ImageID, tags cloud.InstanceTags, init cloud.InitCommand, pk ssh.PublicKey) (cloud.Instance, error) {
<-is.ticker.C
inst, err := is.InstanceSet.Create(it, image, tags, init, pk)
return inst.Instance.Destroy()
}
+func (inst *rateLimitedInstance) SetTags(tags cloud.InstanceTags) error {
+ <-inst.ticker.C
+ return inst.Instance.SetTags(tags)
+}
+
// Adds the specified defaultTags to every Create() call.
type defaultTaggingInstanceSet struct {
cloud.InstanceSet