hatetepe 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.md +21 -16
  2. data/hatetepe.gemspec +1 -2
  3. data/lib/hatetepe/body.rb +5 -3
  4. data/lib/hatetepe/builder.rb +6 -3
  5. data/lib/hatetepe/cli.rb +27 -5
  6. data/lib/hatetepe/client.rb +157 -82
  7. data/lib/hatetepe/client/keep_alive.rb +58 -0
  8. data/lib/hatetepe/client/pipeline.rb +19 -0
  9. data/lib/hatetepe/connection.rb +42 -0
  10. data/lib/hatetepe/deferred_status_fix.rb +11 -0
  11. data/lib/hatetepe/message.rb +4 -4
  12. data/lib/hatetepe/parser.rb +3 -4
  13. data/lib/hatetepe/request.rb +19 -6
  14. data/lib/hatetepe/response.rb +11 -3
  15. data/lib/hatetepe/server.rb +115 -85
  16. data/lib/hatetepe/{app.rb → server/app.rb} +7 -2
  17. data/lib/hatetepe/server/keep_alive.rb +61 -0
  18. data/lib/hatetepe/server/pipeline.rb +24 -0
  19. data/lib/hatetepe/{proxy.rb → server/proxy.rb} +5 -11
  20. data/lib/hatetepe/version.rb +1 -1
  21. data/lib/rack/handler/hatetepe.rb +1 -4
  22. data/spec/integration/cli/start_spec.rb +75 -123
  23. data/spec/integration/client/keep_alive_spec.rb +74 -0
  24. data/spec/integration/server/keep_alive_spec.rb +99 -0
  25. data/spec/spec_helper.rb +41 -16
  26. data/spec/unit/app_spec.rb +16 -5
  27. data/spec/unit/builder_spec.rb +4 -4
  28. data/spec/unit/client/pipeline_spec.rb +40 -0
  29. data/spec/unit/client_spec.rb +355 -199
  30. data/spec/unit/connection_spec.rb +64 -0
  31. data/spec/unit/parser_spec.rb +3 -2
  32. data/spec/unit/proxy_spec.rb +9 -18
  33. data/spec/unit/rack_handler_spec.rb +2 -12
  34. data/spec/unit/server_spec.rb +154 -60
  35. metadata +31 -36
  36. data/.rspec +0 -1
  37. data/.travis.yml +0 -3
  38. data/.yardopts +0 -1
  39. data/lib/hatetepe/pipeline.rb +0 -27
data/README.md CHANGED
@@ -24,11 +24,13 @@ Using Hatetepe as your HTTP server is easy. Simply use the CLI that ships with
24
24
  the gem:
25
25
 
26
26
  $ hatetepe
27
+ We're in development
27
28
  Booting from /home/lars/workspace/hatetepe/config.ru
28
29
  Binding to 127.0.0.1:3000
29
30
 
30
31
  You can configure the network port and interface as well as the Rackup (.ru)
31
- file to be used. More help is available via the `hatetepe help` command.
32
+ file to be used and the RACK_ENV to run in. More help is available via the
33
+ `hatetepe help` command.
32
34
 
33
35
 
34
36
  Getting Started (Client)
@@ -37,7 +39,7 @@ Getting Started (Client)
37
39
  The `Hatetepe::Client` class can be used to make requests to an HTTP server.
38
40
 
39
41
  client = Hatetepe::Client.start(:host => "example.org", :port => 80)
40
- request = Hatetepe::Request.new("POST", "/search", {}, :q => "herp derp")
42
+ request = Hatetepe::Request.new(:post, "/search", {}, :q => "herp derp")
41
43
  client << request
42
44
  request.callback do |response|
43
45
  puts "Results:"
@@ -56,7 +58,7 @@ The `Hatetepe::Client` class can be used to make requests to an HTTP server.
56
58
  - `#headers`
57
59
  - `#body`
58
60
 
59
- `Request` also has `#to_hash` which will turn the object into something your
61
+ `Request` also has `#to_h` which will turn the object into something your
60
62
  app can respond to.
61
63
 
62
64
 
@@ -133,8 +135,8 @@ Sending and Receiving BLOBs
133
135
  ---------------------------
134
136
 
135
137
  Hatetepe provides a thin wrapper around StringIO that makes it easier to handle
136
- streaming of request and response bodies. That means your app will be `#call`ed as
137
- soon as all headers have arrived. It can then do stuff while it's still
138
+ streaming of request and response bodies. That means your app will be `#call`ed
139
+ as soon as all headers have arrived. It can then do stuff while it's still
138
140
  receiving body data. You might for example want to track upload progress.
139
141
 
140
142
  received = nil
@@ -177,13 +179,21 @@ License
177
179
  Hatetepe is subject to an MIT-style license (see LICENSE file).
178
180
 
179
181
 
180
- To Do and Ideas
181
- ---------------
182
+ Roadmap
183
+ -------
184
+
185
+ - 0.5.0
186
+ - Direct IO via EM.enable_proxy
187
+ - Encoding support (ref. [github.com/tmm1/http_parser.rb#1](https://github.com/tmm1/http_parser.rb/pull/1))
188
+ - Optimize for performance
189
+ - Propagate connection errors to the app
182
190
 
183
- - Proxy
191
+
192
+ Ideas
193
+ -----
194
+
195
+ - Support for rubygems-test
184
196
  - Code reloading
185
- - Keep-alive
186
- - Native file sending/receiving
187
197
  - Preforking
188
198
  - MVM support via Thread Pool
189
199
  - Support for SPDY
@@ -191,9 +201,4 @@ To Do and Ideas
191
201
  - Foreman support
192
202
  - Daemonizing and dropping privileges
193
203
  - Trailing headers
194
- - Propagating connection errors to the app
195
-
196
- - Fix http_parser.rb's parsing of chunked bodies
197
- - Does http_parser.rb recognize trailing headers?
198
- - Encoding support (see https://github.com/tmm1/http_parser.rb/pull/1)
199
- - Are there any good C libs for building HTTP messages?
204
+ - REPL for Server and Client
@@ -21,9 +21,8 @@ Gem::Specification.new do |s|
21
21
 
22
22
  s.add_development_dependency "rspec"
23
23
  s.add_development_dependency "fakefs"
24
- s.add_development_dependency "em-http-request", "~> 1.0"
25
24
 
26
- s.files = `git ls-files`.split("\n") - [".gitignore"]
25
+ s.files = `git ls-files`.split("\n") - [".gitignore", ".rspec", ".travis.yml", ".yardopts"]
27
26
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
27
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
29
28
  s.require_paths = ["lib"]
@@ -2,6 +2,8 @@ require "em-synchrony"
2
2
  require "eventmachine"
3
3
  require "stringio"
4
4
 
5
+ require "hatetepe/deferred_status_fix"
6
+
5
7
  module Hatetepe
6
8
  # Thin wrapper around StringIO for asynchronous body processing.
7
9
  class Body
@@ -158,9 +160,9 @@ module Hatetepe
158
160
  # The number of bytes written.
159
161
  def write(data)
160
162
  ret = io.write data
161
- Fiber.new do
162
- @receivers.each {|r| r.call data }
163
- end.resume
163
+ @receivers.each do |r|
164
+ Fiber.new { r.call data }.resume
165
+ end
164
166
  ret
165
167
  end
166
168
  end
@@ -1,3 +1,5 @@
1
+ require "rack/utils"
2
+
1
3
  module Hatetepe
2
4
  class BuilderError < StandardError; end
3
5
 
@@ -59,7 +61,7 @@ module Hatetepe
59
61
  end
60
62
 
61
63
  def request(req)
62
- request_line req[0], req[1]
64
+ request_line req[0], req[1], (req[4] || "1.1")
63
65
  headers req[2]
64
66
  body req[3] if req[3]
65
67
  complete
@@ -72,7 +74,7 @@ module Hatetepe
72
74
  end
73
75
 
74
76
  def response(res)
75
- response_line res[0]
77
+ response_line res[0], (res[3] || "1.1")
76
78
  headers res[1]
77
79
  body res[2] if res[2]
78
80
  complete
@@ -89,7 +91,8 @@ module Hatetepe
89
91
  end
90
92
 
91
93
  def header(name, value)
92
- raw_header "#{name}: #{value}"
94
+ value = String(value)
95
+ raw_header "#{name}: #{value}" unless value.empty?
93
96
  end
94
97
 
95
98
  def headers(hash)
@@ -1,7 +1,5 @@
1
1
  require "thor"
2
2
 
3
- require "hatetepe"
4
-
5
3
  module Hatetepe
6
4
  class CLI < Thor
7
5
  map "--version" => :version
@@ -11,20 +9,32 @@ module Hatetepe
11
9
 
12
10
  desc :version, "Print version information"
13
11
  def version
14
- say Rity::VERSION
12
+ require "hatetepe/version"
13
+ say Hatetepe::VERSION
15
14
  end
16
15
 
17
- desc :start, "Start an instance of Rity"
16
+ desc "[start]", "Start a server"
18
17
  method_option :bind, :aliases => "-b", :type => :string,
19
18
  :banner => "Bind to the specified TCP interface (default: 127.0.0.1)"
20
19
  method_option :port, :aliases => "-p", :type => :numeric,
21
20
  :banner => "Bind to the specified port (default: 3000)"
22
21
  method_option :rackup, :aliases => "-r", :type => :string,
23
22
  :banner => "Load specified rackup (.ru) file (default: config.ru)"
23
+ method_option :env, :aliases => "-e", :type => :string,
24
+ :banner => "Boot the app in the specified environment (default: development)"
25
+ method_option :timeout, :aliases => "-t", :type => :numeric,
26
+ :banner => "Time out connections after the specified admount of seconds (default: 1)"
24
27
  def start
28
+ require "hatetepe/server"
29
+
30
+ ENV["RACK_ENV"] = expand_env(options[:env]) || ENV["RACK_ENV"] || "development"
31
+ $stderr << "We're in #{ENV["RACK_ENV"]}\n"
32
+ $stderr.flush
33
+
25
34
  rackup = File.expand_path(options[:rackup] || "config.ru")
26
35
  $stderr << "Booting from #{rackup}\n"
27
36
  $stderr.flush
37
+
28
38
  app = Rack::Builder.parse_file(rackup)[0]
29
39
 
30
40
  EM.epoll
@@ -41,9 +51,21 @@ module Hatetepe
41
51
  :app => app,
42
52
  :errors => $stderr,
43
53
  :host => host,
44
- :port => port
54
+ :port => port,
55
+ :timeout => (options[:timeout] || 1)
45
56
  })
46
57
  end
47
58
  end
59
+
60
+ protected
61
+
62
+ def expand_env(env)
63
+ env &&= env.dup.downcase
64
+ case env
65
+ when /^dev(el(op)?)?$/ then "development"
66
+ when /^test(ing)?$/ then "testing"
67
+ else env
68
+ end
69
+ end
48
70
  end
49
71
  end
@@ -1,111 +1,186 @@
1
1
  require "em-synchrony"
2
2
  require "eventmachine"
3
+ require "rack"
3
4
  require "uri"
4
5
 
5
- require "hatetepe/body"
6
6
  require "hatetepe/builder"
7
+ require "hatetepe/connection"
8
+ require "hatetepe/deferred_status_fix"
7
9
  require "hatetepe/parser"
8
10
  require "hatetepe/request"
9
- require "hatetepe/response"
10
11
  require "hatetepe/version"
11
12
 
12
13
  module Hatetepe
13
- class Client < EM::Connection
14
- def self.start(config)
15
- EM.connect config[:host], config[:port], self, config
16
- end
14
+ class Client < Hatetepe::Connection; end
15
+ end
16
+
17
+ require "hatetepe/client/keep_alive"
18
+ require "hatetepe/client/pipeline"
19
+
20
+ class Hatetepe::Client
21
+ attr_reader :app, :config
22
+ attr_reader :parser, :builder
23
+ attr_reader :requests, :pending_transmission, :pending_response
24
+
25
+ def initialize(config)
26
+ @config = config
27
+ @parser, @builder = Hatetepe::Parser.new, Hatetepe::Builder.new
17
28
 
18
- def self.request(verb, uri, headers = {}, body = nil)
19
- uri = URI.parse(uri)
20
- client = start(:host => uri.host, :port => uri.port)
21
-
22
- headers["User-Agent"] ||= "hatetepe/#{VERSION}"
23
-
24
- Request.new(verb, uri.request_uri).tap do |req|
25
- req.headers = headers
26
- req.body = body || Body.new.tap {|b| b.close_write }
27
- client << req
28
- EM::Synchrony.sync req
29
- end.response
29
+ @requests = []
30
+ @pending_transmission, @pending_response = {}, {}
31
+
32
+ @app = Rack::Builder.new.tap do |b|
33
+ b.use KeepAlive
34
+ b.use Pipeline
35
+ b.run method(:send_request)
36
+ end.to_app
37
+
38
+ super
39
+ end
40
+
41
+ def post_init
42
+ parser.on_response << method(:receive_response)
43
+ # XXX check if the connection is still present
44
+ builder.on_write << method(:send_data)
45
+ #builder.on_write {|data| p "client >> #{data}" }
46
+
47
+ self.processing_enabled = true
48
+ end
49
+
50
+ def receive_data(data)
51
+ #p "client << #{data}"
52
+ parser << data
53
+ rescue => e
54
+ close_connection
55
+ raise e
56
+ end
57
+
58
+ def send_request(request)
59
+ id = request.object_id
60
+
61
+ request.headers.delete "X-Hatetepe-Single"
62
+ builder.request request.to_a
63
+ pending_transmission[id].succeed
64
+
65
+ pending_response[id] = EM::DefaultDeferrable.new
66
+ EM::Synchrony.sync pending_response[id]
67
+ ensure
68
+ pending_response.delete id
69
+ end
70
+
71
+ def receive_response(response)
72
+ requests.find {|req| !req.response }.tap do |req|
73
+ req.response = response
74
+ pending_response[req.object_id].succeed response
75
+ end
76
+ end
77
+
78
+ def <<(request)
79
+ request.connection = self
80
+ unless processing_enabled?
81
+ request.fail
82
+ return
30
83
  end
31
84
 
32
- class << self
33
- [:get, :head].each do |verb|
34
- define_method verb do |uri, headers = {}|
35
- request verb.to_s.upcase, uri, headers
36
- end
37
- end
38
- [:options, :post, :put, :delete, :trace, :connect].each do |verb|
39
- define_method verb do |uri, headers = {}, body = nil|
40
- request verb.to_s.upcase, uri, headers, body
85
+ requests << request
86
+
87
+ Fiber.new do
88
+ begin
89
+ pending_transmission[request.object_id] = EM::DefaultDeferrable.new
90
+
91
+ app.call(request).tap do |response|
92
+ request.response = response
93
+ # XXX check for response.nil?
94
+ status = (response && response.success?) ? :succeed : :fail
95
+ requests.delete(request).send status, response
41
96
  end
97
+ ensure
98
+ pending_transmission.delete request.object_id
42
99
  end
100
+ end.resume
101
+ end
102
+
103
+ def request(verb, uri, headers = {}, body = nil, http_version = "1.1")
104
+ headers["Host"] ||= "#{config[:host]}:#{config[:port]}"
105
+ headers["User-Agent"] ||= "hatetepe/#{Hatetepe::VERSION}"
106
+
107
+ body = wrap_body(body)
108
+ if headers["Content-Type"] == "application/x-www-form-urlencoded"
109
+ enum = Enumerator.new(body)
110
+ headers["Content-Length"] = enum.inject(0) {|a, e| a + e.length }
43
111
  end
44
112
 
45
- attr_reader :config
46
- attr_reader :requests, :parser, :builder
113
+ request = Hatetepe::Request.new(verb, uri, headers, body, http_version)
114
+ self << request
115
+ self.processing_enabled = false
116
+ EM::Synchrony.sync request
47
117
 
48
- def initialize(config)
49
- @config = config
50
- @requests = []
51
- @parser, @builder = Parser.new, Builder.new
52
- super
53
- end
118
+ request.response.body.close_write if request.verb == "HEAD"
54
119
 
55
- def post_init
56
- parser.on_response do |response|
57
- requests.find {|req| !req.response }.response = response
58
- end
59
-
60
- parser.on_headers do
61
- requests.reverse.find {|req| !!req.response }.tap do |req|
62
- req.response.body.source = self
63
- req.succeed req.response
64
- end
65
- end
66
-
67
- #builder.on_write {|chunk|
68
- # ap "-> #{chunk}"
69
- #}
70
- builder.on_write << method(:send_data)
120
+ request.response
121
+ end
122
+
123
+ def stop
124
+ unless requests.empty?
125
+ last_response = EM::Synchrony.sync(requests.last)
126
+ EM::Synchrony.sync last_response.body if last_response.body
71
127
  end
128
+ close_connection
129
+ end
130
+
131
+ def unbind
132
+ super
72
133
 
73
- def <<(request)
74
- request.headers["Host"] = "#{config[:host]}:#{config[:port]}"
75
-
76
- requests << request
77
- Fiber.new do
78
- builder.request_line request.verb, request.uri
79
-
80
- if request.headers["Content-Type"] == "application/x-www-form-urlencoded"
81
- if request.body.respond_to? :read
82
- request.headers["Content-Length"] = request.body.read.bytesize
83
- else
84
- request.headers["Content-Length"] = request.body.length
85
- end
134
+ EM.next_tick do
135
+ requests.each do |req|
136
+ # fail state triggers
137
+ req.object_id.tap do |id|
138
+ pending_transmission[id].fail if pending_transmission[id]
139
+ pending_response[id].fail if pending_response[id]
86
140
  end
87
- builder.headers request.headers
88
-
89
- b = request.body
90
- if Body === b || b.respond_to?(:each)
91
- builder.body b
92
- elsif b.respond_to? :read
93
- builder.body [b.read]
94
- else
95
- builder.body [b]
141
+ # fail reponse body if the response has already been started
142
+ if req.response
143
+ req.response.body.tap {|b| b.close_write unless b.closed_write? }
96
144
  end
97
-
98
- builder.complete
99
- end.resume
145
+ # XXX FiberError: dead fiber called because req already succeeded
146
+ # or failed, see github.com/eventmachine/eventmachine/issues/287
147
+ req.fail req.response
148
+ end
100
149
  end
101
-
102
- def receive_data(data)
103
- #ap "<- #{data}"
104
- parser << data
150
+ end
151
+
152
+ def wrap_body(body)
153
+ if body.respond_to? :each
154
+ body
155
+ elsif body.respond_to? :read
156
+ [body.read]
157
+ elsif body
158
+ [body]
159
+ else
160
+ []
161
+ end
162
+ end
163
+
164
+ class << self
165
+ def start(config)
166
+ EM.connect config[:host], config[:port], self, config
105
167
  end
106
168
 
107
- def stop
108
- close_connection
169
+ def request(verb, uri, headers = {}, body = nil)
170
+ uri = URI(uri)
171
+ client = start(:host => uri.host, :port => uri.port)
172
+
173
+ headers["X-Hatetepe-Single"] = true
174
+ client.request(verb, uri.request_uri, headers, body).tap do |*|
175
+ client.stop
176
+ end
177
+ end
178
+ end
179
+
180
+ [self, self.singleton_class].each do |cls|
181
+ [:get, :head, :post, :put, :delete,
182
+ :options, :trace, :connect].each do |verb|
183
+ cls.send(:define_method, verb) {|uri, *args| request verb, uri, *args }
109
184
  end
110
185
  end
111
186
  end