reel 0.4.0.pre → 0.4.0.pre2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of reel might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2e1a822cd1a5dff03e573f0220f40e9c0990ebd7
4
- data.tar.gz: a560a182cfede95adaf56b9e7adadb51871cc0b2
3
+ metadata.gz: 4c139c2fa0dcd5e9456cb8867fbe296eddc1c9f6
4
+ data.tar.gz: 0ba97001f92ac212ba4184ba5059580e0e124245
5
5
  SHA512:
6
- metadata.gz: bf75fd111a422d070f574895cd2866e515e6645daab72f2bd654562af56419140826aaec4695f5f7a4c58d115ac4ae5025af74b2a3d4d06aead1134c9bb76ef9
7
- data.tar.gz: a126435e2b5c33a7256ad8ecd9e3aed5396b3a791a88d6dd643f134e9c06869b369a8bba52cb710e4a01e9bf556dc485421d55a1b45da5960c7beab9ee5921bf
6
+ metadata.gz: 02cfe9e2b456d5f6abb5c7810df4d6133ef5a1acfb49917de65f245d6bfadfe48c525b527ed060348ff8b306c071ac6714fa274c47aef693e08d79330aa639d9
7
+ data.tar.gz: a9ebba27cce7485bae2ae2236311d5fe43e59fc8e2d03c2d3b5a2f2ca4b728b6cccf430b6962d6abad639b614da846b1367c0880095cae2e420cbdf91bbefedf
data/CHANGES.md CHANGED
@@ -1,6 +1,14 @@
1
- HEAD
2
- ----
3
- * Remove on_error callback system
1
+ 0.4.0.pre2
2
+ ----------
3
+ * Pipelining support
4
+ * Reel::Connection#each_request for iterating through keep-alive requests
5
+ * Reel::Request#body now returns a Reel::RequestBody object instead of a String
6
+ * New WebSocket API: obtain WebSockets through Reel::Request#websocket instead
7
+ of through Reel::Connection#request. Allows processing of WebSockets through
8
+ other means than the built-in WebSocket support
9
+ * Allow Reel to stop cleanly
10
+ * Remove `on_error` callback system
11
+ * Increase buffer size
4
12
 
5
13
  0.3.0
6
14
  -----
data/Gemfile CHANGED
@@ -9,3 +9,7 @@ gem 'coveralls', require: false
9
9
  # Specify your gem's dependencies in reel.gemspec
10
10
  gemspec
11
11
 
12
+ group :development do
13
+ gem 'guard-rspec'
14
+ gem 'pry'
15
+ end
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
12
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
13
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
+ watch('config/routes.rb') { "spec/routing" }
15
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
+
17
+ # Capybara features specs
18
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
19
+
20
+ # Turnip features and steps
21
+ watch(%r{^spec/acceptance/(.+)\.feature$})
22
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
23
+ end
24
+
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![Code Climate](https://codeclimate.com/github/celluloid/reel.png)](https://codeclimate.com/github/celluloid/reel)
7
7
  [![Coverage Status](https://coveralls.io/repos/celluloid/reel/badge.png?branch=master)](https://coveralls.io/r/celluloid/reel)
8
8
 
9
- > "A dizzying lifetime... Reeling by on Celluloid" _-- Rush / Between The Wheels_
9
+ > "A dizzying lifetime... reeling by on Celluloid" _-- Rush / Between The Wheels_
10
10
 
11
11
  Reel is a fast, non-blocking "evented" web server built on [http_parser.rb][parser],
12
12
  [websocket_parser][websockets], [Celluloid::IO][celluloidio], and [nio4r][nio4r]. Thanks
@@ -16,7 +16,7 @@ traditional multithreaded blocking I/O support too.
16
16
  [parser]: https://github.com/tmm1/http_parser.rb
17
17
  [websockets]: https://github.com/afcapel/websocket_parser
18
18
  [celluloidio]: https://github.com/celluloid/celluloid-io
19
- [nio4r]: https://github.com/tarcieri/nio4r
19
+ [nio4r]: https://github.com/celluloid/nio4r
20
20
 
21
21
  Connections to Reel can be either non-blocking and handled entirely within
22
22
  the Reel::Server thread, or the same connections can be dispatched to worker
@@ -63,6 +63,8 @@ and are not amortized across all concurrent requests.
63
63
  API
64
64
  ---
65
65
 
66
+ *NOTE: these examples are for the Reel 0.4.0.pre2 API*
67
+
66
68
  Reel also provides a "bare metal" API which was used in the benchmarks above.
67
69
  Here are some examples:
68
70
 
@@ -74,16 +76,18 @@ Reel lets you pass a block to initialize which receives connections:
74
76
  require 'reel'
75
77
 
76
78
  Reel::Server.supervise("0.0.0.0", 3000) do |connection|
77
- while request = connection.request
78
- case request
79
- when Reel::Request
79
+ # Support multiple keep-alive requests per connection
80
+ connection.each_request do |request|
81
+ # WebSocket support
82
+ if request.websocket?
83
+ puts "Client made a WebSocket request to: #{request.url}"
84
+ websocket = request.websocket
85
+
86
+ websocket << "Hello everyone out there in WebSocket land"
87
+ websocket.close
88
+ else
80
89
  puts "Client requested: #{request.method} #{request.url}"
81
90
  request.respond :ok, "Hello, world!"
82
- when Reel::WebSocket
83
- puts "Client made a WebSocket request to: #{request.url}"
84
- request << "Hello everyone out there in WebSocket land"
85
- request.close
86
- break
87
91
  end
88
92
  end
89
93
  end
@@ -108,12 +112,11 @@ class MyServer < Reel::Server
108
112
  end
109
113
 
110
114
  def on_connection(connection)
111
- while request = connection.request
112
- case request
113
- when Reel::Request
114
- handle_request(request)
115
- when Reel::WebSocket
115
+ connection.each_request do |request|
116
+ if request.websocket?
116
117
  handle_websocket(request)
118
+ else
119
+ handle_request(request)
117
120
  end
118
121
  end
119
122
  end
@@ -133,6 +136,7 @@ MyServer.run
133
136
 
134
137
  Framework Adapters
135
138
  ------------------
139
+
136
140
  ### Rack
137
141
 
138
142
  Reel can be used as a standard Rack server via the "reel" command line
@@ -143,6 +147,12 @@ by Celluloid. In addition, the Rack specification mandates that request bodies
143
147
  are rewindable, which prevents streaming request bodies as the spec dictates
144
148
  they must be written to disk.
145
149
 
150
+ To run `.ru` file using Reel w/ 16 workers
151
+
152
+ ```
153
+ rackup -p 1234 -s reel config.ru -Enone -O "workers=16"
154
+ ```
155
+
146
156
  To really leverage Reel's capabilities, you must use Reel via its own API,
147
157
  or another Ruby library with direct Reel support.
148
158
 
@@ -173,7 +183,7 @@ MyApp = Webmachine::Application.new do |app|
173
183
  config.port = MYAPP_PORT
174
184
  config.adapter = :Reel
175
185
 
176
- # Optional: (WM master only) handler for incoming websockets
186
+ # Optional: handler for incoming websockets
177
187
  config.adapter_options[:websocket_handler] = proc do |websocket|
178
188
  # socket is a Reel::WebSocket
179
189
  socket << "hello, world"
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # Run with: bundle exec examples/hello_world.rb
3
+
4
+ require 'rubygems'
5
+ require 'bundler/setup'
6
+ require 'reel'
7
+
8
+ addr, port = '127.0.0.1', 4430
9
+ options = {
10
+ :cert => File.read(File.expand_path("../../spec/fixtures/server.crt", __FILE__)),
11
+ :key => File.read(File.expand_path("../../spec/fixtures/server.key", __FILE__))
12
+ }
13
+
14
+ puts "*** Starting server on #{addr}:#{port}"
15
+ Reel::SSLServer.supervise(addr, port, options) do |connection|
16
+ # To use keep-alive with Reel, use a while loop that repeatedly calls
17
+ # connection.request and consumes connection objects
18
+ while request = connection.request
19
+ # Ordinarily we'd route the request here, e.g.
20
+ # route request.url
21
+ connection.respond :ok, "hello, world!"
22
+ end
23
+
24
+ # Reel takes care of closing the connection
25
+ end
26
+
27
+ sleep
@@ -1 +1 @@
1
- rackup -s reel websocket.ru -Enone
1
+ rackup -s reel websocket.ru -Enone -O "workers=16"
@@ -48,12 +48,22 @@ class WebServer < Reel::Server
48
48
 
49
49
  def on_connection(connection)
50
50
  while request = connection.request
51
- case request
52
- when Reel::Request
53
- route_request connection, request
54
- when Reel::WebSocket
51
+ if request.websocket?
55
52
  info "Received a WebSocket connection"
56
- route_websocket request
53
+
54
+ # We're going to hand off this connection to another actor (TimeClient)
55
+ # However, initially Reel::Connections are "attached" to the
56
+ # Reel::Server actor, meaning that the server manages the connection
57
+ # lifecycle (e.g. error handling) for us.
58
+ #
59
+ # If we want to hand this connection off to another actor, we first
60
+ # need to detach it from the Reel::Server
61
+ connection.detach
62
+
63
+ route_websocket request.websocket
64
+ return
65
+ else
66
+ route_request connection, request
57
67
  end
58
68
  end
59
69
  end
@@ -83,6 +83,7 @@ module Rack
83
83
  options = options.inject({}) { |h, (k,v)| h[k.downcase] = v ; h }
84
84
  options[:rackup] = options[:config] if options[:config]
85
85
  options[:port] = options[:port].to_i if options[:port]
86
+ options[:workers] = options[:workers].to_i if options[:workers]
86
87
  options
87
88
  end
88
89
  end
@@ -11,10 +11,14 @@ require 'reel/version'
11
11
  require 'reel/mixins'
12
12
  require 'reel/connection'
13
13
  require 'reel/logger'
14
+ require 'reel/request_info'
14
15
  require 'reel/request'
16
+ require 'reel/request_body'
15
17
  require 'reel/request_parser'
16
18
  require 'reel/response'
19
+ require 'reel/response_writer'
17
20
  require 'reel/server'
21
+ require 'reel/ssl_server'
18
22
  require 'reel/websocket'
19
23
  require 'reel/stream'
20
24
 
@@ -29,9 +33,16 @@ module Reel
29
33
  # Error reading a request
30
34
  class RequestError < StandardError; end
31
35
 
32
- # Error occured performing IO on a socket
36
+ # Error occurred performing IO on a socket
33
37
  class SocketError < RequestError; end
34
38
 
39
+ # Error occurred when trying to use the socket after it was upgraded
40
+ class SocketUpgradedError < NilClass
41
+ def self.method_missing(m, *)
42
+ raise(Reel::RequestError, 'Reel::Connection#socket can not be used anymore as it was upgraded. Use Reel::WebSocket instance instead.')
43
+ end
44
+ end
45
+
35
46
  # Error occured during a WebSockets handshake
36
47
  class HandshakeError < RequestError; end
37
48
 
@@ -16,7 +16,7 @@ module Reel
16
16
  super()
17
17
  @server = Reel::Server.supervise(host, port) do |connection|
18
18
  while request = connection.request
19
- status, headers, body = call Rack::MockRequest.env_for(request.url, :method => request.method, :input => request.body)
19
+ status, headers, body = call Rack::MockRequest.env_for(request.url, :method => request.method, :input => request.body.to_s)
20
20
  response_klass = body.is_a?(Stream) ? StreamResponse : Response
21
21
  connection.respond(response_klass.new(status_symbol(status), headers, body))
22
22
  end
@@ -10,18 +10,20 @@ module Reel
10
10
  TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
11
11
  KEEP_ALIVE = 'Keep-Alive'.freeze
12
12
  CLOSE = 'close'.freeze
13
- CHUNKED = 'chunked'.freeze
14
13
 
15
14
  attr_reader :socket, :parser
16
15
 
17
16
  # Attempt to read this much data
18
17
  BUFFER_SIZE = 16384
18
+ attr_reader :buffer_size
19
19
 
20
- def initialize(socket)
20
+ def initialize(socket, buffer_size = nil)
21
21
  @attached = true
22
22
  @socket = socket
23
23
  @keepalive = true
24
- @parser = Request::Parser.new
24
+ @parser = Request::Parser.new(socket, self)
25
+ @writer = Response::Writer.new(socket, self)
26
+ @buffer_size = buffer_size.nil? ? BUFFER_SIZE : buffer_size
25
27
  reset_request
26
28
 
27
29
  @response_state = :header
@@ -40,26 +42,29 @@ module Reel
40
42
  end
41
43
 
42
44
  # Reset the current request state
43
- def reset_request(state = :header)
45
+ def reset_request(state = :ready)
44
46
  @request_state = state
45
- @header_buffer = "" # Buffer headers in case of an upgrade request
47
+ @current_request = nil
46
48
  @parser.reset
47
49
  end
48
50
 
51
+ def readpartial(size = @buffer_size)
52
+ raise StateError, "can't read in the '#{@request_state}' request state" unless @request_state == :ready
53
+ @parser.readpartial(size)
54
+ end
55
+
56
+ def current_request
57
+ @current_request
58
+ end
59
+
49
60
  # Read a request object from the connection
50
61
  def request
51
- return if @request_state == :websocket
52
- req = Request.read(self)
53
-
54
- case req
55
- when Request
56
- @request_state = :body
57
- @keepalive = false if req[CONNECTION] == CLOSE || req.version == HTTP_VERSION_1_0
58
- when WebSocket
59
- @request_state = @response_state = :websocket
60
- @socket = nil
61
- else raise "unexpected request type: #{req.class}"
62
- end
62
+ raise StateError, "already processing a request" if current_request
63
+
64
+ req = @parser.current_request
65
+ @request_state = :ready
66
+ @keepalive = false if req[CONNECTION] == CLOSE || req.version == HTTP_VERSION_1_0
67
+ @current_request = req
63
68
 
64
69
  req
65
70
  rescue IOError, Errno::ECONNRESET, Errno::EPIPE
@@ -69,45 +74,23 @@ module Reel
69
74
  nil
70
75
  end
71
76
 
72
- # Read a chunk from the request
73
- def readpartial(size = BUFFER_SIZE)
74
- raise StateError, "can't read in the `#{@request_state}' state" unless @request_state == :body
77
+ # Enumerate the requests from this connection, since we might receive
78
+ # many if the client is using keep-alive
79
+ def each_request
80
+ while req = request
81
+ yield req
75
82
 
76
- chunk = @parser.chunk
77
- unless chunk || @parser.finished?
78
- @parser << @socket.readpartial(size)
79
- chunk = @parser.chunk
83
+ # Websockets upgrade the connection to the Websocket protocol
84
+ # Once we have finished processing a Websocket, we can't handle
85
+ # additional requests
86
+ break if req.websocket?
80
87
  end
81
-
82
- chunk
83
- end
84
-
85
- # read length bytes from request body
86
- def read(length = nil, buffer = nil)
87
- raise ArgumentError, "negative length #{length} given" if length && length < 0
88
-
89
- return '' if length == 0
90
-
91
- res = buffer.nil? ? '' : buffer.clear
92
-
93
- chunk_size = length.nil? ? BUFFER_SIZE : length
94
- begin
95
- while chunk_size > 0
96
- chunk = readpartial(chunk_size)
97
- break unless chunk
98
- res << chunk
99
- chunk_size = length - res.length unless length.nil?
100
- end
101
- rescue EOFError
102
- end
103
-
104
- return length && res.length == 0 ? nil : res
105
88
  end
106
89
 
107
90
  # Send a response back to the client
108
91
  # Response can be a symbol indicating the status code or a Reel::Response
109
92
  def respond(response, headers_or_body = {}, body = nil)
110
- raise StateError "not in header state" if @response_state != :header
93
+ raise StateError, "not in header state" if @response_state != :header
111
94
 
112
95
  if headers_or_body.is_a? Hash
113
96
  headers = headers_or_body
@@ -129,10 +112,10 @@ module Reel
129
112
  else raise TypeError, "invalid response: #{response.inspect}"
130
113
  end
131
114
 
132
- response.render(@socket)
115
+ @writer.handle_response(response)
133
116
 
134
117
  # Enable streaming mode
135
- if response.headers[TRANSFER_ENCODING] == CHUNKED and response.body.nil?
118
+ if response.chunked? and response.body.nil?
136
119
  @response_state = :chunked_body
137
120
  end
138
121
  rescue IOError, Errno::ECONNRESET, Errno::EPIPE
@@ -140,7 +123,7 @@ module Reel
140
123
  @keepalive = false
141
124
  ensure
142
125
  if @keepalive
143
- reset_request(:header)
126
+ reset_request(:ready)
144
127
  else
145
128
  @socket.close unless @socket.closed?
146
129
  reset_request(:closed)
@@ -150,23 +133,43 @@ module Reel
150
133
  # Write body chunks directly to the connection
151
134
  def write(chunk)
152
135
  raise StateError, "not in chunked body mode" unless @response_state == :chunked_body
153
- chunk_header = chunk.bytesize.to_s(16)
154
- @socket << chunk_header + Response::CRLF
155
- @socket << chunk + Response::CRLF
136
+ @writer.write(chunk)
156
137
  end
157
138
  alias_method :<<, :write
158
139
 
159
140
  # Finish the response and reset the response state to header
160
141
  def finish_response
161
142
  raise StateError, "not in body state" if @response_state != :chunked_body
162
- @socket << "0#{Response::CRLF * 2}"
143
+ @writer.finish_response
163
144
  @response_state = :header
164
145
  end
165
146
 
166
147
  # Close the connection
167
148
  def close
149
+ raise StateError, "socket has been hijacked from this connection" unless @socket
150
+
168
151
  @keepalive = false
169
152
  @socket.close unless @socket.closed?
170
153
  end
154
+
155
+ # Hijack the socket from the connection
156
+ def hijack_socket
157
+ # FIXME: this doesn't do a great job of ensuring we can hijack the socket
158
+ # in its current state. Improve the state detection.
159
+ if @request_state != :ready && @response_state != :header
160
+ raise StateError, "connection is not in a hijackable state"
161
+ end
162
+
163
+ @request_state = @response_state = :hijacked
164
+ socket = @socket
165
+ @socket = nil
166
+ socket
167
+ end
168
+
169
+ # Raw access to the underlying socket
170
+ def socket
171
+ raise StateError, "socket has already been hijacked" unless @socket
172
+ @socket
173
+ end
171
174
  end
172
175
  end