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
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
module Bluepill
|
|
2
2
|
module ProcessConditions
|
|
3
|
+
# Checks how long a process has been running, in seconds
|
|
4
|
+
#
|
|
5
|
+
# A config names it `checks :running_time`, with below: the longest the
|
|
6
|
+
# process may run (24.hours, say), to restart a process that has run for
|
|
7
|
+
# too long.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# process.checks :running_time, every: 10.minutes, below: 24.hours
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
3
13
|
class RunningTime < ProcessCondition
|
|
14
|
+
# Create the check
|
|
15
|
+
#
|
|
16
|
+
# @param [Hash{Symbol => Object}] options the check's options, with
|
|
17
|
+
# :below, the longest the process may run, in seconds
|
|
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 how long the process has run from the tick's ps table
|
|
28
|
+
#
|
|
29
|
+
# @param [Integer, nil] pid the process's pid
|
|
30
|
+
# @param [Boolean] _include_children whether to include its children
|
|
31
|
+
#
|
|
32
|
+
# @return [Integer] the seconds since it started
|
|
33
|
+
#
|
|
34
|
+
# @api private
|
|
8
35
|
def run(pid, _include_children)
|
|
9
|
-
|
|
36
|
+
# Like the other ps conditions, a process missing from ps reads as 0
|
|
37
|
+
System.running_time(pid).to_i
|
|
10
38
|
end
|
|
11
39
|
|
|
40
|
+
# Return whether the process has run for less than the limit
|
|
41
|
+
#
|
|
42
|
+
# @param [Integer] value the seconds run read
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean] true when it's below the limit
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
12
47
|
def check(value)
|
|
13
48
|
value < @below
|
|
14
49
|
end
|
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
module Bluepill
|
|
2
2
|
module ProcessConditions
|
|
3
|
-
#
|
|
3
|
+
# Checks whether a process is a zombie, which ps shows as <defunct>
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
5
|
+
# A config names it `checks :zombie_process`. The process must have
|
|
6
|
+
# cache_actual_pid set to false for it to work correctly.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# process.checks :zombie_process, every: 5.seconds
|
|
10
|
+
# process.cache_actual_pid = false
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
8
13
|
class ZombieProcess < ProcessCondition
|
|
14
|
+
# Read the process's command line from the tick's ps table
|
|
15
|
+
#
|
|
16
|
+
# @param [Integer, nil] pid the process's pid
|
|
17
|
+
# @param [Boolean] _include_children whether to include its children
|
|
18
|
+
#
|
|
19
|
+
# @return [String, nil] the command line, or nil for a process ps
|
|
20
|
+
# doesn't list
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
9
23
|
def run(pid, _include_children)
|
|
10
24
|
System.command(pid)
|
|
11
25
|
end
|
|
12
26
|
|
|
27
|
+
# Return whether the process isn't a zombie
|
|
28
|
+
#
|
|
29
|
+
# @param [String, nil] value the command line run read
|
|
30
|
+
#
|
|
31
|
+
# @return [Boolean] true unless the command line says <defunct>
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
13
34
|
def check(value)
|
|
14
|
-
(value =~
|
|
35
|
+
(value =~ /<defunct>/).nil?
|
|
15
36
|
end
|
|
16
37
|
end
|
|
17
38
|
end
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
module Bluepill
|
|
2
|
+
# The checks a process can run, named in a config by their underscored
|
|
3
|
+
# class name, such as `checks :mem_usage`
|
|
4
|
+
#
|
|
5
|
+
# @api private
|
|
2
6
|
module ProcessConditions
|
|
7
|
+
# Return the check class a config names
|
|
8
|
+
#
|
|
9
|
+
# @param [Symbol, String] name the check's underscored name, such as
|
|
10
|
+
# :mem_usage
|
|
11
|
+
#
|
|
12
|
+
# @return [Class] the ProcessCondition subclass, such as MemUsage
|
|
13
|
+
#
|
|
14
|
+
# @raise [NameError] when no check has that name
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
3
17
|
def self.[](name)
|
|
4
18
|
const_get(name.to_s.camelcase)
|
|
5
19
|
end
|
|
@@ -7,6 +21,6 @@ module Bluepill
|
|
|
7
21
|
end
|
|
8
22
|
|
|
9
23
|
require 'bluepill/process_conditions/process_condition'
|
|
10
|
-
Dir["#{File.dirname(__FILE__)}/process_conditions/*.rb"].each do |pc|
|
|
24
|
+
Dir["#{File.dirname(__FILE__)}/process_conditions/*.rb"].sort.each do |pc|
|
|
11
25
|
require pc
|
|
12
26
|
end
|
|
@@ -1,172 +1,314 @@
|
|
|
1
1
|
require 'bluepill/system'
|
|
2
2
|
|
|
3
3
|
module Bluepill
|
|
4
|
+
# Remember the pids and process groups of the processes bluepill starts
|
|
5
|
+
#
|
|
6
|
+
# Each process has a pid journal and a process group journal, files in the
|
|
7
|
+
# journal directory with one id a line. When bluepill starts again, it
|
|
8
|
+
# kills whatever an earlier run left in them.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
4
11
|
module ProcessJournal
|
|
5
|
-
module_function
|
|
6
|
-
|
|
7
12
|
class << self
|
|
13
|
+
# Return the logger the journal writes to
|
|
14
|
+
#
|
|
15
|
+
# @return [Logger] the first logger that was set
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
8
18
|
attr_reader :logger
|
|
19
|
+
|
|
20
|
+
# Return the directory the journals are in
|
|
21
|
+
#
|
|
22
|
+
# @return [String] the journals directory under bluepill's base dir
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
9
25
|
attr_reader :journal_base_dir
|
|
10
26
|
|
|
27
|
+
# Set the logger the journal writes to, unless it already has one
|
|
28
|
+
#
|
|
29
|
+
# Only the first logger is kept.
|
|
30
|
+
#
|
|
31
|
+
# @param [Logger] new_logger the logger to use
|
|
32
|
+
#
|
|
33
|
+
# @return [Logger] the logger
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
11
36
|
def logger=(new_logger)
|
|
12
37
|
@logger ||= new_logger
|
|
13
38
|
end
|
|
14
39
|
|
|
40
|
+
# Set the journal directory from bluepill's base dir, and create it
|
|
41
|
+
#
|
|
42
|
+
# Only the first base dir is kept. The directory is made world-writable
|
|
43
|
+
# so processes that run as other users can lock their journals.
|
|
44
|
+
#
|
|
45
|
+
# @param [String] base_dir bluepill's base directory
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
#
|
|
49
|
+
# @api private
|
|
15
50
|
def base_dir=(base_dir)
|
|
16
51
|
@journal_base_dir ||= File.join(base_dir, 'journals')
|
|
17
52
|
FileUtils.mkdir_p(@journal_base_dir) unless File.exist?(@journal_base_dir)
|
|
18
|
-
FileUtils.chmod(
|
|
53
|
+
FileUtils.chmod(0o777, @journal_base_dir)
|
|
19
54
|
end
|
|
20
55
|
end
|
|
21
56
|
|
|
22
|
-
|
|
57
|
+
# Return whether a pid can't be journaled or signaled
|
|
58
|
+
#
|
|
59
|
+
# A pid of 1 or less would reach init, or a whole process group.
|
|
60
|
+
#
|
|
61
|
+
# @param [Integer, nil] pid the pid to check
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean] true unless pid is an Integer above 1
|
|
64
|
+
#
|
|
65
|
+
# @api private
|
|
66
|
+
def self.skip_pid?(pid)
|
|
23
67
|
!pid.is_a?(Integer) || pid <= 1
|
|
24
68
|
end
|
|
25
69
|
|
|
26
|
-
|
|
70
|
+
# Return whether a process group id can't be journaled or signaled
|
|
71
|
+
#
|
|
72
|
+
# @param [Integer, nil] pgid the process group id to check
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] true unless pgid is an Integer above 1
|
|
75
|
+
#
|
|
76
|
+
# @api private
|
|
77
|
+
def self.skip_pgid?(pgid)
|
|
27
78
|
!pgid.is_a?(Integer) || pgid <= 1
|
|
28
79
|
end
|
|
29
80
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
81
|
+
# Run a block while holding a lock on a journal
|
|
82
|
+
#
|
|
83
|
+
# The lock is a directory, since making one is atomic on POSIX
|
|
84
|
+
# filesystems and f.flock(File::LOCK_SH) is not available on all
|
|
85
|
+
# platforms. It waits up to 10 seconds for another process's lock.
|
|
86
|
+
#
|
|
87
|
+
# @param [String] name the journal's file name
|
|
88
|
+
#
|
|
89
|
+
# @yield while holding the lock
|
|
90
|
+
# @yieldreturn [Object] what the method returns
|
|
91
|
+
#
|
|
92
|
+
# @return [Object] what the block returns
|
|
93
|
+
#
|
|
94
|
+
# @raise [RuntimeError] if the lock is still held after 10 tries
|
|
95
|
+
#
|
|
96
|
+
# @api private
|
|
97
|
+
def self.acquire_atomic_fs_lock(name)
|
|
98
|
+
lock = "#{name}.lock"
|
|
33
99
|
times = 0
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
100
|
+
begin
|
|
101
|
+
Dir.mkdir lock, 0o700
|
|
102
|
+
rescue Errno::EEXIST
|
|
103
|
+
times += 1
|
|
104
|
+
if times < 10
|
|
105
|
+
logger.debug("Waiting for lock #{lock}")
|
|
106
|
+
sleep 1
|
|
107
|
+
retry
|
|
108
|
+
end
|
|
109
|
+
logger.info("Timeout waiting for lock #{lock}")
|
|
110
|
+
raise "Timeout waiting for lock #{lock}"
|
|
111
|
+
end
|
|
112
|
+
logger.debug("Acquired lock #{lock}")
|
|
113
|
+
begin
|
|
114
|
+
yield
|
|
115
|
+
ensure
|
|
116
|
+
clear_atomic_fs_lock(lock)
|
|
47
117
|
end
|
|
48
|
-
ensure
|
|
49
|
-
clear_atomic_fs_lock(name)
|
|
50
118
|
end
|
|
51
119
|
|
|
52
|
-
|
|
120
|
+
# Remove the journal locks in the current directory
|
|
121
|
+
#
|
|
122
|
+
# @return [void]
|
|
123
|
+
#
|
|
124
|
+
# @api private
|
|
125
|
+
def self.clear_all_atomic_fs_locks
|
|
53
126
|
Dir['.*.lock'].each do |f|
|
|
54
127
|
System.delete_if_exists(f) if File.directory?(f)
|
|
55
128
|
end
|
|
56
129
|
end
|
|
57
130
|
|
|
58
|
-
|
|
59
|
-
|
|
131
|
+
# Return the path of a process's pid journal
|
|
132
|
+
#
|
|
133
|
+
# @param [String] journal_name the process's name
|
|
134
|
+
#
|
|
135
|
+
# @return [String] the journal's path
|
|
136
|
+
#
|
|
137
|
+
# @api private
|
|
138
|
+
def self.pid_journal_filename(journal_name)
|
|
139
|
+
File.join(journal_base_dir, ".bluepill_pids_journal.#{journal_name}")
|
|
60
140
|
end
|
|
61
141
|
|
|
62
|
-
|
|
63
|
-
|
|
142
|
+
# Return the path of a process's process group journal
|
|
143
|
+
#
|
|
144
|
+
# @param [String] journal_name the process's name
|
|
145
|
+
#
|
|
146
|
+
# @return [String] the journal's path
|
|
147
|
+
#
|
|
148
|
+
# @api private
|
|
149
|
+
def self.pgid_journal_filename(journal_name)
|
|
150
|
+
File.join(journal_base_dir, ".bluepill_pgids_journal.#{journal_name}")
|
|
64
151
|
end
|
|
65
152
|
|
|
66
|
-
|
|
153
|
+
# Return the valid pids in a pid journal
|
|
154
|
+
#
|
|
155
|
+
# @param [String] filename the journal's path
|
|
156
|
+
#
|
|
157
|
+
# @return [Array<Integer>] the pids, or none if there's no journal
|
|
158
|
+
#
|
|
159
|
+
# @api private
|
|
160
|
+
def self.pid_journal(filename)
|
|
67
161
|
logger.debug("pid journal file: #{filename}")
|
|
68
|
-
result = File.
|
|
162
|
+
result = File.readlines(filename).collect(&:to_i).reject { |pid| skip_pid?(pid) }
|
|
69
163
|
logger.debug("pid journal = #{result.join(' ')}")
|
|
70
164
|
result
|
|
71
165
|
rescue Errno::ENOENT
|
|
72
166
|
[]
|
|
73
167
|
end
|
|
74
168
|
|
|
75
|
-
|
|
169
|
+
# Return the valid process group ids in a process group journal
|
|
170
|
+
#
|
|
171
|
+
# @param [String] filename the journal's path
|
|
172
|
+
#
|
|
173
|
+
# @return [Array<Integer>] the process group ids, or none if there's no
|
|
174
|
+
# journal
|
|
175
|
+
#
|
|
176
|
+
# @api private
|
|
177
|
+
def self.pgid_journal(filename)
|
|
76
178
|
logger.debug("pgid journal file: #{filename}")
|
|
77
|
-
result = File.
|
|
179
|
+
result = File.readlines(filename).collect(&:to_i).reject { |pgid| skip_pgid?(pgid) }
|
|
78
180
|
logger.debug("pgid journal = #{result.join(' ')}")
|
|
79
181
|
result
|
|
80
182
|
rescue Errno::ENOENT
|
|
81
183
|
[]
|
|
82
184
|
end
|
|
83
185
|
|
|
84
|
-
|
|
186
|
+
# Release a journal lock, if it's there
|
|
187
|
+
#
|
|
188
|
+
# @param [String] name the lock's directory
|
|
189
|
+
#
|
|
190
|
+
# @return [void]
|
|
191
|
+
#
|
|
192
|
+
# @api private
|
|
193
|
+
def self.clear_atomic_fs_lock(name)
|
|
85
194
|
return unless File.directory?(name)
|
|
195
|
+
|
|
86
196
|
Dir.rmdir(name)
|
|
87
197
|
logger.debug("Cleared lock #{name}")
|
|
88
198
|
end
|
|
89
199
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
200
|
+
# Kill what every journal in the current directory lists
|
|
201
|
+
#
|
|
202
|
+
# @return [void]
|
|
203
|
+
#
|
|
204
|
+
# @api private
|
|
205
|
+
def self.kill_all_from_all_journals
|
|
206
|
+
pids = Dir['.bluepill_pids_journal.*'].collect { |p| p.sub(/\A\.bluepill_pids_journal\./, '') }
|
|
207
|
+
pids.reject! { |p| p.end_with?('.lock') }
|
|
93
208
|
pids.each do |journal_name|
|
|
94
209
|
kill_all_from_journal(journal_name)
|
|
95
210
|
end
|
|
96
211
|
end
|
|
97
212
|
|
|
98
|
-
|
|
213
|
+
# Kill the processes and process groups that a process's journals list
|
|
214
|
+
#
|
|
215
|
+
# @param [String] journal_name the process's name
|
|
216
|
+
#
|
|
217
|
+
# @return [void]
|
|
218
|
+
#
|
|
219
|
+
# @api private
|
|
220
|
+
def self.kill_all_from_journal(journal_name)
|
|
99
221
|
kill_all_pids_from_journal(journal_name)
|
|
100
222
|
kill_all_pgids_from_journal(journal_name)
|
|
101
223
|
end
|
|
102
224
|
|
|
103
|
-
|
|
225
|
+
# Kill the process groups that a process's journal lists
|
|
226
|
+
#
|
|
227
|
+
# @param [String] journal_name the process's name
|
|
228
|
+
#
|
|
229
|
+
# @return [void]
|
|
230
|
+
#
|
|
231
|
+
# @api private
|
|
232
|
+
def self.kill_all_pgids_from_journal(journal_name)
|
|
104
233
|
filename = pgid_journal_filename(journal_name)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
234
|
+
kill_all_from_journal_file(filename, pgid_journal(filename), 'process group', -1)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Kill the processes that a process's journal lists
|
|
238
|
+
#
|
|
239
|
+
# @param [String] journal_name the process's name
|
|
240
|
+
#
|
|
241
|
+
# @return [void]
|
|
242
|
+
#
|
|
243
|
+
# @api private
|
|
244
|
+
def self.kill_all_pids_from_journal(journal_name)
|
|
245
|
+
filename = pid_journal_filename(journal_name)
|
|
246
|
+
kill_all_from_journal_file(filename, pid_journal(filename), 'process', 1)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Kill the ids a journal lists, and then delete the journal
|
|
250
|
+
#
|
|
251
|
+
# Sends TERM, waits a second for any survivors, then sends KILL to those
|
|
252
|
+
# still alive. A negative sign targets process groups instead of pids.
|
|
253
|
+
#
|
|
254
|
+
# @param [String] filename the journal's path
|
|
255
|
+
# @param [Array<Integer>] ids the pids or process group ids in it
|
|
256
|
+
# @param [String] label what the ids are, for the log
|
|
257
|
+
# @param [Integer] sign 1 for pids, or -1 for process groups
|
|
258
|
+
#
|
|
259
|
+
# @return [void]
|
|
260
|
+
#
|
|
261
|
+
# @api private
|
|
262
|
+
def self.kill_all_from_journal_file(filename, ids, label, sign)
|
|
263
|
+
if ids.empty?
|
|
132
264
|
logger.debug('No previous process journal - Skipping cleanup')
|
|
265
|
+
return
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
acquire_atomic_fs_lock(filename) do
|
|
269
|
+
signal_journaled('TERM', ids, label, sign)
|
|
270
|
+
survivors = ids.select { |id| System.pid_alive?(sign * id) }
|
|
271
|
+
unless survivors.empty?
|
|
272
|
+
sleep(1)
|
|
273
|
+
survivors = survivors.select { |id| System.pid_alive?(sign * id) }
|
|
274
|
+
signal_journaled('KILL', survivors, label, sign)
|
|
275
|
+
end
|
|
276
|
+
System.delete_if_exists(filename) # reset journal
|
|
277
|
+
logger.debug('Journal cleanup completed')
|
|
133
278
|
end
|
|
134
279
|
end
|
|
135
280
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
::Process.kill('KILL', pid)
|
|
155
|
-
logger.info("Killed old process #{pid}")
|
|
156
|
-
rescue Errno::ESRCH
|
|
157
|
-
logger.debug("Unable to kill missing process #{pid}")
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
System.delete_if_exists(filename) # reset journal
|
|
162
|
-
logger.debug('Journal cleanup completed')
|
|
281
|
+
# Send a signal to each journaled id, and log what happened
|
|
282
|
+
#
|
|
283
|
+
# @param [String] signal TERM or KILL
|
|
284
|
+
# @param [Array<Integer>] ids the pids or process group ids
|
|
285
|
+
# @param [String] label what the ids are, for the log
|
|
286
|
+
# @param [Integer] sign 1 for pids, or -1 for process groups
|
|
287
|
+
#
|
|
288
|
+
# @return [void]
|
|
289
|
+
#
|
|
290
|
+
# @api private
|
|
291
|
+
def self.signal_journaled(signal, ids, label, sign)
|
|
292
|
+
past, verb = signal.eql?('TERM') ? %w[Termed term] : %w[Killed kill]
|
|
293
|
+
ids.each do |id|
|
|
294
|
+
begin
|
|
295
|
+
::Process.kill(signal, sign * id)
|
|
296
|
+
logger.info("#{past} old #{label} #{id}")
|
|
297
|
+
rescue Errno::ESRCH
|
|
298
|
+
logger.debug("Unable to #{verb} missing #{label} #{id}")
|
|
163
299
|
end
|
|
164
|
-
else
|
|
165
|
-
logger.debug('No previous process journal - Skipping cleanup')
|
|
166
300
|
end
|
|
167
301
|
end
|
|
168
302
|
|
|
169
|
-
|
|
303
|
+
# Add a process group id to a process's journal, unless it's there
|
|
304
|
+
#
|
|
305
|
+
# @param [String] journal_name the process's name
|
|
306
|
+
# @param [Integer, nil] pgid the process group id
|
|
307
|
+
#
|
|
308
|
+
# @return [void]
|
|
309
|
+
#
|
|
310
|
+
# @api private
|
|
311
|
+
def self.append_pgid_to_journal(journal_name, pgid)
|
|
170
312
|
if skip_pgid?(pgid)
|
|
171
313
|
logger.debug("Skipping invalid pgid #{pgid} (class #{pgid.class})")
|
|
172
314
|
return
|
|
@@ -178,22 +320,32 @@ module Bluepill
|
|
|
178
320
|
logger.debug("Skipping duplicate pgid #{pgid} already in journal #{journal_name}")
|
|
179
321
|
else
|
|
180
322
|
logger.debug("Saving pgid #{pgid} to process journal #{journal_name}")
|
|
181
|
-
File.open(filename, 'a+',
|
|
323
|
+
File.open(filename, 'a+', 0o600) { |f| f.puts(pgid) }
|
|
182
324
|
logger.info("Saved pgid #{pgid} to journal #{journal_name}")
|
|
183
|
-
logger.debug("Journal now = #{File.
|
|
325
|
+
logger.debug("Journal now = #{File.read(filename)}")
|
|
184
326
|
end
|
|
185
327
|
end
|
|
186
328
|
end
|
|
187
329
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
330
|
+
# Add a pid, and its process group id, to a process's journals
|
|
331
|
+
#
|
|
332
|
+
# @param [String] journal_name the process's name
|
|
333
|
+
# @param [Integer, nil] pid the pid
|
|
334
|
+
#
|
|
335
|
+
# @return [void]
|
|
336
|
+
#
|
|
337
|
+
# @api private
|
|
338
|
+
def self.append_pid_to_journal(journal_name, pid)
|
|
193
339
|
if skip_pid?(pid)
|
|
194
340
|
logger.debug("Skipping invalid pid #{pid} (class #{pid.class})")
|
|
195
341
|
return
|
|
196
342
|
end
|
|
343
|
+
# skip_pid? made sure it's an Integer
|
|
344
|
+
valid_pid = pid #: Integer
|
|
345
|
+
begin
|
|
346
|
+
append_pgid_to_journal(journal_name, ::Process.getpgid(valid_pid))
|
|
347
|
+
rescue Errno::ESRCH
|
|
348
|
+
end
|
|
197
349
|
|
|
198
350
|
filename = pid_journal_filename(journal_name)
|
|
199
351
|
acquire_atomic_fs_lock(filename) do
|
|
@@ -201,9 +353,9 @@ module Bluepill
|
|
|
201
353
|
logger.debug("Skipping duplicate pid #{pid} already in journal #{journal_name}")
|
|
202
354
|
else
|
|
203
355
|
logger.debug("Saving pid #{pid} to process journal #{journal_name}")
|
|
204
|
-
File.open(filename, 'a+',
|
|
356
|
+
File.open(filename, 'a+', 0o600) { |f| f.puts(pid) }
|
|
205
357
|
logger.info("Saved pid #{pid} to journal #{journal_name}")
|
|
206
|
-
logger.debug("Journal now = #{File.
|
|
358
|
+
logger.debug("Journal now = #{File.read(filename)}")
|
|
207
359
|
end
|
|
208
360
|
end
|
|
209
361
|
end
|
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
module Bluepill
|
|
2
|
+
# Keeps a process's last few events, which bluepill status shows
|
|
3
|
+
#
|
|
4
|
+
# @api private
|
|
2
5
|
class ProcessStatistics
|
|
3
|
-
|
|
6
|
+
# How status shows each event's time
|
|
7
|
+
STRFTIME = '%m/%d/%Y %H:%M:%S'.freeze
|
|
8
|
+
|
|
9
|
+
# How many events are kept
|
|
4
10
|
EVENTS_TO_PERSIST = 10
|
|
5
11
|
|
|
12
|
+
# Return the recorded events, newest last
|
|
13
|
+
#
|
|
14
|
+
# @return [Util::RotationalArray<Array(Symbol, String, nil, Time)>] each
|
|
15
|
+
# event, its reason and its time
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
6
18
|
attr_reader :events
|
|
7
19
|
|
|
8
|
-
#
|
|
20
|
+
# Create an empty event history
|
|
21
|
+
#
|
|
22
|
+
# @return [void]
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
9
25
|
def initialize
|
|
10
26
|
@events = Util::RotationalArray.new(EVENTS_TO_PERSIST)
|
|
11
27
|
end
|
|
12
28
|
|
|
29
|
+
# Add an event to the history, with the time it happened
|
|
30
|
+
#
|
|
31
|
+
# @param [Symbol, String] event the event, such as :restart
|
|
32
|
+
# @param [String, nil] reason why it happened
|
|
33
|
+
#
|
|
34
|
+
# @return [Util::RotationalArray<Array(Symbol, String, nil, Time)>] the
|
|
35
|
+
# history, newest last
|
|
36
|
+
#
|
|
37
|
+
# @api private
|
|
13
38
|
def record_event(event, reason)
|
|
14
39
|
events.push([event, reason, Time.now])
|
|
15
40
|
end
|
|
16
41
|
|
|
42
|
+
# Return the history as text, newest first, for bluepill status
|
|
43
|
+
#
|
|
44
|
+
# @return [String]
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
17
47
|
def to_s
|
|
18
|
-
str = events.reverse.collect do |
|
|
48
|
+
str = events.reverse.collect do |event, reason, time|
|
|
19
49
|
" #{event} at #{time.strftime(STRFTIME)} - #{reason || 'unspecified'}"
|
|
20
50
|
end.join("\n")
|
|
21
51
|
|