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
@@ -3,7 +3,7 @@ require "rack"
3
3
 
4
4
  Rack::STREAMING = "Rack::STREAMING"
5
5
 
6
- module Hatetepe
6
+ class Hatetepe::Server
7
7
  ASYNC_RESPONSE = [-1, {}, []].freeze
8
8
 
9
9
  ERROR_RESPONSE = [500, {"Content-Type" => "text/html"},
@@ -39,7 +39,12 @@ module Hatetepe
39
39
 
40
40
  response = ASYNC_RESPONSE
41
41
  catch :async do
42
- response = app.call(env) rescue ERROR_RESPONSE
42
+ response = begin
43
+ app.call env
44
+ rescue => ex
45
+ raise ex if ENV["RACK_ENV"] == "testing"
46
+ ERROR_RESPONSE
47
+ end
43
48
  end
44
49
 
45
50
  postprocess env, response
@@ -0,0 +1,61 @@
1
+ class Hatetepe::Server
2
+ class KeepAlive
3
+ attr_reader :app
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ m = case env["HTTP_CONNECTION"].to_s.downcase
11
+ when "close" then :close
12
+ when "keep-alive" then :keep_alive
13
+ else env["HTTP_VERSION"] =~ /^HTTP\/(0\.9|1\.0)$/ ? :close : :keep_alive
14
+ end
15
+
16
+ send :"call_and_#{m}", env
17
+ end
18
+
19
+ def call_and_close(env, response = nil)
20
+ req, conn = extract_request(env), extract_connection(env)
21
+
22
+ conn.processing_enabled = false
23
+ req.callback &conn.method(:close_connection_after_writing)
24
+ req.errback &conn.method(:close_connection_after_writing)
25
+
26
+ if response
27
+ response[1]["Connection"] = "close"
28
+ else
29
+ stream_start = env["stream.start"]
30
+ env["stream.start"] = proc do |res|
31
+ res[1]["Connection"] = "close"
32
+ stream_start.call res
33
+ end
34
+ end
35
+
36
+ response || app.call(env)
37
+ end
38
+
39
+ def call_and_keep_alive(env)
40
+ stream_start = env["stream.start"]
41
+ env["stream.start"] = proc do |res|
42
+ if res[1]["Connection"] == "close"
43
+ call_and_close env, res
44
+ else
45
+ res[1]["Connection"] = "keep-alive"
46
+ end
47
+ stream_start.call res
48
+ end
49
+
50
+ app.call env
51
+ end
52
+
53
+ def extract_request(env)
54
+ env["hatetepe.request"] || raise("env[hatetepe.request] not set")
55
+ end
56
+
57
+ def extract_connection(env)
58
+ env["hatetepe.connection"] || raise("env[hatetepe.connection] not set")
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+ require "em-synchrony"
2
+
3
+ class Hatetepe::Server
4
+ # TODO move specs from server_spec.rb to server/pipeline_spec.rb
5
+ class Pipeline
6
+ attr_reader :app
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ previous = env["hatetepe.connection"].requests[-2]
14
+
15
+ stream_start = env["stream.start"]
16
+ env["stream.start"] = proc do |response|
17
+ EM::Synchrony.sync previous if previous
18
+ stream_start.call response
19
+ end
20
+
21
+ app.call env
22
+ end
23
+ end
24
+ end
@@ -2,7 +2,7 @@ require "hatetepe/client"
2
2
  require "hatetepe/request"
3
3
  require "uri"
4
4
 
5
- module Hatetepe
5
+ class Hatetepe::Server
6
6
  class Proxy
7
7
  attr_reader :app
8
8
 
@@ -23,7 +23,8 @@ module Hatetepe
23
23
 
24
24
  env["proxy.callback"] ||= env["async.callback"]
25
25
 
26
- cl = client || Client.start(:host => target.host, :port => target.port)
26
+ cl = client || Hatetepe::Client.start(:host => target.host,
27
+ :port => target.port)
27
28
  build_request(env, target).tap do |req|
28
29
  cl << req
29
30
  EM::Synchrony.sync req
@@ -40,15 +41,8 @@ module Hatetepe
40
41
 
41
42
  uri = target.path + base.uri
42
43
  host = "#{target.host}:#{target.port}"
43
- headers = base.headers.merge({
44
- "X-Forwarded-For" => env["REMOTE_ADDR"],
45
- "Host" => [base.headers["Host"], host].compact.join(", ")
46
- })
47
-
48
- Request.new(base.verb, uri, base.http_version).tap do |req|
49
- req.headers = headers
50
- req.body = base.body
51
- end
44
+ headers = base.headers.merge("Host" => host)
45
+ Hatetepe::Request.new base.verb, uri, headers, base.body
52
46
  end
53
47
  end
54
48
  end
@@ -1,3 +1,3 @@
1
1
  module Hatetepe
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -15,10 +15,7 @@ module Rack
15
15
  Signal.trap("TERM") { EM.stop }
16
16
 
17
17
  EM.epoll
18
- EM.synchrony do
19
- server = ::Hatetepe::Server.start options
20
- yield server if block_given?
21
- end
18
+ EM.synchrony { ::Hatetepe::Server.start options }
22
19
  end
23
20
 
24
21
  def self.valid_options
@@ -1,27 +1,18 @@
1
1
  require "spec_helper"
2
2
  require "hatetepe/cli"
3
- require "socket"
4
3
 
5
- describe "start command" do
6
- def hook_event_loop(&block)
7
- EM.spec_hooks << block
8
- end
9
-
10
- def add_stop_timer(timeout)
11
- hook_event_loop do
12
- EM.add_timer(timeout) { EM.stop }
13
- end
14
- end
15
-
4
+ describe "The `hatetepe start' command" do
16
5
  before do
17
- $stderr = StringIO.new ""
6
+ ENV.delete "RACK_ENV"
7
+
8
+ $stderr = StringIO.new
18
9
 
19
10
  FakeFS.activate!
20
- File.open("config.ru", "w") do |f|
21
- f.write %q{run proc {|e| [200, {"Content-Type" => "text/plain"}, [e["REQUEST_URI"]]] }}
11
+ File.open "config.ru", "w" do |f|
12
+ f.write 'run proc {|env| [200, {}, ["Hello world!"]] }'
22
13
  end
23
- File.open("config2.ru", "w") do |f|
24
- f.write %q{run proc {|e| [200, {"Content-Type" => "text/plain"}, ["config2.ru loaded"]] }}
14
+ File.open "config2.ru", "w" do |f|
15
+ f.write 'run proc {|env| [501, {}, ["Herp derp"]] }'
25
16
  end
26
17
  end
27
18
 
@@ -32,138 +23,99 @@ describe "start command" do
32
23
  FakeFS::FileSystem.clear
33
24
  end
34
25
 
35
- it "starts an instance of Rity" do
36
- add_stop_timer 0.05
37
- hook_event_loop do
38
- Socket.tcp("127.0.0.1", 3000) {|*| }
39
- end
40
- Hatetepe::CLI.start %w{}
41
-
42
- $stderr.string.should include("127.0.0.1:3000")
43
- end
44
-
45
- it "answers HTTP requests" do
46
- add_stop_timer 0.02
47
- hook_event_loop do
48
- request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
49
- response = EM::Synchrony.sync(request)
50
-
51
- response.response_header.status.should == 200
52
- response.response_header["CONTENT_TYPE"].should == "text/plain"
53
- response.response.should == "/"
54
- end
55
- Hatetepe::CLI.start %w{}
56
- end
57
-
58
- describe "--port option" do
59
- it "changes the listen port" do
60
- add_stop_timer 0.05
61
- hook_event_loop do
62
- Socket.tcp("127.0.0.1", 3001) {|*| }
26
+ describe "without options" do
27
+ it "starts a Hatetepe::Server with default options" do
28
+ command "" do
29
+ Socket.tcp("127.0.0.1", 3000) {|*| }
30
+ ENV["RACK_ENV"].should == "development"
31
+ $stderr.string.should include("config.ru", "127.0.0.1:3000", "development")
63
32
  end
64
- Hatetepe::CLI.start %w{--port=3001}
65
-
66
- $stderr.string.should include(":3001")
67
33
  end
68
34
 
69
- it "has an alias: -p" do
70
- add_stop_timer 0.05
71
- hook_event_loop do
72
- Socket.tcp("127.0.0.1", 3002) {|*| }
35
+ it "serves HTTP requests" do
36
+ command "" do
37
+ Hatetepe::Client.get("http://127.0.0.1:3000").tap do |response|
38
+ response.status.should equal(200)
39
+ response.body.read.should == "Hello world!"
40
+ end
73
41
  end
74
- Hatetepe::CLI.start %w{-p 3002}
75
-
76
- $stderr.string.should include(":3002")
77
42
  end
78
43
  end
79
44
 
80
- describe "--bind option" do
81
- it "changes the listen interface" do
82
- add_stop_timer 0.05
83
- hook_event_loop do
84
- Socket.tcp("127.0.0.2", 3000) {|*| }
85
- end
86
- Hatetepe::CLI.start %w{--bind=127.0.0.2}
87
-
88
- $stderr.string.should include("127.0.0.2:")
89
- end
90
-
91
- it "has an alias: -b" do
92
- add_stop_timer 0.05
93
- hook_event_loop do
94
- Socket.tcp("127.0.0.3", 3000) {|*| }
45
+ ["--port", "-p"].each do |opt|
46
+ describe "with #{opt} option" do
47
+ it "binds the Hatetepe::Server to the specified TCP port" do
48
+ command "#{opt} 3002" do
49
+ Socket.tcp("127.0.0.1", 3002) {|*| }
50
+ end
95
51
  end
96
- Hatetepe::CLI.start %w{-b 127.0.0.3}
97
-
98
- $stderr.string.should include("127.0.0.3:")
99
52
  end
100
53
  end
101
54
 
102
- describe "--rackup option" do
103
- it "changes the rackup file that'll be loaded" do
104
- add_stop_timer 0.05
105
- hook_event_loop do
106
- request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
107
- response = EM::Synchrony.sync(request)
108
- response.response.should include("config2.ru")
55
+ ["--bind", "-b"].each do |opt|
56
+ describe "with #{opt} option" do
57
+ it "binds the Hatetepe::Server to the specified TCP interface" do
58
+ command "#{opt} 127.0.0.2" do
59
+ Socket.tcp("127.0.0.2", 3000) {|*| }
60
+ end
109
61
  end
110
- Hatetepe::CLI.start %w{--rackup=config2.ru}
111
62
  end
112
-
113
- it "has an alias: -r" do
114
- add_stop_timer 0.05
115
- hook_event_loop do
116
- request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
117
- response = EM::Synchrony.sync(request)
118
- response.response.should include("config2.ru")
63
+ end
64
+
65
+ ["--rackup", "-r"].each do |opt|
66
+ describe "with #{opt} option" do
67
+ it "loads the specified rackup (.ru) file" do
68
+ command "#{opt} config2.ru" do
69
+ Hatetepe::Client.get("http://127.0.0.1:3000").tap do |response|
70
+ response.status.should equal(501)
71
+ response.body.read.should == "Herp derp"
72
+ end
73
+ end
119
74
  end
120
- Hatetepe::CLI.start %w{-r config2.ru}
121
75
  end
122
76
  end
123
77
 
124
- describe "--quiet option" do
125
- it "discards all output" do
126
- pending
127
-
128
- add_stop_timer 0.05
129
- Hatetepe::CLI.start %w{--quiet}
130
-
131
- $stderr.string.should be_empty
132
- end
133
-
134
- it "has an alias: -q" do
135
- pending
78
+ ["--env", "-e"].each do |opt|
79
+ describe "with #{opt} option" do
80
+ it "boots the app in the specified environment" do
81
+ command "#{opt} herpderp" do
82
+ ENV["RACK_ENV"].should == "herpderp"
83
+ end
84
+ end
136
85
 
137
- add_stop_timer 0.05
138
- Hatetepe::CLI.start %w{-q}
86
+ ["dev", "devel", "develop"].each do |value|
87
+ it "expands #{value} to `development'" do
88
+ command "#{opt} #{value}" do
89
+ ENV["RACK_ENV"].should == "development"
90
+ end
91
+ end
92
+ end
139
93
 
140
- $stderr.string.should be_empty
94
+ it "expands test to `testing'" do
95
+ command "#{opt} test" do
96
+ ENV["RACK_ENV"].should == "testing"
97
+ end
98
+ end
141
99
  end
142
100
  end
143
101
 
144
- describe "--verbose option" do
145
- it "prints debugging data" do
146
- pending
147
-
148
- add_stop_timer 0.05
149
- hook_event_loop do
150
- request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
102
+ ["--timeout", "-t"].each do |opt|
103
+ describe "with #{opt} option" do
104
+ let :client do
105
+ Hatetepe::Client.start :host => "127.0.0.1", :port => 3000
151
106
  end
152
- Hatetepe::CLI.start %w{--verbose}
153
107
 
154
- $stderr.string.split("\n").size.should > 10
155
- end
108
+ it "times out a connection after the specified amount of seconds" do
109
+ command "#{opt} 0.5", 1 do
110
+ client.should_not be_closed
156
111
 
157
- it "has an alias: -V" do
158
- pending
159
-
160
- add_stop_timer 0.05
161
- hook_event_loop do
162
- request = EM::HttpRequest.new("http://127.0.0.1:3000").aget
112
+ EM::Synchrony.sleep 0.45
113
+ client.should_not be_closed
114
+
115
+ EM::Synchrony.sleep 0.1
116
+ client.should be_closed_by_remote
117
+ end
163
118
  end
164
- Hatetepe::CLI.start %w{-V}
165
-
166
- $stderr.string.split("\n").size.should > 10
167
119
  end
168
120
  end
169
121
  end
@@ -0,0 +1,74 @@
1
+ require "hatetepe/cli"
2
+ require "hatetepe/client"
3
+ require "spec_helper"
4
+ require "stringio"
5
+ require "yaml"
6
+
7
+ describe Hatetepe::Client, "with Keep-Alive" do
8
+ before do
9
+ $stderr = StringIO.new
10
+
11
+ FakeFS.activate!
12
+ File.open "config.ru", "w" do |f|
13
+ f.write 'run proc {|env| [200,
14
+ {"Content_Type" => "text/plain"},
15
+ [env["HTTP_CONNECTION"]]] }'
16
+ end
17
+ File.open "config_close.ru", "w" do |f|
18
+ f.write 'run proc {|env| [200,
19
+ {"Content-Type" => "text/plain",
20
+ "Connection" => "close"},
21
+ [env["HTTP_CONNECTION"]]] }'
22
+ end
23
+ end
24
+
25
+ after do
26
+ $stderr = STDERR
27
+
28
+ FakeFS.deactivate!
29
+ FakeFS::FileSystem.clear
30
+ end
31
+
32
+ let :client do
33
+ Hatetepe::Client.start :host => "127.0.0.1", :port => 30001
34
+ end
35
+
36
+ it "keeps the connection open" do
37
+ command "-p 30001", 2 do
38
+ client
39
+ EM::Synchrony.sleep 1.95
40
+ client.should_not be_closed_by_self
41
+ end
42
+ end
43
+
44
+ it "sends Connection: keep-alive" do
45
+ command "-p 30001" do
46
+ client.get("/").body.read.should == "keep-alive"
47
+ end
48
+ end
49
+
50
+ describe "and an obviously single request" do
51
+ it "sends Connection: close" do
52
+ command "-p 30001" do
53
+ Hatetepe::Client.get "http://127.0.0.1:30001/" do |response|
54
+ YAML.load(response.body.read).should == "close"
55
+ end
56
+ end
57
+ end
58
+
59
+ it "closes the connection immediately after the response" do
60
+ command "-p 30001" do
61
+ #Hatetepe::Client.any_instance.should_receive :stop
62
+ Hatetepe::Client.get "http://127.0.0.1:30001/"
63
+ end
64
+ end
65
+ end
66
+
67
+ it "closes the connection if the server tells it to" do
68
+ #pending "Server can't send Conn: close as its Keep-Alive middleware overwrites it"
69
+ command "-p 30001 -r config_close.ru" do
70
+ client.get "/"
71
+ client.should be_closed
72
+ end
73
+ end
74
+ end