kato 0.1.0

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/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.1.0 2008-03-26
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/License.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Jonathan Younger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,27 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ config/hoe.rb
7
+ config/requirements.rb
8
+ lib/kato.rb
9
+ lib/kato/pool_manager.rb
10
+ lib/kato/pool_supervisor.rb
11
+ lib/kato/version.rb
12
+ script/destroy
13
+ script/generate
14
+ script/txt2html
15
+ setup.rb
16
+ spec/kato_spec.rb
17
+ spec/spec.opts
18
+ spec/spec_helper.rb
19
+ tasks/deployment.rake
20
+ tasks/environment.rake
21
+ tasks/rspec.rake
22
+ tasks/website.rake
23
+ website/index.html
24
+ website/index.txt
25
+ website/javascripts/rounded_corners_lite.inc.js
26
+ website/stylesheets/screen.css
27
+ website/template.rhtml
data/README.txt ADDED
@@ -0,0 +1,55 @@
1
+ = kato
2
+
3
+ * http://kato.rubyforge.org
4
+
5
+ == DESCRIPTION:
6
+
7
+ Kato is a library for managing pools of Amazon EC2 servers.
8
+ It is a ruby port of the java lifeguard http://code.google.com/p/lifeguard/ library.
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ * Manage multiple EC2 pools
13
+ * Minimum number of instances
14
+ * Maximum number of instances
15
+ * Ramp up/down intervals
16
+
17
+ == SYNOPSIS:
18
+
19
+ require 'rubygems'
20
+ require 'kato'
21
+ pool_supervisor = Kato::PoolSupervisor.new(config)
22
+ pool_supervisor.run
23
+
24
+ == REQUIREMENTS:
25
+
26
+ * right_aws
27
+
28
+ == INSTALL:
29
+
30
+ * gem install kato
31
+
32
+ == LICENSE:
33
+
34
+ (The MIT License)
35
+
36
+ Copyright (c) 2008 Jonathan Younger
37
+
38
+ Permission is hereby granted, free of charge, to any person obtaining
39
+ a copy of this software and associated documentation files (the
40
+ 'Software'), to deal in the Software without restriction, including
41
+ without limitation the rights to use, copy, modify, merge, publish,
42
+ distribute, sublicense, and/or sell copies of the Software, and to
43
+ permit persons to whom the Software is furnished to do so, subject to
44
+ the following conditions:
45
+
46
+ The above copyright notice and this permission notice shall be
47
+ included in all copies or substantial portions of the Software.
48
+
49
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
50
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
51
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
52
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
53
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
54
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
55
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
data/config/hoe.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'kato/version'
2
+
3
+ AUTHOR = 'Jonathan Younger' # can also be an array of Authors
4
+ EMAIL = "jonathan@daikini.com"
5
+ DESCRIPTION = "Kato is a library for managing pools of Amazon EC2 servers"
6
+ GEM_NAME = 'kato' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'kato' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "poogle"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = Kato::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'kato documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.developer(AUTHOR, EMAIL)
52
+ p.description = DESCRIPTION
53
+ p.summary = DESCRIPTION
54
+ p.url = HOMEPATH
55
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
56
+ p.test_globs = ["test/**/test_*.rb"]
57
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
58
+
59
+ # == Optional
60
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
61
+ p.extra_deps = [['right_aws', '>= 1.6.1']] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
62
+
63
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
64
+
65
+ end
66
+
67
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
68
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
69
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
70
+ hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,17 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
16
+
17
+ require 'kato'
data/lib/kato.rb ADDED
@@ -0,0 +1,3 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+ require 'kato/pool_supervisor'
3
+ require 'kato/pool_manager'
@@ -0,0 +1,253 @@
1
+ require 'right_aws'
2
+
3
+ module Kato
4
+ class PoolManager
5
+ attr_accessor :config, :aws_config, :instances
6
+
7
+ def initialize(config, aws_config)
8
+ @config = config
9
+ @aws_config = aws_config
10
+ @keep_running = true
11
+ @instances = []
12
+ end
13
+
14
+ def run
15
+ add_existing_instances if config[:find_existing_instances?]
16
+
17
+ # fire up the minimum servers first. They take a least 2 minutes to start up
18
+ minimum_number_of_instances = config[:minimum_number_of_instances]
19
+ launch_instances(minimum_number_of_instances - instances.size) if minimum_number_of_instances > instances.size
20
+
21
+ # used to track time pool has no idle capacity
22
+ start_busy_interval = 0
23
+
24
+ # used to track time pool has spare capacity
25
+ start_idle_interval = 0
26
+
27
+ # loopey
28
+ while @keep_running do
29
+ messages = status_queue.receive_messages(config[:receive_count] || 20)
30
+ messages.each do |message|
31
+ break unless @keep_running
32
+
33
+ instance_status = InstanceStatus.parse(message.body)
34
+ if instance = instances.find { |i| i.id == instance_status.instance_id }
35
+ if instance_status.state == "busy"
36
+ instance.last_busy_interval = instance_status.last_interval
37
+ elsif instance_status.state == "idle"
38
+ instance.last_idle_interval = instance_status.last_interval
39
+ end
40
+
41
+ instance.last_report_time = Time.now
42
+ instance.update_load
43
+ end
44
+
45
+ message.delete
46
+ end
47
+
48
+ # for servers that haven't reported recently, bump idle interval...
49
+ instances.each do |instance|
50
+ # if more than a minute (arbitrarily) has gone by without a report,
51
+ # increase the last_idle_interval, and recalc the load_estimate
52
+ if instance.last_report_time < (Time.now - config[:idle_bump_interval])
53
+ instance.last_idle_interval += config[:idle_bump_interval]
54
+ instance.last_report_time = Time.now
55
+ instance.update_load
56
+ end
57
+ end
58
+
59
+ # calculate pool load average
60
+ sum = instances.inject(0) { |sum, instance| sum + instance.load_estimate }
61
+ number_of_instances = instances.size
62
+ pool_load = number_of_instances == 0 ? 0 : (sum / number_of_instances)
63
+ STDERR.puts "Pool Load Average: #{pool_load}"
64
+
65
+ # now, see if were full busy, or somewhat idle
66
+ if pool_load > 75 # Busy
67
+ start_busy_interval = Time.now if start_busy_interval == 0
68
+ start_idle_interval = 0
69
+ else
70
+ start_idle_interval = Time.now if start_idle_interval == 0
71
+ start_busy_interval = 0
72
+ end
73
+
74
+ queue_depth = work_queue.size
75
+ STDERR.puts "Queue Depth: #{queue_depth}"
76
+
77
+ # fast exit
78
+ break unless @keep_running
79
+
80
+ # now, based on busy/idle timers and queue depth, make a call on
81
+ # whether to start or terminate servers
82
+ idle_interval = start_idle_interval == 0 ? 0 : (Time.now - start_idle_interval)
83
+ busy_interval = start_busy_interval == 0 ? 0 : (Time.now - start_busy_interval)
84
+
85
+ # idle interval has elapsed
86
+ minimum_number_of_instances = config[:minimum_number_of_instances]
87
+ if idle_interval >= config[:ramp_down_delay]
88
+ if number_of_instances > minimum_number_of_instances
89
+ # terminate as many servers (up to the interval)
90
+ number_of_instances_to_kill = [config[:ramp_down_interval], number_of_instances].min
91
+
92
+ # ensure we don't kill too many servers (not below min)
93
+ if (number_of_instances - number_of_instances_to_kill) < minimum_number_of_instances
94
+ number_of_instances_to_kill -= (minimum_number_of_instances - (number_of_instances - number_of_instances_to_kill))
95
+ end
96
+
97
+ # if there are still messages in work queue, leave an idle server
98
+ # (this helps prevent cyclic launching and terminating of servers)
99
+ if queue_depth >= 1 && (number_of_instances_to_kill == number_of_instances)
100
+ number_of_instances_to_kill -= 1
101
+ end
102
+
103
+ if number_of_instances_to_kill > 0
104
+ # terminate the instances with the lowest load estimate
105
+ instances_sorted_by_lowest_load_estimate = instances.sort do |a,b|
106
+ # Compare the elapsed lifetime status. If the status differs, instances
107
+ # that have lived beyond the minimum lifetime will be sorted earlier.
108
+ if a.minimum_lifetime_elapsed? != b.minimum_lifetime_elapsed?
109
+ if a.minimum_lifetime_elapsed?
110
+ # This instance has lived long enough, the other hasn't
111
+ -1
112
+ else
113
+ # The other instance has lived long enough, this one hasn't
114
+ 1
115
+ end
116
+ else
117
+ a.load_estimate - b.load_estimate
118
+ end
119
+ end
120
+
121
+ terminate_instances(instances_sorted_by_lowest_load_estimate[0...number_of_instances_to_kill], false)
122
+ end
123
+
124
+ # reset
125
+ start_idle_interval = 0
126
+ end
127
+ end
128
+
129
+ # busy interval has elapsed
130
+ maximum_number_of_instances = config[:maximum_number_of_instances]
131
+ if busy_interval >= config[:ramp_up_delay]
132
+ if number_of_instances < maximum_number_of_instances
133
+ number_of_instances_to_launch = config[:ramp_up_interval]
134
+ size_factor = config[:queue_size_factor]
135
+
136
+ # use queue_depth to adjust the number_of_instances_to_launch
137
+ number_of_instances_to_launch = number_of_instances_to_launch * ((queue_depth / (size_factor < 1 ? 1 :size_factor).to_f) +1 ).to_i
138
+ if (number_of_instances + number_of_instances_to_launch) > maximum_number_of_instances
139
+ number_of_instances_to_launch -= ((number_of_instances + number_of_instances_to_launch) - maximum_number_of_instances)
140
+ end
141
+
142
+ if number_of_instances_to_launch > 0
143
+ launch_instances(number_of_instances_to_launch)
144
+ end
145
+ end
146
+ end
147
+
148
+ # this test will get instances started if there is work and zero instances.
149
+ if number_of_instances == 0 && queue_depth > 0 && maximum_number_of_instances > 0
150
+ launch_instances(config[:ramp_up_interval])
151
+ start_idle_interval = 0
152
+ start_busy_interval = 0
153
+ end
154
+
155
+ sleep 2
156
+ end
157
+ end
158
+
159
+ def shutdown
160
+ @keep_running = false
161
+ end
162
+
163
+ def status_queue
164
+ @status_queue ||= sqs.queue(config[:queue_prefix] + config[:pool_status_queue])
165
+ end
166
+
167
+ def work_queue
168
+ @work_queue ||= sqs.queue(config[:queue_prefix] + config[:service_work_queue])
169
+ end
170
+
171
+ def sqs
172
+ @sqs ||= RightAws::Sqs.new(aws_config[:access_id], aws_config[:access_key], :server => aws_config[:sqs][:server], :port => aws_config[:sqs][:port], :protocol => aws_config[:sqs][:protocol])
173
+ end
174
+
175
+ def ec2
176
+ @ec2 ||= RightAws::Ec2.new(aws_config[:access_id], aws_config[:access_key], :server => aws_config[:ec2][:server], :port => aws_config[:ec2][:port], :protocol => aws_config[:ec2][:protocol])
177
+ end
178
+
179
+ def add_existing_instances
180
+ ec2.describe_instances.each do |instance|
181
+ if instance[:aws_image_id] == config[:service_ami] && %w[pending running].include?(instance[:aws_state])
182
+ instances << Instance.new(instance[:aws_instance_id], config[:minimum_lifetime_in_minutes])
183
+ end
184
+ end
185
+ end
186
+
187
+ def launch_instances(number_of_instances_to_launch)
188
+ launched_instances = ec2.run_instances(config[:service_ami], 1, number_of_instances_to_launch, nil, config[:key_pair_name], config[:user_data])
189
+
190
+ if launched_instances.size < number_of_instances_to_launch
191
+ STDERR.puts "Failed to launch desired number of instances. (#{launched_instances.size} instead of #{number_of_instances_to_launch})"
192
+ end
193
+
194
+ launched_instances.each do |launched_instance|
195
+ instances << Instance.new(launched_instance[:aws_instance_id], config[:minimum_lifetime_in_minutes])
196
+ STDERR.puts "launched instance #{launched_instance[:aws_instance_id]}"
197
+ end
198
+ end
199
+
200
+ def terminate_instances(instances_to_terminate, force = false)
201
+ instances_to_terminate = instances_to_terminate.find_all do |instance|
202
+ # Don't stop instances before minimum_lifetime_in_minutes
203
+ force || instance.minimum_lifetime_elapsed?
204
+ end
205
+
206
+ instances_to_terminate.each do |instance|
207
+ STDERR.puts "Terminating instance #{instance.id}"
208
+ instances.delete instance
209
+ end
210
+
211
+ ec2.terminate_instances(instances_to_terminate.collect { |instance| instance.id.to_s }) if instances_to_terminate.any?
212
+ end
213
+ end
214
+
215
+ class Instance
216
+ attr_accessor :id, :load_estimate, :last_idle_interval, :last_busy_interval, :last_report_time, :startup_time, :minimum_lifetime_in_minutes
217
+
218
+ def initialize(id, minimum_lifetime_in_minutes = 55)
219
+ @id = id
220
+ @minimum_lifetime_in_minutes = minimum_lifetime_in_minutes
221
+ @load_estimate = 0
222
+ @last_idle_interval = 0
223
+ @last_busy_interval = 0
224
+ @last_report_time = Time.now
225
+ @startup_time = Time.now
226
+ end
227
+
228
+ def update_load
229
+ @load_estimate = (last_busy_interval.to_i / (last_idle_interval.to_i + last_busy_interval.to_i).to_f * 100)
230
+ end
231
+
232
+ def minimum_lifetime_elapsed?
233
+ (Time.now - startup_time) > (minimum_lifetime_in_minutes * 60)
234
+ end
235
+ end
236
+
237
+ class InstanceStatus
238
+ attr_accessor :instance_id, :state, :last_interval, :timestamp
239
+
240
+ def initialize(instance_id, state, last_interval, timestamp)
241
+ @instance_id, @state, @last_interval, @timestamp = instance_id, state, last_interval, timestamp
242
+ end
243
+
244
+ def self.parse(xml_or_yaml)
245
+ if xml_or_yaml =~ /<InstanceStatus>/
246
+ # FIXME Parse the xml
247
+ else
248
+ status = YAML.load(xml_or_yaml)
249
+ new(status[:instance_id], status[:state], status[:last_interval], status[:timestamp])
250
+ end
251
+ end
252
+ end
253
+ end