yahns 0.0.2 → 0.0.3
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 +4 -4
- data/.gitignore +1 -0
- data/Documentation/yahns_config.txt +20 -2
- data/GIT-VERSION-GEN +1 -1
- data/Rakefile +114 -38
- data/extras/exec_cgi.rb +22 -8
- data/lib/yahns/acceptor.rb +1 -1
- data/lib/yahns/daemon.rb +5 -1
- data/lib/yahns/fdmap.rb +12 -9
- data/lib/yahns/http_client.rb +2 -1
- data/lib/yahns/http_response.rb +13 -13
- data/lib/yahns/queue_epoll.rb +17 -4
- data/lib/yahns/server.rb +17 -6
- data/lib/yahns/stream_file.rb +1 -1
- data/lib/yahns/tmpio.rb +1 -1
- data/lib/yahns/wbuf.rb +24 -0
- data/lib/yahns/wbuf_str.rb +15 -1
- data/test/server_helper.rb +1 -0
- data/test/test_extras_exec_cgi.rb +104 -5
- data/test/test_extras_exec_cgi.sh +17 -0
- data/test/test_extras_try_gzip_static.rb +2 -1
- data/test/test_server.rb +0 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fff238b384ecd740912bd989b3f9f5940f1a8ed1
|
4
|
+
data.tar.gz: 4f0a23620f3de110285e6f0da568dbb8448713ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 019a14bdaf0613f75c89b1b170f705373698645a115f06f260e008080f3cb1b365091a2cfface0695bc841b090fd1d528d35b0ea54f0fa4b74424c6d28fc4cd0
|
7
|
+
data.tar.gz: ff28c0cee74dc2602ca0eb74dca63e9b98e84c28179f043db8875f29537b15a3d6df9b93439fbdc12cdfe6deb7006575248d7e54cce56351baefde378ba62494
|
data/.gitignore
CHANGED
@@ -278,6 +278,11 @@ Ruby it is running under.
|
|
278
278
|
|
279
279
|
HTTP request headers are always buffered in memory.
|
280
280
|
|
281
|
+
Do not be tempted to disable any buffering because it improves
|
282
|
+
numbers on a synthetic benchmark over a fast connection.
|
283
|
+
Slow, real-world clients can easily overwhelm servers without both
|
284
|
+
input and output buffering.
|
285
|
+
|
281
286
|
Default: true
|
282
287
|
|
283
288
|
The following OPTIONS may be specified:
|
@@ -418,12 +423,19 @@ Ruby it is running under.
|
|
418
423
|
|
419
424
|
This enables or disables buffering of the HTTP response. If enabled,
|
420
425
|
buffering is only performed lazily. In other words, buffering only
|
421
|
-
happens if socket buffers (in the kernel) are filled up
|
426
|
+
happens if socket buffers (in the kernel) are filled up, and yahns
|
427
|
+
will continously try to flush the buffered data to the socket while
|
428
|
+
it is buffering.
|
422
429
|
|
423
|
-
Disabling output buffering is only recommended if
|
430
|
+
Disabling output buffering is only recommended if ALL clients
|
424
431
|
connecting to this application context are fast, trusted or
|
425
432
|
you are willing and able to run many worker threads.
|
426
433
|
|
434
|
+
Do not be tempted to disable any buffering because it improves
|
435
|
+
numbers on a synthetic benchmark over a fast connection.
|
436
|
+
Slow, real-world clients can easily overwhelm servers without both
|
437
|
+
input and output buffering.
|
438
|
+
|
427
439
|
If output buffering is disabled, client_timeout controls the maximum
|
428
440
|
amount of time a worker thread will wait synchronously.
|
429
441
|
|
@@ -495,6 +507,12 @@ Ruby it is running under.
|
|
495
507
|
pthread_atfork(3)-style hooks. See WORKER_PROCESSES-LEVEL DIRECTIVES
|
496
508
|
for details.
|
497
509
|
|
510
|
+
Using worker_processes is strongly recommended if your application
|
511
|
+
relies on using a SIGCHLD handler for reaping forked processes.
|
512
|
+
Without worker_processes, yahns must reserve SIGCHLD for rolling
|
513
|
+
back SIGUSR2 upgrades, leading to conflicts if the appplication
|
514
|
+
expects to handle SIGCHLD.
|
515
|
+
|
498
516
|
Default: nil (single process, no master/worker separation)
|
499
517
|
|
500
518
|
## WORKER_PROCESSES-LEVEL DIRECTIVES
|
data/GIT-VERSION-GEN
CHANGED
data/Rakefile
CHANGED
@@ -2,59 +2,135 @@
|
|
2
2
|
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
3
|
require 'tempfile'
|
4
4
|
include Rake::DSL
|
5
|
-
task "NEWS" do
|
6
|
-
latest = nil
|
7
|
-
fp = Tempfile.new("NEWS", ".")
|
8
|
-
fp.sync = true
|
9
|
-
`git tag -l`.split(/\n/).reverse.each do |tag|
|
10
|
-
%r{\Av(.+)} =~ tag or next
|
11
|
-
version = $1
|
12
|
-
header, subject, body = `git cat-file tag #{tag}`.split(/\n\n/, 3)
|
13
|
-
header = header.split(/\n/)
|
14
|
-
tagger = header.grep(/\Atagger /)[0]
|
15
|
-
time = Time.at(tagger.split(/ /)[-2].to_i).utc
|
16
|
-
latest ||= time
|
17
|
-
date = time.strftime("%Y-%m-%d")
|
18
|
-
fp.puts "# #{version} / #{date}\n\n#{subject}"
|
19
|
-
if body && body.strip.size > 0
|
20
|
-
fp.puts "\n\n#{body}"
|
21
|
-
end
|
22
|
-
fp.puts
|
23
|
-
end
|
24
|
-
fp.write("Unreleased\n\n") unless fp.size > 0
|
25
|
-
fp.puts "# COPYRIGHT"
|
26
|
-
bdfl = 'Eric Wong <normalperson@yhbt.net>'
|
27
|
-
fp.puts "Copyright (C) 2013, #{bdfl} and all contributors"
|
28
|
-
fp.puts "License: GPLv3 or later (http://www.gnu.org/licenses/gpl-3.0.txt)"
|
29
|
-
fp.rewind
|
30
|
-
assert_equal fp.read, File.read("NEWS") rescue nil
|
31
|
-
fp.chmod 0644
|
32
|
-
File.rename(fp.path, "NEWS")
|
33
|
-
fp.close!
|
34
|
-
end
|
35
5
|
|
36
|
-
|
6
|
+
gendocs = %w(NEWS NEWS.atom.xml)
|
7
|
+
task rsync_docs: gendocs do
|
37
8
|
dest = ENV["RSYNC_DEST"] || "yahns.yhbt.net:/srv/yahns/"
|
38
|
-
top = %w(INSTALL HACKING
|
39
|
-
files = []
|
9
|
+
top = %w(INSTALL HACKING README COPYING)
|
40
10
|
|
41
11
|
# git-set-file-times is distributed with rsync,
|
42
12
|
# Also available at: http://yhbt.net/git-set-file-times
|
43
13
|
# on Debian systems: /usr/share/doc/rsync/scripts/git-set-file-times.gz
|
44
14
|
sh("git", "set-file-times", "Documentation", "examples", *top)
|
45
15
|
|
46
|
-
|
16
|
+
do_gzip = lambda do |txt|
|
47
17
|
gz = "#{txt}.gz"
|
48
18
|
tmp = "#{gz}.#$$"
|
49
|
-
sh("gzip -9 < #{txt} > #{tmp}")
|
19
|
+
sh("gzip --rsyncable -9 < #{txt} > #{tmp}")
|
50
20
|
st = File.stat(txt)
|
51
21
|
File.utime(st.atime, st.mtime, tmp) # make nginx gzip_static happy
|
52
22
|
File.rename(tmp, gz)
|
53
|
-
|
54
|
-
files << gz
|
23
|
+
gz
|
55
24
|
end
|
25
|
+
|
26
|
+
files = `git ls-files Documentation/*.txt`.split(/\n/)
|
27
|
+
files.concat(top)
|
28
|
+
files.concat(gendocs)
|
29
|
+
gzfiles = files.map { |txt| do_gzip.call(txt) }
|
30
|
+
files.concat(gzfiles)
|
31
|
+
|
56
32
|
sh("rsync --chmod=Fugo=r -av #{files.join(' ')} #{dest}")
|
57
33
|
|
58
|
-
examples = `git ls-files examples`.split(
|
34
|
+
examples = `git ls-files examples`.split(/\n/)
|
35
|
+
gzex = examples.map { |txt| do_gzip.call(txt) }
|
36
|
+
examples.concat(gzex)
|
37
|
+
|
59
38
|
sh("rsync --chmod=Fugo=r -av #{examples.join(' ')} #{dest}/examples/")
|
60
39
|
end
|
40
|
+
|
41
|
+
def tags
|
42
|
+
timefmt = '%Y-%m-%dT%H:%M:%SZ'
|
43
|
+
@tags ||= `git tag -l`.split(/\n/).map do |tag|
|
44
|
+
if %r{\Av[\d\.]+} =~ tag
|
45
|
+
header, subject, body = `git cat-file tag #{tag}`.split(/\n\n/, 3)
|
46
|
+
header = header.split(/\n/)
|
47
|
+
tagger = header.grep(/\Atagger /).first
|
48
|
+
body ||= "initial"
|
49
|
+
time = Time.at(tagger.split(/ /)[-2].to_i).utc
|
50
|
+
{
|
51
|
+
time_obj: time,
|
52
|
+
time: time.strftime(timefmt),
|
53
|
+
tagger_name: %r{^tagger ([^<]+)}.match(tagger)[1].strip,
|
54
|
+
tagger_email: %r{<([^>]+)>}.match(tagger)[1].strip,
|
55
|
+
id: `git rev-parse refs/tags/#{tag}`.chomp!,
|
56
|
+
tag: tag,
|
57
|
+
subject: subject,
|
58
|
+
body: body,
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end.compact.sort { |a,b| b[:time] <=> a[:time] }
|
62
|
+
end
|
63
|
+
|
64
|
+
url_base = 'http://yahns.yhbt.net'
|
65
|
+
cgit_url = 'http://yhbt.net/yahns.git'
|
66
|
+
git_url = 'git://yhbt.net/yahns.git'
|
67
|
+
since = "v0.0.2"
|
68
|
+
|
69
|
+
desc 'prints news as an Atom feed'
|
70
|
+
task "NEWS.atom.xml" do
|
71
|
+
require 'nokogiri'
|
72
|
+
new_tags = tags[0,10]
|
73
|
+
time = nil
|
74
|
+
str = Nokogiri::XML::Builder.new do
|
75
|
+
feed :xmlns => "http://www.w3.org/2005/Atom" do
|
76
|
+
id! "#{url_base}/NEWS.atom.xml"
|
77
|
+
title "yahns news"
|
78
|
+
subtitle "sleepy, multi-threaded, non-blocking Ruby application server"
|
79
|
+
link! :rel => 'alternate', :type => 'text/plain',
|
80
|
+
:href => "#{url_base}/NEWS"
|
81
|
+
updated(new_tags.empty? ? "1970-01-01T00:00:00Z" : new_tags.first[:time])
|
82
|
+
new_tags.each do |tag|
|
83
|
+
time ||= tag[:time_obj]
|
84
|
+
entry do
|
85
|
+
title tag[:subject]
|
86
|
+
updated tag[:time]
|
87
|
+
published tag[:time]
|
88
|
+
author {
|
89
|
+
name tag[:tagger_name]
|
90
|
+
email tag[:tagger_email]
|
91
|
+
}
|
92
|
+
url = "#{cgit_url}/tag/?id=#{tag[:tag]}"
|
93
|
+
link! :rel => "alternate", :type => "text/html", :href =>url
|
94
|
+
id! url
|
95
|
+
message_only = tag[:body].split(/\n.+\(\d+\):\n {6}/).first.strip
|
96
|
+
content({:type =>:text}, message_only)
|
97
|
+
content(:type =>:xhtml) { pre tag[:body] }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end.to_xml
|
102
|
+
|
103
|
+
fp = Tempfile.new("NEWS.atom.xml", ".")
|
104
|
+
fp.sync = true
|
105
|
+
fp.write(str)
|
106
|
+
fp.chmod 0644
|
107
|
+
File.utime(time, time, fp.path) if time
|
108
|
+
File.rename(fp.path, "NEWS.atom.xml")
|
109
|
+
fp.close!
|
110
|
+
end
|
111
|
+
|
112
|
+
desc 'prints news as a text file'
|
113
|
+
task "NEWS" do
|
114
|
+
fp = Tempfile.new("NEWS", ".")
|
115
|
+
fp.sync = true
|
116
|
+
time = nil
|
117
|
+
tags.each do |tag|
|
118
|
+
time ||= tag[:time_obj]
|
119
|
+
line = tag[:subject] + " / " + tag[:time].gsub(/T.*/, '')
|
120
|
+
fp.puts line
|
121
|
+
fp.puts("-" * line.length)
|
122
|
+
fp.puts
|
123
|
+
fp.puts tag[:body] #.gsub(/^/m, " ").gsub(/[ \t]+$/m, "")
|
124
|
+
fp.puts
|
125
|
+
end
|
126
|
+
fp.write("Unreleased\n\n") unless fp.size > 0
|
127
|
+
fp.puts "COPYRIGHT"
|
128
|
+
fp.puts "---------"
|
129
|
+
bdfl = 'Eric Wong <normalperson@yhbt.net>'
|
130
|
+
fp.puts "Copyright (C) 2013, #{bdfl} and all contributors"
|
131
|
+
fp.puts "License: GPLv3 or later (http://www.gnu.org/licenses/gpl-3.0.txt)"
|
132
|
+
fp.chmod 0644
|
133
|
+
File.utime(time, time, fp.path) if time
|
134
|
+
File.rename(fp.path, "NEWS")
|
135
|
+
fp.close!
|
136
|
+
end
|
data/extras/exec_cgi.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
2
|
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
|
3
3
|
# License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt)
|
4
|
+
#
|
5
|
+
# if running under yahns, worker_processes is recommended to avoid conflicting
|
6
|
+
# with the SIGCHLD handler in yahns.
|
4
7
|
class ExecCgi
|
5
8
|
class MyIO < Kgio::Pipe
|
6
9
|
attr_writer :my_pid
|
@@ -19,17 +22,24 @@ class ExecCgi
|
|
19
22
|
end
|
20
23
|
yield("0\r\n\r\n") if @chunked
|
21
24
|
self
|
25
|
+
ensure
|
26
|
+
# do this sooner, since the response body may be buffered, we want
|
27
|
+
# to release our FD as soon as possible.
|
28
|
+
close
|
22
29
|
end
|
23
30
|
|
24
31
|
def close
|
32
|
+
# yahns will call this again after its done writing the response
|
33
|
+
# body, so we must ensure its idempotent.
|
34
|
+
# Note: this object (and any client-specific objects) will never
|
35
|
+
# be shared across different threads, so we do not need extra
|
36
|
+
# mutual exclusion here.
|
37
|
+
return if closed?
|
25
38
|
super
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
end
|
32
|
-
nil
|
39
|
+
begin
|
40
|
+
Process.waitpid(@my_pid)
|
41
|
+
rescue Errno::ECHILD
|
42
|
+
end if defined?(@my_pid) && @my_pid
|
33
43
|
end
|
34
44
|
end
|
35
45
|
|
@@ -67,7 +77,8 @@ class ExecCgi
|
|
67
77
|
PASS_VARS.each { |key| val = env[key] and cgi_env[key] = val }
|
68
78
|
env.each { |key,val| cgi_env[key] = val if key =~ /\AHTTP_/ }
|
69
79
|
pipe = MyIO.pipe
|
70
|
-
pipe[0]
|
80
|
+
errbody = pipe[0]
|
81
|
+
errbody.my_pid = Process.spawn(cgi_env, *@args,
|
71
82
|
out: pipe[1], close_others: true)
|
72
83
|
pipe[1].close
|
73
84
|
pipe = pipe[0]
|
@@ -100,9 +111,12 @@ class ExecCgi
|
|
100
111
|
pipe.chunked = true
|
101
112
|
end
|
102
113
|
end
|
114
|
+
errbody = nil
|
103
115
|
[ status, headers, pipe ]
|
104
116
|
else
|
105
117
|
[ 500, { "Content-Length" => "0", "Content-Type" => "text/plain" }, [] ]
|
106
118
|
end
|
119
|
+
ensure
|
120
|
+
errbody.close if errbody
|
107
121
|
end
|
108
122
|
end
|
data/lib/yahns/acceptor.rb
CHANGED
@@ -56,7 +56,7 @@ module Yahns::Acceptor # :nodoc:
|
|
56
56
|
end
|
57
57
|
rescue Errno::EMFILE, Errno::ENFILE => e
|
58
58
|
logger.error("#{e.message}, consider raising open file limits")
|
59
|
-
queue.fdmap.
|
59
|
+
queue.fdmap.desperate_expire(5)
|
60
60
|
sleep 1 # let other threads do some work
|
61
61
|
rescue => e
|
62
62
|
Yahns::Log.exception(logger, "accept loop", e)
|
data/lib/yahns/daemon.rb
CHANGED
@@ -19,7 +19,11 @@ module Yahns::Daemon # :nodoc:
|
|
19
19
|
|
20
20
|
# We only start a new process group if we're not being reexecuted
|
21
21
|
# and inheriting file descriptors from our parent
|
22
|
-
|
22
|
+
if ENV['YAHNS_FD']
|
23
|
+
# if we're inheriting, need to ensure this remains true so
|
24
|
+
# SIGWINCH works when worker processes are in play
|
25
|
+
yahns_server.daemon_pipe = true
|
26
|
+
else
|
23
27
|
# grandparent - reads pipe, exits when master is ready
|
24
28
|
# \_ parent - exits immediately ASAP
|
25
29
|
# \_ yahns master - writes to pipe when ready
|
data/lib/yahns/fdmap.rb
CHANGED
@@ -31,11 +31,16 @@ class Yahns::Fdmap # :nodoc:
|
|
31
31
|
|
32
32
|
# Yes, we call IO#close inside the lock(!)
|
33
33
|
#
|
34
|
-
# We don't want to race with
|
34
|
+
# We don't want to race with __expire. Theoretically, a Ruby
|
35
35
|
# implementation w/o GVL may end up issuing shutdown(2) on the same fd
|
36
36
|
# as one which got accept-ed (a brand new IO object) so we must prevent
|
37
37
|
# IO#close in worker threads from racing with any threads which may run
|
38
|
-
#
|
38
|
+
# __expire
|
39
|
+
#
|
40
|
+
# We must never, ever call this while it is capable of being on the
|
41
|
+
# epoll ready list and returnable by epoll_wait. So we can only call
|
42
|
+
# this on objects which were epoll_ctl-ed with EPOLLONESHOT (and now
|
43
|
+
# retrieved).
|
39
44
|
def sync_close(io)
|
40
45
|
@fdmap_mtx.synchronize do
|
41
46
|
@count -= 1
|
@@ -48,17 +53,16 @@ class Yahns::Fdmap # :nodoc:
|
|
48
53
|
fd = io.fileno
|
49
54
|
@fdmap_mtx.synchronize do
|
50
55
|
if (@count += 1) > @client_expire_threshold
|
51
|
-
|
52
|
-
else
|
53
|
-
@fdmap_ary[fd] = io
|
56
|
+
__expire(nil)
|
54
57
|
end
|
58
|
+
@fdmap_ary[fd] = io
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
58
62
|
# this is only called in Errno::EMFILE/Errno::ENFILE situations
|
59
63
|
# and graceful shutdown
|
60
|
-
def
|
61
|
-
@fdmap_mtx.synchronize {
|
64
|
+
def desperate_expire(timeout)
|
65
|
+
@fdmap_mtx.synchronize { __expire(timeout) }
|
62
66
|
end
|
63
67
|
|
64
68
|
# only called on hijack
|
@@ -74,7 +78,7 @@ class Yahns::Fdmap # :nodoc:
|
|
74
78
|
# expire a bunch of idle clients and register the current one
|
75
79
|
# We should not be calling this too frequently, it is expensive
|
76
80
|
# This is called while @fdmap_mtx is held
|
77
|
-
def
|
81
|
+
def __expire(timeout)
|
78
82
|
nr = 0
|
79
83
|
now = Time.now.to_f
|
80
84
|
(now - @last_expire) >= 1.0 or return # don't expire too frequently
|
@@ -88,7 +92,6 @@ class Yahns::Fdmap # :nodoc:
|
|
88
92
|
nr += c.yahns_expire(tout)
|
89
93
|
end
|
90
94
|
|
91
|
-
@fdmap_ary[io.fileno] = io if io
|
92
95
|
@last_expire = Time.now.to_f
|
93
96
|
msg = timeout ? "timeout=#{timeout}" : "client_timeout"
|
94
97
|
@logger.info("dropping #{nr} of #@count clients for #{msg}")
|
data/lib/yahns/http_client.rb
CHANGED
@@ -269,7 +269,8 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
|
|
269
269
|
# nil to ensure the socket is closed at the end of this function
|
270
270
|
def handle_error(e)
|
271
271
|
code = case e
|
272
|
-
when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::ENOTCONN
|
272
|
+
when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::ENOTCONN,
|
273
|
+
Errno::ETIMEDOUT
|
273
274
|
return # don't send response, drop the connection
|
274
275
|
when Yahns::ClientTimeout
|
275
276
|
408
|
data/lib/yahns/http_response.rb
CHANGED
@@ -50,20 +50,20 @@ module Yahns::HttpResponse # :nodoc:
|
|
50
50
|
wbuf = Yahns::Wbuf.new(body, alive, self.class.output_buffer_tmpdir)
|
51
51
|
rv = wbuf.wbuf_write(self, header)
|
52
52
|
body.each { |chunk| rv = wbuf.wbuf_write(self, chunk) } if body
|
53
|
-
wbuf_maybe(wbuf, rv
|
53
|
+
wbuf_maybe(wbuf, rv)
|
54
54
|
end
|
55
55
|
|
56
|
-
def wbuf_maybe(wbuf, rv
|
57
|
-
case rv #
|
58
|
-
when nil
|
59
|
-
case
|
60
|
-
when :ignore
|
61
|
-
@state =
|
56
|
+
def wbuf_maybe(wbuf, rv)
|
57
|
+
case rv # wbuf_write return value
|
58
|
+
when nil # all done
|
59
|
+
case rv = wbuf.wbuf_close(self)
|
60
|
+
when :ignore # hijacked
|
61
|
+
@state = rv
|
62
62
|
when Yahns::StreamFile
|
63
|
-
@state =
|
63
|
+
@state = rv
|
64
64
|
:wait_writable
|
65
65
|
when true, false
|
66
|
-
http_response_done(
|
66
|
+
http_response_done(rv)
|
67
67
|
end
|
68
68
|
else
|
69
69
|
@state = wbuf
|
@@ -81,9 +81,9 @@ module Yahns::HttpResponse # :nodoc:
|
|
81
81
|
:wait_readable
|
82
82
|
else
|
83
83
|
@state = :pipelined
|
84
|
-
#
|
85
|
-
#
|
86
|
-
:
|
84
|
+
# we shouldn't start processing the application again until we know
|
85
|
+
# the socket is writable for the response
|
86
|
+
:wait_writable
|
87
87
|
end
|
88
88
|
else
|
89
89
|
# shutdown is needed in case the app forked, we rescue here since
|
@@ -184,7 +184,7 @@ module Yahns::HttpResponse # :nodoc:
|
|
184
184
|
# (or :wait_readable for SSL) and hit Yahns::HttpClient#step_write
|
185
185
|
if wbuf
|
186
186
|
body = nil # ensure we do not close the body in ensure
|
187
|
-
wbuf_maybe(wbuf, rv
|
187
|
+
wbuf_maybe(wbuf, rv)
|
188
188
|
else
|
189
189
|
http_response_done(alive)
|
190
190
|
end
|
data/lib/yahns/queue_epoll.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
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
|
+
#
|
5
|
+
# This is the dangerous, low-level epoll interface for sleepy_penguin
|
6
|
+
# It is safe as long as you're aware of all potential concurrency
|
7
|
+
# issues given multithreading, GC, and epoll itself.
|
4
8
|
class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
|
5
9
|
include SleepyPenguin
|
6
10
|
attr_accessor :fdmap # Yahns::Fdmap
|
@@ -9,7 +13,6 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
|
|
9
13
|
QEV_QUIT = Epoll::OUT # Level Trigger for QueueQuitter
|
10
14
|
QEV_RD = Epoll::IN | Epoll::ONESHOT
|
11
15
|
QEV_WR = Epoll::OUT | Epoll::ONESHOT
|
12
|
-
QEV_RDWR = QEV_RD | QEV_WR
|
13
16
|
|
14
17
|
def self.new
|
15
18
|
super(SleepyPenguin::Epoll::CLOEXEC)
|
@@ -18,6 +21,8 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
|
|
18
21
|
# for HTTP and HTTPS servers, we rely on the io writing to us, first
|
19
22
|
# flags: QEV_RD/QEV_WR (usually QEV_RD)
|
20
23
|
def queue_add(io, flags)
|
24
|
+
# order is very important here, this thread cannot do anything with
|
25
|
+
# io once we've issued epoll_ctl() because another thread may use it
|
21
26
|
@fdmap.add(io)
|
22
27
|
epoll_ctl(Epoll::CTL_ADD, io, flags)
|
23
28
|
end
|
@@ -28,9 +33,14 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
|
|
28
33
|
end
|
29
34
|
|
30
35
|
# use only before hijacking, once hijacked, io may be unusable to us
|
36
|
+
# It is not safe to call this unless it is an unarmed EPOLLONESHOT
|
37
|
+
# object.
|
31
38
|
def queue_del(io)
|
32
|
-
|
39
|
+
# order does not really matter here, however Epoll::CTL_DEL
|
40
|
+
# will free up ~200 bytes of unswappable kernel memory,
|
41
|
+
# so we call it first
|
33
42
|
epoll_ctl(Epoll::CTL_DEL, io, 0)
|
43
|
+
@fdmap.forget(io)
|
34
44
|
end
|
35
45
|
|
36
46
|
# returns an array of infinitely running threads
|
@@ -39,13 +49,15 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
|
|
39
49
|
thr_init
|
40
50
|
begin
|
41
51
|
epoll_wait(max_events) do |_, io| # don't care for flags for now
|
52
|
+
|
53
|
+
# Note: we absolutely must not do anything with io after
|
54
|
+
# we've called epoll_ctl on it, io is exclusive to this
|
55
|
+
# thread only until epoll_ctl is called on it.
|
42
56
|
case rv = io.yahns_step
|
43
57
|
when :wait_readable
|
44
58
|
epoll_ctl(Epoll::CTL_MOD, io, QEV_RD)
|
45
59
|
when :wait_writable
|
46
60
|
epoll_ctl(Epoll::CTL_MOD, io, QEV_WR)
|
47
|
-
when :wait_readwrite
|
48
|
-
epoll_ctl(Epoll::CTL_MOD, io, QEV_RDWR)
|
49
61
|
when :ignore # only used by rack.hijack
|
50
62
|
# we cannot call Epoll::CTL_DEL after hijacking, the hijacker
|
51
63
|
# may have already closed it Likewise, io.fileno is not
|
@@ -59,6 +71,7 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
|
|
59
71
|
end
|
60
72
|
end
|
61
73
|
rescue => e
|
74
|
+
break if closed? # can still happen due to shutdown_timeout
|
62
75
|
Yahns::Log.exception(logger, 'queue loop', e)
|
63
76
|
end while true
|
64
77
|
end
|
data/lib/yahns/server.rb
CHANGED
@@ -20,6 +20,7 @@ class Yahns::Server # :nodoc:
|
|
20
20
|
include Yahns::SocketHelper
|
21
21
|
|
22
22
|
def initialize(config)
|
23
|
+
@shutdown_expire = nil
|
23
24
|
@shutdown_timeout = nil
|
24
25
|
@reexec_pid = 0
|
25
26
|
@daemon_pipe = nil # writable IO or true
|
@@ -201,7 +202,7 @@ class Yahns::Server # :nodoc:
|
|
201
202
|
end
|
202
203
|
|
203
204
|
def daemon_ready
|
204
|
-
@daemon_pipe or return
|
205
|
+
@daemon_pipe.respond_to?(:syswrite) or return
|
205
206
|
@daemon_pipe.syswrite("#$$")
|
206
207
|
@daemon_pipe.close
|
207
208
|
@daemon_pipe = true # for SIGWINCH
|
@@ -379,7 +380,8 @@ class Yahns::Server # :nodoc:
|
|
379
380
|
|
380
381
|
def quit_enter(alive)
|
381
382
|
if alive
|
382
|
-
@logger.info("gracefully exiting shutdown_timeout
|
383
|
+
@logger.info("gracefully exiting shutdown_timeout=#@shutdown_timeout")
|
384
|
+
@shutdown_expire ||= Time.now + @shutdown_timeout + 1
|
383
385
|
else # drop connections immediately if signaled twice
|
384
386
|
@logger.info("graceful exit aborted, exiting immediately")
|
385
387
|
# we will still call any app-defined at_exit hooks here
|
@@ -406,17 +408,25 @@ class Yahns::Server # :nodoc:
|
|
406
408
|
@queues.each { |q| q.queue_add(quitter, Yahns::Queue::QEV_QUIT) }
|
407
409
|
|
408
410
|
# watch the monkey wrench destroy all the threads!
|
409
|
-
|
411
|
+
# Ugh, this may fail if we have dedicated threads trickling
|
412
|
+
# response bodies out (e.g. "tail -F") Oh well, have a timeout
|
413
|
+
begin
|
414
|
+
@wthr.delete_if { |t| t.join(0.01) }
|
415
|
+
end while @wthr[0] && Time.now <= @shutdown_expire
|
410
416
|
|
411
417
|
# cleanup, our job is done
|
412
418
|
@queues.each(&:close).clear
|
419
|
+
|
420
|
+
# we must not let quitter get GC-ed if we have any worker threads leftover
|
421
|
+
@wthr.each { |t| t[:yahns_quitter] = quitter }
|
422
|
+
|
413
423
|
quitter.close
|
414
424
|
rescue => e
|
415
425
|
Yahns::Log.exception(@logger, "quit finish", e)
|
416
426
|
ensure
|
417
427
|
if (@wthr.size + @listeners.size) > 0
|
418
|
-
@logger.
|
419
|
-
|
428
|
+
@logger.warn("still active wthr=#{@wthr.size} "\
|
429
|
+
"listeners=#{@listeners.size}")
|
420
430
|
end
|
421
431
|
end
|
422
432
|
|
@@ -452,7 +462,8 @@ class Yahns::Server # :nodoc:
|
|
452
462
|
|
453
463
|
def dropping(fdmap)
|
454
464
|
if drop_acceptors[0] || fdmap.size > 0
|
455
|
-
|
465
|
+
timeout = @shutdown_expire < Time.now ? -1 : @shutdown_timeout
|
466
|
+
fdmap.desperate_expire(timeout)
|
456
467
|
true
|
457
468
|
else
|
458
469
|
false
|
data/lib/yahns/stream_file.rb
CHANGED
data/lib/yahns/tmpio.rb
CHANGED
data/lib/yahns/wbuf.rb
CHANGED
@@ -3,6 +3,30 @@
|
|
3
3
|
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
4
|
require_relative 'wbuf_common'
|
5
5
|
|
6
|
+
# This class is triggered whenever we need write buffering for clients
|
7
|
+
# reading responses slowly. Small responses which fit into kernel socket
|
8
|
+
# buffers do not trigger this. yahns will always attempt to write to kernel
|
9
|
+
# socket buffers first to avoid unnecessary copies in userspace.
|
10
|
+
#
|
11
|
+
# Thus, most data is into copied to the kernel only once, the kernel
|
12
|
+
# will perform zero-copy I/O from the page cache to the socket. The
|
13
|
+
# only time data may be copied twice is the initial write()/send()
|
14
|
+
# which triggers EAGAIN.
|
15
|
+
#
|
16
|
+
# We only buffer to the filesystem (note: likely not a disk, since this
|
17
|
+
# is short-lived). We let the sysadmin/kernel decide whether or not
|
18
|
+
# the data needs to hit disk or not.
|
19
|
+
#
|
20
|
+
# This avoids large allocations from malloc, potentially limiting
|
21
|
+
# fragmentation and keeping (common) smaller allocations fast.
|
22
|
+
# General purpose malloc implementations in the 64-bit era tend to avoid
|
23
|
+
# releasing memory back to the kernel, so large heap allocations are best
|
24
|
+
# avoided as the kernel has little chance to reclaim memory used for a
|
25
|
+
# temporary buffer.
|
26
|
+
#
|
27
|
+
# The biggest downside of this approach is it requires an FD, but yahns
|
28
|
+
# configurations are configured for many FDs anyways, so it's unlikely
|
29
|
+
# to be a scalability issue.
|
6
30
|
class Yahns::Wbuf # :nodoc:
|
7
31
|
include Yahns::WbufCommon
|
8
32
|
|
data/lib/yahns/wbuf_str.rb
CHANGED
@@ -3,12 +3,26 @@
|
|
3
3
|
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
4
|
require_relative 'wbuf_common'
|
5
5
|
|
6
|
+
# we only use this for buffering the tiniest responses (which are already
|
7
|
+
# strings in memory and a handful of bytes).
|
8
|
+
#
|
9
|
+
# "HTTP", "/1.1 "
|
10
|
+
# "HTTP/1.1 100 Continue\r\n\r\n"
|
11
|
+
# "100 Continue\r\n\r\nHTTP/1.1 "
|
12
|
+
#
|
13
|
+
# This is very, very rarely triggered.
|
14
|
+
# 1) check_client_connection is enabled
|
15
|
+
# 2) the client sent an "Expect: 100-continue" header
|
16
|
+
#
|
17
|
+
# Most output buffering goes through
|
18
|
+
# the normal Yahns::Wbuf class which uses a temporary file as a buffer
|
19
|
+
# (suitable for sendfile())
|
6
20
|
class Yahns::WbufStr # :nodoc:
|
7
21
|
include Yahns::WbufCommon
|
8
22
|
|
9
23
|
def initialize(str, next_state)
|
10
24
|
@str = str
|
11
|
-
@next = next_state # :
|
25
|
+
@next = next_state # :ccc_done, :r100_done
|
12
26
|
end
|
13
27
|
|
14
28
|
def wbuf_flush(client)
|
data/test/server_helper.rb
CHANGED
@@ -2,23 +2,24 @@
|
|
2
2
|
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
3
|
require_relative 'server_helper'
|
4
4
|
|
5
|
+
# note: we use worker_processes to avoid polling/excessive wakeup issues
|
6
|
+
# in the test. We recommend using worker_processes if using ExecCgi
|
5
7
|
class TestExtrasExecCGI < Testcase
|
6
8
|
ENV["N"].to_i > 1 and parallelize_me!
|
7
9
|
include ServerHelper
|
8
10
|
alias setup server_helper_setup
|
9
11
|
alias teardown server_helper_teardown
|
12
|
+
RUNME = "#{Dir.pwd}/test/test_extras_exec_cgi.sh"
|
10
13
|
|
11
14
|
def test_exec_cgi
|
12
15
|
err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
|
13
|
-
|
14
|
-
assert File.executable?(runme), "run test in project root"
|
16
|
+
assert File.executable?(RUNME), "run test in project root"
|
15
17
|
pid = mkserver(cfg) do
|
16
18
|
require './extras/exec_cgi'
|
17
19
|
cfg.instance_eval do
|
18
|
-
app(:rack, ExecCgi.new(
|
19
|
-
listen "#{host}:#{port}"
|
20
|
-
end
|
20
|
+
app(:rack, ExecCgi.new(RUNME)) { listen "#{host}:#{port}" }
|
21
21
|
stderr_path err.path
|
22
|
+
worker_processes 1
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
@@ -75,7 +76,105 @@ class TestExtrasExecCGI < Testcase
|
|
75
76
|
assert_nil body
|
76
77
|
c.close
|
77
78
|
end
|
79
|
+
|
80
|
+
Timeout.timeout(30) do # pid of executable
|
81
|
+
c = get_tcp_client(host, port)
|
82
|
+
c.write "GET /pid HTTP/1.0\r\n\r\n"
|
83
|
+
head, body = c.read.split(/\r\n\r\n/, 2)
|
84
|
+
assert_match %r{200 OK}, head
|
85
|
+
assert_match %r{\A\d+\n\z}, body
|
86
|
+
exec_pid = body.to_i
|
87
|
+
c.close
|
88
|
+
poke_until_dead exec_pid
|
89
|
+
end
|
90
|
+
ensure
|
91
|
+
quit_wait(pid)
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_cgi_died
|
95
|
+
err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
|
96
|
+
pid = mkserver(cfg) do
|
97
|
+
require './extras/exec_cgi'
|
98
|
+
cfg.instance_eval do
|
99
|
+
app(:rack, ExecCgi.new(RUNME)) { listen "#{host}:#{port}" }
|
100
|
+
stderr_path err.path
|
101
|
+
worker_processes 1
|
102
|
+
end
|
103
|
+
end
|
104
|
+
exec_pid_tmp = tmpfile(%w(exec_cgi .pid))
|
105
|
+
c = get_tcp_client(host, port)
|
106
|
+
Timeout.timeout(20) do
|
107
|
+
c.write "GET /die HTTP/1.0\r\nX-PID-DEST: #{exec_pid_tmp.path}\r\n\r\n"
|
108
|
+
head, body = c.read.split(/\r\n\r\n/, 2)
|
109
|
+
assert_match %r{500 Internal Server Error}, head
|
110
|
+
assert_match "", body
|
111
|
+
exec_pid = exec_pid_tmp.read
|
112
|
+
assert_match %r{\A(\d+)\n\z}, exec_pid
|
113
|
+
poke_until_dead exec_pid.to_i
|
114
|
+
end
|
115
|
+
ensure
|
116
|
+
exec_pid_tmp.close! if exec_pid_tmp
|
117
|
+
quit_wait(pid)
|
118
|
+
end
|
119
|
+
|
120
|
+
[ 9, 10, 11 ].each do |rtype|
|
121
|
+
[ 1, 2, 3 ].each do |block_on|
|
122
|
+
define_method("test_block_on_block_on_#{block_on}_rtype_#{rtype}") do
|
123
|
+
_blocked_zombie([block_on], rtype)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def _blocked_zombie(block_on, rtype)
|
129
|
+
err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
|
130
|
+
pid = mkserver(cfg) do
|
131
|
+
$_tw_blocked = 0
|
132
|
+
$_tw_block_on = block_on
|
133
|
+
Yahns::HttpClient.__send__(:include, TrywriteBlocked)
|
134
|
+
require './extras/exec_cgi'
|
135
|
+
cfg.instance_eval do
|
136
|
+
app(:rack, ExecCgi.new(RUNME)) { listen "#{host}:#{port}" }
|
137
|
+
stderr_path err.path
|
138
|
+
worker_processes 1
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
c = get_tcp_client(host, port)
|
143
|
+
Timeout.timeout(20) do
|
144
|
+
case rtype
|
145
|
+
when 9 # non-persistent (HTTP/0.9)
|
146
|
+
c.write "GET /pid\r\n\r\n"
|
147
|
+
body = c.read
|
148
|
+
assert_match %r{\A\d+\n\z}, body
|
149
|
+
exec_pid = body.to_i
|
150
|
+
poke_until_dead exec_pid
|
151
|
+
when 10 # non-persistent (HTTP/1.0)
|
152
|
+
c.write "GET /pid HTTP/1.0\r\n\r\n"
|
153
|
+
head, body = c.read.split(/\r\n\r\n/, 2)
|
154
|
+
assert_match %r{200 OK}, head
|
155
|
+
assert_match %r{\A\d+\n\z}, body
|
156
|
+
exec_pid = body.to_i
|
157
|
+
poke_until_dead exec_pid
|
158
|
+
when 11 # pid of executable, persistent
|
159
|
+
c.write "GET /pid HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
|
160
|
+
buf = ""
|
161
|
+
begin
|
162
|
+
buf << c.readpartial(666)
|
163
|
+
end until buf =~ /\r\n\r\n\d+\n/
|
164
|
+
head, body = buf.split(/\r\n\r\n/, 2)
|
165
|
+
assert_match %r{200 OK}, head
|
166
|
+
assert_match %r{\A\d+\n\z}, body
|
167
|
+
exec_pid = body.to_i
|
168
|
+
assert_raises(Errno::EAGAIN, IO::WaitReadable) { c.read_nonblock(666) }
|
169
|
+
poke_until_dead exec_pid
|
170
|
+
# still alive?
|
171
|
+
assert_raises(Errno::EAGAIN, IO::WaitReadable) { c.read_nonblock(666) }
|
172
|
+
else
|
173
|
+
raise "BUG in test, bad rtype"
|
174
|
+
end
|
175
|
+
end
|
78
176
|
ensure
|
177
|
+
c.close if c
|
79
178
|
quit_wait(pid)
|
80
179
|
end
|
81
180
|
end
|
@@ -20,6 +20,23 @@ case $PATH_INFO in
|
|
20
20
|
stdhead
|
21
21
|
env
|
22
22
|
;;
|
23
|
+
/pid)
|
24
|
+
stdhead
|
25
|
+
echo $$
|
26
|
+
;;
|
27
|
+
/die)
|
28
|
+
if test -n "$HTTP_X_PID_DEST"
|
29
|
+
then
|
30
|
+
# obviously this is only for testing on a local machine:
|
31
|
+
echo $$ > "$HTTP_X_PID_DEST"
|
32
|
+
exit 1
|
33
|
+
else
|
34
|
+
echo Content-Type: text/plain
|
35
|
+
echo Status: 400 Bad Request
|
36
|
+
echo Content-Length: 0
|
37
|
+
echo
|
38
|
+
fi
|
39
|
+
;;
|
23
40
|
/known-length)
|
24
41
|
echo Content-Type: text/plain
|
25
42
|
echo Status: 200 OK
|
data/test/test_server.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: yahns
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- yahns hackers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-11-
|
11
|
+
date: 2013-11-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: kgio
|