Repository navigation
Conversation
|
Does this need an addition to the test suite to exercise both |
|
@mikegee The example that uses |
|
Thanks for the detailed commit messages. I was about to ask why |
5b0b6a2 to
4438402
Compare
IO#readpartial is preferable in cases where the IO is a stream such as a pipe or a socket. IO#readpartial differs from IO#read in situations where there is still some data immediately available, but not the number of bytes requested. IO#read would in this case block until the requested number of bytes is available, whereas IO#readpartial would immediately return the available data, and only block if there is no data available. Another technical difference is that, while IO#read simply returns nil when it has reached EOF, IO#readpartial raises an EOFError, so we have to rescue it. With IO#readpartial uploading from pipes should now be faster, because any data that is immediately available will be written to the TCP socket, so the upload is less blocked by the pipe retrieving data. This is also useful with the Down gem, which provides an IO object around a remote file, because again uploading is less blocked by the remote file being downloaded. This IO#readpartial preference is even baked into Ruby itself; `IO.copy_stream` will also prefer IO#readpartial over IO#read.
IO#read and IO#readpartial accept an optional buffer string as a second argument. This will make it so that, instead of each chunk being allocated as a new string, each chunk is read into the buffer string (replacing any previous value). This means that no new strings will be allocated other than the buffer string on upload, so it's much more memory-efficient and gives less work to the GC. Previously we couldn't use a buffer string because of how JRuby implements Enumerators (it fetches the next chunk eagerly, replacing the one being returned). That's why we changed the implementation not to use Enumerators.
4438402 to
aa2fcc2
Compare
IO.copy_stream already has the optimized behaviour of using #readpartial when available and a buffer string. So we create a BlockIO object that responds to #write, calling the underlying block with the given data, and returns the amount of written bytes. This BlockIO object can then be used as the "destination IO" in IO.copy_stream.
aa2fcc2 to
ef587a6
Compare
|
@mikegee I made what I think is an improvement in the last commit. Instead of writing out the Here we use a trick of creating a Proc objects from an implicit block by calling def foo
block = Proc.new
endis equivalent to def foo(&block)
endThis way we can still use |
|
Wow! TIL about |
| read_method = @body.respond_to?(:readpartial) ? :readpartial : :read | ||
|
|
||
| begin | ||
| while (data = @body.send(read_method, CHUNK_SIZE)) |
There was a problem hiding this comment.
probably it's better to call public_send in here.
EDIT: Sorry, just now saw you have changed that
ixti
left a comment
There was a problem hiding this comment.
I'll take closer look later this weekend.
| # | ||
| # @yieldparam [String] | ||
| def each | ||
| return enum_for(__method__) unless block_given? |
There was a problem hiding this comment.
I removed this because we don't need it anymore, and because how JRuby implements Enumerators (jruby/jruby#4583) it's not usable on JRuby when fetching next element mutates the previous one (reading file with a buffer string). So I thought it's better to remove it if it's not being used.
There was a problem hiding this comment.
Hm. If this works with issues on JRuby, I think we should remove it then indeed :D
There was a problem hiding this comment.
Can you please also add a comment so that in future we won't add regression:
# @note This method does not returns Enumerator due to known issue on JRuby:
# https://github.com/jruby/jruby/issues/4583Asking that, beacause I know at least one developer (me) who tries to sneak to_enum __method__ unless block_given? in every each method even if there's no real need in that :D
| while (data = @body.read(BUFFER_SIZE)) | ||
| yield data | ||
| end | ||
| IO.copy_stream(@body, BlockIO.new(Proc.new)) |
There was a problem hiding this comment.
IMHO it worth to simply change each signature to def each(&block) and pass the block here, and in next clause (@body.each(&block))
| rescue StopIteration | ||
| write(headers) | ||
| end | ||
| data = headers |
There was a problem hiding this comment.
we can get rid of previous line in this case in fact:
data = join_headersIO.copy_stream on JRuby doesn't seem to limit the chunk size, it always reads the entire content at once. jruby/jruby#4701 Since we don't care about what is the size of yielded chunks, we just test whether the yielded chunks sum up to the entire content. While here we also bring back HTTP::Request::Body#each returning an Enumerator, so that tests can be simpler.
|
@ixti Updated. I brought back returning Enumerator in |
Using Enumerator interface like Enumerator#next wouldn't work correctly on JRuby with IOs, because JRuby eagerly loads the next element before it returns the current, and because each next chunk overwrites the previous one, it is impossible to retrieve first chunk via Enumerable#next. jruby/jruby#4583 In order to avoid potential bugs in the future of someone wanting to use this Enumerator interface, we remove this entirely and mandate people to pass in a block.
| IO.copy_stream(@body, BlockIO.new(Proc.new)) | ||
| IO.copy_stream(@body, BlockIO.new(block)) | ||
| elsif @body.is_a?(Enumerable) | ||
| @body.each { |chunk| yield chunk } |
There was a problem hiding this comment.
The captured block can safely be used for the is_a?(Enumerable) case, too :)
| raise RequestError, "body of wrong type: #{@body.class}" | ||
| end | ||
|
|
||
| class BlockIO |
There was a problem hiding this comment.
Probably worth name it ProcIO (or FauxIO). It took a while for me to understand that block here refers to ruby block, rather than to blocking. And worth adding a comment to the class.
|
@britishtea @ixti Updated. |
|
@ixti @britishtea Do you think this one is ready for merging? |
|
Sorry I was on a vacation and completely off the grid. Will handle this ASAP. |
|
@ixti Quick bump 😃 |
|
Sorry took me so long. Merged. Thank you! |
|
@ixti No problem at all! Sorry that I've been nagging about it, I just thought about writing a section in Shrine documentation about how to upload files with various HTTP libraries, and I wanted to wait until HTTP.rb had streaming IO uploads, so that I don't have to change the documentation later. Now that we merged these final memory optimizations, and made deflating work with IO/Enumerable uploads (#412), do you think we can release a new HTTP.rb version with the new streaming IO uploads feature? |
|
@janko-m I want to work-on/merge all outstanding PRs that can be merged in relatively fast, and then we'll be good to release 3.0.0.pre1 I just want to merge max breaking changes as we ae doing that anyway ;)) |
|
@ixti Sounds good, thank you! |
This pull request adds two more upload improvements. First commit changes
HTTP::Request::Bodyto useIO#readpartialif available, and second commit adds a buffer string for memory-efficient uploads; both are explained in detail in their commit descriptions.I was hoping that, once this is merged (or rejected), we could release a new version of HTTP.rb and form_data.rb with the streaming upload feature.