navy 0.0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/bin/navy +56 -0
- data/lib/navy.rb +27 -0
- data/lib/navy/admiral.rb +284 -0
- data/lib/navy/captain.rb +191 -0
- data/lib/navy/officer.rb +19 -0
- data/lib/navy/rank.rb +15 -0
- data/lib/navy/ship.rb +51 -0
- data/lib/navy/version.rb +3 -0
- data/navy.gemspec +23 -0
- metadata +105 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Andrew Bennett
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Navy
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'navy'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install navy
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/navy
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib') unless $LOAD_PATH.include?(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
require 'navy/ship'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
ship_options = { options: {} }
|
9
|
+
options = ship_options[:options]
|
10
|
+
|
11
|
+
op = OptionParser.new("", 24, ' ') do |opts|
|
12
|
+
cmd = File.basename($0)
|
13
|
+
opts.banner = "Usage: #{cmd} [#{cmd} options]"
|
14
|
+
|
15
|
+
opts.separator "#{cmd} options:"
|
16
|
+
|
17
|
+
opts.on("-D", "--daemonize", "run daemonized in the background") do |d|
|
18
|
+
ship_options[:daemonize] = !!d
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.separator "Common options:"
|
22
|
+
|
23
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
24
|
+
puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '')
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on_tail("-v", "--version", "Show version") do
|
29
|
+
puts "#{cmd} v#{Navy::VERSION}"
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.parse! ARGV
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
# unless options[:config_file]
|
38
|
+
# $stderr.puts "ERROR: config file required", ''
|
39
|
+
# puts op.to_s.gsub(/^.*DEPRECATED.*$/s, '')
|
40
|
+
# exit 1
|
41
|
+
# end
|
42
|
+
|
43
|
+
# app = Mule.builder('config', op)
|
44
|
+
# op = nil
|
45
|
+
|
46
|
+
# if $DEBUG
|
47
|
+
# require 'pp'
|
48
|
+
# pp({
|
49
|
+
# :mule_options => options,
|
50
|
+
# # :app => app,
|
51
|
+
# :daemonize => server_options[:daemonize],
|
52
|
+
# })
|
53
|
+
# end
|
54
|
+
|
55
|
+
Navy::Ship.launch!(options) if ship_options[:daemonize]
|
56
|
+
Navy::Admiral.new(options).start.join
|
data/lib/navy.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'navy/version'
|
2
|
+
|
3
|
+
require 'fcntl'
|
4
|
+
require 'etc'
|
5
|
+
require 'stringio'
|
6
|
+
require 'kgio'
|
7
|
+
|
8
|
+
require 'forwardable'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
module Navy
|
12
|
+
extend self
|
13
|
+
def logger
|
14
|
+
@logger ||= Logger.new($stderr)
|
15
|
+
end
|
16
|
+
|
17
|
+
def log_error(logger, prefix, exc)
|
18
|
+
message = exc.message
|
19
|
+
message = message.dump if /[[:cntrl:]]/ =~ message
|
20
|
+
logger.error "#{prefix}: #{message} (#{exc.class})"
|
21
|
+
exc.backtrace.each { |line| logger.error(line) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
require 'navy/rank'
|
25
|
+
require 'navy/admiral'
|
26
|
+
require 'navy/captain'
|
27
|
+
require 'navy/officer'
|
data/lib/navy/admiral.rb
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
class Navy::Admiral < Navy::Rank
|
2
|
+
|
3
|
+
# This hash maps PIDs to Captains
|
4
|
+
CAPTAINS = {}
|
5
|
+
|
6
|
+
SELF_PIPE = []
|
7
|
+
|
8
|
+
# signal queue used for self-piping
|
9
|
+
SIG_QUEUE = []
|
10
|
+
|
11
|
+
# list of signals we care about and trap in admiral.
|
12
|
+
QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ]
|
13
|
+
|
14
|
+
START_CTX = {
|
15
|
+
:argv => ARGV.map { |arg| arg.dup },
|
16
|
+
0 => $0.dup,
|
17
|
+
}
|
18
|
+
# We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano
|
19
|
+
# and like systems
|
20
|
+
START_CTX[:cwd] = begin
|
21
|
+
a = File.stat(pwd = ENV['PWD'])
|
22
|
+
b = File.stat(Dir.pwd)
|
23
|
+
a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
|
24
|
+
rescue
|
25
|
+
Dir.pwd
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessor :admiral_pid, :reexec_pid
|
29
|
+
attr_reader :captains, :options, :timeout
|
30
|
+
|
31
|
+
def initialize(options = {})
|
32
|
+
@options = options.dup
|
33
|
+
@ready_pipe = @options.delete(:ready_pipe)
|
34
|
+
@timeout = 60
|
35
|
+
self.reexec_pid = 0
|
36
|
+
@captains = {
|
37
|
+
admin: {
|
38
|
+
job: ->(*args) {
|
39
|
+
trap(:QUIT) { exit }
|
40
|
+
trap(:TERM) { exit }
|
41
|
+
n = 0
|
42
|
+
loop do
|
43
|
+
Navy.logger.info "#{n} admin called #{args.inspect}"
|
44
|
+
Navy.logger.info "START_CTX: #{START_CTX.inspect}"
|
45
|
+
# Navy.logger.info "Navy::Admiral::CAPTAINS: #{Navy::Admiral::CAPTAINS.inspect}"
|
46
|
+
# Navy.logger.info "Navy::Admiral::OFFICERS: #{Navy::Captain::OFFICERS.inspect}"
|
47
|
+
sleep 10
|
48
|
+
n += 1
|
49
|
+
end
|
50
|
+
}
|
51
|
+
},
|
52
|
+
user: {
|
53
|
+
job: ->(*args) {
|
54
|
+
trap(:QUIT) { exit }
|
55
|
+
trap(:TERM) { exit }
|
56
|
+
n = 0
|
57
|
+
loop do
|
58
|
+
Navy.logger.info "#{n} user called #{args.inspect}"
|
59
|
+
# Navy.logger.info "Navy::Admiral::CAPTAINS: #{Navy::Admiral::CAPTAINS.inspect}"
|
60
|
+
# Navy.logger.info "Navy::Admiral::OFFICERS: #{Navy::Captain::OFFICERS.inspect}"
|
61
|
+
sleep 10
|
62
|
+
n += 1
|
63
|
+
end
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def start
|
70
|
+
init_self_pipe!
|
71
|
+
QUEUE_SIGS.each do |sig|
|
72
|
+
trap(sig) do
|
73
|
+
logger.warn "admiral received #{sig}"
|
74
|
+
SIG_QUEUE << sig
|
75
|
+
awaken_admiral
|
76
|
+
end
|
77
|
+
end
|
78
|
+
trap(:CHLD) { awaken_admiral }
|
79
|
+
|
80
|
+
logger.info "admiral starting"
|
81
|
+
|
82
|
+
self.admiral_pid = $$
|
83
|
+
spawn_missing_captains
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def join
|
88
|
+
respawn = true
|
89
|
+
last_check = Time.now
|
90
|
+
|
91
|
+
proc_name 'admiral'
|
92
|
+
logger.info "admiral process ready"
|
93
|
+
if @ready_pipe
|
94
|
+
@ready_pipe.syswrite($$.to_s)
|
95
|
+
@ready_pipe = @ready_pipe.close rescue nil
|
96
|
+
end
|
97
|
+
begin
|
98
|
+
reap_all_captains
|
99
|
+
case SIG_QUEUE.shift
|
100
|
+
when nil
|
101
|
+
# avoid murdering workers after our master process (or the
|
102
|
+
# machine) comes out of suspend/hibernation
|
103
|
+
if (last_check + @timeout) >= (last_check = Time.now)
|
104
|
+
# sleep_time = murder_lazy_workers
|
105
|
+
logger.debug("would normally murder lazy captains")
|
106
|
+
else
|
107
|
+
sleep_time = @timeout/2.0 + 1
|
108
|
+
logger.debug("waiting #{sleep_time}s after suspend/hibernation")
|
109
|
+
end
|
110
|
+
maintain_captain_count if respawn
|
111
|
+
admiral_sleep(sleep_time)
|
112
|
+
when :QUIT # graceful shutdown
|
113
|
+
break
|
114
|
+
when :TERM, :INT # immediate shutdown
|
115
|
+
stop(false)
|
116
|
+
break
|
117
|
+
when :USR1 # rotate logs
|
118
|
+
logger.info "admiral reopening logs..."
|
119
|
+
# Unicorn::Util.reopen_logs
|
120
|
+
logger.info "admiral done reopening logs"
|
121
|
+
kill_each_captain(:USR1)
|
122
|
+
when :USR2 # exec binary, stay alive in case something went wrong
|
123
|
+
reexec
|
124
|
+
when :WINCH
|
125
|
+
# if Unicorn::Configurator::RACKUP[:daemonized]
|
126
|
+
# respawn = false
|
127
|
+
# logger.info "gracefully stopping all workers"
|
128
|
+
# kill_each_worker(:QUIT)
|
129
|
+
# self.worker_processes = 0
|
130
|
+
# else
|
131
|
+
logger.info "SIGWINCH ignored because we're not daemonized"
|
132
|
+
# end
|
133
|
+
when :TTIN
|
134
|
+
respawn = true
|
135
|
+
kill_each_captain(:TTIN)
|
136
|
+
when :TTOU
|
137
|
+
kill_each_captain(:TTOU)
|
138
|
+
when :HUP
|
139
|
+
respawn = true
|
140
|
+
# if config.config_file
|
141
|
+
# load_config!
|
142
|
+
# else # exec binary and exit if there's no config file
|
143
|
+
logger.info "config_file not present, reexecuting binary"
|
144
|
+
reexec
|
145
|
+
# end
|
146
|
+
end
|
147
|
+
rescue => e
|
148
|
+
Navy.log_error(logger, "admiral loop error", e)
|
149
|
+
end while true
|
150
|
+
stop # gracefully shutdown all captains on our way out
|
151
|
+
logger.info "admiral complete"
|
152
|
+
end
|
153
|
+
|
154
|
+
# Terminates all captains, but does not exit admiral process
|
155
|
+
def stop(graceful = true)
|
156
|
+
limit = Time.now + timeout
|
157
|
+
until CAPTAINS.empty? || Time.now > limit
|
158
|
+
kill_each_captain(graceful ? :QUIT : :TERM)
|
159
|
+
sleep(0.1)
|
160
|
+
reap_all_captains
|
161
|
+
end
|
162
|
+
kill_each_captain(:KILL)
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
# wait for a signal hander to wake us up and then consume the pipe
|
168
|
+
def admiral_sleep(sec)
|
169
|
+
IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return
|
170
|
+
SELF_PIPE[0].kgio_tryread(11)
|
171
|
+
end
|
172
|
+
|
173
|
+
def awaken_admiral
|
174
|
+
SELF_PIPE[1].kgio_trywrite('.') # wakeup admiral process from select
|
175
|
+
end
|
176
|
+
|
177
|
+
# reaps all unreaped captains
|
178
|
+
def reap_all_captains
|
179
|
+
begin
|
180
|
+
cpid, status = Process.waitpid2(-1, Process::WNOHANG)
|
181
|
+
cpid or return
|
182
|
+
if reexec_pid == cpid
|
183
|
+
logger.error "reaped #{status.inspect} exec()-ed"
|
184
|
+
self.reexec_pid = 0
|
185
|
+
# self.pid = pid.chomp('.oldbin') if pid
|
186
|
+
proc_name "admiral"
|
187
|
+
else
|
188
|
+
captain = CAPTAINS.delete(cpid) rescue nil
|
189
|
+
m = "reaped #{status.inspect} captain=#{captain.label rescue 'unknown'}"
|
190
|
+
status.success? ? logger.info(m) : logger.error(m)
|
191
|
+
end
|
192
|
+
rescue Errno::ECHILD
|
193
|
+
break
|
194
|
+
end while true
|
195
|
+
end
|
196
|
+
|
197
|
+
# reexecutes the START_CTX with a new binary
|
198
|
+
def reexec
|
199
|
+
if reexec_pid > 0
|
200
|
+
begin
|
201
|
+
Process.kill(0, reexec_pid)
|
202
|
+
logger.error "reexec-ed child already running PID:#{reexec_pid}"
|
203
|
+
return
|
204
|
+
rescue Errno::ESRCH
|
205
|
+
self.reexec_pid = 0
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# if pid
|
210
|
+
# old_pid = "#{pid}.oldbin"
|
211
|
+
# begin
|
212
|
+
# self.pid = old_pid # clear the path for a new pid file
|
213
|
+
# rescue ArgumentError
|
214
|
+
# logger.error "old PID:#{valid_pid?(old_pid)} running with " \
|
215
|
+
# "existing pid=#{old_pid}, refusing rexec"
|
216
|
+
# return
|
217
|
+
# rescue => e
|
218
|
+
# logger.error "error writing pid=#{old_pid} #{e.class} #{e.message}"
|
219
|
+
# return
|
220
|
+
# end
|
221
|
+
# end
|
222
|
+
|
223
|
+
logger.info "reexec admiral"
|
224
|
+
|
225
|
+
self.reexec_pid = fork do
|
226
|
+
# ENV['NAVY_FD'] = listener_fds.keys.join(',')
|
227
|
+
Dir.chdir(START_CTX[:cwd])
|
228
|
+
cmd = [ START_CTX[0] ].concat(START_CTX[:argv])
|
229
|
+
|
230
|
+
# exec(command, hash) works in at least 1.9.1+, but will only be
|
231
|
+
# required in 1.9.4/2.0.0 at earliest.
|
232
|
+
logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
|
233
|
+
# before_exec.call(self)
|
234
|
+
exec(*cmd)
|
235
|
+
end
|
236
|
+
proc_name 'admiral (old)'
|
237
|
+
end
|
238
|
+
|
239
|
+
def spawn_missing_captains
|
240
|
+
captains.each do |label, config|
|
241
|
+
CAPTAINS.value?(label) and next
|
242
|
+
captain = Navy::Captain.new(self, label, config)
|
243
|
+
if pid = fork
|
244
|
+
CAPTAINS[pid] = captain
|
245
|
+
else
|
246
|
+
captain.start.join
|
247
|
+
exit
|
248
|
+
end
|
249
|
+
end
|
250
|
+
self
|
251
|
+
rescue => e
|
252
|
+
logger.error(e) rescue nil
|
253
|
+
exit!
|
254
|
+
end
|
255
|
+
|
256
|
+
def maintain_captain_count
|
257
|
+
(off = CAPTAINS.size - captains.size) == 0 and return
|
258
|
+
off < 0 and return spawn_missing_captains
|
259
|
+
CAPTAINS.dup.each_pair { |cpid,c|
|
260
|
+
captains.key?(c.label) or kill_captain(:QUIT, cpid) rescue nil
|
261
|
+
}
|
262
|
+
end
|
263
|
+
|
264
|
+
# delivers a signal to a worker and fails gracefully if the worker
|
265
|
+
# is no longer running.
|
266
|
+
def kill_captain(signal, cpid)
|
267
|
+
logger.warn "admiral sending #{signal} to #{cpid}"
|
268
|
+
Process.kill(signal, cpid)
|
269
|
+
rescue Errno::ESRCH
|
270
|
+
captain = CAPTAINS.delete(cpid) rescue nil
|
271
|
+
end
|
272
|
+
|
273
|
+
# delivers a signal to each worker
|
274
|
+
def kill_each_captain(signal)
|
275
|
+
CAPTAINS.keys.each { |wpid| kill_captain(signal, wpid) }
|
276
|
+
end
|
277
|
+
|
278
|
+
def init_self_pipe!
|
279
|
+
SELF_PIPE.each { |io| io.close rescue nil }
|
280
|
+
SELF_PIPE.replace(Kgio::Pipe.new)
|
281
|
+
SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
data/lib/navy/captain.rb
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
class Navy::Captain < Navy::Rank
|
2
|
+
|
3
|
+
# This hash maps PIDs to Officers
|
4
|
+
OFFICERS = {}
|
5
|
+
|
6
|
+
SELF_PIPE = []
|
7
|
+
|
8
|
+
# signal queue used for self-piping
|
9
|
+
SIG_QUEUE = []
|
10
|
+
|
11
|
+
# list of signals we care about and trap in admiral.
|
12
|
+
QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ]
|
13
|
+
|
14
|
+
attr_accessor :label, :captain_pid, :timeout, :reexec_pid, :number
|
15
|
+
attr_reader :admiral, :options
|
16
|
+
|
17
|
+
def initialize(admiral, label, options = {})
|
18
|
+
@admiral, @options = admiral, options.dup
|
19
|
+
@label = label
|
20
|
+
@number = 1
|
21
|
+
@timeout = 15
|
22
|
+
end
|
23
|
+
|
24
|
+
def ==(other_label)
|
25
|
+
@label == other_label
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
init_self_pipe!
|
30
|
+
QUEUE_SIGS.each { |sig| trap(sig) { SIG_QUEUE << sig; awaken_captain } }
|
31
|
+
trap(:CHLD) { awaken_captain }
|
32
|
+
|
33
|
+
logger.info "captain[#{label}] starting"
|
34
|
+
|
35
|
+
self.captain_pid = $$
|
36
|
+
spawn_missing_officers
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def join
|
41
|
+
respawn = true
|
42
|
+
last_check = Time.now
|
43
|
+
|
44
|
+
proc_name "captain[#{label}]"
|
45
|
+
logger.info "captain[#{label}] process ready"
|
46
|
+
|
47
|
+
begin
|
48
|
+
reap_all_officers
|
49
|
+
case SIG_QUEUE.shift
|
50
|
+
when nil
|
51
|
+
# avoid murdering workers after our master process (or the
|
52
|
+
# machine) comes out of suspend/hibernation
|
53
|
+
if (last_check + @timeout) >= (last_check = Time.now)
|
54
|
+
# sleep_time = murder_lazy_workers
|
55
|
+
logger.debug("would normally murder lazy officers")
|
56
|
+
else
|
57
|
+
sleep_time = @timeout/2.0 + 1
|
58
|
+
logger.debug("waiting #{sleep_time}s after suspend/hibernation")
|
59
|
+
end
|
60
|
+
maintain_officer_count if respawn
|
61
|
+
captain_sleep(sleep_time)
|
62
|
+
when :QUIT # graceful shutdown
|
63
|
+
break
|
64
|
+
when :TERM, :INT # immediate shutdown
|
65
|
+
stop(false)
|
66
|
+
break
|
67
|
+
when :USR1 # rotate logs
|
68
|
+
logger.info "captain[#{label}] reopening logs..."
|
69
|
+
# Unicorn::Util.reopen_logs
|
70
|
+
logger.info "captaion[#{label}] done reopening logs"
|
71
|
+
kill_each_officer(:USR1)
|
72
|
+
when :WINCH
|
73
|
+
# if Unicorn::Configurator::RACKUP[:daemonized]
|
74
|
+
# respawn = false
|
75
|
+
# logger.info "gracefully stopping all workers"
|
76
|
+
# kill_each_worker(:QUIT)
|
77
|
+
# self.worker_processes = 0
|
78
|
+
# else
|
79
|
+
logger.info "SIGWINCH ignored because we're not daemonized"
|
80
|
+
# end
|
81
|
+
when :TTIN
|
82
|
+
respawn = true
|
83
|
+
self.number += 1
|
84
|
+
when :TTOU
|
85
|
+
self.number -= 1 if self.number > 0
|
86
|
+
when :HUP
|
87
|
+
respawn = true
|
88
|
+
# if config.config_file
|
89
|
+
# load_config!
|
90
|
+
# else # exec binary and exit if there's no config file
|
91
|
+
logger.info "config_file not present, reexecuting binary"
|
92
|
+
reexec
|
93
|
+
# end
|
94
|
+
end
|
95
|
+
rescue => e
|
96
|
+
Navy.log_error(logger, "captain[#{label}] loop error", e)
|
97
|
+
end while true
|
98
|
+
stop # gracefully shutdown all captains on our way out
|
99
|
+
logger.info "captain[#{label}] complete"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Terminates all captains, but does not exit admiral process
|
103
|
+
def stop(graceful = true)
|
104
|
+
limit = Time.now + timeout
|
105
|
+
until OFFICERS.empty? || Time.now > limit
|
106
|
+
kill_each_officer(graceful ? :QUIT : :TERM)
|
107
|
+
sleep(0.1)
|
108
|
+
reap_all_officers
|
109
|
+
end
|
110
|
+
kill_each_officer(:KILL)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# wait for a signal hander to wake us up and then consume the pipe
|
116
|
+
def captain_sleep(sec)
|
117
|
+
IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return
|
118
|
+
SELF_PIPE[0].kgio_tryread(11)
|
119
|
+
end
|
120
|
+
|
121
|
+
def awaken_captain
|
122
|
+
SELF_PIPE[1].kgio_trywrite('.') # wakeup captain process from select
|
123
|
+
end
|
124
|
+
|
125
|
+
# reaps all unreaped officers
|
126
|
+
def reap_all_officers
|
127
|
+
begin
|
128
|
+
opid, status = Process.waitpid2(-1, Process::WNOHANG)
|
129
|
+
opid or return
|
130
|
+
if reexec_pid == opid
|
131
|
+
logger.error "reaped #{status.inspect} exec()-ed"
|
132
|
+
self.reexec_pid = 0
|
133
|
+
# self.pid = pid.chomp('.oldbin') if pid
|
134
|
+
proc_name "captain[#{label}]"
|
135
|
+
else
|
136
|
+
officer = OFFICERS.delete(opid) rescue nil
|
137
|
+
m = "reaped #{status.inspect} officer=#{officer.number rescue 'unknown'}"
|
138
|
+
status.success? ? logger.info(m) : logger.error(m)
|
139
|
+
end
|
140
|
+
rescue Errno::ECHILD
|
141
|
+
break
|
142
|
+
end while true
|
143
|
+
end
|
144
|
+
|
145
|
+
def spawn_missing_officers
|
146
|
+
n = -1
|
147
|
+
until (n += 1) == @number
|
148
|
+
OFFICERS.value?(n) and next
|
149
|
+
officer = Navy::Officer.new(self, n, options[:job])
|
150
|
+
if pid = fork
|
151
|
+
OFFICERS[pid] = officer
|
152
|
+
else
|
153
|
+
officer.start
|
154
|
+
exit
|
155
|
+
end
|
156
|
+
end
|
157
|
+
self
|
158
|
+
rescue => e
|
159
|
+
logger.error(e) rescue nil
|
160
|
+
exit!
|
161
|
+
end
|
162
|
+
|
163
|
+
def maintain_officer_count
|
164
|
+
(off = OFFICERS.size - @number) == 0 and return
|
165
|
+
off < 0 and return spawn_missing_officers
|
166
|
+
OFFICERS.dup.each_pair { |opid,o|
|
167
|
+
o.number >= @number and kill_officer(:QUIT, opid) rescue nil
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
# delivers a signal to a worker and fails gracefully if the worker
|
172
|
+
# is no longer running.
|
173
|
+
def kill_officer(signal, opid)
|
174
|
+
logger.warn "captain[#{label}] sending #{signal} to #{opid}"
|
175
|
+
Process.kill(signal, opid)
|
176
|
+
rescue Errno::ESRCH
|
177
|
+
officer = OFFICERS.delete(opid) rescue nil
|
178
|
+
end
|
179
|
+
|
180
|
+
# delivers a signal to each worker
|
181
|
+
def kill_each_officer(signal)
|
182
|
+
OFFICERS.keys.each { |wpid| kill_officer(signal, wpid) }
|
183
|
+
end
|
184
|
+
|
185
|
+
def init_self_pipe!
|
186
|
+
SELF_PIPE.each { |io| io.close rescue nil }
|
187
|
+
SELF_PIPE.replace(Kgio::Pipe.new)
|
188
|
+
SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
data/lib/navy/officer.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class Navy::Officer < Navy::Rank
|
2
|
+
attr_accessor :number
|
3
|
+
attr_reader :captain, :job
|
4
|
+
def initialize(captain, number, job)
|
5
|
+
@captain, @number, @job = captain, number, job
|
6
|
+
end
|
7
|
+
|
8
|
+
def ==(other_number)
|
9
|
+
@number == other_number
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
proc_name "(#{captain.label}) officer[#{number}]"
|
14
|
+
job.call
|
15
|
+
rescue => e
|
16
|
+
logger.error(e) rescue nil
|
17
|
+
exit!
|
18
|
+
end
|
19
|
+
end
|
data/lib/navy/rank.rb
ADDED
data/lib/navy/ship.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
$stdout.sync = $stderr.sync = true
|
2
|
+
$stdin.binmode
|
3
|
+
$stdout.binmode
|
4
|
+
$stderr.binmode
|
5
|
+
|
6
|
+
require 'navy'
|
7
|
+
|
8
|
+
module Navy::Ship
|
9
|
+
|
10
|
+
extend self
|
11
|
+
|
12
|
+
def launch!(options)
|
13
|
+
# cfg = Mule::Configurator
|
14
|
+
$stdin.reopen("/dev/null")
|
15
|
+
|
16
|
+
# We only start a new process group if we're not being reexecuted
|
17
|
+
# and inheriting file descriptors from our parent
|
18
|
+
unless ENV['NAVY_FD']
|
19
|
+
# grandparent - reads pipe, exits when master is ready
|
20
|
+
# \_ parent - exits immediately ASAP
|
21
|
+
# \_ unicorn master - writes to pipe when ready
|
22
|
+
|
23
|
+
rd, wr = IO.pipe
|
24
|
+
grandparent = $$
|
25
|
+
if fork
|
26
|
+
wr.close # grandparent does not write
|
27
|
+
else
|
28
|
+
rd.close # unicorn master does not read
|
29
|
+
Process.setsid
|
30
|
+
exit if fork # parent dies now
|
31
|
+
end
|
32
|
+
|
33
|
+
if grandparent == $$
|
34
|
+
# this will block until HttpServer#join runs (or it dies)
|
35
|
+
admiral_pid = (rd.readpartial(16) rescue nil).to_i
|
36
|
+
unless admiral_pid > 1
|
37
|
+
warn "admiral failed to start, check stderr log for details"
|
38
|
+
exit!(1)
|
39
|
+
end
|
40
|
+
exit 0
|
41
|
+
else # unicorn master process
|
42
|
+
options[:ready_pipe] = wr
|
43
|
+
end
|
44
|
+
end
|
45
|
+
# $stderr/$stderr can/will be redirected separately in the Unicorn config
|
46
|
+
# cfg::DEFAULTS[:stderr_path] ||= "/dev/null"
|
47
|
+
# cfg::DEFAULTS[:stdout_path] ||= "/dev/null"
|
48
|
+
# cfg::SERVER[:daemonized] = true
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/lib/navy/version.rb
ADDED
data/navy.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/navy/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Andrew Bennett"]
|
6
|
+
gem.email = ["potatosaladx@gmail.com"]
|
7
|
+
gem.description = %q{Ruby daemon inspired by Unicorn.}
|
8
|
+
gem.summary = %q{Ruby daemon inspired by Unicorn.}
|
9
|
+
gem.homepage = "https://github.com/potatosalad/navy"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "navy"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Navy::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency 'pry'
|
19
|
+
gem.add_development_dependency 'rake'
|
20
|
+
gem.add_development_dependency 'rspec', '~> 2.8.0'
|
21
|
+
|
22
|
+
gem.add_dependency 'kgio', '~> 2.6'
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: navy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andrew Bennett
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: pry
|
16
|
+
requirement: &70147356922140 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70147356922140
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &70147356921620 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70147356921620
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &70147356920800 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.8.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70147356920800
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: kgio
|
49
|
+
requirement: &70147356920060 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.6'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70147356920060
|
58
|
+
description: Ruby daemon inspired by Unicorn.
|
59
|
+
email:
|
60
|
+
- potatosaladx@gmail.com
|
61
|
+
executables:
|
62
|
+
- navy
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- .gitignore
|
67
|
+
- Gemfile
|
68
|
+
- LICENSE
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- bin/navy
|
72
|
+
- lib/navy.rb
|
73
|
+
- lib/navy/admiral.rb
|
74
|
+
- lib/navy/captain.rb
|
75
|
+
- lib/navy/officer.rb
|
76
|
+
- lib/navy/rank.rb
|
77
|
+
- lib/navy/ship.rb
|
78
|
+
- lib/navy/version.rb
|
79
|
+
- navy.gemspec
|
80
|
+
homepage: https://github.com/potatosalad/navy
|
81
|
+
licenses: []
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
requirements: []
|
99
|
+
rubyforge_project:
|
100
|
+
rubygems_version: 1.8.17
|
101
|
+
signing_key:
|
102
|
+
specification_version: 3
|
103
|
+
summary: Ruby daemon inspired by Unicorn.
|
104
|
+
test_files: []
|
105
|
+
has_rdoc:
|