yahns 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 32f85a4c4ed36a082242c5b58bae7f18528543c1
4
- data.tar.gz: e741b37a8b0f62d47d9ea08ce36ab69b31536f15
3
+ metadata.gz: fff238b384ecd740912bd989b3f9f5940f1a8ed1
4
+ data.tar.gz: 4f0a23620f3de110285e6f0da568dbb8448713ec
5
5
  SHA512:
6
- metadata.gz: 8eb0d76f793c6bc787ec789214e05f2f03face6e7772cad38ca7da67986e6a579f9b69c927ed39224b75f72e2dafaa3f886a4dc9c17eea24ccd0d3ae59aaf994
7
- data.tar.gz: d9b233d340ef2ca35c5f5c085cac97bb6090d3ba2dbf2c89b57edcc1577d6ea5ef8b67f4821f6cbe04e36938b9d49ebd6d8898838f370baeb89da1845297bbe5
6
+ metadata.gz: 019a14bdaf0613f75c89b1b170f705373698645a115f06f260e008080f3cb1b365091a2cfface0695bc841b090fd1d528d35b0ea54f0fa4b74424c6d28fc4cd0
7
+ data.tar.gz: ff28c0cee74dc2602ca0eb74dca63e9b98e84c28179f043db8875f29537b15a3d6df9b93439fbdc12cdfe6deb7006575248d7e54cce56351baefde378ba62494
data/.gitignore CHANGED
@@ -2,6 +2,7 @@
2
2
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
3
  /GIT-VERSION-FILE
4
4
  /NEWS
5
+ /NEWS.atom.xml
5
6
  /pkg
6
7
  /lib/yahns/version.rb
7
8
  /coverage.dump
@@ -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 all clients
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
@@ -4,7 +4,7 @@
4
4
  CONSTANT = "Yahns::VERSION"
5
5
  RVF = "lib/yahns/version.rb"
6
6
  GVF = "GIT-VERSION-FILE"
7
- DEF_VER = "v0.0.2"
7
+ DEF_VER = "v0.0.3"
8
8
  vn = DEF_VER
9
9
 
10
10
  # First see if there is a version file (included in release tarballs),
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
- task rsync_docs: "NEWS" do
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 NEWS README COPYING)
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
- `git ls-files Documentation/*.txt`.split(/\n/).concat(top).each do |txt|
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
- files << txt
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("\n")
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
@@ -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
- if defined?(@my_pid) && @my_pid
27
- begin
28
- Process.waitpid(@my_pid)
29
- rescue Errno::ECHILD
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].my_pid = Process.spawn(cgi_env, *@args,
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
@@ -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.desperate_expire_for(nil, 5)
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)
@@ -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
- unless ENV['YAHNS_FD']
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
@@ -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 __expire_for. Theoretically, a Ruby
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
- # __expire_for
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
- __expire_for(io)
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 desperate_expire_for(io, timeout)
61
- @fdmap_mtx.synchronize { __expire_for(io, timeout) }
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 __expire_for(io, timeout = nil)
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}")
@@ -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
@@ -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, alive)
53
+ wbuf_maybe(wbuf, rv)
54
54
  end
55
55
 
56
- def wbuf_maybe(wbuf, rv, alive)
57
- case rv # trysendfile return value
58
- when nil
59
- case alive
60
- when :ignore
61
- @state = alive
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 = alive
63
+ @state = rv
64
64
  :wait_writable
65
65
  when true, false
66
- http_response_done(alive)
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
- # may need to wait for readability if SSL,
85
- # only need writability if plain TCP
86
- :wait_readwrite
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, alive)
187
+ wbuf_maybe(wbuf, rv)
188
188
  else
189
189
  http_response_done(alive)
190
190
  end
@@ -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
- @fdmap.forget(io)
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
@@ -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=#{@shutdown_timeout} s")
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
- @wthr.delete_if { |t| t.join(0.01) } while @wthr[0]
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.error("BUG: still active wthr=#{@wthr.size} "\
419
- "listeners=#{@listeners.size}")
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
- fdmap.desperate_expire_for(nil, @shutdown_timeout)
465
+ timeout = @shutdown_expire < Time.now ? -1 : @shutdown_timeout
466
+ fdmap.desperate_expire(timeout)
456
467
  true
457
468
  else
458
469
  false
@@ -24,7 +24,7 @@ class Yahns::StreamFile # :nodoc:
24
24
  rescue Errno::EMFILE, Errno::ENFILE
25
25
  raise if retried
26
26
  retried = true
27
- Thread.current[:yahns_fdmap].desperate_expire_for(nil, 5)
27
+ Thread.current[:yahns_fdmap].desperate_expire(5)
28
28
  sleep(1)
29
29
  retry
30
30
  end
@@ -20,7 +20,7 @@ class Yahns::TmpIO < File # :nodoc:
20
20
  rescue Errno::EMFILE, Errno::ENFILE
21
21
  raise if retried
22
22
  retried = true
23
- Thread.current[:yahns_fdmap].desperate_expire_for(nil, 5)
23
+ Thread.current[:yahns_fdmap].desperate_expire(5)
24
24
  sleep(1)
25
25
  retry
26
26
  end
@@ -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
 
@@ -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 # :check_client_connection, :http_100_response
25
+ @next = next_state # :ccc_done, :r100_done
12
26
  end
13
27
 
14
28
  def wbuf_flush(client)
@@ -16,6 +16,7 @@ module ServerHelper
16
16
  end
17
17
 
18
18
  def poke_until_dead(pid)
19
+ assert_operator pid, :>, 0
19
20
  Timeout.timeout(10) do
20
21
  begin
21
22
  Process.kill(0, pid)
@@ -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
- runme = "#{Dir.pwd}/test/test_extras_exec_cgi.sh"
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(runme)) do
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
@@ -53,7 +53,8 @@ class TestExtrasTryGzipStatic < Testcase
53
53
  c = get_tcp_client(host, port)
54
54
  begin
55
55
  c.write "#{req}\r\n\r\n"
56
- head, body = c.read.split(/\r\n\r\n/)
56
+ buf = c.read(666000)
57
+ head, body = buf.split(/\r\n\r\n/)
57
58
  blk.call(head)
58
59
  body
59
60
  ensure
@@ -702,7 +702,6 @@ class TestServer < Testcase
702
702
  Process.kill(:QUIT, pid)
703
703
  _, status = Timeout.timeout(5) { Process.waitpid2(pid) }
704
704
  assert status.success?, status.inspect
705
- assert_nil c.wait(1)
706
705
  assert_nil c.read(666)
707
706
  end
708
707
 
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.2
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-06 00:00:00.000000000 Z
11
+ date: 2013-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kgio