hatetepe 0.4.1 → 0.5.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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