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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +820 -0
- data/CONTRIBUTING.md +31 -0
- data/LICENSE +1 -1
- data/README.md +64 -29
- data/bin/bluepill +1 -1
- data/bluepill.gemspec +17 -6
- data/lib/bluepill/application.rb +309 -41
- data/lib/bluepill/condition_watch.rb +103 -8
- data/lib/bluepill/controller.rb +124 -13
- data/lib/bluepill/dsl/app_proxy.rb +102 -3
- data/lib/bluepill/dsl/process_factory.rb +183 -60
- data/lib/bluepill/dsl/process_proxy.rb +132 -14
- data/lib/bluepill/dsl.rb +27 -5
- data/lib/bluepill/group.rb +142 -9
- data/lib/bluepill/logger.rb +120 -5
- data/lib/bluepill/process.rb +571 -200
- data/lib/bluepill/process_commands.rb +262 -0
- data/lib/bluepill/process_conditions/always_true.rb +18 -4
- data/lib/bluepill/process_conditions/cpu_usage.rb +34 -0
- data/lib/bluepill/process_conditions/file_time.rb +40 -4
- data/lib/bluepill/process_conditions/http.rb +81 -23
- data/lib/bluepill/process_conditions/mem_usage.rb +61 -2
- data/lib/bluepill/process_conditions/process_condition.rb +43 -0
- data/lib/bluepill/process_conditions/running_time.rb +36 -1
- data/lib/bluepill/process_conditions/zombie_process.rb +26 -5
- data/lib/bluepill/process_conditions.rb +15 -1
- data/lib/bluepill/process_journal.rb +256 -104
- data/lib/bluepill/process_statistics.rb +33 -3
- data/lib/bluepill/socket.rb +63 -16
- data/lib/bluepill/system/commands.rb +286 -0
- data/lib/bluepill/system.rb +192 -228
- data/lib/bluepill/trigger.rb +110 -14
- data/lib/bluepill/triggers/flapping.rb +84 -3
- data/lib/bluepill/util/rotational_array.rb +24 -1
- data/lib/bluepill/version.rb +71 -19
- data/lib/bluepill.rb +25 -2
- data/sig/bluepill.rbs +926 -0
- data/sig/manifest.yaml +7 -0
- metadata +16 -34
- data/bin/sample_forking_server +0 -66
- data/lib/bluepill/application/client.rb +0 -6
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
@
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@pattern = options[:pattern]
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|