hatetepe 0.3.1 → 0.4.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 (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