hatetepe 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +21 -16
- data/hatetepe.gemspec +1 -2
- data/lib/hatetepe/body.rb +5 -3
- data/lib/hatetepe/builder.rb +6 -3
- data/lib/hatetepe/cli.rb +27 -5
- data/lib/hatetepe/client.rb +157 -82
- data/lib/hatetepe/client/keep_alive.rb +58 -0
- data/lib/hatetepe/client/pipeline.rb +19 -0
- data/lib/hatetepe/connection.rb +42 -0
- data/lib/hatetepe/deferred_status_fix.rb +11 -0
- data/lib/hatetepe/message.rb +4 -4
- data/lib/hatetepe/parser.rb +3 -4
- data/lib/hatetepe/request.rb +19 -6
- data/lib/hatetepe/response.rb +11 -3
- data/lib/hatetepe/server.rb +115 -85
- data/lib/hatetepe/{app.rb → server/app.rb} +7 -2
- data/lib/hatetepe/server/keep_alive.rb +61 -0
- data/lib/hatetepe/server/pipeline.rb +24 -0
- data/lib/hatetepe/{proxy.rb → server/proxy.rb} +5 -11
- data/lib/hatetepe/version.rb +1 -1
- data/lib/rack/handler/hatetepe.rb +1 -4
- data/spec/integration/cli/start_spec.rb +75 -123
- data/spec/integration/client/keep_alive_spec.rb +74 -0
- data/spec/integration/server/keep_alive_spec.rb +99 -0
- data/spec/spec_helper.rb +41 -16
- data/spec/unit/app_spec.rb +16 -5
- data/spec/unit/builder_spec.rb +4 -4
- data/spec/unit/client/pipeline_spec.rb +40 -0
- data/spec/unit/client_spec.rb +355 -199
- data/spec/unit/connection_spec.rb +64 -0
- data/spec/unit/parser_spec.rb +3 -2
- data/spec/unit/proxy_spec.rb +9 -18
- data/spec/unit/rack_handler_spec.rb +2 -12
- data/spec/unit/server_spec.rb +154 -60
- metadata +31 -36
- data/.rspec +0 -1
- data/.travis.yml +0 -3
- data/.yardopts +0 -1
- data/lib/hatetepe/pipeline.rb +0 -27
@@ -0,0 +1,58 @@
|
|
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
|
+
|
14
|
+
# priority
|
15
|
+
# 1. if X-Hatetepe-Single then Connection header, Client#request closes
|
16
|
+
# 2. if req.Connection == close then Connection header and close
|
17
|
+
# 3. if res.Connection == close then close
|
18
|
+
def call(request)
|
19
|
+
req, conn = request, request.connection
|
20
|
+
|
21
|
+
single = req.headers.delete("X-Hatetepe-Single")
|
22
|
+
req.headers["Connection"] ||= if single
|
23
|
+
"close"
|
24
|
+
else
|
25
|
+
"keep-alive"
|
26
|
+
end
|
27
|
+
close = req.headers["Connection"] == "close"
|
28
|
+
|
29
|
+
# stop processing further requests as early as possible
|
30
|
+
conn.processing_enabled = false if close
|
31
|
+
|
32
|
+
app.call(request).tap do |response|
|
33
|
+
if !single && response.headers["Connection"] == "close"
|
34
|
+
conn.processing_enabled = false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(request)
|
40
|
+
req, conn = request, request.connection
|
41
|
+
|
42
|
+
single = req.headers.delete("X-Hatetepe-Single")
|
43
|
+
req.headers["Connection"] = "close" if single
|
44
|
+
|
45
|
+
req.headers["Connection"] ||= "keep-alive"
|
46
|
+
close = req.headers["Connection"] == "close"
|
47
|
+
|
48
|
+
conn.processing_enabled = false if close
|
49
|
+
|
50
|
+
app.call(request).tap do |res|
|
51
|
+
if !single && (close || res.headers["Connection"] == "close")
|
52
|
+
conn.processing_enabled = false
|
53
|
+
conn.stop
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,19 @@
|
|
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
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
|
3
|
+
module Hatetepe
|
4
|
+
class Connection < EM::Connection
|
5
|
+
attr_accessor :processing_enabled
|
6
|
+
alias_method :processing_enabled?, :processing_enabled
|
7
|
+
|
8
|
+
def remote_address
|
9
|
+
sockaddr && sockaddr[1]
|
10
|
+
end
|
11
|
+
|
12
|
+
def remote_port
|
13
|
+
sockaddr && sockaddr[0]
|
14
|
+
end
|
15
|
+
|
16
|
+
def sockaddr
|
17
|
+
@sockaddr ||= Socket.unpack_sockaddr_in(get_peername) rescue nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def closed?
|
21
|
+
!!@closed_by
|
22
|
+
end
|
23
|
+
|
24
|
+
def closed_by_remote?
|
25
|
+
@closed_by == :remote
|
26
|
+
end
|
27
|
+
|
28
|
+
def closed_by_self?
|
29
|
+
closed? && !closed_by_remote?
|
30
|
+
end
|
31
|
+
|
32
|
+
def close_connection(after_writing = false)
|
33
|
+
@closed_by = :self
|
34
|
+
super after_writing
|
35
|
+
end
|
36
|
+
|
37
|
+
# TODO how to detect closed-by-timeout?
|
38
|
+
def unbind
|
39
|
+
@closed_by = :remote unless closed?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
|
3
|
+
module EM::Deferrable
|
4
|
+
def set_deferred_status_with_status_fix(status, *args)
|
5
|
+
return if defined?(@deferred_status) && ![:unknown, status].include?(@deferred_status)
|
6
|
+
set_deferred_status_without_status_fix status, *args
|
7
|
+
end
|
8
|
+
|
9
|
+
alias_method :set_deferred_status_without_status_fix, :set_deferred_status
|
10
|
+
alias_method :set_deferred_status, :set_deferred_status_with_status_fix
|
11
|
+
end
|
data/lib/hatetepe/message.rb
CHANGED
@@ -3,11 +3,11 @@ require "hatetepe/body"
|
|
3
3
|
module Hatetepe
|
4
4
|
class Message
|
5
5
|
attr_accessor :http_version, :headers, :body
|
6
|
+
attr_accessor :connection
|
6
7
|
|
7
|
-
def initialize(http_version = "1.1")
|
8
|
-
@http_version = http_version
|
9
|
-
@
|
10
|
-
@body = Body.new
|
8
|
+
def initialize(headers = {}, body = nil, http_version = "1.1")
|
9
|
+
@headers, @http_version = headers, http_version
|
10
|
+
@body = body || Body.new
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
data/lib/hatetepe/parser.rb
CHANGED
@@ -23,16 +23,15 @@ module Hatetepe
|
|
23
23
|
p.on_headers_complete = proc do
|
24
24
|
version = p.http_version.join(".")
|
25
25
|
if p.http_method
|
26
|
-
@message = Request.new(p.http_method, p.request_url,
|
26
|
+
@message = Request.new(p.http_method, p.request_url,
|
27
|
+
p.headers, Body.new, version)
|
27
28
|
event! :request, message
|
28
29
|
else
|
29
|
-
@message = Response.new(p.status_code, version)
|
30
|
+
@message = Response.new(p.status_code, p.headers, Body.new, version)
|
30
31
|
event! :response, message
|
31
32
|
end
|
32
33
|
|
33
|
-
message.headers = p.headers
|
34
34
|
event! :headers, message.headers
|
35
|
-
|
36
35
|
event! :body, message.body
|
37
36
|
nil
|
38
37
|
end
|
data/lib/hatetepe/request.rb
CHANGED
@@ -1,17 +1,28 @@
|
|
1
|
+
require "hatetepe/deferred_status_fix"
|
1
2
|
require "hatetepe/message"
|
2
3
|
|
3
4
|
module Hatetepe
|
4
5
|
class Request < Message
|
5
6
|
include EM::Deferrable
|
6
7
|
|
7
|
-
|
8
|
+
attr_reader :verb
|
9
|
+
attr_accessor :uri, :response
|
8
10
|
|
9
|
-
def initialize(verb, uri, http_version = "1.1")
|
10
|
-
|
11
|
-
|
11
|
+
def initialize(verb, uri, headers = {}, body = nil, http_version = "1.1")
|
12
|
+
self.verb = verb
|
13
|
+
@uri = uri
|
14
|
+
super headers, body, http_version
|
12
15
|
end
|
13
16
|
|
14
|
-
def
|
17
|
+
def verb=(verb)
|
18
|
+
@verb = verb.to_s.upcase
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_a
|
22
|
+
[verb, uri, headers, body, http_version]
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
15
26
|
{
|
16
27
|
"rack.version" => [1, 0],
|
17
28
|
"hatetepe.request" => self,
|
@@ -20,13 +31,15 @@ module Hatetepe
|
|
20
31
|
"REQUEST_URI" => uri.dup
|
21
32
|
}.tap do |hsh|
|
22
33
|
headers.each do |key, value|
|
23
|
-
key = key.upcase.gsub
|
34
|
+
key = key.upcase.gsub /[^A-Z]/, "_"
|
24
35
|
key = "HTTP_#{key}" unless key =~ /^CONTENT_(TYPE|LENGTH)$/
|
25
36
|
hsh[key] = value.dup
|
26
37
|
end
|
27
38
|
|
28
39
|
hsh["REQUEST_PATH"], qm, hsh["QUERY_STRING"] = uri.partition("?")
|
29
40
|
hsh["PATH_INFO"], hsh["SCRIPT_NAME"] = hsh["REQUEST_PATH"].dup, ""
|
41
|
+
|
42
|
+
hsh["HTTP_VERSION"] = "HTTP/#{http_version}"
|
30
43
|
end
|
31
44
|
end
|
32
45
|
end
|
data/lib/hatetepe/response.rb
CHANGED
@@ -2,11 +2,19 @@ require "hatetepe/message"
|
|
2
2
|
|
3
3
|
module Hatetepe
|
4
4
|
class Response < Message
|
5
|
-
attr_accessor :status
|
5
|
+
attr_accessor :status, :request
|
6
6
|
|
7
|
-
def initialize(status, http_version = "1.1")
|
7
|
+
def initialize(status, headers = {}, body = nil, http_version = "1.1")
|
8
8
|
@status = status
|
9
|
-
super http_version
|
9
|
+
super headers, body, http_version
|
10
|
+
end
|
11
|
+
|
12
|
+
def success?
|
13
|
+
status.between? 100, 399
|
14
|
+
end
|
15
|
+
|
16
|
+
def failure?
|
17
|
+
status.between? 400, 599
|
10
18
|
end
|
11
19
|
|
12
20
|
def to_a
|
data/lib/hatetepe/server.rb
CHANGED
@@ -2,106 +2,136 @@ require "eventmachine"
|
|
2
2
|
require "em-synchrony"
|
3
3
|
require "rack"
|
4
4
|
|
5
|
-
require "hatetepe/app"
|
6
5
|
require "hatetepe/builder"
|
6
|
+
require "hatetepe/connection"
|
7
7
|
require "hatetepe/parser"
|
8
|
-
require "hatetepe/pipeline"
|
9
|
-
require "hatetepe/proxy"
|
10
|
-
require "hatetepe/request"
|
11
8
|
require "hatetepe/version"
|
12
9
|
|
13
10
|
module Hatetepe
|
14
|
-
class Server <
|
15
|
-
|
16
|
-
EM.start_server config[:host], config[:port], self, config
|
17
|
-
end
|
18
|
-
|
19
|
-
attr_reader :app, :config, :errors
|
20
|
-
attr_reader :requests, :parser, :builder
|
21
|
-
|
22
|
-
def initialize(config)
|
23
|
-
@config = config
|
24
|
-
@errors = config[:errors] || $stderr
|
11
|
+
class Server < Hatetepe::Connection; end
|
12
|
+
end
|
25
13
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
b.run config[:app]
|
31
|
-
end.to_app
|
14
|
+
require "hatetepe/server/app"
|
15
|
+
require "hatetepe/server/keep_alive"
|
16
|
+
require "hatetepe/server/pipeline"
|
17
|
+
require "hatetepe/server/proxy"
|
32
18
|
|
33
|
-
|
34
|
-
|
19
|
+
class Hatetepe::Server
|
20
|
+
def self.start(config)
|
21
|
+
EM.start_server config[:host], config[:port], self, config
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :app, :config, :errors
|
25
|
+
attr_reader :requests, :parser, :builder
|
26
|
+
|
27
|
+
def initialize(config)
|
28
|
+
@config = {:timeout => 1}.merge(config)
|
29
|
+
@errors = @config.delete(:errors) || $stderr
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def post_init
|
35
|
+
@requests = []
|
36
|
+
@parser, @builder = Hatetepe::Parser.new, Hatetepe::Builder.new
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
@parser, @builder = Parser.new, Builder.new
|
39
|
-
|
40
|
-
parser.on_request << requests.method(:<<)
|
41
|
-
parser.on_headers << method(:process)
|
38
|
+
parser.on_request << requests.method(:<<)
|
39
|
+
parser.on_headers << method(:process)
|
42
40
|
|
43
|
-
|
44
|
-
|
41
|
+
# XXX check if the connection is still present
|
42
|
+
builder.on_write << method(:send_data)
|
43
|
+
#builder.on_write {|data| p "server >> #{data}" }
|
44
|
+
|
45
|
+
@app = Rack::Builder.new.tap do |b|
|
46
|
+
# middleware is NOT ordered alphabetically
|
47
|
+
b.use Pipeline
|
48
|
+
b.use App
|
49
|
+
b.use KeepAlive
|
50
|
+
b.use Proxy
|
51
|
+
b.run config[:app]
|
52
|
+
end.to_app
|
53
|
+
|
54
|
+
self.processing_enabled = true
|
55
|
+
self.comm_inactivity_timeout = config[:timeout]
|
56
|
+
end
|
57
|
+
|
58
|
+
def receive_data(data)
|
59
|
+
#p "server << #{data}"
|
60
|
+
parser << data
|
61
|
+
rescue Hatetepe::ParserError => ex
|
62
|
+
close_connection
|
63
|
+
raise ex if ENV["RACK_ENV"] == "testing"
|
64
|
+
rescue Exception => ex
|
65
|
+
close_connection_after_writing
|
66
|
+
backtrace = ex.backtrace.map {|line| "\t#{line}" }.join("\n")
|
67
|
+
errors << "#{ex.class}: #{ex.message}\n#{backtrace}\n"
|
68
|
+
errors.flush
|
69
|
+
raise ex if ENV["RACK_ENV"] == "testing"
|
70
|
+
end
|
71
|
+
|
72
|
+
# XXX fail response bodies properly
|
73
|
+
# XXX make sure no more data is sent
|
74
|
+
def unbind
|
75
|
+
super
|
76
|
+
#requests.map(&:body).each &:fail
|
77
|
+
end
|
78
|
+
|
79
|
+
def process(*)
|
80
|
+
return unless processing_enabled?
|
81
|
+
request = requests.last
|
45
82
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
close_connection
|
50
|
-
rescue Exception => ex
|
51
|
-
close_connection_after_writing
|
52
|
-
backtrace = ex.backtrace.map {|line| "\t#{line}" }.join("\n")
|
53
|
-
errors << "#{ex.class}: #{ex.message}\n#{backtrace}\n"
|
54
|
-
errors.flush
|
83
|
+
self.comm_inactivity_timeout = 0
|
84
|
+
reset_timeout = proc do
|
85
|
+
self.comm_inactivity_timeout = config[:timeout] if requests.empty?
|
55
86
|
end
|
87
|
+
request.callback &reset_timeout
|
88
|
+
request.errback &reset_timeout
|
56
89
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
e
|
67
|
-
|
68
|
-
e.delete "stream.send"
|
69
|
-
e.delete "stream.close"
|
70
|
-
close_response request
|
71
|
-
end
|
90
|
+
env = request.to_h.tap do |e|
|
91
|
+
inject_environment e
|
92
|
+
e["stream.start"] = proc do |response|
|
93
|
+
e.delete "stream.start"
|
94
|
+
start_response response
|
95
|
+
end
|
96
|
+
e["stream.send"] = builder.method(:body_chunk)
|
97
|
+
e["stream.close"] = proc do
|
98
|
+
e.delete "stream.send"
|
99
|
+
e.delete "stream.close"
|
100
|
+
close_response request
|
72
101
|
end
|
73
|
-
|
74
|
-
Fiber.new { app.call env }.resume
|
75
102
|
end
|
76
103
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
104
|
+
Fiber.new { app.call env }.resume
|
105
|
+
end
|
106
|
+
|
107
|
+
def start_response(response)
|
108
|
+
builder.response_line response[0]
|
109
|
+
response[1]["Server"] ||= "hatetepe/#{Hatetepe::VERSION}"
|
110
|
+
builder.headers response[1]
|
111
|
+
end
|
112
|
+
|
113
|
+
def close_response(request)
|
114
|
+
builder.complete
|
115
|
+
requests.delete(request).succeed
|
116
|
+
end
|
82
117
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
host = env["HTTP_HOST"] || config[:host].dup
|
103
|
-
host += ":#{config[:port]}" unless host.include? ":"
|
104
|
-
env["HTTP_HOST"] = host
|
105
|
-
end
|
118
|
+
def inject_environment(env)
|
119
|
+
env["hatetepe.connection"] = self
|
120
|
+
env["rack.url_scheme"] = "http"
|
121
|
+
env["rack.input"].source = self
|
122
|
+
env["rack.errors"] = errors
|
123
|
+
|
124
|
+
env["rack.multithread"] = false
|
125
|
+
env["rack.multiprocess"] = false
|
126
|
+
env["rack.run_once"] = false
|
127
|
+
|
128
|
+
env["SERVER_NAME"] = config[:host].dup
|
129
|
+
env["SERVER_PORT"] = String(config[:port])
|
130
|
+
env["REMOTE_ADDR"] = remote_address.dup
|
131
|
+
env["REMOTE_PORT"] = String(remote_port)
|
132
|
+
|
133
|
+
host = env["HTTP_HOST"] || config[:host].dup
|
134
|
+
host += ":#{config[:port]}" unless host.include? ":"
|
135
|
+
env["HTTP_HOST"] = host
|
106
136
|
end
|
107
137
|
end
|