yahns 0.0.0TP1
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 +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
|