neverblock 0.1.6.2
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 +24 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/lib/active_record/connection_adapters/neverblock_mysql_adapter.rb +68 -0
- data/lib/active_record/connection_adapters/neverblock_postgresql_adapter.rb +85 -0
- data/lib/never_block.rb +102 -0
- data/lib/never_block/db/fibered_db_connection.rb +72 -0
- data/lib/never_block/db/fibered_mysql_connection.rb +62 -0
- data/lib/never_block/db/fibered_postgres_connection.rb +64 -0
- data/lib/never_block/db/pooled_db_connection.rb +43 -0
- data/lib/never_block/extensions/fiber_extensions.rb +31 -0
- data/lib/never_block/frameworks/activerecord.rb +37 -0
- data/lib/never_block/frameworks/rails.rb +65 -0
- data/lib/never_block/pool/fiber_pool.rb +74 -0
- data/lib/never_block/pool/fibered_connection_pool.rb +130 -0
- data/lib/never_block/servers/mongrel.rb +236 -0
- data/lib/never_block/servers/thin.rb +32 -0
- data/lib/neverblock-mysql.rb +5 -0
- data/lib/neverblock-pg.rb +5 -0
- data/lib/neverblock.rb +8 -0
- data/neverblock.gemspec +73 -0
- data/spec/fiber_extensions_spec.rb +24 -0
- data/spec/fiber_pool_spec.rb +60 -0
- data/spec/fibered_connection_pool_spec.rb +106 -0
- data/tasks/spec.rake +12 -0
- data/test/test_mysql.rb +36 -0
- data/test/test_pg.rb +102 -0
- metadata +97 -0
@@ -0,0 +1,236 @@
|
|
1
|
+
# This module rewrites pieces of the very good Mongrel web server in
|
2
|
+
# order to change it from a threaded application to an event based
|
3
|
+
# application running inside an EventMachine event loop. It should
|
4
|
+
# be compatible with the existing Mongrel handlers for Rails,
|
5
|
+
# Camping, Nitro, etc....
|
6
|
+
|
7
|
+
# NeverBlock support added
|
8
|
+
|
9
|
+
begin
|
10
|
+
load_attempted ||= false
|
11
|
+
require 'eventmachine'
|
12
|
+
rescue LoadError
|
13
|
+
unless load_attempted
|
14
|
+
load_attempted = true
|
15
|
+
require 'rubygems'
|
16
|
+
retry
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'rubygems'
|
21
|
+
require 'neverblock'
|
22
|
+
require 'mongrel'
|
23
|
+
|
24
|
+
module Mongrel
|
25
|
+
class MongrelProtocol < EventMachine::Connection
|
26
|
+
def post_init
|
27
|
+
@parser = HttpParser.new
|
28
|
+
@params = HttpParams.new
|
29
|
+
@nparsed = 0
|
30
|
+
@request = nil
|
31
|
+
@request_len = nil
|
32
|
+
@linebuffer = ''
|
33
|
+
end
|
34
|
+
|
35
|
+
def receive_data data
|
36
|
+
@linebuffer << data
|
37
|
+
@nparsed = @parser.execute(@params, @linebuffer, @nparsed) unless @parser.finished?
|
38
|
+
if @parser.finished?
|
39
|
+
if @request_len.nil?
|
40
|
+
@request_len = @params[::Mongrel::Const::CONTENT_LENGTH].to_i
|
41
|
+
script_name, path_info, handlers = ::Mongrel::HttpServer::Instance.classifier.resolve(@params[::Mongrel::Const::REQUEST_PATH])
|
42
|
+
if handlers
|
43
|
+
@params[::Mongrel::Const::PATH_INFO] = path_info
|
44
|
+
@params[::Mongrel::Const::SCRIPT_NAME] = script_name
|
45
|
+
@params[::Mongrel::Const::REMOTE_ADDR] = @params[::Mongrel::Const::HTTP_X_FORWARDED_FOR] || ::Socket.unpack_sockaddr_in(get_peername)[1]
|
46
|
+
@notifiers = handlers.select { |h| h.request_notify }
|
47
|
+
end
|
48
|
+
if @request_len > ::Mongrel::Const::MAX_BODY
|
49
|
+
new_buffer = Tempfile.new(::Mongrel::Const::MONGREL_TMP_BASE)
|
50
|
+
new_buffer.binmode
|
51
|
+
new_buffer << @linebuffer[@nparsed..-1]
|
52
|
+
@linebuffer = new_buffer
|
53
|
+
else
|
54
|
+
@linebuffer = StringIO.new(@linebuffer[@nparsed..-1])
|
55
|
+
@linebuffer.pos = @linebuffer.length
|
56
|
+
end
|
57
|
+
end
|
58
|
+
if @linebuffer.length >= @request_len
|
59
|
+
@linebuffer.rewind
|
60
|
+
::Mongrel::HttpServer::Instance.process_http_request(@params,@linebuffer,self)
|
61
|
+
@linebuffer.delete if Tempfile === @linebuffer
|
62
|
+
end
|
63
|
+
elsif @linebuffer.length > ::Mongrel::Const::MAX_HEADER
|
64
|
+
close_connection
|
65
|
+
raise ::Mongrel::HttpParserError.new("HEADER is longer than allowed, aborting client early.")
|
66
|
+
end
|
67
|
+
rescue ::Mongrel::HttpParserError
|
68
|
+
if $mongrel_debug_client
|
69
|
+
STDERR.puts "#{Time.now}: BAD CLIENT (#{params[Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last}): #$!"
|
70
|
+
STDERR.puts "#{Time.now}: REQUEST DATA: #{data.inspect}\n---\nPARAMS: #{params.inspect}\n---\n"
|
71
|
+
end
|
72
|
+
close_connection
|
73
|
+
rescue Exception => e
|
74
|
+
close_connection
|
75
|
+
raise e
|
76
|
+
end
|
77
|
+
|
78
|
+
def write data
|
79
|
+
send_data data
|
80
|
+
end
|
81
|
+
|
82
|
+
def closed?
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
class HttpServer
|
89
|
+
DEFAULT_FIBER_POOL_SIZE = 20
|
90
|
+
def initialize(host, port, num_processors=950, x=0, y=nil) # Deal with Mongrel 1.0.1 or earlier, as well as later.
|
91
|
+
@socket = nil
|
92
|
+
@classifier = URIClassifier.new
|
93
|
+
@host = host
|
94
|
+
@port = port
|
95
|
+
@workers = ThreadGroup.new
|
96
|
+
if y
|
97
|
+
@throttle = x
|
98
|
+
@timeout = y || 60
|
99
|
+
else
|
100
|
+
@timeout = x
|
101
|
+
end
|
102
|
+
@num_processors = num_processors #num_processors is pointless for evented....
|
103
|
+
@death_time = 60
|
104
|
+
self.class.const_set(:Instance,self)
|
105
|
+
end
|
106
|
+
|
107
|
+
def fiber_pool
|
108
|
+
@fiber_pool ||= NB::Pool::FiberPool.new(DEFAULT_FIBER_POOL_SIZE)
|
109
|
+
end
|
110
|
+
|
111
|
+
def run
|
112
|
+
trap('INT') { raise StopServer }
|
113
|
+
trap('TERM') { raise StopServer }
|
114
|
+
@acceptor = Thread.new do
|
115
|
+
EventMachine.epoll
|
116
|
+
EventMachine.set_descriptor_table_size(4096)
|
117
|
+
EventMachine.run do
|
118
|
+
begin
|
119
|
+
EventMachine.start_server(@host,@port,MongrelProtocol)
|
120
|
+
rescue StopServer
|
121
|
+
EventMachine.stop_event_loop
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def process_http_request(params,linebuffer,client)
|
128
|
+
if not params[Const::REQUEST_PATH]
|
129
|
+
uri = URI.parse(params[Const::REQUEST_URI])
|
130
|
+
params[Const::REQUEST_PATH] = uri.request_uri
|
131
|
+
end
|
132
|
+
|
133
|
+
raise "No REQUEST PATH" if not params[Const::REQUEST_PATH]
|
134
|
+
|
135
|
+
script_name, path_info, handlers = @classifier.resolve(params[Const::REQUEST_PATH])
|
136
|
+
|
137
|
+
if handlers
|
138
|
+
notifiers = handlers.select { |h| h.request_notify }
|
139
|
+
request = HttpRequest.new(params, linebuffer, notifiers)
|
140
|
+
|
141
|
+
# request is good so far, continue processing the response
|
142
|
+
response = HttpResponse.new(client)
|
143
|
+
|
144
|
+
# Process each handler in registered order until we run out or one finalizes the response.
|
145
|
+
fiber_pool.spawn do
|
146
|
+
dispatch_to_handlers(handlers,request,response)
|
147
|
+
end
|
148
|
+
# And finally, if nobody closed the response off, we finalize it.
|
149
|
+
unless response.done
|
150
|
+
response.finished
|
151
|
+
else
|
152
|
+
response.close_connection_after_writing
|
153
|
+
end
|
154
|
+
else
|
155
|
+
# Didn't find it, return a stock 404 response.
|
156
|
+
client.send_data(Const::ERROR_404_RESPONSE)
|
157
|
+
client.close_connection_after_writing
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def dispatch_to_handlers(handlers,request,response)
|
162
|
+
handlers.each do |handler|
|
163
|
+
handler.process(request, response)
|
164
|
+
break if response.done
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class HttpRequest
|
170
|
+
def initialize(params, linebuffer, dispatchers)
|
171
|
+
@params = params
|
172
|
+
@dispatchers = dispatchers
|
173
|
+
@body = linebuffer
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class HttpResponse
|
178
|
+
def send_file(path, small_file = false)
|
179
|
+
File.open(path, "rb") do |f|
|
180
|
+
while chunk = f.read(Const::CHUNK_SIZE) and chunk.length > 0
|
181
|
+
begin
|
182
|
+
write(chunk)
|
183
|
+
rescue Object => exc
|
184
|
+
break
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
@body_sent = true
|
189
|
+
end
|
190
|
+
|
191
|
+
def write(data)
|
192
|
+
@socket.send_data data
|
193
|
+
end
|
194
|
+
|
195
|
+
def close_connection_after_writing
|
196
|
+
@socket.close_connection_after_writing
|
197
|
+
end
|
198
|
+
|
199
|
+
def socket_error(details)
|
200
|
+
@socket.close_connection
|
201
|
+
done = true
|
202
|
+
raise details
|
203
|
+
end
|
204
|
+
|
205
|
+
def finished
|
206
|
+
send_status
|
207
|
+
send_header
|
208
|
+
send_body
|
209
|
+
@socket.close_connection_after_writing
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
class Configurator
|
214
|
+
# This version fixes a bug in the regular Mongrel version by adding
|
215
|
+
# initialization of groups.
|
216
|
+
def change_privilege(user, group)
|
217
|
+
if user and group
|
218
|
+
log "Initializing groups for {#user}:{#group}."
|
219
|
+
Process.initgroups(user,Etc.getgrnam(group).gid)
|
220
|
+
end
|
221
|
+
|
222
|
+
if group
|
223
|
+
log "Changing group to #{group}."
|
224
|
+
Process::GID.change_privilege(Etc.getgrnam(group).gid)
|
225
|
+
end
|
226
|
+
|
227
|
+
if user
|
228
|
+
log "Changing user to #{user}."
|
229
|
+
Process::UID.change_privilege(Etc.getpwnam(user).uid)
|
230
|
+
end
|
231
|
+
rescue Errno::EPERM
|
232
|
+
log "FAILED to change user:group #{user}:#{group}: #$!"
|
233
|
+
exit 1
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'neverblock' unless defined?(NeverBlock)
|
3
|
+
require 'thin'
|
4
|
+
|
5
|
+
module Thin
|
6
|
+
|
7
|
+
# Patch the thin server to use NeverBlock::Pool::FiberPool to be able to
|
8
|
+
# wrap requests in fibers
|
9
|
+
class Server
|
10
|
+
|
11
|
+
DEFAULT_FIBER_POOL_SIZE = 20
|
12
|
+
|
13
|
+
def fiber_pool
|
14
|
+
@fiber_pool ||= NB::Pool::FiberPool.new(DEFAULT_FIBER_POOL_SIZE)
|
15
|
+
end
|
16
|
+
|
17
|
+
end # Server
|
18
|
+
|
19
|
+
# A request is processed by wrapping it in a fiber from the fiber pool.
|
20
|
+
# If all the fibers are busy the request will wait in a queue to be picked up
|
21
|
+
# later. Meanwhile, the server will still be processing requests
|
22
|
+
class Connection < EventMachine::Connection
|
23
|
+
|
24
|
+
def process
|
25
|
+
@request.threaded = false
|
26
|
+
@backend.server.fiber_pool.spawn {post_process(pre_process)}
|
27
|
+
end
|
28
|
+
|
29
|
+
end # Connection
|
30
|
+
|
31
|
+
|
32
|
+
end # Thin
|
data/lib/neverblock.rb
ADDED
data/neverblock.gemspec
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{neverblock}
|
8
|
+
s.version = "0.1.6.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Muhammad A. Ali", "Ahmed Sobhi", "Osama Brekaa", "Nicholas Silva"]
|
12
|
+
s.date = %q{2010-02-03}
|
13
|
+
s.description = %q{NeverBlock is a collection of classes and modules that help you write evented non-blocking applications in a seemingly blocking mannner.}
|
14
|
+
s.email = %q{conickal@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"README",
|
20
|
+
"Rakefile",
|
21
|
+
"VERSION",
|
22
|
+
"lib/active_record/connection_adapters/neverblock_mysql_adapter.rb",
|
23
|
+
"lib/active_record/connection_adapters/neverblock_postgresql_adapter.rb",
|
24
|
+
"lib/never_block.rb",
|
25
|
+
"lib/never_block/db/fibered_db_connection.rb",
|
26
|
+
"lib/never_block/db/fibered_mysql_connection.rb",
|
27
|
+
"lib/never_block/db/fibered_postgres_connection.rb",
|
28
|
+
"lib/never_block/db/pooled_db_connection.rb",
|
29
|
+
"lib/never_block/extensions/fiber_extensions.rb",
|
30
|
+
"lib/never_block/frameworks/activerecord.rb",
|
31
|
+
"lib/never_block/frameworks/rails.rb",
|
32
|
+
"lib/never_block/pool/fiber_pool.rb",
|
33
|
+
"lib/never_block/pool/fibered_connection_pool.rb",
|
34
|
+
"lib/never_block/servers/mongrel.rb",
|
35
|
+
"lib/never_block/servers/thin.rb",
|
36
|
+
"lib/neverblock-mysql.rb",
|
37
|
+
"lib/neverblock-pg.rb",
|
38
|
+
"lib/neverblock.rb",
|
39
|
+
"neverblock.gemspec",
|
40
|
+
"spec/fiber_extensions_spec.rb",
|
41
|
+
"spec/fiber_pool_spec.rb",
|
42
|
+
"spec/fibered_connection_pool_spec.rb",
|
43
|
+
"tasks/spec.rake",
|
44
|
+
"test/test_mysql.rb",
|
45
|
+
"test/test_pg.rb"
|
46
|
+
]
|
47
|
+
s.homepage = %q{http://github.com/conickal/neverblock}
|
48
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
49
|
+
s.require_paths = ["lib"]
|
50
|
+
s.rubygems_version = %q{1.3.5}
|
51
|
+
s.summary = %q{Utilities for non-blocking stack components}
|
52
|
+
s.test_files = [
|
53
|
+
"spec/fiber_extensions_spec.rb",
|
54
|
+
"spec/fiber_pool_spec.rb",
|
55
|
+
"spec/fibered_connection_pool_spec.rb",
|
56
|
+
"test/test_mysql.rb",
|
57
|
+
"test/test_pg.rb"
|
58
|
+
]
|
59
|
+
|
60
|
+
if s.respond_to? :specification_version then
|
61
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
62
|
+
s.specification_version = 3
|
63
|
+
|
64
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
65
|
+
s.add_runtime_dependency(%q<eventmachine>, [">= 0.12.2"])
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<eventmachine>, [">= 0.12.2"])
|
68
|
+
end
|
69
|
+
else
|
70
|
+
s.add_dependency(%q<eventmachine>, [">= 0.12.2"])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
$:.unshift File.expand_path('..')
|
2
|
+
require 'lib/neverblock'
|
3
|
+
|
4
|
+
describe Fiber do
|
5
|
+
before(:all) do
|
6
|
+
@fiber = Fiber.new {puts "I'm a new fiber"}
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should be able to set fiber local variable" do
|
10
|
+
@fiber[:x] = "wow"
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be able to retrieve an already set fiber local variable" do
|
14
|
+
@fiber[:x].should == "wow"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return nil when trying to retrieve an unset fiber local variable" do
|
18
|
+
@fiber[:y].should == nil
|
19
|
+
end
|
20
|
+
|
21
|
+
after(:all) do
|
22
|
+
@fiber = nil
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
$:.unshift File.expand_path('..')
|
2
|
+
require 'lib/neverblock'
|
3
|
+
|
4
|
+
describe NeverBlock::Pool::FiberPool do
|
5
|
+
before(:each) do
|
6
|
+
@fiber_pool = NeverBlock::Pool::FiberPool.new(10)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should have all fibers ready and an empty queue initially" do
|
10
|
+
@fiber_pool.fibers.length.should == 10
|
11
|
+
@fiber_pool.instance_variable_get(:@queue).length.should == 0
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should have fibers with :neverblock fiber variable set to true" do
|
15
|
+
@fiber_pool.fibers.each {|f| f[:neverblock].should == true}
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should process a new block if there are available fibers" do
|
19
|
+
x = false
|
20
|
+
@fiber_pool.spawn {x = true}
|
21
|
+
x.should == true
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should queue requests if requests are more than fibers" do
|
25
|
+
progress = Array.new(15, false)
|
26
|
+
fibers = []
|
27
|
+
@fiber_pool.fibers.each {|f| fibers << f}
|
28
|
+
10.times do |i|
|
29
|
+
@fiber_pool.spawn {Fiber.yield; progress[i] = true}
|
30
|
+
end
|
31
|
+
@fiber_pool.fibers.length.should == 0
|
32
|
+
@fiber_pool.instance_variable_get(:@queue).length.should == 0
|
33
|
+
|
34
|
+
#it should now queue
|
35
|
+
(10..14).each {|i| @fiber_pool.spawn {progress[i] = true}}
|
36
|
+
@fiber_pool.fibers.length.should == 0
|
37
|
+
@fiber_pool.instance_variable_get(:@queue).length.should == 5
|
38
|
+
|
39
|
+
#resume the first fiber, this should also process the queued requests
|
40
|
+
fibers[0].resume
|
41
|
+
@fiber_pool.fibers.length.should == 1
|
42
|
+
@fiber_pool.instance_variable_get(:@queue).length.should == 0
|
43
|
+
[0,*10..14].each {|i| progress[i].should == true}
|
44
|
+
(1..9).to_a.each {|i| progress[i].should == false}
|
45
|
+
fibers_count = 1
|
46
|
+
(1..9).to_a.each do |i|
|
47
|
+
fibers[i].resume
|
48
|
+
fibers_count = fibers_count + 1
|
49
|
+
progress[i].should == true
|
50
|
+
@fiber_pool.fibers.length.should == fibers_count
|
51
|
+
end
|
52
|
+
@fiber_pool.instance_variable_get(:@queue).length.should == 0
|
53
|
+
end
|
54
|
+
|
55
|
+
after(:each) do
|
56
|
+
@fiber_pool = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|