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.
- 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
|