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.
@@ -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
@@ -0,0 +1,7 @@
1
+ require "rhebok/version"
2
+ require "rhebok/rhebok"
3
+
4
+ class Rhebok
5
+ # see Rack::Handler::Rhebok
6
+ end
7
+
@@ -0,0 +1,3 @@
1
+ class Rhebok
2
+ VERSION = "0.0.1"
3
+ end
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,7 @@
1
+ require 'rack/handler/rhebok'
2
+
3
+ describe Rhebok do
4
+ should 'has a version number' do
5
+ Rhebok::VERSION.should.not.equal nil
6
+ end
7
+ 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
+
@@ -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
+