m2r 0.0.3 → 1.0.0
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/Gemfile +6 -0
- data/README.md +141 -35
- data/Rakefile +13 -45
- data/example/Procfile +4 -0
- data/example/config.sqlite +0 -0
- data/example/http_0mq.rb +37 -19
- data/example/lobster.ru +14 -6
- data/example/mongrel2.conf +47 -0
- data/example/tmp/access.log +505 -0
- data/example/uploading.ru +37 -0
- data/lib/m2r.rb +49 -3
- data/lib/m2r/connection.rb +66 -0
- data/lib/m2r/connection_factory.rb +41 -0
- data/lib/m2r/handler.rb +130 -0
- data/lib/m2r/headers.rb +72 -0
- data/lib/m2r/rack_handler.rb +47 -0
- data/lib/m2r/request.rb +129 -0
- data/lib/m2r/request/base.rb +33 -0
- data/lib/m2r/request/upload.rb +60 -0
- data/lib/m2r/response.rb +102 -0
- data/lib/m2r/response/content_length.rb +18 -0
- data/lib/m2r/version.rb +5 -0
- data/lib/rack/handler/mongrel2.rb +33 -0
- data/m2r.gemspec +30 -63
- data/test/acceptance/examples_test.rb +32 -0
- data/test/support/capybara.rb +4 -0
- data/test/support/mongrel_helper.rb +40 -0
- data/test/support/test_handler.rb +51 -0
- data/test/support/test_user.rb +37 -0
- data/test/test_helper.rb +5 -0
- data/test/unit/connection_factory_test.rb +29 -0
- data/test/unit/connection_test.rb +49 -0
- data/test/unit/handler_test.rb +41 -0
- data/test/unit/headers_test.rb +50 -0
- data/test/unit/m2r_test.rb +40 -0
- data/test/unit/rack_handler_test.rb +52 -0
- data/test/unit/request_test.rb +38 -0
- data/test/unit/response_test.rb +30 -0
- metadata +310 -105
- data/.document +0 -5
- data/.gitignore +0 -21
- data/ISSUES +0 -62
- data/VERSION +0 -1
- data/benchmarks/jruby +0 -60
- data/example/rack_handler.rb +0 -69
- data/lib/connection.rb +0 -158
- data/lib/fiber_handler.rb +0 -43
- data/lib/handler.rb +0 -66
- data/lib/request.rb +0 -44
- data/test/helper.rb +0 -10
- data/test/test_m2r.rb +0 -7
@@ -0,0 +1,37 @@
|
|
1
|
+
# Running this example:
|
2
|
+
#
|
3
|
+
# m2sh load -config mongrel2.conf
|
4
|
+
# bundle exec foreman start
|
5
|
+
#
|
6
|
+
# Browse now to http://localhost:6767/uploading to see the effect.
|
7
|
+
#
|
8
|
+
# This example is not threadsafe !
|
9
|
+
|
10
|
+
$stdout.sync = true
|
11
|
+
$stderr.sync = true
|
12
|
+
|
13
|
+
require 'rack'
|
14
|
+
require 'pathname'
|
15
|
+
|
16
|
+
use Rack::ContentLength
|
17
|
+
size = 0
|
18
|
+
app = Proc.new do |env|
|
19
|
+
req = Rack::Request.new(env)
|
20
|
+
if req.post?
|
21
|
+
size = req.params["file"][:tempfile].size.to_s rescue size = 0
|
22
|
+
end
|
23
|
+
note = req.post?? "You submitted file of size: #{size}" : "Last submitted file was of size: #{size}"
|
24
|
+
body = <<-EOF
|
25
|
+
<html>
|
26
|
+
<body>
|
27
|
+
<form name="uploading" id="uploading_form" method="post" enctype="multipart/form-data" action="/uploading">
|
28
|
+
<input type="file" name="file" id="file"></input>
|
29
|
+
<input type="submit" name="submit" id="submit">Submit</input>
|
30
|
+
</form>
|
31
|
+
<p>#{note}</p>
|
32
|
+
</body>
|
33
|
+
</html>
|
34
|
+
EOF
|
35
|
+
[200, {'Content-Type' => 'text/html'}, [body]]
|
36
|
+
end
|
37
|
+
run app
|
data/lib/m2r.rb
CHANGED
@@ -1,3 +1,49 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
1
|
+
require 'ffi-rzmq'
|
2
|
+
require 'multi_json'
|
3
|
+
require 'tnetstring'
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
# Allows you to easily interact with Mongrel2 webserver from
|
7
|
+
# your ruby code.
|
8
|
+
# @api public
|
9
|
+
module M2R
|
10
|
+
class << self
|
11
|
+
|
12
|
+
# Sets ZMQ context used by M2R to create sockets
|
13
|
+
# @param [ZMQ::Context] value Context to by used by M2R
|
14
|
+
# @see #zmq_context
|
15
|
+
# @api public
|
16
|
+
attr_writer :zmq_context
|
17
|
+
|
18
|
+
# Gets (or sets if not existing) ZMQ context used by M2R
|
19
|
+
# to create sockets.
|
20
|
+
#
|
21
|
+
# @note This method is thread-safe
|
22
|
+
# but it uses Thread.exclusive to achive that.
|
23
|
+
# However it is unlikely that it affects the performance as you probably
|
24
|
+
# do not create more than a dozen of sockets in your code.
|
25
|
+
#
|
26
|
+
# @param [Fixnum] zmq_io_threads Size of the ZMQ thread pool to handle I/O operations.
|
27
|
+
# The rule of thumb is to make it equal to the number gigabits per second
|
28
|
+
# that the application will produce.
|
29
|
+
#
|
30
|
+
# @return [ZMQ::Context]
|
31
|
+
# @see #zmq_context=
|
32
|
+
# @api public
|
33
|
+
def zmq_context(zmq_io_threads = 1)
|
34
|
+
Thread.exclusive do
|
35
|
+
@zmq_context ||= ZMQ::Context.new(zmq_io_threads)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @deprecated: Use M2R instead
|
42
|
+
# Namespace used in the past in 0.0.* gem releases
|
43
|
+
Mongrel2 = M2R
|
44
|
+
|
45
|
+
require 'm2r/request'
|
46
|
+
require 'm2r/response'
|
47
|
+
require 'm2r/connection'
|
48
|
+
require 'm2r/connection_factory'
|
49
|
+
require 'm2r/handler'
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'm2r'
|
2
|
+
|
3
|
+
module M2R
|
4
|
+
# Connection for exchanging data with mongrel2
|
5
|
+
class Connection
|
6
|
+
|
7
|
+
# @param [ZMQ::Socket] request_socket socket for receiving requests
|
8
|
+
# from Mongrel2
|
9
|
+
# @param [ZMQ::Socket] response_socket socket for sending responses
|
10
|
+
# to Mongrel2
|
11
|
+
# @param [#parse] request_parser Object responsible for parsing Mongrel2
|
12
|
+
# requests
|
13
|
+
# @api public
|
14
|
+
def initialize(request_socket, response_socket, request_parser = Request)
|
15
|
+
@request_socket = request_socket
|
16
|
+
@response_socket = response_socket
|
17
|
+
@request_parser = request_parser
|
18
|
+
end
|
19
|
+
|
20
|
+
# For compatibility with {M2R::ConnectionFactory}
|
21
|
+
#
|
22
|
+
# @return [Connection] self
|
23
|
+
# @api public
|
24
|
+
def connection
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns parsed Mongrel2 request
|
29
|
+
#
|
30
|
+
# @note This is blocking call
|
31
|
+
# @return [Request] Request parsed by {#request_parser}
|
32
|
+
# @api public
|
33
|
+
def receive
|
34
|
+
@request_socket.recv_string(msg = "")
|
35
|
+
@request_parser.parse(msg)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sends response to Mongrel2 for given request
|
39
|
+
#
|
40
|
+
# @param [Response, #to_s] response_or_string Response
|
41
|
+
# for the request. Anything convertable to [String]
|
42
|
+
# @api public
|
43
|
+
def reply(request, response_or_string)
|
44
|
+
deliver(request.sender, request.conn_id, response_or_string.to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Delivers data to multiple mongrel2 connections.
|
48
|
+
# Useful for streaming.
|
49
|
+
#
|
50
|
+
# @param [String] uuid Mongrel2 instance uuid
|
51
|
+
# @param [Array<String>, String] connection_ids Mongrel2 connections ids
|
52
|
+
# @param [String] data Data that should be delivered to the connections
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
def deliver(uuid, connection_ids, data)
|
56
|
+
msg = "#{uuid} #{TNetstring.dump([*connection_ids].join(' '))} #{data}"
|
57
|
+
@response_socket.send_string(msg)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :request_socket
|
63
|
+
attr_reader :response_socket
|
64
|
+
attr_reader :request_parser
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'm2r'
|
2
|
+
|
3
|
+
module M2R
|
4
|
+
# {Connection} factory so that every thread can use it generate its own
|
5
|
+
# {Connection} for communication with Mongrel2.
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
class ConnectionFactory
|
9
|
+
|
10
|
+
# @param [String, nil] sender_id {ZMQ::IDENTITY} option for response socket
|
11
|
+
# @param [String] request_addr ZMQ connection address. This is the
|
12
|
+
# send_spec option from Handler configuration in mongrel2.conf
|
13
|
+
# @param [String] response_addr ZMQ connection address. This is the
|
14
|
+
# recv_spec option from Handler configuration in mongrel2.conf
|
15
|
+
# @param [#parse] request_parser Mongrel2 request parser
|
16
|
+
# @param [ZMQ::Context] context Context for creating new ZMQ sockets
|
17
|
+
def initialize(sender_id, request_addr, response_addr, request_parser = Request, context = M2R.zmq_context)
|
18
|
+
@sender_id = sender_id.to_s
|
19
|
+
@request_addr = request_addr.to_s
|
20
|
+
@response_addr = response_addr.to_s
|
21
|
+
@request_parser = request_parser
|
22
|
+
@context = context
|
23
|
+
end
|
24
|
+
|
25
|
+
# Builds new Connection which can be used to receive, parse
|
26
|
+
# Mongrel2 requests and send responses.
|
27
|
+
#
|
28
|
+
# @return [Connection]
|
29
|
+
def connection
|
30
|
+
request_socket = @context.socket(ZMQ::PULL)
|
31
|
+
request_socket.connect(@request_addr)
|
32
|
+
|
33
|
+
response_socket = @context.socket(ZMQ::PUB)
|
34
|
+
response_socket.connect(@response_addr)
|
35
|
+
response_socket.setsockopt(ZMQ::IDENTITY, @sender_id) if @sender_id
|
36
|
+
|
37
|
+
Connection.new(request_socket, response_socket, @request_parser)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
data/lib/m2r/handler.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'm2r'
|
2
|
+
|
3
|
+
module M2R
|
4
|
+
|
5
|
+
# Basic handler, scaffold for your own Handler.
|
6
|
+
# Overwrite hook methods to define behavior.
|
7
|
+
# After calling #listen the Handler will block
|
8
|
+
# waiting for request from connection generated
|
9
|
+
# by {M2R::Handler#connection_factory}, process them and send
|
10
|
+
# reponses back.
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
# @abstract Subclass and override method hooks to implement your own Handler
|
14
|
+
class Handler
|
15
|
+
# @return [Connection] used for receiving requests and sending responses
|
16
|
+
attr_accessor :connection
|
17
|
+
|
18
|
+
# @param [ConnectionFactory, Connection, #connection] connection_factory
|
19
|
+
# Factory for generating connections
|
20
|
+
def initialize(connection_factory)
|
21
|
+
@connection = connection_factory.connection
|
22
|
+
end
|
23
|
+
|
24
|
+
# Start processing request
|
25
|
+
def listen
|
26
|
+
catch(:stop) do
|
27
|
+
loop { one_loop }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Schedule stop after processing request
|
32
|
+
def stop
|
33
|
+
@stop = true
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
# Callback executed when waiting for a request
|
39
|
+
# @api public
|
40
|
+
# @!visibility public
|
41
|
+
def on_wait()
|
42
|
+
end
|
43
|
+
|
44
|
+
# Callback when a request is received
|
45
|
+
# @api public
|
46
|
+
# @!visibility public
|
47
|
+
def on_request(request)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Override to return a response
|
51
|
+
# @api public
|
52
|
+
# @!visibility public
|
53
|
+
# @return [Response, String, #to_s] Response that should be sent to
|
54
|
+
# Mongrel2 instance
|
55
|
+
def process(request)
|
56
|
+
raise NotImplementedError
|
57
|
+
end
|
58
|
+
|
59
|
+
# Callback executed when response could not be delivered by Mongrel2
|
60
|
+
# because client already disconnected.
|
61
|
+
# @api public
|
62
|
+
# @!visibility public
|
63
|
+
def on_disconnect(request)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Callback when async-upload started
|
67
|
+
# @api public
|
68
|
+
# @!visibility public
|
69
|
+
def on_upload_start(request)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Callback when async-upload finished
|
73
|
+
# @api public
|
74
|
+
# @!visibility public
|
75
|
+
def on_upload_done(request)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Callback after process_request is done
|
79
|
+
# @api public
|
80
|
+
# @!visibility public
|
81
|
+
def after_process(request, response)
|
82
|
+
return response
|
83
|
+
end
|
84
|
+
|
85
|
+
# Callback after sending the response back
|
86
|
+
# @api public
|
87
|
+
# @!visibility public
|
88
|
+
def after_reply(request, response)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Callback after request is processed that is executed
|
92
|
+
# even when execption occured. Useful for releasing
|
93
|
+
# resources (closing files etc)
|
94
|
+
# @api public
|
95
|
+
# @!visibility public
|
96
|
+
def after_all(request, response)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def one_loop
|
102
|
+
on_wait
|
103
|
+
throw :stop if stop?
|
104
|
+
request_lifecycle(@connection.receive)
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def request_lifecycle(request)
|
109
|
+
on_request(request)
|
110
|
+
|
111
|
+
return on_disconnect(request) if request.disconnect?
|
112
|
+
return on_upload_start(request) if request.upload_start?
|
113
|
+
on_upload_done(request) if request.upload_done?
|
114
|
+
|
115
|
+
response = process(request)
|
116
|
+
response = after_process(request, response)
|
117
|
+
|
118
|
+
@connection.reply(request, response)
|
119
|
+
|
120
|
+
after_reply(request, response)
|
121
|
+
ensure
|
122
|
+
after_all(request, response)
|
123
|
+
end
|
124
|
+
|
125
|
+
def stop?
|
126
|
+
@stop
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
data/lib/m2r/headers.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module M2R
|
4
|
+
# Normalize headers access so that it is not case-sensitive
|
5
|
+
# @api public
|
6
|
+
class Headers
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# @param [Hash, #inject] hash Collection of headers
|
10
|
+
def initialize(hash = {})
|
11
|
+
@headers = hash.inject({}) do |headers,(header,value)|
|
12
|
+
headers[transform_key(header)] = value
|
13
|
+
headers
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get header
|
18
|
+
# @param [String, Symbol, #to_s] header HTTP Header key
|
19
|
+
# @return [String, nil] Value of given Header, nil when not present
|
20
|
+
def [](header)
|
21
|
+
@headers[transform_key(header)]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Set header
|
25
|
+
# @param [String, Symbol, #to_s] header HTTP Header key
|
26
|
+
# @param [String] value HTTP Header value
|
27
|
+
# @return [String] Set value
|
28
|
+
def []=(header, value)
|
29
|
+
@headers[transform_key(header)] = value
|
30
|
+
end
|
31
|
+
|
32
|
+
# Delete header
|
33
|
+
# @param [String, Symbol, #to_s] header HTTP Header key
|
34
|
+
# @return [String, nil] Value of deleted header, nil when was not present
|
35
|
+
def delete(header)
|
36
|
+
@headers.delete(transform_key(header))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Iterate over headers
|
40
|
+
# @yield HTTP header and its value
|
41
|
+
# @yieldparam [String] header HTTP Header name (downcased)
|
42
|
+
# @yieldparam [String] value HTTP Header value
|
43
|
+
# @return [Hash, Enumerator]
|
44
|
+
def each(&proc)
|
45
|
+
@headers.each(&proc)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Fill Hash with Headers compatibile with Rack standard.
|
49
|
+
# Every header except for Content-Length and Content-Type
|
50
|
+
# is capitalized, underscored, and prefixed with HTTP.
|
51
|
+
# Content-Length and Content-Type are not prefixed
|
52
|
+
# (according to the spec)
|
53
|
+
#
|
54
|
+
# @param [Hash] env Hash representing Rack Env or compatible
|
55
|
+
# @return [Hash] same Hash as provided as argument.
|
56
|
+
def rackify(env = {})
|
57
|
+
@headers.each do |header, value|
|
58
|
+
key = "HTTP_" + header.upcase.gsub("-", "_")
|
59
|
+
env[key] = value
|
60
|
+
end
|
61
|
+
env["CONTENT_LENGTH"] = env.delete("HTTP_CONTENT_LENGTH") if env.key?("HTTP_CONTENT_LENGTH")
|
62
|
+
env["CONTENT_TYPE"] = env.delete("HTTP_CONTENT_TYPE") if env.key?("HTTP_CONTENT_TYPE")
|
63
|
+
env
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def transform_key(key)
|
69
|
+
key.to_s.downcase
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'm2r'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module M2R
|
5
|
+
# Handle Mongrel2 requests using Rack application
|
6
|
+
# @private
|
7
|
+
class RackHandler < Handler
|
8
|
+
attr_accessor :app
|
9
|
+
|
10
|
+
def initialize(app, connection_factory)
|
11
|
+
@app = app
|
12
|
+
super(connection_factory)
|
13
|
+
|
14
|
+
trap('INT') { stop }
|
15
|
+
end
|
16
|
+
|
17
|
+
def process(request)
|
18
|
+
script_name = request.pattern.split('(', 2).first.gsub(/\/$/, '')
|
19
|
+
|
20
|
+
env = {
|
21
|
+
'REQUEST_METHOD' => request.method,
|
22
|
+
'SCRIPT_NAME' => script_name,
|
23
|
+
'PATH_INFO' => request.path.gsub(script_name, ''),
|
24
|
+
'QUERY_STRING' => request.query || "",
|
25
|
+
'rack.version' => ::Rack::VERSION,
|
26
|
+
'rack.errors' => $stderr,
|
27
|
+
'rack.multithread' => false,
|
28
|
+
'rack.multiprocess' => true,
|
29
|
+
'rack.run_once' => false,
|
30
|
+
'rack.url_scheme' => request.scheme,
|
31
|
+
'rack.input' => request.body_io
|
32
|
+
}
|
33
|
+
env['SERVER_NAME'], env['SERVER_PORT'] = request.headers['Host'].split(':', 2)
|
34
|
+
request.headers.rackify(env)
|
35
|
+
|
36
|
+
status, headers, body = @app.call(env)
|
37
|
+
buffer = ""
|
38
|
+
body.each { |part| buffer << part }
|
39
|
+
return Response.new(status, headers, buffer)
|
40
|
+
end
|
41
|
+
|
42
|
+
def after_all(request, response)
|
43
|
+
request.free!
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|