yahns 0.0.0TP1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/COPYING +674 -0
- data/GIT-VERSION-GEN +41 -0
- data/GNUmakefile +90 -0
- data/README +127 -0
- data/Rakefile +60 -0
- data/bin/yahns +32 -0
- data/examples/README +3 -0
- data/examples/init.sh +76 -0
- data/examples/logger_mp_safe.rb +28 -0
- data/examples/logrotate.conf +32 -0
- data/examples/yahns_multi.conf.rb +89 -0
- data/examples/yahns_rack_basic.conf.rb +27 -0
- data/lib/yahns.rb +73 -0
- data/lib/yahns/acceptor.rb +28 -0
- data/lib/yahns/client_expire.rb +40 -0
- data/lib/yahns/client_expire_portable.rb +39 -0
- data/lib/yahns/config.rb +344 -0
- data/lib/yahns/daemon.rb +51 -0
- data/lib/yahns/fdmap.rb +90 -0
- data/lib/yahns/http_client.rb +198 -0
- data/lib/yahns/http_context.rb +65 -0
- data/lib/yahns/http_response.rb +184 -0
- data/lib/yahns/log.rb +73 -0
- data/lib/yahns/queue.rb +7 -0
- data/lib/yahns/queue_egg.rb +23 -0
- data/lib/yahns/queue_epoll.rb +57 -0
- data/lib/yahns/rack.rb +80 -0
- data/lib/yahns/server.rb +336 -0
- data/lib/yahns/server_mp.rb +181 -0
- data/lib/yahns/sigevent.rb +7 -0
- data/lib/yahns/sigevent_efd.rb +18 -0
- data/lib/yahns/sigevent_pipe.rb +29 -0
- data/lib/yahns/socket_helper.rb +117 -0
- data/lib/yahns/stream_file.rb +34 -0
- data/lib/yahns/stream_input.rb +150 -0
- data/lib/yahns/tee_input.rb +114 -0
- data/lib/yahns/tmpio.rb +27 -0
- data/lib/yahns/wbuf.rb +36 -0
- data/lib/yahns/wbuf_common.rb +32 -0
- data/lib/yahns/worker.rb +58 -0
- data/test/covshow.rb +29 -0
- data/test/helper.rb +115 -0
- data/test/server_helper.rb +65 -0
- data/test/test_bin.rb +97 -0
- data/test/test_client_expire.rb +132 -0
- data/test/test_config.rb +56 -0
- data/test/test_fdmap.rb +19 -0
- data/test/test_output_buffering.rb +291 -0
- data/test/test_queue.rb +59 -0
- data/test/test_rack.rb +28 -0
- data/test/test_serve_static.rb +42 -0
- data/test/test_server.rb +415 -0
- data/test/test_stream_file.rb +30 -0
- data/test/test_wbuf.rb +136 -0
- data/yahns.gemspec +19 -0
- metadata +165 -0
data/lib/yahns/log.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
3
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
|
+
|
5
|
+
# logging-related utility functions for all of yahns
|
6
|
+
module Yahns::Log # :nodoc:
|
7
|
+
def self.exception(logger, prefix, exc)
|
8
|
+
message = exc.message
|
9
|
+
message = message.dump if /[[:cntrl:]]/ =~ message # prevent code injection
|
10
|
+
logger.error "#{prefix}: #{message} (#{exc.class})"
|
11
|
+
exc.backtrace.each { |line| logger.error(line) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.is_log?(fp)
|
15
|
+
append_flags = IO::WRONLY | IO::APPEND
|
16
|
+
|
17
|
+
! fp.closed? &&
|
18
|
+
fp.stat.file? &&
|
19
|
+
fp.sync &&
|
20
|
+
(fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
|
21
|
+
rescue IOError, Errno::EBADF
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.chown_all(uid, gid)
|
26
|
+
ObjectSpace.each_object(File) do |fp|
|
27
|
+
fp.chown(uid, gid) if is_log?(fp)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# This reopens ALL logfiles in the process that have been rotated
|
32
|
+
# using logrotate(8) (without copytruncate) or similar tools.
|
33
|
+
# A +File+ object is considered for reopening if it is:
|
34
|
+
# 1) opened with the O_APPEND and O_WRONLY flags
|
35
|
+
# 2) the current open file handle does not match its original open path
|
36
|
+
# 3) unbuffered (as far as userspace buffering goes, not O_SYNC)
|
37
|
+
# Returns the number of files reopened
|
38
|
+
def self.reopen_all
|
39
|
+
to_reopen = []
|
40
|
+
nr = 0
|
41
|
+
ObjectSpace.each_object(File) { |fp| is_log?(fp) and to_reopen << fp }
|
42
|
+
|
43
|
+
to_reopen.each do |fp|
|
44
|
+
orig_st = begin
|
45
|
+
fp.stat
|
46
|
+
rescue IOError, Errno::EBADF
|
47
|
+
next
|
48
|
+
end
|
49
|
+
|
50
|
+
begin
|
51
|
+
b = File.stat(fp.path)
|
52
|
+
next if orig_st.ino == b.ino && orig_st.dev == b.dev
|
53
|
+
rescue Errno::ENOENT
|
54
|
+
end
|
55
|
+
|
56
|
+
begin
|
57
|
+
File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
|
58
|
+
fp.sync = true
|
59
|
+
new_st = fp.stat
|
60
|
+
|
61
|
+
# this should only happen in the master:
|
62
|
+
if orig_st.uid != new_st.uid || orig_st.gid != new_st.gid
|
63
|
+
fp.chown(orig_st.uid, orig_st.gid)
|
64
|
+
end
|
65
|
+
|
66
|
+
nr += 1
|
67
|
+
rescue IOError, Errno::EBADF
|
68
|
+
# not much we can do...
|
69
|
+
end
|
70
|
+
end
|
71
|
+
nr
|
72
|
+
end
|
73
|
+
end
|
data/lib/yahns/queue.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
2
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
|
+
|
4
|
+
# this represents a Yahns::Queue before its vivified. This only
|
5
|
+
# lives in the parent process and should be clobbered after qc_vivify
|
6
|
+
class Yahns::QueueEgg # :nodoc:
|
7
|
+
attr_writer :max_events, :worker_threads
|
8
|
+
attr_accessor :logger
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@max_events = 1 # 1 is good if worker_threads > 1
|
12
|
+
@worker_threads = 7 # any default is wrong for most apps...
|
13
|
+
@logger = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# only call after forking
|
17
|
+
def qc_vivify(fdmap)
|
18
|
+
queue = Yahns::Queue.new
|
19
|
+
queue.fdmap = fdmap
|
20
|
+
queue.spawn_worker_threads(@logger, @worker_threads, @max_events)
|
21
|
+
queue
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
3
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
|
+
class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
|
5
|
+
include SleepyPenguin
|
6
|
+
attr_accessor :fdmap # Yahns::Fdmap
|
7
|
+
|
8
|
+
# public
|
9
|
+
QEV_RD = Epoll::IN | Epoll::ONESHOT
|
10
|
+
QEV_WR = Epoll::OUT | Epoll::ONESHOT
|
11
|
+
QEV_RDWR = QEV_RD | QEV_WR
|
12
|
+
|
13
|
+
def self.new
|
14
|
+
super(SleepyPenguin::Epoll::CLOEXEC)
|
15
|
+
end
|
16
|
+
|
17
|
+
# for HTTP and HTTPS servers, we rely on the io writing to us, first
|
18
|
+
# flags: QEV_RD/QEV_WR (usually QEV_RD)
|
19
|
+
def queue_add(io, flags)
|
20
|
+
@fdmap.add(io)
|
21
|
+
epoll_ctl(Epoll::CTL_ADD, io, flags)
|
22
|
+
end
|
23
|
+
|
24
|
+
# returns an array of infinitely running threads
|
25
|
+
def spawn_worker_threads(logger, worker_threads, max_events)
|
26
|
+
worker_threads.times do
|
27
|
+
Thread.new do
|
28
|
+
Thread.current[:yahns_rbuf] = ""
|
29
|
+
begin
|
30
|
+
epoll_wait(max_events) do |_, io| # don't care for flags for now
|
31
|
+
case rv = io.yahns_step
|
32
|
+
when :wait_readable
|
33
|
+
epoll_ctl(Epoll::CTL_MOD, io, QEV_RD)
|
34
|
+
when :wait_writable
|
35
|
+
epoll_ctl(Epoll::CTL_MOD, io, QEV_WR)
|
36
|
+
when :wait_readwrite
|
37
|
+
epoll_ctl(Epoll::CTL_MOD, io, QEV_RDWR)
|
38
|
+
when :delete # only used by rack.hijack
|
39
|
+
epoll_ctl(Epoll::CTL_DEL, io, 0)
|
40
|
+
@fdmap.delete(io)
|
41
|
+
when nil
|
42
|
+
# this is be the ONLY place where we call IO#close on
|
43
|
+
# things inside the queue
|
44
|
+
io.close
|
45
|
+
@fdmap.decr
|
46
|
+
else
|
47
|
+
raise "BUG: #{io.inspect}#yahns_step returned: #{rv.inspect}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
rescue => e
|
51
|
+
break if (IOError === e || Errno::EBADF === e) && closed?
|
52
|
+
Yahns::Log.exception(logger, 'queue loop', e)
|
53
|
+
end while true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/yahns/rack.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
3
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
|
+
require 'rack'
|
5
|
+
class Yahns::Rack # :nodoc:
|
6
|
+
attr_reader :preload
|
7
|
+
|
8
|
+
# enforce a single instance for the identical config.ru
|
9
|
+
def self.instance_key(*args)
|
10
|
+
ru = args[0]
|
11
|
+
|
12
|
+
# it's safe to expand_path now since we enforce working_directory in the
|
13
|
+
# top-level config is called before any apps are created
|
14
|
+
# ru may also be a Rack::Builder object or any already-built Rack app
|
15
|
+
ru.respond_to?(:call) ? ru.object_id : File.expand_path(ru)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(ru, opts = {})
|
19
|
+
# always called after config file parsing, may be called after forking
|
20
|
+
@app = lambda do
|
21
|
+
ENV["RACK_ENV"] ||= "none"
|
22
|
+
if ru.respond_to?(:call)
|
23
|
+
inner_app = ru.respond_to?(:to_app) ? ru.to_app : ru
|
24
|
+
else
|
25
|
+
inner_app = case ru
|
26
|
+
when /\.ru$/
|
27
|
+
raw = File.read(ru)
|
28
|
+
raw.sub!(/^__END__\n.*/, '')
|
29
|
+
eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
|
30
|
+
else
|
31
|
+
require ru
|
32
|
+
Object.const_get(File.basename(ru, '.rb').capitalize)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
inner_app
|
36
|
+
end
|
37
|
+
@ru = ru
|
38
|
+
@preload = opts[:preload]
|
39
|
+
build_app! if @preload
|
40
|
+
end
|
41
|
+
|
42
|
+
def config_context
|
43
|
+
ctx_class = Class.new(Yahns::HttpClient)
|
44
|
+
ctx_class.extend(Yahns::HttpContext)
|
45
|
+
ctx_class.http_ctx_init(self)
|
46
|
+
ctx_class
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_app!
|
50
|
+
if @app.respond_to?(:arity) && @app.arity == 0
|
51
|
+
Gem.refresh if defined?(Gem) && Gem.respond_to?(:refresh)
|
52
|
+
@app = @app.call
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# allow different HttpContext instances to have different Rack defaults
|
57
|
+
def app_defaults
|
58
|
+
{
|
59
|
+
# logger is set in http_context
|
60
|
+
"rack.errors" => $stderr,
|
61
|
+
"rack.multiprocess" => true,
|
62
|
+
"rack.multithread" => true,
|
63
|
+
"rack.run_once" => false,
|
64
|
+
"rack.hijack?" => true,
|
65
|
+
"rack.version" => [ 1, 2 ],
|
66
|
+
"SCRIPT_NAME" => "",
|
67
|
+
|
68
|
+
# this is not in the Rack spec, but some apps may rely on it
|
69
|
+
"SERVER_SOFTWARE" => "yahns"
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def app_after_fork
|
74
|
+
build_app! unless @preload
|
75
|
+
@app
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# register ourselves
|
80
|
+
Yahns::Config::APP_CLASS[:rack] = Yahns::Rack
|
data/lib/yahns/server.rb
ADDED
@@ -0,0 +1,336 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
3
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
|
+
class Yahns::Server # :nodoc:
|
5
|
+
QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ]
|
6
|
+
attr_accessor :daemon_pipe
|
7
|
+
attr_accessor :logger
|
8
|
+
attr_writer :worker_processes
|
9
|
+
include Yahns::SocketHelper
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
@reexec_pid = 0
|
13
|
+
@daemon_pipe = nil # writable IO or true
|
14
|
+
@config = config
|
15
|
+
@workers = {} # pid -> workers
|
16
|
+
@sig_queue = [] # nil in forked workers
|
17
|
+
@logger = Logger.new($stderr)
|
18
|
+
@sev = Yahns::Sigevent.new
|
19
|
+
@listeners = []
|
20
|
+
@pid = nil
|
21
|
+
@worker_processes = nil
|
22
|
+
@user = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def sqwakeup(sig)
|
26
|
+
@sig_queue << sig
|
27
|
+
@sev.sev_signal
|
28
|
+
end
|
29
|
+
|
30
|
+
def start
|
31
|
+
@config.commit!(self)
|
32
|
+
inherit_listeners!
|
33
|
+
# we try inheriting listeners first, so we bind them later.
|
34
|
+
# we don't write the pid file until we've bound listeners in case
|
35
|
+
# yahns was started twice by mistake. Even though our #pid= method
|
36
|
+
# checks for stale/existing pid files, race conditions are still
|
37
|
+
# possible (and difficult/non-portable to avoid) and can be likely
|
38
|
+
# to clobber the pid if the second start was in quick succession
|
39
|
+
# after the first, so we rely on the listener binding to fail in
|
40
|
+
# that case. Some tests (in and outside of this source tree) and
|
41
|
+
# monitoring tools may also rely on pid files existing before we
|
42
|
+
# attempt to connect to the listener(s)
|
43
|
+
|
44
|
+
# setup signal handlers before writing pid file in case people get
|
45
|
+
# trigger happy and send signals as soon as the pid file exists.
|
46
|
+
QUEUE_SIGS.each { |sig| trap(sig) { sqwakeup(sig) } }
|
47
|
+
self.pid = @config.value(:pid) # write pid file
|
48
|
+
bind_new_listeners!
|
49
|
+
if @worker_processes
|
50
|
+
require 'yahns/server_mp'
|
51
|
+
extend Yahns::ServerMP
|
52
|
+
mp_init
|
53
|
+
end
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
# replaces current listener set with +listeners+. This will
|
58
|
+
# close the socket if it will not exist in the new listener set
|
59
|
+
def listeners=(listeners)
|
60
|
+
cur_names, dead_names = [], []
|
61
|
+
listener_names.each do |name|
|
62
|
+
if ?/ == name[0]
|
63
|
+
# mark unlinked sockets as dead so we can rebind them
|
64
|
+
(File.socket?(name) ? cur_names : dead_names) << name
|
65
|
+
else
|
66
|
+
cur_names << name
|
67
|
+
end
|
68
|
+
end
|
69
|
+
set_names = listener_names(listeners)
|
70
|
+
dead_names.concat(cur_names - set_names).uniq!
|
71
|
+
|
72
|
+
@listeners.delete_if do |io|
|
73
|
+
if dead_names.include?(sock_name(io))
|
74
|
+
(io.close rescue nil).nil? # true
|
75
|
+
else
|
76
|
+
set_server_sockopt(io, sock_opts(io))
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
(set_names - cur_names).each { |addr| listen(addr) }
|
82
|
+
end
|
83
|
+
|
84
|
+
# sets the path for the PID file of the master process
|
85
|
+
def pid=(path)
|
86
|
+
if path
|
87
|
+
if x = valid_pid?(path)
|
88
|
+
return path if @pid && path == @pid && x == $$
|
89
|
+
if x == @reexec_pid && @pid =~ /\.oldbin\z/
|
90
|
+
@logger.warn("will not set pid=#{path} while reexec-ed "\
|
91
|
+
"child is running PID:#{x}")
|
92
|
+
return
|
93
|
+
end
|
94
|
+
raise ArgumentError, "Already running on PID:#{x} " \
|
95
|
+
"(or pid=#{path} is stale)"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
unlink_pid_safe(@pid) if @pid
|
99
|
+
|
100
|
+
if path
|
101
|
+
fp = begin
|
102
|
+
tmp = "#{File.dirname(path)}/#{rand}.#$$"
|
103
|
+
File.open(tmp, File::RDWR|File::CREAT|File::EXCL, 0644)
|
104
|
+
rescue Errno::EEXIST
|
105
|
+
retry
|
106
|
+
end
|
107
|
+
fp.syswrite("#$$\n")
|
108
|
+
File.rename(fp.path, path)
|
109
|
+
fp.close
|
110
|
+
end
|
111
|
+
@pid = path
|
112
|
+
end
|
113
|
+
|
114
|
+
# add a given address to the +listeners+ set, idempotently
|
115
|
+
# Allows workers to add a private, per-process listener via the
|
116
|
+
# after_fork hook. Very useful for debugging and testing.
|
117
|
+
# +:tries+ may be specified as an option for the number of times
|
118
|
+
# to retry, and +:delay+ may be specified as the time in seconds
|
119
|
+
# to delay between retries.
|
120
|
+
# A negative value for +:tries+ indicates the listen will be
|
121
|
+
# retried indefinitely, this is useful when workers belonging to
|
122
|
+
# different masters are spawned during a transparent upgrade.
|
123
|
+
def listen(address)
|
124
|
+
address = @config.expand_addr(address)
|
125
|
+
return if String === address && listener_names.include?(address)
|
126
|
+
|
127
|
+
begin
|
128
|
+
io = bind_listen(address, sock_opts(address))
|
129
|
+
unless Kgio::TCPServer === io || Kgio::UNIXServer === io
|
130
|
+
io = server_cast(io)
|
131
|
+
end
|
132
|
+
@logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
|
133
|
+
@listeners << io
|
134
|
+
io
|
135
|
+
rescue Errno::EADDRINUSE => err
|
136
|
+
@logger.error "adding listener failed addr=#{address} (in use)"
|
137
|
+
rescue => err
|
138
|
+
@logger.fatal "error adding listener addr=#{address}"
|
139
|
+
raise err
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def daemon_ready
|
144
|
+
@daemon_pipe or return
|
145
|
+
@daemon_pipe.syswrite("#$$")
|
146
|
+
@daemon_pipe.close
|
147
|
+
@daemon_pipe = true # for SIGWINCH
|
148
|
+
end
|
149
|
+
|
150
|
+
# reexecutes the Yahns::START with a new binary
|
151
|
+
def reexec
|
152
|
+
if @reexec_pid > 0
|
153
|
+
begin
|
154
|
+
Process.kill(0, @reexec_pid)
|
155
|
+
@logger.error "reexec-ed child already running PID:#@reexec_pid"
|
156
|
+
return
|
157
|
+
rescue Errno::ESRCH
|
158
|
+
@reexec_pid = 0
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
if @pid
|
163
|
+
old_pid = "#@pid.oldbin"
|
164
|
+
begin
|
165
|
+
self.pid = old_pid # clear the path for a new pid file
|
166
|
+
rescue ArgumentError
|
167
|
+
@logger.error "old PID:#{valid_pid?(old_pid)} running with " \
|
168
|
+
"existing pid=#{old_pid}, refusing rexec"
|
169
|
+
return
|
170
|
+
rescue => e
|
171
|
+
@logger.error "error writing pid=#{old_pid} #{e.class} #{e.message}"
|
172
|
+
return
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
@reexec_pid = fork do
|
177
|
+
redirects = {}
|
178
|
+
listeners.each do |sock|
|
179
|
+
sock.close_on_exec = false
|
180
|
+
redirects[sock.fileno] = sock
|
181
|
+
end
|
182
|
+
ENV['YAHNS_FD'] = redirects.keys.map(&:to_s).join(',')
|
183
|
+
Dir.chdir(@config.value(:working_directory) || Yahns::START[:cwd])
|
184
|
+
cmd = [ Yahns::START[0] ].concat(Yahns::START[:argv])
|
185
|
+
@logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
|
186
|
+
cmd << redirects
|
187
|
+
exec(*cmd)
|
188
|
+
end
|
189
|
+
proc_name 'master (old)'
|
190
|
+
end
|
191
|
+
|
192
|
+
# unlinks a PID file at given +path+ if it contains the current PID
|
193
|
+
# still potentially racy without locking the directory (which is
|
194
|
+
# non-portable and may interact badly with other programs), but the
|
195
|
+
# window for hitting the race condition is small
|
196
|
+
def unlink_pid_safe(path)
|
197
|
+
(File.read(path).to_i == $$ and File.unlink(path)) rescue nil
|
198
|
+
end
|
199
|
+
|
200
|
+
# returns a PID if a given path contains a non-stale PID file,
|
201
|
+
# nil otherwise.
|
202
|
+
def valid_pid?(path)
|
203
|
+
wpid = File.read(path).to_i
|
204
|
+
wpid <= 0 and return
|
205
|
+
Process.kill(0, wpid)
|
206
|
+
wpid
|
207
|
+
rescue Errno::EPERM
|
208
|
+
@logger.info "pid=#{path} possibly stale, got EPERM signalling PID:#{wpid}"
|
209
|
+
nil
|
210
|
+
rescue Errno::ESRCH, Errno::ENOENT
|
211
|
+
# don't unlink stale pid files, racy without non-portable locking...
|
212
|
+
end
|
213
|
+
|
214
|
+
def load_config!
|
215
|
+
@logger.info "reloading config_file=#{@config.config_file}"
|
216
|
+
@config.config_reload!
|
217
|
+
@config.commit!(self)
|
218
|
+
kill_each_worker(:QUIT)
|
219
|
+
Yahns::Log.reopen_all
|
220
|
+
@logger.info "done reloading config_file=#{@config.config_file}"
|
221
|
+
rescue StandardError, LoadError, SyntaxError => e
|
222
|
+
Yahns::Log.exception(@logger,
|
223
|
+
"error reloading config_file=#{@config.config_file}", e)
|
224
|
+
end
|
225
|
+
|
226
|
+
# returns an array of string names for the given listener array
|
227
|
+
def listener_names(listeners = @listeners)
|
228
|
+
listeners.map { |io| sock_name(io) }
|
229
|
+
end
|
230
|
+
|
231
|
+
def sock_opts(io)
|
232
|
+
@config.config_listeners[sock_name(io)]
|
233
|
+
end
|
234
|
+
|
235
|
+
def inherit_listeners!
|
236
|
+
# inherit sockets from parents, they need to be plain Socket objects
|
237
|
+
# before they become Kgio::UNIXServer or Kgio::TCPServer
|
238
|
+
inherited = ENV['YAHNS_FD'].to_s.split(/,/).map do |fd|
|
239
|
+
io = Socket.for_fd(fd.to_i)
|
240
|
+
set_server_sockopt(io, sock_opts(io))
|
241
|
+
@logger.info "inherited addr=#{sock_name(io)} fd=#{fd}"
|
242
|
+
server_cast(io)
|
243
|
+
end
|
244
|
+
|
245
|
+
@listeners.replace(inherited)
|
246
|
+
end
|
247
|
+
|
248
|
+
# call only after calling inherit_listeners!
|
249
|
+
# This binds any listeners we did NOT inherit from the parent
|
250
|
+
def bind_new_listeners!
|
251
|
+
self.listeners = @config.config_listeners.keys
|
252
|
+
raise ArgumentError, "no listeners" if @listeners.empty?
|
253
|
+
@listeners.each { |l| l.extend(Yahns::Acceptor) }
|
254
|
+
end
|
255
|
+
|
256
|
+
def proc_name(tag)
|
257
|
+
s = Yahns::START
|
258
|
+
$0 = ([ File.basename(s[0]), tag ]).concat(s[:argv]).join(' ')
|
259
|
+
end
|
260
|
+
|
261
|
+
# spins up processing threads of the server
|
262
|
+
def fdmap_init
|
263
|
+
thresh = @config.value(:client_expire_threshold)
|
264
|
+
|
265
|
+
# keeps track of all connections, like ObjectSpace, but only for IOs
|
266
|
+
fdmap = Yahns::Fdmap.new(@logger, thresh)
|
267
|
+
|
268
|
+
# initialize queues (epoll/kqueue) and associated worker threads
|
269
|
+
queues = {}
|
270
|
+
@config.qeggs.each do |name, qegg|
|
271
|
+
queue = qegg.qc_vivify(fdmap) # worker threads run after this
|
272
|
+
queues[qegg] = queue
|
273
|
+
end
|
274
|
+
|
275
|
+
# spin up applications (which are preload: false)
|
276
|
+
@config.app_ctx.each { |ctx| ctx.after_fork_init }
|
277
|
+
|
278
|
+
# spin up acceptors, clients flow into worker queues after this
|
279
|
+
@listeners.each do |l|
|
280
|
+
ctx = sock_opts(l)[:yahns_app_ctx]
|
281
|
+
qegg = ctx.qegg || @config.qeggs[:default]
|
282
|
+
|
283
|
+
# acceptors feed the the queues
|
284
|
+
l.spawn_acceptor(@logger, ctx, queues[qegg])
|
285
|
+
end
|
286
|
+
fdmap
|
287
|
+
end
|
288
|
+
|
289
|
+
def usr1_reopen(prefix)
|
290
|
+
@logger.info "#{prefix}reopening logs..."
|
291
|
+
Yahns::Log.reopen_all
|
292
|
+
@logger.info "#{prefix}done reopening logs"
|
293
|
+
end
|
294
|
+
|
295
|
+
def quit_enter(alive)
|
296
|
+
self.listeners = []
|
297
|
+
exit(0) unless alive # drop connections immediately if signaled twice
|
298
|
+
@config.config_listeners.each_value do |opts|
|
299
|
+
ctx = opts[:yahns_app_ctx] or next
|
300
|
+
ctx.persistent_connections = false # Yahns::HttpContext
|
301
|
+
end
|
302
|
+
false
|
303
|
+
end
|
304
|
+
|
305
|
+
def sp_sig_handle(alive)
|
306
|
+
@sev.kgio_wait_readable(alive ? nil : 0.01)
|
307
|
+
@sev.yahns_step
|
308
|
+
case sig = @sig_queue.shift
|
309
|
+
when :QUIT, :TERM, :INT
|
310
|
+
return quit_enter(alive)
|
311
|
+
when :USR1
|
312
|
+
usr1_reopen('')
|
313
|
+
when :USR2
|
314
|
+
reexec
|
315
|
+
when :HUP
|
316
|
+
reexec
|
317
|
+
return false
|
318
|
+
when :TTIN, :TTOU, :WINCH
|
319
|
+
@logger.info("SIG#{sig} ignored in single-process mode")
|
320
|
+
end
|
321
|
+
alive
|
322
|
+
end
|
323
|
+
|
324
|
+
# single-threaded only, this is overriden if @worker_processes is non-nil
|
325
|
+
def join
|
326
|
+
daemon_ready
|
327
|
+
fdmap = fdmap_init
|
328
|
+
alive = true
|
329
|
+
begin
|
330
|
+
alive = sp_sig_handle(alive)
|
331
|
+
rescue => e
|
332
|
+
Yahns::Log.exception(@logger, "main loop", e)
|
333
|
+
end while alive || fdmap.size > 0
|
334
|
+
unlink_pid_safe(@pid) if @pid
|
335
|
+
end
|
336
|
+
end
|