ebb 0.0.1

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.
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