yahns 0.0.1 → 0.0.2

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.
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