hatetepe 0.5.2 → 0.6.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/.gitignore +7 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +4 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +9 -4
  6. data/Gemfile.devtools +55 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +39 -192
  9. data/Rakefile +3 -2
  10. data/bin/hatetepe +35 -2
  11. data/config/devtools.yml +2 -0
  12. data/config/flay.yml +3 -0
  13. data/config/flog.yml +2 -0
  14. data/config/mutant.yml +3 -0
  15. data/config/reek.yml +103 -0
  16. data/config/rubocop.yml +58 -0
  17. data/config/yardstick.yml +2 -0
  18. data/hatetepe.gemspec +23 -27
  19. data/lib/hatetepe/client/keep_alive.rb +59 -0
  20. data/lib/hatetepe/client/timeouts.rb +19 -0
  21. data/lib/hatetepe/client.rb +54 -302
  22. data/lib/hatetepe/connection/eventmachine.rb +61 -0
  23. data/lib/hatetepe/connection/status.rb +28 -0
  24. data/lib/hatetepe/errors.rb +7 -0
  25. data/lib/hatetepe/promise.rb +86 -0
  26. data/lib/hatetepe/request.rb +15 -39
  27. data/lib/hatetepe/response.rb +82 -22
  28. data/lib/hatetepe/serializer/encoding.rb +58 -0
  29. data/lib/hatetepe/serializer.rb +61 -0
  30. data/lib/hatetepe/server/keep_alive.rb +53 -13
  31. data/lib/hatetepe/server/timeouts.rb +17 -0
  32. data/lib/hatetepe/server.rb +37 -85
  33. data/lib/hatetepe/support/handlers.rb +19 -0
  34. data/lib/hatetepe/support/keep_alive.rb +14 -0
  35. data/lib/hatetepe/support/message.rb +40 -0
  36. data/lib/hatetepe/version.rb +3 -1
  37. data/lib/hatetepe.rb +29 -7
  38. data/spec/integration/error_handling_spec.rb +7 -0
  39. data/spec/integration/keep_alive_spec.rb +106 -0
  40. data/spec/integration/smoke_spec.rb +21 -0
  41. data/spec/integration/streaming_spec.rb +61 -0
  42. data/spec/integration/timeouts_spec.rb +82 -0
  43. data/spec/shared/integration/server_client_pair.rb +26 -0
  44. data/spec/spec_helper.rb +41 -10
  45. data/spec/support/handler.rb +55 -0
  46. data/spec/support/helper.rb +74 -0
  47. data/spec/unit/client_spec.rb +115 -156
  48. data/spec/unit/connection/eventmachine_spec.rb +146 -0
  49. data/spec/unit/request_spec.rb +35 -0
  50. data/spec/unit/response_spec.rb +42 -0
  51. data/spec/unit/server_spec.rb +65 -100
  52. data/spec/unit/support/keep_alive_spec.rb +52 -0
  53. data/spec/unit/support/message_spec.rb +41 -0
  54. metadata +68 -103
  55. data/Gemfile.lock +0 -46
  56. data/LICENSE +0 -19
  57. data/Procfile +0 -1
  58. data/config.ru +0 -7
  59. data/examples/parallel_requests.rb +0 -32
  60. data/lib/hatetepe/body.rb +0 -182
  61. data/lib/hatetepe/builder.rb +0 -171
  62. data/lib/hatetepe/cli.rb +0 -61
  63. data/lib/hatetepe/connection.rb +0 -73
  64. data/lib/hatetepe/events.rb +0 -35
  65. data/lib/hatetepe/message.rb +0 -13
  66. data/lib/hatetepe/parser.rb +0 -83
  67. data/lib/hatetepe/server/pipeline.rb +0 -20
  68. data/lib/hatetepe/server/rack_app.rb +0 -39
  69. data/lib/rack/handler/hatetepe.rb +0 -33
  70. data/spec/integration/cli/start_spec.rb +0 -113
  71. data/spec/integration/client/keep_alive_spec.rb +0 -23
  72. data/spec/integration/client/timeout_spec.rb +0 -97
  73. data/spec/integration/server/keep_alive_spec.rb +0 -27
  74. data/spec/integration/server/timeout_spec.rb +0 -51
  75. data/spec/unit/body_spec.rb +0 -205
  76. data/spec/unit/builder_spec.rb +0 -372
  77. data/spec/unit/connection_spec.rb +0 -62
  78. data/spec/unit/events_spec.rb +0 -96
  79. data/spec/unit/parser_spec.rb +0 -209
  80. data/spec/unit/rack_handler_spec.rb +0 -60
@@ -1,28 +1,88 @@
1
- require "hatetepe/message"
1
+ # encoding: utf-8
2
2
 
3
3
  module Hatetepe
4
- class Response < Message
5
- attr_accessor :status, :request
6
-
7
- def initialize(status, headers = {}, body = nil, http_version = "1.1")
8
- @status = status
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
18
- end
19
-
20
- def to_a
21
- [status, headers, body]
4
+ class Response
5
+ include Promise::Attribute
6
+ promise :finished
7
+
8
+ attr_reader :status, :headers, :body
9
+ attr_accessor :http_version
10
+
11
+ def initialize(status, headers = {}, body = '')
12
+ @status, @headers, @body = status, headers, body
13
+ @http_version = 1.1
14
+ super()
22
15
  end
23
-
24
- def [](idx)
25
- to_a[idx]
16
+
17
+ def status_name
18
+ STATUS_NAMES[status.to_i] || UNKNOWN_STATUS
26
19
  end
20
+
21
+ UNKNOWN_STATUS = 'Unknown Status'.freeze
22
+
23
+ STATUS_NAMES =
24
+ { 100 => 'Continue'.freeze,
25
+ 101 => 'Switching Protocols'.freeze,
26
+ 102 => 'Processing'.freeze,
27
+ 200 => 'OK'.freeze,
28
+ 201 => 'Created'.freeze,
29
+ 202 => 'Accepted'.freeze,
30
+ 203 => 'Non-Authoritative Information'.freeze,
31
+ 204 => 'No Content'.freeze,
32
+ 205 => 'Reset Content'.freeze,
33
+ 206 => 'Partial Content'.freeze,
34
+ 207 => 'Multi-Status'.freeze,
35
+ 208 => 'Already Reported'.freeze,
36
+ 226 => 'IM Used'.freeze,
37
+ 300 => 'Multiple Choices'.freeze,
38
+ 301 => 'Moved Permanently'.freeze,
39
+ 302 => 'Found'.freeze,
40
+ 303 => 'See Other'.freeze,
41
+ 304 => 'Not Modified'.freeze,
42
+ 305 => 'Use Proxy'.freeze,
43
+ 306 => 'Reserved'.freeze,
44
+ 307 => 'Temporary Redirect'.freeze,
45
+ 308 => 'Permanent Redirect'.freeze,
46
+ 400 => 'Bad Request'.freeze,
47
+ 401 => 'Unauthorized'.freeze,
48
+ 402 => 'Payment Required'.freeze,
49
+ 403 => 'Forbidden'.freeze,
50
+ 404 => 'Not Found'.freeze,
51
+ 405 => 'Method Not Allowed'.freeze,
52
+ 406 => 'Not Acceptable'.freeze,
53
+ 407 => 'Proxy Authentication Required'.freeze,
54
+ 408 => 'Request Timeout'.freeze,
55
+ 409 => 'Conflict'.freeze,
56
+ 410 => 'Gone'.freeze,
57
+ 411 => 'Length Required'.freeze,
58
+ 412 => 'Precondition Failed'.freeze,
59
+ 413 => 'Request Entity Too Large'.freeze,
60
+ 414 => 'Request-URI Too Long'.freeze,
61
+ 415 => 'Unsupported Media Type'.freeze,
62
+ 416 => 'Requested Range Not Satisfiable'.freeze,
63
+ 417 => 'Expectation Failed'.freeze,
64
+ 422 => 'Unprocessable Entity'.freeze,
65
+ 423 => 'Locked'.freeze,
66
+ 424 => 'Failed Dependency'.freeze,
67
+ 425 =>
68
+ 'Reserved for WebDAV advanced collections expired proposal'.freeze,
69
+ 426 => 'Upgrade Required'.freeze,
70
+ 427 => 'Unassigned'.freeze,
71
+ 428 => 'Precondition Required'.freeze,
72
+ 429 => 'Too Many Requests'.freeze,
73
+ 430 => 'Unassigned'.freeze,
74
+ 431 => 'Request Header Fields Too Large'.freeze,
75
+ 500 => 'Internal Server Error'.freeze,
76
+ 501 => 'Not Implemented'.freeze,
77
+ 502 => 'Bad Gateway'.freeze,
78
+ 503 => 'Service Unavailable'.freeze,
79
+ 504 => 'Gateway Timeout'.freeze,
80
+ 505 => 'HTTP Version Not Supported'.freeze,
81
+ 506 => 'Variant Also Negotiates (Experimental)'.freeze,
82
+ 507 => 'Insufficient Storage'.freeze,
83
+ 508 => 'Loop Detected'.freeze,
84
+ 509 => 'Unassigned'.freeze,
85
+ 510 => 'Not Extended'.freeze,
86
+ 511 => 'Network Authentication Required'.freeze }
27
87
  end
28
88
  end
@@ -0,0 +1,58 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ class Serializer
5
+ module Encoding
6
+ TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
7
+ CHUNKED = 'chunked'.freeze
8
+ CHUNK = "%d\r\n%s\r\n".freeze
9
+ CONTENT_LENGTH = 'Content-Length'.freeze
10
+
11
+ def setup_body
12
+ if finished.pending?
13
+ setup_chunked_encoding
14
+ setup_chunked_streaming
15
+ else
16
+ setup_identity_encoding
17
+ end
18
+ end
19
+
20
+ def setup_chunked_encoding
21
+ headers.delete(CONTENT_LENGTH)
22
+ headers[TRANSFER_ENCODING] = CHUNKED
23
+ end
24
+
25
+ def setup_chunked_streaming
26
+ finished.on_progress do |chunk|
27
+ write_chunk(chunk)
28
+ end
29
+ finished.then { |_| write_chunk!('') }
30
+ end
31
+
32
+ def setup_identity_encoding
33
+ headers.delete(TRANSFER_ENCODING)
34
+ headers[CONTENT_LENGTH] = body.bytesize
35
+ end
36
+
37
+ def write_body
38
+ if finished.pending?
39
+ write_chunk(body)
40
+ else
41
+ write(body)
42
+ end
43
+ end
44
+
45
+ def write_chunk(chunk)
46
+ write_chunk!(chunk) unless chunk.empty?
47
+ end
48
+
49
+ def write_chunk!(chunk)
50
+ write(body_chunk(chunk))
51
+ end
52
+
53
+ def body_chunk(chunk)
54
+ sprintf(CHUNK, chunk.bytesize, chunk)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+
3
+ require 'forwardable'
4
+
5
+ module Hatetepe
6
+ class Serializer
7
+ REQUEST_LINE = "%s %s HTTP/%.1f\r\n".freeze
8
+ RESPONSE_LINE = "HTTP/%.1f %d %s\r\n".freeze
9
+ HEADER = "%s: %s\r\n".freeze
10
+ CRLF = "\r\n".freeze
11
+
12
+ include Encoding
13
+
14
+ extend Forwardable
15
+ def_delegator :@message, :headers
16
+ def_delegator :@message, :body
17
+ def_delegator :@message, :finished
18
+
19
+ def initialize(message, writer)
20
+ @message = message
21
+ @writer = writer
22
+ end
23
+
24
+ def serialize
25
+ setup_body
26
+
27
+ write(banana_line)
28
+ write(header_block)
29
+
30
+ write_body
31
+ end
32
+
33
+ private
34
+
35
+ def write(data)
36
+ @writer.call(data)
37
+ end
38
+
39
+ def banana_line
40
+ if Request === @message
41
+ request_line
42
+ else
43
+ response_line
44
+ end
45
+ end
46
+
47
+ def request_line
48
+ sprintf(REQUEST_LINE,
49
+ @message.http_method.upcase, @message.uri, @message.http_version)
50
+ end
51
+
52
+ def response_line
53
+ sprintf(RESPONSE_LINE,
54
+ @message.http_version, @message.status, @message.status_name)
55
+ end
56
+
57
+ def header_block
58
+ headers.reduce('') { |a, e| a << sprintf(HEADER, *e) } + CRLF
59
+ end
60
+ end
61
+ end
@@ -1,23 +1,63 @@
1
- module Hatetepe::Server
2
- class KeepAlive
3
- def initialize(app, connection)
4
- @app, @connection = app, connection
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ class Server::KeepAlive
5
+ CONNECTION = 'Connection'.freeze
6
+ CLOSE = 'close'.freeze
7
+ KEEP_ALIVE = 'keep-alive'.freeze
8
+
9
+ include Support::KeepAlive
10
+
11
+ def initialize(config, server, connection)
12
+ @config, @server = config, server
13
+ @connection = connection
14
+ @requests = []
5
15
  end
6
16
 
7
- def call(request, &respond)
8
- @app.call(request) do |response|
9
- respond.call(response)
10
- maybe_close(request, response)
17
+ def serve(request)
18
+ @requests << request
19
+ end
20
+
21
+ def respond(request, response)
22
+ synchronize_responses(request, response)
23
+
24
+ version = request.http_version
25
+ header = connection_header(request, response)
26
+
27
+ if close_connection?(version, header)
28
+ response.headers[CONNECTION] = CLOSE
29
+ close_connection(response)
30
+ else
31
+ response.headers[CONNECTION] = KEEP_ALIVE
11
32
  end
12
33
  end
13
34
 
14
- def maybe_close(request, response)
15
- version = request.http_version.to_f
16
- header = request.headers["Connection"] || response.headers["Connection"]
35
+ private
17
36
 
18
- if (version < 1.1 && header != "keep-alive") || header == "close"
19
- @connection.stop!
37
+ def synchronize_responses(request, response)
38
+ wait_for_previous(request)
39
+
40
+ callback = proc { @requests.delete(request) }
41
+ response.finished.then(callback, callback)
42
+ end
43
+
44
+ def wait_for_previous(request)
45
+ previous = @requests[@requests.index(request) - 1]
46
+ if previous
47
+ response = previous.served.sync
48
+ response.finished.sync
20
49
  end
21
50
  end
51
+
52
+ def connection_header(request, response)
53
+ header = request.headers[CONNECTION] || response.headers[CONNECTION]
54
+ header.to_s.downcase
55
+ end
56
+
57
+ def close_connection(response)
58
+ callback = proc { |arg| @server.close }
59
+ # XXX possible race condition with other locations waiting for this
60
+ response.finished.then(callback, callback)
61
+ end
22
62
  end
23
63
  end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ # @see EM.heartbeat_interval
5
+ class Server::Timeouts
6
+ def initialize(config, server, connection)
7
+ @config, @server = config, server
8
+ @connection = connection
9
+
10
+ @config[:timeout] ||= 2.0
11
+ end
12
+
13
+ def post_init
14
+ @connection.comm_inactivity_timeout = @config[:timeout]
15
+ end
16
+ end
17
+ end
@@ -1,102 +1,54 @@
1
- require "eventmachine"
2
- require "em-synchrony"
3
-
4
- require "hatetepe/builder"
5
- require "hatetepe/connection"
6
- require "hatetepe/parser"
7
- require "hatetepe/server/keep_alive"
8
- require "hatetepe/server/pipeline"
9
- require "hatetepe/server/rack_app"
10
- require "hatetepe/version"
11
-
12
- module Hatetepe::Server
13
- include Hatetepe::Connection
14
-
15
- attr_reader :config, :requests
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ class Server
5
+ def self.start(config)
6
+ callback = proc { |connection| new(config, connection) }
7
+ EM.start_server(config[:address], config[:port],
8
+ Connection::EventMachine, callback)
9
+ end
16
10
 
17
- CONFIG_DEFAULTS = {
18
- :timeout => 5.0,
19
- :app => [ Pipeline, KeepAlive, RackApp ]
20
- }
11
+ include Support::Handlers
12
+ include Connection::Status
21
13
 
22
- # @api public
23
- def self.start(config)
24
- EM.start_server(config[:host], config[:port], self, config)
25
- end
14
+ attr_reader :config
26
15
 
27
- # @api semipublic
28
- def initialize(config)
29
- @config = CONFIG_DEFAULTS.merge(config).freeze
30
- end
16
+ def initialize(config, connection)
17
+ @config = config
18
+ @connection = connection
31
19
 
32
- # @api semipublic
33
- def post_init
34
- @parser, @builder = Hatetepe::Parser.new, Hatetepe::Builder.new
35
- @parser.on_request &method(:process_request)
36
- @builder.on_write &method(:send_data)
37
- # @builder.on_write {|data| p "<--| #{data}" }
20
+ setup_handlers
21
+ setup_connection
38
22
 
39
- @app = build_app(config[:app])
23
+ notify_handlers(:post_init)
24
+ end
40
25
 
41
- self.comm_inactivity_timeout = config[:timeout]
42
- end
26
+ def serve(request)
27
+ request.served.then { |response| respond(request, response) }
43
28
 
44
- # @api semipubic
45
- def receive_data(data)
46
- # p "-->| #{data}"
47
- @parser << data
48
- rescue Object => ex
49
- puts [ex.message, *ex.backtrace].join("\n\t")
50
- close_connection
51
- end
52
-
53
- # @api private
54
- def process_request(request)
55
- Fiber.new do
56
- @app.call(request) do |response|
57
- send_response(request, response)
29
+ fiber = Fiber.new do
30
+ notify_handlers(:serve, request)
58
31
  end
59
- end.resume
60
- end
61
-
62
- # @api private
63
- def send_response(request, response)
64
- self.comm_inactivity_timeout = 0
65
- @builder.response(response.to_a)
66
- self.comm_inactivity_timeout = config[:timeout]
67
-
68
- if response.failure?
69
- request.fail(response)
70
- else
71
- request.succeed(response)
32
+ fiber.resume
72
33
  end
73
- end
74
34
 
75
- # @api semipublic
76
- def unbind(reason)
77
- super
78
- end
35
+ def respond(request, response)
36
+ notify_handlers(:respond, request, response)
37
+ @connection.serialize(response)
38
+ end
79
39
 
80
- # @api public
81
- def stop
82
- end
40
+ def close
41
+ @connection.close
42
+ end
83
43
 
84
- # @api public
85
- def stop!
86
- close_connection_after_writing
87
- end
44
+ private
88
45
 
89
- private
46
+ def setup_connection
47
+ @connection.parse(method(:serve))
48
+ @connection.closed.then(method(:shutdown))
49
+ end
90
50
 
91
- def build_app(app)
92
- app =
93
- if app.respond_to?(:call)
94
- [ *CONFIG_DEFAULTS[:app], app ]
95
- else
96
- app.dup
97
- end
98
- app.inject(app.pop) do |inner, outer|
99
- outer.new(inner, self)
51
+ def shutdown(_)
100
52
  end
101
53
  end
102
54
  end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ module Support
5
+ module Handlers
6
+ def setup_handlers
7
+ @handlers = @config[:handlers].map do |klass|
8
+ klass.new(@config, self, @connection)
9
+ end
10
+ end
11
+
12
+ def notify_handlers(hook, *args)
13
+ @handlers.each do |handler|
14
+ handler.send(hook, *args) if handler.respond_to?(hook)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ module Support
5
+ module KeepAlive
6
+ CLOSE = 'close'.freeze
7
+ KEEP_ALIVE = 'keep-alive'.freeze
8
+
9
+ def close_connection?(version, header)
10
+ (version < 1.1 && header != KEEP_ALIVE) || header == CLOSE
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ module Support
5
+ class Message
6
+ def self.build(parser)
7
+ if parser.http_method
8
+ build_request(parser)
9
+ else
10
+ build_response(parser)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def self.build_request(parser)
17
+ request = Request.new(http_method_from(parser),
18
+ parser.request_url,
19
+ parser.headers)
20
+ request.http_version = http_version_from(parser)
21
+ request
22
+ end
23
+
24
+ def self.build_response(parser)
25
+ response = Response.new(parser.status_code,
26
+ parser.headers)
27
+ response.http_version = http_version_from(parser)
28
+ response
29
+ end
30
+
31
+ def self.http_method_from(parser)
32
+ parser.http_method.downcase.to_sym
33
+ end
34
+
35
+ def self.http_version_from(parser)
36
+ parser.http_version.join('.').to_f
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Hatetepe
2
- VERSION = "0.5.2"
4
+ VERSION = '0.6.0.pre'
3
5
  end
data/lib/hatetepe.rb CHANGED
@@ -1,7 +1,29 @@
1
- require "hatetepe/builder"
2
- require "hatetepe/client"
3
- require "hatetepe/parser"
4
- require "hatetepe/request"
5
- require "hatetepe/response"
6
- require "hatetepe/server"
7
- require "hatetepe/version"
1
+ # encoding: utf-8
2
+
3
+ require 'eventmachine'
4
+ require 'fiber'
5
+ require 'promise'
6
+ require 'http/parser'
7
+
8
+ require 'hatetepe/version'
9
+
10
+ require 'hatetepe/errors'
11
+ require 'hatetepe/support/handlers'
12
+ require 'hatetepe/support/keep_alive'
13
+ require 'hatetepe/support/message'
14
+
15
+ require 'hatetepe/promise'
16
+ require 'hatetepe/request'
17
+ require 'hatetepe/response'
18
+
19
+ require 'hatetepe/connection/eventmachine'
20
+ require 'hatetepe/connection/status'
21
+ require 'hatetepe/serializer/encoding'
22
+ require 'hatetepe/serializer'
23
+
24
+ require 'hatetepe/client'
25
+ require 'hatetepe/client/keep_alive'
26
+ require 'hatetepe/client/timeouts'
27
+ require 'hatetepe/server'
28
+ require 'hatetepe/server/keep_alive'
29
+ require 'hatetepe/server/timeouts'
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Error handling' do
6
+ specify { pending('Needs more thought') }
7
+ end