From 330dda1be8e37d5be09ba18c8546be75c8d64790 Mon Sep 17 00:00:00 2001 From: James Le Cuirot Date: Fri, 20 Apr 2018 18:17:54 +0100 Subject: [PATCH] Add :Async option to detach connections and handle in separate actors --- lib/rack/handler/reel.rb | 7 +++ lib/reel/rack/handler.rb | 107 +++++++++++++++++++++++++++++++++++++++ lib/reel/rack/server.rb | 96 +++-------------------------------- 3 files changed, 122 insertions(+), 88 deletions(-) create mode 100644 lib/reel/rack/handler.rb diff --git a/lib/rack/handler/reel.rb b/lib/rack/handler/reel.rb index 52b2d8d..c6b18e0 100644 --- a/lib/rack/handler/reel.rb +++ b/lib/rack/handler/reel.rb @@ -7,6 +7,7 @@ class Reel DEFAULT_OPTIONS = { :Host => "0.0.0.0", :Port => 3000, + :Async => false, :quiet => false } @@ -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 diff --git a/lib/reel/rack/handler.rb b/lib/reel/rack/handler.rb new file mode 100644 index 0000000..061d1c0 --- /dev/null +++ b/lib/reel/rack/handler.rb @@ -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 diff --git a/lib/reel/rack/server.rb b/lib/reel/rack/server.rb index ecf1703..e1beeb6 100644 --- a/lib/reel/rack/server.rb +++ b/lib/reel/rack/server.rb @@ -1,15 +1,13 @@ # 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] @@ -17,94 +15,16 @@ def initialize(app, options) 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