bluepill 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/TODO +6 -0
- data/VERSION +1 -1
- data/bluepill.gemspec +5 -2
- data/lib/bluepill.rb +4 -4
- data/lib/bluepill/application.rb +38 -58
- data/lib/bluepill/condition_watch.rb +1 -1
- data/lib/bluepill/dsl.rb +10 -2
- data/lib/bluepill/process.rb +121 -67
- data/lib/bluepill/process_conditions.rb +7 -1
- data/lib/bluepill/trigger.rb +37 -0
- data/lib/bluepill/triggers/flapping.rb +51 -0
- data/lib/bluepill/util/rotational_array.rb +30 -6
- data/lib/example.rb +8 -4
- metadata +5 -2
data/TODO
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
* Flap detection
|
2
|
+
* Figure out proper logger lvl for each msg. So a person can choose only to log local6.warn and get only state transition messages and not any of the watch chatter.
|
3
|
+
* Proper implementation of the cpu and mem watches. Possibly not forking to use ps ax?
|
4
|
+
* Better error output than just vanilla exception traces
|
5
|
+
* Better output for cli commands than just "ok"
|
6
|
+
* munin support?
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.3
|
data/bluepill.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{bluepill}
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.3"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Arya Asemanfar", "Gary Tsang", "Rohith Ravi"]
|
12
|
-
s.date = %q{2009-10-
|
12
|
+
s.date = %q{2009-10-12}
|
13
13
|
s.default_executable = %q{bluepill}
|
14
14
|
s.description = %q{Bluepill keeps your daemons up while taking up as little resources as possible. After all you probably want the resources of your server to be used by whatever daemons you are running rather than the thing that's supposed to make sure they are brought back up, should they die or misbehave.}
|
15
15
|
s.email = %q{entombedvirus@gmail.com}
|
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
|
|
26
26
|
"README",
|
27
27
|
"README.rdoc",
|
28
28
|
"Rakefile",
|
29
|
+
"TODO",
|
29
30
|
"VERSION",
|
30
31
|
"bin/bluepill",
|
31
32
|
"bluepill.gemspec",
|
@@ -46,6 +47,8 @@ Gem::Specification.new do |s|
|
|
46
47
|
"lib/bluepill/process_conditions/mem_usage.rb",
|
47
48
|
"lib/bluepill/process_conditions/process_condition.rb",
|
48
49
|
"lib/bluepill/socket.rb",
|
50
|
+
"lib/bluepill/trigger.rb",
|
51
|
+
"lib/bluepill/triggers/flapping.rb",
|
49
52
|
"lib/bluepill/util/rotational_array.rb",
|
50
53
|
"lib/example.rb",
|
51
54
|
"spec/blue-pill_spec.rb",
|
data/lib/bluepill.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
|
3
|
+
require 'thread'
|
3
4
|
require 'syslog'
|
5
|
+
|
4
6
|
require 'active_support/inflector'
|
5
7
|
require 'active_support/core_ext/hash'
|
6
8
|
|
@@ -11,12 +13,10 @@ require "bluepill/process"
|
|
11
13
|
require "bluepill/group"
|
12
14
|
require "bluepill/logger"
|
13
15
|
require "bluepill/condition_watch"
|
16
|
+
require 'bluepill/trigger'
|
17
|
+
require 'bluepill/triggers/flapping'
|
14
18
|
require "bluepill/dsl"
|
15
19
|
|
16
20
|
require "bluepill/process_conditions"
|
17
|
-
require "bluepill/process_conditions/process_condition"
|
18
|
-
require "bluepill/process_conditions/cpu_usage"
|
19
|
-
require "bluepill/process_conditions/mem_usage"
|
20
|
-
require "bluepill/process_conditions/always_true"
|
21
21
|
|
22
22
|
require "bluepill/util/rotational_array"
|
data/lib/bluepill/application.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Bluepill
|
2
2
|
class Application
|
3
3
|
attr_accessor :name, :logger, :base_dir, :socket, :pid_file
|
4
|
-
attr_accessor :groups
|
5
|
-
|
4
|
+
attr_accessor :groups, :work_queue
|
5
|
+
|
6
6
|
def initialize(name, options = {})
|
7
7
|
self.name = name
|
8
8
|
self.base_dir = options[:base_dir] ||= '/var/bluepill'
|
@@ -43,74 +43,38 @@ module Bluepill
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def stop(process_or_group_name)
|
46
|
-
|
47
|
-
group = self.groups[process_or_group_name]
|
48
|
-
|
49
|
-
if group
|
50
|
-
group.stop
|
51
|
-
|
52
|
-
else
|
53
|
-
self.groups.values.each do |group|
|
54
|
-
group.stop(process_or_group_name)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
"ok"
|
58
|
-
else
|
59
|
-
send_to_server("stop:#{process_or_group_name}")
|
60
|
-
end
|
46
|
+
send_to_process_or_group(:stop, process_or_group_name)
|
61
47
|
end
|
62
48
|
|
63
49
|
def start(process_or_group_name)
|
64
|
-
|
65
|
-
group = self.groups[process_or_group_name]
|
66
|
-
|
67
|
-
if group
|
68
|
-
group.start
|
69
|
-
|
70
|
-
else
|
71
|
-
self.groups.values.each do |group|
|
72
|
-
group.start(process_or_group_name)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
"ok"
|
76
|
-
else
|
77
|
-
send_to_server("start:#{process_or_group_name}")
|
78
|
-
end
|
50
|
+
send_to_process_or_group(:start, process_or_group_name)
|
79
51
|
end
|
80
52
|
|
81
53
|
def restart(process_or_group_name)
|
82
|
-
|
83
|
-
group = self.groups[process_or_group_name]
|
84
|
-
|
85
|
-
if group
|
86
|
-
group.restart
|
87
|
-
|
88
|
-
else
|
89
|
-
self.groups.values.each do |group|
|
90
|
-
group.restart(process_or_group_name)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
"ok"
|
94
|
-
else
|
95
|
-
send_to_server("restart:#{process_or_group_name}")
|
96
|
-
end
|
54
|
+
send_to_process_or_group(:restart, process_or_group_name)
|
97
55
|
end
|
98
56
|
|
99
57
|
def unmonitor(process_or_group_name)
|
58
|
+
send_to_process_or_group(:unmonitor, process_or_group_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def send_to_process_or_group(method, process_or_group_name, async = true)
|
100
62
|
if(@server)
|
101
|
-
|
102
|
-
|
103
|
-
if group
|
104
|
-
group.unmonitor
|
105
|
-
|
63
|
+
if async
|
64
|
+
self.work_queue.push([method, process_or_group_name])
|
106
65
|
else
|
107
|
-
self.groups
|
108
|
-
|
66
|
+
group = self.groups[process_or_group_name]
|
67
|
+
if group
|
68
|
+
group.send(method)
|
69
|
+
else
|
70
|
+
self.groups.values.each do |group|
|
71
|
+
group.send(method, process_or_group_name)
|
72
|
+
end
|
109
73
|
end
|
110
74
|
end
|
111
|
-
"ok"
|
75
|
+
return "ok"
|
112
76
|
else
|
113
|
-
send_to_server("
|
77
|
+
send_to_server("#{method}:#{process_or_group_name}")
|
114
78
|
end
|
115
79
|
end
|
116
80
|
|
@@ -158,28 +122,44 @@ private
|
|
158
122
|
end
|
159
123
|
end
|
160
124
|
end
|
161
|
-
|
125
|
+
|
126
|
+
def worker
|
127
|
+
Thread.new(self) do |app|
|
128
|
+
loop do
|
129
|
+
app.logger.info("Server | worker loop started:")
|
130
|
+
job = self.work_queue.pop
|
131
|
+
app.logger.info("Server | worker job recieved:")
|
132
|
+
send_to_process_or_group(job[0], job[1], false)
|
133
|
+
app.logger.info("Server | worker job processed:")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
162
138
|
def start_server
|
163
139
|
if File.exists?(self.pid_file)
|
164
140
|
previous_pid = File.read(self.pid_file).to_i
|
165
141
|
begin
|
142
|
+
::Process.kill(0, previous_pid)
|
166
143
|
puts "Killing previous bluepilld[#{previous_pid}]"
|
167
144
|
::Process.kill(2, previous_pid)
|
168
145
|
rescue Exception => e
|
169
146
|
exit unless e.is_a?(Errno::ESRCH)
|
170
147
|
# it was probably already dead
|
148
|
+
else
|
149
|
+
sleep 1 # wait for it to die
|
171
150
|
end
|
172
|
-
sleep 1 # wait for it to die
|
173
151
|
end
|
174
152
|
|
175
153
|
Daemonize.daemonize
|
176
154
|
|
177
155
|
@server = true
|
156
|
+
self.work_queue = Queue.new
|
178
157
|
self.socket = Bluepill::Socket.new(name, base_dir).server
|
179
158
|
File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) }
|
180
159
|
$0 = "bluepilld: #{self.name}"
|
181
160
|
self.groups.each {|name, group| group.start }
|
182
161
|
listener
|
162
|
+
worker
|
183
163
|
run
|
184
164
|
end
|
185
165
|
|
@@ -7,7 +7,7 @@ module Bluepill
|
|
7
7
|
@logger = options.delete(:logger)
|
8
8
|
@fires = options.has_key?(:fires) ? [options.delete(:fires)].flatten : [:restart]
|
9
9
|
@every = options.delete(:every)
|
10
|
-
@times = options
|
10
|
+
@times = options[:times] || [1,1]
|
11
11
|
@times = [@times, @times] unless @times.is_a?(Array) # handles :times => 5
|
12
12
|
|
13
13
|
self.clear_history!
|
data/lib/bluepill/dsl.rb
CHANGED
@@ -13,6 +13,8 @@ module Bluepill
|
|
13
13
|
def method_missing(name, *args)
|
14
14
|
if args.size == 1 && name.to_s =~ /^(.*)=$/
|
15
15
|
@attributes[$1.to_sym] = args.first
|
16
|
+
elsif args.empty? && @attributes.key?(name.to_sym)
|
17
|
+
@attributes[name.to_sym]
|
16
18
|
else
|
17
19
|
super
|
18
20
|
end
|
@@ -30,10 +32,16 @@ module Bluepill
|
|
30
32
|
def process(process_name, &process_block)
|
31
33
|
process_proxy = @@process_proxy.new
|
32
34
|
process_block.call(process_proxy)
|
35
|
+
|
33
36
|
group = process_proxy.attributes.delete(:group)
|
37
|
+
|
34
38
|
process = Bluepill::Process.new(process_name, process_proxy.attributes)
|
35
|
-
process_proxy.watches.each do |name, opts|
|
36
|
-
|
39
|
+
process_proxy.watches.each do |name, opts|
|
40
|
+
if Bluepill::Trigger[name]
|
41
|
+
process.add_trigger(name, opts)
|
42
|
+
else
|
43
|
+
process.add_watch(name, opts)
|
44
|
+
end
|
37
45
|
end
|
38
46
|
|
39
47
|
@@app.add_process(process, group)
|
data/lib/bluepill/process.rb
CHANGED
@@ -3,9 +3,20 @@ require "daemons"
|
|
3
3
|
|
4
4
|
module Bluepill
|
5
5
|
class Process
|
6
|
-
CONFIGURABLE_ATTRIBUTES = [
|
6
|
+
CONFIGURABLE_ATTRIBUTES = [
|
7
|
+
:start_command,
|
8
|
+
:stop_command,
|
9
|
+
:restart_command,
|
10
|
+
:daemonize,
|
11
|
+
:pid_file,
|
12
|
+
:start_grace_time,
|
13
|
+
:stop_grace_time,
|
14
|
+
:restart_grace_time,
|
15
|
+
:uid,
|
16
|
+
:gid
|
17
|
+
]
|
7
18
|
|
8
|
-
attr_accessor :name, :watches, :logger, :skip_ticks_until
|
19
|
+
attr_accessor :name, :watches, :triggers, :logger, :skip_ticks_until
|
9
20
|
attr_accessor *CONFIGURABLE_ATTRIBUTES
|
10
21
|
|
11
22
|
state_machine :initial => :unmonitored do
|
@@ -41,33 +52,21 @@ module Bluepill
|
|
41
52
|
|
42
53
|
after_transition any => any do |process, transition|
|
43
54
|
unless transition.loopback?
|
44
|
-
process.record_transition(transition.to_name)
|
45
|
-
# process.logger.info "Going from #{transition.from_name} => #{transition.to_name}"
|
55
|
+
process.record_transition(transition.from_name, transition.to_name)
|
46
56
|
end
|
47
57
|
end
|
48
58
|
|
49
|
-
|
50
|
-
|
51
|
-
def tick
|
52
|
-
return if self.skip_ticks_until && self.skip_ticks_until > Time.now.to_i
|
53
|
-
self.skip_ticks_until = nil
|
54
|
-
|
55
|
-
# clear the momoization per tick
|
56
|
-
@process_running = nil
|
57
|
-
|
58
|
-
# run state machine transitions
|
59
|
-
super
|
60
|
-
|
61
|
-
|
62
|
-
if process_running?
|
63
|
-
run_watches
|
59
|
+
after_transition any => any do |process, transition|
|
60
|
+
process.notify_triggers(transition)
|
64
61
|
end
|
65
62
|
end
|
66
63
|
|
67
64
|
def initialize(process_name, options = {})
|
68
65
|
@name = process_name
|
66
|
+
@event_mutex = Mutex.new
|
69
67
|
@transition_history = Util::RotationalArray.new(10)
|
70
68
|
@watches = []
|
69
|
+
@triggers = []
|
71
70
|
|
72
71
|
@stop_grace_time = @start_grace_time = @restart_grace_time = 3
|
73
72
|
|
@@ -80,40 +79,94 @@ module Bluepill
|
|
80
79
|
# Let state_machine do its initialization stuff
|
81
80
|
super()
|
82
81
|
end
|
83
|
-
|
84
|
-
def
|
85
|
-
|
82
|
+
|
83
|
+
def tick
|
84
|
+
return if self.skipping_ticks?
|
85
|
+
self.skip_ticks_until = nil
|
86
|
+
|
87
|
+
# clear the memoization per tick
|
88
|
+
@process_running = nil
|
89
|
+
|
90
|
+
# run state machine transitions
|
91
|
+
super
|
92
|
+
|
93
|
+
if process_running?
|
94
|
+
run_watches
|
95
|
+
end
|
86
96
|
end
|
87
97
|
|
88
|
-
def
|
89
|
-
|
98
|
+
def logger=(logger)
|
99
|
+
@logger = logger
|
100
|
+
self.watches.each {|w| w.logger = logger }
|
101
|
+
self.triggers.each {|t| t.logger = logger }
|
90
102
|
end
|
91
103
|
|
104
|
+
# State machine methods
|
92
105
|
def dispatch!(event)
|
93
|
-
|
106
|
+
@event_mutex.synchronize do
|
107
|
+
self.send("#{event}!")
|
108
|
+
end
|
94
109
|
end
|
95
110
|
|
96
|
-
def
|
97
|
-
@
|
98
|
-
|
111
|
+
def record_transition(from, to)
|
112
|
+
@transitioned = true
|
113
|
+
logger.info "Going from #{from} => #{to}"
|
114
|
+
self.watches.each { |w| w.clear_history! }
|
115
|
+
end
|
116
|
+
|
117
|
+
def notify_triggers(transition)
|
118
|
+
self.triggers.each {|trigger| trigger.notify(transition)}
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# Watch related methods
|
123
|
+
def add_watch(name, options = {})
|
124
|
+
self.watches << ConditionWatch.new(name, options.merge(:logger => self.logger))
|
99
125
|
end
|
100
126
|
|
127
|
+
def add_trigger(name, options = {})
|
128
|
+
self.triggers << Trigger[name].new(self, options.merge(:logger => self.logger))
|
129
|
+
end
|
130
|
+
|
131
|
+
def run_watches
|
132
|
+
now = Time.now.to_i
|
133
|
+
|
134
|
+
threads = self.watches.collect do |watch|
|
135
|
+
[watch, Thread.new { Thread.current[:events] = watch.run(self.actual_pid, now) }]
|
136
|
+
end
|
137
|
+
|
138
|
+
@transitioned = false
|
139
|
+
|
140
|
+
threads.inject([]) do |events, (watch, thread)|
|
141
|
+
thread.join
|
142
|
+
if thread[:events].size > 0
|
143
|
+
logger.info "#{watch.name} dispatched: #{thread[:events].join(',')}"
|
144
|
+
events << thread[:events]
|
145
|
+
end
|
146
|
+
events
|
147
|
+
end.flatten.uniq.each do |event|
|
148
|
+
break if @transitioned
|
149
|
+
self.dispatch!(event)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# System Process Methods
|
101
155
|
def process_running?(force = false)
|
102
156
|
@process_running = nil if force
|
103
157
|
@process_running ||= signal_process(0)
|
104
158
|
end
|
105
159
|
|
106
160
|
def start_process
|
107
|
-
self.
|
108
|
-
|
109
|
-
starter = lambda {::Kernel.exec(start_command)}
|
161
|
+
if self.daemonize?
|
162
|
+
starter = lambda { drop_privileges; ::Kernel.exec(start_command) }
|
110
163
|
child_pid = Daemonize.call_as_daemon(starter)
|
111
164
|
File.open(pid_file, "w") {|f| f.write(child_pid)}
|
112
|
-
|
113
165
|
else
|
114
166
|
# This is a self-daemonizing process
|
115
167
|
system(start_command)
|
116
168
|
end
|
169
|
+
self.clear_pid
|
117
170
|
|
118
171
|
skip_ticks_for(start_grace_time)
|
119
172
|
|
@@ -121,21 +174,22 @@ module Bluepill
|
|
121
174
|
end
|
122
175
|
|
123
176
|
def stop_process
|
124
|
-
self.clear_pid
|
125
177
|
if stop_command
|
126
178
|
system(stop_command)
|
127
179
|
else
|
128
180
|
signal_process("TERM")
|
129
|
-
|
181
|
+
|
130
182
|
wait_until = Time.now.to_i + stop_grace_time
|
131
183
|
while process_running?(true)
|
132
184
|
if wait_until <= Time.now.to_i
|
133
185
|
signal_process("KILL")
|
134
186
|
break
|
135
187
|
end
|
136
|
-
sleep 0.
|
188
|
+
sleep 0.2
|
137
189
|
end
|
138
190
|
end
|
191
|
+
self.unlink_pid
|
192
|
+
self.clear_pid
|
139
193
|
|
140
194
|
skip_ticks_for(stop_grace_time)
|
141
195
|
|
@@ -143,47 +197,18 @@ module Bluepill
|
|
143
197
|
end
|
144
198
|
|
145
199
|
def restart_process
|
146
|
-
self.clear_pid
|
147
200
|
if restart_command
|
148
201
|
system(restart_command)
|
149
202
|
skip_ticks_for(restart_grace_time)
|
150
|
-
|
203
|
+
self.clear_pid
|
151
204
|
else
|
152
205
|
stop_process
|
153
206
|
start_process
|
154
207
|
end
|
155
208
|
end
|
156
209
|
|
157
|
-
def
|
158
|
-
self.
|
159
|
-
end
|
160
|
-
|
161
|
-
def run_watches
|
162
|
-
now = Time.now.to_i
|
163
|
-
|
164
|
-
threads = self.watches.collect do |watch|
|
165
|
-
[watch, Thread.new { Thread.current[:events] = watch.run(self.actual_pid, now) }]
|
166
|
-
end
|
167
|
-
|
168
|
-
@transitioned = false
|
169
|
-
|
170
|
-
threads.inject([]) do |events, (watch, thread)|
|
171
|
-
thread.join
|
172
|
-
if thread[:events].size > 0
|
173
|
-
logger.info "#{watch.name} dispatched: #{thread[:events].join(',')}"
|
174
|
-
events << thread[:events]
|
175
|
-
end
|
176
|
-
events
|
177
|
-
end.flatten.uniq.each do |event|
|
178
|
-
break if @transitioned
|
179
|
-
self.dispatch!(event)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
def record_transition(state_name)
|
184
|
-
@transitioned = true
|
185
|
-
self.watches.each { |w| w.clear_history! }
|
186
|
-
# do other stuff here?
|
210
|
+
def daemonize?
|
211
|
+
!!self.daemonize
|
187
212
|
end
|
188
213
|
|
189
214
|
def signal_process(code)
|
@@ -199,8 +224,37 @@ module Bluepill
|
|
199
224
|
|
200
225
|
def clear_pid
|
201
226
|
@actual_pid = nil
|
227
|
+
end
|
228
|
+
|
229
|
+
def unlink_pid
|
202
230
|
File.unlink(pid_file) if File.exists?(pid_file)
|
203
231
|
end
|
232
|
+
|
233
|
+
def drop_privileges
|
234
|
+
begin
|
235
|
+
require 'etc'
|
236
|
+
|
237
|
+
uid_num = Etc.getpwnam(self.uid).uid if self.uid
|
238
|
+
gid_num = Etc.getgrnam(self.gid).gid if self.gid
|
239
|
+
|
240
|
+
::Process.groups = [gid_num] if self.gid
|
241
|
+
::Process::Sys.setgid(gid_num) if self.gid
|
242
|
+
::Process::Sys.setuid(uid_num) if self.uid
|
243
|
+
rescue ArgumentError, Errno::EPERM, Errno::ENOENT => e
|
244
|
+
# TODO: log exceptions elsewhere
|
245
|
+
File.open("/tmp/exception.log", "w+"){|f| puts e}
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
# Internal State Methods
|
251
|
+
def skip_ticks_for(seconds)
|
252
|
+
self.skip_ticks_until = (self.skip_ticks_until || Time.now.to_i) + seconds
|
253
|
+
end
|
254
|
+
|
255
|
+
def skipping_ticks?
|
256
|
+
self.skip_ticks_until && self.skip_ticks_until > Time.now.to_i
|
257
|
+
end
|
204
258
|
end
|
205
259
|
end
|
206
260
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Bluepill
|
2
|
+
class Trigger
|
3
|
+
@implementations = {}
|
4
|
+
def self.inherited(klass)
|
5
|
+
@implementations[klass.name.split('::').last.underscore.to_sym] = klass
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.[](name)
|
9
|
+
@implementations[name]
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :process, :logger
|
13
|
+
|
14
|
+
def initialize(process, options = {})
|
15
|
+
self.process = process
|
16
|
+
self.logger = options[:logger]
|
17
|
+
end
|
18
|
+
|
19
|
+
def notify(transition)
|
20
|
+
raise "Implement in subclass"
|
21
|
+
end
|
22
|
+
|
23
|
+
def dispatch!(event)
|
24
|
+
self.process.dispatch!(event)
|
25
|
+
end
|
26
|
+
|
27
|
+
def schedule_event(event, delay)
|
28
|
+
# TODO: maybe wrap this in a ScheduledEvent class with methods like cancel
|
29
|
+
Thread.new do
|
30
|
+
sleep delay
|
31
|
+
self.logger.info("Retrying from flapping")
|
32
|
+
process.dispatch!(event)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Bluepill
|
2
|
+
module Triggers
|
3
|
+
class Flapping < Bluepill::Trigger
|
4
|
+
UP_TO_DOWN = [:up, :down] # to avoid recreating this array on every notify
|
5
|
+
|
6
|
+
PARAMS = [:times, :within, :retry_in]
|
7
|
+
|
8
|
+
attr_accessor *PARAMS
|
9
|
+
attr_reader :timeline
|
10
|
+
|
11
|
+
def initialize(process, options = {})
|
12
|
+
options.reverse_merge!(:times => 5, :within => 1, :retry_in => 5)
|
13
|
+
|
14
|
+
options.each_pair do |name, val|
|
15
|
+
instance_variable_set("@#{name}", val) if PARAMS.include?(name)
|
16
|
+
end
|
17
|
+
|
18
|
+
@timeline = Util::RotationalArray.new(@times)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def notify(transition)
|
23
|
+
if [transition.from_name, transition.to_name] == UP_TO_DOWN
|
24
|
+
self.timeline << Time.now.to_i
|
25
|
+
self.check_flapping
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def check_flapping
|
30
|
+
num_occurances = (@timeline.nitems == self.times)
|
31
|
+
|
32
|
+
# The process has not flapped if we haven't encountered enough incidents
|
33
|
+
return unless num_occurances
|
34
|
+
|
35
|
+
# Check if the incident happend within the timeframe
|
36
|
+
duration = (@timeline.last - @timeline.first) <= self.within
|
37
|
+
|
38
|
+
if duration
|
39
|
+
self.logger.info "Flapping detected: retrying in #{self.retry_in} seconds"
|
40
|
+
|
41
|
+
self.schedule_event(:start, self.retry_in)
|
42
|
+
|
43
|
+
# this happens in the process' thread so we don't have to worry about concurrency issues with this event
|
44
|
+
self.dispatch!(:stop)
|
45
|
+
|
46
|
+
@timeline.clear
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -2,14 +2,18 @@ module Bluepill
|
|
2
2
|
module Util
|
3
3
|
class RotationalArray < Array
|
4
4
|
def initialize(size)
|
5
|
-
super
|
6
|
-
|
5
|
+
super(size)
|
6
|
+
|
7
|
+
@capacity = size
|
8
|
+
@counter = 0
|
7
9
|
end
|
8
10
|
|
9
11
|
def push(value)
|
10
|
-
|
11
|
-
|
12
|
-
|
12
|
+
idx = rotational_idx(@counter)
|
13
|
+
self[idx] = value
|
14
|
+
|
15
|
+
@counter += 1
|
16
|
+
self
|
13
17
|
end
|
14
18
|
|
15
19
|
alias_method :<<, :push
|
@@ -27,7 +31,27 @@ module Bluepill
|
|
27
31
|
end
|
28
32
|
|
29
33
|
def last
|
30
|
-
|
34
|
+
return if @counter.zero?
|
35
|
+
|
36
|
+
self[rotational_idx(@counter - 1)]
|
37
|
+
end
|
38
|
+
|
39
|
+
def first
|
40
|
+
return if @counter.zero?
|
41
|
+
return self[0] if @counter <= @capacity
|
42
|
+
|
43
|
+
self[rotational_idx(@counter)]
|
44
|
+
end
|
45
|
+
|
46
|
+
def clear
|
47
|
+
@counter = 0
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def rotational_idx(idx)
|
54
|
+
idx % @capacity
|
31
55
|
end
|
32
56
|
end
|
33
57
|
end
|
data/lib/example.rb
CHANGED
@@ -20,19 +20,23 @@ ROOT_DIR = "/tmp/bp"
|
|
20
20
|
|
21
21
|
|
22
22
|
Bluepill.application(:sample_app) do |app|
|
23
|
-
|
23
|
+
1.times do |i|
|
24
24
|
app.process("process_#{i}") do |process|
|
25
|
-
process.start_command = "
|
25
|
+
process.start_command = "sleep 2"
|
26
26
|
process.daemonize = true
|
27
27
|
process.pid_file = "#{ROOT_DIR}/pids/process_#{i}.pid"
|
28
|
+
process.uid = "admin"
|
29
|
+
process.gid = "staff"
|
28
30
|
|
29
|
-
|
31
|
+
|
32
|
+
# process.checks :cpu_usage, :every => 1, :below => 1, :times => [1,4]
|
33
|
+
process.checks :flapping, :times => 2, :within => 30, :retry_in => 30
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|
33
37
|
0.times do |i|
|
34
38
|
app.process("group_process_#{i}") do |process|
|
35
|
-
process.start_command = "sleep #{rand(
|
39
|
+
process.start_command = "sleep #{rand(30) + i}"
|
36
40
|
process.group = "Poopfaced"
|
37
41
|
process.daemonize = true
|
38
42
|
process.pid_file = "#{ROOT_DIR}/pids/process_#{i}.pid"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bluepill
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arya Asemanfar
|
@@ -11,7 +11,7 @@ autorequire:
|
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
13
|
|
14
|
-
date: 2009-10-
|
14
|
+
date: 2009-10-12 00:00:00 -07:00
|
15
15
|
default_executable: bluepill
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
@@ -71,6 +71,7 @@ files:
|
|
71
71
|
- README
|
72
72
|
- README.rdoc
|
73
73
|
- Rakefile
|
74
|
+
- TODO
|
74
75
|
- VERSION
|
75
76
|
- bin/bluepill
|
76
77
|
- bluepill.gemspec
|
@@ -91,6 +92,8 @@ files:
|
|
91
92
|
- lib/bluepill/process_conditions/mem_usage.rb
|
92
93
|
- lib/bluepill/process_conditions/process_condition.rb
|
93
94
|
- lib/bluepill/socket.rb
|
95
|
+
- lib/bluepill/trigger.rb
|
96
|
+
- lib/bluepill/triggers/flapping.rb
|
94
97
|
- lib/bluepill/util/rotational_array.rb
|
95
98
|
- lib/example.rb
|
96
99
|
- spec/blue-pill_spec.rb
|