rhebok 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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.gitmodules +3 -0
- data/.travis.yml +8 -0
- data/Changes +4 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +378 -0
- data/README.md +208 -0
- data/Rakefile +21 -0
- data/ext/rhebok/extconf.rb +2 -0
- data/ext/rhebok/picohttpparser/.gitattributes +1 -0
- data/ext/rhebok/picohttpparser/.gitmodules +3 -0
- data/ext/rhebok/picohttpparser/.travis.yml +6 -0
- data/ext/rhebok/picohttpparser/Makefile +40 -0
- data/ext/rhebok/picohttpparser/README.md +24 -0
- data/ext/rhebok/picohttpparser/bench.c +53 -0
- data/ext/rhebok/picohttpparser/picohttpparser.c +434 -0
- data/ext/rhebok/picohttpparser/picohttpparser.h +63 -0
- data/ext/rhebok/picohttpparser/test.c +253 -0
- data/ext/rhebok/rhebok.c +795 -0
- data/lib/rack/handler/rhebok.rb +210 -0
- data/lib/rhebok.rb +7 -0
- data/lib/rhebok/version.rb +3 -0
- data/rhebok.gemspec +51 -0
- data/test/spec_02_basic.rb +7 -0
- data/test/spec_02_server.rb +87 -0
- data/test/testrequest.rb +79 -0
- metadata +143 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
4
|
+
require 'rubygems'
|
5
|
+
require 'rack'
|
6
|
+
require 'stringio'
|
7
|
+
require 'tempfile'
|
8
|
+
require 'socket'
|
9
|
+
require 'rack/utils'
|
10
|
+
require 'io/nonblock'
|
11
|
+
require 'prefork_engine'
|
12
|
+
require 'rhebok'
|
13
|
+
|
14
|
+
module Rack
|
15
|
+
module Handler
|
16
|
+
class Rhebok
|
17
|
+
MAX_MEMORY_BUFFER_SIZE = 1024 * 1024
|
18
|
+
DEFAULT_OPTIONS = {
|
19
|
+
:Host => '0.0.0.0',
|
20
|
+
:Port => 9292,
|
21
|
+
:MaxWorkers => 10,
|
22
|
+
:Timeout => 300,
|
23
|
+
:MaxRequestPerChild => 1000,
|
24
|
+
:MinRequestPerChild => nil,
|
25
|
+
:SpawnInterval => nil,
|
26
|
+
:ErrRespawnInterval => nil
|
27
|
+
}
|
28
|
+
NULLIO = StringIO.new("").set_encoding('BINARY')
|
29
|
+
|
30
|
+
def self.run(app, options={})
|
31
|
+
slf = new(options)
|
32
|
+
slf.setup_listener()
|
33
|
+
slf.run_worker(app)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.valid_options
|
37
|
+
{
|
38
|
+
"Host=HOST" => "Hostname to listen on (default: 0.0.0.0)",
|
39
|
+
"Port=PORT" => "Port to listen on (default: 9292)",
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(options={})
|
44
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
45
|
+
@server = nil
|
46
|
+
@_is_tcp = false
|
47
|
+
@_using_defer_accept = false
|
48
|
+
end
|
49
|
+
|
50
|
+
def setup_listener()
|
51
|
+
if ENV["SERVER_STARTER_PORT"] then
|
52
|
+
hostport, fd = ENV["SERVER_STARTER_PORT"].split("=",2)
|
53
|
+
if m = hostport.match(/(.*):(\d+)/) then
|
54
|
+
@options[:Host] = m[0]
|
55
|
+
@options[:Port] = m[1].to_i
|
56
|
+
else
|
57
|
+
@options[:Port] = hostport
|
58
|
+
end
|
59
|
+
@server = TCPServer.for_fd(fd.to_i)
|
60
|
+
@_is_tcp = true if !@server.local_address.unix?
|
61
|
+
end
|
62
|
+
|
63
|
+
if @server == nil
|
64
|
+
puts "Rhebok starts Listening on #{@options[:Host]}:#{@options[:Port]} Pid:#{$$}"
|
65
|
+
@server = TCPServer.new(@options[:Host], @options[:Port])
|
66
|
+
@server.setsockopt(:SOCKET, :REUSEADDR, 1)
|
67
|
+
@_is_tcp = true
|
68
|
+
end
|
69
|
+
|
70
|
+
if RUBY_PLATFORM.match(/linux/) && @_is_tcp == true then
|
71
|
+
begin
|
72
|
+
@server.setsockopt(Socket::IPPROTO_TCP, 9, 1)
|
73
|
+
@_using_defer_accept = true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def run_worker(app)
|
79
|
+
pm_args = {
|
80
|
+
"max_workers" => @options[:MaxWorkers].to_i,
|
81
|
+
"trap_signals" => {
|
82
|
+
"TERM" => 'TERM',
|
83
|
+
"HUP" => 'TERM',
|
84
|
+
},
|
85
|
+
}
|
86
|
+
if @options[:SpawnInterval] then
|
87
|
+
pm_args["trap_signals"]["USR1"] = ["TERM", @options[:SpawnInterval].to_i]
|
88
|
+
pm_args["spawn_interval"] = @options[:SpawnInterval].to_i
|
89
|
+
end
|
90
|
+
if @options[:ErrRespawnInterval] then
|
91
|
+
pm_args["err_respawn_interval"] = @options[:ErrRespawnInterval].to_i
|
92
|
+
end
|
93
|
+
Signal.trap('INT','SYSTEM_DEFAULT') # XXX
|
94
|
+
|
95
|
+
pe = PreforkEngine.new(pm_args)
|
96
|
+
while !pe.signal_received.match(/^(TERM|USR1)$/)
|
97
|
+
pe.start {
|
98
|
+
srand
|
99
|
+
self.accept_loop(app)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
pe.wait_all_children
|
103
|
+
end
|
104
|
+
|
105
|
+
def _calc_reqs_per_child
|
106
|
+
max = @options[:MaxRequestPerChild].to_i
|
107
|
+
if min = @options[:MinRequestPerChild] then
|
108
|
+
return (max - (max - min.to_i + 1) * rand).to_i
|
109
|
+
end
|
110
|
+
return max.to_i
|
111
|
+
end
|
112
|
+
|
113
|
+
def accept_loop(app)
|
114
|
+
@can_exit = true
|
115
|
+
@term_received = 0
|
116
|
+
proc_req_count = 0
|
117
|
+
Signal.trap(:TERM) do
|
118
|
+
@term_received += 1
|
119
|
+
if @can_exit then
|
120
|
+
exit!(true)
|
121
|
+
end
|
122
|
+
if @can_exit || @term_received > 1 then
|
123
|
+
exit!(true)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
Signal.trap(:PIPE, "IGNORE")
|
127
|
+
max_reqs = self._calc_reqs_per_child()
|
128
|
+
fileno = @server.fileno
|
129
|
+
|
130
|
+
env_template = {
|
131
|
+
"SERVER_NAME" => @options[:Host],
|
132
|
+
"SERVER_PORT" => @options[:Port].to_s,
|
133
|
+
"rack.version" => [1,1],
|
134
|
+
"rack.errors" => STDERR,
|
135
|
+
"rack.multithread" => false,
|
136
|
+
"rack.multiprocess" => true,
|
137
|
+
"rack.run_once" => false,
|
138
|
+
"rack.url_scheme" => "http",
|
139
|
+
"rack.input" => NULLIO
|
140
|
+
}
|
141
|
+
|
142
|
+
while proc_req_count < max_reqs
|
143
|
+
@can_exit = true
|
144
|
+
env = env_template.clone
|
145
|
+
connection, buf = ::Rhebok.accept_rack(fileno, @options[:Timeout], @_is_tcp, env)
|
146
|
+
if connection then
|
147
|
+
# for tempfile
|
148
|
+
buffer = nil
|
149
|
+
begin
|
150
|
+
proc_req_count += 1
|
151
|
+
@can_exit = false
|
152
|
+
# handle request
|
153
|
+
if env.key?("CONTENT_LENGTH") && env["CONTENT_LENGTH"].to_i > 0 then
|
154
|
+
cl = env["CONTENT_LENGTH"].to_i;
|
155
|
+
if cl > MAX_MEMORY_BUFFER_SIZE
|
156
|
+
buffer = Tempfile.open('r')
|
157
|
+
buffer.binmode
|
158
|
+
buffer.set_encoding('BINARY')
|
159
|
+
else
|
160
|
+
buffer = StringIO.new("").set_encoding('BINARY')
|
161
|
+
end
|
162
|
+
while cl > 0
|
163
|
+
chunk = ""
|
164
|
+
if buf.bytesize > 0 then
|
165
|
+
chunk = buf
|
166
|
+
buf = ""
|
167
|
+
else
|
168
|
+
readed = ::Rhebok.read_timeout(connection, chunk, cl, 0, @options[:Timeout])
|
169
|
+
if readed == nil then
|
170
|
+
return
|
171
|
+
end
|
172
|
+
end
|
173
|
+
buffer << chunk
|
174
|
+
cl -= chunk.bytesize
|
175
|
+
end
|
176
|
+
buffer.rewind
|
177
|
+
env["rack.input"] = buffer
|
178
|
+
end
|
179
|
+
|
180
|
+
status_code, headers, body = app.call(env)
|
181
|
+
if body.instance_of?(Array) then
|
182
|
+
::Rhebok.write_response(connection, @options[:Timeout], status_code, headers, body);
|
183
|
+
else
|
184
|
+
::Rhebok.write_response(connection, @options[:Timeout], status_code, headers, []);
|
185
|
+
body.each do |part|
|
186
|
+
ret = ::Rhebok.write_all(connection, part, 0, @options[:Timeout])
|
187
|
+
if ret == nil then
|
188
|
+
break
|
189
|
+
end
|
190
|
+
end #body.each
|
191
|
+
end
|
192
|
+
ensure
|
193
|
+
if buffer.instance_of?(Tempfile)
|
194
|
+
buffer.close!
|
195
|
+
end
|
196
|
+
::Rhebok.close_rack(connection)
|
197
|
+
end #begin
|
198
|
+
end # accept
|
199
|
+
if @term_received > 0 then
|
200
|
+
exit!(true)
|
201
|
+
end
|
202
|
+
end #while max_reqs
|
203
|
+
end #def
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
|
data/lib/rhebok.rb
ADDED
data/rhebok.gemspec
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rhebok/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rhebok"
|
8
|
+
spec.version = Rhebok::VERSION
|
9
|
+
spec.authors = ["Masahiro Nagano"]
|
10
|
+
spec.email = ["kazeburo@gmail.com"]
|
11
|
+
spec.summary = %q{High Perfomance Perforked Rack Handler}
|
12
|
+
spec.description = %q{High Perfomance and Optimized Perforked Rack Handler}
|
13
|
+
spec.homepage = "https://github.com/kazeburo/rhebok"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "bacon"
|
24
|
+
spec.add_dependency "rack", "~> 1.5.2"
|
25
|
+
spec.add_dependency "prefork_engine", "~> 0.0.4"
|
26
|
+
|
27
|
+
# get an array of submodule dirs by executing 'pwd' inside each submodule
|
28
|
+
`git submodule --quiet foreach pwd`.split($\).each do |submodule_path|
|
29
|
+
# for each submodule, change working directory to that submodule
|
30
|
+
Dir.chdir(submodule_path) do
|
31
|
+
|
32
|
+
# issue git ls-files in submodule's directory
|
33
|
+
submodule_files = `git ls-files`.split($\)
|
34
|
+
|
35
|
+
# prepend the submodule path to create absolute file paths
|
36
|
+
submodule_files_fullpaths = submodule_files.map do |filename|
|
37
|
+
"#{submodule_path}/#{filename}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# remove leading path parts to get paths relative to the gem's root dir
|
41
|
+
# (this assumes, that the gemspec resides in the gem's root dir)
|
42
|
+
submodule_files_paths = submodule_files_fullpaths.map do |filename|
|
43
|
+
filename.gsub "#{File.dirname(__FILE__)}/", ""
|
44
|
+
end
|
45
|
+
|
46
|
+
# add relative paths to gem.files
|
47
|
+
spec.files += submodule_files_paths
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require File.expand_path('../testrequest', __FILE__)
|
3
|
+
require 'timeout'
|
4
|
+
require 'rack/handler/rhebok'
|
5
|
+
|
6
|
+
describe Rhebok do
|
7
|
+
extend TestRequest::Helpers
|
8
|
+
begin
|
9
|
+
|
10
|
+
@host = '127.0.0.1'
|
11
|
+
@port = 9202
|
12
|
+
@app = Rack::Lint.new(TestRequest.new)
|
13
|
+
@pid = fork
|
14
|
+
if @pid == nil
|
15
|
+
#child
|
16
|
+
Rack::Handler::Rhebok.run(@app, :Host=>'127.0.0.1', :Port=>9202, :MaxWorkers=>1)
|
17
|
+
exit!(true)
|
18
|
+
end
|
19
|
+
sleep 1
|
20
|
+
|
21
|
+
# test
|
22
|
+
should "respond" do
|
23
|
+
GET("/")
|
24
|
+
response.should.not.be.nil
|
25
|
+
end
|
26
|
+
|
27
|
+
should "be a Rhebok" do
|
28
|
+
GET("/")
|
29
|
+
status.should.equal 200
|
30
|
+
response["SERVER_PROTOCOL"].should.equal "HTTP/1.1"
|
31
|
+
response["SERVER_PORT"].should.equal "9202"
|
32
|
+
response["SERVER_NAME"].should.equal "127.0.0.1"
|
33
|
+
end
|
34
|
+
|
35
|
+
should "have rack headers" do
|
36
|
+
GET("/")
|
37
|
+
response["rack.version"].should.equal [1,1]
|
38
|
+
response["rack.multithread"].should.equal false
|
39
|
+
response["rack.multiprocess"].should.equal true
|
40
|
+
response["rack.run_once"].should.equal false
|
41
|
+
end
|
42
|
+
|
43
|
+
should "have CGI headers on GET" do
|
44
|
+
GET("/")
|
45
|
+
response["REQUEST_METHOD"].should.equal "GET"
|
46
|
+
response["PATH_INFO"].should.be.equal "/"
|
47
|
+
response["QUERY_STRING"].should.equal ""
|
48
|
+
response["test.postdata"].should.equal ""
|
49
|
+
|
50
|
+
GET("/test/foo?quux=1")
|
51
|
+
response["REQUEST_METHOD"].should.equal "GET"
|
52
|
+
response["PATH_INFO"].should.equal "/test/foo"
|
53
|
+
response["QUERY_STRING"].should.equal "quux=1"
|
54
|
+
end
|
55
|
+
|
56
|
+
should "have CGI headers on POST" do
|
57
|
+
POST("/", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
|
58
|
+
status.should.equal 200
|
59
|
+
response["REQUEST_METHOD"].should.equal "POST"
|
60
|
+
response["QUERY_STRING"].should.equal ""
|
61
|
+
response["HTTP_X_TEST_HEADER"].should.equal "42"
|
62
|
+
response["test.postdata"].should.equal "rack-form-data=23"
|
63
|
+
end
|
64
|
+
|
65
|
+
should "support HTTP auth" do
|
66
|
+
GET("/test", {:user => "ruth", :passwd => "secret"})
|
67
|
+
response["HTTP_AUTHORIZATION"].should.equal "Basic cnV0aDpzZWNyZXQ="
|
68
|
+
end
|
69
|
+
|
70
|
+
should "set status" do
|
71
|
+
GET("/test?secret")
|
72
|
+
status.should.equal 403
|
73
|
+
response["rack.url_scheme"].should.equal "http"
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
ensure
|
78
|
+
sleep 1
|
79
|
+
if @pid != nil
|
80
|
+
Process.kill(:TERM, @pid)
|
81
|
+
Process.wait()
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
|
data/test/testrequest.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'net/http'
|
3
|
+
require 'rack/lint'
|
4
|
+
|
5
|
+
class TestRequest
|
6
|
+
NOSERIALIZE = [Method, Proc, Rack::Lint::InputWrapper]
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
status = env["QUERY_STRING"] =~ /secret/ ? 403 : 200
|
10
|
+
env["test.postdata"] = env["rack.input"].read
|
11
|
+
minienv = env.dup
|
12
|
+
# This may in the future want to replace with a dummy value instead.
|
13
|
+
minienv.delete_if { |k,v| NOSERIALIZE.any? { |c| v.kind_of?(c) } }
|
14
|
+
body = minienv.to_yaml
|
15
|
+
size = body.respond_to?(:bytesize) ? body.bytesize : body.size
|
16
|
+
[status, {"Content-Type" => "text/yaml", "Content-Length" => size.to_s}, [body]]
|
17
|
+
end
|
18
|
+
|
19
|
+
module Helpers
|
20
|
+
attr_reader :status, :response
|
21
|
+
|
22
|
+
ROOT = File.expand_path(File.dirname(__FILE__) + "/..")
|
23
|
+
ENV["RUBYOPT"] = "-I#{ROOT}/lib -rubygems"
|
24
|
+
|
25
|
+
def root
|
26
|
+
ROOT
|
27
|
+
end
|
28
|
+
|
29
|
+
def rackup
|
30
|
+
"#{ROOT}/bin/rackup"
|
31
|
+
end
|
32
|
+
|
33
|
+
def GET(path, header={})
|
34
|
+
Net::HTTP.start(@host, @port) { |http|
|
35
|
+
user = header.delete(:user)
|
36
|
+
passwd = header.delete(:passwd)
|
37
|
+
|
38
|
+
get = Net::HTTP::Get.new(path, header)
|
39
|
+
get.basic_auth user, passwd if user && passwd
|
40
|
+
http.request(get) { |response|
|
41
|
+
@status = response.code.to_i
|
42
|
+
begin
|
43
|
+
@response = YAML.load(response.body)
|
44
|
+
rescue TypeError, ArgumentError
|
45
|
+
@response = nil
|
46
|
+
end
|
47
|
+
}
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def POST(path, formdata={}, header={})
|
52
|
+
Net::HTTP.start(@host, @port) { |http|
|
53
|
+
user = header.delete(:user)
|
54
|
+
passwd = header.delete(:passwd)
|
55
|
+
|
56
|
+
post = Net::HTTP::Post.new(path, header)
|
57
|
+
post.form_data = formdata
|
58
|
+
post.basic_auth user, passwd if user && passwd
|
59
|
+
http.request(post) { |response|
|
60
|
+
@status = response.code.to_i
|
61
|
+
@response = YAML.load(response.body)
|
62
|
+
}
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class StreamingRequest
|
69
|
+
def self.call(env)
|
70
|
+
[200, {"Content-Type" => "text/plain"}, new]
|
71
|
+
end
|
72
|
+
|
73
|
+
def each
|
74
|
+
yield "hello there!\n"
|
75
|
+
sleep 5
|
76
|
+
yield "that is all.\n"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|