yahns 0.0.0TP1

Sign up to get free protection for your applications and to get access to all the features.
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