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
@@ -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