Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Implemented tests for Rack behavior and async requests.

  • Loading branch information...
commit 3434ff77c8b32bad1cbf68e5b679fe9794f72178 1 parent 324e2ed
@matadon matadon authored
View
9 README.markdown
@@ -17,7 +17,8 @@ Mizuno is the fastest option for Rack applications on JRuby:
Jetty (via jruby-rack): 2011.67 req/s (mean)
Mongrel: 1479.15 req/sec (mean)
-Mizuno also supports asynchronous request handling.
+Mizuno also supports asynchronous request handling, via the Java Servlet
+3.0 asynchronous processing mechanism
All the speed comes from Jetty 7; Mizuno just ties it to Rack through
JRuby's Ruby/Java integration layer.
@@ -28,10 +29,8 @@ Rack application for installation in a Java web container.
There's also a few features that I have yet to implement:
-1. Integrate the cometd servlet to provide Comet/Bayeux.
-2. Route Jetty's logs into Rack::Logger.
-3. Add hooks for realtime monitoring of server performance.
-4. Add the ability to run multiple Rack apps in a single JVM.
+1. Route Jetty's logs into Rack::Logger.
+2. Add hooks for realtime monitoring of server performance.
Mizuno is licensed under the Apache Public License, version 2.0; see
the LICENSE file for details, and was developed on behalf of
View
5 Rakefile
@@ -0,0 +1,5 @@
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new(:spec)
+
+task :default => :spec
View
BIN  lib/java/bayeux-api-2.1.0.jar
Binary file not shown
View
BIN  lib/java/cometd-java-server-2.1.0.jar
Binary file not shown
View
13 lib/java/jars
@@ -1,13 +0,0 @@
-../../tmp/cometd-2.1.0/cometd-archetypes/dojo-jetty6/target/cometd-archetype-dojo-jetty6-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-archetypes/dojo-jetty7/target/cometd-archetype-dojo-jetty7-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-archetypes/jquery-jetty6/target/cometd-archetype-jquery-jetty6-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-archetypes/jquery-jetty7/target/cometd-archetype-jquery-jetty7-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-archetypes/spring-dojo-jetty7/target/cometd-archetype-spring-dojo-jetty7-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-archetypes/spring-jquery-jetty7/target/cometd-archetype-spring-jquery-jetty7-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-java/bayeux-api/target/bayeux-api-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-java/cometd-java-annotations/target/cometd-java-annotations-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-java/cometd-java-client/target/cometd-java-client-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-java/cometd-java-common/target/cometd-java-common-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-java/cometd-java-oort/target/cometd-java-oort-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-java/cometd-java-server/target/cometd-java-server-2.1.0.jar
-../../tmp/cometd-2.1.0/cometd-javascript/common-test/target/cometd-javascript-common-test-2.1.0.jar
View
60 lib/mizuno/http_server.rb
@@ -9,45 +9,79 @@ class HttpServer
include_class 'org.eclipse.jetty.servlet.ServletHolder'
include_class 'org.eclipse.jetty.server.nio.SelectChannelConnector'
include_class 'org.eclipse.jetty.util.thread.QueuedThreadPool'
+ include_class 'org.eclipse.jetty.servlet.DefaultServlet'
+ #
+ # Provide accessors so we can set a custom logger and a location
+ # for static assets.
+ #
+ class << self
+ attr_accessor :logger
+ end
+
+ #
+ # Start up an instance of Jetty, running a Rack application.
+ # Options can be any of the follwing, and are not
+ # case-sensitive:
+ #
+ # :host::
+ # String specifying the IP address to bind to; defaults
+ # to 0.0.0.0.
+ #
+ # :port::
+ # String or integer with the port to bind to; defaults
+ # to 9292.
+ #
+ # FIXME: Clean up options hash (all downcase, all symbols)
+ #
def self.run(app, options = {})
# The Jetty server
- server = Server.new
+ @server = Server.new
+
+ options = Hash[options.map { |o|
+ [ o[0].to_s.downcase.to_sym, o[1] ] }]
# Thread pool
thread_pool = QueuedThreadPool.new
thread_pool.min_threads = 5
thread_pool.max_threads = 50
- server.set_thread_pool(thread_pool)
+ @server.set_thread_pool(thread_pool)
# Connector
connector = SelectChannelConnector.new
- connector.setPort(options[:Port].to_i)
- connector.setHost(options[:Host])
- server.addConnector(connector)
+ connector.setPort(options[:port].to_i)
+ connector.setHost(options[:host])
+ @server.addConnector(connector)
# Servlet context.
context = ServletContextHandler.new(nil, "/",
ServletContextHandler::NO_SESSIONS)
# The servlet itself.
- servlet = RackServlet.new
- servlet.rackup(app)
- holder = ServletHolder.new(servlet)
+ rack_servlet = RackServlet.new
+ rack_servlet.rackup(app)
+ holder = ServletHolder.new(rack_servlet)
context.addServlet(holder, "/")
# Add the context to the server and start.
- server.set_handler(context)
- puts "Started Jetty on #{connector.getHost}:#{connector.getPort}"
- server.start
+ @server.set_handler(context)
+ puts "Listening on #{connector.getHost}:#{connector.getPort}"
+ @server.start
# Stop the server when we get The Signal.
- trap("SIGINT") { server.stop and exit }
+ trap("SIGINT") { @server.stop and exit }
# Join with the server thread, so that currently open file
# descriptors don't get closed by accident.
# http://www.ruby-forum.com/topic/209252
- server.join
+ @server.join unless options[:embedded]
+ end
+
+ #
+ # Shuts down an embedded Jetty instance.
+ #
+ def self.stop
+ @server.stop
end
end
end
View
5 lib/mizuno/rack_servlet.rb
@@ -3,8 +3,9 @@
#
# Relevant documentation:
#
-# http://rack.rubyforge.org/doc/SPEC.html
-# http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax/servlet/http/HttpServlet.html
+# http://rack.rubyforge.org/doc/SPEC.html
+# http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax
+# /servlet/http/HttpServlet.html
#
module Mizuno
include_class javax.servlet.http.HttpServlet
View
73 lib/testrequest.rb
@@ -1,73 +0,0 @@
-require 'yaml'
-require 'net/http'
-
-class TestApp
- def call(env)
- status = env["QUERY_STRING"] =~ /secret/ ? 403 : 200
- env["test.postdata"] = env["rack.input"].read
- body = env.to_yaml
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
- [status, {"Content-Type" => "text/yaml",
- "Content-Length" => size.to_s}, [body]]
- end
-
- module Helpers
- attr_reader :status, :response
-
- ROOT = File.expand_path(File.dirname(__FILE__) + "/..")
- ENV["RUBYOPT"] = "-I#{ROOT}/lib -rubygems"
-
- def root
- ROOT
- end
-
- def rackup
- "#{ROOT}/bin/rackup"
- end
-
- def GET(path, header={})
- Net::HTTP.start(@options[:Host], @options[:Port]) { |http|
- user = header.delete(:user)
- passwd = header.delete(:passwd)
-
- get = Net::HTTP::Get.new(path, header)
- get.basic_auth user, passwd if user && passwd
- http.request(get) { |response|
- @status = response.code.to_i
- begin
- @response = YAML.load(response.body)
- rescue ArgumentError
- @response = nil
- end
- }
- }
- end
-
- def POST(path, formdata={}, header={})
- Net::HTTP.start(@options[:Host], @options[:Port]) { |http|
- user = header.delete(:user)
- passwd = header.delete(:passwd)
-
- post = Net::HTTP::Post.new(path, header)
- post.form_data = formdata
- post.basic_auth user, passwd if user && passwd
- http.request(post) { |response|
- @status = response.code.to_i
- @response = YAML.load(response.body)
- }
- }
- end
- end
-end
-
-class StreamingRequest
- def self.call(env)
- [200, {"Content-Type" => "text/plain"}, new]
- end
-
- def each
- yield "hello there!\n"
- sleep 5
- yield "that is all.\n"
- end
-end
View
11 mizuno.gemspec
@@ -1,6 +1,6 @@
Gem::Specification.new do |spec|
spec.name = "mizuno"
- spec.version = "0.3.7"
+ spec.version = "0.4.0"
spec.required_rubygems_version = Gem::Requirement.new(">= 1.2") \
if spec.respond_to?(:required_rubygems_version=)
spec.authors = [ "Don Werve" ]
@@ -12,8 +12,6 @@ Gem::Specification.new do |spec|
README.markdown
LICENSE
mizuno.gemspec
- lib/java/bayeux-api-2.1.0.jar
- lib/java/cometd-java-server-2.1.0.jar
lib/java/jetty-continuation-7.3.0.v20110203.jar
lib/java/jetty-http-7.3.0.v20110203.jar
lib/java/jetty-io-7.3.0.v20110203.jar
@@ -24,12 +22,13 @@ Gem::Specification.new do |spec|
lib/java/jetty-servlets-7.3.0.v20110203.jar
lib/java/jetty-util-7.3.0.v20110203.jar
lib/java/servlet-api-2.5.jar
- lib/rack/handler/mizuno/http_server.rb
- lib/rack/handler/mizuno/rack_servlet.rb
- lib/rack/handler/mizuno.rb
+ lib/mizuno/http_server.rb
+ lib/mizuno/rack_servlet.rb
+ lib/mizuno.rb
bin/mizuno )
spec.homepage = 'http://github.com/matadon/mizuno'
spec.has_rdoc = false
spec.require_paths = [ "lib" ]
spec.rubygems_version = '1.3.6'
+ spec.add_dependency('rack', '>= 1.0.0')
end
View
1  spec/data/hello.txt
@@ -0,0 +1 @@
+Hello, world!
View
BIN  spec/data/reddit-icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
162 spec/mizuno_spec.rb
@@ -0,0 +1,162 @@
+require 'spec_helper'
+require 'test_app'
+require 'thread'
+require 'digest/md5'
+require 'base64'
+
+describe Mizuno do
+ def get(path, headers = {})
+ Net::HTTP.start(@options[:host], @options[:port]) do |http|
+ request = Net::HTTP::Get.new(path, headers)
+ http.request(request)
+ end
+ end
+
+ def post(path, params = nil, headers = {}, body = nil)
+ Net::HTTP.start(@options[:host], @options[:port]) do |http|
+ request = Net::HTTP::Post.new(path, headers)
+ request.form_data = params if params
+ request.body = body if body
+ http.request(request)
+ end
+ end
+
+ before(:all) do
+ @lock = Mutex.new
+ @app = Rack::Lint.new(TestApp.new)
+ @options = { :host => '127.0.0.1', :port => 9201,
+ :embedded => true }
+ Net::HTTP.version_1_2
+ Mizuno::HttpServer.run(@app, @options)
+ end
+
+ after(:all) do
+ Mizuno::HttpServer.stop
+ end
+
+ it "returns 200 OK" do
+ response = get("/ping")
+ response.code.should == "200"
+ end
+
+ it "returns 403 FORBIDDEN" do
+ response = get("/error/403")
+ response.code.should == "403"
+ end
+
+ it "returns 404 NOT FOUND" do
+ response = get("/jimmy/hoffa")
+ response.code.should == "404"
+ end
+
+ it "sets Rack headers" do
+ response = get("/echo")
+ response.code.should == "200"
+ content = YAML.load(response.body)
+ content["rack.version"].should == [ 1, 1 ]
+ content["rack.multithread"].should be_true
+ content["rack.multiprocess"].should be_false
+ content["rack.run_once"].should be_false
+ end
+
+ it "passes form variables via GET" do
+ response = get("/echo?answer=42")
+ response.code.should == "200"
+ content = YAML.load(response.body)
+ content['request.params']['answer'].should == '42'
+ end
+
+ it "passes form variables via POST" do
+ question = "What is the answer to life, the universe, and everything?"
+ response = post("/echo", 'question' => question)
+ response.code.should == "200"
+ content = YAML.load(response.body)
+ content['request.params']['question'].should == question
+ end
+
+ it "passes custom headers" do
+ response = get("/echo", "X-My-Header" => "Pancakes")
+ response.code.should == "200"
+ content = YAML.load(response.body)
+ content["HTTP_X_MY_HEADER"].should == "Pancakes"
+ end
+
+ it "lets the Rack app know it's running as a servlet" do
+ response = get("/echo", 'answer' => '42')
+ response.code.should == "200"
+ content = YAML.load(response.body)
+ content['rack.java.servlet'].should be_true
+ end
+
+ it "is clearly Jetty" do
+ response = get("/ping")
+ response['server'].should =~ /jetty/i
+ end
+
+ it "sets the server port and hostname" do
+ response = get("/echo")
+ content = YAML.load(response.body)
+ content["SERVER_PORT"].should == "9201"
+ content["SERVER_NAME"].should == "127.0.0.1"
+ end
+
+ it "passes the URI scheme" do
+ response = get("/echo")
+ content = YAML.load(response.body)
+ content['rack.url_scheme'].should == 'http'
+ end
+
+ it "supports file downloads" do
+ response = get("/download")
+ response.code.should == "200"
+ response['Content-Type'].should == 'image/png'
+ response['Content-Disposition'].should == \
+ 'attachment; filename=reddit-icon.png'
+ checksum = Digest::MD5.hexdigest(response.body)
+ checksum.should == '8da4b60a9bbe205d4d3699985470627e'
+ end
+
+ it "supports file uploads" do
+ boundary = '349832898984244898448024464570528145'
+ content = []
+ content << "--#{boundary}"
+ content << 'Content-Disposition: form-data; name="file"; ' \
+ + 'filename="reddit-icon.png"'
+ content << 'Content-Type: image/png'
+ content << 'Content-Transfer-Encoding: base64'
+ content << ''
+ content << Base64.encode64( \
+ File.read('spec/data/reddit-icon.png')).strip
+ content << "--#{boundary}--"
+ body = content.map { |l| l + "\r\n" }.join('')
+ headers = { "Content-Type" => \
+ "multipart/form-data; boundary=#{boundary}" }
+ response = post("/upload", nil, headers, body)
+ response.code.should == "200"
+ response.body.should == '8da4b60a9bbe205d4d3699985470627e'
+ end
+
+ it "handles async requests" do
+ lock = Mutex.new
+ buffer = Array.new
+
+ clients = 10.times.map do |index|
+ Thread.new do
+ Net::HTTP.start(@options[:host], @options[:port]) do |http|
+ response = http.get("/pull")
+ lock.synchronize {
+ buffer << "#{index}: #{response.body}" }
+ end
+ end
+ end
+
+ lock.synchronize { buffer.should be_empty }
+ post("/push", 'message' => "one")
+ clients.each { |c| c.join }
+ lock.synchronize { buffer.should_not be_empty }
+ lock.synchronize { buffer.count.should == 10 }
+ end
+
+ pending "logs to a custom logger" do
+ end
+end
View
11 spec/spec_helper.rb
@@ -0,0 +1,11 @@
+# Load our local copy of Mizuno before anything else.
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+# All dependencies for testing.
+require 'yaml'
+require 'net/http'
+require 'rack/urlmap'
+require 'rack/lint'
+require 'mizuno'
+
+Thread.abort_on_exception = true
View
99 spec/test_app.rb
@@ -0,0 +1,99 @@
+#
+# A tiny Rack application for testing the Mizuno webserver. Each of the
+# following paths can be used to test webserver behavior:
+#
+# /ping:: Always returns 200 OK.
+#
+# /error/:number:: Returns the HTTP status code specified in the path.
+#
+# /echo:: Returns a plaintext rendering of the original request.
+#
+# /file:: Returns a file for downloading.
+#
+# /push:: Publishes a message to async listeners.
+#
+# /pull:: Recieves messages sent via /push using async.
+#
+# A request to any endpoint not listed above will return a 404 error.
+#
+class TestApp
+ def initialize
+ @subscribers = Array.new
+ end
+
+ def call(env)
+ begin
+ request = Rack::Request.new(env)
+ method = request.path[/^\/(\w+)/, 1]
+ return(error(request, 404)) if (method.nil? or method.empty?)
+ return(error(request, 404)) unless respond_to?(method.to_sym)
+ send(method.to_sym, request)
+ rescue => error
+ puts error
+ puts error.backtrace
+ error(nil, 500)
+ end
+ end
+
+ def ping(request)
+ [ 200, { "Content-Type" => "text/plain",
+ "Content-Length" => "2" }, [ "OK" ] ]
+ end
+
+ def error(request, code = nil)
+ code ||= (request.path[/^\/\w+\/(\d+)/, 1] or "500")
+ [ code.to_i, { "Content-Type" => "text/plain",
+ "Content-Length" => "5" }, [ "ERROR" ] ]
+ end
+
+ def echo(request)
+ response = Rack::Response.new
+ env = request.env.merge('request.params' => request.params)
+ response.write(env.to_yaml)
+ response.finish
+ end
+
+ def push(request)
+ message = request.params['message']
+
+ @subscribers.reject! do |subscriber|
+ begin
+ response = Rack::Response.new
+ if(message.empty?)
+ subscriber.call(response.finish)
+ next(true)
+ else
+ response.write(message)
+ subscriber.call(response.finish)
+ next(false)
+ end
+ rescue java.io.IOException => error
+ next(true)
+ end
+ end
+
+ ping(request)
+ end
+
+ def pull(request)
+ @subscribers << request.env['async.callback']
+ throw(:async)
+ end
+
+ def download(request)
+ file = File.new('spec/data/reddit-icon.png', 'r')
+ response = Rack::Response.new(file)
+ response['Content-Type'] = 'image/png'
+ response['Content-Disposition'] = \
+ 'attachment; filename=reddit-icon.png'
+ response.finish
+ end
+
+ def upload(request)
+ data = request.params['file'][:tempfile].read
+ checksum = Digest::MD5.hexdigest(Base64.decode64(data))
+ response = Rack::Response.new
+ response.write(checksum)
+ response.finish
+ end
+end
View
86 test/rack.rb
@@ -1,86 +0,0 @@
-#require 'test/spec'
-
-begin
-require 'rack/handler/mizuno'
-require 'rack/urlmap'
-require 'rack/lint'
-require 'testrequest'
-
-Thread.abort_on_exception = true
-
-context "Rack::Handler::Mizuno" do
- include TestApp::Helpers
-
- before(:all) do
- @app = Rack::Lint.new(TestApp.new)
- @options = { :Host => '0.0.0.0', :Port => 9201 }
- @server = Rack::Handler::Mizuno::HttpServer.run(@app, @options)
- end
-
- specify "should respond" do
- lambda { GET("/test") }.should_not raise_error
- end
-
- specify "should be using Jetty" do
- GET("/test")
- status.should == 200
- response['rack.java.servlet'].should_not be_nil
- response["HTTP_VERSION"].should == "HTTP/1.1"
- response["SERVER_PROTOCOL"].should == "HTTP/1.1"
- response["SERVER_PORT"].should == "9201"
- response["SERVER_NAME"].should == "0.0.0.0"
- end
-
- specify "should have rack headers" do
- GET("/test")
- response["rack.version"].should == [1,1]
- response["rack.multithread"].should be_true
- response["rack.multiprocess"].should be_false
- response["rack.run_once"].should be_false
- end
-
- specify "should have CGI headers on GET" do
- GET("/test")
- response["REQUEST_METHOD"].should == "GET"
- response["REQUEST_PATH"].should == "/test"
- response["PATH_INFO"].should == "/test"
- response["QUERY_STRING"].should == ""
- response["test.postdata"].should == ""
-
- GET("/test/foo?quux=1")
- response["REQUEST_METHOD"].should == "GET"
- response["REQUEST_PATH"].should == "/test/foo"
- response["PATH_INFO"].should == "/test/foo"
- response["QUERY_STRING"].should == "quux=1"
- end
-
- specify "should have CGI headers on POST" do
- POST("/test", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
- status.should == 200
- response["REQUEST_METHOD"].should == "POST"
- response["REQUEST_PATH"].should == "/test"
- response["QUERY_STRING"].should == ""
- response["HTTP_X_TEST_HEADER"].should == "42"
- response["test.postdata"].should == "rack-form-data=23"
- end
-
- specify "should support HTTP auth" do
- GET("/test", {:user => "ruth", :passwd => "secret"})
- response["HTTP_AUTHORIZATION"].should == "Basic cnV0aDpzZWNyZXQ="
- end
-
- specify "should set status" do
- GET("/test?secret")
- status.should == 403
- response["rack.url_scheme"].should == "http"
- end
-
-# teardown do
-# end
-end
-
-rescue LoadError => e
- $stderr.puts e
- $stderr.puts e.backtrace
- $stderr.puts "Skipping Rack::Handler::Mizuno tests (Mizuno is required). `gem install mizuno` and try again."
-end
Please sign in to comment.
Something went wrong with that request. Please try again.