Live Streaming in Rails 4.0
tenderlovemaking.com
tenderlovemaking.com
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.
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.htmlThis still forces an inversion of control as far as who is in control of writes, i.e. the IO operation drives the generation of data to write, and not the other way around. I find this incredibly annoying.
FWIW I have implemented both patterns in my own web server Reel. I definitely think It's Just A Socket (duck type) is far and away the way to go.
def index
self.response_body = Enumerator.new do |socket|
100.times do
socket << "hello world"
end
end
end
Your API is a little less verbose, I'll give you that, but I don't see why you need Queues and Threads unless the problem actually calls for those things. It still looks like you are writing to a socket either way. headers['X-Accel-Buffering'] = 'no'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.
> That's what you do when you yield to the each's block.
No IO object in ruby uses the `each` method for writing.
Rack responses are IO objects, so they use #each to write data out. That's perfectly coherent.
Don't believe me? Let's undefine #each on the metaclass of an IO object and see what happens. Guess what: it still works!
[-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).
Does anybody know if there's a Rack 2.0 spec in the works that takes care of async request/response cycles?
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.
Personally I do. That said, Aaron does amazing work and he should be able to brand it however he likes. I'm also very grateful for what he gives to the community.
So, am I just an old fuddy-duddy, or what? I mean, personally, I actually enjoy it, despite the distraction, but I'm surprised that this seems to be such a non-issue to everyone else.
I'm so scared of distracting anyone with my personality that I write in black, white, and Helvetica.
With that said, the developer community is much much tolerating place than other communities. I mean, think about the rude behavior from some developers and how far we go in tolerating it because of our "They built X, so they can do unrelated Y however they want". Compared to that, some love spreading is actually very nice. :)
Edit: the sentence "They built X, so they can do unrelated Y however they want" is confusing, even to me. What I meant is, we tolerate some otherwise really inconvenient stuff, like Crockford calling all JSHint users "stupid people". You get the point.
I love this kind of quirkiness. I kind of miss having handles we all took seriously that didn't resolve directly into real names, said Phill to Rhett.
The hardest part of managing mature software is keeping it lean.
I don't think this is something that could be done easily as a gem (at least not without a serious dose of unstable monkey-patching).
Rack itself has response streaming support[0]. As a result, even before Sinatra 1.3 and the `stream` helper method you could stream responses in Sinatra.
[0] since pretty much forever ago: https://github.com/rack/rack/commit/53e299724b9173efe50e9afc...
Is there some problem with running Thin together with stream in rails 4?