summary refs log tree commit homepage
path: root/lib/rainbows/dev_fd_response.rb
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2009-10-18 15:59:29 -0700
committerEric Wong <normalperson@yhbt.net>2009-10-18 21:25:47 -0700
commit7b01d94dd9287ac402d91451f1e93c9faaf913c4 (patch)
tree8f4005f4e92108748af53b8cbf709522f33419db /lib/rainbows/dev_fd_response.rb
parentd0103759ae63b0ed1084f6a9d2b7ede538e8c871 (diff)
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.
Diffstat (limited to 'lib/rainbows/dev_fd_response.rb')
-rw-r--r--lib/rainbows/dev_fd_response.rb69
1 files changed, 69 insertions, 0 deletions
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