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