hatetepe 0.4.1 → 0.5.0.pre
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.
- 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
|