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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +820 -0
- data/CONTRIBUTING.md +31 -0
- data/LICENSE +1 -1
- data/README.md +69 -30
- data/bin/bluepill +2 -2
- data/bluepill.gemspec +17 -6
- data/lib/bluepill/application.rb +311 -43
- data/lib/bluepill/condition_watch.rb +105 -10
- data/lib/bluepill/controller.rb +125 -14
- data/lib/bluepill/dsl/app_proxy.rb +103 -4
- data/lib/bluepill/dsl/process_factory.rb +185 -62
- 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 +122 -7
- data/lib/bluepill/process.rb +582 -211
- 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 +64 -5
- data/lib/bluepill/process_conditions/process_condition.rb +45 -2
- 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 +193 -229
- data/lib/bluepill/trigger.rb +111 -15
- data/lib/bluepill/triggers/flapping.rb +86 -5
- data/lib/bluepill/util/rotational_array.rb +25 -2
- 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 -35
- data/bin/sample_forking_server +0 -66
- data/lib/bluepill/application/client.rb +0 -6
- data/lib/bluepill/application/server.rb +0 -21
data/lib/bluepill/application.rb
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
attr_accessor :groups, :work_queue
|
|
11
|
-
|
|
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
|
|
21
|
-
self.pid_file = File.join(base_dir, 'pids',
|
|
22
|
-
self.pids_dir = File.join(base_dir, 'pids',
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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 <<-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
265
|
+
group.public_send(method)
|
|
77
266
|
end.flatten
|
|
78
267
|
elsif groups.key?(group_name)
|
|
79
|
-
groups
|
|
80
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
client
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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.
|
|
346
|
+
groups.each_value(&:determine_initial_state)
|
|
136
347
|
|
|
137
348
|
write_pid_file
|
|
138
|
-
self.socket =
|
|
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.
|
|
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(
|
|
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
|
|
197
|
-
exit(4) unless e.
|
|
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
|
-
|
|
6
|
-
|
|
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) ||
|
|
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
|
-
|
|
76
|
+
clear_history!
|
|
19
77
|
|
|
20
78
|
@process_condition = ProcessConditions[@name].new(options)
|
|
21
79
|
end
|
|
22
80
|
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
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"
|