yahns 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Documentation/.gitignore +5 -0
  4. data/Documentation/GNUmakefile +50 -0
  5. data/Documentation/yahns-rackup.txt +152 -0
  6. data/Documentation/yahns.txt +68 -0
  7. data/Documentation/yahns_config.txt +563 -0
  8. data/GIT-VERSION-GEN +1 -1
  9. data/GNUmakefile +14 -7
  10. data/HACKING +56 -0
  11. data/INSTALL +8 -0
  12. data/README +15 -2
  13. data/Rakefile +2 -2
  14. data/bin/yahns +1 -2
  15. data/bin/yahns-rackup +9 -0
  16. data/examples/yahns_multi.conf.rb +14 -4
  17. data/examples/yahns_rack_basic.conf.rb +17 -1
  18. data/extras/README +16 -0
  19. data/extras/autoindex.rb +151 -0
  20. data/extras/exec_cgi.rb +108 -0
  21. data/extras/proxy_pass.rb +210 -0
  22. data/extras/try_gzip_static.rb +208 -0
  23. data/lib/yahns.rb +5 -2
  24. data/lib/yahns/acceptor.rb +64 -22
  25. data/lib/yahns/cap_input.rb +2 -2
  26. data/lib/yahns/{client_expire_portable.rb → client_expire_generic.rb} +12 -11
  27. data/lib/yahns/{client_expire.rb → client_expire_tcpi.rb} +7 -6
  28. data/lib/yahns/config.rb +107 -22
  29. data/lib/yahns/daemon.rb +2 -0
  30. data/lib/yahns/fdmap.rb +28 -9
  31. data/lib/yahns/http_client.rb +123 -37
  32. data/lib/yahns/http_context.rb +21 -3
  33. data/lib/yahns/http_response.rb +80 -19
  34. data/lib/yahns/log.rb +23 -4
  35. data/lib/yahns/queue_epoll.rb +20 -9
  36. data/lib/yahns/queue_quitter.rb +16 -0
  37. data/lib/yahns/queue_quitter_pipe.rb +24 -0
  38. data/lib/yahns/rack.rb +0 -1
  39. data/lib/yahns/rackup_handler.rb +57 -0
  40. data/lib/yahns/server.rb +189 -59
  41. data/lib/yahns/server_mp.rb +43 -35
  42. data/lib/yahns/sigevent_pipe.rb +1 -0
  43. data/lib/yahns/socket_helper.rb +37 -11
  44. data/lib/yahns/stream_file.rb +14 -4
  45. data/lib/yahns/stream_input.rb +13 -7
  46. data/lib/yahns/tcp_server.rb +7 -0
  47. data/lib/yahns/tmpio.rb +10 -3
  48. data/lib/yahns/unix_server.rb +7 -0
  49. data/lib/yahns/wbuf.rb +19 -2
  50. data/lib/yahns/wbuf_common.rb +10 -3
  51. data/lib/yahns/wbuf_str.rb +24 -0
  52. data/lib/yahns/worker.rb +5 -26
  53. data/test/helper.rb +15 -5
  54. data/test/server_helper.rb +37 -1
  55. data/test/test_bin.rb +17 -8
  56. data/test/test_buffer_tmpdir.rb +103 -0
  57. data/test/test_client_expire.rb +71 -35
  58. data/test/test_client_max_body_size.rb +5 -13
  59. data/test/test_config.rb +1 -1
  60. data/test/test_expect_100.rb +176 -0
  61. data/test/test_extras_autoindex.rb +53 -0
  62. data/test/test_extras_exec_cgi.rb +81 -0
  63. data/test/test_extras_exec_cgi.sh +35 -0
  64. data/test/test_extras_try_gzip_static.rb +177 -0
  65. data/test/test_input.rb +128 -0
  66. data/test/test_mt_accept.rb +48 -0
  67. data/test/test_output_buffering.rb +90 -63
  68. data/test/test_rack.rb +1 -1
  69. data/test/test_rack_hijack.rb +2 -6
  70. data/test/test_reopen_logs.rb +2 -8
  71. data/test/test_serve_static.rb +104 -8
  72. data/test/test_server.rb +448 -73
  73. data/test/test_stream_file.rb +1 -1
  74. data/test/test_unix_socket.rb +72 -0
  75. data/test/test_wbuf.rb +20 -17
  76. data/yahns.gemspec +3 -0
  77. metadata +57 -5
data/lib/yahns/log.rb CHANGED
@@ -41,9 +41,9 @@ module Yahns::Log # :nodoc:
41
41
  ObjectSpace.each_object(File) { |fp| is_log?(fp) and to_reopen << fp }
42
42
 
43
43
  to_reopen.each do |fp|
44
- orig_st = begin
45
- fp.stat
46
- rescue IOError, Errno::EBADF
44
+ begin
45
+ orig_st = fp.stat
46
+ rescue IOError, Errno::EBADF # race
47
47
  next
48
48
  end
49
49
 
@@ -54,8 +54,27 @@ module Yahns::Log # :nodoc:
54
54
  end
55
55
 
56
56
  begin
57
- File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
57
+ # stdin, stdout, stderr are special. The following dance should
58
+ # guarantee there is no window where `fp' is unwritable in MRI
59
+ # (or any correct Ruby implementation).
60
+ #
61
+ # Fwiw, GVL has zero bearing here. This is tricky because of
62
+ # the unavoidable existence of stdio FILE * pointers for
63
+ # std{in,out,err} in all programs which may use the standard C library
64
+ if fp.fileno <= 2
65
+ # We do not want to hit fclose(3)->dup(2) window for std{in,out,err}
66
+ # MRI will use freopen(3) here internally on std{in,out,err}
67
+ fp.reopen(fp.path, "a")
68
+ else
69
+ # We should not need this: http://bugs.ruby-lang.org/issues/9036
70
+ # MRI will not call call fclose(3) or freopen(3) here
71
+ # since there's no associated std{in,out,err} FILE * pointer
72
+ # This should atomically use dup3(2) (or dup2(2)) syscall
73
+ File.open(fp.path, "a") { |tmpfp| fp.reopen(tmpfp) }
74
+ end
75
+
58
76
  fp.sync = true
77
+ fp.flush # IO#sync=true may not implicitly flush
59
78
  new_st = fp.stat
60
79
 
61
80
  # this should only happen in the master:
@@ -6,6 +6,7 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
6
6
  attr_accessor :fdmap # Yahns::Fdmap
7
7
 
8
8
  # public
9
+ QEV_QUIT = Epoll::OUT # Level Trigger for QueueQuitter
9
10
  QEV_RD = Epoll::IN | Epoll::ONESHOT
10
11
  QEV_WR = Epoll::OUT | Epoll::ONESHOT
11
12
  QEV_RDWR = QEV_RD | QEV_WR
@@ -21,10 +22,21 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
21
22
  epoll_ctl(Epoll::CTL_ADD, io, flags)
22
23
  end
23
24
 
25
+ def thr_init
26
+ Thread.current[:yahns_rbuf] = ""
27
+ Thread.current[:yahns_fdmap] = @fdmap
28
+ end
29
+
30
+ # use only before hijacking, once hijacked, io may be unusable to us
31
+ def queue_del(io)
32
+ @fdmap.forget(io)
33
+ epoll_ctl(Epoll::CTL_DEL, io, 0)
34
+ end
35
+
24
36
  # returns an array of infinitely running threads
25
37
  def worker_thread(logger, max_events)
26
38
  Thread.new do
27
- Thread.current[:yahns_rbuf] = ""
39
+ thr_init
28
40
  begin
29
41
  epoll_wait(max_events) do |_, io| # don't care for flags for now
30
42
  case rv = io.yahns_step
@@ -35,19 +47,18 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
35
47
  when :wait_readwrite
36
48
  epoll_ctl(Epoll::CTL_MOD, io, QEV_RDWR)
37
49
  when :ignore # only used by rack.hijack
38
- @fdmap.decr
39
- when nil
40
- # this is be the ONLY place where we call IO#close on
41
- # things inside the queue
42
- io.close
43
- @fdmap.decr
50
+ # we cannot call Epoll::CTL_DEL after hijacking, the hijacker
51
+ # may have already closed it Likewise, io.fileno is not
52
+ # expected to work, so we had to erase it from fdmap before hijack
53
+ when nil, :close
54
+ # this must be the ONLY place where we call IO#close on
55
+ # things that got inside the queue
56
+ @fdmap.sync_close(io)
44
57
  else
45
58
  raise "BUG: #{io.inspect}#yahns_step returned: #{rv.inspect}"
46
59
  end
47
60
  end
48
61
  rescue => e
49
- # sleep since this check is racy (and uncommon)
50
- break if closed? || (sleep(0.01) && closed?)
51
62
  Yahns::Log.exception(logger, 'queue loop', e)
52
63
  end while true
53
64
  end
@@ -0,0 +1,16 @@
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
+ require 'sleepy_penguin'
5
+
6
+ # add this as a level-triggered to any thread pool stuck on epoll_wait
7
+ # and watch it die!
8
+ if SleepyPenguin.const_defined?(:EventFD)
9
+ class Yahns::QueueQuitter < Yahns::Sigevent # :nodoc:
10
+ def yahns_step
11
+ Thread.current.exit
12
+ end
13
+ end
14
+ else
15
+ require_relative 'queue_quitter_pipe'
16
+ end
@@ -0,0 +1,24 @@
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
+ # POSIX pipe version, see queue_quitter.rb for the (preferred) eventfd one
5
+ class Yahns::QueueQuitter # :nodoc:
6
+ attr_reader :to_io
7
+ def initialize
8
+ reader, @to_io = IO.pipe
9
+ @to_io.close_on_exec = true
10
+ reader.close
11
+ end
12
+
13
+ def yahns_step
14
+ Thread.current.exit
15
+ end
16
+
17
+ def fileno
18
+ @to_io.fileno
19
+ end
20
+
21
+ def close
22
+ @to_io.close
23
+ end
24
+ end
data/lib/yahns/rack.rb CHANGED
@@ -18,7 +18,6 @@ class Yahns::Rack # :nodoc:
18
18
  def initialize(ru, opts = {})
19
19
  # always called after config file parsing, may be called after forking
20
20
  @app = lambda do
21
- ENV["RACK_ENV"] ||= "none"
22
21
  if ru.respond_to?(:call)
23
22
  inner_app = ru.respond_to?(:to_app) ? ru.to_app : ru
24
23
  else
@@ -0,0 +1,57 @@
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
+ require 'yahns'
4
+
5
+ module Yahns::RackupHandler # :nodoc:
6
+ def self.default_host
7
+ environment = ENV['RACK_ENV'] || 'development'
8
+ environment == 'development' ? '127.0.0.1' : '0.0.0.0'
9
+ end
10
+
11
+ def self.run(app, o)
12
+ cfg = Yahns::Config.new
13
+ cfg.instance_eval do
14
+ # we need this because "rackup -D" sends us to "/", which might be
15
+ # fine for most apps, but we have SIGUSR2 restarts to support
16
+ working_directory(Yahns::START[:cwd])
17
+
18
+ app(:rack, app) do
19
+ addr = o[:listen] || "#{o[:Host]||default_host}:#{o[:Port]||8080}"
20
+ # allow listening to multiple addresses
21
+ if addr =~ /,/
22
+ addr.split(/,/).each { |l| listen(l) }
23
+ else
24
+ listen addr
25
+ end
26
+
27
+ val = o[:client_timeout] and client_timeout(val)
28
+ end
29
+
30
+ queue do
31
+ wt = o[:worker_threads] and worker_threads(wt)
32
+ end
33
+
34
+ %w(stderr_path stdout_path).each do |x|
35
+ x = x.to_sym
36
+ val = o[x] and __send__(x, val)
37
+ end
38
+ end
39
+ Yahns::Server.new(cfg).start.join
40
+ end
41
+
42
+ def self.valid_options
43
+ # these should be the most common options
44
+ {
45
+ "listen=ADDRESS" => "address(es) to listen on (e.g. /tmp/sock)",
46
+ "worker_threads=NUM" => "number of worker threads to run",
47
+
48
+ # this affects how quickly graceful shutdown goes
49
+ "client_timeout=SECONDS" => "timeout for idle clients",
50
+
51
+ # I don't want these here, but rackup supports daemonize and
52
+ # we lose useful information when that sends stdout/stderr to /dev/null
53
+ "stderr_path=PATH" => "stderr destination",
54
+ "stdout_path=PATH" => "stdout destination",
55
+ }
56
+ end
57
+ end
data/lib/yahns/server.rb CHANGED
@@ -1,14 +1,26 @@
1
1
  # -*- encoding: binary -*-
2
2
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
3
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require_relative 'queue_quitter'
5
+ require_relative 'tcp_server'
6
+ require_relative 'unix_server'
7
+
4
8
  class Yahns::Server # :nodoc:
5
- QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ]
9
+ QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU,
10
+ :CHLD ]
6
11
  attr_accessor :daemon_pipe
7
12
  attr_accessor :logger
13
+ attr_writer :user
14
+ attr_writer :before_exec
8
15
  attr_writer :worker_processes
16
+ attr_writer :shutdown_timeout
17
+ attr_writer :atfork_prepare
18
+ attr_writer :atfork_parent
19
+ attr_writer :atfork_child
9
20
  include Yahns::SocketHelper
10
21
 
11
22
  def initialize(config)
23
+ @shutdown_timeout = nil
12
24
  @reexec_pid = 0
13
25
  @daemon_pipe = nil # writable IO or true
14
26
  @config = config
@@ -19,9 +31,11 @@ class Yahns::Server # :nodoc:
19
31
  @listeners = []
20
32
  @pid = nil
21
33
  @worker_processes = nil
34
+ @before_exec = nil
35
+ @atfork_prepare = @atfork_parent = @atfork_child = nil
22
36
  @user = nil
23
37
  @queues = []
24
- @thr = []
38
+ @wthr = []
25
39
  end
26
40
 
27
41
  def sqwakeup(sig)
@@ -34,28 +48,40 @@ class Yahns::Server # :nodoc:
34
48
  inherit_listeners!
35
49
  # we try inheriting listeners first, so we bind them later.
36
50
  # we don't write the pid file until we've bound listeners in case
37
- # yahns was started twice by mistake. Even though our #pid= method
38
- # checks for stale/existing pid files, race conditions are still
39
- # possible (and difficult/non-portable to avoid) and can be likely
40
- # to clobber the pid if the second start was in quick succession
41
- # after the first, so we rely on the listener binding to fail in
42
- # that case. Some tests (in and outside of this source tree) and
43
- # monitoring tools may also rely on pid files existing before we
44
- # attempt to connect to the listener(s)
51
+ # yahns was started twice by mistake.
45
52
 
46
53
  # setup signal handlers before writing pid file in case people get
47
54
  # trigger happy and send signals as soon as the pid file exists.
48
55
  QUEUE_SIGS.each { |sig| trap(sig) { sqwakeup(sig) } }
49
- self.pid = @config.value(:pid) # write pid file
50
56
  bind_new_listeners!
57
+ self.pid = @config.value(:pid) # write pid file
51
58
  if @worker_processes
52
59
  require 'yahns/server_mp'
53
60
  extend Yahns::ServerMP
54
- mp_init
61
+ else
62
+ switch_user(*@user) if @user
55
63
  end
56
64
  self
57
65
  end
58
66
 
67
+ def switch_user(user, group = nil)
68
+ # we do not protect the caller, checking Process.euid == 0 is
69
+ # insufficient because modern systems have fine-grained
70
+ # capabilities. Let the caller handle any and all errors.
71
+ uid = Etc.getpwnam(user).uid
72
+ gid = Etc.getgrnam(group).gid if group
73
+ Yahns::Log.chown_all(uid, gid)
74
+ if gid && Process.egid != gid
75
+ Process.initgroups(user, gid)
76
+ Process::GID.change_privilege(gid)
77
+ end
78
+ Process.euid != uid and Process::UID.change_privilege(uid)
79
+ end
80
+
81
+ def drop_acceptors
82
+ @listeners.delete_if(&:ac_quit)
83
+ end
84
+
59
85
  # replaces current listener set with +listeners+. This will
60
86
  # close the socket if it will not exist in the new listener set
61
87
  def listeners=(listeners)
@@ -70,19 +96,41 @@ class Yahns::Server # :nodoc:
70
96
  end
71
97
  set_names = listener_names(listeners)
72
98
  dead_names.concat(cur_names - set_names).uniq!
73
-
99
+ dying = []
74
100
  @listeners.delete_if do |io|
75
101
  if dead_names.include?(sock_name(io))
76
- (io.close rescue nil).nil? # true
102
+ if io.ac_quit
103
+ true
104
+ else
105
+ dying << io
106
+ false
107
+ end
77
108
  else
78
109
  set_server_sockopt(io, sock_opts(io))
79
110
  false
80
111
  end
81
112
  end
82
113
 
114
+ dying.delete_if(&:ac_quit) while dying[0]
115
+
83
116
  (set_names - cur_names).each { |addr| listen(addr) }
84
117
  end
85
118
 
119
+ def clobber_pid(path)
120
+ unlink_pid_safe(@pid) if @pid
121
+ if path
122
+ fp = begin
123
+ tmp = "#{File.dirname(path)}/#{rand}.#$$"
124
+ File.open(tmp, File::RDWR|File::CREAT|File::EXCL, 0644)
125
+ rescue Errno::EEXIST
126
+ retry
127
+ end
128
+ fp.syswrite("#$$\n")
129
+ File.rename(fp.path, path)
130
+ fp.close
131
+ end
132
+ end
133
+
86
134
  # sets the path for the PID file of the master process
87
135
  def pid=(path)
88
136
  if path
@@ -97,18 +145,18 @@ class Yahns::Server # :nodoc:
97
145
  "(or pid=#{path} is stale)"
98
146
  end
99
147
  end
100
- unlink_pid_safe(@pid) if @pid
101
148
 
102
- if path
103
- fp = begin
104
- tmp = "#{File.dirname(path)}/#{rand}.#$$"
105
- File.open(tmp, File::RDWR|File::CREAT|File::EXCL, 0644)
106
- rescue Errno::EEXIST
107
- retry
149
+ # rename the old pid if possible
150
+ if @pid && path
151
+ begin
152
+ File.rename(@pid, path)
153
+ rescue Errno::ENOENT, Errno::EXDEV
154
+ # a user may have accidentally removed the original,
155
+ # obviously cross-FS renames don't work, either.
156
+ clobber_pid(path)
108
157
  end
109
- fp.syswrite("#$$\n")
110
- File.rename(fp.path, path)
111
- fp.close
158
+ else
159
+ clobber_pid(path)
112
160
  end
113
161
  @pid = path
114
162
  end
@@ -125,17 +173,27 @@ class Yahns::Server # :nodoc:
125
173
  def listen(address)
126
174
  address = @config.expand_addr(address)
127
175
  return if String === address && listener_names.include?(address)
176
+ delay = 0.5
177
+ tries = 5
128
178
 
129
179
  begin
130
180
  io = bind_listen(address, sock_opts(address))
131
- unless Kgio::TCPServer === io || Kgio::UNIXServer === io
181
+ unless Yahns::TCPServer === io || Yahns::UNIXServer === io
132
182
  io = server_cast(io)
133
183
  end
134
184
  @logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
135
185
  @listeners << io
136
186
  io
137
187
  rescue Errno::EADDRINUSE => err
138
- @logger.error "adding listener failed addr=#{address} (in use)"
188
+ if tries == 0
189
+ @logger.error "adding listener failed addr=#{address} (in use)"
190
+ raise err
191
+ end
192
+ tries -= 1
193
+ @logger.warn "retrying in #{delay} seconds " \
194
+ "(#{tries < 0 ? 'infinite' : tries} tries left)"
195
+ sleep(delay)
196
+ retry
139
197
  rescue => err
140
198
  @logger.fatal "error adding listener addr=#{address}"
141
199
  raise err
@@ -175,15 +233,25 @@ class Yahns::Server # :nodoc:
175
233
  end
176
234
  end
177
235
 
178
- opts = {}
179
- @listeners.each { |sock| opts[sock.fileno] = sock }
180
- env = { "YAHNS_FD" => opts.keys.map(&:to_s).join(',') }
181
- opts[:chdir] = @config.value(:working_directory) || Yahns::START[:cwd]
182
- cmd = [ Yahns::START[0] ].concat(Yahns::START[:argv])
183
- @logger.info "spawning #{cmd.inspect} (in #{opts[:chdir]})"
184
- cmd << opts
185
- @reexec_pid = Process.spawn(env, *cmd)
186
- proc_name 'master (old)'
236
+ # We cannot use Process.spawn here because of redirects + close-on-exec
237
+ # We must keep close_on_exec=true in the parent process and only set
238
+ # close_on_exec=false in the child. There must be no opportunity
239
+ # for the user app to ever get a listen socket with close_on_exec=false
240
+ @reexec_pid = fork do
241
+ redirects = {}
242
+ @listeners.each do |sock|
243
+ sock.close_on_exec = false
244
+ redirects[sock.fileno] = sock
245
+ end
246
+ ENV['YAHNS_FD'] = redirects.keys.join(',')
247
+ redirects[:close_others] = true
248
+ Dir.chdir(@config.value(:working_directory) || Yahns::START[:cwd])
249
+ cmd = [ Yahns::START[0] ].concat(Yahns::START[:argv])
250
+ @logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
251
+ @before_exec.call(cmd) if @before_exec
252
+ cmd << redirects
253
+ exec(*cmd)
254
+ end
187
255
  end
188
256
 
189
257
  # unlinks a PID file at given +path+ if it contains the current PID
@@ -231,7 +299,16 @@ class Yahns::Server # :nodoc:
231
299
 
232
300
  def inherit_listeners!
233
301
  # inherit sockets from parents, they need to be plain Socket objects
234
- # before they become Kgio::UNIXServer or Kgio::TCPServer
302
+ # before they become Yahns::UNIXServer or Yahns::TCPServer
303
+ #
304
+ # Note: we intentionally use a yahns-specific environment variable
305
+ # here because existing servers may use non-blocking listen sockets.
306
+ # yahns uses _blocking_ listen sockets exclusively. We cannot
307
+ # change an existing socket to blocking mode if two servers are
308
+ # running (one expecting blocking, one expecting non-blocking)
309
+ # because that can completely break the non-blocking one.
310
+ # Unfortunately, there is no one-off MSG_DONTWAIT-like flag for
311
+ # accept4(2).
235
312
  inherited = ENV['YAHNS_FD'].to_s.split(/,/).map do |fd|
236
313
  io = Socket.for_fd(fd.to_i)
237
314
  set_server_sockopt(io, sock_opts(io))
@@ -247,7 +324,6 @@ class Yahns::Server # :nodoc:
247
324
  def bind_new_listeners!
248
325
  self.listeners = @config.config_listeners.keys
249
326
  raise ArgumentError, "no listeners" if @listeners.empty?
250
- @listeners.each { |l| l.extend(Yahns::Acceptor) }
251
327
  end
252
328
 
253
329
  def proc_name(tag)
@@ -255,6 +331,15 @@ class Yahns::Server # :nodoc:
255
331
  $0 = ([ File.basename(s[0]), tag ]).concat(s[:argv]).join(' ')
256
332
  end
257
333
 
334
+ def qegg_vivify(qegg, fdmap)
335
+ queue = qegg.vivify(fdmap)
336
+ qegg.worker_threads.times do
337
+ @wthr << queue.worker_thread(@logger, qegg.max_events)
338
+ end
339
+ @queues << queue
340
+ queue
341
+ end
342
+
258
343
  # spins up processing threads of the server
259
344
  def fdmap_init
260
345
  thresh = @config.value(:client_expire_threshold)
@@ -262,27 +347,26 @@ class Yahns::Server # :nodoc:
262
347
  # keeps track of all connections, like ObjectSpace, but only for IOs
263
348
  fdmap = Yahns::Fdmap.new(@logger, thresh)
264
349
 
265
- # initialize queues (epoll/kqueue) and associated worker threads
350
+ # once initialize queues (epoll/kqueue) and associated worker threads
266
351
  queues = {}
267
- @config.qeggs.each do |name, qe|
268
- queue = qe.vivify(fdmap)
269
- qe.worker_threads.times do
270
- @thr << queue.worker_thread(@logger, qe.max_events)
271
- end
272
- @queues << queue
273
- queues[qe] = queue
274
- end
275
352
 
276
353
  # spin up applications (which are preload: false)
277
- @config.app_ctx.each { |ctx| ctx.after_fork_init }
354
+ @config.app_ctx.each(&:after_fork_init)
355
+
356
+ @shutdown_timeout ||= @config.app_ctx.map(&:client_timeout).max
278
357
 
279
358
  # spin up acceptor threads, clients flow into worker queues after this
280
359
  @listeners.each do |l|
281
- ctx = sock_opts(l)[:yahns_app_ctx]
360
+ opts = sock_opts(l)
361
+ ctx = opts[:yahns_app_ctx]
362
+ ctx_list = opts[:yahns_app_ctx_list] ||= []
282
363
  qegg = ctx.qegg || @config.qeggs[:default]
283
-
364
+ ctx.queue = queues[qegg] ||= qegg_vivify(qegg, fdmap)
365
+ ctx = ctx.dup
366
+ ctx.__send__(:include, l.expire_mod)
367
+ ctx_list << ctx
284
368
  # acceptors feed the the queues
285
- @thr << l.spawn_acceptor(@logger, ctx, queues[qegg])
369
+ l.spawn_acceptor(opts[:threads] || 1, @logger, ctx)
286
370
  end
287
371
  fdmap
288
372
  end
@@ -294,20 +378,55 @@ class Yahns::Server # :nodoc:
294
378
  end
295
379
 
296
380
  def quit_enter(alive)
297
- self.listeners = [] # close acceptors, we close epolls in quit_done
298
- exit(0) unless alive # drop connections immediately if signaled twice
381
+ if alive
382
+ @logger.info("gracefully exiting shutdown_timeout=#{@shutdown_timeout} s")
383
+ else # drop connections immediately if signaled twice
384
+ @logger.info("graceful exit aborted, exiting immediately")
385
+ # we will still call any app-defined at_exit hooks here
386
+ # use SIGKILL if you don't want that.
387
+ exit(0)
388
+ end
389
+
390
+ drop_acceptors # stop acceptors, we close epolls in quit_done
299
391
  @config.config_listeners.each_value do |opts|
300
- ctx = opts[:yahns_app_ctx] or next
301
- ctx.persistent_connections = false # Yahns::HttpContext
392
+ list= opts[:yahns_app_ctx_list] or next
393
+ # Yahns::HttpContext#persistent_connections=
394
+ list.each { |ctx| ctx.persistent_connections = false }
302
395
  end
303
396
  false
304
397
  end
305
398
 
306
399
  # drops all the the IO objects we have threads waiting on before exiting
400
+ # This just injects the QueueQuitter object which acts like a
401
+ # monkey wrench thrown into a perfectly good engine :)
307
402
  def quit_finish
308
- @queues.each(&:close)
309
- self.listeners = [] # just in case, this is used in ensure
310
- @thr.each(&:join)
403
+ quitter = Yahns::QueueQuitter.new
404
+
405
+ # throw the monkey wrench into the worker threads
406
+ @queues.each { |q| q.queue_add(quitter, Yahns::Queue::QEV_QUIT) }
407
+
408
+ # watch the monkey wrench destroy all the threads!
409
+ @wthr.delete_if { |t| t.join(0.01) } while @wthr[0]
410
+
411
+ # cleanup, our job is done
412
+ @queues.each(&:close).clear
413
+ quitter.close
414
+ rescue => e
415
+ Yahns::Log.exception(@logger, "quit finish", e)
416
+ ensure
417
+ if (@wthr.size + @listeners.size) > 0
418
+ @logger.error("BUG: still active wthr=#{@wthr.size} "\
419
+ "listeners=#{@listeners.size}")
420
+ end
421
+ end
422
+
423
+ def reap_reexec
424
+ @reexec_pid > 0 or return
425
+ wpid, status = Process.waitpid2(@reexec_pid, Process::WNOHANG)
426
+ wpid or return
427
+ @logger.error "reaped #{status.inspect} exec()-ed"
428
+ @reexec_pid = 0
429
+ self.pid = @pid.chomp('.oldbin') if @pid
311
430
  end
312
431
 
313
432
  def sp_sig_handle(alive)
@@ -316,19 +435,30 @@ class Yahns::Server # :nodoc:
316
435
  case sig = @sig_queue.shift
317
436
  when :QUIT, :TERM, :INT
318
437
  return quit_enter(alive)
438
+ when :CHLD
439
+ reap_reexec
319
440
  when :USR1
320
441
  usr1_reopen('')
321
442
  when :USR2
322
443
  reexec
323
444
  when :HUP
324
445
  reexec
325
- return false
446
+ return quit_enter(alive)
326
447
  when :TTIN, :TTOU, :WINCH
327
448
  @logger.info("SIG#{sig} ignored in single-process mode")
328
449
  end
329
450
  alive
330
451
  end
331
452
 
453
+ def dropping(fdmap)
454
+ if drop_acceptors[0] || fdmap.size > 0
455
+ fdmap.desperate_expire_for(nil, @shutdown_timeout)
456
+ true
457
+ else
458
+ false
459
+ end
460
+ end
461
+
332
462
  # single-threaded only, this is overriden if @worker_processes is non-nil
333
463
  def join
334
464
  daemon_ready
@@ -338,7 +468,7 @@ class Yahns::Server # :nodoc:
338
468
  alive = sp_sig_handle(alive)
339
469
  rescue => e
340
470
  Yahns::Log.exception(@logger, "main loop", e)
341
- end while alive || fdmap.size > 0
471
+ end while alive || dropping(fdmap)
342
472
  unlink_pid_safe(@pid) if @pid
343
473
  ensure
344
474
  quit_finish