Skip to content
This repository was archived by the owner on Dec 7, 2018. It is now read-only.

Add :Async option to detach connections and handle in separate actors #26

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/rack/handler/reel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Reel
DEFAULT_OPTIONS = {
:Host => "0.0.0.0",
:Port => 3000,
:Async => false,
:quiet => false
}

Expand All @@ -25,6 +26,12 @@ def self.run(app, options = {})
supervisor.terminate
end
end

def self.valid_options
{
'Async' => 'handle each request in a separate actor (default: false)'
}
end
end

register :reel, Reel
Expand Down
114 changes: 114 additions & 0 deletions lib/reel/rack/handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
require 'reel'
require 'rack'

module Reel
module Rack
class Handler
include Celluloid

def initialize(app)
@app = app
end

def self.on_connection(app, connection)
handler = new(app)
handler.async.handle_and_terminate(connection)
end

def on_connection(connection)
connection.each_request do |request|
if request.websocket?
request.respond :bad_request, "WebSockets not supported"
else
route_request request
end
end
end

def handle_and_terminate(connection)
on_connection(connection)
rescue Reel::SocketError
connection.close
ensure
terminate
end

# Compile the regex once
CONTENT_LENGTH_HEADER = %r{^content-length$}i

def route_request(request)
options = {
:method => request.method,
:input => request.body.to_s,
"REMOTE_ADDR" => request.remote_addr
}.merge(convert_headers(request.headers))

normalize_env(options)

status, headers, body = @app.call ::Rack::MockRequest.env_for(request.url, options)

if body.respond_to? :each
# If Content-Length was specified we can send the response all at once
if headers.keys.detect { |h| h =~ CONTENT_LENGTH_HEADER }
# Can't use collect here because Rack::BodyProxy/Rack::Lint isn't a real Enumerable
full_body = ''
body.each { |b| full_body << b }
request.respond status_symbol(status), headers, full_body
else
request.respond status_symbol(status), headers.merge(:transfer_encoding => :chunked)
body.each { |chunk| request << chunk }
request.finish_response
end
else
Logger.error("don't know how to render: #{body.inspect}")
request.respond :internal_server_error, "An error occurred processing your request"
end

body.close if body.respond_to? :close
end

# Those headers must not start with 'HTTP_'.
NO_PREFIX_HEADERS=%w[CONTENT_TYPE CONTENT_LENGTH].freeze

def convert_headers(headers)
Hash[headers.map { |key, value|
header = key.upcase.gsub('-','_')

if NO_PREFIX_HEADERS.member?(header)
[header, value]
else
['HTTP_' + header, value]
end
}]
end

# Copied from lib/puma/server.rb
def normalize_env(env)
if host = env["HTTP_HOST"]
if colon = host.index(":")
env["SERVER_NAME"] = host[0, colon]
env["SERVER_PORT"] = host[colon+1, host.bytesize]
else
env["SERVER_NAME"] = host
env["SERVER_PORT"] = default_server_port(env)
end
else
env["SERVER_NAME"] = "localhost"
env["SERVER_PORT"] = default_server_port(env)
end
end

def default_server_port(env)
env['HTTP_X_FORWARDED_PROTO'] == 'https' ? 443 : 80
end

def status_symbol(status)
if status.is_a?(Fixnum)
Reel::Response::STATUS_CODES[status].downcase.gsub(/\s|-/, '_').to_sym
else
status.to_sym
end
end
end
end
end
96 changes: 8 additions & 88 deletions lib/reel/rack/server.rb
Original file line number Diff line number Diff line change
@@ -1,110 +1,30 @@
# Adapted from code orinially Copyright (c) 2013 Jonathan Stott

require 'reel'
require 'rack'
require 'reel/rack/handler'

module Reel
module Rack
class Server < Reel::Server::HTTP
include Celluloid::Internals::Logger

attr_reader :app

def initialize(app, options)
raise ArgumentError, "no host given" unless options[:Host]
raise ArgumentError, "no port given" unless options[:Port]

info "A Reel good HTTP server! (Codename \"#{::Reel::CODENAME}\")"
info "Listening on http://#{options[:Host]}:#{options[:Port]}"

super(options[:Host], options[:Port], &method(:on_connection))
@app = app
end

def on_connection(connection)
connection.each_request do |request|
if request.websocket?
request.respond :bad_request, "WebSockets not supported"
else
route_request request
end
end
end

# Compile the regex once
CONTENT_LENGTH_HEADER = %r{^content-length$}i

def route_request(request)
options = {
:method => request.method,
:input => request.body.to_s,
"REMOTE_ADDR" => request.remote_addr
}.merge(convert_headers(request.headers))

normalize_env(options)

status, headers, body = app.call ::Rack::MockRequest.env_for(request.url, options)

if body.respond_to? :each
# If Content-Length was specified we can send the response all at once
if headers.keys.detect { |h| h =~ CONTENT_LENGTH_HEADER }
# Can't use collect here because Rack::BodyProxy/Rack::Lint isn't a real Enumerable
full_body = ''
body.each { |b| full_body << b }
request.respond status_symbol(status), headers, full_body
else
request.respond status_symbol(status), headers.merge(:transfer_encoding => :chunked)
body.each { |chunk| request << chunk }
request.finish_response
if options[:Async]
super(options[:Host], options[:Port]) do |connection|
connection.detach
Reel::Rack::Handler.on_connection(app, connection)
end
else
Logger.error("don't know how to render: #{body.inspect}")
request.respond :internal_server_error, "An error occurred processing your request"
end

body.close if body.respond_to? :close
end

# Those headers must not start with 'HTTP_'.
NO_PREFIX_HEADERS=%w[CONTENT_TYPE CONTENT_LENGTH].freeze

def convert_headers(headers)
Hash[headers.map { |key, value|
header = key.upcase.gsub('-','_')

if NO_PREFIX_HEADERS.member?(header)
[header, value]
else
['HTTP_' + header, value]
handler = Reel::Rack::Handler.new(app)
super(options[:Host], options[:Port]) do |connection|
handler.on_connection(connection)
end
}]
end

# Copied from lib/puma/server.rb
def normalize_env(env)
if host = env["HTTP_HOST"]
if colon = host.index(":")
env["SERVER_NAME"] = host[0, colon]
env["SERVER_PORT"] = host[colon+1, host.bytesize]
else
env["SERVER_NAME"] = host
env["SERVER_PORT"] = default_server_port(env)
end
else
env["SERVER_NAME"] = "localhost"
env["SERVER_PORT"] = default_server_port(env)
end
end

def default_server_port(env)
env['HTTP_X_FORWARDED_PROTO'] == 'https' ? 443 : 80
end

def status_symbol(status)
if status.is_a?(Fixnum)
Reel::Response::STATUS_CODES[status].downcase.gsub(/\s|-/, '_').to_sym
else
status.to_sym
end
end
end
Expand Down