ebb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,119 @@
1
+ # A Web Server Called *Ebb*
2
+
3
+ Ebb aims to be a small and fast web server specifically for hosting
4
+ web frameworks like Rails, Merb, and in the future Django.
5
+
6
+ It is not meant to be a full featured web server like Lighttpd, Apache, or
7
+ Nginx. Rather it should be used in multiplicity behind a
8
+ [load balancer](http://brainspl.at/articles/2007/11/09/a-fair-proxy-balancer-for-nginx-and-mongrel)
9
+ and a front-end server. It is not meant to serve static files in production.
10
+
11
+ ## Design
12
+
13
+ The design is similar to the
14
+ [Evented Mongrel](http://swiftiply.swiftcore.org/mongrel.html) web server;
15
+ except instead of using EventMachine (a ruby binding to libevent), the Ebb
16
+ web server is written in C and uses the
17
+ [libev](http://software.schmorp.de/pkg/libev.html) event loop library.
18
+
19
+ Connections are processed as follows:
20
+
21
+ 1. libev loops and waits for incoming connections.
22
+
23
+ 2. When Ebb receives a connection, it passes the request into the
24
+ [mongrel state machine](http://mongrel.rubyforge.org/browser/tags/rel_1-0-1/ext/http11/http11_parser.rl)
25
+ which securely parses the headers.
26
+
27
+ 3. When the request is complete, Ebb passes the information to a user
28
+ supplied callback.
29
+
30
+ 4. The Ruby binding supplying this callback transforms the
31
+ request into a [Rack](http://rack.rubyforge.org/) compatible `env` hash
32
+ and passes it on a Rack adapter.
33
+
34
+ Because Ebb is written mostly in C, other language bindings can be added to
35
+ make it useful to Non-Ruby frameworks. For example, a Python WSGI interface is
36
+ forthcoming.
37
+
38
+ ## Download
39
+
40
+ The Ruby binding is available as a Ruby Gem. It can be install by executing
41
+
42
+ `gem install ebb`
43
+
44
+ Ebb depends on having glib2 headers and libraries installed. (Easily available
45
+ on any UNIX system.) A manual downloads can be found at
46
+ the [RubyForge project page](http://rubyforge.org/frs/?group_id=5640).
47
+
48
+ ## Why?
49
+
50
+ Because by building the server in C one is able to side-step the
51
+ limitations on speed of many scripting languages. Inefficiencies are okay
52
+ for quick and beautiful code, but for production web servers that might handle
53
+ thousands of requests a second, an attempt should be made to be as efficient
54
+ as possible in processing connections.
55
+
56
+ Following are some benchmarks. Please take these measurements with a grain
57
+ of salt. Benchmarks like these are notorious for presenting an inaccurate
58
+ or highly slanted view of how software performs.
59
+ The code for these can be found in the `benchmark` directory.
60
+
61
+ ![Response Size](http://s3.amazonaws.com/four.livejournal/20080227/response_size.png)
62
+
63
+ This shows how the web servers perform with respect to throughput (using a
64
+ simple Rack application). Concurrency is at 50 clients.
65
+
66
+ ![Concurrency](http://s3.amazonaws.com/four.livejournal/20080227/concurrency.png)
67
+
68
+ A simple concurrent clients benchmark serving a *hello world* page.
69
+
70
+ ![Uploads](http://s3.amazonaws.com/four.livejournal/20080227/post_size.png)
71
+
72
+ Ebb processes uploads before handing it over to the web application. This
73
+ allows Ebb to continue to process other clients while the upload is in
74
+ progress. The cliff at 40k here is because Ebb's internal request
75
+ buffer is set at 40 kilobytes before it writes to file.
76
+
77
+
78
+ ## Contributions
79
+
80
+
81
+ Contributions (patches, criticism, advice) are very welcome! The source code
82
+ is hosted at [repo.or.cz](http://repo.or.cz/w/ebb.git). It can be retrieved
83
+ by executing
84
+
85
+ `git clone http://repo.or.cz/r/ebb.git`
86
+
87
+ I intend to keep the C code base very small, so do email me before writing any
88
+ large additions. Here are some features that I would like to add:
89
+
90
+ * Multipart parser
91
+ * Streaming responses
92
+ * Optimize and clean up upload handling
93
+ * Option to listen on unix sockets instead of TCP
94
+ * Python binding
95
+
96
+ ## (The MIT) License
97
+
98
+ Copyright © 2008 [Ry Dahl](http://tinyclouds.org) (ry at tiny clouds dot org)
99
+
100
+ <div id="license">
101
+ Permission is hereby granted, free of charge, to any person obtaining
102
+ a copy of this software and associated documentation files (the
103
+ "Software"), to deal in the Software without restriction, including
104
+ without limitation the rights to use, copy, modify, merge, publish,
105
+ distribute, sublicense, and/or sell copies of the Software, and to
106
+ permit persons to whom the Software is furnished to do so, subject to
107
+ the following conditions:
108
+
109
+ The above copyright notice and this permission notice shall be
110
+ included in all copies or substantial portions of the Software.
111
+
112
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
113
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
114
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
115
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
116
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
117
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
118
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
119
+ </div>
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,84 @@
1
+ DIR = File.dirname(__FILE__)
2
+
3
+ def fib(n)
4
+ return 1 if n <= 1
5
+ fib(n-1) + fib(n-2)
6
+ end
7
+
8
+ def wait(seconds)
9
+ n = (seconds / 0.01).to_i
10
+ n.times do
11
+ sleep(0.01)
12
+ #File.read(DIR + '/yahoo.html')
13
+ end
14
+ end
15
+
16
+ class SimpleApp
17
+ @@responses = {}
18
+
19
+ def initialize
20
+ @count = 0
21
+ end
22
+
23
+ def call(env)
24
+ commands = env['PATH_INFO'].split('/')
25
+
26
+ @count += 1
27
+ if commands.include?('periodical_activity') and @count % 10 != 1
28
+ return [200, {'Content-Type'=>'text/plain'}, "quick response!\r\n"]
29
+ end
30
+
31
+ if commands.include?('fibonacci')
32
+ n = commands.last.to_i
33
+ raise "fibonacci called with n <= 0" if n <= 0
34
+ body = (1..n).to_a.map { |i| fib(i).to_s }.join(' ')
35
+ status = 200
36
+
37
+ elsif commands.include?('wait')
38
+ n = commands.last.to_i
39
+ raise "wait called with n <= 0" if n <= 0
40
+ wait(n)
41
+ body = "waited about #{n} seconds"
42
+ status = 200
43
+
44
+ elsif commands.include?('bytes')
45
+ n = commands.last.to_i
46
+ raise "bytes called with n <= 0" if n <= 0
47
+ body = @@responses[n] || "C"*n
48
+ status = 200
49
+
50
+ elsif commands.include?('test_post_length')
51
+ input_body = ""
52
+ while chunk = env['rack.input'].read(512)
53
+ input_body << chunk
54
+ end
55
+ if env['HTTP_CONTENT_LENGTH'].to_i == input_body.length
56
+ body = "Content-Length matches input length"
57
+ status = 200
58
+ else
59
+ body = "Content-Length doesn't matches input length!
60
+ content_length = #{env['HTTP_CONTENT_LENGTH'].to_i}
61
+ input_body.length = #{input_body.length}"
62
+ status = 500
63
+ end
64
+
65
+ else
66
+ status = 404
67
+ body = "Undefined url"
68
+ end
69
+
70
+ [status, {'Content-Type' => 'text/plain'}, body + "\r\n\r\n"]
71
+ end
72
+ end
73
+
74
+
75
+ if $0 == __FILE__
76
+ require DIR + '/../ruby_lib/ebb'
77
+ require 'rubygems'
78
+ require 'ruby-debug'
79
+ Debugger.start
80
+
81
+ server = Ebb::Server.new(SimpleApp.new, :port => 4001)
82
+ puts "Ebb started on http://0.0.0.0:4001/"
83
+ server.start
84
+ end
@@ -0,0 +1,45 @@
1
+ # supply the benchmark dump file as an argumetn to this program
2
+ require 'rubygems'
3
+ require 'google_chart'
4
+ require 'server_test'
5
+
6
+ class Array
7
+ def avg
8
+ sum.to_f / length
9
+ end
10
+ def sum
11
+ inject(0) { |i, s| s += i }
12
+ end
13
+ end
14
+
15
+
16
+
17
+ colors = %w{F74343 444130 7DA478 E4AC3D}
18
+ max_x = 0
19
+ max_y = 0
20
+ results = ServerTestResults.open(ARGV[0])
21
+ all_m = []
22
+ response_chart = GoogleChart::LineChart.new('400x300', Time.now.strftime('%Y.%m.%d'), true)
23
+ results.servers.each do |server|
24
+ data = results.data(server).sort
25
+ response_chart.data(server, data, colors.shift)
26
+ x = data.map { |d| d[0] }.max
27
+ y = data.map { |d| d[1] }.max
28
+ max_x = x if x > max_x
29
+ max_y = y if y > max_y
30
+ end
31
+
32
+ label = case results.benchmark
33
+ when "response_size"
34
+ "kilobytes served"
35
+ when "wait_fib", "concurrency"
36
+ "concurrency"
37
+ when "post_size"
38
+ "kilobytes uploaded"
39
+ end
40
+
41
+ response_chart.axis(:y, :range => [0,max_y])
42
+ response_chart.axis(:y, :labels => ['req/s'], :positions => [50])
43
+ response_chart.axis(:x, :range => [0,max_x])
44
+ response_chart.axis(:x, :labels => [label], :positions => [50])
45
+ puts response_chart.to_url
@@ -0,0 +1,152 @@
1
+ $: << File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'rubygems'
4
+ require 'rack'
5
+ require 'application'
6
+
7
+
8
+ class Array
9
+ def avg
10
+ sum.to_f / length
11
+ end
12
+
13
+ def sum
14
+ inject(0) { |i, s| s += i }
15
+ end
16
+
17
+ def rand_each(&block)
18
+ sort_by{ rand }.each &block
19
+ end
20
+ end
21
+
22
+ class ServerTestResults
23
+ def self.open(filename)
24
+ if File.readable?(filename)
25
+ new(Marshal.load(File.read(filename)))
26
+ else
27
+ new
28
+ end
29
+ end
30
+
31
+ def initialize(results = [])
32
+ @results = results
33
+ end
34
+
35
+ def benchmark
36
+ @results.first[:benchmark]
37
+ end
38
+
39
+ def write(filename='results.dump')
40
+ puts "writing dump file to #{filename}"
41
+ File.open(filename, 'w+') do |f|
42
+ f.write Marshal.dump(@results)
43
+ end
44
+ end
45
+
46
+ def <<(r)
47
+ @results << r
48
+ end
49
+
50
+ def length
51
+ @results.length
52
+ end
53
+
54
+ def servers
55
+ @results.map {|r| r[:server] }.uniq.sort
56
+ end
57
+
58
+ def data(server)
59
+ server_data = @results.find_all { |r| r[:server] == server }
60
+ ticks = server_data.map { |d| d[:input] }.uniq
61
+ datas = []
62
+ ticks.each do |c|
63
+ measurements = server_data.find_all { |d| d[:input] == c }.map { |d| d[:rps] }
64
+ datas << [c, measurements.avg]
65
+ end
66
+ datas
67
+ end
68
+
69
+ end
70
+
71
+ class ServerTest
72
+ attr_reader :name, :port, :app, :pid
73
+ def initialize(name, port, &start_block)
74
+ @name = name
75
+ @port = port.to_i
76
+ end
77
+
78
+ def <=>(a)
79
+ @name <=> a.name
80
+ end
81
+
82
+ def kill
83
+ Process.kill('KILL', @pid)
84
+ end
85
+
86
+ def running?
87
+ !@pid.nil?
88
+ end
89
+
90
+ def start
91
+ puts "Starting #{name}"
92
+ case name
93
+ when 'emongrel'
94
+ @pid = fork { start_emongrel }
95
+ when 'ebb'
96
+ @pid = fork { start_ebb }
97
+ when 'mongrel'
98
+ @pid = fork { start_mongrel }
99
+ when 'thin'
100
+ @pid = fork { start_thin }
101
+ end
102
+ end
103
+
104
+ def app
105
+ SimpleApp.new
106
+ end
107
+
108
+ def start_emongrel
109
+ require 'mongrel'
110
+ require 'swiftcore/evented_mongrel'
111
+ ENV['EVENT'] = "1"
112
+ Rack::Handler::Mongrel.run(app, :Host => '0.0.0.0', :Port => @port.to_i)
113
+ end
114
+
115
+ def start_ebb
116
+ require File.dirname(__FILE__) + '/../ruby_lib/ebb'
117
+ server = Ebb::Server.run(app, :port => @port)
118
+ end
119
+
120
+ def start_mongrel
121
+ require 'mongrel'
122
+ ENV.delete('EVENT')
123
+ Rack::Handler::Mongrel.run(app, :Port => @port)
124
+ end
125
+
126
+ def start_thin
127
+ require 'thin'
128
+ Rack::Handler::Thin.run(app, :Port => @port)
129
+ end
130
+
131
+ def trial(ab_cmd)
132
+ cmd = ab_cmd.sub('PORT', @port.to_s)
133
+
134
+ puts "#{@name} (#{cmd})"
135
+
136
+ r = %x{#{cmd}}
137
+
138
+ return nil unless r =~ /Requests per second:\s*(\d+\.\d\d)/
139
+ rps = $1.to_f
140
+ if r =~ /Complete requests:\s*(\d+)/
141
+ requests_completed = $1.to_i
142
+ end
143
+ puts " #{rps} req/sec (#{requests_completed} completed)"
144
+
145
+ {
146
+ :server => @name,
147
+ :rps => rps,
148
+ :requests_completed => requests_completed,
149
+ :ab_cmd => cmd
150
+ }
151
+ end
152
+ end
data/benchmark/test.rb ADDED
@@ -0,0 +1,141 @@
1
+ require File.dirname(__FILE__) + '/../ruby_lib/ebb'
2
+ require 'test/unit'
3
+ require 'net/http'
4
+ require 'base64'
5
+
6
+
7
+ class EbbTest < Test::Unit::TestCase
8
+ def setup
9
+ @pid = fork do
10
+ server = Ebb::Server.new(self, :port => 4044)
11
+ server.start
12
+ end
13
+ sleep 0.5
14
+ end
15
+
16
+ def teardown
17
+ Process.kill('KILL', @pid)
18
+ sleep 0.5
19
+ end
20
+
21
+ def get(path)
22
+ Net::HTTP.get_response(URI.parse("http://0.0.0.0:4044#{path}"))
23
+ end
24
+
25
+ def post(path, data)
26
+ Net::HTTP.post_form(URI.parse("http://0.0.0.0:4044#{path}"), data)
27
+ end
28
+
29
+ @@responses = {}
30
+ def call(env)
31
+ commands = env['PATH_INFO'].split('/')
32
+
33
+ if commands.include?('bytes')
34
+ n = commands.last.to_i
35
+ raise "bytes called with n <= 0" if n <= 0
36
+ body = @@responses[n] || "C"*n
37
+ status = 200
38
+
39
+ elsif commands.include?('env')
40
+ env.delete('rack.input') # delete this because it's hard to marshal
41
+ env.delete('rack.errors')
42
+ body = Base64.encode64(Marshal.dump(env))
43
+ status = 200
44
+
45
+ elsif commands.include?('test_post_length')
46
+ input_body = ""
47
+ while chunk = env['rack.input'].read(512)
48
+ input_body << chunk
49
+ end
50
+
51
+ content_length_header = env['HTTP_CONTENT_LENGTH'].to_i
52
+
53
+ if content_length_header == input_body.length
54
+ body = "Content-Length matches input length"
55
+ status = 200
56
+ else
57
+ body = "Content-Length header is #{content_length_header} but body length is #{input_body.length}"
58
+ # content_length = #{env['HTTP_CONTENT_LENGTH'].to_i}
59
+ # input_body.length = #{input_body.length}"
60
+ status = 500
61
+ end
62
+
63
+ else
64
+ status = 404
65
+ body = "Undefined url"
66
+ end
67
+
68
+ [status, {'Content-Type' => 'text/plain'}, body]
69
+ end
70
+
71
+ def test_get_bytes
72
+ [1,10,1000].each do |i|
73
+ response = get("/bytes/#{i}")
74
+ assert_equal "#{'C'*i.to_i}", response.body
75
+ end
76
+ end
77
+
78
+ def test_get_unknown
79
+ response = get('/blah')
80
+ assert_equal "Undefined url", response.body
81
+ end
82
+
83
+ def test_small_posts
84
+ [1,10,321,123,1000].each do |i|
85
+ response = post("/test_post_length", 'C'*i)
86
+ assert_equal 200, response.code.to_i, response.body
87
+ end
88
+ end
89
+
90
+ # this is rough but does detect major problems
91
+ def test_ab
92
+ r = %x{ab -n 1000 -c 50 -q http://0.0.0.0:4044/bytes/123}
93
+ assert r =~ /Requests per second:\s*(\d+)/, r
94
+ assert $1.to_i > 100, r
95
+ end
96
+
97
+ def test_large_post
98
+ [50,60,100].each do |i|
99
+ response = post("/test_post_length", 'C'*1024*i)
100
+ assert_equal 200, response.code.to_i, response.body
101
+ end
102
+ end
103
+
104
+ def test_env
105
+ response = get('/env')
106
+ env = Marshal.load(Base64.decode64(response.body))
107
+ assert_equal '/env', env['PATH_INFO']
108
+ assert_equal '/env', env['REQUEST_PATH']
109
+ assert_equal 'HTTP/1.1', env['SERVER_PROTOCOL']
110
+ assert_equal 'CGI/1.2', env['GATEWAY_INTERFACE']
111
+ assert_equal '0.0.0.0', env['SERVER_NAME']
112
+ assert_equal '4044', env['SERVER_PORT']
113
+ assert_equal 'GET', env['REQUEST_METHOD']
114
+ end
115
+ end
116
+
117
+ class EbbRailsTest < Test::Unit::TestCase
118
+ # just to make sure there isn't some load error
119
+ def test_ebb_rails_version
120
+ out = %x{ruby #{Ebb::LIBDIR}/../bin/ebb_rails -v}
121
+ assert_match %r{Ebb #{Ebb::VERSION}}, out
122
+ end
123
+ end
124
+
125
+
126
+ #
127
+ # class SocketTest < Test::Unit::TestCase
128
+ # def test_socket_creation
129
+ # filename = '/tmp/ebb.socket'
130
+ # @pid = fork do
131
+ # server = Ebb::Server.new(TestApp.new, {:socket => filename})
132
+ # server.start
133
+ # end
134
+ # sleep(1)
135
+ # assert File.exists?(filename)
136
+ #
137
+ # Process.kill('KILL', @pid)
138
+ #
139
+ # assert !File.exists?(filename)
140
+ # end
141
+ # end