bluepill 0.1.2 → 0.1.4

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.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +820 -0
  3. data/CONTRIBUTING.md +31 -0
  4. data/LICENSE +1 -1
  5. data/README.md +69 -30
  6. data/bin/bluepill +2 -2
  7. data/bluepill.gemspec +17 -6
  8. data/lib/bluepill/application.rb +311 -43
  9. data/lib/bluepill/condition_watch.rb +105 -10
  10. data/lib/bluepill/controller.rb +125 -14
  11. data/lib/bluepill/dsl/app_proxy.rb +103 -4
  12. data/lib/bluepill/dsl/process_factory.rb +185 -62
  13. data/lib/bluepill/dsl/process_proxy.rb +132 -14
  14. data/lib/bluepill/dsl.rb +27 -5
  15. data/lib/bluepill/group.rb +142 -9
  16. data/lib/bluepill/logger.rb +122 -7
  17. data/lib/bluepill/process.rb +582 -211
  18. data/lib/bluepill/process_commands.rb +262 -0
  19. data/lib/bluepill/process_conditions/always_true.rb +18 -4
  20. data/lib/bluepill/process_conditions/cpu_usage.rb +34 -0
  21. data/lib/bluepill/process_conditions/file_time.rb +40 -4
  22. data/lib/bluepill/process_conditions/http.rb +81 -23
  23. data/lib/bluepill/process_conditions/mem_usage.rb +64 -5
  24. data/lib/bluepill/process_conditions/process_condition.rb +45 -2
  25. data/lib/bluepill/process_conditions/running_time.rb +36 -1
  26. data/lib/bluepill/process_conditions/zombie_process.rb +26 -5
  27. data/lib/bluepill/process_conditions.rb +15 -1
  28. data/lib/bluepill/process_journal.rb +256 -104
  29. data/lib/bluepill/process_statistics.rb +33 -3
  30. data/lib/bluepill/socket.rb +63 -16
  31. data/lib/bluepill/system/commands.rb +286 -0
  32. data/lib/bluepill/system.rb +193 -229
  33. data/lib/bluepill/trigger.rb +111 -15
  34. data/lib/bluepill/triggers/flapping.rb +86 -5
  35. data/lib/bluepill/util/rotational_array.rb +25 -2
  36. data/lib/bluepill/version.rb +71 -19
  37. data/lib/bluepill.rb +25 -2
  38. data/sig/bluepill.rbs +926 -0
  39. data/sig/manifest.yaml +7 -0
  40. metadata +16 -35
  41. data/bin/sample_forking_server +0 -66
  42. data/lib/bluepill/application/client.rb +0 -6
  43. data/lib/bluepill/application/server.rb +0 -21
@@ -3,43 +3,153 @@ require 'bluepill/system'
3
3
  require 'bluepill/process_journal'
4
4
 
5
5
  module Bluepill
6
+ # The bluepill daemon for one application
7
+ #
8
+ # It holds the application's groups of processes, ticks them every
9
+ # second, and answers the commands that the CLI sends over its socket.
10
+ #
11
+ # @api private
6
12
  class Application
7
- PROCESS_COMMANDS = [:start, :stop, :restart, :unmonitor, :status]
8
-
9
- attr_accessor :name, :logger, :base_dir, :socket, :pid_file, :kill_timeout
10
- attr_accessor :groups, :work_queue
11
- attr_accessor :pids_dir, :log_file
12
-
13
+ # The commands that act on a process or group, and that the CLI sends
14
+ PROCESS_COMMANDS = [:start, :stop, :restart, :unmonitor, :status].freeze
15
+
16
+ attr_accessor :name, :logger, :base_dir, :socket, :pid_file, :kill_timeout, :groups, :work_queue, :pids_dir, :log_file
17
+
18
+ # @!attribute [rw] name
19
+ # The application's name, which names its socket and pid file
20
+ #
21
+ # @return [String]
22
+ #
23
+ # @api private
24
+
25
+ # @!attribute [rw] logger
26
+ # The application's logger, prefixed with its name
27
+ #
28
+ # @return [Logger]
29
+ #
30
+ # @api private
31
+
32
+ # @!attribute [rw] base_dir
33
+ # The directory with the daemon's sockets, pid files and journals
34
+ #
35
+ # @return [String]
36
+ #
37
+ # @api private
38
+
39
+ # @!attribute [rw] socket
40
+ # The socket the daemon listens for commands on, once it's started
41
+ #
42
+ # @return [UNIXServer, nil]
43
+ #
44
+ # @api private
45
+
46
+ # @!attribute [rw] pid_file
47
+ # The daemon's own pid file
48
+ #
49
+ # @return [String]
50
+ #
51
+ # @api private
52
+
53
+ # @!attribute [rw] kill_timeout
54
+ # How many half seconds to wait for a previous daemon to exit
55
+ #
56
+ # @return [Integer]
57
+ #
58
+ # @api private
59
+
60
+ # @!attribute [rw] groups
61
+ # The application's groups by name, with nil for processes in no group
62
+ #
63
+ # @return [Hash{String, nil => Group}]
64
+ #
65
+ # @api private
66
+
67
+ # @!attribute [rw] work_queue
68
+ # An attribute that bluepill doesn't use
69
+ #
70
+ # @return [Object]
71
+ #
72
+ # @api private
73
+
74
+ # @!attribute [rw] pids_dir
75
+ # The directory with the pid files of the application's processes
76
+ #
77
+ # @return [String]
78
+ #
79
+ # @api private
80
+
81
+ # @!attribute [rw] log_file
82
+ # The file the daemon logs to, or nil to log to syslog
83
+ #
84
+ # @return [String, nil]
85
+ #
86
+ # @api private
87
+
88
+ # Create the daemon for an application
89
+ #
90
+ # It sets up signal traps, the logger and the pids directory. It exits
91
+ # with status 5 if it can't write to its log file or base directory.
92
+ #
93
+ # @param [String] name the application's name
94
+ # @param [Hash{Symbol => Object}] options :foreground, :log_file,
95
+ # :base_dir and :kill_timeout
96
+ #
97
+ # @return [void]
98
+ #
99
+ # @api private
13
100
  def initialize(name, options = {})
14
101
  self.name = name
15
102
 
16
103
  @foreground = options[:foreground]
17
104
  self.log_file = options[:log_file]
105
+ # Only used for a non-root user, who has a home
106
+ home = ENV['HOME'] #: String
18
107
  self.base_dir = ProcessJournal.base_dir = options[:base_dir] ||
19
108
  ENV['BLUEPILL_BASE_DIR'] ||
20
- (::Process.euid != 0 ? File.join(ENV['HOME'], '.bluepill') : '/var/run/bluepill')
21
- self.pid_file = File.join(base_dir, 'pids', self.name + '.pid')
22
- self.pids_dir = File.join(base_dir, 'pids', self.name)
109
+ (::Process.euid.zero? ? '/var/run/bluepill' : File.join(home, '.bluepill'))
110
+ self.pid_file = File.join(base_dir, 'pids', "#{name}.pid")
111
+ self.pids_dir = File.join(base_dir, 'pids', name)
23
112
  self.kill_timeout = options[:kill_timeout] || 10
24
113
 
25
114
  self.groups = {}
26
115
 
27
- self.logger = ProcessJournal.logger = Bluepill::Logger.new(log_file: log_file, stdout: foreground?).prefix_with(self.name)
28
-
29
116
  setup_signal_traps
30
- setup_pids_dir
117
+ setup_logger_and_pids_dir
31
118
 
32
119
  @mutex = Mutex.new
33
120
  end
34
121
 
122
+ # Return whether the daemon runs in the foreground
123
+ #
124
+ # @return [Boolean]
125
+ #
126
+ # @api private
35
127
  def foreground?
36
128
  !!@foreground
37
129
  end
38
130
 
39
- def mutex(&b)
40
- @mutex.synchronize(&b)
131
+ # Run a block while holding the daemon's lock
132
+ #
133
+ # Ticks and commands from the CLI hold it, so they never overlap.
134
+ #
135
+ # @yield the work to do while holding the lock
136
+ # @yieldreturn [Object] what the block returns
137
+ #
138
+ # @return [Object] what the block returns
139
+ #
140
+ # @api private
141
+ def mutex(&block)
142
+ @mutex.synchronize(&block)
41
143
  end
42
144
 
145
+ # Start the daemon and monitor the application's processes
146
+ #
147
+ # It prints what went wrong, and exits with status 5, if the daemon
148
+ # fails to start.
149
+ #
150
+ # @return [void]
151
+ #
152
+ # @api private
43
153
  def load
44
154
  start_server
45
155
  rescue StandardError => e
@@ -49,65 +159,157 @@ module Bluepill
49
159
  exit(5)
50
160
  end
51
161
 
162
+ # @!method start(group_name = nil, process_name = nil)
163
+ # Start a process or group, or every process without either
164
+ #
165
+ # @param [String, nil] group_name the group, or the process's name
166
+ # when no group has it
167
+ # @param [String, nil] process_name the process
168
+ #
169
+ # @return [Array<String>] the affected processes, as group:process
170
+ #
171
+ # @api private
172
+
173
+ # @!method stop(group_name = nil, process_name = nil)
174
+ # Stop a process or group, or every process without either
175
+ #
176
+ # @param [String, nil] group_name the group, or the process's name
177
+ # when no group has it
178
+ # @param [String, nil] process_name the process
179
+ #
180
+ # @return [Array<String>] the affected processes, as group:process
181
+ #
182
+ # @api private
183
+
184
+ # @!method restart(group_name = nil, process_name = nil)
185
+ # Restart a process or group, or every process without either
186
+ #
187
+ # @param [String, nil] group_name the group, or the process's name
188
+ # when no group has it
189
+ # @param [String, nil] process_name the process
190
+ #
191
+ # @return [Array<String>] the affected processes, as group:process
192
+ #
193
+ # @api private
194
+
195
+ # @!method unmonitor(group_name = nil, process_name = nil)
196
+ # Stop monitoring a process or group, or every process without either
197
+ #
198
+ # @param [String, nil] group_name the group, or the process's name
199
+ # when no group has it
200
+ # @param [String, nil] process_name the process
201
+ #
202
+ # @return [Array<String>] the affected processes, as group:process
203
+ #
204
+ # @api private
205
+
206
+ # @!method status(group_name = nil, process_name = nil)
207
+ # Return the status of a process or group, or of every process
208
+ #
209
+ # @param [String, nil] group_name the group, or the process's name
210
+ # when no group has it
211
+ # @param [String, nil] process_name the process
212
+ #
213
+ # @return [Array<String>] the status lines
214
+ #
215
+ # @api private
216
+
217
+ # Define a method for each of the PROCESS_COMMANDS
52
218
  PROCESS_COMMANDS.each do |command|
53
- class_eval <<-END
219
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
54
220
  def #{command}(group_name = nil, process_name = nil)
55
221
  self.send_to_process_or_group(:#{command}, group_name, process_name)
56
222
  end
57
- END
223
+ RUBY
58
224
  end
59
225
 
226
+ # Add a process to a group, which is created if it's new
227
+ #
228
+ # @param [Process] process the process
229
+ # @param [#to_s, nil] group_name the group, or nil for no group
230
+ #
231
+ # @return [Array<Process>] the group's processes
232
+ #
233
+ # @api private
60
234
  def add_process(process, group_name = nil)
61
235
  group_name = group_name.to_s if group_name
62
236
 
63
237
  groups[group_name] ||= Group.new(group_name, logger: logger.prefix_with(group_name))
64
- groups[group_name].add_process(process)
238
+ groups.fetch(group_name).add_process(process)
65
239
  end
66
240
 
241
+ # Return the daemon's version, which the CLI checks against its own
242
+ #
243
+ # @return [Module] the Version module
244
+ #
245
+ # @api private
67
246
  def version
68
- Bluepill::Version
247
+ Version
69
248
  end
70
249
 
71
250
  protected
72
251
 
252
+ # Send a command to a process, a group, or every process
253
+ #
254
+ # @param [Symbol] method the command
255
+ # @param [String, nil] group_name the group, or the start of a process
256
+ # name that contains a colon
257
+ # @param [String, nil] process_name the process
258
+ #
259
+ # @return [Array<String>] what the groups returned, flattened
260
+ #
261
+ # @api private
73
262
  def send_to_process_or_group(method, group_name, process_name)
74
263
  if group_name.nil? && process_name.nil?
75
264
  groups.values.collect do |group|
76
- group.send(method)
265
+ group.public_send(method)
77
266
  end.flatten
78
267
  elsif groups.key?(group_name)
79
- groups[group_name].send(method, process_name)
80
- elsif process_name.nil?
81
- # they must be targeting just by process name
82
- process_name = group_name
268
+ groups.fetch(group_name).public_send(method, process_name)
269
+ else
270
+ # they must be targeting just by process name, which may contain ':'
271
+ process_name = [group_name, process_name].compact.join(':')
83
272
  groups.values.collect do |group|
84
- group.send(method, process_name)
273
+ group.public_send(method, process_name)
85
274
  end.flatten
86
- else
87
- []
88
275
  end
89
276
  end
90
277
 
278
+ # Start the thread that answers commands on the socket
279
+ #
280
+ # It kills the thread that a previous call started. Each command is a
281
+ # line, such as start:group:process, and the answer is the command's
282
+ # result, or the error it raised, marshaled.
283
+ #
284
+ # @return [Thread] the listener thread
285
+ #
286
+ # @api private
91
287
  def start_listener
92
- @listener_thread.kill if @listener_thread
288
+ previous_thread = @listener_thread
289
+ previous_thread.kill if previous_thread
93
290
  @listener_thread = Thread.new do
94
291
  loop do
292
+ client = nil #: UNIXSocket?
95
293
  begin
96
- client = socket.accept
97
- client.close_on_exec = true if client.respond_to?(:close_on_exec=)
98
- command, *args = client.readline.strip.split(':')
294
+ server = socket #: UNIXServer
295
+ client = server.accept
296
+ client.close_on_exec = true if client.respond_to?(:close_on_exec=)
297
+ # The target is group:process and the process name may contain ':'
298
+ command, *args = client.readline.strip.split(':', 3).reject(&:empty?)
99
299
  response = begin
100
- mutex { send(command, *args) }
101
- rescue => e
300
+ # An empty line leaves command nil, so public_send raises an error for the client
301
+ mutex { public_send(command, *args) } # steep:ignore ArgumentTypeMismatch
302
+ rescue StandardError => e
102
303
  e
103
304
  end
104
305
  client.write(Marshal.dump(response))
105
306
  rescue StandardError => e
106
307
  logger.err(format('Got exception in cmd listener: %s `%s`', e.class.name, e.message))
107
- e.backtrace.each { |l| logger.err(l) }
308
+ Array(e.backtrace).each { |l| logger.err(l) }
108
309
  ensure
109
310
  begin
110
- client.close
311
+ # Ruby before 3.0 never counts branches in ensure as covered
312
+ client && client.close
111
313
  rescue IOError
112
314
  # closed stream
113
315
  end
@@ -116,6 +318,15 @@ module Bluepill
116
318
  end
117
319
  end
118
320
 
321
+ # Start the daemon
322
+ #
323
+ # It stops a previous daemon for the application, kills what the
324
+ # journals recorded, daemonizes unless it runs in the foreground, opens
325
+ # the socket, and runs until a signal stops it.
326
+ #
327
+ # @return [void]
328
+ #
329
+ # @api private
119
330
  def start_server
120
331
  kill_previous_bluepill
121
332
  ProcessJournal.kill_all_from_all_journals
@@ -132,26 +343,36 @@ module Bluepill
132
343
 
133
344
  $0 = "bluepilld: #{name}"
134
345
 
135
- groups.each { |_, group| group.determine_initial_state }
346
+ groups.each_value(&:determine_initial_state)
136
347
 
137
348
  write_pid_file
138
- self.socket = Bluepill::Socket.server(base_dir, name)
349
+ self.socket = Socket.server(base_dir, name)
139
350
  start_listener
140
351
 
141
352
  run
142
353
  end
143
354
 
355
+ # Tick every group once a second, until a signal stops the daemon
356
+ #
357
+ # @return [void]
358
+ #
359
+ # @api private
144
360
  def run
145
361
  @running = true # set to false by signal trap
146
362
  while @running
147
363
  mutex do
148
364
  System.reset_data
149
- groups.each { |_, group| group.tick }
365
+ groups.each_value(&:tick)
150
366
  end
151
367
  sleep 1
152
368
  end
153
369
  end
154
370
 
371
+ # Kill what the journals recorded and remove the socket and pid file
372
+ #
373
+ # @return [void]
374
+ #
375
+ # @api private
155
376
  def cleanup
156
377
  ProcessJournal.kill_all_from_all_journals
157
378
  ProcessJournal.clear_all_atomic_fs_locks
@@ -162,12 +383,17 @@ module Bluepill
162
383
  System.delete_if_exists(pid_file)
163
384
  end
164
385
 
386
+ # Trap TERM and INT to clean up and stop, and HUP to reopen the log
387
+ #
388
+ # @return [void]
389
+ #
390
+ # @api private
165
391
  def setup_signal_traps
166
392
  terminator = proc do
167
393
  puts 'Terminating...'
168
394
  cleanup
169
395
  @running = false
170
- end
396
+ end #: ^(Integer signal) -> void
171
397
 
172
398
  Signal.trap('TERM', &terminator)
173
399
  Signal.trap('INT', &terminator)
@@ -177,24 +403,61 @@ module Bluepill
177
403
  end
178
404
  end
179
405
 
406
+ # Create the logger and the pids directory
407
+ #
408
+ # Bluepill opens its log file and creates its pids dir as the user that
409
+ # runs it. The uid and gid in the config only apply to its processes.
410
+ # It exits with status 5 if it can't.
411
+ #
412
+ # @return [void]
413
+ #
414
+ # @api private
415
+ def setup_logger_and_pids_dir
416
+ self.logger = ProcessJournal.logger = Logger.new(log_file: log_file, stdout: foreground?).prefix_with(name)
417
+ setup_pids_dir
418
+ rescue SystemCallError => e
419
+ $stderr.puts("Failed to start bluepill: #{e}")
420
+ message = 'The user running bluepill must be able to write to its log_file and base_dir. ' \
421
+ 'The uid and gid in the config only apply to the processes it monitors.'
422
+ $stderr.puts(message)
423
+ exit(5)
424
+ end
425
+
426
+ # Create the pids directory, and let every user write to it
427
+ #
428
+ # @return [void]
429
+ #
430
+ # @api private
180
431
  def setup_pids_dir
181
432
  FileUtils.mkdir_p(pids_dir) unless File.exist?(pids_dir)
182
433
  # we need everybody to be able to write to the pids_dir as processes managed by
183
434
  # bluepill will be writing to this dir after they've dropped privileges
184
- FileUtils.chmod(0777, pids_dir)
435
+ FileUtils.chmod(0o777, pids_dir)
185
436
  end
186
437
 
438
+ # Interrupt a daemon for this application that's still running
439
+ #
440
+ # It waits up to kill_timeout half seconds for the daemon to exit, and
441
+ # exits with status 4 if it doesn't, or if signaling it fails.
442
+ #
443
+ # @return [void]
444
+ #
445
+ # @api private
187
446
  def kill_previous_bluepill
188
447
  return unless File.exist?(pid_file)
448
+
449
+ # An empty or garbled pid file reads as 0, and signaling pid 0 would
450
+ # interrupt bluepill's own process group
189
451
  previous_pid = File.read(pid_file).to_i
190
- return unless System.pid_alive?(previous_pid)
452
+ return unless previous_pid > 0 && System.pid_alive?(previous_pid)
453
+
191
454
  ::Process.kill(0, previous_pid)
192
455
  puts "Killing previous bluepilld[#{previous_pid}]"
193
456
  ::Process.kill(2, previous_pid)
194
- rescue => e
457
+ rescue StandardError => e
195
458
  $stderr.puts 'Encountered error trying to kill previous bluepill:'
196
- $stderr.puts "#{e.class}: #{e.message}"
197
- exit(4) unless e.is_a?(Errno::ESRCH)
459
+ $stderr.puts "#{e.class}: #{e}"
460
+ exit(4) unless e.instance_of?(Errno::ESRCH)
198
461
  else
199
462
  kill_timeout.times do |_i|
200
463
  sleep 0.5
@@ -207,6 +470,11 @@ module Bluepill
207
470
  end
208
471
  end
209
472
 
473
+ # Write the daemon's pid to its pid file
474
+ #
475
+ # @return [void]
476
+ #
477
+ # @api private
210
478
  def write_pid_file
211
479
  File.open(pid_file, 'w') { |x| x.write(::Process.pid) }
212
480
  end
@@ -1,52 +1,147 @@
1
1
  module Bluepill
2
+ # One entry of a watch's history: the value it read, as its log shows it,
3
+ # and whether the value was fine
4
+ #
5
+ # @!attribute value
6
+ # Return the value as the history shows it
7
+ #
8
+ # @return [#to_s] the formatted value
9
+ #
10
+ # @api private
11
+ #
12
+ # @!attribute critical
13
+ # Return whether the value was fine
14
+ #
15
+ # It's true for a value that passed the check, and false for one that
16
+ # counts toward the watch firing.
17
+ #
18
+ # @return [Boolean] whether the value passed the check
19
+ #
20
+ # @api private
21
+ #
22
+ # @api private
2
23
  HistoryValue = Struct.new(:value, :critical)
3
24
 
25
+ # Runs a process's check every so often, and fires its events when the
26
+ # check fails often enough
27
+ #
28
+ # A config's `checks :mem_usage, every: 10.seconds, below: 100.megabytes,
29
+ # times: [3, 5]` makes one: it runs every 10 seconds, and fires (restarts
30
+ # the process, by default) when 3 of the last 5 values failed.
31
+ #
32
+ # @api private
4
33
  class ConditionWatch
5
- attr_accessor :logger, :name
6
- EMPTY_ARRAY = [].freeze # no need to recreate one every tick
34
+ # Return the logger the watch writes its history to
35
+ #
36
+ # @return [Logger] the process's logger
37
+ #
38
+ # @api private
39
+ attr_accessor :logger
7
40
 
41
+ # Return the check's name, such as :mem_usage
42
+ #
43
+ # @return [Symbol] the name a config gave the check
44
+ #
45
+ # @api private
46
+ attr_accessor :name
47
+
48
+ no_events = [] #: Array[Symbol]
49
+
50
+ # The events of a run that fires nothing, made once instead of every tick
51
+ #
52
+ # @api private
53
+ EMPTY_ARRAY = no_events.freeze
54
+
55
+ # Create a watch of a check
56
+ #
57
+ # @param [Symbol] name the check's name, such as :mem_usage
58
+ # @param [Hash{Symbol => Object}] options the check's options. The
59
+ # watch takes :logger, :fires (the events to fire, [:restart] by
60
+ # default), :every (seconds between runs), :times (how many of the
61
+ # last values must fail, as a count or [count, of]), and
62
+ # :include_children. The check gets the rest.
63
+ #
64
+ # @return [void]
65
+ #
66
+ # @api private
8
67
  def initialize(name, options = {})
9
68
  @name = name
10
69
 
11
70
  @logger = options.delete(:logger)
12
71
  @fires = options.key?(:fires) ? Array(options.delete(:fires)) : [:restart]
13
72
  @every = options.delete(:every)
14
- @times = options.delete(:times) || [1, 1]
15
- @times = [@times, @times] unless @times.is_a?(Array) # handles times: 5
73
+ @times = Array(options.delete(:times) || 1) # handles times: 5
16
74
  @include_children = options.delete(:include_children) || false
17
75
 
18
- self.clear_history!
76
+ clear_history!
19
77
 
20
78
  @process_condition = ProcessConditions[@name].new(options)
21
79
  end
22
80
 
23
- def run(pid, tick_number = Time.now.to_i)
24
- if @last_ran_at.nil? || (@last_ran_at + @every) <= tick_number
81
+ # Run the check if it's due, and return the events to fire
82
+ #
83
+ # A check is due on its first run, and then every `every` seconds. A
84
+ # run adds the value to the history and logs the history.
85
+ #
86
+ # @param [Integer, nil] pid the process's pid
87
+ # @param [Integer] tick_number the current time, in seconds
88
+ #
89
+ # @return [Array<Symbol>] the events to fire, empty unless the check
90
+ # ran and failed often enough
91
+ #
92
+ # @raise [StandardError] whatever the check raised, other than a
93
+ # System::PsError, after logging it
94
+ #
95
+ # @api private
96
+ def run(pid, tick_number = Integer(Time.now))
97
+ last_ran_at = @last_ran_at
98
+ if last_ran_at.nil? || (last_ran_at + @every) <= tick_number
25
99
  @last_ran_at = tick_number
26
100
 
27
101
  begin
28
102
  value = @process_condition.run(pid, @include_children)
29
- rescue => e
103
+ rescue System::PsError => e
104
+ # No data this time, so leave the history alone and check again next interval
105
+ logger.warning("#{@name}: #{e}")
106
+ return EMPTY_ARRAY
107
+ rescue StandardError => e
30
108
  logger.err(e.backtrace)
31
- raise e
109
+ raise
32
110
  end
33
111
 
34
112
  @history << HistoryValue.new(@process_condition.format_value(value), @process_condition.check(value))
35
113
  logger.info(to_s)
36
114
 
37
- return @fires if self.fired?
115
+ return @fires if fired?
38
116
  end
39
117
  EMPTY_ARRAY
40
118
  end
41
119
 
120
+ # Forget the values the check read, as when the process changes state
121
+ #
122
+ # @return [Util::RotationalArray<HistoryValue>] the new, empty history
123
+ #
124
+ # @api private
42
125
  def clear_history!
43
126
  @history = Util::RotationalArray.new(@times.last)
44
127
  end
45
128
 
129
+ # Return whether enough of the last values failed for the watch to fire
130
+ #
131
+ # @return [Boolean] true when the failures reach the times the check
132
+ # allows
133
+ #
134
+ # @api private
46
135
  def fired?
47
136
  @history.count { |v| !v.critical } >= @times.first
48
137
  end
49
138
 
139
+ # Return the check's name and history, as the log shows it
140
+ #
141
+ # @return [String] such as "mem_usage: [312MB, 401MB*]", where * marks a
142
+ # value that failed
143
+ #
144
+ # @api private
50
145
  def to_s
51
146
  data = @history.collect { |v| "#{v.value}#{'*' unless v.critical}" }.join(', ')
52
147
  "#{@name}: [#{data}]\n"