kostya-bluepill 0.0.60.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +10 -0
  2. data/.rspec +1 -0
  3. data/DESIGN.md +10 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE +22 -0
  6. data/README.md +305 -0
  7. data/Rakefile +38 -0
  8. data/bin/bluepill +104 -0
  9. data/bluepill.gemspec +37 -0
  10. data/examples/example.rb +87 -0
  11. data/examples/new_example.rb +89 -0
  12. data/examples/new_runit_example.rb +29 -0
  13. data/examples/runit_example.rb +26 -0
  14. data/lib/bluepill.rb +38 -0
  15. data/lib/bluepill/application.rb +201 -0
  16. data/lib/bluepill/application/client.rb +8 -0
  17. data/lib/bluepill/application/server.rb +23 -0
  18. data/lib/bluepill/condition_watch.rb +50 -0
  19. data/lib/bluepill/controller.rb +110 -0
  20. data/lib/bluepill/dsl.rb +12 -0
  21. data/lib/bluepill/dsl/app_proxy.rb +25 -0
  22. data/lib/bluepill/dsl/process_factory.rb +122 -0
  23. data/lib/bluepill/dsl/process_proxy.rb +44 -0
  24. data/lib/bluepill/group.rb +72 -0
  25. data/lib/bluepill/process.rb +480 -0
  26. data/lib/bluepill/process_conditions.rb +14 -0
  27. data/lib/bluepill/process_conditions/always_true.rb +18 -0
  28. data/lib/bluepill/process_conditions/cpu_usage.rb +19 -0
  29. data/lib/bluepill/process_conditions/http.rb +58 -0
  30. data/lib/bluepill/process_conditions/mem_usage.rb +32 -0
  31. data/lib/bluepill/process_conditions/process_condition.rb +22 -0
  32. data/lib/bluepill/process_statistics.rb +27 -0
  33. data/lib/bluepill/socket.rb +58 -0
  34. data/lib/bluepill/system.rb +236 -0
  35. data/lib/bluepill/trigger.rb +59 -0
  36. data/lib/bluepill/triggers/flapping.rb +56 -0
  37. data/lib/bluepill/util/rotational_array.rb +20 -0
  38. data/lib/bluepill/version.rb +4 -0
  39. data/spec/lib/bluepill/process_statistics_spec.rb +24 -0
  40. data/spec/lib/bluepill/system_spec.rb +36 -0
  41. data/spec/spec_helper.rb +19 -0
  42. metadata +304 -0
@@ -0,0 +1,8 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module Application
4
+ module Client
5
+
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module Application
4
+ module ServerMethods
5
+
6
+ def status
7
+ self.processes.collect do |process|
8
+ "#{process.name} #{process.state}"
9
+ end.join("\n")
10
+ end
11
+
12
+ def restart
13
+ self.socket = Bluepill::Socket.new(name, base_dir).client
14
+ socket.send("restart\n", 0)
15
+ end
16
+
17
+ def stop
18
+ self.socket = Bluepill::Socket.new(name, base_dir).client
19
+ socket.send("stop\n", 0)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,50 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ class HistoryValue < Struct.new(:value, :critical)
4
+ end
5
+
6
+ class ConditionWatch
7
+ attr_accessor :logger, :name
8
+ EMPTY_ARRAY = [].freeze # no need to recreate one every tick
9
+
10
+ def initialize(name, options = {})
11
+ @name = name
12
+
13
+ @logger = options.delete(:logger)
14
+ @fires = options.has_key?(:fires) ? Array(options.delete(:fires)) : [:restart]
15
+ @every = options.delete(:every)
16
+ @times = options.delete(:times) || [1,1]
17
+ @times = [@times, @times] unless @times.is_a?(Array) # handles :times => 5
18
+
19
+ self.clear_history!
20
+
21
+ @process_condition = ProcessConditions[@name].new(options)
22
+ end
23
+
24
+ def run(pid, tick_number = Time.now.to_i)
25
+ if @last_ran_at.nil? || (@last_ran_at + @every) <= tick_number
26
+ @last_ran_at = tick_number
27
+
28
+ value = @process_condition.run(pid)
29
+ @history << HistoryValue.new(@process_condition.format_value(value), @process_condition.check(value))
30
+ self.logger.info(self.to_s)
31
+
32
+ return @fires if self.fired?
33
+ end
34
+ EMPTY_ARRAY
35
+ end
36
+
37
+ def clear_history!
38
+ @history = Util::RotationalArray.new(@times.last)
39
+ end
40
+
41
+ def fired?
42
+ @history.count {|v| not v.critical} >= @times.first
43
+ end
44
+
45
+ def to_s
46
+ data = @history.collect {|v| "#{v.value}#{'*' unless v.critical}"}.join(", ")
47
+ "#{@name}: [#{data}]\n"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,110 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'fileutils'
3
+
4
+ module Bluepill
5
+ class Controller
6
+ attr_accessor :base_dir, :sockets_dir, :pids_dir
7
+
8
+ def initialize(options = {})
9
+ self.base_dir = options[:base_dir] || File.join(ENV['HOME'], '.bluepill')
10
+ self.sockets_dir = File.join(base_dir, 'socks')
11
+ self.pids_dir = File.join(base_dir, 'pids')
12
+
13
+ setup_dir_structure
14
+ cleanup_bluepill_directory
15
+ end
16
+
17
+ def running_applications
18
+ Dir[File.join(sockets_dir, "*.sock")].map{|x| File.basename(x, ".sock")}
19
+ end
20
+
21
+ def handle_command(application, command, *args)
22
+ case command.to_sym
23
+ when :status
24
+ puts self.send_to_daemon(application, :status, *args)
25
+ when *Application::PROCESS_COMMANDS
26
+ # these need to be sent to the daemon and the results printed out
27
+ affected = self.send_to_daemon(application, command, *args)
28
+ if affected.empty?
29
+ puts "No processes effected"
30
+ else
31
+ puts "Sent #{command} to:"
32
+ affected.each do |process|
33
+ puts " #{process}"
34
+ end
35
+ end
36
+ when :quit
37
+ pid = pid_for(application)
38
+ if System.pid_alive?(pid)
39
+ ::Process.kill("TERM", pid)
40
+ puts "Killing bluepill[#{pid}]"
41
+ else
42
+ puts "bluepill[#{pid}] not running"
43
+ end
44
+ else
45
+ $stderr.puts "Unknown command `%s` (or application `%s` has not been loaded yet)" % [command, command]
46
+ exit(1)
47
+ end
48
+ end
49
+
50
+ def send_to_daemon(application, command, *args)
51
+ begin
52
+ verify_version!(application)
53
+
54
+ command = ([command, *args]).join(":")
55
+ response = Socket.client_command(base_dir, application, command)
56
+ if response.is_a?(Exception)
57
+ $stderr.puts "Received error from server:"
58
+ $stderr.puts response.inspect
59
+ $stderr.puts response.backtrace.join("\n")
60
+ exit(8)
61
+ else
62
+ response
63
+ end
64
+
65
+ rescue Errno::ECONNREFUSED
66
+ abort("Connection Refused: Server is not running")
67
+ end
68
+ end
69
+
70
+ def grep_pattern(application, query = nil)
71
+ pattern = [application, query].compact.join(':')
72
+ ['\[.*', Regexp.escape(pattern), '.*'].compact.join
73
+ end
74
+ private
75
+
76
+ def cleanup_bluepill_directory
77
+ self.running_applications.each do |app|
78
+ pid = pid_for(app)
79
+ if !pid || !System.pid_alive?(pid)
80
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
81
+ sock_file = File.join(self.sockets_dir, "#{app}.sock")
82
+ File.unlink(pid_file) if File.exists?(pid_file)
83
+ File.unlink(sock_file) if File.exists?(sock_file)
84
+ end
85
+ end
86
+ end
87
+
88
+ def pid_for(app)
89
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
90
+ File.exists?(pid_file) && File.read(pid_file).to_i
91
+ end
92
+
93
+ def setup_dir_structure
94
+ [@sockets_dir, @pids_dir].each do |dir|
95
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
96
+ end
97
+ end
98
+
99
+ def verify_version!(application)
100
+ begin
101
+ version = Socket.client_command(base_dir, application, "version")
102
+ if version != Bluepill::VERSION
103
+ abort("The running version of your daemon seems to be out of date.\nDaemon Version: #{version}, CLI Version: #{Bluepill::VERSION}")
104
+ end
105
+ rescue ArgumentError
106
+ abort("The running version of your daemon seems to be out of date.")
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,12 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ def self.application(app_name, options = {}, &block)
4
+ app_proxy = AppProxy.new(app_name, options)
5
+ if block.arity == 0
6
+ app_proxy.instance_eval &block
7
+ else
8
+ app_proxy.instance_exec(app_proxy, &block)
9
+ end
10
+ app_proxy.app.load
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ class AppProxy
4
+ APP_ATTRIBUTES = [:working_dir, :uid, :gid, :environment, :auto_start ]
5
+
6
+ attr_accessor *APP_ATTRIBUTES
7
+ attr_reader :app
8
+
9
+ def initialize(app_name, options)
10
+ @app = Application.new(app_name.to_s, options)
11
+ end
12
+
13
+ def process(process_name, &process_block)
14
+ attributes = {}
15
+ APP_ATTRIBUTES.each { |a| attributes[a] = self.send(a) }
16
+
17
+ process_factory = ProcessFactory.new(attributes, process_block)
18
+
19
+ process = process_factory.create_process(process_name, @app.pids_dir)
20
+ group = process_factory.attributes.delete(:group)
21
+
22
+ @app.add_process(process, group)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,122 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ class ProcessFactory
4
+ attr_reader :attributes
5
+
6
+ @@process_keys = Hash.new
7
+ @@pid_files = Hash.new
8
+
9
+ def initialize(attributes, process_block)
10
+ @attributes = attributes
11
+ @process_block = process_block
12
+ end
13
+
14
+ def create_process(name, pids_dir)
15
+ self.assign_default_pid_file(name, pids_dir)
16
+
17
+ process = ProcessProxy.new(name, @attributes, @process_block)
18
+ child_process_block = @attributes.delete(:child_process_block)
19
+ @attributes[:child_process_factory] = ProcessFactory.new(@attributes, child_process_block) if @attributes[:monitor_children]
20
+
21
+ self.validate_process! process
22
+ process.to_process
23
+ end
24
+
25
+ def create_child_process(name, pid, logger)
26
+ attributes = {}
27
+ [:start_grace_time, :stop_grace_time, :restart_grace_time].each {|a| attributes[a] = @attributes[a]}
28
+ attributes[:actual_pid] = pid
29
+ attributes[:logger] = logger
30
+
31
+ child = ProcessProxy.new(name, attributes, @process_block)
32
+ self.validate_child_process! child
33
+ process = child.to_process
34
+
35
+ process.determine_initial_state
36
+ process
37
+ end
38
+
39
+ protected
40
+
41
+ def assign_default_pid_file(process_name, pids_dir)
42
+ unless @attributes.key?(:pid_file)
43
+ group_name = @attributes[:group]
44
+ default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_\-]/, "_")
45
+ @attributes[:pid_file] = File.join(pids_dir, default_pid_name + ".pid")
46
+ end
47
+ end
48
+
49
+ def validate_process!(process)
50
+ # validate uniqueness of group:process
51
+ process_key = [process.attributes[:group], process.name].join(":")
52
+ if @@process_keys.key?(process_key)
53
+ $stderr.print "Config Error: You have two entries for the process name '#{process.name}'"
54
+ $stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
55
+ $stderr.puts
56
+ exit(6)
57
+ else
58
+ @@process_keys[process_key] = 0
59
+ end
60
+
61
+ # validate required attributes
62
+ [:start_command].each do |required_attr|
63
+ if !process.attributes.key?(required_attr)
64
+ $stderr.puts "Config Error: You must specify a #{required_attr} for '#{process.name}'"
65
+ exit(6)
66
+ end
67
+ end
68
+
69
+ # validate uniqueness of pid files
70
+ pid_key = process.attributes[:pid_file].strip
71
+ if @@pid_files.key?(pid_key)
72
+ $stderr.puts "Config Error: You have two entries with the pid file: #{pid_key}"
73
+ exit(6)
74
+ else
75
+ @@pid_files[pid_key] = 0
76
+ end
77
+
78
+ #validate stop_signals array
79
+ stop_grace_time = process.attributes[:stop_grace_time]
80
+ stop_signals = process.attributes[:stop_signals]
81
+
82
+ unless stop_signals.nil?
83
+ #Start with the more helpful error messages before the 'odd number' message.
84
+ delay_sum = 0
85
+ stop_signals.each_with_index do |s_or_d, i|
86
+ if i % 2 == 0
87
+ signal = s_or_d
88
+ unless signal.is_a? Symbol
89
+ $stderr.puts "Config Error: Invalid stop_signals! Expected a symbol (signal) at position #{i} instead of '#{signal}'."
90
+ exit(6)
91
+ end
92
+ else
93
+ delay = s_or_d
94
+ unless delay.is_a? Fixnum
95
+ $stderr.puts "Config Error: Invalid stop_signals! Expected a number (delay) at position #{i} instead of '#{delay}'."
96
+ exit(6)
97
+ end
98
+ delay_sum += delay
99
+ end
100
+ end
101
+
102
+ unless stop_signals.size % 2 == 1
103
+ $stderr.puts "Config Error: Invalid stop_signals! Expected an odd number of elements."
104
+ exit(6)
105
+ end
106
+
107
+ if stop_grace_time.nil? || stop_grace_time <= delay_sum
108
+ $stderr.puts "Config Error: Stop_grace_time should be greater than the sum of stop_signals delays!"
109
+ exit(6)
110
+ end
111
+ end
112
+ end
113
+
114
+ def validate_child_process!(child)
115
+ unless child.attributes.has_key?(:stop_command)
116
+ $stderr.puts "Config Error: Invalid child process monitor for #{child.name}"
117
+ $stderr.puts "You must specify a stop command to monitor child processes."
118
+ exit(6)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,44 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ class ProcessProxy
4
+ attr_reader :attributes, :watches, :name
5
+ def initialize(process_name, attributes, process_block)
6
+ @name = process_name
7
+ @attributes = attributes
8
+ @watches = {}
9
+
10
+ if process_block.arity == 0
11
+ instance_eval &process_block
12
+ else
13
+ instance_exec(self, &process_block)
14
+ end
15
+ end
16
+
17
+ def method_missing(name, *args)
18
+ if args.size == 1 && name.to_s =~ /^(.*)=$/
19
+ @attributes[$1.to_sym] = args.first
20
+ elsif args.size == 1
21
+ @attributes[name.to_sym] = args.first
22
+ elsif args.size == 0 && name.to_s =~ /^(.*)!$/
23
+ @attributes[$1.to_sym] = true
24
+ elsif args.empty? && @attributes.key?(name.to_sym)
25
+ @attributes[name.to_sym]
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def checks(name, options = {})
32
+ @watches[name] = options
33
+ end
34
+
35
+ def monitor_children(&child_process_block)
36
+ @attributes[:monitor_children] = true
37
+ @attributes[:child_process_block] = child_process_block
38
+ end
39
+
40
+ def to_process
41
+ Process.new(@name, @watches, @attributes)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,72 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ class Group
4
+ attr_accessor :name, :processes, :logger
5
+ attr_accessor :process_logger
6
+
7
+ def initialize(name, options = {})
8
+ self.name = name
9
+ self.processes = []
10
+ self.logger = options[:logger]
11
+ end
12
+
13
+ def add_process(process)
14
+ process.logger = self.logger
15
+ self.processes << process
16
+ end
17
+
18
+ def tick
19
+ self.processes.each do |process|
20
+ process.tick
21
+ end
22
+ end
23
+
24
+ def determine_initial_state
25
+ self.processes.each do |process|
26
+ process.determine_initial_state
27
+ end
28
+ end
29
+
30
+ # proxied events
31
+ [:start, :unmonitor, :stop, :restart].each do |event|
32
+ class_eval <<-END
33
+ def #{event}(process_name = nil)
34
+ threads = []
35
+ affected = []
36
+ self.processes.each do |process|
37
+ next if process_name && process_name != process.name
38
+ affected << [self.name, process.name].join(":")
39
+ threads << Thread.new { process.handle_user_command("#{event}") }
40
+ end
41
+ threads.each { |t| t.join }
42
+ affected
43
+ end
44
+ END
45
+ end
46
+
47
+ def status(process_name = nil)
48
+ lines = []
49
+ if process_name.nil?
50
+ prefix = self.name ? " " : ""
51
+ lines << "#{self.name}:" if self.name
52
+
53
+ self.processes.each do |process|
54
+ lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
55
+ if process.monitor_children?
56
+ process.children.each do |child|
57
+ lines << " %s%s: %s" % [prefix, child.name, child.state]
58
+ end
59
+ end
60
+ end
61
+ else
62
+ self.processes.each do |process|
63
+ next if process_name != process.name
64
+ lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
65
+ lines << process.statistics.to_s
66
+ end
67
+ end
68
+ lines << ""
69
+ end
70
+
71
+ end
72
+ end