hatetepe 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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
- @headers = {}
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
@@ -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, version)
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
@@ -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
- attr_accessor :verb, :uri, :response
8
+ attr_reader :verb
9
+ attr_accessor :uri, :response
8
10
 
9
- def initialize(verb, uri, http_version = "1.1")
10
- @verb, @uri = verb, uri
11
- super http_version
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 to_hash
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! /[^A-Z]/, "_"
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
@@ -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
@@ -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 < EM::Connection
15
- def self.start(config)
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
- @app = Rack::Builder.new.tap do |b|
27
- b.use Hatetepe::Pipeline
28
- b.use Hatetepe::App
29
- b.use Hatetepe::Proxy
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
- super
34
- end
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
- def post_init
37
- @requests = []
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
- builder.on_write << method(:send_data)
44
- end
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
- def receive_data(data)
47
- parser << data
48
- rescue ParserError
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
- def process(*)
58
- request = requests.last
59
-
60
- env = request.to_hash.tap do |e|
61
- inject_environment e
62
- e["stream.start"] = proc do |response|
63
- e.delete "stream.start"
64
- start_response response
65
- end
66
- e["stream.send"] = builder.method(:body_chunk)
67
- e["stream.close"] = proc do
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
- def start_response(response)
78
- builder.response_line response[0]
79
- response[1]["Server"] = "hatetepe/#{VERSION}"
80
- builder.headers response[1]
81
- end
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
- def close_response(request)
84
- builder.complete
85
- requests.delete request
86
- close_connection_after_writing if requests.empty?
87
- end
88
-
89
- def inject_environment(env)
90
- env["hatetepe.connection"] = self
91
- env["rack.url_scheme"] = "http"
92
- env["rack.input"].source = self
93
- env["rack.errors"] = errors
94
-
95
- env["rack.multithread"] = false
96
- env["rack.multiprocess"] = false
97
- env["rack.run_once"] = false
98
-
99
- env["SERVER_NAME"] = config[:host].dup
100
- env["SERVER_PORT"] = String(config[:port])
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