class Alicorn::Scaler

Attributes

app_name[RW]
buffer[RW]
delay[RW]
dry_run[RW]
listener_address[RW]
listener_type[RW]
logger[RW]
max_workers[RW]
min_workers[RW]
sample_count[RW]
signal_delay[R]
target_ratio[RW]

Public Class Methods

new(options = {}) click to toggle source
# File lib/alicorn/scaler.rb, line 14
def initialize(options = {})
  @min_workers        = options[:min_workers]         || 1
  @max_workers        = options[:max_workers]
  @target_ratio       = options[:target_ratio]        || 1.3
  @buffer             = options[:buffer]              || 2
  @listener_type      = options[:listener_type]       || "tcp"
  @listener_address   = options[:listener_address]    || "0.0.0.0:80"
  @delay              = options[:delay]               || 1
  @sample_count       = options[:sample_count]        || 30
  @app_name           = options[:app_name]            || "unicorn"
  @dry_run            = options[:dry_run]
  @signal_delay       = 0.5
  log_path            = options[:log_path]            || "/dev/null"

  self.logger = Logger.new(log_path)
  logger.level = options[:verbose] ? Logger::DEBUG : Logger::WARN
end

Public Instance Methods

auto_scale(data, worker_count) click to toggle source
# File lib/alicorn/scaler.rb, line 51
def auto_scale(data, worker_count)
  return nil if data[:active].empty? or data[:queued].empty?
  connections = data[:active].zip(data[:queued]).map { |e| e.inject(:+) }
  connections = DataSet.new(connections)

  # Calculate target
  target = connections.max * target_ratio + buffer

  # Check hard thresholds
  target = max_workers if max_workers and target > max_workers
  target = min_workers if target < min_workers
  target = target.ceil

  logger.debug "target calculated at: #{target}, worker count at #{worker_count}"
  if worker_count == max_workers and target == max_workers
    logger.warn "at maximum capacity! cannot scale up"
    return nil, 0
  elsif connections.avg > worker_count and data[:queued].avg > 1
    logger.warn "danger, will robinson! scaling up fast!"
    return "TTIN", target - worker_count
  elsif target > worker_count
    logger.debug "scaling up!"
    return "TTIN", 1
  elsif target < worker_count
    logger.debug "scaling down!"
    return "TTOU", 1
  elsif target == worker_count
    logger.debug "just right!"
    return nil, 0
  end
end
scale() click to toggle source
# File lib/alicorn/scaler.rb, line 32
def scale
  data          = collect_data
  unicorns      = find_unicorns

  master_pid    = find_master_pid(unicorns)
  worker_count  = find_worker_count(unicorns)

  sig, number = auto_scale(data, worker_count)
  if sig and !dry_run
    number.times do
      send_signal(master_pid, sig) 
      sleep(signal_delay) # Make sure unicorn doesn't discard repeated signals
    end
  end
rescue StandardError => e
  logger.error "exception occurred: #{e.class}\n\n#{e.message}"
  raise e unless e.is_a?(AmbiguousMasterError) or e.is_a?(NoMasterError) # Master-related errors are fine, usually just indicate a start or restart
end

Protected Instance Methods

collect_data() click to toggle source
# File lib/alicorn/scaler.rb, line 85
def collect_data
  logger.debug "Sampling #{listener_type} listener at #{listener_address} #{sample_count} times at #{delay} second intervals"
  active, queued = DataSet.new, DataSet.new
  sample_count.times do
    stats = Raindrops::Linux.send(:"#{listener_type}_listener_stats")[listener_address]
    active << stats.active
    queued << stats.queued
    sleep(delay)
  end

  logger.debug "Collected:"
  logger.debug "active:#{active}"
  logger.debug "queued:#{queued}"

  {:active => active, :queued => queued}
end

Private Instance Methods

find_master_pid(unicorns) click to toggle source

Raises errors if the master is busy restarting, or we can't be certain which PID to signal

# File lib/alicorn/scaler.rb, line 105
def find_master_pid(unicorns)
  master_lines = unicorns.select { |line| line.match /master/ }
  if master_lines.size == 0
    raise NoMasterError.new("No unicorn master processes detected. You may still be starting up.")
  elsif master_lines.size > 1
    raise AmbiguousMasterError.new("Too many unicorn master processes detected. You may be restarting, or have an app name collision: #{master_lines}")
  elsif master_lines.first.match /\(old\)/
    raise AmbiguousMasterError.new("Old master process detected. You may be restarting: #{master_lines.first}")
  else
    master_lines.first.split.first.to_i
  end
end
find_unicorns() click to toggle source
# File lib/alicorn/scaler.rb, line 122
def find_unicorns
  ptable = grep_process_list.split("\n")
  unicorns = ptable.select { |line| line.match(/unicorn/) && line.match(/#{Regexp.escape(app_name)}/) }
  raise NoUnicornsError.new("Could not find any unicorn processes") if unicorns.empty?

  unicorns.map(&:strip)
end
find_worker_count(unicorns) click to toggle source
# File lib/alicorn/scaler.rb, line 118
def find_worker_count(unicorns)
  unicorns.select { |line| line.match /worker\[[\d]+\]/ }.count
end
grep_process_list() click to toggle source
# File lib/alicorn/scaler.rb, line 130
def grep_process_list
  %x(ps ax)
end
send_signal(master_pid, sig) click to toggle source
# File lib/alicorn/scaler.rb, line 134
def send_signal(master_pid, sig)
  Process.kill(sig, master_pid)
end