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.
Files changed (51) hide show
  1. data/Gemfile +6 -0
  2. data/README.md +141 -35
  3. data/Rakefile +13 -45
  4. data/example/Procfile +4 -0
  5. data/example/config.sqlite +0 -0
  6. data/example/http_0mq.rb +37 -19
  7. data/example/lobster.ru +14 -6
  8. data/example/mongrel2.conf +47 -0
  9. data/example/tmp/access.log +505 -0
  10. data/example/uploading.ru +37 -0
  11. data/lib/m2r.rb +49 -3
  12. data/lib/m2r/connection.rb +66 -0
  13. data/lib/m2r/connection_factory.rb +41 -0
  14. data/lib/m2r/handler.rb +130 -0
  15. data/lib/m2r/headers.rb +72 -0
  16. data/lib/m2r/rack_handler.rb +47 -0
  17. data/lib/m2r/request.rb +129 -0
  18. data/lib/m2r/request/base.rb +33 -0
  19. data/lib/m2r/request/upload.rb +60 -0
  20. data/lib/m2r/response.rb +102 -0
  21. data/lib/m2r/response/content_length.rb +18 -0
  22. data/lib/m2r/version.rb +5 -0
  23. data/lib/rack/handler/mongrel2.rb +33 -0
  24. data/m2r.gemspec +30 -63
  25. data/test/acceptance/examples_test.rb +32 -0
  26. data/test/support/capybara.rb +4 -0
  27. data/test/support/mongrel_helper.rb +40 -0
  28. data/test/support/test_handler.rb +51 -0
  29. data/test/support/test_user.rb +37 -0
  30. data/test/test_helper.rb +5 -0
  31. data/test/unit/connection_factory_test.rb +29 -0
  32. data/test/unit/connection_test.rb +49 -0
  33. data/test/unit/handler_test.rb +41 -0
  34. data/test/unit/headers_test.rb +50 -0
  35. data/test/unit/m2r_test.rb +40 -0
  36. data/test/unit/rack_handler_test.rb +52 -0
  37. data/test/unit/request_test.rb +38 -0
  38. data/test/unit/response_test.rb +30 -0
  39. metadata +310 -105
  40. data/.document +0 -5
  41. data/.gitignore +0 -21
  42. data/ISSUES +0 -62
  43. data/VERSION +0 -1
  44. data/benchmarks/jruby +0 -60
  45. data/example/rack_handler.rb +0 -69
  46. data/lib/connection.rb +0 -158
  47. data/lib/fiber_handler.rb +0 -43
  48. data/lib/handler.rb +0 -66
  49. data/lib/request.rb +0 -44
  50. data/test/helper.rb +0 -10
  51. 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 'connection'
2
- require 'request'
3
- require 'handler'
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
@@ -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
@@ -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