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 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.2
1
+ 0.0.3
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{bluepill}
8
- s.version = "0.0.2"
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-05}
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",
@@ -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"
@@ -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
- if(@server)
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
- if(@server)
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
- if(@server)
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
- group = self.groups[process_or_group_name]
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.values.each do |group|
108
- group.unmonitor(process_or_group_name)
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("unmonitor:#{process_or_group_name}")
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.delete(:times) || [1,1]
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!
@@ -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
- process.add_watch(name, opts)
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)
@@ -3,9 +3,20 @@ require "daemons"
3
3
 
4
4
  module Bluepill
5
5
  class Process
6
- CONFIGURABLE_ATTRIBUTES = [:start_command, :stop_command, :restart_command, :daemonize, :pid_file, :start_grace_time, :stop_grace_time, :restart_grace_time]
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
- end
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 add_watch(name, options = {})
85
- self.watches << ConditionWatch.new(name, options.merge(:logger => self.logger))
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 daemonize?
89
- !!self.daemonize
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
- self.send("#{event}!")
106
+ @event_mutex.synchronize do
107
+ self.send("#{event}!")
108
+ end
94
109
  end
95
110
 
96
- def logger=(logger)
97
- @logger = logger
98
- self.watches.each {|w| w.logger = logger }
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.clear_pid
108
- if daemonize?
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.1
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 skip_ticks_for(seconds)
158
- self.skip_ticks_until = (self.skip_ticks_until || Time.now.to_i) + seconds
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
 
@@ -4,4 +4,10 @@ module Bluepill
4
4
  "#{self}::#{name.to_s.camelcase}".constantize
5
5
  end
6
6
  end
7
- end
7
+ end
8
+
9
+ require "bluepill/process_conditions/process_condition"
10
+ Dir["#{File.dirname(__FILE__)}/process_conditions/*.rb"].each do |pc|
11
+ require pc
12
+ end
13
+
@@ -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
- @index = 0
5
+ super(size)
6
+
7
+ @capacity = size
8
+ @counter = 0
7
9
  end
8
10
 
9
11
  def push(value)
10
- self[@index] = value
11
- @index = (@index + 1) % self.size
12
- puts @index
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
- self[@index - 1]
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
@@ -20,19 +20,23 @@ ROOT_DIR = "/tmp/bp"
20
20
 
21
21
 
22
22
  Bluepill.application(:sample_app) do |app|
23
- 2.times do |i|
23
+ 1.times do |i|
24
24
  app.process("process_#{i}") do |process|
25
- process.start_command = "echo 'Process #{i}' && sleep #{rand(15) + i}"
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
- process.checks :cpu_usage, :every => 1, :below => 1, :times => [1,4]
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(15) + i}"
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.2
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-05 00:00:00 -07:00
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