autoscaler 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/autoscaler.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "autoscaler/version"
2
+
3
+ # Namespace module; no code
4
+ module Autoscaler
5
+ end
@@ -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,4 @@
1
+ module Autoscaler
2
+ # version number
3
+ VERSION = "0.0.1"
4
+ 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
@@ -0,0 +1,5 @@
1
+ RSpec.configure do |config|
2
+ config.mock_with :rspec
3
+
4
+ config.filter_run_excluding :online => true unless ENV['HEROKU_APP']
5
+ end
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: