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
data/lib/hatetepe/body.rb DELETED
@@ -1,182 +0,0 @@
1
- require "em-synchrony"
2
- require "eventmachine"
3
- require "stringio"
4
-
5
- module Hatetepe
6
- # Thin wrapper around StringIO for asynchronous body processing.
7
- class Body
8
- include EM::Deferrable
9
-
10
- # The wrapped StringIO.
11
- attr_reader :io
12
-
13
- # The origin Client or Server connection.
14
- attr_accessor :source
15
-
16
- # Create a new Body instance.
17
- #
18
- # @param [String] data
19
- # Initial content of the StringIO object.
20
- def initialize(data = "")
21
- @receivers = []
22
- @io = StringIO.new(data)
23
- end
24
-
25
- # Blocks until the Body is write-closed.
26
- #
27
- # Use this if you want to wait until _all_ of the body has arrived before
28
- # continuing. It will resume the originating connection if it's paused.
29
- #
30
- # @return [undefined]
31
- def sync
32
- source.resume if source && source.paused?
33
- EM::Synchrony.sync self
34
- end
35
-
36
- # Forwards to StringIO#length.
37
- #
38
- # Blocks until the Body is write-closed. Returns the current length of the
39
- # underlying StringIO's content.
40
- #
41
- # @return [Fixnum]
42
- # The StringIO's length.
43
- def length
44
- sync
45
- io.length
46
- end
47
-
48
- # Returns true if the underlying StringIO is empty, false otherwise.
49
- #
50
- # @return [Boolean]
51
- # True if empty, false otherwise.
52
- def empty?
53
- length == 0
54
- end
55
-
56
- # Forwards to StringIO#pos.
57
- #
58
- # Returns the underlying StringIO's current pointer position.
59
- #
60
- # @return [Fixnum]
61
- # The current pointer position.
62
- def pos
63
- io.pos
64
- end
65
-
66
- # Forwards to StringIO#rewind.
67
- #
68
- # Moves the underlying StringIO's pointer back to the beginnung.
69
- #
70
- # @return [undefined]
71
- def rewind
72
- sync
73
- rewind!
74
- end
75
-
76
- # Rewinds underlying IO without blocking
77
- #
78
- # TODO this is a hack. the whole blocking/rewinding stuff needs to be
79
- # more though out.
80
- #
81
- # @api protected
82
- def rewind!
83
- io.rewind
84
- end
85
-
86
- # Forwards to StringIO#close_write.
87
- #
88
- # Write-closes the body and succeeds, thus releasing all blocking method
89
- # calls like #length, #each, #read and #get.
90
- #
91
- # @return [undefined]
92
- def close_write
93
- io.close_write
94
- succeed
95
- end
96
-
97
- # Forwards to StringIO#closed_write?.
98
- #
99
- # Returns true if the body is write-closed, false otherwise.
100
- #
101
- # @return [Boolean]
102
- # True if the body is write-closed, false otherwise.
103
- def closed_write?
104
- io.closed_write?
105
- end
106
-
107
- # Yields incoming body data.
108
- #
109
- # Immediately yields all data that has already arrived. Blocks until the
110
- # Body is write-closed and yields for each call to #write until then.
111
- #
112
- # @yield [String] Block to execute for each incoming data chunk.
113
- #
114
- # @return [Enumerator,self]
115
- def each(&block)
116
- return to_enum(__method__) unless block
117
-
118
- @receivers << block
119
- block.call io.string.dup unless io.string.empty?
120
- sync
121
-
122
- self
123
- end
124
-
125
- # Forwards to StringIO#read.
126
- #
127
- # From the Rack Spec: If given, +length+ must be a non-negative Integer
128
- # (>= 0) or +nil+, and +buffer+ must be a String and may not be nil. If
129
- # +length+ is given and not nil, then this method reads at most +length+
130
- # bytes from the input stream. If +length+ is not given or nil, then this
131
- # method reads all data until EOF. When EOF is reached, this method returns
132
- # nil if +length+ is given and not nil, or "" if +length+ is not given or
133
- # is nil. If +buffer+ is given, then the read data will be placed into
134
- # +buffer+ instead of a newly created String object.
135
- #
136
- # @param [Fixnum] length (optional)
137
- # How many bytes to read.
138
- # @param [String] buffer (optional)
139
- # Buffer for read data.
140
- #
141
- # @return [nil]
142
- # +nil+ if EOF has been reached.
143
- # @return [String]
144
- # All data or at most +length+ bytes of data if +length+ is given.
145
- def read(*args)
146
- sync
147
- io.read *args
148
- end
149
-
150
- # Forwards to StringIO#gets.
151
- #
152
- # Reads one line from the IO. Returns the line or +nil+ if EOF has been
153
- # reached.
154
- #
155
- # @return [String]
156
- # One line.
157
- # @return [nil]
158
- # If has been reached.
159
- def gets
160
- sync
161
- io.gets
162
- end
163
-
164
- # Forwards to StringIO#write.
165
- #
166
- # Appends the given String to the underlying StringIO annd returns the
167
- # number of bytes written.
168
- #
169
- # @param [String] data
170
- # The data to append.
171
- #
172
- # @return [Fixnum]
173
- # The number of bytes written.
174
- def write(data)
175
- ret = io.write data
176
- @receivers.each do |r|
177
- Fiber.new { r.call data }.resume
178
- end
179
- ret
180
- end
181
- end
182
- end
@@ -1,171 +0,0 @@
1
- require "rack/utils"
2
-
3
- module Hatetepe
4
- class BuilderError < StandardError; end
5
-
6
- class Builder
7
- def self.build(&block)
8
- message = ""
9
- builder = new do |b|
10
- b.on_write {|data| message << data }
11
- end
12
-
13
- block.arity == 0 ? builder.instance_eval(&block) : block.call(builder)
14
-
15
- builder.complete
16
- return message.empty? ? nil : message
17
- end
18
-
19
- attr_reader :state
20
-
21
- def initialize(&block)
22
- reset
23
- @on_write, @on_complete, @on_error = [], [], []
24
-
25
- if block
26
- block.arity == 0 ? instance_eval(&block) : block.call(self)
27
- end
28
- end
29
-
30
- def reset
31
- @state = :ready
32
- @chunked = nil
33
- end
34
-
35
- [:write, :complete, :error].each do |hook|
36
- define_method :"on_#{hook}" do |&block|
37
- store = instance_variable_get(:"@on_#{hook}")
38
- return store unless block
39
- store << block
40
- end
41
- end
42
-
43
- def ready?
44
- state == :ready
45
- end
46
-
47
- def writing_headers?
48
- state == :writing_headers
49
- end
50
-
51
- def writing_body?
52
- state == :writing_body
53
- end
54
-
55
- def writing_trailing_headers?
56
- state == :writing_trailing_headers
57
- end
58
-
59
- def chunked?
60
- @chunked
61
- end
62
-
63
- def request(req)
64
- request_line req[0], req[1], (req[4] || "1.1")
65
- headers req[2]
66
- body req[3] if req[3]
67
- complete
68
- end
69
-
70
- def request_line(verb, uri, version = "1.1")
71
- complete unless ready?
72
- write "#{verb.upcase} #{uri} HTTP/#{version}\r\n"
73
- @state = :writing_headers
74
- end
75
-
76
- def response(res)
77
- response_line res[0], (res[3] || "1.1")
78
- headers res[1]
79
- body res[2] if res[2]
80
- complete
81
- end
82
-
83
- def response_line(code, version = "1.1")
84
- complete unless ready?
85
- unless status = Rack::Utils::HTTP_STATUS_CODES[code]
86
- error "Unknown status code: #{code}"
87
- end
88
-
89
- write "HTTP/#{version} #{code} #{status}\r\n"
90
- @state = :writing_headers
91
- end
92
-
93
- def header(name, value)
94
- value = String(value)
95
- raw_header "#{name}: #{value}" unless value.empty?
96
- end
97
-
98
- def headers(hash)
99
- hash.each {|k, v| header k, v }
100
- end
101
-
102
- def raw_header(header)
103
- if ready?
104
- error "A request or response line is required before writing headers"
105
- elsif writing_body?
106
- error "Trailing headers require chunked transfer encoding" unless chunked?
107
- write "0\r\n"
108
- @state = :writing_trailing_headers
109
- end
110
-
111
- if @chunked.nil? && header[0..13] == "Content-Length"
112
- @chunked = false
113
- elsif @chunked.nil? && header[0..16] == "Transfer-Encoding"
114
- @chunked = true
115
- end
116
-
117
- write "#{header}\r\n"
118
- end
119
-
120
- def body(body)
121
- body.each {|c| body_chunk c }
122
- # XXX complete here?
123
- end
124
-
125
- def body_chunk(chunk)
126
- if ready?
127
- error "A request or response line and headers are required before writing body"
128
- elsif writing_trailing_headers?
129
- error "Cannot write body after trailing headers"
130
- elsif writing_headers?
131
- if @chunked.nil?
132
- header "Transfer-Encoding", "chunked"
133
- end
134
- write "\r\n"
135
- @state = :writing_body
136
- end
137
-
138
- chunk = chunk.to_s
139
- if chunked?
140
- write "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
141
- else
142
- write chunk unless chunk.empty?
143
- end
144
- end
145
-
146
- def complete
147
- if ready?
148
- return
149
- elsif writing_headers? && @chunked.nil?
150
- header "Content-Length", "0"
151
- end
152
- body_chunk ""
153
-
154
- on_complete.each {|blk| blk.call }
155
- reset
156
- end
157
-
158
- def write(data)
159
- on_write.each {|blk| blk.call data }
160
- end
161
-
162
- def error(message)
163
- exception = BuilderError.new(message)
164
- unless on_error.empty?
165
- on_error.each {|blk| blk.call exception }
166
- else
167
- raise exception
168
- end
169
- end
170
- end
171
- end
data/lib/hatetepe/cli.rb DELETED
@@ -1,61 +0,0 @@
1
- require "thor"
2
-
3
- module Hatetepe
4
- class CLI < Thor
5
- map "--version" => :version
6
- map "-v" => :version
7
-
8
- default_task :start
9
-
10
- desc :version, "Print version information"
11
- def version
12
- require "hatetepe/version"
13
- say Hatetepe::VERSION
14
- end
15
-
16
- desc "[start]", "Start a server"
17
- method_option :bind, :aliases => "-b", :type => :string,
18
- :banner => "Bind to the specified TCP interface (default: 127.0.0.1)"
19
- method_option :port, :aliases => "-p", :type => :numeric,
20
- :banner => "Bind to the specified port (default: 3000)"
21
- method_option :rackup, :aliases => "-r", :type => :string,
22
- :banner => "Load specified rackup (.ru) file (default: config.ru)"
23
- method_option :env, :aliases => "-e", :type => :string,
24
- :banner => "Boot the app in the specified environment (default: development)"
25
- method_option :timeout, :aliases => "-t", :type => :numeric,
26
- :banner => "Time out connections after the specified admount of seconds (default: see Hatetepe::Server::CONFIG_DEFAULTS)"
27
- def start
28
- require "hatetepe/server"
29
- require "rack"
30
-
31
- config = config_for(options)
32
- ENV["RACK_ENV"] = config[:env]
33
-
34
- $stderr << "We're in #{config[:env]}\n"
35
- $stderr << "Booting from #{config[:rackup]}\n"
36
-
37
- EM.epoll
38
- EM.synchrony do
39
- $stderr << "Binding to #{config[:host]}:#{config[:port]}\n"
40
-
41
- trap("INT") { EM.stop }
42
- trap("TERM") { EM.stop }
43
- Server.start(config)
44
- end
45
- end
46
-
47
- private
48
-
49
- def config_for(options)
50
- rackup = File.expand_path(options[:rackup] || "config.ru")
51
- {
52
- env: options[:env] || ENV["RACK_ENV"] || "development",
53
- host: options[:bind] || "127.0.0.1",
54
- port: options[:port] || 3000,
55
- timeout: options[:timeout],
56
- app: Rack::Builder.parse_file(rackup)[0],
57
- rackup: rackup
58
- }
59
- end
60
- end
61
- end
@@ -1,73 +0,0 @@
1
- module Hatetepe
2
- module Connection
3
- attr_accessor :processing_enabled
4
- alias_method :processing_enabled?, :processing_enabled
5
-
6
- def remote_address
7
- sockaddr && sockaddr[1]
8
- end
9
-
10
- def remote_port
11
- sockaddr && sockaddr[0]
12
- end
13
-
14
- def sockaddr
15
- @sockaddr ||= Socket.unpack_sockaddr_in(get_peername) rescue nil
16
- end
17
-
18
- def connection_completed
19
- @connected = true
20
- end
21
-
22
- def connected?
23
- defined?(@connected) && @connected
24
- end
25
-
26
- def closed?
27
- !!defined?(@closed_by)
28
- end
29
-
30
- def closed_by_remote?
31
- @closed_by == :remote
32
- end
33
-
34
- def closed_by_self?
35
- @closed_by == :self
36
- end
37
-
38
- def closed_by_timeout?
39
- connected? && @closed_by == :timeout
40
- end
41
-
42
- def closed_by_connect_timeout?
43
- !connected? && @closed_by == :timeout
44
- end
45
-
46
- def close_connection(after_writing = false)
47
- @closed_by = :self unless closed?
48
- super
49
- end
50
-
51
- def unbind(reason)
52
- unless closed?
53
- @closed_by = if reason == Errno::ETIMEDOUT
54
- :timeout
55
- else
56
- :remote
57
- end
58
- end
59
- end
60
-
61
- def comm_inactivity_timeout=(seconds)
62
- unless defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
63
- super
64
- end
65
- end
66
-
67
- def pending_connect_timeout=(seconds)
68
- unless defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
69
- super
70
- end
71
- end
72
- end
73
- end
@@ -1,35 +0,0 @@
1
- module Hatetepe
2
- module Events
3
- def self.included(klass)
4
- klass.extend ClassMethods
5
- end
6
-
7
- attr_reader :state
8
-
9
- def event(name, *args)
10
- send(:"on_#{name}").each {|blk| blk.call *args }
11
- end
12
-
13
- def event!(name, *args)
14
- @state = name
15
- event name, *args
16
- end
17
-
18
- module ClassMethods
19
- def event(name, *more_names)
20
- define_method :"on_#{name}" do |&block|
21
- ivar = :"@on_#{name}"
22
- store = instance_variable_get(ivar)
23
- store ||= instance_variable_set(ivar, [])
24
-
25
- return store unless block
26
- store << block
27
- end
28
-
29
- define_method(:"#{name}?") { state == name }
30
-
31
- more_names.each &method(:event)
32
- end
33
- end
34
- end
35
- end
@@ -1,13 +0,0 @@
1
- require "hatetepe/body"
2
-
3
- module Hatetepe
4
- class Message
5
- attr_accessor :http_version, :headers, :body
6
- attr_accessor :connection
7
-
8
- def initialize(headers = {}, body = nil, http_version = "1.1")
9
- @headers, @http_version = headers, http_version
10
- @body = body || Body.new
11
- end
12
- end
13
- end
@@ -1,83 +0,0 @@
1
- require "http/parser"
2
-
3
- require "hatetepe/events"
4
- require "hatetepe/request"
5
- require "hatetepe/response"
6
-
7
- module Hatetepe
8
- class ParserError < StandardError; end
9
-
10
- class Parser
11
- include Events
12
-
13
- event :reset
14
- event :request, :response
15
- event :headers, :body
16
- event :trailing_header, :trailing_headers_complete
17
- event :complete
18
-
19
- attr_reader :message
20
-
21
- def initialize(&block)
22
- initialize_parser
23
- reset
24
-
25
- if block
26
- block.arity == 0 ? instance_eval(&block) : block.call(self)
27
- end
28
- end
29
-
30
- def initialize_parser
31
- @parser = HTTP::Parser.new.tap do |p|
32
- p.on_headers_complete = proc do |headers|
33
- headers_complete(p) if @headers_counter.even?
34
- @headers_counter += 1
35
- nil
36
- end
37
-
38
- p.on_body = method(:body)
39
- p.on_message_complete = method(:complete)
40
- end
41
- end
42
-
43
- def reset
44
- @parser.reset!
45
- event! :reset
46
- @message = nil
47
- @headers_counter = 0
48
- end
49
-
50
- def <<(data)
51
- @parser << data
52
- rescue HTTP::Parser::Error => e
53
- raise Hatetepe::ParserError, e.message, e.backtrace
54
- end
55
-
56
- private
57
-
58
- def headers_complete(parser)
59
- args = [ parser.headers, Body.new, parser.http_version.join(".") ]
60
- if parser.http_method
61
- @message = Request.new(parser.http_method, parser.request_url, *args)
62
- event! :request, @message
63
- else
64
- @message = Response.new(parser.status_code, *args)
65
- event! :response, @message
66
- end
67
-
68
- event! :headers, message.headers
69
- event! :body, message.body
70
- end
71
-
72
- def body(chunk)
73
- message.body.write chunk unless message.body.closed_write?
74
- end
75
-
76
- def complete
77
- message.body.rewind!
78
- message.body.close_write unless message.body.closed_write?
79
- event! :complete
80
- @headers_counter += 1 if @headers_counter.odd?
81
- end
82
- end
83
- end
@@ -1,20 +0,0 @@
1
- module Hatetepe::Server
2
- class Pipeline
3
- def initialize(app, connection)
4
- @requests, @app = [], app
5
- end
6
-
7
- def call(request, &respond)
8
- begin
9
- previous = @requests.last
10
- @requests << request
11
- @app.call(request) do |response|
12
- EM::Synchrony.sync(previous) if previous
13
- respond.call(response)
14
- end
15
- ensure
16
- @requests.delete(request)
17
- end
18
- end
19
- end
20
- end
@@ -1,39 +0,0 @@
1
- module Hatetepe::Server
2
- class RackApp
3
- def initialize(app, connection)
4
- @app, @connection = app, connection
5
- end
6
-
7
- def call(request, &respond)
8
- env = env_for(request)
9
- env["async.callback"] = proc do |response|
10
- async_callback(response, &respond)
11
- end
12
-
13
- response = [ -1 ]
14
- catch :async do
15
- response = @app.call(env)
16
- end
17
-
18
- async_callback(response, &respond)
19
- end
20
-
21
- def async_callback(response, &respond)
22
- if response[0] >= 0
23
- respond.call(Hatetepe::Response.new(*response))
24
- end
25
- end
26
-
27
- def env_for(request)
28
- request.to_h.merge({
29
- "SERVER_NAME" => @connection.config[:host],
30
- "SERVER_PORT" => @connection.config[:port].to_s,
31
- "rack.errors" => $stderr,
32
- "rack.multithread" => false,
33
- "rack.multiprocess" => false,
34
- "rack.run_once" => false,
35
- "rack.url_scheme" => "http"
36
- })
37
- end
38
- end
39
- end
@@ -1,33 +0,0 @@
1
- require "eventmachine"
2
- require "em-synchrony"
3
- require "hatetepe/server"
4
- require "rack"
5
-
6
- module Rack
7
- module Handler
8
- class Hatetepe
9
- def self.run(app, options = {})
10
- options = {
11
- :host => options[:Host] || "0.0.0.0",
12
- :port => options[:Port] || 8080,
13
- :app => app
14
- }
15
-
16
- Signal.trap("INT") { EM.stop }
17
- Signal.trap("TERM") { EM.stop }
18
-
19
- EM.epoll
20
- EM.synchrony { ::Hatetepe::Server.start options }
21
- end
22
-
23
- def self.valid_options
24
- {
25
- "Host=HOST" => "Hostname to listen on (default: 0.0.0.0 / all interfaces)",
26
- "Port=PORT" => "Port to listen on (default: 8080)",
27
- }
28
- end
29
- end
30
-
31
- register "hatetepe", Rack::Handler::Hatetepe
32
- end
33
- end