yahns 0.0.0 → 0.0.1

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