rainbows 4.4.3 → 4.5.0

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 (64) hide show
  1. data/.document +1 -0
  2. data/.gitignore +1 -0
  3. data/Documentation/rainbows.1.txt +8 -4
  4. data/GIT-VERSION-GEN +34 -35
  5. data/GNUmakefile +4 -2
  6. data/HACKING +72 -0
  7. data/bin/rainbows +5 -0
  8. data/lib/rainbows/const.rb +3 -3
  9. data/lib/rainbows/coolio/client.rb +18 -6
  10. data/lib/rainbows/coolio/thread_client.rb +2 -0
  11. data/lib/rainbows/epoll/client.rb +35 -12
  12. data/lib/rainbows/ev_core.rb +5 -4
  13. data/lib/rainbows/event_machine/client.rb +9 -4
  14. data/lib/rainbows/process_client.rb +7 -3
  15. data/lib/rainbows/response.rb +54 -18
  16. data/lib/rainbows/revactor/client/methods.rb +1 -1
  17. data/lib/rainbows/stream_response_epoll.rb +34 -15
  18. data/lib/rainbows/stream_response_epoll/client.rb +11 -3
  19. data/lib/rainbows/writer_thread_pool/client.rb +2 -0
  20. data/lib/rainbows/xepoll/client.rb +0 -3
  21. data/lib/rainbows/xepoll_thread_pool/client.rb +1 -1
  22. data/lib/rainbows/xepoll_thread_spawn/client.rb +1 -1
  23. data/rainbows.gemspec +6 -3
  24. data/t/GNUmakefile +10 -3
  25. data/t/byte-range-common.sh +1 -1
  26. data/t/hijack.ru +56 -0
  27. data/t/t0000-simple-http.sh +9 -9
  28. data/t/t0001-unix-http.sh +8 -8
  29. data/t/t0003-reopen-logs.sh +8 -8
  30. data/t/t0004-heartbeat-timeout.sh +3 -3
  31. data/t/t0005-large-file-response.sh +1 -1
  32. data/t/t0010-keepalive-timeout-effective.sh +2 -2
  33. data/t/t0011-close-on-exec-set.sh +2 -2
  34. data/t/t0017-keepalive-timeout-zero.sh +2 -2
  35. data/t/t0024-pipelined-sendfile-response.sh +2 -2
  36. data/t/t0027-nil-copy_stream.sh +1 -1
  37. data/t/t0030-fast-pipe-response.sh +1 -1
  38. data/t/t0034-pipelined-pipe-response.sh +2 -2
  39. data/t/t0035-kgio-pipe-response.sh +1 -1
  40. data/t/t0040-keepalive_requests-setting.sh +4 -4
  41. data/t/t0043-quit-keepalive-disconnect.sh +3 -3
  42. data/t/t0044-autopush.sh +2 -2
  43. data/t/t0045-client_max_header_size.sh +2 -2
  44. data/t/t0100-rack-input-hammer-chunked.sh +4 -4
  45. data/t/t0100-rack-input-hammer-content-length.sh +4 -4
  46. data/t/t0106-rack-input-keepalive.sh +6 -6
  47. data/t/t0200-async-response.sh +5 -5
  48. data/t/t0202-async-response-one-oh.sh +5 -5
  49. data/t/t0300-async_sinatra.sh +5 -5
  50. data/t/t0400-em-async-app.sh +2 -2
  51. data/t/t0401-em-async-tailer.sh +2 -2
  52. data/t/t0402-async-keepalive.sh +23 -23
  53. data/t/t0500-cramp-streaming.sh +3 -3
  54. data/t/t0600-rack-fiber_pool.sh +1 -1
  55. data/t/t0700-app-deferred.sh +2 -2
  56. data/t/t0800-rack-hijack.sh +27 -0
  57. data/t/t9000-rack-app-pool.sh +3 -3
  58. data/t/t9001-sendfile-to-path.sh +1 -1
  59. data/t/t9100-thread-timeout.sh +1 -1
  60. data/t/t9101-thread-timeout-threshold.sh +1 -1
  61. data/t/test-lib.sh +15 -0
  62. data/t/test_isolate.rb +4 -3
  63. metadata +26 -6
  64. data/t/bin/utee +0 -12
@@ -10,6 +10,7 @@ class Rainbows::EventMachine::Client < EM::Connection
10
10
  end
11
11
 
12
12
  alias write send_data
13
+ alias hijacked detach
13
14
 
14
15
  def receive_data(data)
15
16
  # To avoid clobbering the current streaming response
@@ -37,9 +38,11 @@ class Rainbows::EventMachine::Client < EM::Connection
37
38
  @env[REMOTE_ADDR] = @_io.kgio_addr
38
39
  @env[ASYNC_CALLBACK] = method(:write_async_response)
39
40
  @env[ASYNC_CLOSE] = EM::DefaultDeferrable.new
41
+ @hp.hijack_setup(@env, @_io)
40
42
  status, headers, body = catch(:async) {
41
43
  APP.call(@env.merge!(RACK_DEFAULTS))
42
44
  }
45
+ return hijacked if @hp.hijacked?
43
46
 
44
47
  if (nil == status || -1 == status)
45
48
  @deferred = true
@@ -67,8 +70,8 @@ class Rainbows::EventMachine::Client < EM::Connection
67
70
  def ev_write_response(status, headers, body, alive)
68
71
  @state = :headers if alive
69
72
  if body.respond_to?(:errback) && body.respond_to?(:callback)
73
+ write_headers(status, headers, alive, body) or return hijacked
70
74
  @deferred = body
71
- write_headers(status, headers, alive)
72
75
  write_body_each(body)
73
76
  deferred_errback(body)
74
77
  deferred_callback(body, alive)
@@ -77,21 +80,22 @@ class Rainbows::EventMachine::Client < EM::Connection
77
80
  st = File.stat(path = body.to_path)
78
81
 
79
82
  if st.file?
80
- write_headers(status, headers, alive)
83
+ write_headers(status, headers, alive, body) or return hijacked
81
84
  @deferred = stream_file_data(path)
82
85
  deferred_errback(body)
83
86
  deferred_callback(body, alive)
84
87
  return
85
88
  elsif st.socket? || st.pipe?
89
+ chunk = stream_response_headers(status, headers, alive, body)
90
+ return hijacked if nil == chunk
86
91
  io = body_to_io(@deferred = body)
87
- chunk = stream_response_headers(status, headers, alive)
88
92
  m = chunk ? Rainbows::EventMachine::ResponseChunkPipe :
89
93
  Rainbows::EventMachine::ResponsePipe
90
94
  return EM.watch(io, m, self).notify_readable = true
91
95
  end
92
96
  # char or block device... WTF? fall through to body.each
93
97
  end
94
- write_response(status, headers, body, alive)
98
+ write_response(status, headers, body, alive) or return hijacked
95
99
  if alive
96
100
  if @deferred.nil?
97
101
  if @buf.empty?
@@ -112,6 +116,7 @@ class Rainbows::EventMachine::Client < EM::Connection
112
116
  end
113
117
 
114
118
  def unbind
119
+ return if @hp.hijacked?
115
120
  async_close = @env[ASYNC_CLOSE] and async_close.succeed
116
121
  @deferred.respond_to?(:fail) and @deferred.fail
117
122
  begin
@@ -40,6 +40,7 @@ module Rainbows::ProcessClient
40
40
 
41
41
  set_input(env, hp)
42
42
  env[REMOTE_ADDR] = kgio_addr
43
+ hp.hijack_setup(env, to_io)
43
44
  status, headers, body = APP.call(env.merge!(RACK_DEFAULTS))
44
45
 
45
46
  if 100 == status.to_i
@@ -47,7 +48,8 @@ module Rainbows::ProcessClient
47
48
  env.delete(HTTP_EXPECT)
48
49
  status, headers, body = APP.call(env)
49
50
  end
50
- write_response(status, headers, body, alive = @hp.next?)
51
+ return if hp.hijacked?
52
+ write_response(status, headers, body, alive = hp.next?) or return
51
53
  end while alive
52
54
  # if we get any error, try to write something back to the client
53
55
  # assuming we haven't closed the socket, but don't get hung up
@@ -56,7 +58,7 @@ module Rainbows::ProcessClient
56
58
  rescue => e
57
59
  handle_error(e)
58
60
  ensure
59
- close unless closed?
61
+ close unless closed? || hp.hijacked?
60
62
  end
61
63
 
62
64
  def handle_error(e)
@@ -71,13 +73,15 @@ module Rainbows::ProcessClient
71
73
  begin
72
74
  set_input(env, hp)
73
75
  env[REMOTE_ADDR] = kgio_addr
76
+ hp.hijack_setup(env, to_io)
74
77
  status, headers, body = APP.call(env.merge!(RACK_DEFAULTS))
75
78
  if 100 == status.to_i
76
79
  write(EXPECT_100_RESPONSE)
77
80
  env.delete(HTTP_EXPECT)
78
81
  status, headers, body = APP.call(env)
79
82
  end
80
- write_response(status, headers, body, alive = hp.next?)
83
+ return if hp.hijacked?
84
+ write_response(status, headers, body, alive = hp.next?) or return
81
85
  end while alive && pipeline_ready(hp)
82
86
  alive or close
83
87
  rescue => e
@@ -19,23 +19,56 @@ module Rainbows::Response
19
19
  Rainbows::HttpParser.keepalive_requests = 0
20
20
  end
21
21
 
22
- def write_headers(status, headers, alive)
23
- @hp.headers? or return
22
+ # Rack 1.5.0 (protocol version 1.2) adds response hijacking support
23
+ if ((Rack::VERSION[0] << 8) | Rack::VERSION[1]) >= 0x0102
24
+ RACK_HIJACK = "rack.hijack"
25
+
26
+ def hijack_prepare(value)
27
+ value
28
+ end
29
+
30
+ def hijack_socket
31
+ @hp.env[RACK_HIJACK].call
32
+ end
33
+ else
34
+ def hijack_prepare(_)
35
+ end
36
+ end
37
+
38
+ # returns the original body on success
39
+ # returns nil if the headers hijacked the response body
40
+ def write_headers(status, headers, alive, body)
41
+ @hp.headers? or return body
42
+ hijack = nil
24
43
  status = CODES[status.to_i] || status
25
44
  buf = "HTTP/1.1 #{status}\r\n" \
26
45
  "Date: #{httpdate}\r\n" \
27
- "Status: #{status}\r\n" \
28
- "Connection: #{alive ? KeepAlive : Close}\r\n"
46
+ "Status: #{status}\r\n"
29
47
  headers.each do |key, value|
30
- next if %r{\A(?:Date\z|Connection\z)}i =~ key
31
- if value =~ /\n/
32
- # avoiding blank, key-only cookies with /\n+/
33
- buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
48
+ case key
49
+ when %r{\A(?:Date\z|Connection\z)}i
50
+ next
51
+ when "rack.hijack"
52
+ # this was an illegal key in Rack < 1.5, so it should be
53
+ # OK to silently discard it for those older versions
54
+ hijack = hijack_prepare(value)
55
+ alive = false # No persistent connections for hijacking
34
56
  else
35
- buf << "#{key}: #{value}\r\n"
57
+ if /\n/ =~ value
58
+ # avoiding blank, key-only cookies with /\n+/
59
+ buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
60
+ else
61
+ buf << "#{key}: #{value}\r\n"
62
+ end
36
63
  end
37
64
  end
38
- write(buf << CRLF)
65
+ write(buf << "Connection: #{alive ? KeepAlive : Close}\r\n\r\n")
66
+
67
+ if hijack
68
+ body = nil # ensure caller does not close body
69
+ hijack.call(hijack_socket)
70
+ end
71
+ body
39
72
  end
40
73
 
41
74
  def close_if_private(io)
@@ -70,8 +103,9 @@ module Rainbows::Response
70
103
  # generic response writer, used for most dynamically-generated responses
71
104
  # and also when copy_stream and/or IO#trysendfile is unavailable
72
105
  def write_response(status, headers, body, alive)
73
- write_headers(status, headers, alive)
74
- write_body_each(body)
106
+ body = write_headers(status, headers, alive, body)
107
+ write_body_each(body) if body
108
+ body
75
109
  ensure
76
110
  body.close if body.respond_to?(:close)
77
111
  end
@@ -166,21 +200,23 @@ module Rainbows::Response
166
200
  if File.file?(body.to_path)
167
201
  if r = sendfile_range(status, headers)
168
202
  status, headers, range = r
169
- write_headers(status, headers, alive)
170
- write_body_file(body, range) if range
203
+ body = write_headers(status, headers, alive, body)
204
+ write_body_file(body, range) if body && range
171
205
  else
172
- write_headers(status, headers, alive)
173
- write_body_file(body, nil)
206
+ body = write_headers(status, headers, alive, body)
207
+ write_body_file(body, nil) if body
174
208
  end
175
209
  else
176
- write_headers(status, headers, alive)
177
- write_body_stream(body)
210
+ body = write_headers(status, headers, alive, body)
211
+ write_body_stream(body) if body
178
212
  end
213
+ body
179
214
  ensure
180
215
  body.close if body.respond_to?(:close)
181
216
  end
182
217
 
183
218
  module ToPath
219
+ # returns nil if hijacked
184
220
  def write_response(status, headers, body, alive)
185
221
  if body.respond_to?(:to_path)
186
222
  write_response_path(status, headers, body, alive)
@@ -36,7 +36,7 @@ module Rainbows::Revactor::Client::Methods
36
36
  end
37
37
 
38
38
  def write_response(status, headers, body, alive)
39
- super(status, headers, body, alive)
39
+ super(status, headers, body, alive) or return
40
40
  alive && @ts and @hp.buf << @ts.leftover
41
41
  end
42
42
 
@@ -26,18 +26,24 @@ module Rainbows::StreamResponseEpoll
26
26
 
27
27
  def http_response_write(socket, status, headers, body)
28
28
  status = CODES[status.to_i] || status
29
- ep_client = false
29
+ hijack = ep_client = false
30
30
 
31
31
  if headers
32
32
  # don't set extra headers here, this is only intended for
33
33
  # consuming by nginx.
34
34
  buf = "HTTP/1.0 #{status}\r\nStatus: #{status}\r\n"
35
35
  headers.each do |key, value|
36
- if value =~ /\n/
37
- # avoiding blank, key-only cookies with /\n+/
38
- buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
36
+ case key
37
+ when "rack.hijack"
38
+ hijack = hijack_prepare(value)
39
+ body = nil # ensure we do not close body
39
40
  else
40
- buf << "#{key}: #{value}\r\n"
41
+ if /\n/ =~ value
42
+ # avoiding blank, key-only cookies with /\n+/
43
+ buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
44
+ else
45
+ buf << "#{key}: #{value}\r\n"
46
+ end
41
47
  end
42
48
  end
43
49
  buf << HEADER_END
@@ -48,11 +54,22 @@ module Rainbows::StreamResponseEpoll
48
54
  buf = rv
49
55
  when :wait_writable
50
56
  ep_client = Client.new(socket, buf)
51
- body.each { |chunk| ep_client.write(chunk) }
52
- return ep_client.close
57
+ if hijack
58
+ ep_client.hijack(hijack)
59
+ else
60
+ body.each { |chunk| ep_client.write(chunk) }
61
+ ep_client.close
62
+ end
63
+ # body is nil on hijack, in which case ep_client is never closed by us
64
+ return
53
65
  end while true
54
66
  end
55
67
 
68
+ if hijack
69
+ hijack.call(socket)
70
+ return
71
+ end
72
+
56
73
  body.each do |chunk|
57
74
  if ep_client
58
75
  ep_client.write(chunk)
@@ -67,14 +84,15 @@ module Rainbows::StreamResponseEpoll
67
84
  end while true
68
85
  end
69
86
  end
70
- ensure
71
- body.respond_to?(:close) and body.close
72
- if ep_client
73
- ep_client.close
74
- else
75
- socket.shutdown
76
- socket.close
77
- end
87
+ ensure
88
+ return if hijack
89
+ body.respond_to?(:close) and body.close
90
+ if ep_client
91
+ ep_client.close
92
+ else
93
+ socket.shutdown
94
+ socket.close
95
+ end
78
96
  end
79
97
 
80
98
  # once a client is accepted, it is processed in its entirety here
@@ -88,6 +106,7 @@ module Rainbows::StreamResponseEpoll
88
106
  status, headers, body = @app.call(env)
89
107
  end
90
108
  @request.headers? or headers = nil
109
+ return if @request.hijacked?
91
110
  http_response_write(client, status, headers, body)
92
111
  rescue => e
93
112
  handle_error(client, e)
@@ -18,7 +18,7 @@ class Rainbows::StreamResponseEpoll::Client
18
18
  attr_reader :to_io
19
19
 
20
20
  def initialize(io, unwritten)
21
- @closed = false
21
+ @finish = false
22
22
  @to_io = io
23
23
  @wr_queue = [ unwritten.dup ]
24
24
  EP.set(self, OUT)
@@ -29,7 +29,11 @@ class Rainbows::StreamResponseEpoll::Client
29
29
  end
30
30
 
31
31
  def close
32
- @closed = true
32
+ @finish = true
33
+ end
34
+
35
+ def hijack(hijack)
36
+ @finish = hijack
33
37
  end
34
38
 
35
39
  def epoll_run
@@ -49,10 +53,14 @@ class Rainbows::StreamResponseEpoll::Client
49
53
  end
50
54
 
51
55
  def on_write_complete
52
- if @closed
56
+ if true == @finish
53
57
  @to_io.shutdown
54
58
  @to_io.close
55
59
  N.decr(0, 1)
60
+ elsif @finish.respond_to?(:call) # hijacked
61
+ EP.delete(self)
62
+ N.decr(0, 1)
63
+ @finish.call(@to_io)
56
64
  end
57
65
  end
58
66
  end
@@ -8,11 +8,13 @@ class Rainbows::WriterThreadPool::Client < Struct.new(:to_io, :q)
8
8
 
9
9
  module Methods
10
10
  def write_body_each(body)
11
+ return if @hp.hijacked?
11
12
  q << [ to_io, :write_body_each, body ]
12
13
  end
13
14
 
14
15
  def write_response_close(status, headers, body, alive)
15
16
  to_io.instance_variable_set(:@hp, @hp) # XXX ugh
17
+ return if @hp.hijacked?
16
18
  Rainbows::SyncClose.new(body) { |sync_body|
17
19
  q << [ to_io, :write_response, status, headers, sync_body, alive ]
18
20
  }
@@ -27,9 +27,6 @@ module Rainbows::XEpoll::Client
27
27
  def self.loop
28
28
  begin
29
29
  EP.wait(nil, 1000) { |_, obj| obj.epoll_run }
30
- while obj = ReRun.shift
31
- obj.epoll_run
32
- end
33
30
  Rainbows::Epoll::Client.expire
34
31
  rescue Errno::EINTR
35
32
  rescue => e
@@ -37,7 +37,7 @@ module Rainbows::XEpollThreadPool::Client
37
37
 
38
38
  ep = SleepyPenguin::Epoll
39
39
  EP = ep.new
40
- IN = ep::IN | ep::ET | ep::ONESHOT
40
+ IN = ep::IN | ep::ONESHOT
41
41
  KATO = {}
42
42
  KATO.compare_by_identity if KATO.respond_to?(:compare_by_identity)
43
43
  LOCK = Mutex.new
@@ -26,7 +26,7 @@ module Rainbows::XEpollThreadSpawn::Client
26
26
 
27
27
  ep = SleepyPenguin::Epoll
28
28
  EP = ep.new
29
- IN = ep::IN | ep::ET | ep::ONESHOT
29
+ IN = ep::IN | ep::ONESHOT
30
30
  KATO = {}
31
31
  KATO.compare_by_identity if KATO.respond_to?(:compare_by_identity)
32
32
  LOCK = Mutex.new
@@ -28,7 +28,9 @@ Gem::Specification.new do |s|
28
28
  s.add_dependency(%q<kgio>, ['~> 2.5'])
29
29
 
30
30
  # we need Unicorn for the HTTP parser and process management
31
- s.add_dependency(%q<unicorn>, ["~> 4.1"])
31
+ # 4.6.0+ supports hijacking, 4.6.2 fixes the chunk parser (for Ruby 2.0.0)
32
+ s.add_dependency(%q<unicorn>, ["~> 4.6", ">= 4.6.2"])
33
+
32
34
  s.add_development_dependency(%q<isolate>, "~> 3.1")
33
35
  s.add_development_dependency(%q<wrongdoc>, "~> 1.6")
34
36
 
@@ -53,6 +55,7 @@ Gem::Specification.new do |s|
53
55
  # NeverBlock, currently only available on http://gems.github.com/
54
56
  # s.add_dependency(%q<espace-neverblock>, ["~> 0.1.6.1"])
55
57
 
56
- # accessor not compatible with older RubyGems
57
- # s.licenses = %w(GPLv3 GPLv2 Ruby)
58
+ # We inherited the Ruby 1.8 license from Mongrel, so we're stuck with it.
59
+ # GPLv3 is preferred.
60
+ s.licenses = ["GPLv2", "GPLv3", "Ruby 1.8"]
58
61
  end
@@ -30,13 +30,12 @@ models += WriterThreadSpawn
30
30
  models += ThreadPool
31
31
  models += ThreadSpawn
32
32
  models += Coolio
33
- models += EventMachine
34
- models += NeverBlock
33
+
35
34
  models += StreamResponseEpoll
36
35
 
37
36
  ifeq ($(RUBY_ENGINE),ruby)
38
37
  rp := )
39
- ONENINE := $(shell case $(RUBY_VERSION) in 1.9.*$(rp) echo true;;esac)
38
+ ONENINE := $(shell case $(RUBY_VERSION) in 1.9.*|2.0.*$(rp) echo true;;esac)
40
39
  ifeq ($(ONENINE),true)
41
40
  ifeq ($(RUBY_VERSION),1.9.2)
42
41
  models += Revactor
@@ -46,6 +45,14 @@ ifeq ($(RUBY_ENGINE),ruby)
46
45
  models += CoolioThreadPool
47
46
  models += CoolioThreadSpawn
48
47
  models += CoolioFiberSpawn
48
+
49
+ # EventMachine 1.0.0 currently does not build on Ruby 2.0.0
50
+ # NeverBlock depends on 2.0.0
51
+ RBTWO := $(shell case $(RUBY_VERSION) in 2.0.*$(rp) echo true;;esac)
52
+ ifeq ($(RBTWO),)
53
+ models += EventMachine
54
+ models += NeverBlock
55
+ endif
49
56
  endif
50
57
  endif
51
58
 
@@ -1,5 +1,5 @@
1
1
  t_begin "byte-range setup vars" && {
2
- random_blob_size=$(wc -c < random_blob)
2
+ random_blob_size=$(count_bytes < random_blob)
3
3
  rb_1=$(( $random_blob_size - 1 ))
4
4
  range_head=-r-365
5
5
  range_tail=-r155-