rainbows 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -2,12 +2,12 @@
2
2
  require 'rainbows/fiber/coolio'
3
3
 
4
4
  # A combination of the Coolio and FiberSpawn models. This allows Ruby
5
- # 1.9 Fiber-based concurrency for application processing while
6
- # exposing a synchronous execution model and using scalable network
7
- # concurrency provided by Cool.io. A "rack.input" is exposed as well
8
- # being Sunshowers-compatible. Applications are strongly advised to
9
- # wrap all slow IO objects (sockets, pipes) using the
10
- # Rainbows::Fiber::IO or a Cool.io-compatible class whenever possible.
5
+ # 1.9 Fiber-based concurrency for application processing while exposing
6
+ # a synchronous execution model and using scalable network concurrency
7
+ # provided by Cool.io. A streaming "rack.input" is exposed.
8
+ # Applications are strongly advised to wrap all slow IO objects
9
+ # (sockets, pipes) using the Rainbows::Fiber::IO or a Cool.io-compatible
10
+ # class whenever possible.
11
11
  module Rainbows::CoolioFiberSpawn
12
12
 
13
13
  include Rainbows::Base
@@ -15,6 +15,13 @@ class Rainbows::DevFdResponse < Struct.new(:app)
15
15
 
16
16
  # :stopdoc:
17
17
  FD_MAP = Rainbows::FD_MAP
18
+ Content_Length = "Content-Length".freeze
19
+ Transfer_Encoding = "Transfer-Encoding".freeze
20
+ Rainbows_autochunk = "rainbows.autochunk".freeze
21
+ Rainbows_model = "rainbows.model"
22
+ HTTP_1_0 = "HTTP/1.0"
23
+ HTTP_VERSION = "HTTP_VERSION"
24
+ Chunked = "chunked"
18
25
 
19
26
  # make this a no-op under Rubinius, it's pointless anyways
20
27
  # since Rubinius doesn't have IO.copy_stream
@@ -44,19 +51,19 @@ class Rainbows::DevFdResponse < Struct.new(:app)
44
51
  fileno = io.fileno
45
52
  FD_MAP[fileno] = io
46
53
  if st.file?
47
- headers['Content-Length'] ||= st.size.to_s
48
- headers.delete('Transfer-Encoding')
54
+ headers[Content_Length] ||= st.size.to_s
55
+ headers.delete(Transfer_Encoding)
49
56
  elsif st.pipe? || st.socket? # epoll-able things
50
- unless headers.include?('Content-Length')
51
- if env['rainbows.autochunk']
52
- headers['Transfer-Encoding'] = 'chunked'
57
+ unless headers.include?(Content_Length)
58
+ if env[Rainbows_autochunk] && HTTP_1_0 != env[HTTP_VERSION]
59
+ headers[Transfer_Encoding] = Chunked
53
60
  else
54
- headers['X-Rainbows-Autochunk'] = 'no'
61
+ env[Rainbows_autochunk] = false
55
62
  end
56
63
  end
57
64
 
58
65
  # we need to make sure our pipe output is Fiber-compatible
59
- case env["rainbows.model"]
66
+ case env[Rainbows_model]
60
67
  when :FiberSpawn, :FiberPool, :RevFiberSpawn, :CoolioFiberSpawn
61
68
  io.respond_to?(:kgio_wait_readable) or
62
69
  io = Rainbows::Fiber::IO.new(io)
@@ -72,8 +79,8 @@ class Rainbows::DevFdResponse < Struct.new(:app)
72
79
  class Body < Struct.new(:to_io, :to_path, :orig_body)
73
80
  # called by the webserver or other middlewares if they can't
74
81
  # handle #to_path
75
- def each(&block)
76
- to_io.each(&block)
82
+ def each
83
+ to_io.each { |x| yield x }
77
84
  end
78
85
 
79
86
  # remain Rack::Lint-compatible for people with wonky systems :P
@@ -0,0 +1,43 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ require 'sleepy_penguin'
4
+ require 'sendfile'
5
+
6
+ # Edge-triggered epoll concurrency model. This is extremely unfair
7
+ # and optimized for throughput at the expense of fairness
8
+ module Rainbows::Epoll
9
+ include Rainbows::Base
10
+ ReRun = []
11
+ autoload :Server, 'rainbows/epoll/server'
12
+ autoload :Client, 'rainbows/epoll/client'
13
+ autoload :ResponsePipe, 'rainbows/epoll/response_pipe'
14
+ autoload :ResponseChunkPipe, 'rainbows/epoll/response_chunk_pipe'
15
+ class << self
16
+ attr_writer :nr_clients
17
+ end
18
+
19
+ def self.loop
20
+ timeout = Rainbows.server.timeout
21
+ begin
22
+ EP.wait(nil, timeout) { |flags, obj| obj.epoll_run }
23
+ while obj = ReRun.shift
24
+ obj.epoll_run
25
+ end
26
+ Rainbows::Epoll::Client.expire
27
+ rescue Errno::EINTR
28
+ rescue => e
29
+ Rainbows::Error.listen_loop(e)
30
+ end while Rainbows.tick || @nr_clients.call > 0
31
+ end
32
+
33
+ def init_worker_process(worker)
34
+ super
35
+ Rainbows::Epoll.const_set :EP, SleepyPenguin::Epoll.new
36
+ Rainbows::Client.__send__ :include, Client
37
+ end
38
+
39
+ def worker_loop(worker) # :nodoc:
40
+ init_worker_process(worker)
41
+ Server.run
42
+ end
43
+ end
@@ -0,0 +1,232 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+
4
+ module Rainbows::Epoll::Client
5
+
6
+ include Rainbows::EvCore
7
+ APP = Rainbows.server.app
8
+ Server = Rainbows::Epoll::Server
9
+ IN = SleepyPenguin::Epoll::IN | SleepyPenguin::Epoll::ET
10
+ OUT = SleepyPenguin::Epoll::OUT | SleepyPenguin::Epoll::ET
11
+ KATO = {}
12
+ KATO.compare_by_identity if KATO.respond_to?(:compare_by_identity)
13
+ KEEPALIVE_TIMEOUT = Rainbows.keepalive_timeout
14
+ EP = Rainbows::Epoll::EP
15
+ @@last_expire = Time.now
16
+
17
+ def self.expire
18
+ return if ((now = Time.now) - @@last_expire) < 1.0
19
+ if (ot = KEEPALIVE_TIMEOUT) >= 0
20
+ ot = now - ot
21
+ KATO.delete_if { |client, time| time < ot and client.timeout! }
22
+ end
23
+ @@last_expire = now
24
+ end
25
+
26
+ # only call this once
27
+ def epoll_once
28
+ @wr_queue = [] # may contain String, ResponsePipe, and StreamFile objects
29
+ post_init
30
+ on_readable
31
+ rescue => e
32
+ handle_error(e)
33
+ end
34
+
35
+ def on_readable
36
+ case rv = kgio_tryread(16384, RBUF)
37
+ when String
38
+ on_read(rv)
39
+ return if @wr_queue[0] || closed?
40
+ when :wait_readable
41
+ KATO[self] = @@last_expire if :headers == @state
42
+ return EP.set(self, IN)
43
+ else
44
+ break
45
+ end until :close == @state
46
+ close unless closed?
47
+ rescue Errno::ECONNRESET
48
+ close
49
+ rescue IOError
50
+ end
51
+
52
+ def app_call input # called by on_read()
53
+ @env[RACK_INPUT] = input
54
+ @env[REMOTE_ADDR] = kgio_addr
55
+ status, headers, body = APP.call(@env.merge!(RACK_DEFAULTS))
56
+ ev_write_response(status, headers, body, @hp.next?)
57
+ end
58
+
59
+ def write_response_path(status, headers, body, alive)
60
+ io = body_to_io(body)
61
+ st = io.stat
62
+
63
+ if st.file?
64
+ defer_file(status, headers, body, alive, io, st)
65
+ elsif st.socket? || st.pipe?
66
+ chunk = stream_response_headers(status, headers, alive)
67
+ stream_response_body(body, io, chunk)
68
+ else
69
+ # char or block device... WTF?
70
+ write_response(status, headers, body, alive)
71
+ end
72
+ end
73
+
74
+ # used for streaming sockets and pipes
75
+ def stream_response_body(body, io, chunk)
76
+ pipe = (chunk ? Rainbows::Epoll::ResponseChunkPipe :
77
+ Rainbows::Epoll::ResponsePipe).new(io, self, body)
78
+ return @wr_queue << pipe if @wr_queue[0]
79
+ stream_pipe(pipe) or return
80
+ @wr_queue[0] or @wr_queue << Z
81
+ end
82
+
83
+ def ev_write_response(status, headers, body, alive)
84
+ if body.respond_to?(:to_path)
85
+ write_response_path(status, headers, body, alive)
86
+ else
87
+ write_response(status, headers, body, alive)
88
+ end
89
+ @state = alive ? :headers : :close
90
+ on_read(Z) if alive && 0 == @wr_queue.size && 0 != @buf.size
91
+ end
92
+
93
+ def epoll_run
94
+ if @wr_queue[0]
95
+ on_writable
96
+ else
97
+ KATO.delete self
98
+ on_readable
99
+ end
100
+ end
101
+
102
+ def want_more
103
+ Rainbows::Epoll::ReRun << self
104
+ end
105
+
106
+ def on_deferred_write_complete
107
+ :close == @state and return close
108
+ 0 == @buf.size ? on_readable : on_read(Z)
109
+ end
110
+
111
+ def handle_error(e)
112
+ msg = Rainbows::Error.response(e) and kgio_trywrite(msg) rescue nil
113
+ ensure
114
+ close
115
+ end
116
+
117
+ def write_deferred(obj)
118
+ Rainbows::StreamFile === obj ? stream_file(obj) : stream_pipe(obj)
119
+ end
120
+
121
+ # writes until our write buffer is empty or we block
122
+ # returns true if we're done writing everything
123
+ def on_writable
124
+ obj = @wr_queue.shift
125
+
126
+ case rv = String === obj ? kgio_trywrite(obj) : write_deferred(obj)
127
+ when nil
128
+ obj = @wr_queue.shift or return on_deferred_write_complete
129
+ when String
130
+ obj = rv # retry
131
+ when :wait_writable # Strings and StreamFiles only
132
+ @wr_queue.unshift(obj)
133
+ EP.set(self, OUT)
134
+ return
135
+ when :deferred
136
+ return
137
+ end while true
138
+ rescue => e
139
+ handle_error(e)
140
+ end
141
+
142
+ def write(buf)
143
+ unless @wr_queue[0]
144
+ case rv = kgio_trywrite(buf)
145
+ when nil
146
+ return # all written
147
+ when String
148
+ buf = rv # retry
149
+ when :wait_writable
150
+ @wr_queue << buf.dup # >3-word 1.9 strings are copy-on-write
151
+ return EP.set(self, OUT)
152
+ end while true
153
+ end
154
+ @wr_queue << buf.dup # >3-word 1.9 strings are copy-on-write
155
+ end
156
+
157
+ def close
158
+ @wr_queue.each { |x| x.respond_to?(:close) and x.close rescue nil }
159
+ super
160
+ on_close
161
+ end
162
+
163
+ def on_close
164
+ KATO.delete(self)
165
+ Server.decr
166
+ end
167
+
168
+ def timeout!
169
+ close
170
+ true
171
+ end
172
+
173
+ def defer_file(status, headers, body, alive, io, st)
174
+ if r = sendfile_range(status, headers)
175
+ status, headers, range = r
176
+ write_headers(status, headers, alive)
177
+ range and defer_file_stream(range[0], range[1], io, body)
178
+ else
179
+ write_headers(status, headers, alive)
180
+ defer_file_stream(0, st.size, io, body)
181
+ end
182
+ end
183
+
184
+ # returns +nil+ on EOF, :wait_writable if the client blocks
185
+ def stream_file(sf) # +sf+ is a Rainbows::StreamFile object
186
+ begin
187
+ sf.offset += (n = sendfile_nonblock(sf, sf.offset, sf.count))
188
+ 0 == (sf.count -= n) and return sf.close
189
+ rescue Errno::EAGAIN
190
+ return :wait_writable
191
+ rescue
192
+ sf.close
193
+ raise
194
+ end while true
195
+ end
196
+
197
+ def defer_file_stream(offset, count, io, body)
198
+ sf = Rainbows::StreamFile.new(offset, count, io, body)
199
+ unless @wr_queue[0]
200
+ stream_file(sf) or return
201
+ end
202
+ @wr_queue << sf
203
+ EP.set(self, OUT)
204
+ end
205
+
206
+ # this alternates between a push and pull model from the pipe -> client
207
+ # to avoid having too much data in userspace on either end.
208
+ def stream_pipe(pipe)
209
+ case buf = pipe.tryread
210
+ when String
211
+ write(buf)
212
+ if @wr_queue[0]
213
+ # client is blocked on write, client will pull from pipe later
214
+ EP.delete pipe
215
+ @wr_queue << pipe
216
+ EP.set(self, OUT)
217
+ return :deferred
218
+ end
219
+ # continue looping...
220
+ when :wait_readable
221
+ # pipe blocked on read, let the pipe push to the client in the future
222
+ EP.delete self
223
+ EP.set(pipe, IN)
224
+ return :deferred
225
+ else # nil => EOF
226
+ return pipe.close # nil
227
+ end while true
228
+ rescue => e
229
+ pipe.close
230
+ raise
231
+ end
232
+ end
@@ -0,0 +1,18 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ #
4
+ class Rainbows::Epoll::ResponseChunkPipe < Rainbows::Epoll::ResponsePipe
5
+ def tryread
6
+ @io or return
7
+
8
+ case rv = super
9
+ when String
10
+ "#{rv.size.to_s(16)}\r\n#{rv}\r\n"
11
+ when nil
12
+ close
13
+ "0\r\n\r\n"
14
+ else
15
+ rv
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ #
4
+ class Rainbows::Epoll::ResponsePipe
5
+ attr_reader :io
6
+ alias to_io io
7
+ RBUF = Rainbows::EvCore::RBUF
8
+ EP = Rainbows::Epoll::EP
9
+
10
+ def initialize(io, client, body)
11
+ @io, @client, @body = io, client, body
12
+ end
13
+
14
+ def epoll_run
15
+ return close if @client.closed?
16
+ @client.stream_pipe(self) or @client.on_deferred_write_complete
17
+ rescue => e
18
+ close
19
+ @client.handle_error(e)
20
+ end
21
+
22
+ def close
23
+ @io or return
24
+ EP.delete self
25
+ @body.respond_to?(:close) and @body.close
26
+ @io = @body = nil
27
+ end
28
+
29
+ def tryread
30
+ Kgio.tryread(@io, 16384, RBUF)
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ module Rainbows::Epoll::Server
4
+ @@nr = 0
5
+ Rainbows::Epoll.nr_clients = lambda { @@nr }
6
+ IN = SleepyPenguin::Epoll::IN | SleepyPenguin::Epoll::ET
7
+ MAX = Rainbows.server.worker_connections
8
+ THRESH = MAX - 1
9
+ LISTENERS = Rainbows::HttpServer::LISTENERS
10
+ EP = Rainbows::Epoll::EP
11
+
12
+ def self.run
13
+ LISTENERS.each { |sock| EP.add(sock.extend(self), IN) }
14
+ Rainbows::Epoll.loop
15
+ end
16
+
17
+ # rearms all listeners when there's a free slot
18
+ def self.decr
19
+ THRESH == (@@nr -= 1) and LISTENERS.each { |sock| EP.set(sock, IN) }
20
+ end
21
+
22
+ def epoll_run
23
+ return EP.delete(self) if @@nr >= MAX
24
+ while io = kgio_tryaccept
25
+ @@nr += 1
26
+ # there's a chance the client never even sees epoll for simple apps
27
+ io.epoll_once
28
+ return EP.delete(self) if @@nr >= MAX
29
+ end
30
+ end
31
+ end
@@ -7,13 +7,7 @@ module Rainbows::Error
7
7
  # if the socket is already closed or broken. We'll always ensure
8
8
  # the socket is closed at the end of this function
9
9
  def self.write(io, e)
10
- if msg = response(e)
11
- if io.respond_to?(:kgio_trywrite)
12
- io.kgio_trywrite(msg)
13
- else
14
- io.write_nonblock(msg)
15
- end
16
- end
10
+ msg = response(e) and Kgio.trywrite(io, msg)
17
11
  rescue
18
12
  end
19
13
 
@@ -35,8 +29,6 @@ module Rainbows::Error
35
29
  when EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL,
36
30
  Errno::EBADF, Errno::ENOTCONN
37
31
  # swallow error if client shuts down one end or disconnects
38
- when Rainbows::Response416
39
- Rainbows::Const::ERROR_416_RESPONSE
40
32
  when Unicorn::HttpParserError
41
33
  Rainbows::Const::ERROR_400_RESPONSE # try to tell the client they're bad
42
34
  when IOError # HttpParserError is an IOError