hatetepe 0.4.1 → 0.5.0.pre

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.
@@ -1,61 +1,23 @@
1
- class Hatetepe::Server
1
+ module Hatetepe::Server
2
2
  class KeepAlive
3
- attr_reader :app
4
-
5
- def initialize(app)
6
- @app = app
3
+ def initialize(app, connection)
4
+ @app, @connection = app, connection
7
5
  end
8
-
9
- def call(env)
10
- m = case env["HTTP_CONNECTION"].to_s.downcase
11
- when "close" then :close
12
- when "keep-alive" then :keep_alive
13
- else env["HTTP_VERSION"] =~ /^HTTP\/(0\.9|1\.0)$/ ? :close : :keep_alive
6
+
7
+ def call(request, &respond)
8
+ @app.call(request) do |response|
9
+ respond.call(response)
10
+ maybe_close(request, response)
14
11
  end
15
-
16
- send :"call_and_#{m}", env
17
12
  end
18
-
19
- def call_and_close(env, response = nil)
20
- req, conn = extract_request(env), extract_connection(env)
21
-
22
- conn.processing_enabled = false
23
- req.callback &conn.method(:close_connection_after_writing)
24
- req.errback &conn.method(:close_connection_after_writing)
25
-
26
- if response
27
- response[1]["Connection"] = "close"
28
- else
29
- stream_start = env["stream.start"]
30
- env["stream.start"] = proc do |res|
31
- res[1]["Connection"] = "close"
32
- stream_start.call res
33
- end
13
+
14
+ def maybe_close(request, response)
15
+ version = request.http_version.to_f
16
+ header = request.headers["Connection"] || response.headers["Connection"]
17
+
18
+ if (version < 1.1 && header != "keep-alive") || header == "close"
19
+ @connection.stop!
34
20
  end
35
-
36
- response || app.call(env)
37
- end
38
-
39
- def call_and_keep_alive(env)
40
- stream_start = env["stream.start"]
41
- env["stream.start"] = proc do |res|
42
- if res[1]["Connection"] == "close"
43
- call_and_close env, res
44
- else
45
- res[1]["Connection"] = "keep-alive"
46
- end
47
- stream_start.call res
48
- end
49
-
50
- app.call env
51
- end
52
-
53
- def extract_request(env)
54
- env["hatetepe.request"] || raise("env[hatetepe.request] not set")
55
- end
56
-
57
- def extract_connection(env)
58
- env["hatetepe.connection"] || raise("env[hatetepe.connection] not set")
59
21
  end
60
22
  end
61
23
  end
@@ -1,24 +1,20 @@
1
- require "em-synchrony"
2
-
3
- class Hatetepe::Server
4
- # TODO move specs from server_spec.rb to server/pipeline_spec.rb
1
+ module Hatetepe::Server
5
2
  class Pipeline
6
- attr_reader :app
7
-
8
- def initialize(app)
9
- @app = app
3
+ def initialize(app, connection)
4
+ @requests, @app = [], app
10
5
  end
11
-
12
- def call(env)
13
- previous = env["hatetepe.connection"].requests[-2]
14
-
15
- stream_start = env["stream.start"]
16
- env["stream.start"] = proc do |response|
17
- EM::Synchrony.sync previous if previous
18
- stream_start.call response
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)
19
17
  end
20
-
21
- app.call env
22
18
  end
23
19
  end
24
20
  end
@@ -0,0 +1,39 @@
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,137 +1,89 @@
1
1
  require "eventmachine"
2
2
  require "em-synchrony"
3
- require "rack"
4
3
 
5
4
  require "hatetepe/builder"
6
5
  require "hatetepe/connection"
7
6
  require "hatetepe/parser"
7
+ require "hatetepe/server/keep_alive"
8
+ require "hatetepe/server/pipeline"
9
+ require "hatetepe/server/rack_app"
8
10
  require "hatetepe/version"
9
11
 
10
- module Hatetepe
11
- class Server < Hatetepe::Connection; end
12
- end
12
+ module Hatetepe::Server
13
+ include Hatetepe::Connection
13
14
 
14
- require "hatetepe/server/app"
15
- require "hatetepe/server/keep_alive"
16
- require "hatetepe/server/pipeline"
17
- require "hatetepe/server/proxy"
15
+ attr_reader :config, :requests
16
+
17
+ CONFIG_DEFAULTS = { :timeout => 5.0 }
18
18
 
19
- class Hatetepe::Server
19
+ # @api public
20
20
  def self.start(config)
21
- EM.start_server config[:host], config[:port], self, config
21
+ EM.start_server(config[:host], config[:port], self, config)
22
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
23
 
31
- super
24
+ # @api semipublic
25
+ def initialize(config)
26
+ @config = CONFIG_DEFAULTS.merge(config)
32
27
  end
33
-
28
+
29
+ # @api semipublic
34
30
  def post_init
35
- @requests = []
36
31
  @parser, @builder = Hatetepe::Parser.new, Hatetepe::Builder.new
37
-
38
- parser.on_request << requests.method(:<<)
39
- parser.on_headers << method(:process)
40
-
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
32
+ @parser.on_request &method(:process_request)
33
+ @builder.on_write &method(:send_data)
34
+ # @builder.on_write {|data| p "<--| #{data}" }
35
+
36
+ @app = [
37
+ Pipeline,
38
+ KeepAlive,
39
+ RackApp
40
+ ].reverse.inject(config[:app]) {|inner, outer| outer.new(inner, self) }
41
+
55
42
  self.comm_inactivity_timeout = config[:timeout]
56
43
  end
57
-
44
+
45
+ # @api semipubic
58
46
  def receive_data(data)
59
- #p "server << #{data}"
60
- parser << data
61
- rescue Hatetepe::ParserError => ex
47
+ # p "-->| #{data}"
48
+ @parser << data
49
+ rescue Object => ex
50
+ p ex
62
51
  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
52
  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
53
+
54
+ # @api private
55
+ def process_request(request)
56
+ Fiber.new do
57
+ @app.call(request) do |response|
58
+ send_response(request, response)
59
+ end
60
+ end.resume
77
61
  end
78
-
79
- def process(*)
80
- return unless processing_enabled?
81
- request = requests.last
82
-
62
+
63
+ # @api private
64
+ def send_response(request, response)
83
65
  self.comm_inactivity_timeout = 0
84
- reset_timeout = proc do
85
- self.comm_inactivity_timeout = config[:timeout] if requests.empty?
86
- end
87
- request.callback &reset_timeout
88
- request.errback &reset_timeout
89
-
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
101
- end
66
+ @builder.response(response.to_a)
67
+ self.comm_inactivity_timeout = config[:timeout]
68
+
69
+ if response.failure?
70
+ request.fail(response)
71
+ else
72
+ request.succeed(response)
102
73
  end
103
-
104
- Fiber.new { app.call env }.resume
105
74
  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]
75
+
76
+ # @api semipublic
77
+ def unbind(reason)
78
+ super
111
79
  end
112
-
113
- def close_response(request)
114
- builder.complete
115
- requests.delete(request).succeed
80
+
81
+ # @api public
82
+ def stop
116
83
  end
117
-
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
84
+
85
+ # @api public
86
+ def stop!
87
+ close_connection_after_writing
136
88
  end
137
89
  end
@@ -1,3 +1,3 @@
1
1
  module Hatetepe
2
- VERSION = "0.4.1"
2
+ VERSION = "0.5.0.pre"
3
3
  end
@@ -1,5 +1,7 @@
1
1
  require "eventmachine"
2
+ require "em-synchrony"
2
3
  require "hatetepe/server"
4
+ require "rack"
3
5
 
4
6
  module Rack
5
7
  module Handler
@@ -1,120 +1,112 @@
1
1
  require "spec_helper"
2
2
  require "hatetepe/cli"
3
+ require "hatetepe/server"
4
+ require "hatetepe/version"
5
+ require "rack/builder"
6
+
7
+ describe Hatetepe::CLI do
8
+ let :rackup do
9
+ [ proc {|*| }, {} ]
10
+ end
3
11
 
4
- describe "The `hatetepe start' command" do
5
12
  before do
6
- ENV.delete "RACK_ENV"
7
-
8
- $stderr = StringIO.new
9
-
10
- FakeFS.activate!
11
- File.open "config.ru", "w" do |f|
12
- f.write 'run proc {|env| [200, {}, ["Hello world!"]] }'
13
- end
14
- File.open "config2.ru", "w" do |f|
15
- f.write 'run proc {|env| [501, {}, ["Herp derp"]] }'
16
- end
13
+ file = File.expand_path("config.ru")
14
+ Rack::Builder.stub(:parse_file).with(file) { rackup }
15
+
16
+ $stdout, $stderr = StringIO.new, StringIO.new
17
+ @old_env = ENV.delete("RACK_ENV")
17
18
  end
18
19
 
19
20
  after do
20
- $stderr = STDERR
21
-
22
- FakeFS.deactivate!
23
- FakeFS::FileSystem.clear
21
+ $stdout, $stderr = STDOUT, STDERR
22
+ ENV["RACK_ENV"] = @old_env
24
23
  end
25
-
26
- describe "without options" do
27
- it "starts a Hatetepe::Server with default options" do
28
- command "" do
29
- Socket.tcp("127.0.0.1", 3000) {|*| }
24
+
25
+ describe "#version" do
26
+ it "prints Hatetepe's version" do
27
+ Hatetepe::CLI.start([ "version" ])
28
+ $stdout.rewind
29
+ $stdout.read.should include(Hatetepe::VERSION)
30
+ end
31
+ end
32
+
33
+ describe "#start" do
34
+ it "starts a server running the default configuration" do
35
+ Hatetepe::Server.should_receive(:start) do |config|
36
+ config[:host].should == "127.0.0.1"
37
+ config[:port].should equal(3000)
38
+ config[:timeout].should == 5
39
+
40
+ config[:app].should equal(rackup[0])
30
41
  ENV["RACK_ENV"].should == "development"
31
- $stderr.string.should include("config.ru", "127.0.0.1:3000", "development")
32
42
  end
43
+ Hatetepe::CLI.start([])
33
44
  end
34
-
35
- it "serves HTTP requests" do
36
- command "" do
37
- Hatetepe::Client.get("http://127.0.0.1:3000").tap do |response|
38
- response.status.should equal(200)
39
- response.body.read.should == "Hello world!"
40
- end
41
- end
45
+
46
+ it "writes stuff to stderr" do
47
+ Hatetepe::CLI.start([])
48
+ $stderr.rewind
49
+ $stderr.read.should include("config.ru", "127.0.0.1:3000", "development")
42
50
  end
43
- end
44
-
45
- ["--port", "-p"].each do |opt|
46
- describe "with #{opt} option" do
47
- it "binds the Hatetepe::Server to the specified TCP port" do
48
- command "#{opt} 3002" do
49
- Socket.tcp("127.0.0.1", 3002) {|*| }
50
- end
51
- end
51
+
52
+ it "starts an EventMachine reactor" do
53
+ EM.should_receive(:synchrony)
54
+ Hatetepe::CLI.start([])
52
55
  end
53
- end
54
-
55
- ["--bind", "-b"].each do |opt|
56
- describe "with #{opt} option" do
57
- it "binds the Hatetepe::Server to the specified TCP interface" do
58
- command "#{opt} 127.0.0.2" do
59
- Socket.tcp("127.0.0.2", 3000) {|*| }
60
- end
61
- end
56
+
57
+ it "enables epoll" do
58
+ EM.should_receive(:epoll)
59
+ Hatetepe::CLI.start([])
62
60
  end
63
- end
64
-
65
- ["--rackup", "-r"].each do |opt|
66
- describe "with #{opt} option" do
67
- it "loads the specified rackup (.ru) file" do
68
- command "#{opt} config2.ru" do
69
- Hatetepe::Client.get("http://127.0.0.1:3000").tap do |response|
70
- response.status.should equal(501)
71
- response.body.read.should == "Herp derp"
72
- end
61
+
62
+ it "doesn't overwrite RACK_ENV" do
63
+ ENV["RACK_ENV"] = "foobar"
64
+ Hatetepe::CLI.start([])
65
+ ENV["RACK_ENV"].should == "foobar"
66
+ end
67
+
68
+ describe "with --bind option" do
69
+ it "passes the :host option to Server.start" do
70
+ Hatetepe::Server.should_receive(:start) do |config|
71
+ config[:host].should == "127.0.5.1"
73
72
  end
73
+ Hatetepe::CLI.start([ "--bind", "127.0.5.1" ])
74
74
  end
75
75
  end
76
- end
77
-
78
- ["--env", "-e"].each do |opt|
79
- describe "with #{opt} option" do
80
- it "boots the app in the specified environment" do
81
- command "#{opt} herpderp" do
82
- ENV["RACK_ENV"].should == "herpderp"
76
+
77
+ describe "with --port option" do
78
+ it "passes the :port option to Server.start" do
79
+ Hatetepe::Server.should_receive(:start) do |config|
80
+ config[:port].should == 5234
83
81
  end
82
+ Hatetepe::CLI.start([ "--port", "5234" ])
84
83
  end
85
-
86
- ["dev", "devel", "develop"].each do |value|
87
- it "expands #{value} to `development'" do
88
- command "#{opt} #{value}" do
89
- ENV["RACK_ENV"].should == "development"
90
- end
84
+ end
85
+
86
+ describe "with --timeout option" do
87
+ it "passes the :timeout option to Server.start" do
88
+ Hatetepe::Server.should_receive(:start) do |config|
89
+ config[:timeout].should == 123.4
91
90
  end
91
+ Hatetepe::CLI.start([ "--timeout", "123.4" ])
92
92
  end
93
-
94
- it "expands test to `testing'" do
95
- command "#{opt} test" do
96
- ENV["RACK_ENV"].should == "testing"
93
+ end
94
+
95
+ describe "with --rackup option" do
96
+ it "boots from the specified RackUp file" do
97
+ file = File.expand_path("./other_config.ru")
98
+ Rack::Builder.should_receive(:parse_file).with(file) { rackup }
99
+ Hatetepe::Server.should_receive(:start) do |config|
100
+ config[:app].should equal(rackup[0])
97
101
  end
102
+ Hatetepe::CLI.start([ "--rackup", "other_config.ru" ])
98
103
  end
99
104
  end
100
- end
101
-
102
- ["--timeout", "-t"].each do |opt|
103
- describe "with #{opt} option" do
104
- let :client do
105
- Hatetepe::Client.start :host => "127.0.0.1", :port => 3000
106
- end
107
-
108
- it "times out a connection after the specified amount of seconds" do
109
- command "#{opt} 0.5", 1 do
110
- client.should_not be_closed
111
105
 
112
- EM::Synchrony.sleep 0.45
113
- client.should_not be_closed
114
-
115
- EM::Synchrony.sleep 0.1
116
- client.should be_closed_by_remote
117
- end
106
+ describe "with --env option" do
107
+ it "sets RACK_ENV to the specified value" do
108
+ Hatetepe::CLI.start([ "--env", "production" ])
109
+ ENV["RACK_ENV"].should == "production"
118
110
  end
119
111
  end
120
112
  end
@@ -5,70 +5,19 @@ require "stringio"
5
5
  require "yaml"
6
6
 
7
7
  describe Hatetepe::Client, "with Keep-Alive" do
8
- before do
9
- $stderr = StringIO.new
10
-
11
- FakeFS.activate!
12
- File.open "config.ru", "w" do |f|
13
- f.write 'run proc {|env| [200,
14
- {"Content_Type" => "text/plain"},
15
- [env["HTTP_CONNECTION"]]] }'
16
- end
17
- File.open "config_close.ru", "w" do |f|
18
- f.write 'run proc {|env| [200,
19
- {"Content-Type" => "text/plain",
20
- "Connection" => "close"},
21
- [env["HTTP_CONNECTION"]]] }'
22
- end
23
- end
24
-
25
- after do
26
- $stderr = STDERR
27
-
28
- FakeFS.deactivate!
29
- FakeFS::FileSystem.clear
30
- end
31
-
32
8
  let :client do
33
9
  Hatetepe::Client.start :host => "127.0.0.1", :port => 30001
34
10
  end
35
11
 
36
- it "keeps the connection open" do
37
- command "-p 30001", 2 do
38
- client
39
- EM::Synchrony.sleep 1.95
40
- client.should_not be_closed_by_self
41
- end
42
- end
12
+ it "keeps the connection open"
43
13
 
44
- it "sends Connection: keep-alive" do
45
- command "-p 30001" do
46
- client.get("/").body.read.should == "keep-alive"
47
- end
48
- end
14
+ it "sends Connection: keep-alive"
49
15
 
50
16
  describe "and an obviously single request" do
51
- it "sends Connection: close" do
52
- command "-p 30001" do
53
- Hatetepe::Client.get "http://127.0.0.1:30001/" do |response|
54
- YAML.load(response.body.read).should == "close"
55
- end
56
- end
57
- end
17
+ it "sends Connection: close"
58
18
 
59
- it "closes the connection immediately after the response" do
60
- command "-p 30001" do
61
- #Hatetepe::Client.any_instance.should_receive :stop
62
- Hatetepe::Client.get "http://127.0.0.1:30001/"
63
- end
64
- end
19
+ it "closes the connection immediately after the response"
65
20
  end
66
21
 
67
- it "closes the connection if the server tells it to" do
68
- #pending "Server can't send Conn: close as its Keep-Alive middleware overwrites it"
69
- command "-p 30001 -r config_close.ru" do
70
- client.get "/"
71
- client.should be_closed
72
- end
73
- end
22
+ it "closes the connection if the server tells it to"
74
23
  end