reel 0.0.2 → 0.1.0

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.

@@ -0,0 +1,4 @@
1
+ 0.1.0
2
+ -----
3
+
4
+ * First official release
data/README.md CHANGED
@@ -15,16 +15,17 @@ applications and provides traditional multithreaded blocking I/O support too.
15
15
  Connections to Reel can be either non-blocking and handled entirely within
16
16
  the Reel::Server thread, or the same connections can be dispatched to worker
17
17
  threads where they will perform ordinary blocking IO. Reel provides no
18
- built-in thread pool, however you can build one yourself using Celluloid::Pool,
18
+ built-in thread pool, however you can build one yourself using Celluloid.pool,
19
19
  or because Celluloid already pools threads to begin with, you can simply use
20
20
  an actor per connection.
21
21
 
22
22
  This gives you the best of both worlds: non-blocking I/O for when you're
23
23
  primarily I/O bound, and threads for where you're compute bound.
24
24
 
25
- ### Is It Good?
25
+ ### Is it any good?
26
26
 
27
- Yes, but it has room for improvement. A "hello world" web server benchmark,
27
+ [Yes](http://news.ycombinator.com/item?id=3067434),
28
+ but it has room for improvement. A "hello world" web server benchmark,
28
29
  run on a 2GHz i7 (OS X 10.7.3). All servers used in a single-threaded mode.
29
30
 
30
31
  Reel performance on various Ruby VMs:
@@ -7,7 +7,7 @@ require 'reel/version'
7
7
  require 'reel/connection'
8
8
  require 'reel/logger'
9
9
  require 'reel/request'
10
- require 'reel/request/parser'
10
+ require 'reel/request_parser'
11
11
  require 'reel/response'
12
12
  require 'reel/server'
13
13
 
@@ -1,7 +1,7 @@
1
1
  module Reel
2
2
  # A connection to the HTTP server
3
3
  class Connection
4
- class StateError < RuntimeError; end # wrong state for a given request
4
+ class StateError < RuntimeError; end # wrong state for a given operation
5
5
 
6
6
  attr_reader :request
7
7
 
@@ -12,7 +12,7 @@ module Reel
12
12
  @socket = socket
13
13
  @keepalive = true
14
14
  @parser = Request::Parser.new
15
- reset
15
+ reset_request
16
16
 
17
17
  @response_state = :header
18
18
  @body_remaining = nil
@@ -21,13 +21,19 @@ module Reel
21
21
  # Is the connection still active?
22
22
  def alive?; @keepalive; end
23
23
 
24
+ # Reset the current request state
25
+ def reset_request
26
+ @request_state = :header
27
+ @request = nil
28
+ @parser.reset
29
+ end
30
+
31
+ # Read a request object from the connection
24
32
  def read_request
25
33
  raise StateError, "can't read header" unless @request_state == :header
26
34
 
27
35
  begin
28
- until @parser.headers
29
- @parser << @socket.readpartial(BUFFER_SIZE)
30
- end
36
+ @parser << @socket.readpartial(BUFFER_SIZE) until @parser.headers
31
37
  rescue IOError, Errno::ECONNRESET, Errno::EPIPE
32
38
  @keepalive = false
33
39
  @socket.close unless @socket.closed?
@@ -37,8 +43,8 @@ module Reel
37
43
  @request_state = :body
38
44
 
39
45
  headers = {}
40
- @parser.headers.each do |header, value|
41
- headers[Http.canonicalize_header(header)] = value
46
+ @parser.headers.each do |field, value|
47
+ headers[Http.canonicalize_header(field)] = value
42
48
  end
43
49
 
44
50
  if headers['Connection']
@@ -51,6 +57,7 @@ module Reel
51
57
  @request = Request.new(@parser.http_method, @parser.url, @parser.http_version, headers, self)
52
58
  end
53
59
 
60
+ # Read a chunk from the request
54
61
  def readpartial(size = BUFFER_SIZE)
55
62
  if @body_remaining and @body_remaining > 0
56
63
  chunk = @parser.chunk
@@ -67,11 +74,22 @@ module Reel
67
74
  end
68
75
  end
69
76
 
70
- def respond(response, body = nil)
77
+ # Send a response back to the client
78
+ # Response can be a symbol indicating the status code or a Reel::Response
79
+ def respond(response, headers_or_body = {}, body = nil)
80
+ raise StateError "not in header state" if @response_state != :header
81
+
82
+ if headers_or_body.is_a? Hash
83
+ headers = headers_or_body
84
+ else
85
+ headers = {}
86
+ body = headers_or_body
87
+ end
88
+
71
89
  if @keepalive
72
- headers = {'Connection' => 'Keep-Alive'}
90
+ headers['Connection'] = 'Keep-Alive'
73
91
  else
74
- headers = {'Connection' => 'close'}
92
+ headers['Connection'] = 'close'
75
93
  end
76
94
 
77
95
  case response
@@ -82,12 +100,17 @@ module Reel
82
100
  end
83
101
 
84
102
  response.render(@socket)
103
+
104
+ # Enable streaming mode
105
+ if response.headers['Transfer-Encoding'] == "chunked" and response.body.nil?
106
+ @response_state = :chunked_body
107
+ end
85
108
  rescue IOError, Errno::ECONNRESET, Errno::EPIPE
86
109
  # The client disconnected early
87
110
  @keepalive = false
88
111
  ensure
89
112
  if @keepalive
90
- reset
113
+ reset_request
91
114
  @request_state = :header
92
115
  else
93
116
  @socket.close unless @socket.closed?
@@ -95,10 +118,26 @@ module Reel
95
118
  end
96
119
  end
97
120
 
98
- def reset
99
- @request_state = :header
100
- @request = nil
101
- @parser.reset
121
+ # Write body chunks directly to the connection
122
+ def write(chunk)
123
+ raise StateError, "not in chunked body mode" unless @response_state == :chunked_body
124
+ chunk_header = chunk.bytesize.to_s(16) + Response::CRLF
125
+ @socket << chunk_header
126
+ @socket << chunk
127
+ end
128
+ alias_method :<<, :write
129
+
130
+ # Finish the response and reset the response state to header
131
+ def finish_response
132
+ raise StateError, "not in body state" if @response_state != :chunked_body
133
+ @socket << "0" << Response::CRLF * 2
134
+ @response_state = :header
135
+ end
136
+
137
+ # Close the connection
138
+ def close
139
+ @keepalive = false
140
+ @socket.close
102
141
  end
103
142
  end
104
143
  end
@@ -1,5 +1,4 @@
1
1
  module Reel
2
- # Parses incoming HTTP requests
3
2
  class Request
4
3
  class Parser
5
4
  attr_reader :headers
@@ -6,17 +6,26 @@ module Reel
6
6
  CRLF = "\r\n"
7
7
 
8
8
  attr_reader :status # Status has a special setter to coerce symbol names
9
- attr_accessor :reason
9
+ attr_accessor :reason # Reason can be set explicitly if desired
10
+ attr_reader :headers, :body
10
11
 
11
12
  def initialize(status, body_or_headers = nil, body = nil)
12
13
  self.status = status
13
14
 
14
- if body_or_headers and not body
15
- @body = body_or_headers
16
- @headers = {}
17
- else
15
+ if body_or_headers.is_a?(Hash)
16
+ headers = body_or_headers
18
17
  @body = body
19
- @headers = body_or_headers
18
+ else
19
+ headers = {}
20
+ @body = body_or_headers
21
+ end
22
+
23
+ @headers = {}
24
+ headers.each do |name, value|
25
+ name = name.to_s
26
+ key = name[Http::CANONICAL_HEADER]
27
+ key ||= canonicalize_header(name)
28
+ @headers[key] = value.to_s
20
29
  end
21
30
 
22
31
  case @body
@@ -24,8 +33,15 @@ module Reel
24
33
  @headers['Content-Length'] ||= @body.bytesize
25
34
  when IO
26
35
  @headers['Content-Length'] ||= @body.stat.size
36
+ when Enumerable
37
+ @headers['Transfer-Encoding'] ||= 'chunked'
38
+ when NilClass
39
+ else raise ArgumentError, "can't render #{@body.class} as a response body"
27
40
  end
28
41
 
42
+ # Prevent modification through the accessor
43
+ @headers.freeze
44
+
29
45
  # FIXME: real HTTP versioning
30
46
  @version = "HTTP/1.1"
31
47
  end
@@ -59,6 +75,14 @@ module Reel
59
75
  while data = @body.read(4096)
60
76
  socket << data
61
77
  end
78
+ when Enumerable
79
+ @body.each do |chunk|
80
+ chunk_header = chunk.bytesize.to_s(16) + CRLF
81
+ socket << chunk_header
82
+ socket << chunk
83
+ end
84
+
85
+ socket << "0" << CRLF * 2
62
86
  end
63
87
  end
64
88
 
@@ -76,5 +100,11 @@ module Reel
76
100
  response_header << CRLF
77
101
  end
78
102
  private :render_header
103
+
104
+ # Transform to canonical HTTP header capitalization
105
+ def canonicalize_header(header)
106
+ header.to_s.split(/[\-_]/).map(&:capitalize).join('-')
107
+ end
108
+ private :canonicalize_header
79
109
  end
80
110
  end
@@ -1,3 +1,3 @@
1
1
  module Reel
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -52,6 +52,35 @@ describe Reel::Connection do
52
52
  end
53
53
  end
54
54
 
55
+ it "streams responses when transfer-encoding is chunked" do
56
+ with_socket_pair do |client, connection|
57
+ client << ExampleRequest.new.to_s
58
+ request = connection.read_request
59
+
60
+ # Sending transfer_encoding chunked without a body enables streaming mode
61
+ connection.respond :ok, :transfer_encoding => :chunked
62
+
63
+ # This will send individual chunks
64
+ connection << "Hello"
65
+ connection << "World"
66
+ connection.finish_response # Write trailer and reset connection to header mode
67
+ connection.close
68
+
69
+ response = ""
70
+
71
+ begin
72
+ while chunk = client.readpartial(4096)
73
+ response << chunk
74
+ end
75
+ rescue EOFError
76
+ end
77
+
78
+ crlf = "\r\n"
79
+ fixture = "5#{crlf}Hello5#{crlf}World0#{crlf*2}"
80
+ response[(response.length - fixture.length)..-1].should eq fixture
81
+ end
82
+ end
83
+
55
84
  def with_socket_pair
56
85
  host = '127.0.0.1'
57
86
  port = 10103
@@ -60,10 +89,12 @@ describe Reel::Connection do
60
89
  client = TCPSocket.new(host, port)
61
90
  peer = server.accept
62
91
 
63
- yield client, Reel::Connection.new(peer)
64
-
65
- server.close
66
- client.close
67
- peer.close
92
+ begin
93
+ yield client, Reel::Connection.new(peer)
94
+ ensure
95
+ server.close rescue nil
96
+ client.close rescue nil
97
+ peer.close rescue nil
98
+ end
68
99
  end
69
100
  end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe Reel::Response do
4
+ it "streams enumerables" do
5
+ with_socket_pair do |client, connection|
6
+ client << ExampleRequest.new.to_s
7
+ request = connection.read_request
8
+
9
+ connection.respond Reel::Response.new(:ok, ["Hello", "World"])
10
+
11
+ response = client.readpartial(4096)
12
+ crlf = "\r\n"
13
+ fixture = "5#{crlf}Hello5#{crlf}World0#{crlf*2}"
14
+ response[(response.length - fixture.length)..-1].should eq fixture
15
+ end
16
+ end
17
+
18
+ def with_socket_pair
19
+ host = '127.0.0.1'
20
+ port = 10103
21
+
22
+ server = TCPServer.new(host, port)
23
+ client = TCPSocket.new(host, port)
24
+ peer = server.accept
25
+
26
+ begin
27
+ yield client, Reel::Connection.new(peer)
28
+ ensure
29
+ server.close
30
+ client.close
31
+ peer.close
32
+ end
33
+ end
34
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-03 00:00:00.000000000 Z
12
+ date: 2012-07-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: celluloid-io
16
- requirement: &70171644061140 !ruby/object:Gem::Requirement
16
+ requirement: &70093521317700 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.8.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70171644061140
24
+ version_requirements: *70093521317700
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: http
27
- requirement: &70171644060640 !ruby/object:Gem::Requirement
27
+ requirement: &70093521316860 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.2.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70171644060640
35
+ version_requirements: *70093521316860
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: http_parser.rb
38
- requirement: &70171644060180 !ruby/object:Gem::Requirement
38
+ requirement: &70093521316180 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 0.5.3
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70171644060180
46
+ version_requirements: *70093521316180
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rake
49
- requirement: &70171644059800 !ruby/object:Gem::Requirement
49
+ requirement: &70093521315560 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70171644059800
57
+ version_requirements: *70093521315560
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rspec
60
- requirement: &70171644075700 !ruby/object:Gem::Requirement
60
+ requirement: &70093521314860 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,7 +65,7 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70171644075700
68
+ version_requirements: *70093521314860
69
69
  description: A Celluloid::IO-powered HTTP server
70
70
  email:
71
71
  - tony.arcieri@gmail.com
@@ -77,6 +77,7 @@ files:
77
77
  - .gitignore
78
78
  - .rspec
79
79
  - .travis.yml
80
+ - CHANGES.md
80
81
  - Gemfile
81
82
  - LICENSE.txt
82
83
  - README.md
@@ -91,7 +92,7 @@ files:
91
92
  - lib/reel/connection.rb
92
93
  - lib/reel/logger.rb
93
94
  - lib/reel/request.rb
94
- - lib/reel/request/parser.rb
95
+ - lib/reel/request_parser.rb
95
96
  - lib/reel/response.rb
96
97
  - lib/reel/server.rb
97
98
  - lib/reel/version.rb
@@ -99,6 +100,7 @@ files:
99
100
  - reel.gemspec
100
101
  - spec/fixtures/example.txt
101
102
  - spec/reel/connection_spec.rb
103
+ - spec/reel/response_spec.rb
102
104
  - spec/reel/server_spec.rb
103
105
  - spec/spec_helper.rb
104
106
  - tasks/rspec.rake
@@ -129,6 +131,7 @@ summary: A reel good HTTP server
129
131
  test_files:
130
132
  - spec/fixtures/example.txt
131
133
  - spec/reel/connection_spec.rb
134
+ - spec/reel/response_spec.rb
132
135
  - spec/reel/server_spec.rb
133
136
  - spec/spec_helper.rb
134
137
  has_rdoc: