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 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: