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

Commit

Permalink
Add :Async option to detach connections and handle in separate actors
Browse files Browse the repository at this point in the history
  • Loading branch information
chewi committed Apr 23, 2018
1 parent b0aaf74 commit 330dda1
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 88 deletions.
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
107 changes: 107 additions & 0 deletions lib/reel/rack/handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
require 'reel'
require 'rack'

module Reel
module Rack
class Handler
include Celluloid

def initialize(app)
@app = app
end

def async_on_connection(connection)
on_connection(connection)
rescue Reel::SocketError
connection.close
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
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.new(app).async.async_on_connection(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

0 comments on commit 330dda1

Please sign in to comment.