yahns 0.0.0TP1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/COPYING +674 -0
  4. data/GIT-VERSION-GEN +41 -0
  5. data/GNUmakefile +90 -0
  6. data/README +127 -0
  7. data/Rakefile +60 -0
  8. data/bin/yahns +32 -0
  9. data/examples/README +3 -0
  10. data/examples/init.sh +76 -0
  11. data/examples/logger_mp_safe.rb +28 -0
  12. data/examples/logrotate.conf +32 -0
  13. data/examples/yahns_multi.conf.rb +89 -0
  14. data/examples/yahns_rack_basic.conf.rb +27 -0
  15. data/lib/yahns.rb +73 -0
  16. data/lib/yahns/acceptor.rb +28 -0
  17. data/lib/yahns/client_expire.rb +40 -0
  18. data/lib/yahns/client_expire_portable.rb +39 -0
  19. data/lib/yahns/config.rb +344 -0
  20. data/lib/yahns/daemon.rb +51 -0
  21. data/lib/yahns/fdmap.rb +90 -0
  22. data/lib/yahns/http_client.rb +198 -0
  23. data/lib/yahns/http_context.rb +65 -0
  24. data/lib/yahns/http_response.rb +184 -0
  25. data/lib/yahns/log.rb +73 -0
  26. data/lib/yahns/queue.rb +7 -0
  27. data/lib/yahns/queue_egg.rb +23 -0
  28. data/lib/yahns/queue_epoll.rb +57 -0
  29. data/lib/yahns/rack.rb +80 -0
  30. data/lib/yahns/server.rb +336 -0
  31. data/lib/yahns/server_mp.rb +181 -0
  32. data/lib/yahns/sigevent.rb +7 -0
  33. data/lib/yahns/sigevent_efd.rb +18 -0
  34. data/lib/yahns/sigevent_pipe.rb +29 -0
  35. data/lib/yahns/socket_helper.rb +117 -0
  36. data/lib/yahns/stream_file.rb +34 -0
  37. data/lib/yahns/stream_input.rb +150 -0
  38. data/lib/yahns/tee_input.rb +114 -0
  39. data/lib/yahns/tmpio.rb +27 -0
  40. data/lib/yahns/wbuf.rb +36 -0
  41. data/lib/yahns/wbuf_common.rb +32 -0
  42. data/lib/yahns/worker.rb +58 -0
  43. data/test/covshow.rb +29 -0
  44. data/test/helper.rb +115 -0
  45. data/test/server_helper.rb +65 -0
  46. data/test/test_bin.rb +97 -0
  47. data/test/test_client_expire.rb +132 -0
  48. data/test/test_config.rb +56 -0
  49. data/test/test_fdmap.rb +19 -0
  50. data/test/test_output_buffering.rb +291 -0
  51. data/test/test_queue.rb +59 -0
  52. data/test/test_rack.rb +28 -0
  53. data/test/test_serve_static.rb +42 -0
  54. data/test/test_server.rb +415 -0
  55. data/test/test_stream_file.rb +30 -0
  56. data/test/test_wbuf.rb +136 -0
  57. data/yahns.gemspec +19 -0
  58. metadata +165 -0
@@ -0,0 +1,51 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ module Yahns::Daemon # :nodoc:
5
+ # We don't do a lot of standard daemonization stuff:
6
+ # * umask is whatever was set by the parent process at startup
7
+ # and can be set in config.ru and config_file, so making it
8
+ # 0000 and potentially exposing sensitive log data can be bad
9
+ # policy.
10
+ # * don't bother to chdir("/") here since yahns is designed to
11
+ # run inside APP_ROOT. Yahns will also re-chdir() to
12
+ # the directory it was started in when being re-executed
13
+ # to pickup code changes if the original deployment directory
14
+ # is a symlink or otherwise got replaced.
15
+ def self.daemon(yahns_server)
16
+ $stdin.reopen("/dev/null")
17
+
18
+ # We only start a new process group if we're not being reexecuted
19
+ # and inheriting file descriptors from our parent
20
+ unless ENV['YAHNS_FD']
21
+ # grandparent - reads pipe, exits when master is ready
22
+ # \_ parent - exits immediately ASAP
23
+ # \_ yahns master - writes to pipe when ready
24
+
25
+ # We cannot use Yahns::Sigevent (eventfd) here because we need
26
+ # to detect EOF on unexpected death, not just read/write
27
+ rd, wr = IO.pipe
28
+ grandparent = $$
29
+ if fork
30
+ wr.close # grandparent does not write
31
+ else
32
+ rd.close # yahns master does not read
33
+ Process.setsid
34
+ exit if fork # parent dies now
35
+ end
36
+
37
+ if grandparent == $$
38
+ # this will block until Server#join runs (or it dies)
39
+ master_pid = (rd.readpartial(16) rescue nil).to_i
40
+ unless master_pid > 1
41
+ warn "master failed to start, check stderr log for details"
42
+ exit!(1)
43
+ end
44
+ exit 0
45
+ else # yahns master process
46
+ yahns_server.daemon_pipe = wr
47
+ end
48
+ end
49
+ # $stderr/$stderr can/will be redirected separately in the Yahns config
50
+ end
51
+ end
@@ -0,0 +1,90 @@
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 'thread'
4
+
5
+ # only initialize this after forking, this is highly volatile and won't
6
+ # be able to share data across processes at all.
7
+ # This is really a singleton
8
+
9
+ class Yahns::Fdmap # :nodoc:
10
+ def initialize(logger, client_expire_threshold)
11
+ @logger = logger
12
+
13
+ if Float === client_expire_threshold
14
+ client_expire_threshold *= Process.getrlimit(:NOFILE)[0]
15
+ elsif client_expire_threshold < 0
16
+ client_expire_threshold = Process.getrlimit(:NOFILE)[0] +
17
+ client_expire_threshold
18
+ end
19
+ @client_expire_threshold = client_expire_threshold.to_i
20
+
21
+ # This is an array because any sane OS will frequently reuse FDs
22
+ # to keep this tightly-packed and favor lower FD numbers
23
+ # (consider select(2) performance (not that we use select))
24
+ # An (unpacked) Hash (in MRI) uses 5 more words per entry than an Array,
25
+ # and we should expect this array to have around 60K elements
26
+ @fdmap_ary = []
27
+ @fdmap_mtx = Mutex.new
28
+ @last_expire = 0.0
29
+ @count = 0
30
+ end
31
+
32
+ # called immediately after accept()
33
+ def add(io)
34
+ fd = io.fileno
35
+ @fdmap_mtx.synchronize do
36
+ if (@count += 1) > @client_expire_threshold
37
+ __expire_for(io)
38
+ else
39
+ @fdmap_ary[fd] = io
40
+ end
41
+ end
42
+ end
43
+
44
+ # this is only called in Errno::EMFILE/Errno::ENFILE situations
45
+ def desperate_expire_for(io, timeout)
46
+ @fdmap_mtx.synchronize { __expire_for(io, timeout) }
47
+ end
48
+
49
+ # called before IO#close
50
+ def decr
51
+ # don't bother clearing the element in @fdmap_ary, it'll just be
52
+ # overwritten when another client connects (soon). We must not touch
53
+ # @fdmap_ary[io.fileno] after IO#close on io
54
+ @fdmap_mtx.synchronize { @count -= 1 }
55
+ end
56
+
57
+ def delete(io) # use with rack.hijack (via yahns)
58
+ fd = io.fileno
59
+ @fdmap_mtx.synchronize do
60
+ @fdmap_ary[fd] = nil
61
+ @count -= 1
62
+ end
63
+ end
64
+
65
+ # expire a bunch of idle clients and register the current one
66
+ # We should not be calling this too frequently, it is expensive
67
+ # This is called while @fdmap_mtx is held
68
+ def __expire_for(io, timeout = nil)
69
+ nr = 0
70
+ now = Time.now.to_f
71
+ (now - @last_expire) >= 1.0 or return # don't expire too frequently
72
+
73
+ # @fdmap_ary may be huge, so always expire a bunch at once to
74
+ # avoid getting to this method too frequently
75
+ @fdmap_ary.each do |c|
76
+ c.respond_to?(:yahns_expire) or next
77
+ nr += c.yahns_expire(timeout || c.class.client_timeout)
78
+ end
79
+
80
+ @fdmap_ary[io.fileno] = io
81
+ @last_expire = Time.now.to_f
82
+ msg = timeout ? "timeout=#{timeout})" : "client_timeout"
83
+ @logger.info("dropping #{nr} of #@count clients for #{msg}")
84
+ end
85
+
86
+ # used for graceful shutdown
87
+ def size
88
+ @fdmap_mtx.synchronize { @count }
89
+ end
90
+ end
@@ -0,0 +1,198 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ class Yahns::HttpClient < Kgio::Socket # :nodoc:
5
+ NULL_IO = StringIO.new("")
6
+
7
+ # FIXME: we shouldn't have this at all
8
+ Unicorn::HttpParser.keepalive_requests = 0xffffffff
9
+
10
+ include Yahns::HttpResponse
11
+ include Yahns::ClientExpire
12
+ QEV_FLAGS = Yahns::Queue::QEV_RD # used by acceptor
13
+ HTTP_RESPONSE_START = [ 'HTTP', '/1.1 ' ]
14
+
15
+ # A frozen format for this is about 15% faster (note from Mongrel)
16
+ REMOTE_ADDR = 'REMOTE_ADDR'.freeze
17
+ RACK_INPUT = 'rack.input'.freeze
18
+ RACK_HIJACK = 'rack.hijack'.freeze
19
+ RACK_HIJACK_IO = "rack.hijack_io".freeze
20
+
21
+ # called from acceptor thread
22
+ def yahns_init
23
+ @hs = Unicorn::HttpRequest.new
24
+ @response_start_sent = false
25
+ @state = :headers # :body, :trailers, :pipelined, Wbuf, StreamFile
26
+ @input = nil
27
+ end
28
+
29
+ # use if writes are deferred by buffering, this return value goes to
30
+ # the main epoll/kqueue worker loop
31
+ # returns :wait_readable, :wait_writable, or nil
32
+ def step_write
33
+ case rv = @state.wbuf_flush(self)
34
+ when :wait_writable, :wait_readable
35
+ return rv # tell epoll/kqueue to wait on this more
36
+ when :delete # :delete on hijack
37
+ @state = :delete
38
+ return :delete
39
+ when Yahns::StreamFile
40
+ @state = rv # continue looping
41
+ when true, false # done
42
+ return http_response_done(rv)
43
+ else
44
+ raise "BUG: #{@state.inspect}#wbuf_flush returned #{rv.inspect}"
45
+ end while true
46
+ end
47
+
48
+ def mkinput_preread
49
+ @state = :body
50
+ @input = self.class.tmpio_for(@hs.content_length)
51
+ rbuf = Thread.current[:yahns_rbuf]
52
+ @hs.filter_body(rbuf, @hs.buf)
53
+ @input.write(rbuf)
54
+ end
55
+
56
+ def input_ready
57
+ empty_body = 0 == @hs.content_length
58
+ k = self.class
59
+ case k.input_buffering
60
+ when true
61
+ # common case is an empty body
62
+ return NULL_IO if empty_body
63
+
64
+ # content_length is nil (chunked) or len > 0
65
+ mkinput_preread # keep looping
66
+ false
67
+ else # :lazy, false
68
+ empty_body ? NULL_IO : (@input = k.mkinput(self, @hs))
69
+ end
70
+ end
71
+
72
+ # the main entry point of the epoll/kqueue worker loop
73
+ def yahns_step
74
+ # always write unwritten data first if we have any
75
+ return step_write if Yahns::WbufCommon === @state
76
+
77
+ # only read if we had nothing to write in this event loop iteration
78
+ k = self.class
79
+ rbuf = Thread.current[:yahns_rbuf] # running under spawn_worker_threads
80
+
81
+ case @state
82
+ when :pipelined
83
+ if @hs.parse
84
+ input = input_ready and return app_call(input)
85
+ # @state == :body if we get here point (input_ready -> mkinput_preread)
86
+ else
87
+ @state = :headers
88
+ end
89
+ # continue to outer loop
90
+ when :headers
91
+ case rv = kgio_tryread(k.client_header_buffer_size, rbuf)
92
+ when String
93
+ if @hs.add_parse(rv)
94
+ input = input_ready and return app_call(input)
95
+ break # to outer loop to reevaluate @state == :body
96
+ end
97
+ # keep looping on kgio_tryread
98
+ when :wait_readable, :wait_writable, nil
99
+ return rv
100
+ end while true
101
+ when :body
102
+ if @hs.body_eof?
103
+ if @hs.content_length || @hs.parse # hp.parse == trailers done!
104
+ @input.rewind
105
+ return app_call(@input)
106
+ else # possible Transfer-Encoding:chunked, keep looping
107
+ @state = :trailers
108
+ end
109
+ else
110
+ case rv = kgio_tryread(k.client_body_buffer_size, rbuf)
111
+ when String
112
+ @hs.filter_body(rbuf, @hs.buf << rbuf)
113
+ @input.write(rbuf)
114
+ # keep looping on kgio_tryread...
115
+ when :wait_readable, :wait_writable
116
+ return rv # have epoll/kqueue wait for more
117
+ when nil # unexpected EOF
118
+ return @input.close # nil
119
+ end # continue to outer loop (case @state)
120
+ end
121
+ when :trailers
122
+ case rv = kgio_tryread(k.client_header_buffer_size, rbuf)
123
+ when String
124
+ if @hs.add_parse(rbuf)
125
+ @input.rewind
126
+ return app_call(@input)
127
+ end
128
+ # keep looping on kgio_tryread...
129
+ when :wait_readable, :wait_writable
130
+ return rv # wait for more
131
+ when nil # unexpected EOF
132
+ return @input.close # nil
133
+ end while true
134
+ end while true # outer loop
135
+ rescue => e
136
+ handle_error(e)
137
+ end
138
+
139
+ def app_call(input)
140
+ env = @hs.env
141
+ env[REMOTE_ADDR] = @kgio_addr
142
+ env[RACK_HIJACK] = hijack_proc(env)
143
+ env[RACK_INPUT] = input
144
+ k = self.class
145
+
146
+ if k.check_client_connection && @hs.headers?
147
+ @response_start_sent = true
148
+ # FIXME: we should buffer this just in case
149
+ HTTP_RESPONSE_START.each { |c| kgio_write(c) }
150
+ end
151
+
152
+ # run the rack app
153
+ response = k.app.call(env.merge!(k.app_defaults))
154
+ return :delete if env.include?(RACK_HIJACK_IO)
155
+
156
+ # this returns :wait_readable, :wait_writable, :delete, or nil:
157
+ http_response_write(*response)
158
+ end
159
+
160
+ def hijack_proc(env)
161
+ proc { env[RACK_HIJACK_IO] = self }
162
+ end
163
+
164
+ # called automatically by kgio_write
165
+ def kgio_wait_writable(timeout = self.class.client_timeout)
166
+ super timeout
167
+ end
168
+
169
+ # called automatically by kgio_read
170
+ def kgio_wait_readable(timeout = self.class.client_timeout)
171
+ super timeout
172
+ end
173
+
174
+ # if we get any error, try to write something back to the client
175
+ # assuming we haven't closed the socket, but don't get hung up
176
+ # if the socket is already closed or broken. We'll always return
177
+ # nil to ensure the socket is closed at the end of this function
178
+ def handle_error(e)
179
+ code = case e
180
+ when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::ENOTCONN
181
+ return # don't send response, drop the connection
182
+ when Unicorn::RequestURITooLongError
183
+ 414
184
+ when Unicorn::RequestEntityTooLargeError
185
+ 413
186
+ when Unicorn::HttpParserError # try to tell the client they're bad
187
+ 400
188
+ else
189
+ Yahns::Log.exception(@hs.env["rack.logger"], "app error", e)
190
+ 500
191
+ end
192
+ kgio_trywrite(err_response(code))
193
+ rescue
194
+ ensure
195
+ shutdown rescue nil
196
+ return # always drop the connection on uncaught errors
197
+ end
198
+ end
@@ -0,0 +1,65 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+
5
+ # subclasses of Yahns::HttpClient will class extend this
6
+
7
+ module Yahns::HttpContext # :nodoc:
8
+ attr_accessor :check_client_connection
9
+ attr_accessor :client_body_buffer_size
10
+ attr_accessor :client_header_buffer_size
11
+ attr_accessor :client_max_body_size
12
+ attr_accessor :client_max_header_size
13
+ attr_accessor :input_buffering # :lazy, true, false
14
+ attr_accessor :output_buffering # true, false
15
+ attr_accessor :persistent_connections # true or false only
16
+ attr_accessor :client_timeout
17
+ attr_accessor :qegg
18
+ attr_reader :app
19
+ attr_reader :app_defaults
20
+
21
+ def http_ctx_init(yahns_rack)
22
+ @yahns_rack = yahns_rack
23
+ @app_defaults = yahns_rack.app_defaults
24
+ @check_client_connection = false
25
+ @client_body_buffer_size = 112 * 1024
26
+ @client_header_buffer_size = 4000
27
+ @client_max_body_size = 1024 * 1024
28
+ @input_buffering = true
29
+ @output_buffering = true
30
+ @persistent_connections = true
31
+ @client_timeout = 15
32
+ @qegg = nil
33
+ end
34
+
35
+ # call this after forking
36
+ def after_fork_init
37
+ @app = @yahns_rack.app_after_fork
38
+ end
39
+
40
+ # call this immediately after successful accept()/accept4()
41
+ def logger=(l) # cold
42
+ @logger = @app_defaults["rack.logger"] = l
43
+ end
44
+
45
+ def logger
46
+ @app_defaults["rack.logger"]
47
+ end
48
+
49
+ def mkinput(client, hs)
50
+ (@input_buffering ? Yahns::TeeInput : Yahns::StreamInput).new(client, hs)
51
+ end
52
+
53
+ def errors=(dest)
54
+ @app_defaults["rack.errors"] = dest
55
+ end
56
+
57
+ def errors
58
+ @app_defaults["rack.errors"]
59
+ end
60
+
61
+ def tmpio_for(len)
62
+ len && len <= @client_body_buffer_size ?
63
+ StringIO.new("") : Yahns::TmpIO.new
64
+ end
65
+ end
@@ -0,0 +1,184 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require_relative 'stream_file'
5
+
6
+ # Writes a Rack response to your client using the HTTP/1.1 specification.
7
+ # You use it by simply doing:
8
+ #
9
+ # status, headers, body = rack_app.call(env)
10
+ # http_response_write(status, headers, body)
11
+ #
12
+ # Most header correctness (including Content-Length and Content-Type)
13
+ # is the job of Rack, with the exception of the "Date" header.
14
+ module Yahns::HttpResponse # :nodoc:
15
+ include Unicorn::HttpResponse
16
+
17
+ # avoid GC overhead for frequently used-strings:
18
+ CONN_KA = "Connection: keep-alive\r\n\r\n"
19
+ CONN_CLOSE = "Connection: close\r\n\r\n"
20
+ Z = ""
21
+ RESPONSE_START = "HTTP/1.1 "
22
+
23
+ def response_start
24
+ @response_start_sent ? Z : RESPONSE_START
25
+ end
26
+
27
+ def response_wait_write(rv)
28
+ # call the kgio_wait_readable or kgio_wait_writable method
29
+ ok = __send__("kgio_#{rv}") and return ok
30
+ k = self.class
31
+ k.logger.info("fd=#{fileno} ip=#@kgio_addr timeout on :#{rv} after "\
32
+ "#{k.client_timeout}s")
33
+ nil
34
+ end
35
+
36
+ def err_response(code)
37
+ "#{response_start}#{CODES[code]}\r\n\r\n"
38
+ end
39
+
40
+ def response_header_blocked(ret, header, body, alive, offset, count)
41
+ if body.respond_to?(:to_path)
42
+ alive = Yahns::StreamFile.new(body, alive, offset, count)
43
+ body = nil
44
+ end
45
+ wbuf = Yahns::Wbuf.new(body, alive)
46
+ rv = wbuf.wbuf_write(self, header)
47
+ body.each { |chunk| rv = wbuf.wbuf_write(self, chunk) } if body
48
+ wbuf_maybe(wbuf, rv, alive)
49
+ end
50
+
51
+ def wbuf_maybe(wbuf, rv, alive)
52
+ case rv # trysendfile return value
53
+ when nil
54
+ case alive
55
+ when :delete
56
+ @state = :delete
57
+ when true, false
58
+ http_response_done(alive)
59
+ end
60
+ else
61
+ @state = wbuf
62
+ rv
63
+ end
64
+ end
65
+
66
+ def http_response_done(alive)
67
+ @input = @input.close if @input
68
+ if alive
69
+ @response_start_sent = false
70
+ # @hs.buf will have data if the client pipelined
71
+ if @hs.buf.empty?
72
+ @state = :headers
73
+ :wait_readable
74
+ else
75
+ @state = :pipelined
76
+ # may need to wait for readability if SSL,
77
+ # only need writability if plain TCP
78
+ :wait_readwrite
79
+ end
80
+ else
81
+ # shutdown is needed in case the app forked, we rescue here since
82
+ # StreamInput may issue shutdown as well
83
+ shutdown rescue nil
84
+ nil # trigger close
85
+ end
86
+ end
87
+
88
+ # writes the rack_response to socket as an HTTP response
89
+ # returns :wait_readable, :wait_writable, :forget, or nil
90
+ def http_response_write(status, headers, body)
91
+ status = CODES[status.to_i] || status
92
+ offset = 0
93
+ count = hijack = nil
94
+ k = self.class
95
+ alive = @hs.next? && k.persistent_connections
96
+
97
+ if @hs.headers?
98
+ buf = "#{response_start}#{status}\r\nDate: #{httpdate}\r\n"
99
+ headers.each do |key, value|
100
+ case key
101
+ when %r{\ADate\z}
102
+ next
103
+ when %r{\AContent-Range\z}i
104
+ if %r{\Abytes (\d+)-(\d+)/\d+\z} =~ value
105
+ offset = $1.to_i
106
+ count = $2.to_i - offset + 1
107
+ end
108
+ when %r{\AConnection\z}i
109
+ # allow Rack apps to tell us they want to drop the client
110
+ alive = !!(value =~ /\bclose\b/i)
111
+ when "rack.hijack"
112
+ hijack = value
113
+ body = nil # ensure we do not close body
114
+ else
115
+ if value =~ /\n/
116
+ # avoiding blank, key-only cookies with /\n+/
117
+ buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
118
+ else
119
+ buf << "#{key}: #{value}\r\n"
120
+ end
121
+ end
122
+ end
123
+ buf << (alive ? CONN_KA : CONN_CLOSE)
124
+ case rv = kgio_trywrite(buf)
125
+ when nil # all done, likely
126
+ break
127
+ when String
128
+ buf = rv # hope the skb grows
129
+ when :wait_writable, :wait_readable
130
+ if k.output_buffering
131
+ alive = hijack ? hijack : alive
132
+ rv = response_header_blocked(rv, buf, body, alive, offset, count)
133
+ body = nil # ensure we do not close body in ensure
134
+ return rv
135
+ else
136
+ response_wait_write(rv) or return
137
+ end
138
+ end while true
139
+ end
140
+
141
+ if hijack
142
+ hijack.call(self)
143
+ return :delete # trigger EPOLL_CTL_DEL
144
+ end
145
+
146
+ if body.respond_to?(:to_path)
147
+ @state = body = Yahns::StreamFile.new(body, alive, offset, count)
148
+ return step_write
149
+ end
150
+
151
+ wbuf = rv = nil
152
+ body.each do |chunk|
153
+ if wbuf
154
+ rv = wbuf.wbuf_write(self, chunk)
155
+ else
156
+ case rv = kgio_trywrite(chunk)
157
+ when nil # all done, likely and good!
158
+ break
159
+ when String
160
+ chunk = rv # hope the skb grows when we loop into the trywrite
161
+ when :wait_writable, :wait_readable
162
+ if k.output_buffering
163
+ wbuf = Yahns::Wbuf.new(body, alive)
164
+ rv = wbuf.wbuf_write(self, chunk)
165
+ break
166
+ else
167
+ response_wait_write(rv) or return
168
+ end
169
+ end while true
170
+ end
171
+ end
172
+
173
+ # if we buffered the write body, we must return :wait_writable
174
+ # (or :wait_readable for SSL) and hit Yahns::HttpClient#step_write
175
+ if wbuf
176
+ body = nil # ensure we do not close the body in ensure
177
+ wbuf_maybe(wbuf, rv, alive)
178
+ else
179
+ http_response_done(alive)
180
+ end
181
+ ensure
182
+ body.respond_to?(:close) and body.close
183
+ end
184
+ end