bluepill 0.0.46 → 0.0.47
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/.gitignore +4 -1
- data/Gemfile +4 -0
- data/README.md +2 -2
- data/Rakefile +2 -55
- data/bin/bluepill +1 -1
- data/bin/sample_forking_server +53 -0
- data/bluepill.gemspec +21 -81
- data/{lib → examples}/example.rb +24 -24
- data/{lib → examples}/runit_example.rb +4 -3
- data/lib/bluepill.rb +6 -1
- data/lib/bluepill/application.rb +27 -26
- data/lib/bluepill/application/client.rb +2 -1
- data/lib/bluepill/application/server.rb +4 -3
- data/lib/bluepill/condition_watch.rb +25 -30
- data/lib/bluepill/controller.rb +13 -12
- data/lib/bluepill/dsl.rb +4 -151
- data/lib/bluepill/dsl/app_proxy.rb +25 -0
- data/lib/bluepill/dsl/process_factory.rb +87 -0
- data/lib/bluepill/dsl/process_proxy.rb +36 -0
- data/lib/bluepill/group.rb +9 -8
- data/lib/bluepill/logger.rb +10 -9
- data/lib/bluepill/process.rb +90 -97
- data/lib/bluepill/process_conditions.rb +2 -1
- data/lib/bluepill/process_conditions/always_true.rb +3 -2
- data/lib/bluepill/process_conditions/cpu_usage.rb +3 -2
- data/lib/bluepill/process_conditions/http.rb +1 -0
- data/lib/bluepill/process_conditions/mem_usage.rb +5 -4
- data/lib/bluepill/process_conditions/process_condition.rb +5 -4
- data/lib/bluepill/process_statistics.rb +7 -8
- data/lib/bluepill/socket.rb +6 -6
- data/lib/bluepill/system.rb +43 -42
- data/lib/bluepill/trigger.rb +10 -19
- data/lib/bluepill/triggers/flapping.rb +13 -14
- data/lib/bluepill/util/rotational_array.rb +14 -66
- data/lib/bluepill/version.rb +2 -1
- metadata +53 -46
- data/VERSION +0 -1
@@ -1,7 +1,8 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
1
2
|
module Bluepill
|
2
3
|
module Application
|
3
4
|
module ServerMethods
|
4
|
-
|
5
|
+
|
5
6
|
def status
|
6
7
|
self.processes.collect do |process|
|
7
8
|
"#{process.name} #{process.state}"
|
@@ -9,12 +10,12 @@ module Bluepill
|
|
9
10
|
end
|
10
11
|
|
11
12
|
def restart
|
12
|
-
self.socket = Bluepill::Socket.new(name, base_dir).client
|
13
|
+
self.socket = Bluepill::Socket.new(name, base_dir).client
|
13
14
|
socket.send("restart\n", 0)
|
14
15
|
end
|
15
16
|
|
16
17
|
def stop
|
17
|
-
self.socket = Bluepill::Socket.new(name, base_dir).client
|
18
|
+
self.socket = Bluepill::Socket.new(name, base_dir).client
|
18
19
|
socket.send("stop\n", 0)
|
19
20
|
end
|
20
21
|
end
|
@@ -1,55 +1,50 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
1
2
|
module Bluepill
|
3
|
+
class HistoryValue < Struct.new(:value, :critical)
|
4
|
+
end
|
5
|
+
|
2
6
|
class ConditionWatch
|
3
7
|
attr_accessor :logger, :name
|
4
8
|
EMPTY_ARRAY = [].freeze # no need to recreate one every tick
|
5
|
-
|
9
|
+
|
6
10
|
def initialize(name, options = {})
|
7
11
|
@name = name
|
8
12
|
|
9
|
-
@logger = options.delete(:logger)
|
10
|
-
@fires
|
11
|
-
@every
|
12
|
-
@times
|
13
|
-
@times
|
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
|
14
18
|
|
15
19
|
self.clear_history!
|
16
|
-
|
20
|
+
|
17
21
|
@process_condition = ProcessConditions[@name].new(options)
|
18
22
|
end
|
19
|
-
|
23
|
+
|
20
24
|
def run(pid, tick_number = Time.now.to_i)
|
21
25
|
if @last_ran_at.nil? || (@last_ran_at + @every) <= tick_number
|
22
26
|
@last_ran_at = tick_number
|
23
|
-
|
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
|
+
|
24
32
|
return @fires if self.fired?
|
25
33
|
end
|
26
34
|
EMPTY_ARRAY
|
27
35
|
end
|
28
|
-
|
29
|
-
def record_value(value)
|
30
|
-
# TODO: record value in ProcessStatistics
|
31
|
-
@history[@history_index] = [value, @process_condition.check(value)]
|
32
|
-
@history_index = (@history_index + 1) % @history.size
|
33
|
-
self.logger.info(self.to_s)
|
34
|
-
end
|
35
|
-
|
36
|
+
|
36
37
|
def clear_history!
|
37
|
-
@
|
38
|
-
@history = Array.new(@times[1])
|
39
|
-
@history_index = 0
|
38
|
+
@history = Util::RotationalArray.new(@times.last)
|
40
39
|
end
|
41
|
-
|
40
|
+
|
42
41
|
def fired?
|
43
|
-
@history.
|
42
|
+
@history.count {|v| not v.critical} >= @times.first
|
44
43
|
end
|
45
|
-
|
44
|
+
|
46
45
|
def to_s
|
47
|
-
|
48
|
-
#
|
49
|
-
# collect {|v| "#{v[0]}#{v[1] ? '' : '*'}"}.join(", ")
|
50
|
-
# but that's gross so... it's gonna be out of order till we figure out a better way to get it in order
|
51
|
-
data = @history.collect {|v| "#{@process_condition.format_value(v[0])}#{v[1] ? '' : '*'}" if v}.compact.join(", ")
|
52
|
-
"#{@name}: [#{data}]"
|
46
|
+
data = @history.collect {|v| "#{v.value}#{'*' unless v.critical}"}.join(", ")
|
47
|
+
"#{@name}: [#{data}]\n"
|
53
48
|
end
|
54
49
|
end
|
55
|
-
end
|
50
|
+
end
|
data/lib/bluepill/controller.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
1
2
|
require 'fileutils'
|
2
3
|
|
3
4
|
module Bluepill
|
4
5
|
class Controller
|
5
6
|
attr_accessor :base_dir, :log_file, :sockets_dir, :pids_dir
|
6
|
-
|
7
|
+
|
7
8
|
def initialize(options = {})
|
8
9
|
self.log_file = options[:log_file]
|
9
10
|
self.base_dir = options[:base_dir]
|
10
11
|
self.sockets_dir = File.join(base_dir, 'socks')
|
11
12
|
self.pids_dir = File.join(base_dir, 'pids')
|
12
|
-
|
13
|
+
|
13
14
|
setup_dir_structure
|
14
15
|
cleanup_bluepill_directory
|
15
16
|
end
|
16
|
-
|
17
|
+
|
17
18
|
def running_applications
|
18
19
|
Dir[File.join(sockets_dir, "*.sock")].map{|x| File.basename(x, ".sock")}
|
19
20
|
end
|
20
|
-
|
21
|
+
|
21
22
|
def handle_command(application, command, *args)
|
22
23
|
case command.to_sym
|
23
24
|
when :status
|
@@ -44,10 +45,10 @@ module Bluepill
|
|
44
45
|
when :log
|
45
46
|
log_file_location = self.send_to_daemon(application, :log_file)
|
46
47
|
log_file_location = self.log_file if log_file_location.to_s.strip.empty?
|
47
|
-
|
48
|
+
|
48
49
|
requested_pattern = args.first
|
49
50
|
grep_pattern = self.grep_pattern(application, requested_pattern)
|
50
|
-
|
51
|
+
|
51
52
|
tail = "tail -n 100 -f #{log_file_location} | grep -E '#{grep_pattern}'"
|
52
53
|
puts "Tailing log for #{requested_pattern}..."
|
53
54
|
Kernel.exec(tail)
|
@@ -56,7 +57,7 @@ module Bluepill
|
|
56
57
|
exit(1)
|
57
58
|
end
|
58
59
|
end
|
59
|
-
|
60
|
+
|
60
61
|
def send_to_daemon(application, command, *args)
|
61
62
|
begin
|
62
63
|
verify_version!(application)
|
@@ -76,13 +77,13 @@ module Bluepill
|
|
76
77
|
abort("Connection Refused: Server is not running")
|
77
78
|
end
|
78
79
|
end
|
79
|
-
|
80
|
+
|
80
81
|
def grep_pattern(application, query = nil)
|
81
82
|
pattern = [application, query].compact.join(':')
|
82
83
|
['\[.*', Regexp.escape(pattern), '.*'].compact.join
|
83
84
|
end
|
84
85
|
private
|
85
|
-
|
86
|
+
|
86
87
|
def cleanup_bluepill_directory
|
87
88
|
self.running_applications.each do |app|
|
88
89
|
pid = pid_for(app)
|
@@ -94,18 +95,18 @@ module Bluepill
|
|
94
95
|
end
|
95
96
|
end
|
96
97
|
end
|
97
|
-
|
98
|
+
|
98
99
|
def pid_for(app)
|
99
100
|
pid_file = File.join(self.pids_dir, "#{app}.pid")
|
100
101
|
File.exists?(pid_file) && File.read(pid_file).to_i
|
101
102
|
end
|
102
|
-
|
103
|
+
|
103
104
|
def setup_dir_structure
|
104
105
|
[@sockets_dir, @pids_dir].each do |dir|
|
105
106
|
FileUtils.mkdir_p(dir) unless File.exists?(dir)
|
106
107
|
end
|
107
108
|
end
|
108
|
-
|
109
|
+
|
109
110
|
def verify_version!(application)
|
110
111
|
begin
|
111
112
|
version = Socket.client_command(base_dir, application, "version")
|
data/lib/bluepill/dsl.rb
CHANGED
@@ -1,155 +1,8 @@
|
|
1
|
-
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
2
|
module Bluepill
|
3
|
-
def self.define_process_condition(name, &block)
|
4
|
-
klass = Class.new(ProcessConditions::ProcessCondition, &block)
|
5
|
-
ProcessConditions.const_set("#{name.to_s.camelcase}", klass)
|
6
|
-
end
|
7
|
-
|
8
3
|
def self.application(app_name, options = {}, &block)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
attr_reader :attributes, :watches
|
13
|
-
def initialize(process_name = nil)
|
14
|
-
@name = process_name
|
15
|
-
@attributes = {}
|
16
|
-
@watches = {}
|
17
|
-
end
|
18
|
-
|
19
|
-
def method_missing(name, *args)
|
20
|
-
if args.size == 1 && name.to_s =~ /^(.*)=$/
|
21
|
-
@attributes[$1.to_sym] = args.first
|
22
|
-
elsif args.empty? && @attributes.key?(name.to_sym)
|
23
|
-
@attributes[name.to_sym]
|
24
|
-
else
|
25
|
-
super
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def checks(name, options = {})
|
30
|
-
@watches[name] = options
|
31
|
-
end
|
32
|
-
|
33
|
-
def validate_child_process(child)
|
34
|
-
unless child.attributes.has_key?(:stop_command)
|
35
|
-
$stderr.puts "Config Error: Invalid child process monitor for #{@name}"
|
36
|
-
$stderr.puts "You must specify a stop command to monitor child processes."
|
37
|
-
exit(6)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def create_child_process_template
|
42
|
-
if @child_process_block
|
43
|
-
child_proxy = self.class.new
|
44
|
-
# Children inherit some properties of the parent
|
45
|
-
[:start_grace_time, :stop_grace_time, :restart_grace_time].each do |attribute|
|
46
|
-
child_proxy.send("#{attribute}=", @attributes[attribute]) if @attributes.key?(attribute)
|
47
|
-
end
|
48
|
-
@child_process_block.call(child_proxy)
|
49
|
-
validate_child_process(child_proxy)
|
50
|
-
@attributes[:child_process_template] = child_proxy.to_process(nil)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def monitor_children(&child_process_block)
|
55
|
-
@child_process_block = child_process_block
|
56
|
-
@attributes[:monitor_children] = true
|
57
|
-
end
|
58
|
-
|
59
|
-
def to_process(process_name)
|
60
|
-
process = Bluepill::Process.new(process_name, @attributes)
|
61
|
-
@watches.each do |name, opts|
|
62
|
-
if Bluepill::Trigger[name]
|
63
|
-
process.add_trigger(name, opts)
|
64
|
-
else
|
65
|
-
process.add_watch(name, opts)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
process
|
70
|
-
end
|
71
|
-
end
|
72
|
-
app_proxy = Class.new do
|
73
|
-
if RUBY_VERSION >= '1.9'
|
74
|
-
class_variable_set(:@@app, app)
|
75
|
-
class_variable_set(:@@process_proxy, process_proxy)
|
76
|
-
class_variable_set(:@@process_keys, Hash.new) # because I don't want to require Set just for validations
|
77
|
-
class_variable_set(:@@pid_files, Hash.new)
|
78
|
-
else
|
79
|
-
@@app = app
|
80
|
-
@@process_proxy = process_proxy
|
81
|
-
@@process_keys = Hash.new
|
82
|
-
@@pid_files = Hash.new
|
83
|
-
end
|
84
|
-
attr_accessor :working_dir, :uid, :gid, :environment
|
85
|
-
|
86
|
-
def validate_process(process, process_name)
|
87
|
-
# validate uniqueness of group:process
|
88
|
-
process_key = [process.attributes[:group], process_name].join(":")
|
89
|
-
if @@process_keys.key?(process_key)
|
90
|
-
$stderr.print "Config Error: You have two entries for the process name '#{process_name}'"
|
91
|
-
$stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
|
92
|
-
$stderr.puts
|
93
|
-
exit(6)
|
94
|
-
else
|
95
|
-
@@process_keys[process_key] = 0
|
96
|
-
end
|
97
|
-
|
98
|
-
# validate required attributes
|
99
|
-
[:start_command].each do |required_attr|
|
100
|
-
if !process.attributes.key?(required_attr)
|
101
|
-
$stderr.puts "Config Error: You must specify a #{required_attr} for '#{process_name}'"
|
102
|
-
exit(6)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# validate uniqueness of pid files
|
107
|
-
pid_key = process.pid_file.strip
|
108
|
-
if @@pid_files.key?(pid_key)
|
109
|
-
$stderr.puts "Config Error: You have two entries with the pid file: #{process.pid_file}"
|
110
|
-
exit(6)
|
111
|
-
else
|
112
|
-
@@pid_files[pid_key] = 0
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def process(process_name, &process_block)
|
117
|
-
process_proxy = @@process_proxy.new(process_name)
|
118
|
-
process_block.call(process_proxy)
|
119
|
-
process_proxy.create_child_process_template
|
120
|
-
|
121
|
-
set_app_wide_attributes(process_proxy)
|
122
|
-
|
123
|
-
assign_default_pid_file(process_proxy, process_name)
|
124
|
-
|
125
|
-
validate_process(process_proxy, process_name)
|
126
|
-
|
127
|
-
group = process_proxy.attributes.delete(:group)
|
128
|
-
process = process_proxy.to_process(process_name)
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
@@app.add_process(process, group)
|
133
|
-
end
|
134
|
-
|
135
|
-
def set_app_wide_attributes(process_proxy)
|
136
|
-
[:working_dir, :uid, :gid, :environment].each do |attribute|
|
137
|
-
unless process_proxy.attributes.key?(attribute)
|
138
|
-
process_proxy.attributes[attribute] = self.send(attribute)
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def assign_default_pid_file(process_proxy, process_name)
|
144
|
-
unless process_proxy.attributes.key?(:pid_file)
|
145
|
-
group_name = process_proxy.attributes["group"]
|
146
|
-
default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_\-]/, "_")
|
147
|
-
process_proxy.pid_file = File.join(@@app.pids_dir, default_pid_name + ".pid")
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
yield(app_proxy.new)
|
153
|
-
app.load
|
4
|
+
app_proxy = AppProxy.new(app_name, options)
|
5
|
+
yield(app_proxy)
|
6
|
+
app_proxy.app.load
|
154
7
|
end
|
155
8
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
class AppProxy
|
4
|
+
APP_ATTRIBUTES = [:working_dir, :uid, :gid, :environment]
|
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,87 @@
|
|
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
|
+
end
|
78
|
+
|
79
|
+
def validate_child_process!(child)
|
80
|
+
unless child.attributes.has_key?(:stop_command)
|
81
|
+
$stderr.puts "Config Error: Invalid child process monitor for #{child.name}"
|
82
|
+
$stderr.puts "You must specify a stop command to monitor child processes."
|
83
|
+
exit(6)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|