bluepill 0.1.3 → 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 +64 -29
  6. data/bin/bluepill +1 -1
  7. data/bluepill.gemspec +17 -6
  8. data/lib/bluepill/application.rb +309 -41
  9. data/lib/bluepill/condition_watch.rb +103 -8
  10. data/lib/bluepill/controller.rb +124 -13
  11. data/lib/bluepill/dsl/app_proxy.rb +102 -3
  12. data/lib/bluepill/dsl/process_factory.rb +183 -60
  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 +120 -5
  17. data/lib/bluepill/process.rb +571 -200
  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 +61 -2
  24. data/lib/bluepill/process_conditions/process_condition.rb +43 -0
  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 +192 -228
  33. data/lib/bluepill/trigger.rb +110 -14
  34. data/lib/bluepill/triggers/flapping.rb +84 -3
  35. data/lib/bluepill/util/rotational_array.rb +24 -1
  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 -34
  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
@@ -0,0 +1,262 @@
1
+ module Bluepill
2
+ # Starts, stops and restarts a Process with its configured commands
3
+ #
4
+ # @api private
5
+ module ProcessCommands
6
+ # Start the process with its start command
7
+ #
8
+ # Anything still running from an earlier start is killed first, and the
9
+ # pre_start_command runs. A process bluepill daemonizes has its pid added
10
+ # to the journal. A self-daemonizing one gets start_grace_time to finish,
11
+ # and then on_start_timeout is dispatched. Ticks are skipped for
12
+ # start_grace_time either way.
13
+ #
14
+ # @return [Integer] when ticks resume, in epoch seconds
15
+ #
16
+ # @api private
17
+ def start_process
18
+ # An old stop signal thread would otherwise signal and clean up the new process
19
+ clean_threads
20
+ ProcessJournal.kill_all_from_journal(name) # be sure nothing else is running from previous runs
21
+ pre_start_process
22
+ # ProcessFactory makes sure every process has a start command
23
+ command = start_command #: String
24
+ logger.warning "Executing start command: #{command}"
25
+ if daemonize?
26
+ daemon_id = System.daemonize(command, system_command_options)
27
+ if daemon_id
28
+ ProcessJournal.append_pid_to_journal(name, daemon_id)
29
+ # Only refresh_children! fills children, and only when monitoring them
30
+ children.each do |child|
31
+ ProcessJournal.append_pid_to_journal(name, child.actual_pid)
32
+ end
33
+ end
34
+ else
35
+ # This is a self-daemonizing process
36
+ with_timeout(start_grace_time, on_start_timeout) do
37
+ run_command(command, 'Start')
38
+ end
39
+ end
40
+
41
+ skip_ticks_for(start_grace_time)
42
+ end
43
+
44
+ # Run the pre_start_command, if there is one
45
+ #
46
+ # @return [void]
47
+ #
48
+ # @api private
49
+ def pre_start_process
50
+ return unless pre_start_command
51
+
52
+ logger.warning "Executing pre start command: #{pre_start_command}"
53
+ run_command(pre_start_command, 'Pre start')
54
+ end
55
+
56
+ # Stop the process with its stop command, stop signals, or TERM
57
+ #
58
+ # The children's pids go in the journal first, so cleanup kills them too.
59
+ # Stop signals are sent from a thread of their own, with their delays.
60
+ # Ticks are skipped for stop_grace_time.
61
+ #
62
+ # @return [Integer] when ticks resume, in epoch seconds
63
+ #
64
+ # @api private
65
+ def stop_process
66
+ if monitor_children
67
+ System.get_children(actual_pid).each do |child_pid|
68
+ ProcessJournal.append_pid_to_journal(name, child_pid)
69
+ end
70
+ end
71
+
72
+ if stop_command
73
+ cmd = prepare_command(stop_command)
74
+ logger.warning "Executing stop command: #{cmd}"
75
+
76
+ with_timeout(stop_grace_time, 'stop') do
77
+ run_command(cmd, 'Stop')
78
+ end
79
+ cleanup_process
80
+ elsif stop_signals
81
+ # issue stop signals with configurable delay between each
82
+ logger.warning "Sending stop signals to #{actual_pid}"
83
+ @threads << Thread.new(stop_signals.clone) { |signals| send_stop_signals(signals) }
84
+ else
85
+ logger.warning "Executing default stop command. Sending TERM signal to #{actual_pid}"
86
+ signal_process('TERM')
87
+ cleanup_process
88
+ end
89
+
90
+ skip_ticks_for(stop_grace_time)
91
+ end
92
+
93
+ # Restart the process with its restart command, or by stopping and starting
94
+ #
95
+ # @return [Integer] when ticks resume, in epoch seconds
96
+ #
97
+ # @api private
98
+ def restart_process
99
+ if restart_command
100
+ cmd = prepare_command(restart_command)
101
+
102
+ logger.warning "Executing restart command: #{cmd}"
103
+
104
+ with_timeout(restart_grace_time, 'restart') do
105
+ run_command(cmd, 'Restart')
106
+ end
107
+
108
+ skip_ticks_for(restart_grace_time)
109
+ else
110
+ logger.warning 'No restart_command specified. Must stop and start to restart'
111
+ stop_process
112
+ # Stop signals run in a thread. Wait for them, or start_process would
113
+ # kill the stopping process from the journal before its delays are up.
114
+ @threads.each(&:join)
115
+ start_process
116
+ end
117
+ end
118
+
119
+ # Kill what's left of the process, and delete its pid file
120
+ #
121
+ # Bluepill only writes the pid file for processes it daemonizes, but it
122
+ # removes it for every process, so a stopped process never leaves a stale
123
+ # pid behind for a later process to reuse.
124
+ #
125
+ # @return [void]
126
+ #
127
+ # @api private
128
+ def cleanup_process
129
+ ProcessJournal.kill_all_from_journal(name) # finish cleanup
130
+ unlink_pid
131
+ end
132
+
133
+ # Return a command with <tt>{{PID}}</tt> replaced by the process's pid
134
+ #
135
+ # @param [String] command the stop or restart command
136
+ #
137
+ # @return [String]
138
+ #
139
+ # @api private
140
+ def prepare_command(command)
141
+ command.gsub('{{PID}}', actual_pid.to_s)
142
+ end
143
+
144
+ # Return the options the process's commands run with
145
+ #
146
+ # @return [Hash{Symbol => Object}] the uid, gid, working_dir, environment,
147
+ # pid_file, logger, stdin, stdout, stderr and supplementary_groups
148
+ #
149
+ # @api private
150
+ def system_command_options
151
+ {
152
+ uid: uid,
153
+ gid: gid,
154
+ working_dir: working_dir,
155
+ environment: environment,
156
+ pid_file: pid_file,
157
+ logger: logger,
158
+ stdin: stdin,
159
+ stdout: stdout,
160
+ stderr: stderr,
161
+ supplementary_groups: supplementary_groups,
162
+ }
163
+ end
164
+
165
+ # Run a block, and dispatch an event if it takes too long
166
+ #
167
+ # @param [Integer, Float] secs how long the block may take
168
+ # @param [String, Symbol] next_state the event to dispatch if it times out
169
+ #
170
+ # @yield the work to time
171
+ # @yieldparam [Numeric] secs the time limit
172
+ #
173
+ # @return [Object, Boolean] what the block returned, or whether the event's
174
+ # transition happened
175
+ #
176
+ # @api private
177
+ def with_timeout(secs, next_state, &blk)
178
+ Timeout.timeout(secs, &blk)
179
+ rescue Timeout::Error
180
+ logger.err 'Execution is taking longer than expected.'
181
+ logger.err 'Did you forget to tell bluepill to daemonize this process?'
182
+ dispatch!(next_state)
183
+ end
184
+
185
+ # Run a command, and wait for it to finish
186
+ #
187
+ # A command that fails, or that a signal kills, is logged as a warning.
188
+ #
189
+ # @param [String] cmd the command
190
+ # @param [String] description what the command is, for the log, such as
191
+ # Start
192
+ #
193
+ # @return [void]
194
+ #
195
+ # @api private
196
+ def run_command(cmd, description)
197
+ result = System.execute_blocking(cmd, system_command_options)
198
+ # The exit code is nil when a signal killed the command
199
+ exit_code = result.fetch(:exit_code)
200
+ return if exit_code && exit_code.zero?
201
+
202
+ logger.warning "#{description} command execution returned non-zero exit code:"
203
+ logger.warning result.inspect
204
+ end
205
+
206
+ # Send stop signals, with their delays, until the process stops
207
+ #
208
+ # Sends the first signal, then each later one after its delay, as long as
209
+ # the process is still running. Then gives the last signal the rest of
210
+ # stop_grace_time to work before cleaning up.
211
+ #
212
+ # @param [Array<Symbol, Integer>] signals signals with the delays between
213
+ #
214
+ # @return [void]
215
+ #
216
+ # @api private
217
+ def send_stop_signals(signals)
218
+ remaining = stop_grace_time
219
+ signal = signals.shift
220
+ logger.info "Sending signal #{signal} to #{actual_pid}"
221
+ signal_process(signal) # send first signal
222
+
223
+ until signals.empty?
224
+ # we already checked to make sure signals had an odd number of items
225
+ delay = signals.shift #: Integer
226
+ signal = signals.shift
227
+ remaining -= delay
228
+
229
+ logger.debug "Sleeping for #{delay} seconds"
230
+ sleep delay
231
+ # break unless signal_process(0) #break unless the process can be reached
232
+ unless signal_process(0)
233
+ logger.debug 'Process has terminated.'
234
+ return cleanup_process
235
+ end
236
+ logger.info "Sending signal #{signal} to #{actual_pid}"
237
+ signal_process(signal)
238
+ end
239
+ wait_for_stop(remaining)
240
+ cleanup_process
241
+ end
242
+
243
+ # Wait for the process to exit, checking once a second
244
+ #
245
+ # It stops 2 seconds short of the grace time: ticks resume up to a second
246
+ # early (they count whole seconds) and can kill this thread, and the
247
+ # cleanup itself takes up to another second.
248
+ #
249
+ # @param [Integer, Float] seconds the grace time that's left
250
+ #
251
+ # @return [void]
252
+ #
253
+ # @api private
254
+ def wait_for_stop(seconds)
255
+ (seconds - 2).ceil.times do
256
+ break unless signal_process(0)
257
+
258
+ sleep 1
259
+ end
260
+ end
261
+ end
262
+ end
@@ -1,14 +1,28 @@
1
1
  module Bluepill
2
2
  module ProcessConditions
3
+ # A check that always passes, for testing a config: `checks :always_true`
4
+ #
5
+ # @api private
3
6
  class AlwaysTrue < ProcessCondition
4
- def initialize(options = {})
5
- @below = options[:below]
6
- end
7
-
7
+ # Return a value that's always fine
8
+ #
9
+ # @param [Integer, nil] _pid the process's pid
10
+ # @param [Boolean] _include_children whether to include its children
11
+ #
12
+ # @return [Integer] always 1
13
+ #
14
+ # @api private
8
15
  def run(_pid, _include_children)
9
16
  1
10
17
  end
11
18
 
19
+ # Pass whatever the value is
20
+ #
21
+ # @param [Integer] _value what run read
22
+ #
23
+ # @return [Boolean] always true
24
+ #
25
+ # @api private
12
26
  def check(_value)
13
27
  true
14
28
  end
@@ -1,15 +1,49 @@
1
1
  module Bluepill
2
2
  module ProcessConditions
3
+ # Checks a process's CPU usage, as a percentage
4
+ #
5
+ # A config names it `checks :cpu_usage`, with below: the highest
6
+ # percentage that's fine, and include_children: true to add the
7
+ # process's children to it.
8
+ #
9
+ # @example
10
+ # process.checks :cpu_usage, every: 10.seconds, below: 5, times: 3
11
+ #
12
+ # @api private
3
13
  class CpuUsage < ProcessCondition
14
+ # Create the check
15
+ #
16
+ # @param [Hash{Symbol => Object}] options the check's options, with
17
+ # :below, the highest percentage that's fine
18
+ #
19
+ # @return [void]
20
+ #
21
+ # @api private
4
22
  def initialize(options = {})
23
+ super
5
24
  @below = options[:below]
6
25
  end
7
26
 
27
+ # Read the process's CPU usage from the tick's ps table
28
+ #
29
+ # @param [Integer, nil] pid the process's pid
30
+ # @param [Boolean] include_children whether to add its children's usage
31
+ #
32
+ # @return [Float] the percentage, or 0.0 for a process ps doesn't list
33
+ #
34
+ # @api private
8
35
  def run(pid, include_children)
9
36
  # third col in the ps axu output
10
37
  System.cpu_usage(pid, include_children).to_f
11
38
  end
12
39
 
40
+ # Return whether the usage is below the limit
41
+ #
42
+ # @param [Float] value the percentage run read
43
+ #
44
+ # @return [Boolean] true when it's below the limit
45
+ #
46
+ # @api private
13
47
  def check(value)
14
48
  value < @below
15
49
  end
@@ -1,20 +1,56 @@
1
1
  module Bluepill
2
2
  module ProcessConditions
3
+ # Checks how long ago a file changed, in seconds
4
+ #
5
+ # A config names it `checks :file_time`, with filename: the file, and
6
+ # below: the longest it may go without changing (3.minutes, say), to
7
+ # restart a process that stopped writing its log or heartbeat file.
8
+ #
9
+ # @example
10
+ # process.checks :file_time, every: 60.seconds, below: 3.minutes, filename: "/tmp/some_file.log", times: 2
11
+ #
12
+ # @api private
3
13
  class FileTime < ProcessCondition
14
+ # Create the check
15
+ #
16
+ # @param [Hash{Symbol => Object}] options the check's options, with
17
+ # :filename, the file, and :below, the longest it may go without
18
+ # changing, in seconds
19
+ #
20
+ # @return [void]
21
+ #
22
+ # @api private
4
23
  def initialize(options = {})
24
+ super
5
25
  @below = options[:below]
6
26
  @filename = options[:filename]
7
27
  end
8
28
 
29
+ # Read the seconds since the file changed
30
+ #
31
+ # @param [Integer, nil] _pid the process's pid
32
+ # @param [Boolean] _include_children whether to include its children
33
+ #
34
+ # @return [Float, StandardError, nil] the seconds, nil when the file
35
+ # doesn't exist, or the error that stopped bluepill from finding out
36
+ #
37
+ # @api private
9
38
  def run(_pid, _include_children)
10
39
  Time.now - File.mtime(@filename) if File.exist?(@filename)
11
- rescue
12
- $ERROR_INFO
40
+ rescue StandardError => e
41
+ e
13
42
  end
14
43
 
44
+ # Return whether the file changed recently enough
45
+ #
46
+ # @param [Float, StandardError, nil] value what run read
47
+ #
48
+ # @return [Boolean] true when the seconds are below the limit, false
49
+ # for a missing file or an error
50
+ #
51
+ # @api private
15
52
  def check(value)
16
- return false if value.nil?
17
- value < @below
53
+ value.is_a?(Numeric) && value < @below
18
54
  end
19
55
  end
20
56
  end
@@ -3,27 +3,63 @@ require 'uri'
3
3
 
4
4
  module Bluepill
5
5
  module ProcessConditions
6
+ # Checks that a URL answers as expected
7
+ #
8
+ # A config names it `checks :http`, with url: the URL to get, kind: the
9
+ # response it should be (a Net::HTTP class name such as :success or
10
+ # :ok, or a status code such as 200, :success by default), pattern: a
11
+ # Regexp its body should match, and timeout: (or open_timeout: and
12
+ # read_timeout:) in seconds, 5 by default.
13
+ #
14
+ # @example
15
+ # process.checks :http, url: 'http://localhost:4567/', kind: :success, pattern: /html/, timeout: 3.seconds
16
+ #
17
+ # @api private
6
18
  class Http < ProcessCondition
19
+ # Create the check
20
+ #
21
+ # @param [Hash{Symbol => Object}] options the check's options, with
22
+ # :url, :kind, :pattern, :timeout, :open_timeout and :read_timeout
23
+ #
24
+ # @return [void]
25
+ #
26
+ # @raise [KeyError] for a numeric kind that isn't an HTTP status
27
+ #
28
+ # @raise [NameError] for a kind that names no Net::HTTP response
29
+ #
30
+ # @api private
7
31
  def initialize(options = {})
8
- @uri = URI.parse(options[:url])
9
- @kind = begin
10
- case options[:kind]
11
- when Fixnum
12
- Net::HTTPResponse::CODE_TO_OBJ[options[:kind].to_s]
13
- when String, Symbol
14
- Net.const_get("HTTP#{options[:kind].to_s.camelize}")
15
- else
16
- Net::HTTPSuccess
17
- end
18
- end
19
- @pattern = options[:pattern] || nil
32
+ super
33
+ @uri = URI.parse(options[:url]) #: URI::HTTP
34
+ @kind = case options[:kind]
35
+ when Integer
36
+ Net::HTTPResponse::CODE_TO_OBJ.fetch(options.fetch(:kind).to_s)
37
+ when String, Symbol
38
+ Net.const_get("HTTP#{options.fetch(:kind).to_s.camelize}")
39
+ else
40
+ Net::HTTPSuccess
41
+ end
42
+
43
+ @pattern = options[:pattern]
20
44
  @open_timeout = (options[:open_timeout] || options[:timeout] || 5).to_i
21
45
  @read_timeout = (options[:read_timeout] || options[:timeout] || 5).to_i
22
46
  end
23
47
 
48
+ # Get the URL
49
+ #
50
+ # HTTPS is checked without verifying the certificate.
51
+ #
52
+ # @param [Integer, nil] _pid the process's pid
53
+ # @param [Boolean] _include_children whether to include its children
54
+ #
55
+ # @return [Net::HTTPResponse, StandardError] the response, or the error
56
+ # that stopped the request
57
+ #
58
+ # @api private
24
59
  def run(_pid, _include_children)
25
- session = Net::HTTP.new(@uri.host, @uri.port)
26
- if @uri.scheme == 'https'
60
+ host = @uri.host #: String
61
+ session = Net::HTTP.new(host, @uri.port)
62
+ if @uri.scheme.eql?('https')
27
63
  require 'net/https'
28
64
  session.use_ssl = true
29
65
  session.verify_mode = OpenSSL::SSL::VERIFY_NONE
@@ -35,27 +71,49 @@ module Bluepill
35
71
  http.get(@uri.request_uri)
36
72
  end
37
73
  end
38
- rescue
39
- $ERROR_INFO
74
+ rescue StandardError => e
75
+ e
40
76
  end
41
77
 
78
+ # Return whether the response is the expected kind, and matches
79
+ #
80
+ # @param [Net::HTTPResponse, StandardError] value what run read
81
+ #
82
+ # @return [Boolean] true when the response is the kind the check
83
+ # expects, and its body matches the pattern if the check has one
84
+ #
85
+ # @api private
42
86
  def check(value)
43
87
  return false unless value.is_a?(@kind)
44
88
  return true unless @pattern
45
- return false unless value.class.body_permitted?
46
- @pattern =~ value.body
89
+
90
+ response = value #: Net::HTTPResponse
91
+ !(@pattern =~ response.body).nil?
47
92
  end
48
93
 
49
94
  private
50
95
 
96
+ # Run the block, raising a refused connection as one
97
+ #
98
+ # Net::HTTP raises NoMethodError (closed? for nil) when it can't
99
+ # connect, so this raises Errno::ECONNREFUSED instead.
100
+ #
101
+ # @yield the request
102
+ # @yieldreturn [Object] the response
103
+ #
104
+ # @return [Object] what the block returns
105
+ #
106
+ # @raise [Errno::ECONNREFUSED] when the connection was refused
107
+ #
108
+ # @api private
51
109
  def hide_net_http_bug
52
110
  yield
53
111
  rescue NoMethodError => e
54
- if e.to_s =~ /#{Regexp.escape("undefined method `closed?' for nil:NilClass")}/
55
- raise(Errno::ECONNREFUSED, "Connection refused attempting to contact #{@uri.scheme}://#{@uri.host}:#{@uri.port}")
56
- else
57
- raise
58
- end
112
+ # Ruby 3.4 changed the wording of this error
113
+ message = e.to_s
114
+ raise(Errno::ECONNREFUSED, "Connection refused attempting to contact #{@uri.scheme}://#{@uri.host}:#{@uri.port}") if message.include?("undefined method `closed?' for nil:NilClass") || message.include?("undefined method 'closed?' for nil")
115
+
116
+ raise
59
117
  end
60
118
  end
61
119
  end
@@ -1,26 +1,85 @@
1
1
  module Bluepill
2
2
  module ProcessConditions
3
+ # Checks a process's memory usage (its resident set size)
4
+ #
5
+ # A config names it `checks :mem_usage`, with below: the most memory
6
+ # that's fine, in bytes (100.megabytes, say), and include_children: true
7
+ # to add the process's children to it.
8
+ #
9
+ # @example
10
+ # process.checks :mem_usage, every: 10.seconds, below: 100.megabytes, times: [3, 5]
11
+ #
12
+ # @api private
3
13
  class MemUsage < ProcessCondition
4
- MB = (1024**2).freeze
14
+ # The bytes in a megabyte, from which a value shows in megabytes
15
+ #
16
+ # @api private
17
+ MB = (1024**2) #: Integer
18
+
19
+ # How a value shows in the history: a number and its unit
20
+ #
21
+ # @api private
5
22
  FORMAT_STR = '%d%s'.freeze
23
+
24
+ # The unit of a value that shows in megabytes
25
+ #
26
+ # @api private
6
27
  MB_LABEL = 'MB'.freeze
28
+
29
+ # The unit of a value that shows in kilobytes
30
+ #
31
+ # @api private
7
32
  KB_LABEL = 'KB'.freeze
8
33
 
34
+ # Create the check
35
+ #
36
+ # @param [Hash{Symbol => Object}] options the check's options, with
37
+ # :below, the most memory that's fine, in bytes
38
+ #
39
+ # @return [void]
40
+ #
41
+ # @api private
9
42
  def initialize(options = {})
43
+ super
10
44
  @below = options[:below]
11
45
  end
12
46
 
47
+ # Read the process's memory usage from the tick's ps table
48
+ #
49
+ # @param [Integer, nil] pid the process's pid
50
+ # @param [Boolean] include_children whether to add its children's usage
51
+ #
52
+ # @return [Float] the usage in kilobytes, or 0.0 for a process ps
53
+ # doesn't list
54
+ #
55
+ # @api private
13
56
  def run(pid, include_children)
14
57
  # rss is on the 5th col
15
58
  System.memory_usage(pid, include_children).to_f
16
59
  end
17
60
 
61
+ # Return whether the usage is below the limit
62
+ #
63
+ # A limit that can't be compared counts as fine.
64
+ #
65
+ # @param [Float] value the kilobytes run read
66
+ #
67
+ # @return [Boolean] true when it's below the limit
68
+ #
69
+ # @api private
18
70
  def check(value)
19
71
  value.kilobytes < @below
20
- rescue
72
+ rescue StandardError
21
73
  true
22
74
  end
23
75
 
76
+ # Format the usage in kilobytes, or in megabytes from a megabyte up
77
+ #
78
+ # @param [Float] value the kilobytes run read
79
+ #
80
+ # @return [String] the usage and its unit, such as "312KB" or "250MB"
81
+ #
82
+ # @api private
24
83
  def format_value(value)
25
84
  if value.kilobytes >= MB
26
85
  format(FORMAT_STR, (value / 1024).round, MB_LABEL)
@@ -1,18 +1,61 @@
1
1
  module Bluepill
2
2
  module ProcessConditions
3
+ # Checks something about a process, as one of a process's checks
4
+ #
5
+ # A ConditionWatch runs the condition every so often. The condition reads
6
+ # a value (run), says whether the value is fine (check), and formats it
7
+ # for the watch's history (format_value). Subclasses implement run and
8
+ # check.
9
+ #
10
+ # @api private
3
11
  class ProcessCondition
12
+ # Create a condition with the options a config gave its check
13
+ #
14
+ # @param [Hash{Symbol => Object}] options the check's options, such as
15
+ # below: 100.megabytes
16
+ #
17
+ # @return [void]
18
+ #
19
+ # @api private
4
20
  def initialize(options = {})
5
21
  @options = options
6
22
  end
7
23
 
24
+ # Read the value to check from the process
25
+ #
26
+ # @param [Integer, nil] _pid the process's pid
27
+ # @param [Boolean] _include_children whether to include its children
28
+ #
29
+ # @return [Object] the value to check
30
+ #
31
+ # @raise [RuntimeError] unless a subclass implements it
32
+ #
33
+ # @api private
8
34
  def run(_pid, _include_children)
9
35
  raise 'Implement in subclass!'
10
36
  end
11
37
 
38
+ # Return whether a value is fine
39
+ #
40
+ # @param [Object] _value what run read
41
+ #
42
+ # @return [Boolean] true when the value is fine, false when it's not
43
+ #
44
+ # @raise [RuntimeError] unless a subclass implements it
45
+ #
46
+ # @api private
12
47
  def check(_value)
13
48
  raise 'Implement in subclass!'
14
49
  end
15
50
 
51
+ # Format a value for the watch's history and log
52
+ #
53
+ # @param [Object] value what run read
54
+ #
55
+ # @return [Object] the value as the history shows it, by default the
56
+ # value itself
57
+ #
58
+ # @api private
16
59
  def format_value(value)
17
60
  value
18
61
  end