staugaard-cloudmaster 0.1.3 → 0.1.4
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/VERSION.yml +1 -1
- data/app/active_set_factory.rb +16 -0
- data/app/active_set_none.rb +16 -0
- data/app/active_set_queue.rb +27 -0
- data/app/active_set_s3.rb +25 -0
- data/app/configuration.rb +85 -0
- data/app/default-config.ini +95 -0
- data/app/ec2_image_enumerator.rb +41 -0
- data/app/ec2_instance_enumerator.rb +25 -0
- data/app/instance.rb +146 -0
- data/app/instance_pool.rb +326 -0
- data/app/named_queue.rb +75 -0
- data/app/policy.rb +113 -0
- data/app/policy_daytime.rb +18 -0
- data/app/policy_factory.rb +16 -0
- data/app/policy_fixed.rb +19 -0
- data/app/policy_job.rb +54 -0
- data/app/policy_limit.rb +68 -0
- data/app/policy_manual.rb +36 -0
- data/app/policy_resource.rb +110 -0
- data/app/pool_configuration.rb +172 -0
- data/app/pool_manager.rb +239 -0
- data/app/pool_runner.rb +54 -0
- data/app/reporter.rb +81 -0
- data/app/status_parser_factory.rb +16 -0
- data/app/status_parser_lifeguard.rb +48 -0
- data/app/status_parser_std.rb +11 -0
- metadata +27 -1
@@ -0,0 +1,326 @@
|
|
1
|
+
require 'aws_context'
|
2
|
+
require 'ec2_instance_enumerator'
|
3
|
+
require 'instance'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module Cloudmaster
|
7
|
+
|
8
|
+
# Stores and operates on a collection of instances
|
9
|
+
#
|
10
|
+
# Internally, instances are stored as an array of Instance objects.
|
11
|
+
class InstancePool
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
# Create an instance pool.
|
15
|
+
# This class knows how to start and stop instances, and how to detect
|
16
|
+
# that new instance have come about, or existing ones have gone away.
|
17
|
+
# The constructor takes:
|
18
|
+
# [config] describes configurable instance properties
|
19
|
+
def initialize(reporter, config)
|
20
|
+
@ec2 = AwsContext.instance.ec2
|
21
|
+
@reporter = reporter
|
22
|
+
@config = config
|
23
|
+
@state_change_time = Clock.now
|
24
|
+
@instances = [] # holds Instance objects
|
25
|
+
end
|
26
|
+
|
27
|
+
# private
|
28
|
+
|
29
|
+
# Create and return options, in a way that is acceptable to EC2.
|
30
|
+
def start_opts
|
31
|
+
groups = @config[:security_groups]
|
32
|
+
# this can throw an exception if groups is not formetteed properly
|
33
|
+
begin
|
34
|
+
groups = eval(groups) if groups.kind_of?(String)
|
35
|
+
rescue
|
36
|
+
groups = [:default]
|
37
|
+
end
|
38
|
+
{:key_name => @config[:key_pair_name],
|
39
|
+
:user_data => YAML.dump(@config[:user_data]),
|
40
|
+
:security_groups => groups,
|
41
|
+
:instance_type => @config[:instance_type]}
|
42
|
+
end
|
43
|
+
|
44
|
+
def max2(a, b)
|
45
|
+
a > b ? a : b
|
46
|
+
end
|
47
|
+
|
48
|
+
# Allows iteration through instances.
|
49
|
+
# So enumeration on InstancePool is implicitly enumeration
|
50
|
+
# on @instances.
|
51
|
+
def each
|
52
|
+
@instances.each {|i| yield i}
|
53
|
+
end
|
54
|
+
|
55
|
+
# return first instance
|
56
|
+
def first
|
57
|
+
@instances.first
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return the number of instances in the pool.
|
61
|
+
def size
|
62
|
+
@instances.size
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create an instance and add to the list.
|
66
|
+
# Return the newly created instance.
|
67
|
+
def add(id, public_dns)
|
68
|
+
new_instance = Instance.new(id, public_dns, @config)
|
69
|
+
@instances << new_instance
|
70
|
+
new_instance
|
71
|
+
end
|
72
|
+
|
73
|
+
# Delete the instance from the list
|
74
|
+
def delete(instance)
|
75
|
+
@instances.delete(instance)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Find an instance given its instance id.
|
79
|
+
def find_by_id(id)
|
80
|
+
find {|i| i.id == id}
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return a list of all instance ids.
|
84
|
+
def id_list
|
85
|
+
map {|i| i.id}
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return the maximum number of instances allowed.
|
89
|
+
def maximum
|
90
|
+
@config[:maximum_number_of_instances].to_i
|
91
|
+
end
|
92
|
+
|
93
|
+
# Return the minimum number of instances allowed.
|
94
|
+
def minimum
|
95
|
+
@config[:minimum_number_of_instances].to_i
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return true if the number of instances is less than the minimum.
|
99
|
+
def less_than_minimum?
|
100
|
+
size < minimum
|
101
|
+
end
|
102
|
+
|
103
|
+
# return true if number of instances is more than maximum
|
104
|
+
def greater_than_maximum?
|
105
|
+
size > maximum
|
106
|
+
end
|
107
|
+
|
108
|
+
# Return count of instance below minimum
|
109
|
+
def below_minimum_count
|
110
|
+
less_than_minimum? ? minimum - size : 0
|
111
|
+
end
|
112
|
+
|
113
|
+
# Return count of instance above maximum
|
114
|
+
def above_maximum_count
|
115
|
+
greater_than_maximum? ? size - maximum : 0
|
116
|
+
end
|
117
|
+
|
118
|
+
# Return a list of instances missing public_dns.
|
119
|
+
def missing_public_dns_instances
|
120
|
+
find_all {|i| i.public_dns.nil? || i.public_dns.empty? }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Return ids of all instances missing a public_dns.
|
124
|
+
def missing_public_dns_ids
|
125
|
+
missing_public_dns_instances.collect {|i| i.id}
|
126
|
+
end
|
127
|
+
|
128
|
+
# Find the instance identified by id and update its public_dns
|
129
|
+
# If there is no dns information, then skip it.
|
130
|
+
def update_public_dns(id, public_dns)
|
131
|
+
return if public_dns.nil? || public_dns.empty?
|
132
|
+
i = find_by_id(id)
|
133
|
+
i.public_dns = public_dns if i
|
134
|
+
end
|
135
|
+
|
136
|
+
# Return instances that have not seen status in watchdog_interval
|
137
|
+
def hung_instances
|
138
|
+
find_all {|i| i.watchdog_time_elapsed?}
|
139
|
+
end
|
140
|
+
|
141
|
+
# Return all instances in active state
|
142
|
+
def active_instances
|
143
|
+
find_all {|i| i.state == :active}
|
144
|
+
end
|
145
|
+
|
146
|
+
# Return all instances in shut_down state
|
147
|
+
def shut_down_instances
|
148
|
+
find_all {|i| i.state == :shut_down}
|
149
|
+
end
|
150
|
+
|
151
|
+
# Return instances that are active and have load <= target_load
|
152
|
+
def active_idle_instances
|
153
|
+
target_load = 0
|
154
|
+
active_instances.find_all {|i| i.load_estimate <= target_load}
|
155
|
+
end
|
156
|
+
|
157
|
+
# Shut down all instances who have a load below the target.
|
158
|
+
# Shut down is not the same as stop -- the instances continue to
|
159
|
+
# provide service, but are no longer allocated new clients.
|
160
|
+
def shut_down_idle_instances
|
161
|
+
target_load = @config[:shut_down_threshold].to_i
|
162
|
+
shut_down_instances.find_all {|i| i.load_estimate <= target_load}
|
163
|
+
end
|
164
|
+
|
165
|
+
# Return instances that are shut_down and have
|
166
|
+
# time_since_state_change > shut_down_interval.
|
167
|
+
def shut_down_timeout_instances
|
168
|
+
shut_down_interval = @config[:shut_down_interval].to_i * 60
|
169
|
+
|
170
|
+
shut_down_instances.find_all {|i| i.time_since_state_change > shut_down_interval}
|
171
|
+
end
|
172
|
+
|
173
|
+
# Return the latest time since any state change of any instance.
|
174
|
+
def state_change_time
|
175
|
+
@state_change_time = inject(@state_change_time) do |latest, instance|
|
176
|
+
max2(latest, instance.state_change_time)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Return the sum of all the extra capacity of active instances
|
181
|
+
# that have excess capacity (load less than target load).
|
182
|
+
def excess_capacity
|
183
|
+
target_load = @config[:target_upper_load].to_f
|
184
|
+
active_instances.inject(0) do |sum, instance|
|
185
|
+
sum + max2(target_load - instance.load_estimate, 0)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Return the sum of capacity in excess of the target upper load
|
190
|
+
def over_capacity
|
191
|
+
target_load = @config[:target_upper_load].to_f
|
192
|
+
active_instances.inject(0) do |sum, instance|
|
193
|
+
sum + max2(instance.load_estimate - target_load, 0)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Return the total load of all active instances
|
198
|
+
def total_load
|
199
|
+
active_instances.inject(0) do |sum, instance|
|
200
|
+
sum + instance.load_estimate
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Update the status of an instance using the contents of the status message.
|
205
|
+
def update_status(msg)
|
206
|
+
id = msg[:instance_id]
|
207
|
+
if instance = find_by_id(id)
|
208
|
+
instance.update_status(msg)
|
209
|
+
else
|
210
|
+
@reporter.error("Received status message from unknown instance: #{id}") unless id == 'unknown'
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Return a YAML encoded representation of the active set.
|
215
|
+
# The active set describes the id, public DNS, and load average of
|
216
|
+
# each active instance in the pool.
|
217
|
+
def active_set
|
218
|
+
message = active_instances.collect do |instance|
|
219
|
+
{ :id => instance.id,
|
220
|
+
:public_dns => instance.public_dns,
|
221
|
+
:load_estimate => instance.load_estimate }
|
222
|
+
end
|
223
|
+
YAML.dump(message)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Return all the instance, sortd by lowest load estimate.
|
227
|
+
def sorted_by_lowest_load
|
228
|
+
@instances.sort do |a,b|
|
229
|
+
# Compare the elapsed lifetime status. If the status differs, instances
|
230
|
+
# that have lived beyond the minimum lifetime will be sorted earlier.
|
231
|
+
if a.minimum_lifetime_elapsed? != b.minimum_lifetime_elapsed?
|
232
|
+
if a.minimum_lifetime_elapsed?
|
233
|
+
-1 # This instance has lived long enough, the other hasn't
|
234
|
+
else
|
235
|
+
1 # The other instance has lived long enough, this one hasn't
|
236
|
+
end
|
237
|
+
else
|
238
|
+
a.load_estimate - b.load_estimate
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Find all instances for which we don't have a public_dns,
|
244
|
+
# For each one,see of EC2 now has the public DNS. If so, store it.
|
245
|
+
def update_public_dns_all
|
246
|
+
missing_ids = missing_public_dns_ids
|
247
|
+
return if missing_ids.size == 0
|
248
|
+
EC2InstanceEnumerator.new(missing_ids).each do |instance|
|
249
|
+
update_public_dns(instance[:id], instance[:public_dns])
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Return instances that match our ami_id that are either pending
|
254
|
+
# or running.
|
255
|
+
# These instances are as returned by ec2: containing fields such as
|
256
|
+
# id and :public_dns.
|
257
|
+
def our_running_instances
|
258
|
+
EC2InstanceEnumerator.new.find_all do |instance|
|
259
|
+
instance[:image_id] == @config[:ami_id] &&
|
260
|
+
%w[pending running].include?(instance[:state])
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Audit the list of instances based on what is currently known to EC2.
|
265
|
+
# In other words, bring our list of instances into agreement with the instances
|
266
|
+
# EC2 knows about by
|
267
|
+
# (1) adding instances that EC2 knows but that we do not, and
|
268
|
+
# (2) deleting instance that EC2 no longer knows about.
|
269
|
+
# This is used initially to build the instance list, and
|
270
|
+
# periodically thereafter to catch instances started or stopped
|
271
|
+
# outside cloudmaster.
|
272
|
+
def audit_existing_instances
|
273
|
+
running_instances = our_running_instances
|
274
|
+
|
275
|
+
# add running instances that we don't have
|
276
|
+
running_instances.each do |running|
|
277
|
+
if ! find_by_id(running[:id])
|
278
|
+
add(running[:id], running[:public_dns])
|
279
|
+
@reporter.info("Instance discovered #{running[:public_dns]}", running[:id])
|
280
|
+
end
|
281
|
+
end
|
282
|
+
# delete instances that are no longer running
|
283
|
+
each do |instance|
|
284
|
+
if ! running_instances.find {|running| running[:id] == instance.id}
|
285
|
+
delete(instance)
|
286
|
+
@reporter.info("Instance disappeared #{instance.public_dns}", instance.id)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Start the given number of instances.
|
292
|
+
# Remember started instances by creating an Instance object and storing it
|
293
|
+
# in the pool.
|
294
|
+
# Return an array of the ones we just started.
|
295
|
+
def start_n_instances(number_to_start)
|
296
|
+
return [] if number_to_start <= 0
|
297
|
+
started_instances = @ec2.run_instances(@config[:ami_id], 1,
|
298
|
+
number_to_start, start_opts)[:instances]
|
299
|
+
started_instances.collect do |started_instance|
|
300
|
+
# the public dns is not available yet
|
301
|
+
add(started_instance[:id], nil)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Stop the given set of instances.
|
306
|
+
# Remove stopped instance from the pool.
|
307
|
+
# Return an array of stopped instances.
|
308
|
+
def stop_instances(instances_to_stop)
|
309
|
+
instances_to_stop.collect do |instance|
|
310
|
+
@ec2.terminate_instances(instance.id.to_s)
|
311
|
+
delete(instance)
|
312
|
+
instance
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# Shut down the given set of instances.
|
317
|
+
# Set the state to shut_down
|
318
|
+
# Return an array of shut down instances.
|
319
|
+
def shut_down(instances_to_shut_down)
|
320
|
+
instances_to_shut_down.collect do |instance|
|
321
|
+
instance.shutdown
|
322
|
+
instance
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
data/app/named_queue.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'aws_context'
|
2
|
+
|
3
|
+
module Cloudmaster
|
4
|
+
|
5
|
+
# Implements a queue specified by name.
|
6
|
+
# The queue name is used to look up the actual SQS queue.
|
7
|
+
# The queue name is used in a substring match, so possibly more than one
|
8
|
+
# string may match.
|
9
|
+
# If no queue is found for given name, or if there are
|
10
|
+
# multiple, the NamedQueue throws an exception.
|
11
|
+
# Implements the read and delete operations.
|
12
|
+
# It also caches queue_depth, so it need not query SQS every time
|
13
|
+
# it is needed.
|
14
|
+
class NamedQueue
|
15
|
+
attr_reader :queue_depth
|
16
|
+
|
17
|
+
# Create a queue given the queue name.
|
18
|
+
# A SQS interface must be supplied: it is used to interact with Amazon.
|
19
|
+
# The queue name is given, and is used to look up the actual queue.
|
20
|
+
# Raises an exception if there is more than one queue matching the given
|
21
|
+
# name.
|
22
|
+
# If there is no queue with the given name, create one.
|
23
|
+
def initialize(queue_name)
|
24
|
+
@sqs = AwsContext.instance.sqs
|
25
|
+
queues = @sqs.list_queues(queue_name)
|
26
|
+
@queue_depth = 0
|
27
|
+
@queue = case queues.length
|
28
|
+
when 0
|
29
|
+
queue = @sqs.create_queue(queue_name)
|
30
|
+
raise "Bad Configuration -- no queue: #{queue_name}" if queue.nil?
|
31
|
+
queue
|
32
|
+
when 1
|
33
|
+
queues.first
|
34
|
+
else
|
35
|
+
raise "Bad configuration -- multiple queues match #{queue_name}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Read some messages off a queue.
|
40
|
+
# For some reason, we never receive more than 1.
|
41
|
+
# But we are prepared for more.
|
42
|
+
# return an array of the messages that were read.
|
43
|
+
def read_messages(count = 1)
|
44
|
+
@sqs.receive_messages(@queue, count)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Delete a message from the queue given its id.
|
48
|
+
def delete_message(receipt_handle)
|
49
|
+
@sqs.delete_message(@queue, receipt_handle)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Read and discard all messages on the queue.
|
53
|
+
# Return the number read.
|
54
|
+
def empty_queue
|
55
|
+
n = 0
|
56
|
+
while true
|
57
|
+
msgs = read_messages
|
58
|
+
break if msgs.size == 0
|
59
|
+
n += 1
|
60
|
+
msgs.each {|msg| delete_message(msg[:receipt_handle])}
|
61
|
+
end
|
62
|
+
n
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get qeue depth and store it.
|
66
|
+
# In case of failure, keep the old value.
|
67
|
+
# The last value read is available through the queue_depth attribute.
|
68
|
+
def read_queue_depth
|
69
|
+
attr = 'ApproximateNumberOfMessages'
|
70
|
+
attrs = @sqs.get_queue_attributes(@queue, attr)
|
71
|
+
@queue_depth = attrs[attr] if attrs.has_key?(attr)
|
72
|
+
@queue_depth
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/app/policy.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'policy_limit'
|
2
|
+
|
3
|
+
module Cloudmaster
|
4
|
+
|
5
|
+
# Provides the common data and behaviors for policies.
|
6
|
+
# This includes storing the configuration and instance collection.
|
7
|
+
# It also includes implementing essential methods such as ensure_limite and
|
8
|
+
# stop_hung_instances.
|
9
|
+
# Finally, this implements helpers that process the queue and image names
|
10
|
+
# which might be desirable to override.
|
11
|
+
class Policy
|
12
|
+
def initialize(reporter, config, instances)
|
13
|
+
@reporter = reporter
|
14
|
+
@config = config
|
15
|
+
@instances = instances
|
16
|
+
@limit_policy = PolicyLimit.new(reporter, config, instances)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Make sure there are at least the minimum instances running.
|
20
|
+
# Also make sure there are no more than maximum number of instances.
|
21
|
+
# If not, start or stop enough to conform.
|
22
|
+
# These actions bypass the creation and termination limits normally in place
|
23
|
+
def ensure_limits
|
24
|
+
n = @limit_policy.adjust_limits
|
25
|
+
case
|
26
|
+
when n > 0
|
27
|
+
start_instances(n)
|
28
|
+
when n < 0
|
29
|
+
stop_instances(-n)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# If instances have not sent status in a long time, they
|
34
|
+
# are probably hung, and should be stopped.
|
35
|
+
def stop_hung_instances
|
36
|
+
if @instances.hung_instances.size > 0
|
37
|
+
@reporter.info("Stopping hung instances #{@instances.hung_instances.size}")
|
38
|
+
stop_instances(@instances.hung_instances)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Default policy application function
|
43
|
+
# Calculate the number to adjust by, and then start or
|
44
|
+
# stop instances accordingly.
|
45
|
+
def apply
|
46
|
+
n = @limit_policy.adjust(adjust)
|
47
|
+
case
|
48
|
+
when n > 0
|
49
|
+
start_instances(n)
|
50
|
+
when n == 0
|
51
|
+
0
|
52
|
+
when n < 0
|
53
|
+
stop_instances(-n)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the number of additional instances to start (positive)
|
58
|
+
# or the number to stop (negative)
|
59
|
+
# This must be implemented by derived class.
|
60
|
+
def adjust
|
61
|
+
raise NotImpelemntedError("#{self.class.name}#adjust is not implemented.")
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
# Start the given number of instances.
|
67
|
+
# Return the number started.
|
68
|
+
def start_instances(number_to_start)
|
69
|
+
started_instances = @instances.start_n_instances(number_to_start)
|
70
|
+
if started_instances.size < number_to_start
|
71
|
+
@reporter.error("Failed to start requested number of instances. (#{started_instances.size} instead of #{number_to_start})")
|
72
|
+
end
|
73
|
+
started_instances.each { |started| @reporter.info("Started instance #{started.id}")}
|
74
|
+
started_instances.size
|
75
|
+
end
|
76
|
+
|
77
|
+
# Stop the given list of instances.
|
78
|
+
# Don't stop ones whose minimum lifetime has not elapsed.
|
79
|
+
# Returns the number stopped.
|
80
|
+
def stop_instances_list(instances_to_stop, force = false)
|
81
|
+
instances_to_stop = instances_to_stop.find_all do |instance|
|
82
|
+
# Don't stop instances before minimum_time
|
83
|
+
force || instance.minimum_time_elapsed?
|
84
|
+
end
|
85
|
+
instances = @instances.stop_instances(instances_to_stop)
|
86
|
+
instances.each {|i| @reporter.info("Terminating instance ", i.id) }
|
87
|
+
instances.size
|
88
|
+
end
|
89
|
+
|
90
|
+
# Stop a given number of instances.
|
91
|
+
# Return the number stopped.
|
92
|
+
def stop_n_instances(number_to_stop)
|
93
|
+
return if number_to_stop <= 0
|
94
|
+
# stop the instances with the lowest load estimate
|
95
|
+
instances_with_lowest_load = @instances.sorted_by_lowest_load
|
96
|
+
stop_instances_list(instances_with_lowest_load[0...number_to_stop])
|
97
|
+
end
|
98
|
+
|
99
|
+
# Stop instances.
|
100
|
+
# If passed a number, it stops that number of instances.
|
101
|
+
# If passed a list of instance ids, it stops those instances.
|
102
|
+
def stop_instances(to_stop, *params)
|
103
|
+
if to_stop.class == Fixnum
|
104
|
+
stop_n_instances(to_stop)
|
105
|
+
elsif to_stop.class == Array
|
106
|
+
stop_instances_list(to_stop, *params)
|
107
|
+
else
|
108
|
+
raise "Bad call -- stop_instances #{to_stop.class}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'policy'
|
2
|
+
|
3
|
+
module Cloudmaster
|
4
|
+
|
5
|
+
# Provide example daytime policy
|
6
|
+
# This increases the pool size during the day.
|
7
|
+
# This is provided as part of an example on how to create custom policies.
|
8
|
+
class PolicyDaytime < Policy
|
9
|
+
# Define the adjust function increase the pool in the
|
10
|
+
# daytime if it is below daytime minimum
|
11
|
+
def adjust
|
12
|
+
hour = Clock.hour
|
13
|
+
return 0 if hour < 10 || hour > 17
|
14
|
+
return 0 if @instances.size >= @config[:minimum_number_of_instances_daytime].to_i
|
15
|
+
return 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# This is a factory for Policy implementations.
|
2
|
+
require 'factory'
|
3
|
+
|
4
|
+
module Cloudmaster
|
5
|
+
class PolicyFactory
|
6
|
+
include Factory
|
7
|
+
def PolicyFactory.create(policy_name, *params)
|
8
|
+
name = policy_name.nil? ? 'default' : policy_name.to_s
|
9
|
+
require 'policy_' + name.downcase
|
10
|
+
class_name = 'Policy' +name.capitalize
|
11
|
+
policy = Factory.create_object_from_string(class_name, *params)
|
12
|
+
raise "Bad configuration -- no policy #{class_name}" unless policy
|
13
|
+
policy
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/app/policy_fixed.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'policy'
|
2
|
+
|
3
|
+
module Cloudmaster
|
4
|
+
|
5
|
+
# Provide fixed policy.
|
6
|
+
# This never adjusts the size of the pool.
|
7
|
+
# PolicyFixed is still useful, because it still ensures that the number
|
8
|
+
# of instances stay beteen the maximum and minimum (because this is
|
9
|
+
# enforced in the base class apply).
|
10
|
+
class PolicyFixed < Policy
|
11
|
+
# Use everything from the base class.
|
12
|
+
|
13
|
+
# Adjust never changes instances.
|
14
|
+
def adjust
|
15
|
+
0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/app/policy_job.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
require 'policy'
|
3
|
+
|
4
|
+
module Cloudmaster
|
5
|
+
|
6
|
+
# Provides job policy
|
7
|
+
# Instances managed by the job policy take work from a queue and report
|
8
|
+
# status thrugh another queue.
|
9
|
+
class PolicyJob < Policy
|
10
|
+
# Initialize the policy by giving it access to the configuration and
|
11
|
+
# the collection of instances.
|
12
|
+
def initialize(reporter, config, instances)
|
13
|
+
super(reporter, config, instances)
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
# Adjust the pool size.
|
18
|
+
# Add instances if the queue is getting backed up.
|
19
|
+
# Delete instances if the queue is empty and instances are idle.
|
20
|
+
def adjust
|
21
|
+
depth = @config[:work_queue].queue_depth
|
22
|
+
|
23
|
+
# See if we should add instances
|
24
|
+
if (depth > 0 && @instances.size == 0) || depth >= @config[:start_threshold].to_i
|
25
|
+
additional = 0
|
26
|
+
# need this many more to service the queued work
|
27
|
+
needed = (depth.to_f / @config[:start_threshold].to_f).floor
|
28
|
+
needed = 1 if needed < 1
|
29
|
+
if needed > @instances.size
|
30
|
+
additional = needed - @instances.size
|
31
|
+
end
|
32
|
+
@reporter.info("Job policy need additional #{additional} depth: #{depth} needed #{needed}")
|
33
|
+
return additional
|
34
|
+
end
|
35
|
+
|
36
|
+
# Queue not empty -- don't stop any
|
37
|
+
return 0 if depth > 0
|
38
|
+
|
39
|
+
# See if we should stop some
|
40
|
+
# count how many are active idle and active
|
41
|
+
idle = @instances.active_idle_instances
|
42
|
+
active = @instances.active_instances
|
43
|
+
if idle.size > 0
|
44
|
+
# stop some fraction of the idle instances
|
45
|
+
excess = (idle.size / @config[:idle_threshold].to_f).round
|
46
|
+
@reporter.info("Job policy need fewer #{excess} idle: #{idle.size} active #{active.size}")
|
47
|
+
return -excess
|
48
|
+
end
|
49
|
+
0
|
50
|
+
end
|
51
|
+
|
52
|
+
# uses the default apply
|
53
|
+
end
|
54
|
+
end
|
data/app/policy_limit.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
module Cloudmaster
|
2
|
+
|
3
|
+
# This enforces the limits on starting and stopping instances.
|
4
|
+
# It can be used by other policies to make sure thrie own adjust method
|
5
|
+
# does not increase or decrease by more than the configurable start limit or stop limit.
|
6
|
+
# This also provides an exaplanation for its actions.
|
7
|
+
class PolicyLimit
|
8
|
+
def initialize(reporter, config, instances)
|
9
|
+
@reporter = reporter
|
10
|
+
@config = config
|
11
|
+
@instances = instances
|
12
|
+
end
|
13
|
+
|
14
|
+
def max2(a, b)
|
15
|
+
a > b ? a : b
|
16
|
+
end
|
17
|
+
|
18
|
+
# Make sure there are at least the minimum instances running.
|
19
|
+
# Also make sure there are no more than maximum number of instances.
|
20
|
+
# Return the number to start (positive) or stop (negative) to stay within the limits.
|
21
|
+
# Do not enforce start_limit and stop_limit.
|
22
|
+
def adjust_limits
|
23
|
+
if @instances.less_than_minimum?
|
24
|
+
number_to_start = @instances.below_minimum_count
|
25
|
+
@reporter.info("Less than minimum -- start more #{number_to_start}")
|
26
|
+
number_to_start
|
27
|
+
elsif @instances.greater_than_maximum?
|
28
|
+
number_to_stop = @instances.above_maximum_count
|
29
|
+
@reporter.info("Greater than maximum -- stop some #{number_to_stop}")
|
30
|
+
-number_to_stop
|
31
|
+
else
|
32
|
+
0
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# After other policies have computed a value for adjust, then this one possibly
|
37
|
+
# modifies the value by ensuring that the start_limit and stop_limit constraints are
|
38
|
+
# honored. It also makes sure that the adjustment makes the instance count
|
39
|
+
# stay between the minimum and maximum.
|
40
|
+
def adjust(n)
|
41
|
+
case
|
42
|
+
when n > 0
|
43
|
+
start_limit = @config[:start_limit].to_i
|
44
|
+
if n > start_limit
|
45
|
+
@reporter.info("Limit start -- requested: #{n} limit: #{start_limit}")
|
46
|
+
n = start_limit
|
47
|
+
end
|
48
|
+
remaining = max2(@instances.maximum - @instances.size, 0)
|
49
|
+
if n > remaining
|
50
|
+
@reporter.info("Limit start -- requested: #{n} remaining: #{remaining}")
|
51
|
+
n = remaining
|
52
|
+
end
|
53
|
+
when n < 0
|
54
|
+
stop_limit = @config[:stop_limit].to_i
|
55
|
+
if -n > stop_limit
|
56
|
+
@reporter.info("Limit stop -- requested: #{-n} limit: #{stop_limit}")
|
57
|
+
n = -stop_limit
|
58
|
+
end
|
59
|
+
remaining = max2(@instances.size - @instances.minimum, 0)
|
60
|
+
if -n > remaining
|
61
|
+
@reporter.info("Limit stop -- requested: #{n} remaining: #{remaining}")
|
62
|
+
n = -remaining
|
63
|
+
end
|
64
|
+
end
|
65
|
+
n
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|