From 7b01d94dd9287ac402d91451f1e93c9faaf913c4 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 18 Oct 2009 15:59:29 -0700 Subject: rev: async response bodies with DevFdResponse middleware This new middleware should be a no-op for non-Rev concurrency models (or by explicitly setting env['rainbows.autochunk'] to false). Setting env['rainbows.autochunk'] to true (the default when Rev is used) allows (e)poll-able IO objects (sockets, pipes) to be sent asynchronously after app.call(env) returns. This also has a fortunate side effect of introducing a code path which allows large, static files to be sent without slurping them into a Rev IO::Buffer, too. This new change works even without the DevFdResponse middleware, so you won't have to reconfigure your app. This lets us epoll on response bodies that come in from a pipe or even a socket and send them either straight through or with chunked encoding. --- lib/rainbows/dev_fd_response.rb | 69 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 lib/rainbows/dev_fd_response.rb (limited to 'lib/rainbows/dev_fd_response.rb') diff --git a/lib/rainbows/dev_fd_response.rb b/lib/rainbows/dev_fd_response.rb new file mode 100644 index 0000000..e4e5f0c --- /dev/null +++ b/lib/rainbows/dev_fd_response.rb @@ -0,0 +1,69 @@ +# -*- encoding: binary -*- + +module Rainbows + + # Rack response middleware wrapping any IO-like object with an + # OS-level file descriptor associated with it. May also be used to + # create responses from integer file descriptors or existing +IO+ + # objects. This may be used in conjunction with the #to_path method + # on servers that support it to pass arbitrary file descriptors into + # the HTTP response without additional open(2) syscalls + + class DevFdResponse < Struct.new(:app, :to_io, :to_path) + include Rack::Utils + + # Rack middleware entry point, we'll just pass through responses + # unless they respond to +to_io+ or +to_path+ + def call(env) + status, headers, body = response = app.call(env) + + # totally uninteresting to us if there's no body + return response if STATUS_WITH_NO_ENTITY_BODY.include?(status) + + io = body.to_io if body.respond_to?(:to_io) + io ||= File.open(body.to_path, 'rb') if body.respond_to?(:to_path) + return response if io.nil? + + headers = HeaderHash.new(headers) + st = io.stat + if st.file? + headers['Content-Length'] ||= st.size.to_s + headers.delete('Transfer-Encoding') + elsif st.pipe? || st.socket? # epoll-able things + if env['rainbows.autochunk'] + headers['Transfer-Encoding'] = 'chunked' + headers.delete('Content-Length') + else + headers['X-Rainbows-Autochunk'] = 'no' + end + else # unlikely, char/block device file, directory, ... + return response + end + resp = dup # be reentrant here + resp.to_path = "/dev/fd/#{io.fileno}" + resp.to_io = io + [ status, headers.to_hash, resp ] + end + + # called by the webserver or other middlewares if they can't + # handle #to_path + def each(&block) + to_io.each(&block) + end + + # remain Rack::Lint-compatible for people with wonky systems :P + unless File.exist?("/dev/fd/0") + alias to_path_orig to_path + undef_method :to_path + end + + # called by the web server after #each + def close + begin + to_io.close if to_io.respond_to?(:close) + rescue IOError # could've been IO::new()'ed and closed + end + end + + end # class +end -- cgit v1.2.3-24-ge0c7