hatetepe 0.4.1 → 0.5.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/examples/parallel_requests.rb +32 -0
- data/hatetepe.gemspec +3 -4
- data/lib/hatetepe/builder.rb +1 -0
- data/lib/hatetepe/cli.rb +11 -20
- data/lib/hatetepe/client.rb +210 -181
- data/lib/hatetepe/connection.rb +31 -12
- data/lib/hatetepe/server/keep_alive.rb +15 -53
- data/lib/hatetepe/server/pipeline.rb +14 -18
- data/lib/hatetepe/server/rack_app.rb +39 -0
- data/lib/hatetepe/server.rb +60 -108
- data/lib/hatetepe/version.rb +1 -1
- data/lib/rack/handler/hatetepe.rb +2 -0
- data/spec/integration/cli/start_spec.rb +84 -92
- data/spec/integration/client/keep_alive_spec.rb +5 -56
- data/spec/integration/client/timeout_spec.rb +93 -0
- data/spec/integration/server/keep_alive_spec.rb +8 -80
- data/spec/integration/server/timeout_spec.rb +45 -0
- data/spec/spec_helper.rb +7 -59
- data/spec/unit/client_spec.rb +68 -363
- data/spec/unit/connection_spec.rb +5 -7
- data/spec/unit/server_spec.rb +108 -338
- metadata +58 -43
- data/lib/hatetepe/client/keep_alive.rb +0 -32
- data/lib/hatetepe/client/pipeline.rb +0 -19
- data/lib/hatetepe/server/app.rb +0 -85
- data/lib/hatetepe/server/proxy.rb +0 -48
- data/spec/unit/app_spec.rb +0 -125
- data/spec/unit/client/pipeline_spec.rb +0 -40
- data/spec/unit/proxy_spec.rb +0 -145
@@ -1,32 +0,0 @@
|
|
1
|
-
class Hatetepe::Client
|
2
|
-
class KeepAlive
|
3
|
-
attr_reader :app
|
4
|
-
|
5
|
-
def initialize(app)
|
6
|
-
@app = app
|
7
|
-
end
|
8
|
-
|
9
|
-
# XXX should we be explicit about Connection: keep-alive?
|
10
|
-
# i think it doesn't matter if we send it as we don't wait
|
11
|
-
# for the first response to see if we're talking to an HTTP/1.1
|
12
|
-
# server. we're sending more requests anyway.
|
13
|
-
def call(request)
|
14
|
-
req, conn = request, request.connection
|
15
|
-
|
16
|
-
single = req.headers.delete("X-Hatetepe-Single")
|
17
|
-
req.headers["Connection"] = "close" if single
|
18
|
-
|
19
|
-
req.headers["Connection"] ||= "keep-alive"
|
20
|
-
close = req.headers["Connection"] == "close"
|
21
|
-
|
22
|
-
conn.processing_enabled = false if close
|
23
|
-
|
24
|
-
app.call(request).tap do |res|
|
25
|
-
if !single && (close || res.headers["Connection"] == "close")
|
26
|
-
conn.processing_enabled = false
|
27
|
-
conn.stop
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
require "em-synchrony"
|
2
|
-
|
3
|
-
class Hatetepe::Client
|
4
|
-
class Pipeline
|
5
|
-
attr_reader :app
|
6
|
-
|
7
|
-
def initialize(app)
|
8
|
-
@app = app
|
9
|
-
end
|
10
|
-
|
11
|
-
def call(request)
|
12
|
-
previous = request.connection.requests[-2]
|
13
|
-
lock = request.connection.pending_transmission[previous.object_id]
|
14
|
-
EM::Synchrony.sync lock if previous != request && lock
|
15
|
-
|
16
|
-
app.call request
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
data/lib/hatetepe/server/app.rb
DELETED
@@ -1,85 +0,0 @@
|
|
1
|
-
require "async-rack"
|
2
|
-
require "rack"
|
3
|
-
|
4
|
-
Rack::STREAMING = "Rack::STREAMING"
|
5
|
-
|
6
|
-
class Hatetepe::Server
|
7
|
-
ASYNC_RESPONSE = [-1, {}, []].freeze
|
8
|
-
|
9
|
-
ERROR_RESPONSE = [500, {"Content-Type" => "text/html"},
|
10
|
-
["Internal Server Error"]].freeze
|
11
|
-
|
12
|
-
# Interface between Rack-compatible applications and Hatetepe's server.
|
13
|
-
# Provides support for both synchronous and asynchronous responses.
|
14
|
-
class App
|
15
|
-
attr_reader :app
|
16
|
-
|
17
|
-
# Initializes a new App object.
|
18
|
-
#
|
19
|
-
# @param [#call] app
|
20
|
-
# The Rack app.
|
21
|
-
#
|
22
|
-
def initialize(app)
|
23
|
-
@app = app
|
24
|
-
end
|
25
|
-
|
26
|
-
# Processes the request.
|
27
|
-
#
|
28
|
-
# Will call #postprocess with the Rack app's response. Catches :async
|
29
|
-
# as an additional indicator for an asynchronous response. Uses a standard
|
30
|
-
# 500 response if the Rack app raises an error.
|
31
|
-
#
|
32
|
-
# @param [Hash] env
|
33
|
-
# The Rack environment.
|
34
|
-
#
|
35
|
-
def call(env)
|
36
|
-
env["async.callback"] = proc do |response|
|
37
|
-
postprocess env, response
|
38
|
-
end
|
39
|
-
|
40
|
-
response = ASYNC_RESPONSE
|
41
|
-
catch :async do
|
42
|
-
response = begin
|
43
|
-
app.call env
|
44
|
-
rescue => ex
|
45
|
-
raise ex if ENV["RACK_ENV"] == "testing"
|
46
|
-
ERROR_RESPONSE
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
postprocess env, response
|
51
|
-
end
|
52
|
-
|
53
|
-
# Sends the response.
|
54
|
-
#
|
55
|
-
# Does nothing if response status is indicating an asynchronous response.
|
56
|
-
# This is the case if the response +Array+'s first element equals -1.
|
57
|
-
# Otherwise it will start sending the response (status and headers).
|
58
|
-
#
|
59
|
-
# If the body indicates streaming it will return after sending the status
|
60
|
-
# and headers. This happens if the body equals +Rack::STREAMING+ or isn't
|
61
|
-
# set. Otherwise it sends each body chunk and then closes the response
|
62
|
-
# stream.
|
63
|
-
#
|
64
|
-
# Sending an empty body is as simple as passing an object that responds to
|
65
|
-
# +each+ but doesn't actually yield anything.
|
66
|
-
#
|
67
|
-
# @param [Hash] env
|
68
|
-
# The Rack environment.
|
69
|
-
# @param [Array] response
|
70
|
-
# An array of 1..3 length containing the status, headers, body.
|
71
|
-
#
|
72
|
-
def postprocess(env, response)
|
73
|
-
return if response[0] == ASYNC_RESPONSE[0]
|
74
|
-
|
75
|
-
env["stream.start"].call response[0..1]
|
76
|
-
return if !response[2] || response[2] == Rack::STREAMING
|
77
|
-
|
78
|
-
begin
|
79
|
-
response[2].each &env["stream.send"]
|
80
|
-
ensure
|
81
|
-
env["stream.close"].call
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
@@ -1,48 +0,0 @@
|
|
1
|
-
require "hatetepe/client"
|
2
|
-
require "hatetepe/request"
|
3
|
-
require "uri"
|
4
|
-
|
5
|
-
class Hatetepe::Server
|
6
|
-
class Proxy
|
7
|
-
attr_reader :app
|
8
|
-
|
9
|
-
def initialize(app)
|
10
|
-
@app = app
|
11
|
-
end
|
12
|
-
|
13
|
-
def call(env)
|
14
|
-
env["proxy.start"] = proc do |target, client = nil|
|
15
|
-
start env, target, client
|
16
|
-
end
|
17
|
-
app.call env
|
18
|
-
end
|
19
|
-
|
20
|
-
def start(env, target, client)
|
21
|
-
target = URI.parse(target)
|
22
|
-
env.delete "proxy.start"
|
23
|
-
|
24
|
-
env["proxy.callback"] ||= env["async.callback"]
|
25
|
-
|
26
|
-
cl = client || Hatetepe::Client.start(:host => target.host,
|
27
|
-
:port => target.port)
|
28
|
-
build_request(env, target).tap do |req|
|
29
|
-
cl << req
|
30
|
-
EM::Synchrony.sync req
|
31
|
-
req.response.body.callback { cl.stop } unless client
|
32
|
-
env["proxy.callback"].call req.response
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# TODO use only +env+ to build the request
|
37
|
-
def build_request(env, target)
|
38
|
-
unless base = env["hatetepe.request"]
|
39
|
-
raise ArgumentError, "Proxying requires env[hatetepe.request] to be set"
|
40
|
-
end
|
41
|
-
|
42
|
-
uri = target.path + base.uri
|
43
|
-
host = "#{target.host}:#{target.port}"
|
44
|
-
headers = base.headers.merge("Host" => host)
|
45
|
-
Hatetepe::Request.new base.verb, uri, headers, base.body
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
data/spec/unit/app_spec.rb
DELETED
@@ -1,125 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
require "hatetepe/server"
|
3
|
-
|
4
|
-
describe Hatetepe::Server::App do
|
5
|
-
let(:inner_app) { stub "inner app", :call => response }
|
6
|
-
let(:app) { Hatetepe::Server::App.new inner_app }
|
7
|
-
let(:env) {
|
8
|
-
{
|
9
|
-
"stream.start" => proc {},
|
10
|
-
"stream.send" => proc {},
|
11
|
-
"stream.close" => proc {}
|
12
|
-
}
|
13
|
-
}
|
14
|
-
|
15
|
-
let(:status) { 123 }
|
16
|
-
let(:headers) { stub "headers" }
|
17
|
-
let(:body) { [stub("chunk#1"), stub("chunk#2")] }
|
18
|
-
let(:response) { [status, headers, body] }
|
19
|
-
|
20
|
-
context "#initialize(inner_app)" do
|
21
|
-
it "keeps the inner app" do
|
22
|
-
Hatetepe::Server::App.new(inner_app).app.should equal(inner_app)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
context "#call(env)" do
|
27
|
-
it "sets env[async.callback] before #call'ing inner_app" do
|
28
|
-
app.call env
|
29
|
-
|
30
|
-
app.should_receive(:postprocess) {|e, res|
|
31
|
-
e.should equal(env)
|
32
|
-
res.should equal(response)
|
33
|
-
}
|
34
|
-
env["async.callback"].call response
|
35
|
-
end
|
36
|
-
|
37
|
-
it "calls #postprocess with the return of inner_app#call(env)" do
|
38
|
-
inner_app.stub :call => response
|
39
|
-
app.should_receive(:postprocess) {|e, res|
|
40
|
-
e.should equal(env)
|
41
|
-
res.should equal(response)
|
42
|
-
}
|
43
|
-
|
44
|
-
app.call env
|
45
|
-
end
|
46
|
-
|
47
|
-
let(:error_response) {
|
48
|
-
[500, {"Content-Type" => "text/html"}, ["Internal Server Error"]]
|
49
|
-
}
|
50
|
-
|
51
|
-
it "responds with 500 when catching an error" do
|
52
|
-
ENV.delete "RACK_ENV"
|
53
|
-
|
54
|
-
inner_app.stub(:call) { raise }
|
55
|
-
app.should_receive(:postprocess) {|e, res|
|
56
|
-
res.should == error_response
|
57
|
-
}
|
58
|
-
|
59
|
-
app.call env
|
60
|
-
end
|
61
|
-
|
62
|
-
describe "if server's :env option is testing" do
|
63
|
-
let(:error) { StandardError.new }
|
64
|
-
|
65
|
-
it "doesn't catch errors" do
|
66
|
-
inner_app.stub(:call) { raise error }
|
67
|
-
expect { app.call env }.to raise_error(error)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
let(:async_response) { [-1, {}, []] }
|
72
|
-
|
73
|
-
it "catches :async for Thin compatibility" do
|
74
|
-
inner_app.stub(:call) { throw :async }
|
75
|
-
app.should_receive(:postprocess) {|e, res|
|
76
|
-
res.should == async_response
|
77
|
-
}
|
78
|
-
|
79
|
-
app.call env
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
context "#postprocess(env, response)" do
|
84
|
-
it "does nothing if the response status is lighter than 0" do
|
85
|
-
env["stream.start"].should_not_receive :call
|
86
|
-
app.postprocess env, [-1]
|
87
|
-
end
|
88
|
-
|
89
|
-
it "starts the response stream" do
|
90
|
-
env["stream.start"].should_receive(:call).with([status, headers])
|
91
|
-
app.postprocess env, [status, headers, []]
|
92
|
-
end
|
93
|
-
|
94
|
-
it "streams the body" do
|
95
|
-
body.should_receive :each do |&blk|
|
96
|
-
blk.should equal(env["stream.send"])
|
97
|
-
end
|
98
|
-
app.postprocess env, [status, headers, body]
|
99
|
-
end
|
100
|
-
|
101
|
-
it "doesn't stream the body if it equals Rack::STREAMING" do
|
102
|
-
body.should_not_receive :each
|
103
|
-
app.postprocess env, [status, headers, Rack::STREAMING]
|
104
|
-
end
|
105
|
-
|
106
|
-
it "doesn't try to stream a body that isn't set" do
|
107
|
-
body.should_not_receive :each
|
108
|
-
app.postprocess env, [status, headers]
|
109
|
-
end
|
110
|
-
|
111
|
-
it "closes the response stream after streaming the body" do
|
112
|
-
env["stream.close"].should_receive :call
|
113
|
-
app.postprocess env, [status, headers, body]
|
114
|
-
end
|
115
|
-
|
116
|
-
it "closes the response even if streaming the body fails" do
|
117
|
-
body.should_receive(:each).and_raise
|
118
|
-
env["stream.close"].should_receive :call
|
119
|
-
|
120
|
-
proc {
|
121
|
-
app.postprocess env, [status, headers, body]
|
122
|
-
}.should raise_error
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
@@ -1,40 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
require "hatetepe/client"
|
3
|
-
|
4
|
-
describe Hatetepe::Client::Pipeline do
|
5
|
-
let(:app) { stub "app", :call => nil }
|
6
|
-
let(:pipeline) { Hatetepe::Client::Pipeline.new app }
|
7
|
-
|
8
|
-
describe "#initialize(app)" do
|
9
|
-
it "sets the app" do
|
10
|
-
pipeline.app.should equal(app)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
let(:requests) {
|
15
|
-
[stub("previous_request"), stub("request")]
|
16
|
-
}
|
17
|
-
let(:lock) { stub "lock" }
|
18
|
-
let(:pending) { {requests.first.object_id => lock} }
|
19
|
-
let(:client) do
|
20
|
-
stub "client", :requests => requests, :pending_transmission => pending
|
21
|
-
end
|
22
|
-
let(:response) { stub "response" }
|
23
|
-
|
24
|
-
before do
|
25
|
-
requests.last.stub :connection => client
|
26
|
-
EM::Synchrony.stub :sync
|
27
|
-
end
|
28
|
-
|
29
|
-
describe "#call(request)" do
|
30
|
-
it "waits until the previous request has been transmitted" do
|
31
|
-
EM::Synchrony.should_receive(:sync).with lock
|
32
|
-
pipeline.call requests.last
|
33
|
-
end
|
34
|
-
|
35
|
-
it "calls the app" do
|
36
|
-
app.should_receive(:call).with(requests.last) { response }
|
37
|
-
pipeline.call(requests.last).should equal(response)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
data/spec/unit/proxy_spec.rb
DELETED
@@ -1,145 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
require "hatetepe/server"
|
3
|
-
|
4
|
-
describe Hatetepe::Server::Proxy do
|
5
|
-
let(:app) { stub "app" }
|
6
|
-
|
7
|
-
describe "#initialize(app)" do
|
8
|
-
it "sets the app" do
|
9
|
-
Hatetepe::Server::Proxy.new(app).app.should equal(app)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
let(:proxy) { Hatetepe::Server::Proxy.new app }
|
14
|
-
let(:target) { stub "target" }
|
15
|
-
let(:env) { {} }
|
16
|
-
let(:client) { stub "client", :<< => nil }
|
17
|
-
|
18
|
-
describe "#call(env)" do
|
19
|
-
it "sets env[proxy.start]" do
|
20
|
-
app.stub :call do |env|
|
21
|
-
env["proxy.start"].should respond_to(:call)
|
22
|
-
end
|
23
|
-
proxy.call env
|
24
|
-
end
|
25
|
-
|
26
|
-
let(:response) { stub "response" }
|
27
|
-
|
28
|
-
it "calls the app" do
|
29
|
-
app.should_receive(:call).with(env) { response }
|
30
|
-
proxy.call(env).should equal(response)
|
31
|
-
end
|
32
|
-
|
33
|
-
describe "env[proxy.start]" do
|
34
|
-
it "forwards to #start" do
|
35
|
-
proxy.should_receive(:start).with(env, target, client)
|
36
|
-
app.stub :call do |env|
|
37
|
-
env["proxy.start"].call target, client
|
38
|
-
end
|
39
|
-
proxy.call env
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
describe "#start(env, target, client)" do
|
45
|
-
let(:request) { stub "request" }
|
46
|
-
let(:response) { stub "response" }
|
47
|
-
let(:callback) { stub "async.callback", :call => nil }
|
48
|
-
|
49
|
-
let(:host) { stub "host" }
|
50
|
-
let(:port) { stub "port" }
|
51
|
-
let(:target) { stub "target", :host => host, :port => port }
|
52
|
-
|
53
|
-
before do
|
54
|
-
URI.stub :parse => target
|
55
|
-
proxy.stub :build_request => request
|
56
|
-
|
57
|
-
request.stub :dup => request, :response => response
|
58
|
-
request.extend EM::Deferrable
|
59
|
-
env["async.callback"] = callback
|
60
|
-
end
|
61
|
-
|
62
|
-
it "deletes env[proxy.start] from the env hash" do
|
63
|
-
env.should_receive(:delete).with "proxy.start"
|
64
|
-
Fiber.new { proxy.start env, target, client }.resume
|
65
|
-
end
|
66
|
-
|
67
|
-
it "defaults env[proxy.callback] to env[async.callback]" do
|
68
|
-
Fiber.new { proxy.start env, target, client }.resume
|
69
|
-
env["proxy.callback"].should equal(env["async.callback"])
|
70
|
-
end
|
71
|
-
|
72
|
-
let(:new_client) { stub "new client" }
|
73
|
-
|
74
|
-
it "starts a client if none was passed" do
|
75
|
-
Hatetepe::Client.stub :start do |config|
|
76
|
-
config[:host].should equal(host)
|
77
|
-
config[:port].should equal(port)
|
78
|
-
new_client
|
79
|
-
end
|
80
|
-
new_client.should_receive(:<<).with request
|
81
|
-
Fiber.new { proxy.start env, target, nil }.resume
|
82
|
-
end
|
83
|
-
|
84
|
-
it "doesn't stop a client that was passed" do
|
85
|
-
client.should_not_receive :stop
|
86
|
-
Fiber.new { proxy.start env, target, client }.resume
|
87
|
-
request.succeed
|
88
|
-
end
|
89
|
-
|
90
|
-
it "passes the request to the client" do
|
91
|
-
proxy.should_receive :build_request do |e, t|
|
92
|
-
env.should equal(e)
|
93
|
-
target.should equal(t)
|
94
|
-
request
|
95
|
-
end
|
96
|
-
client.should_receive(:<<).with request
|
97
|
-
Fiber.new { proxy.start env, target, client }.resume
|
98
|
-
end
|
99
|
-
|
100
|
-
it "passes the response to env[async.callback]" do
|
101
|
-
callback.should_receive(:call).with response
|
102
|
-
Fiber.new { proxy.start env, target, client }.resume
|
103
|
-
request.succeed
|
104
|
-
end
|
105
|
-
|
106
|
-
it "waits for the request to succeed" do
|
107
|
-
succeeded = false
|
108
|
-
callback.stub(:call) {|response| succeeded = true }
|
109
|
-
|
110
|
-
Fiber.new { proxy.start env, target, client }.resume
|
111
|
-
succeeded.should be_false
|
112
|
-
|
113
|
-
request.succeed
|
114
|
-
succeeded.should be_true
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
describe "#build_request(env, target)" do
|
119
|
-
let(:target) { URI.parse "http://localhost:3000/bar" }
|
120
|
-
let(:base_request) { Hatetepe::Request.new "GET", "/foo" }
|
121
|
-
|
122
|
-
before do
|
123
|
-
env["hatetepe.request"] = base_request
|
124
|
-
env["REMOTE_ADDR"] = "123.234.123.234"
|
125
|
-
end
|
126
|
-
|
127
|
-
it "fails if env[hatetepe.request] isn't set" do
|
128
|
-
env.delete "hatetepe.request"
|
129
|
-
proc { proxy.build_request env, target }.should raise_error(ArgumentError)
|
130
|
-
end
|
131
|
-
|
132
|
-
it "combines the original URI with the target URI" do
|
133
|
-
proxy.build_request(env, target).uri.should == "/bar/foo"
|
134
|
-
end
|
135
|
-
|
136
|
-
it "builds a new request" do
|
137
|
-
proxy.build_request(env, target).should_not equal(base_request)
|
138
|
-
end
|
139
|
-
|
140
|
-
it "sets version to HTTP/1.1" do
|
141
|
-
base_request.http_version = "1.0"
|
142
|
-
proxy.build_request(env, target).http_version.should == "1.1"
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|