-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
"fast-start" the delivery of gzip-compressed bodies, and support flushing of GZIPOutputStreams on JDK7+ #3
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,55 @@ | ||
(ns ring.middleware.gzip | ||
(:require [clojure.java.io :as io]) | ||
(:require [clojure.java.io :as io] | ||
clojure.reflect) | ||
(:import (java.util.zip GZIPOutputStream) | ||
(java.io InputStream | ||
OutputStream | ||
Closeable | ||
File | ||
PipedInputStream | ||
PipedOutputStream))) | ||
|
||
; only available on JDK7 | ||
(def ^:private flushable-gzip? | ||
(delay (->> (clojure.reflect/reflect GZIPOutputStream) | ||
:members | ||
(some (comp '#{[java.io.OutputStream boolean]} :parameter-types))))) | ||
|
||
; only proxying here so we can specialize io/copy (which ring uses to transfer | ||
; InputStream bodies to the servlet response) for reading from the result of | ||
; piped-gzipped-input-stream | ||
(defn- piped-gzipped-input-stream* | ||
[] | ||
(proxy [PipedInputStream] [])) | ||
|
||
; exactly the same as do-copy for [InputStream OutputStream], but | ||
; flushes the output on every chunk; this allows gzipped content to start | ||
; flowing to clients ASAP (a reasonable change to ring IMO) | ||
(defmethod @#'io/do-copy [(class (piped-gzipped-input-stream*)) OutputStream] | ||
[^InputStream input ^OutputStream output opts] | ||
(let [buffer (make-array Byte/TYPE (or (:buffer-size opts) 1024))] | ||
(loop [] | ||
(let [size (.read input buffer)] | ||
(when (pos? size) | ||
(do (.write output buffer 0 size) | ||
(.flush output) | ||
(recur))))))) | ||
|
||
(defn piped-gzipped-input-stream [in] | ||
(let [pipe-in (PipedInputStream.) | ||
(let [pipe-in (piped-gzipped-input-stream*) | ||
pipe-out (PipedOutputStream. pipe-in)] | ||
(future ; new thread to prevent blocking deadlock | ||
(with-open [out (GZIPOutputStream. pipe-out)] | ||
; separate thread to prevent blocking deadlock | ||
(future | ||
(with-open [out (if @flushable-gzip? | ||
(GZIPOutputStream. pipe-out true) | ||
(GZIPOutputStream. pipe-out))] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doing this check at runtime works, but only because there happens to be a constructor accepting two args (stream, int) on JDK6 - even though the |
||
(if (seq? in) | ||
(doseq [string in] (io/copy (str string) out)) | ||
(doseq [string in] | ||
(io/copy (str string) out) | ||
(.flush out)) | ||
(io/copy in out))) | ||
(when (instance? Closeable in) | ||
(.close in))) | ||
(.close ^Closeable in))) | ||
pipe-in)) | ||
|
||
(defn gzipped-response [resp] | ||
|
@@ -34,7 +67,7 @@ | |
(not (get-in resp [:headers "Content-Encoding"])) | ||
(or | ||
(and (string? body) (> (count body) 200)) | ||
(seq? body) | ||
(and (seq? body) @flushable-gzip?) | ||
(instance? InputStream body) | ||
(instance? File body))) | ||
(let [accepts (get-in req [:headers "accept-encoding"] "") | ||
|
@@ -43,4 +76,4 @@ | |
(match 3)))) | ||
(gzipped-response resp) | ||
resp)) | ||
resp)))) | ||
resp)))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does defining this check at compile-time still work if this library ever gets AOTed? I remember some issues in clojure.core.reducers trying to auto-detect JDK7 in a similar way, and all it did was auto-detect what was present on the build machine. I'd be more comfortable defining this as a
delay
and then forcing it when needed, so it always runs on the client machine.