rainbows 3.0.0 → 3.1.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 (77) hide show
  1. data/.wrongdoc.yml +2 -2
  2. data/Documentation/comparison.haml +6 -6
  3. data/GIT-VERSION-GEN +1 -1
  4. data/GNUmakefile +16 -128
  5. data/README +2 -3
  6. data/Rakefile +3 -3
  7. data/examples/reverse_proxy.ru +9 -0
  8. data/lib/rainbows.rb +14 -6
  9. data/lib/rainbows/base.rb +0 -1
  10. data/lib/rainbows/const.rb +1 -10
  11. data/lib/rainbows/coolio/client.rb +8 -4
  12. data/lib/rainbows/coolio/core.rb +0 -3
  13. data/lib/rainbows/coolio/thread_client.rb +2 -2
  14. data/lib/rainbows/coolio_fiber_spawn.rb +6 -6
  15. data/lib/rainbows/dev_fd_response.rb +16 -9
  16. data/lib/rainbows/epoll.rb +43 -0
  17. data/lib/rainbows/epoll/client.rb +232 -0
  18. data/lib/rainbows/epoll/response_chunk_pipe.rb +18 -0
  19. data/lib/rainbows/epoll/response_pipe.rb +32 -0
  20. data/lib/rainbows/epoll/server.rb +31 -0
  21. data/lib/rainbows/error.rb +1 -9
  22. data/lib/rainbows/ev_core.rb +12 -12
  23. data/lib/rainbows/ev_core/cap_input.rb +1 -1
  24. data/lib/rainbows/event_machine.rb +0 -6
  25. data/lib/rainbows/event_machine/client.rb +3 -3
  26. data/lib/rainbows/event_machine/response_chunk_pipe.rb +5 -7
  27. data/lib/rainbows/event_machine/response_pipe.rb +7 -8
  28. data/lib/rainbows/fiber/base.rb +2 -2
  29. data/lib/rainbows/fiber/io.rb +21 -63
  30. data/lib/rainbows/fiber/io/methods.rb +1 -1
  31. data/lib/rainbows/http_server.rb +4 -4
  32. data/lib/rainbows/join_threads.rb +18 -0
  33. data/lib/rainbows/max_body.rb +2 -1
  34. data/lib/rainbows/max_body/wrapper.rb +1 -1
  35. data/lib/rainbows/never_block/event_machine.rb +2 -2
  36. data/lib/rainbows/process_client.rb +9 -1
  37. data/lib/rainbows/queue_pool.rb +2 -2
  38. data/lib/rainbows/response.rb +1 -1
  39. data/lib/rainbows/rev_fiber_spawn.rb +4 -4
  40. data/lib/rainbows/revactor/client.rb +4 -5
  41. data/lib/rainbows/revactor/proxy.rb +1 -1
  42. data/lib/rainbows/reverse_proxy.rb +189 -0
  43. data/lib/rainbows/reverse_proxy/coolio.rb +61 -0
  44. data/lib/rainbows/reverse_proxy/ev_client.rb +39 -0
  45. data/lib/rainbows/reverse_proxy/event_machine.rb +46 -0
  46. data/lib/rainbows/reverse_proxy/multi_thread.rb +7 -0
  47. data/lib/rainbows/reverse_proxy/synchronous.rb +21 -0
  48. data/lib/rainbows/sendfile.rb +1 -1
  49. data/lib/rainbows/sync_close.rb +2 -2
  50. data/lib/rainbows/thread_pool.rb +1 -1
  51. data/lib/rainbows/writer_thread_pool.rb +1 -1
  52. data/lib/rainbows/xepoll.rb +24 -0
  53. data/lib/rainbows/xepoll/client.rb +45 -0
  54. data/pkg.mk +171 -0
  55. data/rainbows.gemspec +2 -4
  56. data/t/GNUmakefile +4 -0
  57. data/t/bin/content-md5-put +1 -0
  58. data/t/kgio-pipe-response.ru +9 -1
  59. data/t/rack-fiber_pool/app.ru +6 -1
  60. data/t/simple-http_Epoll.ru +9 -0
  61. data/t/simple-http_XEpoll.ru +9 -0
  62. data/t/t0014-config-conflict.sh +5 -3
  63. data/t/t0023-sendfile-byte-range.sh +1 -7
  64. data/t/t0034-pipelined-pipe-response.sh +2 -1
  65. data/t/t0035-kgio-pipe-response.sh +0 -7
  66. data/t/t0041-optional-pool-size.sh +51 -0
  67. data/t/t0050-response-body-close-has-env.sh +2 -1
  68. data/t/t0104-rack-input-limit-tiny.sh +6 -4
  69. data/t/t0105-rack-input-limit-bigger.sh +6 -4
  70. data/t/t0106-rack-input-keepalive.sh +2 -0
  71. data/t/t0107-rack-input-limit-zero.sh +60 -0
  72. data/t/t0113-rewindable-input-false.sh +1 -0
  73. data/t/t0114-rewindable-input-true.sh +1 -0
  74. data/t/t0202-async-response-one-oh.sh +56 -0
  75. data/t/test_isolate.rb +5 -2
  76. metadata +42 -37
  77. data/lib/rainbows/rack_input.rb +0 -17
@@ -26,7 +26,7 @@ module Rainbows::Response
26
26
  "Status: #{status}\r\n" \
27
27
  "Connection: #{alive ? KeepAlive : Close}\r\n"
28
28
  headers.each do |key, value|
29
- next if %r{\A(?:X-Rainbows-|Date\z|Connection\z)}i =~ key
29
+ next if %r{\A(?:Date\z|Connection\z)}i =~ key
30
30
  if value =~ /\n/
31
31
  # avoiding blank, key-only cookies with /\n+/
32
32
  buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
@@ -6,8 +6,8 @@ Rainbows.const_set(:RevFiberSpawn, Rainbows::CoolioFiberSpawn)
6
6
  # A combination of the Rev and FiberSpawn models. This allows Ruby
7
7
  # 1.9 Fiber-based concurrency for application processing while
8
8
  # exposing a synchronous execution model and using scalable network
9
- # concurrency provided by Rev. A "rack.input" is exposed as well
10
- # being Sunshowers-compatible. Applications are strongly advised to
11
- # wrap all slow IO objects (sockets, pipes) using the
12
- # Rainbows::Fiber::IO or a Rev-compatible class whenever possible.
9
+ # concurrency provided by Rev. A streaming "rack.input" is exposed.
10
+ # Applications are strongly advised to wrap all slow IO objects
11
+ # (sockets, pipes) using the Rainbows::Fiber::IO or a Rev-compatible
12
+ # class whenever possible.
13
13
  module Rainbows::RevFiberSpawn; end
@@ -28,10 +28,6 @@ class Rainbows::Revactor::Client
28
28
  @client.write(buf)
29
29
  end
30
30
 
31
- def write_nonblock(buf) # only used for errors
32
- @client.instance_variable_get(:@_io).write_nonblock(buf)
33
- end
34
-
35
31
  def timed_read(buf2)
36
32
  buf2.replace(@client.read(*@rd_args))
37
33
  end
@@ -39,7 +35,10 @@ class Rainbows::Revactor::Client
39
35
  def set_input(env, hp)
40
36
  env[RACK_INPUT] = 0 == hp.content_length ?
41
37
  NULL_IO : IC.new(@ts = TeeSocket.new(@client), hp)
42
- env[CLIENT_IO] = @client
38
+ end
39
+
40
+ def to_io
41
+ @client.instance_variable_get(:@_io)
43
42
  end
44
43
 
45
44
  def close
@@ -17,7 +17,7 @@ class Rainbows::Revactor::Proxy < Rev::IO
17
17
  end
18
18
  end
19
19
 
20
- def each(&block)
20
+ def each
21
21
  # when yield-ing, Revactor::TCP#write may raise EOFError
22
22
  # (instead of Errno::EPIPE), so we need to limit the rescue
23
23
  # to just readpartial and let EOFErrors during yield bubble up
@@ -0,0 +1,189 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ require 'socket'
4
+ require 'thread'
5
+ require 'uri'
6
+ require 'kcar' # http://bogomips.org/kcar/ -- gem install kcar
7
+
8
+ # This is lightly tested and has an unstable configuration interface.
9
+ # ***** Do not rely on anything under the ReverseProxy namespace! *****
10
+ #
11
+ # A reverse proxy implementation for \Rainbows! It is a Rack application
12
+ # compatible and optimized for most \Rainbows! concurrency models.
13
+ #
14
+ # It makes HTTP/1.0 connections without keepalive to backends, so
15
+ # it is only recommended for proxying to upstreams on the same LAN
16
+ # or machine. It can proxy to TCP hosts as well as UNIX domain sockets.
17
+ #
18
+ # Currently it only does simple round-robin balancing and does not
19
+ # know to retry connections from failed backends.
20
+ #
21
+ # Buffering-behavior is currently dependent on the concurrency model selected:
22
+ #
23
+ # Fully-buffered (uploads and response bodies):
24
+ # Coolio, EventMachine, NeverBlock, CoolioThreadSpawn, CoolioThreadPool
25
+ # If you're proxying to Unicorn, fully-buffered is the way to go.
26
+ #
27
+ # Buffered input only (uploads, but not response bodies):
28
+ # ThreadSpawn, ThreadPool, FiberSpawn, FiberPool, CoolioFiberSpawn
29
+ #
30
+ # It is not recommended to use Base, WriterThreadSpawn or WriterThreadPool
31
+ # to host this application. However, you may proxy to a backend running
32
+ # one of these concurrency models with a fully-buffering concurrency model.
33
+ #
34
+ # See the {example config}[link:examples/reverse_proxy.ru] for a sample
35
+ # configuration
36
+ #
37
+ # TODO: Revactor support
38
+ # TODO: Support HTTP trailers
39
+ # TODO: optional streaming input for synchronous
40
+ # TODO: error handling
41
+ #
42
+ # WARNING! this is only lightly tested and has no automated tests, yet!
43
+ class Rainbows::ReverseProxy
44
+ autoload :MultiThread, 'rainbows/reverse_proxy/multi_thread'
45
+ autoload :Synchronous, 'rainbows/reverse_proxy/synchronous'
46
+ autoload :Coolio, 'rainbows/reverse_proxy/coolio'
47
+ autoload :EventMachine, 'rainbows/reverse_proxy/event_machine'
48
+ autoload :EvClient, 'rainbows/reverse_proxy/ev_client'
49
+
50
+ HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR"
51
+ REMOTE_ADDR = "REMOTE_ADDR"
52
+ REQUEST_METHOD = "REQUEST_METHOD"
53
+ REQUEST_URI = "REQUEST_URI"
54
+ CRLF = "\r\n"
55
+ TR = %w(_ -)
56
+ CONTENT_LENGTH = "CONTENT_LENGTH"
57
+ HTTP_TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING"
58
+ RackInput = "rack.input"
59
+ E502 = [ 502, [ %w(Content-Length 0), %w(Content-Type text/plain) ], [] ]
60
+
61
+ def initialize(opts)
62
+ @lock = Mutex.new
63
+ upstreams = opts[:upstreams]
64
+ @upstreams = []
65
+ upstreams.each do |url|
66
+ url, cfg = *url if Array === url
67
+ if url =~ %r{\Ahttp://}
68
+ uri = URI.parse(url)
69
+ host = uri.host =~ %r{\A\[([a-fA-F0-9:]+)\]\z} ? $1 : uri.host
70
+ sockaddr = Socket.sockaddr_in(uri.port, host)
71
+ else
72
+ path = url.gsub(%r{\Aunix:}, "") # nginx compat
73
+ %r{\A~} =~ path and path = File.expand_path(path)
74
+ sockaddr = Socket.sockaddr_un(path)
75
+ end
76
+ ((cfg && cfg[:weight]) || 1).times { @upstreams << sockaddr }
77
+ end
78
+ @nr = 0
79
+ end
80
+
81
+ # detects the concurrency model at first run and replaces itself
82
+ def call(env)
83
+ if @lock.try_lock
84
+ case model = env["rainbows.model"]
85
+ when :EventMachine, :NeverBlock
86
+ extend(EventMachine)
87
+ when :Coolio, :CoolioThreadPool, :CoolioThreadSpawn
88
+ extend(Coolio)
89
+ when :RevFiberSpawn, :Rev, :RevThreadPool, :RevThreadSpawn
90
+ warn "#{model} is not *well* supported with #{self.class}"
91
+ warn "Switch to #{model.to_s.gsub(/Rev/, 'Coolio')}!"
92
+ extend(Synchronous)
93
+ when :Revactor
94
+ warn "Revactor is not *well* supported with #{self.class} yet"
95
+ extend(Synchronous)
96
+ when :FiberSpawn, :FiberPool, :CoolioFiberSpawn
97
+ extend(Synchronous)
98
+ Synchronous::UpstreamSocket.
99
+ __send__(:include, Rainbows::Fiber::IO::Methods)
100
+ when :WriterThreadSpawn, :WriterThreadPool
101
+ warn "#{model} is not recommended for use with #{self.class}"
102
+ extend(Synchronous)
103
+ else
104
+ extend(Synchronous)
105
+ end
106
+ extend(MultiThread) if env["rack.multithread"]
107
+ @lock.unlock
108
+ else
109
+ @lock.synchronize {} # wait for the first locker to finish
110
+ end
111
+ call(env)
112
+ end
113
+
114
+ # returns request headers for sending to the upstream as a string
115
+ def build_headers(env, input)
116
+ remote_addr = env[REMOTE_ADDR]
117
+ xff = env[HTTP_X_FORWARDED_FOR]
118
+ xff = xff ? "#{xff},#{remote_addr}" : remote_addr
119
+ req = "#{env[REQUEST_METHOD]} #{env[REQUEST_URI]} HTTP/1.0\r\n" \
120
+ "Connection: close\r\n" \
121
+ "X-Forwarded-For: #{xff}\r\n"
122
+ uscore, dash = *TR
123
+ env.each do |key, value|
124
+ %r{\AHTTP_(\w+)\z} =~ key or next
125
+ key = $1
126
+ next if %r{\A(?:VERSION|CONNECTION|KEEP_ALIVE|X_FORWARDED_FOR)\z}x =~ key
127
+ key.tr!(uscore, dash)
128
+ req << "#{key}: #{value}\r\n"
129
+ end
130
+ input and req << (input.respond_to?(:size) ?
131
+ "Content-Length: #{input.size}\r\n" :
132
+ "Transfer-Encoding: chunked\r\n")
133
+ req << CRLF
134
+ end
135
+
136
+ def pick_upstream(env) # +env+ is reserved for future expansion
137
+ @nr += 1
138
+ @upstreams[@nr %= @upstreams.size]
139
+ end
140
+
141
+ def prepare_input!(env)
142
+ if cl = env[CONTENT_LENGTH]
143
+ size = cl.to_i
144
+ size > 0 or return
145
+ elsif %r{\Achunked\z}i =~ env.delete(HTTP_TRANSFER_ENCODING)
146
+ # do people use multiple transfer-encodings?
147
+ else
148
+ return
149
+ end
150
+
151
+ input = env[RackInput]
152
+ if input.respond_to?(:rewind)
153
+ if input.respond_to?(:size)
154
+ input.size # TeeInput-specific behavior
155
+ return input
156
+ else
157
+ return SizedInput.new(input, size)
158
+ end
159
+ end
160
+ tmp = size && size < 0x4000 ? StringIO.new("") : Unicorn::TmpIO.new
161
+ each_block(input) { |x| tmp.syswrite(x) }
162
+ tmp.rewind
163
+ tmp
164
+ end
165
+
166
+ class SizedInput
167
+ attr_reader :size
168
+
169
+ def initialize(input, n)
170
+ buf = ""
171
+ if n == nil
172
+ n = 0
173
+ while input.read(16384, buf)
174
+ n += buf.size
175
+ end
176
+ input.rewind
177
+ end
178
+ @input, @size = input, n
179
+ end
180
+
181
+ def read(*args)
182
+ @input.read(*args)
183
+ end
184
+ end
185
+
186
+ class UpstreamSocket < Kgio::Socket
187
+ alias readpartial kgio_read!
188
+ end
189
+ end
@@ -0,0 +1,61 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ # TODO: handle large responses without having it all in memory
4
+ module Rainbows::ReverseProxy::Coolio
5
+ LOOP = Cool.io::Loop.default
6
+
7
+ class Backend < Cool.io::IO
8
+ include Rainbows::ReverseProxy::EvClient
9
+
10
+ def initialize(env, addr, input)
11
+ @env = env
12
+ @input = input
13
+ @junk, @rbuf = "", ""
14
+ @parser = Kcar::Parser.new
15
+ @response = @body = nil
16
+ @headers = Rack::Utils::HeaderHash.new
17
+ super(UpstreamSocket.start(addr)) # kgio-enabled socket
18
+ end
19
+
20
+ def on_write_complete
21
+ if @input
22
+ buf = @input.read(16384, @junk) and return write(buf)
23
+ @input = nil
24
+ end
25
+ end
26
+
27
+ def on_readable
28
+ # avoiding IO#read_nonblock since that's expensive in 1.9.2
29
+ case buf = @_io.kgio_tryread(16384, @junk)
30
+ when String
31
+ receive_data(buf)
32
+ when :wait_readable
33
+ return
34
+ when nil
35
+ @env[AsyncCallback].call(@response)
36
+ return close
37
+ end while true # we always read until EAGAIN or EOF
38
+
39
+ rescue => e
40
+ case e
41
+ when Errno::ECONNRESET
42
+ @env[AsyncCallback].call(@response)
43
+ return close
44
+ when SystemCallError
45
+ else
46
+ logger = @env["rack.logger"]
47
+ logger.error "#{e} #{e.message}"
48
+ e.backtrace.each { |m| logger.error m }
49
+ end
50
+ @env[AsyncCallback].call(Rainbows::ReverseProxy::E502)
51
+ close
52
+ end
53
+ end
54
+
55
+ def call(env)
56
+ input = prepare_input!(env)
57
+ sock = Backend.new(env, pick_upstream(env), input).attach(LOOP)
58
+ sock.write(build_headers(env, input))
59
+ throw :async
60
+ end
61
+ end
@@ -0,0 +1,39 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ require 'tempfile'
4
+ module Rainbows::ReverseProxy::EvClient
5
+ include Rainbows::ReverseProxy::Synchronous
6
+ AsyncCallback = "async.callback"
7
+ CBB = Unicorn::TeeInput.client_body_buffer_size
8
+ Content_Length = "Content-Length"
9
+ Transfer_Encoding = "Transfer-Encoding"
10
+
11
+ def receive_data(buf)
12
+ if @body
13
+ @body << buf
14
+ else
15
+ response = @parser.headers(@headers, @rbuf << buf) or return
16
+ if (cl = @headers[Content_Length] && cl.to_i > CBB) ||
17
+ (%r{\bchunked\b} =~ @headers[Transfer_Encoding])
18
+ @body = LargeBody.new("")
19
+ @body << @rbuf
20
+ @response = response << @body
21
+ else
22
+ @body = @rbuf.dup
23
+ @response = response << [ @body ]
24
+ end
25
+ end
26
+ end
27
+
28
+ class LargeBody < Tempfile
29
+ def each
30
+ buf = ""
31
+ rewind
32
+ while read(16384, buf)
33
+ yield buf
34
+ end
35
+ end
36
+
37
+ alias close close!
38
+ end
39
+ end
@@ -0,0 +1,46 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ # TODO: handle large responses without having it all in memory
4
+ module Rainbows::ReverseProxy::EventMachine
5
+ class Backend < EM::Connection
6
+ include Rainbows::ReverseProxy::EvClient # provides receive_data
7
+
8
+ # +addr+ is a packed sockaddr, so it can be either a UNIX or TCP socket
9
+ def initialize(env)
10
+ @env = env
11
+ @rbuf = ""
12
+ @parser = Kcar::Parser.new
13
+ @response = @body = nil
14
+ @headers = Rack::Utils::HeaderHash.new
15
+ end
16
+
17
+ # prevents us from sending too much at once and OOM-ing on large uploads
18
+ def stream_input(input)
19
+ if buf = input.read(16384)
20
+ send_data buf
21
+ EM.next_tick { stream_input(input) }
22
+ end
23
+ end
24
+
25
+ def on_write_complete
26
+ if @input
27
+ buf = @input.read(16384, @junk) and return write(buf)
28
+ @input = nil
29
+ end
30
+ end
31
+
32
+ def unbind
33
+ @env[AsyncCallback].call(@response || Rainbows::ReverseProxy::E502)
34
+ end
35
+ end
36
+
37
+ UpstreamSocket = Rainbows::ReverseProxy::UpstreamSocket
38
+ def call(env)
39
+ input = prepare_input!(env)
40
+ io = UpstreamSocket.start(pick_upstream(env))
41
+ sock = EM.attach(io, Backend, env)
42
+ sock.send_data(build_headers(env, input))
43
+ sock.stream_input(input) if input
44
+ throw :async
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ # -*- encoding -*-
2
+ # :enddoc:
3
+ module Rainbows::ReverseProxy::MultiThread
4
+ def pick_upstream(env)
5
+ @lock.synchronize { super(env) }
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ module Rainbows::ReverseProxy::Synchronous
4
+ UpstreamSocket = Rainbows::ReverseProxy::UpstreamSocket
5
+
6
+ def each_block(input)
7
+ buf = ""
8
+ while input.read(16384, buf)
9
+ yield buf
10
+ end
11
+ end
12
+
13
+ def call(env)
14
+ input = prepare_input!(env)
15
+ req = build_headers(env, input)
16
+ sock = UpstreamSocket.new(pick_upstream(env))
17
+ sock.write(req)
18
+ each_block(input) { |buf| sock.kgio_write(buf) } if input
19
+ Kcar::Response.new(sock).rack
20
+ end
21
+ end
@@ -63,7 +63,7 @@ class Rainbows::Sendfile < Struct.new(:app)
63
63
  end
64
64
 
65
65
  # fallback in case our +to_path+ doesn't get handled for whatever reason
66
- def each(&block)
66
+ def each
67
67
  buf = ''
68
68
  File.open(to_path) do |fp|
69
69
  yield buf while fp.read(0x4000, buf)
@@ -20,8 +20,8 @@ class Rainbows::SyncClose
20
20
  @body.to_path
21
21
  end
22
22
 
23
- def each(&block)
24
- @body.each(&block)
23
+ def each
24
+ @body.each { |x| yield x }
25
25
  end
26
26
 
27
27
  def to_io