5 ms·
I didn't go in to this in much detail in my article, but I'd like to follow up with my long term plan for this feature. I think that buffered responses are a s
by tenderlove 14y ago
I didn't go in to this in much detail in my article, but I'd like to follow up with my long term plan for this feature. I think that buffered responses are a special case of streaming responses (they're streaming responses with one chunk), and I'd like to make this API the underpinnings of the response system in Rails.
One of the things that I think Rack got wrong and that J2EE, Node.js, etc got right is that the response bodys should be treated as IO objects. Whether the IO object streams, buffers, gzips, etc is up to the IO object. Regardless, the API remains the same.
I hope to eventually push the concept of an IO response up to Rack itself and eliminate this code from Rails.
- thibaut_barrere 14y agoI look forward to your follow-up on sending JSON streams back to the client! It would be a nice thing to gradually add the items to a js, client-rendered list (such as with KnockOut.js).
- masklinn 14y ago> One of the things that I think Rack got wrong and that J2EE, Node.js, etc got right is that the response bodys should be treated as IO objects. Whether the IO object streams, buffers, gzips, etc is up to the IO object. I am rather surprised by this declaration, because Rack response bodies do indeed behave like "IO objects": they respond to `each`. That's the only requirement. Guess what responds to `each`? IO. The problem seems entirely self-inflicted by Rails: if it builds an eager, non-streaming Response object, that means you can't stream with it. But it's got nothing whatsoever to do with Rack. By comparison, Rack's own Response helper object does provide easy streaming access: calls to `Response#write` within the `Response#finish` block are streamed directly. Your code would look roughly like this in e.g. Sinatra (note: entirely untested): response.finish do 100.times { response.write "hello, world\n" } end Alternatively, you could implement your own #each: class Stream def each 100.times { yield "hello, world\n" } end end get('/') { Stream.new } this one is straight from the Sinatra docs[0], and aside from the `get` call it can trivially be converted to a raw Rack application: lambda do |env| [200, {'Content-Type' => 'application/octet-stream'}, Stream.new] end [0] Sinatra also provides a `stream` helper method which does most of the wrapping and tries to make non-streaming-aware middleware play nicely, that can be used in stead of these lower-level calls: http://www.sinatrarb.com/contrib/streaming.html http://www.sinatrarb.com/contrib/streaming.html
- tenderlove 14y agoYup, IO objects use `each` for reading. I want to write to the socket. I am lamenting the hoops I have to jump through in order to make a reader act like a writer. Somehow data must be fed to the `each` function which means probably a Queue and a Thread, or possibly feeding Rack the read end of a pipe. None of these solutions is very appetizing to me. :)
- masklinn 14y ago> I want to write to the socket. That's what you do when you yield to the each's block. It's not like the caller calls each multiple times when it wants new data, he calls `each` once and you feed it data via the block it provides. > Somehow data must be fed to the `each` function Just as it "must be" fed to the `write` in your examples.
- tenderlove 14y ago> > I want to write to the socket. > That's what you do when you yield to the each's block. No IO object in ruby uses the `each` method for writing.
- masklinn 14y agoYou are mistaken and confused. All IO objects in Ruby use `each` for writing out, non-IO objects use something else to write to IO objects. But from an IO object's perspective, that's a read. Rack responses are IO objects, so they use #each to write data out. That's perfectly coherent.
- bascule 14y agoResponding to #each is neither a necessary or sufficient property of Ruby IO objects. You are mistaken and confused. Don't believe me? Let's undefine #each on the metaclass of an IO object and see what happens. Guess what: it still works! https://gist.github.com/3218710 https://gist.github.com/3218710
- bradgessler 14y agoTreating bodies as IO streams is only part of the problem. Today in Rack to stream responses, the rack app returns: [-1, {}, []] Which is interpreted by the app server like Thin, EventMachine as a deferrable response. The problem with this approach is that there's no way to process response headers asynchronously. Consider: class Middleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) # This wouldn't work because rack returns [-1, {}, []] for an async response. if headers['Content-Type'] == 'json' Logger.info "Its JSON!" end status, headers, body end end The logger line would never evaluate because of the `[-1, {}, []]` response. The Goliath web server is the closest thing I've seen in Ruby land that considers these problems (https://github.com/postrank-labs/goliath/wiki/Middleware https://github.com/postrank-labs/goliath/wiki/Middleware). Does anybody know if there's a Rack 2.0 spec in the works that takes care of async request/response cycles?
- codebeaker 14y agoI hope that Rails has the weight to push these kind of changes into Rack. I think that Rack is one of the best things that happened to the Ruby web-application community, and I'm a little sad that Rails is getting a "vendor specific" implementation of what should be a wide reaching feature. As you said at @scotruby though, Rails is in a unique position to direct the development on both sides, front and back end of development. (I believe in Scotland you were referring specifically to pushing backend queue implementors to meet the new Rails queueing API.) I'm looking forward to seeing people building upon this for common use-cases such as streaming reports, CSV exports etc to clients, as well as the more `fun` stuff about live browser reloading (which guard-reload implements very nicely, actually, using a different technique) And, I think it's fair to say that using a queue, and having a queue consumer (again, I think you demo'ed that in Scotland) isn't such a bad solution, at least as a more experienced developer I appreciate the moving parts, and it doesn't distract me too much from solving the problem at hand, and - most importantly it works. That all said, I think this is a valuable change, and I thank you for your time in making it work, and coming up with an interesting test-case. For what it's worth, my last word would be about a post on HN a few days ago, about streaming JPEGs, multi-frame streams often (apparently.) used for IP camera livestreams, that's quite an interesting case which could lead to this controller technique being used to do live image previews without page reloads in Rails with AJAX image upload.