class Bosh::AwsCloud::ResourceWait

Constants

DEFAULT_TRIES

a sane amount of retries on AWS (~25 minutes), as things can take anywhere between a minute and forever

DEFAULT_WAIT_ATTEMPTS
MAX_SLEEP_TIME

Public Class Methods

for_attachment(args) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 42
def self.for_attachment(args)
  attachment = args.fetch(:attachment) { raise ArgumentError, 'attachment object required' }
  target_state = args.fetch(:state) { raise ArgumentError, 'state symbol required' }
  valid_states = [:attached, :detached]
  validate_states(valid_states, target_state)

  ignored_errors = []
  if target_state == :attached
    ignored_errors << AWS::Core::Resource::NotFound
  end
  description = "volume %s to be %s to instance %s as device %s" % [
      attachment.volume.id, target_state, attachment.instance.id, attachment.device
  ]

  new.for_resource(resource: attachment, errors: ignored_errors, target_state: target_state, description: description) do |current_state|
    current_state == target_state
  end
rescue AWS::Core::Resource::NotFound
  # if an attachment is detached, AWS can reap the object and the reference is no longer found,
  # so consider this exception a success condition if we are detaching
  raise unless target_state == :detached
end
for_image(args) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 65
def self.for_image(args)
  image = args.fetch(:image) { raise ArgumentError, 'image object required' }
  target_state = args.fetch(:state) { raise ArgumentError, 'state symbol required' }
  valid_states = [:available, :deleted]
  validate_states(valid_states, target_state)

  ignored_errors = []
  if target_state == :available
    ignored_errors = [AWS::EC2::Errors::InvalidAMIID::NotFound]
  end

  new.for_resource(resource: image, errors: ignored_errors, target_state: target_state, state_method: :state) do |current_state|
    current_state == target_state
  end
rescue AWS::Core::Resource::NotFound
  # if an AMI is deleted, AWS can reap the object and the reference is no longer found,
  # so consider this exception a success condition if we are deleting
  raise unless target_state == :deleted
end
for_instance(args) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 13
def self.for_instance(args)
  raise ArgumentError, "args should be a Hash, but `#{args.class}' given" unless args.is_a?(Hash)
  instance = args.fetch(:instance) { raise ArgumentError, 'instance object required' }
  target_state = args.fetch(:state) { raise ArgumentError, 'state symbol required' }
  valid_states = [:running, :terminated]
  validate_states(valid_states, target_state)

  ignored_errors = [
      Bosh::Retryable::ErrorMatcher.new(
          AWS::Errors::ServerError,
          /The service is unavailable. Please try again shortly./,
      ),
  ]

  if target_state == :running
    ignored_errors << AWS::EC2::Errors::InvalidInstanceID::NotFound
    ignored_errors << AWS::Core::Resource::NotFound
  end

  new.for_resource(resource: instance, errors: ignored_errors, target_state: target_state) do |current_state|
    if target_state == :running && current_state == :terminated
      logger.error("instance #{instance.id} terminated while starting")
      raise Bosh::Clouds::VMCreationFailed.new(true)
    else
      current_state == target_state
    end
  end
end
for_sgroup(args) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 124
def self.for_sgroup(args)
  sgroup = args.fetch(:sgroup) { raise ArgumentError, 'sgroup object required' }
  target_state = args.fetch(:state) { raise ArgumentError, 'state symbol required' }
  valid_states = [true, false]
  validate_states(valid_states, target_state)

  new.for_resource(resource: sgroup, target_state: true, state_method: :exists?) do |current_state|
    current_state == target_state
  end
end
for_snapshot(args) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 100
def self.for_snapshot(args)
  snapshot = args.fetch(:snapshot) { raise ArgumentError, 'snapshot object required' }
  target_state = args.fetch(:state) { raise ArgumentError, 'state symbol required' }
  valid_states = [:completed]
  validate_states(valid_states, target_state)

  new.for_resource(resource: snapshot, target_state: target_state) do |current_state|
    current_state == target_state
  end
end
for_subnet(args) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 111
def self.for_subnet(args)
  subnet = args.fetch(:subnet) { raise ArgumentError, 'subnet object required' }
  target_state = args.fetch(:state) { raise ArgumentError, 'state symbol required' }
  valid_states = [:available]
  validate_states(valid_states, target_state)

  ignored_errors = [AWS::EC2::Errors::InvalidSubnetID::NotFound]

  new.for_resource(resource: subnet, target_state: target_state, errors: ignored_errors, state_method: :state) do |current_state|
    current_state == target_state
  end
end
for_volume(args) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 85
def self.for_volume(args)
  volume = args.fetch(:volume) { raise ArgumentError, 'volume object required' }
  target_state = args.fetch(:state) { raise ArgumentError, 'state symbol required' }
  valid_states = [:available, :deleted]
  validate_states(valid_states, target_state)

  new.for_resource(resource: volume, target_state: target_state) do |current_state|
    current_state == target_state
  end
rescue AWS::EC2::Errors::InvalidVolume::NotFound
  # if an volume is deleted, AWS can reap the object and the reference is no longer found,
  # so consider this exception a success condition if we are deleting
  raise unless target_state == :deleted
end
logger() click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 159
def self.logger
  Bosh::Clouds::Config.logger
end
new() click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 163
def initialize
  @started_at = Time.now
end
sleep_callback(description, options) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 141
def self.sleep_callback(description, options)
  max_sleep_time = options.fetch(:max, MAX_SLEEP_TIME)
  lambda do |num_tries, error|
    if options[:tries_before_max] && num_tries >= options[:tries_before_max]
      time = max_sleep_time
    else
      if options[:exponential]
        time = [options[:interval] ** num_tries, max_sleep_time].min
      else
        time = [1 + options[:interval] * num_tries, max_sleep_time].min
      end
    end
    Bosh::AwsCloud::ResourceWait.logger.debug("#{error.class}: `#{error.message}'") if error
    Bosh::AwsCloud::ResourceWait.logger.debug("#{description}, retrying in #{time} seconds (#{num_tries}/#{options[:total]})")
    time
  end
end
validate_states(valid_states, target_state) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 135
def self.validate_states(valid_states, target_state)
  unless valid_states.include?(target_state)
    raise ArgumentError, "target state must be one of #{valid_states.join(', ')}, `#{target_state}' given"
  end
end

Public Instance Methods

for_resource(args, &blk) click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 167
def for_resource(args, &blk)
  resource = args.fetch(:resource)
  state_method = args.fetch(:state_method, :status)
  errors = args.fetch(:errors, [])
  desc = args.fetch(:description) { resource.id }
  tries = args.fetch(:tries, DEFAULT_TRIES).to_i
  target_state = args.fetch(:target_state)

  sleep_cb = self.class.sleep_callback(
    "Waiting for #{desc} to be #{target_state}",
    { interval: 2, total: tries, max: 32, exponential: true }
  )
  errors << AWS::EC2::Errors::RequestLimitExceeded
  ensure_cb = Proc.new do |retries|
    cloud_error("Timed out waiting for #{desc} to be #{target_state}, took #{time_passed}s") if retries == tries
  end

  state = nil
  Bosh::Retryable.new(tries: tries, sleep: sleep_cb, on: errors, ensure: ensure_cb).retryer do

    state = resource.method(state_method).call
    if state == :error || state == :failed
      raise Bosh::Clouds::CloudError, "#{desc} state is #{state}, expected #{target_state}, took #{time_passed}s"
    end

    # the yielded block should return true if we have reached the target state
    blk.call(state)
  end

  Bosh::AwsCloud::ResourceWait.logger.info("#{desc} is now #{state}, took #{time_passed}s")
rescue Bosh::Common::RetryCountExceeded => e
  Bosh::AwsCloud::ResourceWait.logger.error(
    "Timed out waiting for #{desc} state is #{state}, expected to be #{target_state}, took #{time_passed}s")
  raise e
end
time_passed() click to toggle source
# File lib/cloud/aws/resource_wait.rb, line 203
def time_passed
  Time.now - @started_at
end