yahns 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,6 +20,8 @@ class Yahns::Server # :nodoc:
20
20
  @pid = nil
21
21
  @worker_processes = nil
22
22
  @user = nil
23
+ @queues = []
24
+ @thr = []
23
25
  end
24
26
 
25
27
  def sqwakeup(sig)
@@ -173,19 +175,14 @@ class Yahns::Server # :nodoc:
173
175
  end
174
176
  end
175
177
 
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
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)
189
186
  proc_name 'master (old)'
190
187
  end
191
188
 
@@ -267,21 +264,25 @@ class Yahns::Server # :nodoc:
267
264
 
268
265
  # initialize queues (epoll/kqueue) and associated worker threads
269
266
  queues = {}
270
- @config.qeggs.each do |name, qegg|
271
- queue = qegg.qc_vivify(fdmap) # worker threads run after this
272
- queues[qegg] = queue
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
273
274
  end
274
275
 
275
276
  # spin up applications (which are preload: false)
276
277
  @config.app_ctx.each { |ctx| ctx.after_fork_init }
277
278
 
278
- # spin up acceptors, clients flow into worker queues after this
279
+ # spin up acceptor threads, clients flow into worker queues after this
279
280
  @listeners.each do |l|
280
281
  ctx = sock_opts(l)[:yahns_app_ctx]
281
282
  qegg = ctx.qegg || @config.qeggs[:default]
282
283
 
283
284
  # acceptors feed the the queues
284
- l.spawn_acceptor(@logger, ctx, queues[qegg])
285
+ @thr << l.spawn_acceptor(@logger, ctx, queues[qegg])
285
286
  end
286
287
  fdmap
287
288
  end
@@ -293,7 +294,7 @@ class Yahns::Server # :nodoc:
293
294
  end
294
295
 
295
296
  def quit_enter(alive)
296
- self.listeners = []
297
+ self.listeners = [] # close acceptors, we close epolls in quit_done
297
298
  exit(0) unless alive # drop connections immediately if signaled twice
298
299
  @config.config_listeners.each_value do |opts|
299
300
  ctx = opts[:yahns_app_ctx] or next
@@ -302,6 +303,13 @@ class Yahns::Server # :nodoc:
302
303
  false
303
304
  end
304
305
 
306
+ # drops all the the IO objects we have threads waiting on before exiting
307
+ def quit_finish
308
+ @queues.each(&:close)
309
+ self.listeners = [] # just in case, this is used in ensure
310
+ @thr.each(&:join)
311
+ end
312
+
305
313
  def sp_sig_handle(alive)
306
314
  @sev.kgio_wait_readable(alive ? nil : 0.01)
307
315
  @sev.yahns_step
@@ -332,5 +340,7 @@ class Yahns::Server # :nodoc:
332
340
  Yahns::Log.exception(@logger, "main loop", e)
333
341
  end while alive || fdmap.size > 0
334
342
  unlink_pid_safe(@pid) if @pid
343
+ ensure
344
+ quit_finish
335
345
  end
336
346
  end
@@ -150,7 +150,7 @@ module Yahns::ServerMP # :nodoc:
150
150
 
151
151
  def fdmap_init_mp
152
152
  fdmap = fdmap_init # builds apps (if not preloading)
153
- EXIT_SIGS.each { |sig| trap(sig) { sqwakeup(sig) } }
153
+ [:USR1, *EXIT_SIGS].each { |sig| trap(sig) { sqwakeup(sig) } }
154
154
  @config.postfork_cleanup # reduce live objects
155
155
  fdmap
156
156
  end
@@ -164,6 +164,8 @@ module Yahns::ServerMP # :nodoc:
164
164
  Yahns::Log.exception(@logger, "main worker loop", e)
165
165
  end while alive || fdmap.size > 0
166
166
  exit
167
+ ensure
168
+ quit_finish
167
169
  end
168
170
 
169
171
  def mp_sig_handle(worker, alive)
@@ -5,6 +5,7 @@
5
5
  module Yahns::SocketHelper # :nodoc:
6
6
  def set_server_sockopt(sock, opt)
7
7
  opt = {backlog: 1024}.merge!(opt) if opt
8
+ sock.close_on_exec = true
8
9
 
9
10
  TCPSocket === sock and sock.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, 1)
10
11
  sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1)
@@ -60,7 +60,7 @@ class Yahns::StreamInput # :nodoc:
60
60
  end
61
61
 
62
62
  def __rsize
63
- @client.class.client_body_buffer_size
63
+ @client ? @client.class.client_body_buffer_size : nil
64
64
  end
65
65
 
66
66
  # :call-seq:
@@ -79,7 +79,7 @@ class Yahns::StreamInput # :nodoc:
79
79
  return rv.empty? ? nil : rv
80
80
  end
81
81
  re = /\A(.*?#{Regexp.escape(sep)})/
82
- rsize = __rsize
82
+ rsize = __rsize or return
83
83
  begin
84
84
  @rbuf.sub!(re, '') and return $1
85
85
  return @rbuf.empty? ? nil : @rbuf.slice!(0, @rbuf.size) if eof?
@@ -124,8 +124,7 @@ class Yahns::StreamInput # :nodoc:
124
124
 
125
125
  def read_all(dst)
126
126
  dst.replace(@rbuf)
127
- @client or return
128
- rsize = @client.class.client_body_buffer_size
127
+ rsize = __rsize or return
129
128
  until eof?
130
129
  @client.kgio_read(rsize, @buf) or eof!
131
130
  filter_body(@rbuf, @buf)
@@ -24,7 +24,7 @@ module Yahns::WbufCommon # :nodoc:
24
24
  @body.close if @body.respond_to?(:close)
25
25
  if @wbuf_persist.respond_to?(:call) # hijack
26
26
  @wbuf_persist.call(client)
27
- :delete
27
+ :ignore
28
28
  else
29
29
  @wbuf_persist # true or false or Yahns::StreamFile
30
30
  end
@@ -1,6 +1,7 @@
1
1
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
2
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
3
  $stdout.sync = $stderr.sync = Thread.abort_on_exception = true
4
+ $-w = true if RUBY_VERSION.to_f >= 2.0
4
5
  require 'thread'
5
6
 
6
7
  # Global Test Lock, to protect:
@@ -11,69 +12,68 @@ GTL = Mutex.new
11
12
  if ENV["COVERAGE"]
12
13
  require "coverage"
13
14
  COVMATCH = %r{/lib/yahns\b.*rb\z}
14
- COVTMP = File.open("coverage.dump", IO::CREAT|IO::RDWR)
15
- COVTMP.binmode
16
- COVTMP.sync = true
17
15
 
18
16
  def __covmerge
19
17
  res = Coverage.result
20
18
 
21
- # we own this file (at least until somebody tries to use NFS :x)
22
- COVTMP.flock(File::LOCK_EX)
23
-
24
- COVTMP.rewind
25
- prev = COVTMP.read
26
- prev = prev.empty? ? {} : Marshal.load(prev)
27
- res.each do |filename, counts|
28
- # filter out stuff that's not in our project
29
- COVMATCH =~ filename or next
30
-
31
- merge = prev[filename] || []
32
- merge = merge
33
- counts.each_with_index do |count, i|
34
- count or next
35
- merge[i] = (merge[i] || 0) + count
19
+ # do not create the file, Makefile does htis before any tests run
20
+ File.open("coverage.dump", IO::RDWR) do |covtmp|
21
+ covtmp.binmode
22
+ covtmp.sync = true
23
+
24
+ # we own this file (at least until somebody tries to use NFS :x)
25
+ covtmp.flock(File::LOCK_EX)
26
+
27
+ prev = covtmp.read
28
+ prev = prev.empty? ? {} : Marshal.load(prev)
29
+ res.each do |filename, counts|
30
+ # filter out stuff that's not in our project
31
+ COVMATCH =~ filename or next
32
+
33
+ merge = prev[filename] || []
34
+ merge = merge
35
+ counts.each_with_index do |count, i|
36
+ count or next
37
+ merge[i] = (merge[i] || 0) + count
38
+ end
39
+ prev[filename] = merge
36
40
  end
37
- prev[filename] = merge
41
+ covtmp.rewind
42
+ covtmp.truncate(0)
43
+ covtmp.write(Marshal.dump(prev))
44
+ covtmp.flock(File::LOCK_UN)
38
45
  end
39
- COVTMP.rewind
40
- COVTMP.truncate(0)
41
- COVTMP.write(Marshal.dump(prev))
42
- COVTMP.flock(File::LOCK_UN)
43
46
  end
44
47
 
45
48
  Coverage.start
49
+ # we need to nest at_exit to fire after minitest runs
46
50
  at_exit { at_exit { __covmerge } }
47
51
  end
48
52
 
49
53
  gem 'minitest'
50
- require 'minitest/autorun'
51
- require "tempfile"
52
-
53
- Testcase = begin
54
- Minitest::Test # minitest 5
55
- rescue NameError
56
- Minitest::Unit::TestCase # minitest 4
54
+ begin # favor minitest 5
55
+ require 'minitest'
56
+ Testcase = Minitest::Test
57
+ mtobj = Minitest
58
+ rescue NameError, LoadError # but support minitest 4
59
+ require 'minitest/unit'
60
+ Testcase = Minitest::Unit::TestCase
61
+ mtobj = MiniTest::Unit.new
57
62
  end
58
63
 
59
- FIFOS = []
60
- def tmpfifo
61
- tmp = Tempfile.new(%w(yahns-test .fifo))
62
- path = tmp.path
63
- tmp.close!
64
- assert system(*%W(mkfifo #{path})), "mkfifo #{path}"
65
-
66
- GTL.synchronize do
67
- if FIFOS.empty?
68
- at_exit do
69
- FIFOS.each { |(pid,_path)| File.unlink(_path) if $$ == pid }
70
- end
71
- end
72
- FIFOS << [ $$, path ]
64
+ # Not using minitest/autorun because that doesn't guard against redundant
65
+ # extra runs with fork. We cannot use exit! in the tests either
66
+ # (since users/apps hosted on yahns _should_ expect exit, not exit!).
67
+ TSTART_PID = $$
68
+ at_exit do
69
+ # skipping @@after_run stuff in minitest since we don't need it
70
+ case $!
71
+ when nil, SystemExit
72
+ mtobj.run(ARGV) if $$ == TSTART_PID
73
73
  end
74
- path
75
74
  end
76
75
 
76
+ require "tempfile"
77
77
  require 'tmpdir'
78
78
  class Dir
79
79
  require 'fileutils'
@@ -9,8 +9,9 @@ module ServerHelper
9
9
  def check_err(err = @err)
10
10
  err = File.open(err.path, "r") if err.respond_to?(:path)
11
11
  err.rewind
12
- lines = err.readlines.delete_if { |l| l =~ /INFO/ }
13
- assert lines.empty?, lines.join("\n")
12
+ lines = err.readlines
13
+ bad_lines = lines.dup.delete_if { |l| l =~ /INFO/ }
14
+ assert bad_lines.empty?, lines.join("\n")
14
15
  err.close! if err == @err
15
16
  end
16
17
 
@@ -47,6 +47,7 @@ class TestBin < Testcase
47
47
  ENV["YAHNS_FD"] = @srv.fileno.to_s
48
48
  else
49
49
  @srv = TCPServer.new(ENV["TEST_HOST"] || "127.0.0.1", 0)
50
+ @srv.close_on_exec = true # needed for 1.9.3
50
51
  end
51
52
  host, port = @srv.addr[3], @srv.addr[1]
52
53
  listen = ENV["YAHNS_TEST_LISTEN"] = "#{host}:#{port}"
@@ -94,4 +95,99 @@ class TestBin < Testcase
94
95
  end
95
96
  @pid.close! if @pid
96
97
  end
98
+
99
+ def test_usr2_preload_noworker; usr2(true, false); end
100
+ def test_usr2_preload_worker; usr2(true, true); end
101
+ def test_usr2_nopreload_worker; usr2(false, true); end
102
+ def test_usr2_nopreload_noworker; usr2(false, false); end
103
+
104
+ def usr2(preload, worker)
105
+ Dir.mktmpdir { |tmpdir| usr2_dir(tmpdir, preload, worker) }
106
+ end
107
+
108
+ def usr2_dir(tmpdir, preload, worker)
109
+ exe = "#{tmpdir}/yahns"
110
+
111
+ # need to fork here since tests are MT and the FD can leak out and go to
112
+ # other processes which fork (but do not exec), causing ETXTBUSY on
113
+ # Process.spawn
114
+ pid = fork do
115
+ ruby = "#!#{`which ruby`}"
116
+ File.open(exe, "w") { |y|
117
+ lines = File.readlines("bin/yahns")
118
+ lines[0] = ruby
119
+ y.chmod(0755)
120
+ y.syswrite(lines.join)
121
+ }
122
+ end
123
+ _, status = Process.waitpid2(pid)
124
+ assert status.success?, status.inspect
125
+
126
+ @pid = tmpfile(%w(test_bin_daemon .pid))
127
+ host, port = @srv.addr[3], @srv.addr[1]
128
+ @ru = tmpfile(%w(test_bin_daemon .ru))
129
+ @ru.puts("use Rack::ContentLength")
130
+ @ru.puts("use Rack::ContentType, 'text/plain'")
131
+ @ru.puts("run lambda { |_| [ 200, {}, [ Process.pid.to_s ] ] }")
132
+ cfg = tmpfile(%w(test_bin_daemon_conf .rb))
133
+ cfg.puts "pid '#{@pid.path}'"
134
+ cfg.puts "stderr_path '#{@err.path}'"
135
+ cfg.puts "worker_processes 1" if worker
136
+ cfg.puts "app(:rack, '#{@ru.path}', preload: #{preload}) do"
137
+ cfg.puts " listen '#{host}:#{port}'"
138
+ cfg.puts "end"
139
+ env = {
140
+ "YAHNS_FD" => @srv.fileno.to_s,
141
+ "PATH" => "#{tmpdir}:#{ENV['PATH']}",
142
+ "RUBYLIB" => "#{Dir.pwd}/lib",
143
+ }
144
+ cmd = %W(#{exe} -D -c #{cfg.path})
145
+ cmd << { @srv => @srv, close_others: true }
146
+ pid = GTL.synchronize { Process.spawn(env, *cmd) }
147
+ res = Net::HTTP.start(host, port) { |h| h.get("/") }
148
+ assert_equal 200, res.code.to_i
149
+ orig = res.body
150
+ Process.kill(:USR2, pid)
151
+ newpid = pid
152
+ Timeout.timeout(10) do
153
+ begin
154
+ newpid = File.read(@pid.path)
155
+ rescue Errno::ENOENT
156
+ end while newpid.to_i == pid && sleep(0.01)
157
+ end
158
+ Process.kill(:QUIT, pid)
159
+ _, status = Timeout.timeout(10) { Process.waitpid2(pid) }
160
+ assert status.success?, status
161
+ res = Net::HTTP.start(host, port) { |h| h.get("/") }
162
+ assert_equal 200, res.code.to_i
163
+ second = res.body
164
+ refute_equal orig, second
165
+
166
+ newpid = newpid.to_i
167
+ assert_operator newpid, :>, 0
168
+ Process.kill(:HUP, newpid)
169
+ third = second
170
+ Timeout.timeout(10) do
171
+ begin
172
+ third = Net::HTTP.start(host, port) { |h| h.get("/") }.body
173
+ end while third == second && sleep(0.01)
174
+ end
175
+ if worker
176
+ Process.kill(0, newpid) # nothing should raise
177
+ else
178
+ poke_until_dead newpid
179
+ end
180
+ rescue => e
181
+ Yahns::Log.exception(Logger.new($stderr), "test", e)
182
+ raise
183
+ ensure
184
+ File.unlink(exe) if exe
185
+ cfg.close! if cfg
186
+ pid = File.read(@pid.path)
187
+ pid = pid.to_i
188
+ assert_operator pid, :>, 0
189
+ Process.kill(:QUIT, pid)
190
+ poke_until_dead pid
191
+ @pid.close!
192
+ end
97
193
  end
@@ -0,0 +1,163 @@
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_relative 'server_helper'
4
+
5
+ class TestClientMaxBodySize < Testcase
6
+ parallelize_me!
7
+ include ServerHelper
8
+ alias setup server_helper_setup
9
+ alias teardown server_helper_teardown
10
+ DEFMBS = 1024 * 1024
11
+
12
+ DRAINER = lambda do |e|
13
+ input = e["rack.input"]
14
+ buf = ""
15
+ nr = 0
16
+ while rv = input.read(16384, buf)
17
+ nr += rv.size
18
+ end
19
+ body = nr.to_s
20
+ h = { "Content-Length" => body.size.to_s, "Content-Type" => 'text/plain' }
21
+ [ 200, h, [body] ]
22
+ end
23
+
24
+ def identity_req(bytes, body = true)
25
+ body_bytes = body ? bytes : 0
26
+ "PUT / HTTP/1.1\r\nConnection: close\r\nHost: example.com\r\n" \
27
+ "Content-Length: #{bytes}\r\n\r\n#{'*' * body_bytes}"
28
+ end
29
+
30
+ def mkserver(cfg)
31
+ fork do
32
+ srv = Yahns::Server.new(cfg)
33
+ ENV["YAHNS_FD"] = @srv.fileno.to_s
34
+ srv.start.join
35
+ end
36
+ end
37
+
38
+ def test_0_lazy; cmbs_test_0(:lazy); end
39
+ def test_0_true; cmbs_test_0(true); end
40
+ def test_0_false; cmbs_test_0(false); end
41
+
42
+ def cmbs_test_0(btype)
43
+ err = @err
44
+ cfg = Yahns::Config.new
45
+ host, port = @srv.addr[3], @srv.addr[1]
46
+ cfg.instance_eval do
47
+ GTL.synchronize {
48
+ app(:rack, DRAINER) {
49
+ listen "#{host}:#{port}"
50
+ input_buffering btype
51
+ client_max_body_size 0
52
+ }
53
+ }
54
+ logger(Logger.new(err.path))
55
+ end
56
+ pid = mkserver(cfg)
57
+ default_identity_checks(host, port, 0)
58
+ default_chunked_checks(host, port, 0)
59
+ ensure
60
+ quit_wait(pid)
61
+ end
62
+
63
+ def test_cmbs_lazy; cmbs_test(:lazy); end
64
+ def test_cmbs_true; cmbs_test(true); end
65
+ def test_cmbs_false; cmbs_test(false); end
66
+
67
+ def cmbs_test(btype)
68
+ err = @err
69
+ cfg = Yahns::Config.new
70
+ host, port = @srv.addr[3], @srv.addr[1]
71
+ cfg.instance_eval do
72
+ GTL.synchronize {
73
+ app(:rack, DRAINER) {
74
+ listen "#{host}:#{port}"
75
+ input_buffering btype
76
+ }
77
+ }
78
+ logger(Logger.new(err.path))
79
+ end
80
+ pid = mkserver(cfg)
81
+ default_identity_checks(host, port)
82
+ default_chunked_checks(host, port)
83
+ ensure
84
+ quit_wait(pid)
85
+ end
86
+
87
+ def test_inf_false; big_test(false); end
88
+ def test_inf_true; big_test(true); end
89
+ def test_inf_lazy; big_test(:lazy); end
90
+
91
+ def big_test(btype)
92
+ err = @err
93
+ cfg = Yahns::Config.new
94
+ host, port = @srv.addr[3], @srv.addr[1]
95
+ cfg.instance_eval do
96
+ GTL.synchronize {
97
+ app(:rack, DRAINER) {
98
+ listen "#{host}:#{port}"
99
+ input_buffering btype
100
+ client_max_body_size nil
101
+ }
102
+ }
103
+ logger(Logger.new(err.path))
104
+ end
105
+ pid = mkserver(cfg)
106
+
107
+ bytes = 10 * 1024 * 1024
108
+ r = `dd if=/dev/zero bs=#{bytes} count=1 2>/dev/null | \
109
+ curl -sSf -HExpect: -T- http://#{host}:#{port}/`
110
+ assert $?.success?, $?.inspect
111
+ assert_equal bytes.to_s, r
112
+
113
+ r = `dd if=/dev/zero bs=#{bytes} count=1 2>/dev/null | \
114
+ curl -sSf -HExpect: -HContent-Length:#{bytes} -HTransfer-Encoding: \
115
+ -T- http://#{host}:#{port}/`
116
+ assert $?.success?, $?.inspect
117
+ assert_equal bytes.to_s, r
118
+ ensure
119
+ quit_wait(pid)
120
+ end
121
+
122
+ def default_chunked_checks(host, port, defmax = DEFMBS)
123
+ r = `curl -sSf -HExpect: -T- </dev/null http://#{host}:#{port}/`
124
+ assert $?.success?, $?.inspect
125
+ assert_equal "0", r
126
+
127
+ r = `dd if=/dev/zero bs=#{defmax} count=1 2>/dev/null | \
128
+ curl -sSf -HExpect: -T- http://#{host}:#{port}/`
129
+ assert $?.success?, $?.inspect
130
+ assert_equal "#{defmax}", r
131
+
132
+ r = `dd if=/dev/zero bs=#{defmax + 1} count=1 2>/dev/null | \
133
+ curl -sf -HExpect: -T- --write-out %{http_code} \
134
+ http://#{host}:#{port}/ 2>&1`
135
+ refute $?.success?, $?.inspect
136
+ assert_equal "413", r
137
+ end
138
+
139
+ def default_identity_checks(host, port, defmax = DEFMBS)
140
+ if defmax >= 666
141
+ c = TCPSocket.new(host, port)
142
+ c.write(identity_req(666))
143
+ assert_equal "666", c.read.split(/\r\n\r\n/)[1]
144
+ c.close
145
+ end
146
+
147
+ c = TCPSocket.new(host, port)
148
+ c.write(identity_req(0))
149
+ assert_equal "0", c.read.split(/\r\n\r\n/)[1]
150
+ c.close
151
+
152
+ c = TCPSocket.new(host, port)
153
+ c.write(identity_req(defmax))
154
+ assert_equal "#{defmax}", c.read.split(/\r\n\r\n/)[1]
155
+ c.close
156
+
157
+ toobig = defmax + 1
158
+ c = TCPSocket.new(host, port)
159
+ c.write(identity_req(toobig, false))
160
+ assert_match(%r{\AHTTP/1\.[01] 413 }, Timeout.timeout(10) { c.read })
161
+ c.close
162
+ end
163
+ end