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