Flushing after after very flush is great for real-time apps.
However, flushing is inefficient when apps use Rack::Response
to generate many small writes (e.g. Rack::Lobster).
Allow users to disable the default "sync: true" behavior to
reduce bandwidth usage, otherwise using Rack::Deflater can lead
to using more bandwidth than without it.
---
lib/rack/deflater.rb | 11 ++++++++---
test/spec_deflater.rb | 34 ++++++++++++++++++++++++++++++++++
2 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb
index d575adf..821f708 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -24,11 +24,15 @@ module Rack
# 'if' - a lambda enabling / disabling deflation based on returned boolean value
# e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.map(&:bytesize).reduce(0, :+) > 512 }
# 'include' - a list of content types that should be compressed
+ # 'sync' - Flushing after every chunk reduces latency for
+ # time-sensitive streaming applications, but hurts
+ # compression and throughput. Defaults to `true'.
def initialize(app, options = {})
@app = app
@condition = options[:if]
@compressible_types = options[:include]
+ @sync = options[:sync] == false ? false : true
end
def call(env)
@@ -56,7 +60,7 @@ module Rack
headers.delete('Content-Length')
mtime = headers.key?("Last-Modified") ?
Time.httpdate(headers["Last-Modified"]) : Time.now
- [status, headers, GzipStream.new(body, mtime)]
+ [status, headers, GzipStream.new(body, mtime, @sync)]
when "identity"
[status, headers, body]
when nil
@@ -67,7 +71,8 @@ module Rack
end
class GzipStream
- def initialize(body, mtime)
+ def initialize(body, mtime, sync)
+ @sync = sync
@body = body
@mtime = mtime
end
@@ -78,7 +83,7 @@ module Rack
gzip.mtime = @mtime
@body.each { |part|
gzip.write(part)
- gzip.flush
+ gzip.flush if @sync
}
ensure
gzip.close
diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb
index 0f27c85..410a143 100644
--- a/test/spec_deflater.rb
+++ b/test/spec_deflater.rb
@@ -372,4 +372,38 @@ describe Rack::Deflater do
verify(200, response, 'gzip', options)
end
+
+ it 'will honor sync: false to avoid unnecessary flushing' do
+ app_body = Object.new
+ class << app_body
+ def each
+ (0..20).each { |i| yield "hello\n".freeze }
+ end
+ end
+
+ options = {
+ 'deflater_options' => { :sync => false },
+ 'app_body' => app_body,
+ 'skip_body_verify' => true,
+ }
+ verify(200, app_body, deflate_or_gzip, options) do |status, headers, body|
+ headers.must_equal({
+ 'Content-Encoding' => 'gzip',
+ 'Vary' => 'Accept-Encoding',
+ 'Content-Type' => 'text/plain'
+ })
+
+ buf = ''
+ raw_bytes = 0
+ inflater = auto_inflater
+ body.each do |part|
+ raw_bytes += part.bytesize
+ buf << inflater.inflate(part)
+ end
+ buf << inflater.finish
+ expect = "hello\n" * 21
+ buf.must_equal expect
+ raw_bytes.must_be(:<, expect.bytesize)
+ end
+ end
end
--
EW