long_body 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +20 -0
- data/README.md +75 -0
- data/Rakefile +47 -0
- data/lib/long_body/deferrable_body.rb +32 -0
- data/lib/long_body/hijack_handler.rb +54 -0
- data/lib/long_body/lint_bypass.rb +14 -0
- data/lib/long_body/thin_handler.rb +114 -0
- data/lib/long_body/version.rb +3 -0
- data/lib/long_body.rb +73 -0
- data/long_body.gemspec +89 -0
- data/spec/long_body_spec.rb +12 -0
- data/spec/shared_webserver_examples.rb +61 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/streaming_app.ru +50 -0
- data/spec/support/server_runners.rb +61 -0
- data/spec/test_download.rb +42 -0
- metadata +230 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 919ec90f6d67ef12a71c59222abc33cfb7c48256
|
4
|
+
data.tar.gz: 3ba12c21e137a5ecf7351c36a2c03dfdac80b536
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9947da48ea520ee2a2f5d7d73e103f0264853ce10ae868624d835f61097a78979d6e7c5be1385dfe0a51554e4da22920944c98c6990fcd4f75417b193e37fefc
|
7
|
+
data.tar.gz: 6fcb2f4830d5cc4be8ad2fce1c9c5f899a65423e30d37e3e24b1cfc87d8052bda8e9c24c507d411e7a6a4bb247478dd4a34fc58e6bc6fdf40239d24891edf669
|
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
gem 'rack', '~> 1'
|
3
|
+
|
4
|
+
# Add dependencies to develop your gem here.
|
5
|
+
# Include everything needed to run rake, tests, features, etc.
|
6
|
+
group :development do
|
7
|
+
gem 'thin', '~> 1.6'
|
8
|
+
gem 'puma', '~> 2', '>= 2.13.4'
|
9
|
+
gem "rspec", "~> 3.2", '< 3.3' # 3.3 does not work nicely with TextMate ATM
|
10
|
+
gem "rdoc", "~> 3.12"
|
11
|
+
gem "bundler", "~> 1.0"
|
12
|
+
gem "jeweler", "~> 2.0.1"
|
13
|
+
gem "simplecov", ">= 0"
|
14
|
+
gem 'retriable'
|
15
|
+
gem 'passenger', '~> 5'
|
16
|
+
gem 'rainbows'
|
17
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2015 Julik Tarkhanov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# long_body
|
2
|
+
|
3
|
+
Universal Rack middleware for immediate (after-headers) streaming of long Rack response bodies.
|
4
|
+
Normally most Rack handler webservers will buffer your entire response, or buffer your response
|
5
|
+
without telling you for some time before sending it.
|
6
|
+
|
7
|
+
This module provides a universal wrapper that will use the features of a specific webserver
|
8
|
+
to send your response with as little buffering as possible (direct to socket). For decent
|
9
|
+
servers that allow `rack.hijack` it will use that, for Thin it will use deferrables.
|
10
|
+
|
11
|
+
Note that within Thin sleeping in a long body might block the EM loop.
|
12
|
+
|
13
|
+
## Usage examples
|
14
|
+
|
15
|
+
Server-sent events combined with `Transfer-Encoding: chunked` (can be used for chat applications and so forth):
|
16
|
+
|
17
|
+
class EventSource
|
18
|
+
def each
|
19
|
+
20.times do | event_num |
|
20
|
+
yield "event: ping\n"
|
21
|
+
yield "data: ping_number_#{event_num}"
|
22
|
+
yield "\n\n"
|
23
|
+
sleep 3
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# config.ru
|
29
|
+
use LongBody
|
30
|
+
use Rack::Chunked
|
31
|
+
run ->(env) {
|
32
|
+
h = {'Content-Type' => 'text/event-stream'}
|
33
|
+
[200, h, EventSource.new]
|
34
|
+
}
|
35
|
+
|
36
|
+
Streaming a large file, without buffering:
|
37
|
+
|
38
|
+
# config.ru
|
39
|
+
use LongBody
|
40
|
+
run ->(env) {
|
41
|
+
s = File.size("/tmp/large_file.bin")
|
42
|
+
h = {'Content-Length' => s}
|
43
|
+
[200, h, File.open(s, 'rb')]
|
44
|
+
}
|
45
|
+
|
46
|
+
## Compatibility
|
47
|
+
|
48
|
+
This gem is tested on Ruby 2.2, and should run acceptably well on Ruby 2.+. If you are using Thin you have to
|
49
|
+
use Ruby 2.+ because of the fiber stack size limitation. If you are using Puma, Rainbows or other threaded
|
50
|
+
server running this gem on 1.9.2 should be possible as well.
|
51
|
+
|
52
|
+
<table>
|
53
|
+
<tr><th>Webserver</th><th>Version tested</th><th>Compatibility</th></tr>
|
54
|
+
<tr><td>Puma</td><td>2.13.4</td><td>Yes (use versions >= 2.13.4 due to a bug)</td></tr>
|
55
|
+
<tr><td>Passenger</td><td>5.0.15</td><td>Yes</td></tr>
|
56
|
+
<tr><td>Thin</td><td>1.6.3</td><td>Yes</td></tr>
|
57
|
+
<tr><td>Rainbows</td><td>4.6.2</td><td>Yes</td></tr>
|
58
|
+
<tr><td>WEBrick</td><td>stdlib 2.2.1</td><td>No (uses IO.pipe for hijack)</td></tr>
|
59
|
+
</table>
|
60
|
+
|
61
|
+
## Contributing to long_body
|
62
|
+
|
63
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
64
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
65
|
+
* Fork the project.
|
66
|
+
* Start a feature/bugfix branch.
|
67
|
+
* Commit and push until you are happy with your contribution.
|
68
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
69
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
70
|
+
|
71
|
+
## Copyright
|
72
|
+
|
73
|
+
Copyright (c) 2015 Julik Tarkhanov. See LICENSE.txt for
|
74
|
+
further details.
|
75
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
require_relative 'lib/long_body/version'
|
16
|
+
|
17
|
+
Jeweler::Tasks.new do |gem|
|
18
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
19
|
+
gem.name = "long_body"
|
20
|
+
gem.homepage = "http://github.com/julik/long_body"
|
21
|
+
gem.license = "MIT"
|
22
|
+
gem.summary = %Q{Direct-to-socket streaming of Rack response bodies}
|
23
|
+
gem.description = %Q{Direct-to-socket streaming of Rack response bodies}
|
24
|
+
gem.version = LongBody::VERSION
|
25
|
+
gem.email = "me@julik.nl"
|
26
|
+
gem.authors = ["Julik Tarkhanov"]
|
27
|
+
# dependencies defined in Gemfile
|
28
|
+
end
|
29
|
+
Jeweler::RubygemsDotOrgTasks.new
|
30
|
+
|
31
|
+
require 'rspec/core'
|
32
|
+
require 'rspec/core/rake_task'
|
33
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
34
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
35
|
+
end
|
36
|
+
|
37
|
+
task :default => :spec
|
38
|
+
|
39
|
+
require 'rdoc/task'
|
40
|
+
Rake::RDocTask.new do |rdoc|
|
41
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
42
|
+
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = "long_body #{version}"
|
45
|
+
rdoc.rdoc_files.include('README*')
|
46
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
47
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Lifted from https://github.com/macournoyer/thin_async/blob/master/lib/thin/async.rb
|
2
|
+
# and originally written by James Tucker <raggi@rubyforge.org>
|
3
|
+
class LongBody::DeferrableBody
|
4
|
+
include EM::Deferrable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@queue = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(body)
|
11
|
+
@queue << body
|
12
|
+
schedule_dequeue
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(&blk)
|
16
|
+
@body_callback = blk
|
17
|
+
schedule_dequeue
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def schedule_dequeue
|
23
|
+
return unless @body_callback
|
24
|
+
EM.next_tick do
|
25
|
+
next unless body = @queue.shift
|
26
|
+
body.each do |chunk|
|
27
|
+
@body_callback.call(chunk)
|
28
|
+
end
|
29
|
+
schedule_dequeue unless @queue.empty?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# The problem with fast each() bodies is that the Ruby process will
|
2
|
+
# enter a busy wait if the write socket for the webserver is saturated.
|
3
|
+
#
|
4
|
+
# We can bypass that by releasing the CPU using a select(), but for that
|
5
|
+
# we have to use "rack.hijack" in combination with a nonblocking write.
|
6
|
+
#
|
7
|
+
# For more on this:
|
8
|
+
# http://apidock.com/ruby/IO/write_nonblock
|
9
|
+
# http://old.blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/
|
10
|
+
#
|
11
|
+
# By using this class as a middleware you will put a select() wait
|
12
|
+
# spinlock on the output socket of the webserver.
|
13
|
+
#
|
14
|
+
# The middleware will only trigger for 200 and 206 responses, and only
|
15
|
+
# if the Rack handler it is running on supports rack hijacking.
|
16
|
+
module LongBody::HijackHandler
|
17
|
+
extend self
|
18
|
+
HIJACK_HEADER = 'rack.hijack'.freeze
|
19
|
+
|
20
|
+
def perform(env, s, h, b)
|
21
|
+
# Replace the output with our socket moderating technology 2.0
|
22
|
+
h[HIJACK_HEADER] = create_socket_writer_lambda_with_body(b)
|
23
|
+
[s, h, []] # Recommended response body for partial hijack is an empty Array
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_socket_writer_lambda_with_body(rack_response_body)
|
27
|
+
lambda do |socket|
|
28
|
+
begin
|
29
|
+
rack_response_body.each do | chunk |
|
30
|
+
begin
|
31
|
+
num_bytes_written = socket.write_nonblock(chunk)
|
32
|
+
# If we could write only partially, make sure we do a retry on the next
|
33
|
+
# iteration with the remaining part
|
34
|
+
if num_bytes_written < chunk.bytesize
|
35
|
+
chunk = chunk[num_bytes_written..-1]
|
36
|
+
raise Errno::EINTR
|
37
|
+
end
|
38
|
+
rescue IO::WaitWritable, Errno::EINTR # The output socket is saturated.
|
39
|
+
# If we are running within a threaded server,
|
40
|
+
# let another thread preempt here. We are waiting for IO
|
41
|
+
# and some other thread might have things to do here.
|
42
|
+
IO.select(nil, [socket]) # ...then wait on the socket to be writable again
|
43
|
+
retry # and off we go...
|
44
|
+
rescue Errno::EPIPE, Errno::EPROTOTYPE # Happens when the client aborts the connection
|
45
|
+
return
|
46
|
+
end
|
47
|
+
end
|
48
|
+
ensure
|
49
|
+
rack_response_body.close if rack_response_body.respond_to?(:close)
|
50
|
+
socket.close if socket.respond_to?(:closed?) && socket.respond_to?(:close) && !socket.closed?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Rack::Lint bails on async.callback responses. "Fix" it to bypass
|
2
|
+
# if running within Thin and when async.callback is available.
|
3
|
+
module Rack
|
4
|
+
class Lint
|
5
|
+
THIN_RE = /^thin/.freeze
|
6
|
+
def call(env=nil)
|
7
|
+
if env && env['async.callback'] && env['SERVER_SOFTWARE'].to_s =~ THIN_RE
|
8
|
+
@app.call(env)
|
9
|
+
else
|
10
|
+
dup._call(env)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# Turns an iterable Rack response body that responds to each() into
|
2
|
+
# something that Thin can use within EventMachine. Uses internal Thin
|
3
|
+
# interfaces so not applicable for other servers.
|
4
|
+
module LongBody::ThinHandler
|
5
|
+
extend self
|
6
|
+
|
7
|
+
AsyncResponse = [-1, {}, []].freeze
|
8
|
+
|
9
|
+
# A wrapper that allows us to access an object that yields from each()
|
10
|
+
# as if it were an Enumerator-ish object.
|
11
|
+
#
|
12
|
+
# arr = %w( a b )
|
13
|
+
# w = FiberWrapper.new(arr)
|
14
|
+
# w.take #=> 'a'
|
15
|
+
# w.take #=> 'b'
|
16
|
+
# w.take #=> nil # Ended
|
17
|
+
class FiberWrapper
|
18
|
+
def initialize(eachable)
|
19
|
+
@fiber = Fiber.new do
|
20
|
+
eachable.each{|chunk| Fiber.yield(chunk.to_s) }
|
21
|
+
eachable.close if eachable.respond_to?(:close)
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def take
|
27
|
+
@fiber.resume
|
28
|
+
rescue FiberError
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Controls the scheduling of the trickle-feed using EM.next_tick.
|
34
|
+
class ResponseSender
|
35
|
+
attr_reader :deferrable_body
|
36
|
+
def initialize(eachable_body)
|
37
|
+
require_relative 'deferrable_body'
|
38
|
+
require_relative 'lint_bypass'
|
39
|
+
@eachable_body = eachable_body
|
40
|
+
@enumerator = FiberWrapper.new(eachable_body)
|
41
|
+
@deferrable_body = LongBody::DeferrableBody.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def abort!
|
45
|
+
@eachable_body.abort!
|
46
|
+
@deferrable_body.fail
|
47
|
+
end
|
48
|
+
|
49
|
+
def send_next_chunk
|
50
|
+
next_chunk = begin
|
51
|
+
@enumerator.take
|
52
|
+
rescue StandardError => e
|
53
|
+
abort!
|
54
|
+
end
|
55
|
+
|
56
|
+
if next_chunk
|
57
|
+
@deferrable_body.call([next_chunk]) # Has to be given in an Array
|
58
|
+
EM.next_tick { send_next_chunk }
|
59
|
+
else
|
60
|
+
@deferrable_body.succeed
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# We need a way to raise from each() when the connection
|
66
|
+
# is closed prematurely.
|
67
|
+
class BodyWrapperWithExplicitClose
|
68
|
+
def abort!
|
69
|
+
@aborted = true; close
|
70
|
+
end
|
71
|
+
|
72
|
+
def close
|
73
|
+
@rack_body.close if @rack_body.respond_to?(:close)
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize(rack_body)
|
77
|
+
@rack_body = rack_body
|
78
|
+
end
|
79
|
+
|
80
|
+
def each
|
81
|
+
@rack_body.each do | bytes |
|
82
|
+
# Break the body out of the loop if the response is aborted (client disconnect)
|
83
|
+
raise "Disconnect or connection close" if @aborted
|
84
|
+
yield(bytes)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
C_async_close = 'async.close'.freeze
|
90
|
+
C_async_callback = 'async.callback'.freeze
|
91
|
+
|
92
|
+
def perform(env, s, h, b)
|
93
|
+
# Wrap in a handler that will raise from within each()
|
94
|
+
# if async.close is triggered while the response is dripping by
|
95
|
+
b = BodyWrapperWithExplicitClose.new(b)
|
96
|
+
|
97
|
+
# Wrap in a handler that manages the DeferrableBody
|
98
|
+
sender = ResponseSender.new(b)
|
99
|
+
|
100
|
+
# Abort the body sending on both of those.
|
101
|
+
env[C_async_close].callback { sender.abort! }
|
102
|
+
env[C_async_close].errback { sender.abort! }
|
103
|
+
env[C_async_callback].call([s, h, sender.deferrable_body])
|
104
|
+
sender.send_next_chunk
|
105
|
+
|
106
|
+
AsyncResponse # Let Thin know we are using async.*
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def running_with_thin?(env)
|
112
|
+
defined?(Thin) && env[C_async_callback] && env[C_async_callback].respond_to?(:call)
|
113
|
+
end
|
114
|
+
end
|
data/lib/long_body.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# The base middleware class to use in your Rack application, Sinatra, Rails etc.
|
2
|
+
#
|
3
|
+
# use LongBody
|
4
|
+
#
|
5
|
+
# Note that if you want to use Rack::Chunked (and you most likely do) you have to insert it
|
6
|
+
# below LongBody:
|
7
|
+
#
|
8
|
+
# use LongBody
|
9
|
+
# use Rack::Chunked
|
10
|
+
class LongBody
|
11
|
+
|
12
|
+
require_relative 'long_body/version'
|
13
|
+
require_relative 'long_body/thin_handler'
|
14
|
+
require_relative 'long_body/hijack_handler'
|
15
|
+
|
16
|
+
class MisconfiguredBody < StandardError
|
17
|
+
def message
|
18
|
+
"Either Transfer-Encoding: chunked or Content-Length: <digits> must be set. " +
|
19
|
+
"If uncertain, insert Rack::ContentLength into your middleware chain to set Content-Length " +
|
20
|
+
"for all responses that do not pre-specify it, and Rack::Chunked to apply chunked encoding " +
|
21
|
+
"to all responses of unknown length"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
STREAMABLE_CODES = [200, 206]
|
26
|
+
|
27
|
+
def status_might_have_streamable_body?(status_code)
|
28
|
+
STREAMABLE_CODES.include?(status_code.to_i)
|
29
|
+
end
|
30
|
+
|
31
|
+
def ensure_chunked_or_content_length!(header_hash)
|
32
|
+
unless header_hash['Transfer-Encoding'] == 'chunked' || header_hash['Content-Length']
|
33
|
+
raise MisconfiguredBody
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(app)
|
38
|
+
@app = app
|
39
|
+
end
|
40
|
+
|
41
|
+
C_rack_logger = 'rack.logger'.freeze
|
42
|
+
HIJACK_SUPPORTED = 'rack.hijack?'.freeze
|
43
|
+
ASYNC_SUPPORT = 'async.callback'.freeze
|
44
|
+
|
45
|
+
def call(env)
|
46
|
+
# Call the upstream first
|
47
|
+
s, h, b = @app.call(env)
|
48
|
+
|
49
|
+
# If the response has nothing to do with the streaming response, just
|
50
|
+
# let it go through as it is not big enough to bother. Also if there is no hijack
|
51
|
+
# support there is no sense to bother at all.
|
52
|
+
return [s, h, b] unless status_might_have_streamable_body?(s)
|
53
|
+
|
54
|
+
# If the body is nil or is not each-able, return the original response - there probably is some other
|
55
|
+
# async trickery going on downstream from us, and we should not intercept this response.
|
56
|
+
return [s, h, b] if (b.nil? || !b.respond_to?(:each) || (b.respond_to?(:empty?) && b.empty?))
|
57
|
+
|
58
|
+
# Ensure either Content-Length or chunking is in place
|
59
|
+
ensure_chunked_or_content_length!(h)
|
60
|
+
|
61
|
+
# TODO: ensure not already hijacked
|
62
|
+
|
63
|
+
if env[ASYNC_SUPPORT]
|
64
|
+
env[C_rack_logger].info("Streaming via async.callback (Thin)") if env[C_rack_logger].respond_to?(:info)
|
65
|
+
ThinHandler.perform(env, s, h, b)
|
66
|
+
elsif env[HIJACK_SUPPORTED]
|
67
|
+
env[C_rack_logger].info("Streaming via hijack and IO.select") if env[C_rack_logger].respond_to?(:info)
|
68
|
+
HijackHandler.perform(env, s, h, b)
|
69
|
+
else
|
70
|
+
[s, h, b] # No recourse
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/long_body.gemspec
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: long_body 0.0.1 ruby lib
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "long_body"
|
9
|
+
s.version = "0.0.1"
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.authors = ["Julik Tarkhanov"]
|
14
|
+
s.date = "2015-08-29"
|
15
|
+
s.description = "Direct-to-socket streaming of Rack response bodies"
|
16
|
+
s.email = "me@julik.nl"
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE.txt",
|
19
|
+
"README.md"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".rspec",
|
24
|
+
"Gemfile",
|
25
|
+
"LICENSE.txt",
|
26
|
+
"README.md",
|
27
|
+
"Rakefile",
|
28
|
+
"lib/long_body.rb",
|
29
|
+
"lib/long_body/deferrable_body.rb",
|
30
|
+
"lib/long_body/hijack_handler.rb",
|
31
|
+
"lib/long_body/lint_bypass.rb",
|
32
|
+
"lib/long_body/thin_handler.rb",
|
33
|
+
"lib/long_body/version.rb",
|
34
|
+
"long_body.gemspec",
|
35
|
+
"spec/long_body_spec.rb",
|
36
|
+
"spec/shared_webserver_examples.rb",
|
37
|
+
"spec/spec_helper.rb",
|
38
|
+
"spec/streaming_app.ru",
|
39
|
+
"spec/support/server_runners.rb",
|
40
|
+
"spec/test_download.rb"
|
41
|
+
]
|
42
|
+
s.homepage = "http://github.com/julik/long_body"
|
43
|
+
s.licenses = ["MIT"]
|
44
|
+
s.rubygems_version = "2.2.2"
|
45
|
+
s.summary = "Direct-to-socket streaming of Rack response bodies"
|
46
|
+
|
47
|
+
if s.respond_to? :specification_version then
|
48
|
+
s.specification_version = 4
|
49
|
+
|
50
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
51
|
+
s.add_runtime_dependency(%q<rack>, ["~> 1"])
|
52
|
+
s.add_development_dependency(%q<thin>, ["~> 1.6"])
|
53
|
+
s.add_development_dependency(%q<puma>, [">= 2.13.4", "~> 2"])
|
54
|
+
s.add_development_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
|
55
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
56
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
57
|
+
s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
|
58
|
+
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
59
|
+
s.add_development_dependency(%q<retriable>, [">= 0"])
|
60
|
+
s.add_development_dependency(%q<passenger>, ["~> 5"])
|
61
|
+
s.add_development_dependency(%q<rainbows>, [">= 0"])
|
62
|
+
else
|
63
|
+
s.add_dependency(%q<rack>, ["~> 1"])
|
64
|
+
s.add_dependency(%q<thin>, ["~> 1.6"])
|
65
|
+
s.add_dependency(%q<puma>, [">= 2.13.4", "~> 2"])
|
66
|
+
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
|
67
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
68
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
69
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
70
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
71
|
+
s.add_dependency(%q<retriable>, [">= 0"])
|
72
|
+
s.add_dependency(%q<passenger>, ["~> 5"])
|
73
|
+
s.add_dependency(%q<rainbows>, [">= 0"])
|
74
|
+
end
|
75
|
+
else
|
76
|
+
s.add_dependency(%q<rack>, ["~> 1"])
|
77
|
+
s.add_dependency(%q<thin>, ["~> 1.6"])
|
78
|
+
s.add_dependency(%q<puma>, [">= 2.13.4", "~> 2"])
|
79
|
+
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2"])
|
80
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
81
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
82
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
83
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
84
|
+
s.add_dependency(%q<retriable>, [">= 0"])
|
85
|
+
s.add_dependency(%q<passenger>, ["~> 5"])
|
86
|
+
s.add_dependency(%q<rainbows>, [">= 0"])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require_relative 'test_download'
|
3
|
+
require 'retriable'
|
4
|
+
require_relative 'shared_webserver_examples'
|
5
|
+
|
6
|
+
describe "LongBody" do
|
7
|
+
SERVERS.each do | server_engine |
|
8
|
+
context "on #{server_engine.name}" do
|
9
|
+
it_behaves_like "compliant"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
RSpec.shared_examples "compliant" do
|
2
|
+
it "when the response is sent in full works with predefined Content-Length" do | example |
|
3
|
+
parts = TestDownload.perform("http://0.0.0.0:9393/with-content-length")
|
4
|
+
timings = parts.map(&:time_difference)
|
5
|
+
|
6
|
+
# $postrun.puts ""
|
7
|
+
# $postrun.puts "#{example.full_description} part receive timings: #{timings.inspect}"
|
8
|
+
# $postrun.puts ""
|
9
|
+
|
10
|
+
expect(File).to exist('/tmp/streamer_close.mark')
|
11
|
+
|
12
|
+
# Ensure the time recieved of each part is within the tolerances, and certainly
|
13
|
+
# at least 1 second after the previous
|
14
|
+
(1..(parts.length-1)).each do | part_i|
|
15
|
+
this_part = parts[part_i]
|
16
|
+
previous_part = parts[part_i -1]
|
17
|
+
received_after_previous = this_part.time_difference - previous_part.time_difference
|
18
|
+
|
19
|
+
# Ensure there was some time before this chunk arrived. This is the most important test.
|
20
|
+
expect(received_after_previous).to be_within(1).of(0.3)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'when the response is sent in full works with chunked encoding' do | example |
|
25
|
+
parts = TestDownload.perform("http://0.0.0.0:9393/chunked")
|
26
|
+
timings = parts.map(&:time_difference)
|
27
|
+
|
28
|
+
# $postrun.puts example.full_description
|
29
|
+
# $postrun.puts "Part receive timings: #{timings.inspect}"
|
30
|
+
# $postrun.puts ""
|
31
|
+
|
32
|
+
expect(File).to exist('/tmp/streamer_close.mark')
|
33
|
+
|
34
|
+
(1..(parts.length-1)).each do | part_i|
|
35
|
+
this_part = parts[part_i]
|
36
|
+
previous_part = parts[part_i -1]
|
37
|
+
received_after_previous = this_part.time_difference - previous_part.time_difference
|
38
|
+
|
39
|
+
# Ensure there was some time before this chunk arrived. This is the most important test.
|
40
|
+
expect(received_after_previous).to be_within(1).of(0.3)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'when the HTTP client process is killed midflight, does not read more chunks from the body object' do
|
45
|
+
# This test checks whether the server makes the iterable body complete if the client closes the connection
|
46
|
+
# prematurely. If you have the callbacks set up wrong on Thin, for instance, it will read the response
|
47
|
+
# completely and potentially buffer it in memory, filling up your RAM. We need to ensure that the server
|
48
|
+
# uses it's internal mechanics to stop reading the body once the client is dropped.
|
49
|
+
pid = fork do
|
50
|
+
TestDownload.perform("http://0.0.0.0:9393/with-content-length")
|
51
|
+
end
|
52
|
+
sleep(1)
|
53
|
+
Process.kill("KILL", pid)
|
54
|
+
|
55
|
+
written_parts_list = File.read("/tmp/streamer_messages.log").split("\n")
|
56
|
+
expect(written_parts_list.length).to be < 6
|
57
|
+
|
58
|
+
expect(File).to exist('/tmp/streamer_close.mark')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
require 'long_body'
|
6
|
+
|
7
|
+
# Requires supporting files with custom matchers and macros, etc,
|
8
|
+
# in ./support/ and its subdirectories.
|
9
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.order = 'random'
|
13
|
+
config.before :suite do
|
14
|
+
$postrun = StringIO.new
|
15
|
+
$postrun << "\n\n"
|
16
|
+
|
17
|
+
SERVERS.each(&:start!)
|
18
|
+
|
19
|
+
sleep 0.5 until SERVERS.all?(&:running?)
|
20
|
+
end
|
21
|
+
|
22
|
+
config.before :each do
|
23
|
+
FileUtils.rm('/tmp/streamer_messages.log') if File.exist?('/tmp/streamer_messages.log')
|
24
|
+
FileUtils.rm('/tmp/streamer_close.log') if File.exist?('/tmp/streamer_close.log')
|
25
|
+
end
|
26
|
+
|
27
|
+
config.after :suite do
|
28
|
+
SERVERS.each(&:stop!)
|
29
|
+
puts $postrun.string
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/long_body')
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
# The test app
|
5
|
+
class Streamer
|
6
|
+
|
7
|
+
class TestBody
|
8
|
+
def each
|
9
|
+
File.open("/tmp/streamer_messages.log", "w") do |f|
|
10
|
+
25.times do |i|
|
11
|
+
sleep 0.3
|
12
|
+
yield "Message number #{i}"
|
13
|
+
f.puts(i)
|
14
|
+
f.flush # Make sure it is on disk
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def close
|
20
|
+
FileUtils.touch('/tmp/streamer_close.mark')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.call(env)
|
25
|
+
# The absence of Content-Length will trigger Rack::Chunking into work automatically.
|
26
|
+
[200, {'Content-Type' => 'text/plain'}, TestBody.new]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class StreamerWithLength < Streamer
|
31
|
+
def self.call(env)
|
32
|
+
s, h, b = super
|
33
|
+
[s, h.merge('Content-Length' => '415'), b]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
map '/chunked' do
|
38
|
+
use LongBody
|
39
|
+
use Rack::Chunked
|
40
|
+
run Streamer
|
41
|
+
end
|
42
|
+
|
43
|
+
map '/with-content-length' do
|
44
|
+
use LongBody
|
45
|
+
run StreamerWithLength
|
46
|
+
end
|
47
|
+
|
48
|
+
map '/alive' do
|
49
|
+
run ->(env) { [200, {}, ['Yes']]}
|
50
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
TEST_RACK_APP = File.join(File.dirname(File.expand_path(__FILE__)), '..', 'streaming_app.ru')
|
2
|
+
|
3
|
+
class RunningServer < Struct.new(:name, :command, :port)
|
4
|
+
def command
|
5
|
+
super % [port, TEST_RACK_APP]
|
6
|
+
end
|
7
|
+
|
8
|
+
def start!
|
9
|
+
# Boot Puma in a forked process
|
10
|
+
full_path = File.join(File.dirname(File.expand_path(__FILE__)), 'streaming_app.ru')
|
11
|
+
@pid = fork do
|
12
|
+
puts "Spinning up with #{command.inspect}"
|
13
|
+
# Do not pollute the RSpec output with the Puma logs, save the stuff
|
14
|
+
# to the logfiles instead
|
15
|
+
$stdout.reopen(File.open('%s_output.log' % name, 'a'))
|
16
|
+
$stderr.reopen(File.open('%s_output.log' % name, 'a'))
|
17
|
+
|
18
|
+
# Since we have to do with timing tolerances, having the output drip in ASAP is useful
|
19
|
+
$stdout.sync = true
|
20
|
+
$stderr.sync = true
|
21
|
+
exec(command)
|
22
|
+
end
|
23
|
+
|
24
|
+
Thread.new do
|
25
|
+
# Wait for Puma to be online, poll the alive URL until it stops responding
|
26
|
+
loop do
|
27
|
+
sleep 0.5
|
28
|
+
begin
|
29
|
+
this_server_url = "http://0.0.0.0:%d/alive" % port
|
30
|
+
TestDownload.perform(this_server_url)
|
31
|
+
puts "#{name} is alive!"
|
32
|
+
@running = true
|
33
|
+
break
|
34
|
+
rescue Errno::ECONNREFUSED
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
trap("TERM") { stop! }
|
40
|
+
end
|
41
|
+
|
42
|
+
def running?
|
43
|
+
!!@running
|
44
|
+
end
|
45
|
+
|
46
|
+
def stop!
|
47
|
+
return unless @pid
|
48
|
+
|
49
|
+
# Tell the webserver to quit, twice (we do not care if there are running responses)
|
50
|
+
%W( TERM TERM KILL ).each {|sig| Process.kill(sig, @pid); sleep 0.5 }
|
51
|
+
@pid = nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
SERVERS = [
|
56
|
+
RunningServer.new(:puma, "bundle exec puma --port %d %s", 9393),
|
57
|
+
RunningServer.new(:thin, "bundle exec thin --port %d --rackup %s start", 9394),
|
58
|
+
RunningServer.new(:rainbows, "bundle exec rainbows --port %d %s", 9395),
|
59
|
+
RunningServer.new(:passenger, "bundle exec passenger start --port %d --rackup %s", 9396),
|
60
|
+
]
|
61
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module TestDownload
|
2
|
+
extend self
|
3
|
+
Part = Struct.new(:time_difference, :payload)
|
4
|
+
def perform(uri)
|
5
|
+
response_chunks = []
|
6
|
+
uri = URI(uri.to_s)
|
7
|
+
conn = Net::HTTP.new(uri.host, uri.port)
|
8
|
+
conn.read_timeout = 120 # Might take LONG
|
9
|
+
conn.start do |http|
|
10
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
11
|
+
before_first = Time.now
|
12
|
+
http.request(req) do |res|
|
13
|
+
res.read_body do |chunk|
|
14
|
+
diff = (Time.now - before_first).to_f
|
15
|
+
response_chunks << Part.new(diff, chunk)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
response_chunks
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform_and_abort_after_3_chunks(uri)
|
23
|
+
response_chunks = []
|
24
|
+
catch :abort do
|
25
|
+
uri = URI(uri.to_s)
|
26
|
+
conn = Net::HTTP.new(uri.host, uri.port)
|
27
|
+
conn.read_timeout = 120 # Might take LONG
|
28
|
+
conn.start do |http|
|
29
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
30
|
+
before_first = Time.now.to_i
|
31
|
+
http.request(req) do |res|
|
32
|
+
res.read_body do |chunk|
|
33
|
+
diff = Time.now.to_i - before_first
|
34
|
+
response_chunks << Part.new(diff, chunk)
|
35
|
+
throw :abort if response_chunks.length == 3
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
response_chunks
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: long_body
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Julik Tarkhanov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thin
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: puma
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.13.4
|
48
|
+
- - "~>"
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '2'
|
51
|
+
type: :development
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 2.13.4
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rspec
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "<"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.3'
|
68
|
+
- - "~>"
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '3.2'
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - "<"
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '3.3'
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '3.2'
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: rdoc
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '3.12'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '3.12'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: bundler
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '1.0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '1.0'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: jeweler
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - "~>"
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: 2.0.1
|
116
|
+
type: :development
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - "~>"
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 2.0.1
|
123
|
+
- !ruby/object:Gem::Dependency
|
124
|
+
name: simplecov
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
type: :development
|
131
|
+
prerelease: false
|
132
|
+
version_requirements: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
- !ruby/object:Gem::Dependency
|
138
|
+
name: retriable
|
139
|
+
requirement: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
type: :development
|
145
|
+
prerelease: false
|
146
|
+
version_requirements: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
- !ruby/object:Gem::Dependency
|
152
|
+
name: passenger
|
153
|
+
requirement: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - "~>"
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '5'
|
158
|
+
type: :development
|
159
|
+
prerelease: false
|
160
|
+
version_requirements: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - "~>"
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '5'
|
165
|
+
- !ruby/object:Gem::Dependency
|
166
|
+
name: rainbows
|
167
|
+
requirement: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
type: :development
|
173
|
+
prerelease: false
|
174
|
+
version_requirements: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '0'
|
179
|
+
description: Direct-to-socket streaming of Rack response bodies
|
180
|
+
email: me@julik.nl
|
181
|
+
executables: []
|
182
|
+
extensions: []
|
183
|
+
extra_rdoc_files:
|
184
|
+
- LICENSE.txt
|
185
|
+
- README.md
|
186
|
+
files:
|
187
|
+
- ".document"
|
188
|
+
- ".rspec"
|
189
|
+
- Gemfile
|
190
|
+
- LICENSE.txt
|
191
|
+
- README.md
|
192
|
+
- Rakefile
|
193
|
+
- lib/long_body.rb
|
194
|
+
- lib/long_body/deferrable_body.rb
|
195
|
+
- lib/long_body/hijack_handler.rb
|
196
|
+
- lib/long_body/lint_bypass.rb
|
197
|
+
- lib/long_body/thin_handler.rb
|
198
|
+
- lib/long_body/version.rb
|
199
|
+
- long_body.gemspec
|
200
|
+
- spec/long_body_spec.rb
|
201
|
+
- spec/shared_webserver_examples.rb
|
202
|
+
- spec/spec_helper.rb
|
203
|
+
- spec/streaming_app.ru
|
204
|
+
- spec/support/server_runners.rb
|
205
|
+
- spec/test_download.rb
|
206
|
+
homepage: http://github.com/julik/long_body
|
207
|
+
licenses:
|
208
|
+
- MIT
|
209
|
+
metadata: {}
|
210
|
+
post_install_message:
|
211
|
+
rdoc_options: []
|
212
|
+
require_paths:
|
213
|
+
- lib
|
214
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
215
|
+
requirements:
|
216
|
+
- - ">="
|
217
|
+
- !ruby/object:Gem::Version
|
218
|
+
version: '0'
|
219
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
220
|
+
requirements:
|
221
|
+
- - ">="
|
222
|
+
- !ruby/object:Gem::Version
|
223
|
+
version: '0'
|
224
|
+
requirements: []
|
225
|
+
rubyforge_project:
|
226
|
+
rubygems_version: 2.2.2
|
227
|
+
signing_key:
|
228
|
+
specification_version: 4
|
229
|
+
summary: Direct-to-socket streaming of Rack response bodies
|
230
|
+
test_files: []
|