autoscaler 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/autoscaler.rb +5 -0
- data/lib/autoscaler/heroku_scaler.rb +55 -0
- data/lib/autoscaler/sidekiq.rb +86 -0
- data/lib/autoscaler/version.rb +4 -0
- data/spec/autoscaler/heroku_scaler_spec.rb +20 -0
- data/spec/autoscaler/sidekiq_spec.rb +48 -0
- data/spec/spec_helper.rb +5 -0
- metadata +154 -0
    
        data/lib/autoscaler.rb
    ADDED
    
    
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            require 'heroku-api'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Autoscaler
         | 
| 4 | 
            +
              # Wraps the Heroku API to provide just the interface that we need for scaling.
         | 
| 5 | 
            +
              class HerokuScaler
         | 
| 6 | 
            +
                # @param [String] type process type this scaler controls
         | 
| 7 | 
            +
                # @param [String] key Heroku API key
         | 
| 8 | 
            +
                # @param [String] app Heroku app name
         | 
| 9 | 
            +
                def initialize(
         | 
| 10 | 
            +
                    type = 'worker',
         | 
| 11 | 
            +
                    key = ENV['HERKOU_API_KEY'],
         | 
| 12 | 
            +
                    app = ENV['HEROKU_APP'])
         | 
| 13 | 
            +
                  @client = Heroku::API.new(:api_key => key)
         | 
| 14 | 
            +
                  @type = type
         | 
| 15 | 
            +
                  @app = app
         | 
| 16 | 
            +
                  @workers = 0
         | 
| 17 | 
            +
                  @known = Time.now - 1
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                attr_reader :app
         | 
| 21 | 
            +
                attr_reader :type
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                # Read the current worker count (value may be cached)
         | 
| 24 | 
            +
                # @return [Numeric] number of workers
         | 
| 25 | 
            +
                def workers
         | 
| 26 | 
            +
                  if known?
         | 
| 27 | 
            +
                    @workers
         | 
| 28 | 
            +
                  else
         | 
| 29 | 
            +
                    know client.get_ps(app).body.count {|ps| ps['process'].match /#{type}\.\d?/ }
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                # Set the number of workers (noop if workers the same)
         | 
| 34 | 
            +
                # @param [Numeric] n number of workers
         | 
| 35 | 
            +
                def workers=(n)
         | 
| 36 | 
            +
                  if n != @workers || !known?
         | 
| 37 | 
            +
                    p "Scaling #{type} to #{n}"
         | 
| 38 | 
            +
                    client.post_ps_scale(app, type, n)
         | 
| 39 | 
            +
                    know n
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                private
         | 
| 44 | 
            +
                attr_reader :client
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def know(n)
         | 
| 47 | 
            +
                  @known = Time.now + 5
         | 
| 48 | 
            +
                  @workers = n
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def known?
         | 
| 52 | 
            +
                  Time.now < @known
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| @@ -0,0 +1,86 @@ | |
| 1 | 
            +
            require 'securerandom' # bug in Sidekiq as of 2.2.1
         | 
| 2 | 
            +
            require 'sidekiq'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Autoscaler
         | 
| 5 | 
            +
              # namespace module for Sidekiq middlewares
         | 
| 6 | 
            +
              module Sidekiq
         | 
| 7 | 
            +
                # Sidekiq client middleware
         | 
| 8 | 
            +
                # Performs scale-up when items are queued and there are no workers running
         | 
| 9 | 
            +
                class Client
         | 
| 10 | 
            +
                  # @param [Hash] scalers map of queue(String) => scaler (e.g. {HerokuScaler}).
         | 
| 11 | 
            +
                  #   Which scaler to use for each sidekiq queue
         | 
| 12 | 
            +
                  def initialize(scalers)
         | 
| 13 | 
            +
                    @scalers = scalers
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  # Sidekiq middleware api method
         | 
| 17 | 
            +
                  def call(worker_class, item, queue)
         | 
| 18 | 
            +
                    @scalers[queue] && @scalers[queue].workers = 1
         | 
| 19 | 
            +
                    yield
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                # Sidekiq server middleware
         | 
| 24 | 
            +
                # Performs scale-down when the queue is empty
         | 
| 25 | 
            +
                class Server
         | 
| 26 | 
            +
                  # @param [scaler] scaler object that actually performs scaling operations (e.g. {HerokuScaler})
         | 
| 27 | 
            +
                  # @param [Numeric] timeout number of seconds to wait before shutdown
         | 
| 28 | 
            +
                  # @param [Array[String]] specified_queues list of queues to monitor to determine if there is work left.  Defaults to all sidekiq queues.
         | 
| 29 | 
            +
                  def initialize(scaler, timeout, specified_queues = nil)
         | 
| 30 | 
            +
                    @scaler = scaler
         | 
| 31 | 
            +
                    @timeout = timeout
         | 
| 32 | 
            +
                    @specified_queues = specified_queues
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  # Sidekiq middleware api entry point
         | 
| 36 | 
            +
                  def call(worker, msg, queue)
         | 
| 37 | 
            +
                    working!
         | 
| 38 | 
            +
                    yield
         | 
| 39 | 
            +
                  ensure
         | 
| 40 | 
            +
                    working!
         | 
| 41 | 
            +
                    wait_for_task_or_scale
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  private
         | 
| 45 | 
            +
                  def queues
         | 
| 46 | 
            +
                    @specified_queues || registered_queues
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def registered_queues
         | 
| 50 | 
            +
                    ::Sidekiq.redis { |x| x.smembers('queues') }
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def empty?(name)
         | 
| 54 | 
            +
                    ::Sidekiq.redis { |conn| conn.llen("queue:#{name}") == 0 }
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def pending_work?
         | 
| 58 | 
            +
                    queues.any? {|q| !empty?(q)}
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def wait_for_task_or_scale
         | 
| 62 | 
            +
                    loop do
         | 
| 63 | 
            +
                      return if pending_work?
         | 
| 64 | 
            +
                      return @scaler.workers = 0 if idle?
         | 
| 65 | 
            +
                      sleep(0.5)
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  def working!
         | 
| 70 | 
            +
                    ::Sidekiq.redis {|c| c.set('background_activity', Time.now)}
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  def idle_time
         | 
| 74 | 
            +
                    ::Sidekiq.redis {|c|
         | 
| 75 | 
            +
                      t = c.get('background_activity')
         | 
| 76 | 
            +
                      return 0 unless t
         | 
| 77 | 
            +
                      Time.now - Time.parse(t)
         | 
| 78 | 
            +
                    }
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def idle?
         | 
| 82 | 
            +
                    idle_time > @timeout
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
            end
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
            require 'autoscaler/heroku_scaler'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            describe Autoscaler::HerokuScaler, :online => true do
         | 
| 5 | 
            +
              let(:cut) {Autoscaler::HerokuScaler}
         | 
| 6 | 
            +
              let(:client) {cut.new}
         | 
| 7 | 
            +
              subject {client}
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              its(:workers) {should == 0}
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              describe 'scaled' do
         | 
| 12 | 
            +
                around do |example|
         | 
| 13 | 
            +
                  client.workers = 1
         | 
| 14 | 
            +
                  example.yield
         | 
| 15 | 
            +
                  client.workers = 0
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                its(:workers) {should == 1}
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
            end
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
            require 'autoscaler/sidekiq'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            class Scaler
         | 
| 5 | 
            +
              attr_accessor :workers
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              def initialize(n = 0)
         | 
| 8 | 
            +
                self.workers = n
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
            end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            describe Autoscaler::Sidekiq do
         | 
| 13 | 
            +
              let(:scaler) do
         | 
| 14 | 
            +
                Scaler.new(workers)
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              describe Autoscaler::Sidekiq::Client do
         | 
| 18 | 
            +
                let(:cut) {Autoscaler::Sidekiq::Client}
         | 
| 19 | 
            +
                let(:sa) {cut.new('queue' => scaler)}
         | 
| 20 | 
            +
                let(:workers) {0}
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                describe 'scales' do
         | 
| 23 | 
            +
                  before {sa.call(Class, {}, 'queue') {}}
         | 
| 24 | 
            +
                  subject {scaler.workers}
         | 
| 25 | 
            +
                  it {should == 1}
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                describe 'yields' do
         | 
| 29 | 
            +
                  it {sa.call(Class, {}, 'queue') {:foo}.should == :foo}
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              describe Autoscaler::Sidekiq::Server do
         | 
| 34 | 
            +
                let(:cut) {Autoscaler::Sidekiq::Server}
         | 
| 35 | 
            +
                let(:sa) {cut.new(scaler, 0)}
         | 
| 36 | 
            +
                let(:workers) {1}
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                describe 'scales' do
         | 
| 39 | 
            +
                  before{sa.call(Object.new, {}, 'queue') {}}
         | 
| 40 | 
            +
                  subject {scaler.workers}
         | 
| 41 | 
            +
                  it {should == 0}
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                describe 'yields' do
         | 
| 45 | 
            +
                  it {sa.call(Object.new, {}, 'queue') {:foo}.should == :foo}
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
    
        data/spec/spec_helper.rb
    ADDED
    
    
    
        metadata
    ADDED
    
    | @@ -0,0 +1,154 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: autoscaler
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.0.1
         | 
| 5 | 
            +
              prerelease: 
         | 
| 6 | 
            +
            platform: ruby
         | 
| 7 | 
            +
            authors:
         | 
| 8 | 
            +
            - Justin Love
         | 
| 9 | 
            +
            - Fix Peña
         | 
| 10 | 
            +
            autorequire: 
         | 
| 11 | 
            +
            bindir: bin
         | 
| 12 | 
            +
            cert_chain: []
         | 
| 13 | 
            +
            date: 2012-10-21 00:00:00.000000000 Z
         | 
| 14 | 
            +
            dependencies:
         | 
| 15 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 16 | 
            +
              name: sidekiq
         | 
| 17 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 18 | 
            +
                none: false
         | 
| 19 | 
            +
                requirements:
         | 
| 20 | 
            +
                - - ~>
         | 
| 21 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 22 | 
            +
                    version: 2.2.1
         | 
| 23 | 
            +
              type: :runtime
         | 
| 24 | 
            +
              prerelease: false
         | 
| 25 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 26 | 
            +
                none: false
         | 
| 27 | 
            +
                requirements:
         | 
| 28 | 
            +
                - - ~>
         | 
| 29 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 30 | 
            +
                    version: 2.2.1
         | 
| 31 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 32 | 
            +
              name: heroku-api
         | 
| 33 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 34 | 
            +
                none: false
         | 
| 35 | 
            +
                requirements:
         | 
| 36 | 
            +
                - - ! '>='
         | 
| 37 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 38 | 
            +
                    version: '0'
         | 
| 39 | 
            +
              type: :runtime
         | 
| 40 | 
            +
              prerelease: false
         | 
| 41 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 42 | 
            +
                none: false
         | 
| 43 | 
            +
                requirements:
         | 
| 44 | 
            +
                - - ! '>='
         | 
| 45 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 46 | 
            +
                    version: '0'
         | 
| 47 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 48 | 
            +
              name: bundler
         | 
| 49 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 50 | 
            +
                none: false
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ! '>='
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
              type: :development
         | 
| 56 | 
            +
              prerelease: false
         | 
| 57 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                none: false
         | 
| 59 | 
            +
                requirements:
         | 
| 60 | 
            +
                - - ! '>='
         | 
| 61 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 62 | 
            +
                    version: '0'
         | 
| 63 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 64 | 
            +
              name: mast
         | 
| 65 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 66 | 
            +
                none: false
         | 
| 67 | 
            +
                requirements:
         | 
| 68 | 
            +
                - - ! '>='
         | 
| 69 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 70 | 
            +
                    version: '0'
         | 
| 71 | 
            +
              type: :development
         | 
| 72 | 
            +
              prerelease: false
         | 
| 73 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 74 | 
            +
                none: false
         | 
| 75 | 
            +
                requirements:
         | 
| 76 | 
            +
                - - ! '>='
         | 
| 77 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 78 | 
            +
                    version: '0'
         | 
| 79 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 80 | 
            +
              name: rspec
         | 
| 81 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 82 | 
            +
                none: false
         | 
| 83 | 
            +
                requirements:
         | 
| 84 | 
            +
                - - ! '>='
         | 
| 85 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 86 | 
            +
                    version: '0'
         | 
| 87 | 
            +
              type: :development
         | 
| 88 | 
            +
              prerelease: false
         | 
| 89 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 90 | 
            +
                none: false
         | 
| 91 | 
            +
                requirements:
         | 
| 92 | 
            +
                - - ! '>='
         | 
| 93 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 94 | 
            +
                    version: '0'
         | 
| 95 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 96 | 
            +
              name: guard-rspec
         | 
| 97 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 98 | 
            +
                none: false
         | 
| 99 | 
            +
                requirements:
         | 
| 100 | 
            +
                - - ! '>='
         | 
| 101 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 102 | 
            +
                    version: '0'
         | 
| 103 | 
            +
              type: :development
         | 
| 104 | 
            +
              prerelease: false
         | 
| 105 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 106 | 
            +
                none: false
         | 
| 107 | 
            +
                requirements:
         | 
| 108 | 
            +
                - - ! '>='
         | 
| 109 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 110 | 
            +
                    version: '0'
         | 
| 111 | 
            +
            description: Currently provides a Sidekiq middleware that does 0/1 scaling of Heroku
         | 
| 112 | 
            +
              processes
         | 
| 113 | 
            +
            email:
         | 
| 114 | 
            +
            - git@JustinLove.name
         | 
| 115 | 
            +
            executables: []
         | 
| 116 | 
            +
            extensions: []
         | 
| 117 | 
            +
            extra_rdoc_files: []
         | 
| 118 | 
            +
            files:
         | 
| 119 | 
            +
            - lib/autoscaler/heroku_scaler.rb
         | 
| 120 | 
            +
            - lib/autoscaler/sidekiq.rb
         | 
| 121 | 
            +
            - lib/autoscaler/version.rb
         | 
| 122 | 
            +
            - lib/autoscaler.rb
         | 
| 123 | 
            +
            - spec/autoscaler/heroku_scaler_spec.rb
         | 
| 124 | 
            +
            - spec/autoscaler/sidekiq_spec.rb
         | 
| 125 | 
            +
            - spec/spec_helper.rb
         | 
| 126 | 
            +
            homepage: ''
         | 
| 127 | 
            +
            licenses: []
         | 
| 128 | 
            +
            post_install_message: 
         | 
| 129 | 
            +
            rdoc_options: []
         | 
| 130 | 
            +
            require_paths:
         | 
| 131 | 
            +
            - lib
         | 
| 132 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 133 | 
            +
              none: false
         | 
| 134 | 
            +
              requirements:
         | 
| 135 | 
            +
              - - ! '>='
         | 
| 136 | 
            +
                - !ruby/object:Gem::Version
         | 
| 137 | 
            +
                  version: '0'
         | 
| 138 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 139 | 
            +
              none: false
         | 
| 140 | 
            +
              requirements:
         | 
| 141 | 
            +
              - - ! '>='
         | 
| 142 | 
            +
                - !ruby/object:Gem::Version
         | 
| 143 | 
            +
                  version: '0'
         | 
| 144 | 
            +
            requirements: []
         | 
| 145 | 
            +
            rubyforge_project: autoscaler
         | 
| 146 | 
            +
            rubygems_version: 1.8.24
         | 
| 147 | 
            +
            signing_key: 
         | 
| 148 | 
            +
            specification_version: 3
         | 
| 149 | 
            +
            summary: Start/stop Sidekiq workers on Heroku
         | 
| 150 | 
            +
            test_files:
         | 
| 151 | 
            +
            - spec/autoscaler/heroku_scaler_spec.rb
         | 
| 152 | 
            +
            - spec/autoscaler/sidekiq_spec.rb
         | 
| 153 | 
            +
            - spec/spec_helper.rb
         | 
| 154 | 
            +
            has_rdoc: 
         |