bluepill-rwgps 0.0.60

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +1 -0
  4. data/DESIGN.md +10 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE +22 -0
  7. data/README.md +10 -0
  8. data/Rakefile +38 -0
  9. data/bin/bluepill +124 -0
  10. data/bin/bpsv +3 -0
  11. data/bin/sample_forking_server +53 -0
  12. data/bluepill-rwgps.gemspec +36 -0
  13. data/examples/example.rb +87 -0
  14. data/examples/new_example.rb +89 -0
  15. data/examples/new_runit_example.rb +29 -0
  16. data/examples/runit_example.rb +26 -0
  17. data/lib/bluepill/application/client.rb +8 -0
  18. data/lib/bluepill/application/server.rb +23 -0
  19. data/lib/bluepill/application.rb +205 -0
  20. data/lib/bluepill/condition_watch.rb +50 -0
  21. data/lib/bluepill/controller.rb +121 -0
  22. data/lib/bluepill/dsl/app_proxy.rb +25 -0
  23. data/lib/bluepill/dsl/process_factory.rb +122 -0
  24. data/lib/bluepill/dsl/process_proxy.rb +44 -0
  25. data/lib/bluepill/dsl.rb +12 -0
  26. data/lib/bluepill/group.rb +72 -0
  27. data/lib/bluepill/logger.rb +63 -0
  28. data/lib/bluepill/process.rb +490 -0
  29. data/lib/bluepill/process_conditions/always_true.rb +18 -0
  30. data/lib/bluepill/process_conditions/cpu_usage.rb +19 -0
  31. data/lib/bluepill/process_conditions/http.rb +58 -0
  32. data/lib/bluepill/process_conditions/mem_usage.rb +32 -0
  33. data/lib/bluepill/process_conditions/process_condition.rb +22 -0
  34. data/lib/bluepill/process_conditions.rb +14 -0
  35. data/lib/bluepill/process_statistics.rb +27 -0
  36. data/lib/bluepill/socket.rb +58 -0
  37. data/lib/bluepill/system.rb +238 -0
  38. data/lib/bluepill/trigger.rb +60 -0
  39. data/lib/bluepill/triggers/flapping.rb +56 -0
  40. data/lib/bluepill/util/rotational_array.rb +20 -0
  41. data/lib/bluepill/version.rb +4 -0
  42. data/lib/bluepill.rb +38 -0
  43. data/local-bluepill +129 -0
  44. data/spec/lib/bluepill/logger_spec.rb +3 -0
  45. data/spec/lib/bluepill/process_statistics_spec.rb +24 -0
  46. data/spec/lib/bluepill/system_spec.rb +36 -0
  47. data/spec/spec_helper.rb +19 -0
  48. metadata +264 -0
@@ -0,0 +1,63 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ class Logger
4
+ LOG_METHODS = [:emerg, :alert, :crit, :err, :warning, :notice, :info, :debug]
5
+
6
+ def initialize(options = {})
7
+ @options = options
8
+ @logger = options[:logger] || self.create_logger
9
+ @prefix = options[:prefix]
10
+ @stdout = options[:stdout]
11
+ @prefixes = {}
12
+ end
13
+
14
+ LOG_METHODS.each do |method|
15
+ eval <<-END
16
+ def #{method}(msg, prefix = [])
17
+ if @logger.is_a?(self.class)
18
+ @logger.#{method}(msg, [@prefix] + prefix)
19
+ else
20
+ s_prefix = prefix.size > 0 ? "[\#{prefix.compact.join(':')}] " : ""
21
+ if @stdout
22
+ $stdout.puts("[#{method}]: \#{s_prefix}\#{msg}")
23
+ $stdout.flush
24
+ end
25
+ @logger.#{method}("\#{s_prefix}\#{msg}")
26
+ end
27
+ end
28
+ END
29
+ end
30
+
31
+ def prefix_with(prefix)
32
+ @prefixes[prefix] ||= self.class.new(:logger => self, :prefix => prefix)
33
+ end
34
+
35
+ def reopen
36
+ if @logger.is_a?(self.class)
37
+ @logger.reopen
38
+ else
39
+ @logger = create_logger
40
+ end
41
+ end
42
+
43
+ protected
44
+ def create_logger
45
+ if @options[:log_file]
46
+ LoggerAdapter.new(@options[:log_file])
47
+ else
48
+ Syslog.close if Syslog.opened? # need to explictly close it before reopening it
49
+ Syslog.open(@options[:identity] || 'bluepilld', Syslog::LOG_PID, Syslog::LOG_LOCAL6)
50
+ end
51
+ end
52
+
53
+ class LoggerAdapter < ::Logger
54
+ LOGGER_EQUIVALENTS =
55
+ {:debug => :debug, :err => :error, :warning => :warn, :info => :info, :emerg => :fatal, :alert => :warn, :crit => :fatal, :notice => :info}
56
+
57
+ LOG_METHODS.each do |method|
58
+ next if method == LOGGER_EQUIVALENTS[method]
59
+ alias_method method, LOGGER_EQUIVALENTS[method]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,490 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # fixes problem with loading on systems with rubyist-aasm installed
4
+ gem "state_machine"
5
+
6
+ require "state_machine"
7
+ require "daemons"
8
+
9
+ module Bluepill
10
+ class Process
11
+ CONFIGURABLE_ATTRIBUTES = [
12
+ :pre_start_command,
13
+ :start_command,
14
+ :stop_command,
15
+ :restart_command,
16
+
17
+ :stdout,
18
+ :stderr,
19
+ :stdin,
20
+
21
+ :daemonize,
22
+ :pid_file,
23
+ :working_dir,
24
+ :environment,
25
+
26
+ :start_grace_time,
27
+ :stop_grace_time,
28
+ :restart_grace_time,
29
+
30
+ :uid,
31
+ :gid,
32
+
33
+ :cache_actual_pid,
34
+
35
+ :monitor_children,
36
+ :child_process_factory,
37
+
38
+ :pid_command,
39
+ :auto_start,
40
+
41
+ :supplementary_groups,
42
+
43
+ :stop_signals
44
+ ]
45
+
46
+ attr_accessor :name, :watches, :triggers, :logger, :skip_ticks_until, :process_running
47
+ attr_accessor *CONFIGURABLE_ATTRIBUTES
48
+ attr_reader :children, :statistics
49
+
50
+ state_machine :initial => :unmonitored do
51
+ # These are the idle states, i.e. only an event (either external or internal) will trigger a transition.
52
+ # The distinction between down and unmonitored is that down
53
+ # means we know it is not running and unmonitored is that we don't care if it's running.
54
+ state :unmonitored, :up, :down
55
+
56
+ # These are transitionary states, we expect the process to change state after a certain period of time.
57
+ state :starting, :stopping, :restarting
58
+
59
+ event :tick do
60
+ transition :starting => :up, :if => :process_running?
61
+ transition :starting => :down, :unless => :process_running?
62
+
63
+ transition :up => :up, :if => :process_running?
64
+ transition :up => :down, :unless => :process_running?
65
+
66
+ # The process failed to die after entering the stopping state. Change the state to reflect
67
+ # reality.
68
+ transition :stopping => :up, :if => :process_running?
69
+ transition :stopping => :down, :unless => :process_running?
70
+
71
+ transition :down => :up, :if => :process_running?
72
+ transition :down => :starting, :unless => :process_running?
73
+
74
+ transition :restarting => :up, :if => :process_running?
75
+ transition :restarting => :down, :unless => :process_running?
76
+ end
77
+
78
+ event :start do
79
+ transition [:unmonitored, :down] => :starting
80
+ end
81
+
82
+ event :stop do
83
+ transition :up => :stopping
84
+ end
85
+
86
+ event :unmonitor do
87
+ transition any => :unmonitored
88
+ end
89
+
90
+ event :restart do
91
+ transition [:up, :down] => :restarting
92
+ end
93
+
94
+ before_transition any => any, :do => :notify_triggers
95
+ before_transition :stopping => any, :do => :clean_threads
96
+
97
+ after_transition any => :starting, :do => :start_process
98
+ after_transition any => :stopping, :do => :stop_process
99
+ after_transition any => :restarting, :do => :restart_process
100
+
101
+ after_transition any => any, :do => :record_transition
102
+ end
103
+
104
+ def initialize(process_name, checks, options = {})
105
+ @name = process_name
106
+ @event_mutex = Monitor.new
107
+ @watches = []
108
+ @triggers = []
109
+ @children = []
110
+ @threads = []
111
+ @statistics = ProcessStatistics.new
112
+ @actual_pid = options[:actual_pid]
113
+ self.logger = options[:logger]
114
+
115
+ checks.each do |name, opts|
116
+ if Trigger[name]
117
+ self.add_trigger(name, opts)
118
+ else
119
+ self.add_watch(name, opts)
120
+ end
121
+ end
122
+
123
+ # These defaults are overriden below if it's configured to be something else.
124
+ @monitor_children = false
125
+ @cache_actual_pid = true
126
+ @start_grace_time = @stop_grace_time = @restart_grace_time = 3
127
+ @environment = {}
128
+
129
+ CONFIGURABLE_ATTRIBUTES.each do |attribute_name|
130
+ self.send("#{attribute_name}=", options[attribute_name]) if options.has_key?(attribute_name)
131
+ end
132
+
133
+ # Let state_machine do its initialization stuff
134
+ super() # no arguments intentional
135
+ end
136
+
137
+ def tick
138
+ return if self.skipping_ticks?
139
+ self.skip_ticks_until = nil
140
+
141
+ # clear the memoization per tick
142
+ @process_running = nil
143
+
144
+ # Deal with thread cleanup here since the stopping state isn't used
145
+ clean_threads if self.unmonitored?
146
+
147
+ # run state machine transitions
148
+ super
149
+
150
+ if self.up?
151
+ self.run_watches
152
+
153
+ if self.monitor_children?
154
+ refresh_children!
155
+ children.each {|child| child.tick}
156
+ end
157
+ end
158
+ end
159
+
160
+ def logger=(logger)
161
+ @logger = logger
162
+ self.watches.each {|w| w.logger = logger }
163
+ self.triggers.each {|t| t.logger = logger }
164
+ end
165
+
166
+ # State machine methods
167
+ def dispatch!(event, reason = nil)
168
+ @event_mutex.synchronize do
169
+ @statistics.record_event(event, reason)
170
+ self.send("#{event}")
171
+ end
172
+ end
173
+
174
+ def record_transition(transition)
175
+ unless transition.loopback?
176
+ @transitioned = true
177
+
178
+ # When a process changes state, we should clear the memory of all the watches
179
+ self.watches.each { |w| w.clear_history! }
180
+
181
+ # Also, when a process changes state, we should re-populate its child list
182
+ if self.monitor_children?
183
+ self.logger.warning "Clearing child list"
184
+ self.children.clear
185
+ end
186
+ logger.info "Going from #{transition.from_name} => #{transition.to_name}"
187
+ end
188
+ end
189
+
190
+ def notify_triggers(transition)
191
+ self.triggers.each {|trigger| trigger.notify(transition)}
192
+ end
193
+
194
+ # Watch related methods
195
+ def add_watch(name, options = {})
196
+ self.watches << ConditionWatch.new(name, options.merge(:logger => self.logger))
197
+ end
198
+
199
+ def add_trigger(name, options = {})
200
+ self.triggers << Trigger[name].new(self, options.merge(:logger => self.logger))
201
+ end
202
+
203
+ def run_watches
204
+ now = Time.now.to_i
205
+
206
+ threads = self.watches.collect do |watch|
207
+ [watch, Thread.new { Thread.current[:events] = watch.run(self.actual_pid, now) }]
208
+ end
209
+
210
+ @transitioned = false
211
+
212
+ threads.inject([]) do |events, (watch, thread)|
213
+ thread.join
214
+ if thread[:events].size > 0
215
+ logger.info "#{watch.name} dispatched: #{thread[:events].join(',')}"
216
+ thread[:events].each do |event|
217
+ events << [event, watch.to_s]
218
+ end
219
+ end
220
+ events
221
+ end.each do |(event, reason)|
222
+ break if @transitioned
223
+ self.dispatch!(event, reason)
224
+ end
225
+ end
226
+
227
+ def determine_initial_state
228
+ if self.process_running?(true)
229
+ self.state = 'up'
230
+ else
231
+ self.state = (auto_start == false) ? 'unmonitored' : 'down' # we need to check for false value
232
+ end
233
+ end
234
+
235
+ def handle_user_command(cmd)
236
+ case cmd
237
+ when "start"
238
+ if self.process_running?(true)
239
+ logger.warning("Refusing to re-run start command on an already running process.")
240
+ else
241
+ dispatch!(:start, "user initiated")
242
+ end
243
+ when "stop"
244
+ stop_process
245
+ dispatch!(:unmonitor, "user initiated")
246
+ when "restart"
247
+ restart_process
248
+ when "unmonitor"
249
+ # When the user issues an unmonitor cmd, reset any triggers so that
250
+ # scheduled events gets cleared
251
+ triggers.each {|t| t.reset! }
252
+ dispatch!(:unmonitor, "user initiated")
253
+ end
254
+ end
255
+
256
+ # System Process Methods
257
+ def process_running?(force = false)
258
+ @process_running = nil if force # clear existing state if forced
259
+
260
+ @process_running ||= signal_process(0)
261
+ # the process isn't running, so we should clear the PID
262
+ self.clear_pid unless @process_running
263
+ @process_running
264
+ end
265
+
266
+ def start_process
267
+ pre_start_process
268
+ logger.warning "Executing start command: #{start_command}"
269
+ if self.daemonize?
270
+ System.daemonize(start_command, self.system_command_options)
271
+ else
272
+ # This is a self-daemonizing process
273
+ with_timeout(start_grace_time) do
274
+ result = System.execute_blocking(start_command, self.system_command_options)
275
+
276
+ unless result[:exit_code].zero?
277
+ logger.warning "Start command execution returned non-zero exit code:"
278
+ logger.warning result.inspect
279
+ end
280
+ end
281
+ end
282
+
283
+ self.skip_ticks_for(start_grace_time)
284
+ end
285
+
286
+ def pre_start_process
287
+ return unless pre_start_command
288
+ logger.warning "Executing pre start command: #{pre_start_command}"
289
+ result = System.execute_blocking(pre_start_command, self.system_command_options)
290
+ unless result[:exit_code].zero?
291
+ logger.warning "Pre start command execution returned non-zero exit code:"
292
+ logger.warning result.inspect
293
+ end
294
+ end
295
+
296
+ def stop_process
297
+ if stop_command
298
+ cmd = self.prepare_command(stop_command)
299
+ logger.warning "Executing stop command: #{cmd}"
300
+
301
+ with_timeout(stop_grace_time) do
302
+ result = System.execute_blocking(cmd, self.system_command_options)
303
+
304
+ unless result[:exit_code].zero?
305
+ logger.warning "Stop command execution returned non-zero exit code:"
306
+ logger.warning result.inspect
307
+ end
308
+ end
309
+
310
+ elsif stop_signals
311
+ # issue stop signals with configurable delay between each
312
+ logger.warning "Sending stop signals to #{actual_pid}"
313
+ @threads << Thread.new(self, stop_signals.clone) do |process, stop_signals|
314
+ signal = stop_signals.shift
315
+ logger.info "Sending signal #{signal} to #{process.actual_pid}"
316
+ process.signal_process(signal) # send first signal
317
+
318
+ until stop_signals.empty?
319
+ # we already checked to make sure stop_signals had an odd number of items
320
+ delay = stop_signals.shift
321
+ signal = stop_signals.shift
322
+
323
+ logger.debug "Sleeping for #{delay} seconds"
324
+ sleep delay
325
+ #break unless signal_process(0) #break unless the process can be reached
326
+ unless process.signal_process(0)
327
+ logger.debug "Process has terminated."
328
+ break
329
+ end
330
+ logger.info "Sending signal #{signal} to #{process.actual_pid}"
331
+ process.signal_process(signal)
332
+ end
333
+ end
334
+ else
335
+ logger.warning "Executing default stop command. Sending TERM signal to #{actual_pid}"
336
+ signal_process("TERM")
337
+ end
338
+ self.unlink_pid # TODO: we only write the pid file if we daemonize, should we only unlink it if we daemonize?
339
+
340
+ self.skip_ticks_for(stop_grace_time)
341
+ end
342
+
343
+ def restart_process
344
+ if restart_command
345
+ cmd = self.prepare_command(restart_command)
346
+
347
+ logger.warning "Executing restart command: #{cmd}"
348
+
349
+ with_timeout(restart_grace_time) do
350
+ result = System.execute_blocking(cmd, self.system_command_options)
351
+
352
+ unless result[:exit_code].zero?
353
+ logger.warning "Restart command execution returned non-zero exit code:"
354
+ logger.warning result.inspect
355
+ end
356
+ end
357
+
358
+ self.skip_ticks_for(restart_grace_time)
359
+ else
360
+ logger.warning "No restart_command specified. Must stop and start to restart"
361
+ self.stop_process
362
+ # the tick will bring it back.
363
+ end
364
+ end
365
+
366
+ def clean_threads
367
+ @threads.each { |t| t.kill }
368
+ @threads.clear
369
+ end
370
+
371
+ def daemonize?
372
+ !!self.daemonize
373
+ end
374
+
375
+ def monitor_children?
376
+ !!self.monitor_children
377
+ end
378
+
379
+ def signal_process(code)
380
+ code = code.to_s.upcase if code.is_a?(String) || code.is_a?(Symbol)
381
+ ::Process.kill(code, actual_pid)
382
+ true
383
+ rescue Exception => e
384
+ logger.err "Failed to signal process #{actual_pid} with code #{code}: #{e}"
385
+ false
386
+ end
387
+
388
+ def cache_actual_pid?
389
+ !!@cache_actual_pid
390
+ end
391
+
392
+ def actual_pid
393
+ pid_command ? pid_from_command : pid_from_file
394
+ end
395
+
396
+ def pid_from_file
397
+ return @actual_pid if cache_actual_pid? && @actual_pid
398
+ @actual_pid = begin
399
+ if pid_file
400
+ if File.exists?(pid_file)
401
+ str = File.read(pid_file)
402
+ str.to_i if str.size > 0
403
+ else
404
+ logger.warning("pid_file #{pid_file} does not exist or cannot be read")
405
+ nil
406
+ end
407
+ end
408
+ end
409
+ end
410
+
411
+ def pid_from_command
412
+ pid = %x{#{pid_command}}.strip
413
+ (pid =~ /\A\d+\z/) ? pid.to_i : nil
414
+ end
415
+
416
+ def actual_pid=(pid)
417
+ @actual_pid = pid
418
+ end
419
+
420
+ def clear_pid
421
+ @actual_pid = nil
422
+ end
423
+
424
+ def unlink_pid
425
+ File.unlink(pid_file) if pid_file && File.exists?(pid_file)
426
+ rescue Errno::ENOENT
427
+ end
428
+
429
+ # Internal State Methods
430
+ def skip_ticks_for(seconds)
431
+ # TODO: should this be addative or longest wins?
432
+ # i.e. if two calls for skip_ticks_for come in for 5 and 10, should it skip for 10 or 15?
433
+ self.skip_ticks_until = (self.skip_ticks_until || Time.now.to_i) + seconds.to_i
434
+ end
435
+
436
+ def skipping_ticks?
437
+ self.skip_ticks_until && self.skip_ticks_until > Time.now.to_i
438
+ end
439
+
440
+ def refresh_children!
441
+ # First prune the list of dead children
442
+ @children.delete_if {|child| !child.process_running?(true) }
443
+
444
+ # Add new found children to the list
445
+ new_children_pids = System.get_children(self.actual_pid) - @children.map {|child| child.actual_pid}
446
+
447
+ unless new_children_pids.empty?
448
+ logger.info "Existing children: #{@children.collect{|c| c.actual_pid}.join(",")}. Got new children: #{new_children_pids.inspect} for #{actual_pid}"
449
+ end
450
+
451
+ # Construct a new process wrapper for each new found children
452
+ new_children_pids.each do |child_pid|
453
+ name = "<child(pid:#{child_pid})>"
454
+ logger = self.logger.prefix_with(name)
455
+
456
+ child = self.child_process_factory.create_child_process(name, child_pid, logger)
457
+ @children << child
458
+ end
459
+ end
460
+
461
+ def prepare_command(command)
462
+ command.to_s.gsub("{{PID}}", actual_pid.to_s)
463
+ end
464
+
465
+ def system_command_options
466
+ {
467
+ :uid => self.uid,
468
+ :gid => self.gid,
469
+ :working_dir => self.working_dir,
470
+ :environment => self.environment,
471
+ :pid_file => self.pid_file,
472
+ :logger => self.logger,
473
+ :stdin => self.stdin,
474
+ :stdout => self.stdout,
475
+ :stderr => self.stderr,
476
+ :supplementary_groups => self.supplementary_groups
477
+ }
478
+ end
479
+
480
+ def with_timeout(secs, &blk)
481
+ Timeout.timeout(secs.to_f, &blk)
482
+
483
+ rescue Timeout::Error
484
+ logger.err "Execution is taking longer than expected. Unmonitoring."
485
+ logger.err "Did you forget to tell bluepill to daemonize this process?"
486
+ self.dispatch!("unmonitor")
487
+ end
488
+ end
489
+ end
490
+
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module ProcessConditions
4
+ class AlwaysTrue < ProcessCondition
5
+ def initialize(options = {})
6
+ @below = options[:below]
7
+ end
8
+
9
+ def run(pid)
10
+ 1
11
+ end
12
+
13
+ def check(value)
14
+ true
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module ProcessConditions
4
+ class CpuUsage < ProcessCondition
5
+ def initialize(options = {})
6
+ @below = options[:below]
7
+ end
8
+
9
+ def run(pid)
10
+ # third col in the ps axu output
11
+ System.cpu_usage(pid).to_f
12
+ end
13
+
14
+ def check(value)
15
+ value < @below
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module Bluepill
6
+ module ProcessConditions
7
+ class Http < ProcessCondition
8
+ def initialize(options = {})
9
+ @uri = URI.parse(options[:url])
10
+ @kind = case options[:kind]
11
+ when Fixnum then Net::HTTPResponse::CODE_TO_OBJ[options[:kind].to_s]
12
+ when String, Symbol then Net.const_get("HTTP#{options[:kind].to_s.camelize}")
13
+ else
14
+ Net::HTTPSuccess
15
+ end
16
+ @pattern = options[:pattern] || nil
17
+ @open_timeout = (options[:open_timeout] || options[:timeout] || 5).to_i
18
+ @read_timeout = (options[:read_timeout] || options[:timeout] || 5).to_i
19
+ end
20
+
21
+ def run(pid)
22
+ session = Net::HTTP.new(@uri.host, @uri.port)
23
+ if @uri.scheme == 'https'
24
+ require 'net/https'
25
+ session.use_ssl=true
26
+ session.verify_mode = OpenSSL::SSL::VERIFY_NONE
27
+ end
28
+ session.open_timeout = @open_timeout
29
+ session.read_timeout = @read_timeout
30
+ hide_net_http_bug do
31
+ session.start do |http|
32
+ http.get(@uri.path)
33
+ end
34
+ end
35
+ rescue
36
+ $!
37
+ end
38
+
39
+ def check(value)
40
+ return false unless value.kind_of?(@kind)
41
+ return true unless @pattern
42
+ return false unless value.class.body_permitted?
43
+ @pattern === value.body
44
+ end
45
+
46
+ private
47
+ def hide_net_http_bug
48
+ yield
49
+ rescue NoMethodError => e
50
+ if e.to_s =~ /#{Regexp.escape(%q|undefined method `closed?' for nil:NilClass|)}/
51
+ raise Errno::ECONNREFUSED, "Connection refused attempting to contact #{@uri.scheme}://#{@uri.host}:#{@uri.port}"
52
+ else
53
+ raise
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module ProcessConditions
4
+ class MemUsage < ProcessCondition
5
+ MB = 1024 ** 2
6
+ FORMAT_STR = "%d%s"
7
+ MB_LABEL = "MB"
8
+ KB_LABEL = "KB"
9
+
10
+ def initialize(options = {})
11
+ @below = options[:below]
12
+ end
13
+
14
+ def run(pid)
15
+ # rss is on the 5th col
16
+ System.memory_usage(pid).to_f
17
+ end
18
+
19
+ def check(value)
20
+ value.kilobytes < @below
21
+ end
22
+
23
+ def format_value(value)
24
+ if value.kilobytes >= MB
25
+ FORMAT_STR % [(value / 1024).round, MB_LABEL]
26
+ else
27
+ FORMAT_STR % [value, KB_LABEL]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module ProcessConditions
4
+ class ProcessCondition
5
+ def initialize(options = {})
6
+ @options = options
7
+ end
8
+
9
+ def run(pid)
10
+ raise "Implement in subclass!"
11
+ end
12
+
13
+ def check(value)
14
+ raise "Implement in subclass!"
15
+ end
16
+
17
+ def format_value(value)
18
+ value
19
+ end
20
+ end
21
+ end
22
+ end