Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Live Streaming in Rails 4.0 (tenderlovemaking.com)
141 points by tenderlove on July 30, 2012 | hide | past | favorite | 38 comments


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.


> 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


I'd say you're missing the point if you think each (i.e. Enumerable-like) behavior solves the problem.

This 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.


Ruby solves the inversion of control with enumerators (not to be confused with Enumerable): http://www.ruby-doc.org/core-1.9.3/Enumerator.html. It's similar to Python's generators and `yield`, letting you write imperative, sequential code that is executed at the behest of a caller. See randomdata's comment (http://news.ycombinator.com/item?id=4314810) above.


There's another way to do this: just write to the goddamn socket.


Yes, but enumerators are (in most cases) implemented with Fibers. So you're going to get hit with at least some performance degradation.


Yup, 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. :)


In sufficiently modern versions of Rails you could write:

    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.


That's what I do, encapsulated in a bunch of methods aggregated in a streaming module. Don't forget to set this if you're using nginx (which my module does).

    headers['X-Accel-Buffering'] = 'no'


Does this solve the problem of Rails' ridiculously high stack? (functions in functions in functions, Rails' stack is daunting...) If you had the ability to write to a buffer, you don't need the stack, just pass the buffer object around.


> 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.


> > 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.


You 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.


Responding 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


Treating 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).

Does anybody know if there's a Rack 2.0 spec in the works that takes care of async request/response cycles?


I 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).


I 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.


I don't want to be a wet blanket here, but does anyone else find this "tender love making" branding all over Aaron Patterson's technical content a bit... distracting?

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 think adapting to people with low tolerance for eccentricity/personality is universally destructive. Ever see one of Aaron's presentations? Every time he startles someone by tenderlovemaking all over their expectations, I consider that a good thing. Most people take life too seriously, and people like Aaron are a rare breed.

I'm so scared of distracting anyone with my personality that I write in black, white, and Helvetica.


Do you have an issue with "tender love making" branding or branding in general? I think a healthy dose of personal branding is ok for me.

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.


lol! Do you have suggestions for improvement? I'm not sure what I'm doing to brand the site. My expertise is in tech, not branding. I'm open to suggestions, but I will not compromise on the color pink (or Ruby-San)! :-)


Don't change a thing, mate.


Agreed, I think it's brilliant and enjoyable to read (unlike many technical writings).


What makes it uncomfortable for you?

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.


I think that why has made us numb to developer quirkiness :)


I didn't even click through to his stuff, initially. The branding resulted in a false positive from my mental spam filter.


I really respect the hard work going in to the evolution of Rails, but it does feel like some new features should be considered as Gems.

The hardest part of managing mature software is keeping it lean.


Streaming looks like a natural evolution to me, to gradually send JSON data back to the client etc for instance.

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).


The lightweight framework Sinatra has had streaming for a some time now, and if it is not too heavy for Sinatra it should not be too heavy for rails. Rails is way larger than Sinatra.


> The lightweight framework Sinatra has had streaming for a some time now

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...


Yeah, I have implemented streaming myself once for a rack app. It was rather simple to implement and has worked stably in production for over two years now.


I find it curios that he does not mention Thin as a webserver which is good for streaming. Thin was perhaps the first ruby webserver which got good support for streaming responses. While Rainbows! and Puma might be better at least Thin should deserve a mention due to its popularity alone. It is way more downloaded than both Rainbows! and Puma together.

Is there some problem with running Thin together with stream in rails 4?


Ah, sorry. I forgot to mention thin. I will update the article. I just mentioned the web servers I've tested with! :)


Good to know. I was worried there could be some bad interaction between rails and the eventmachine. :)


I suspect there will be bad interactions with EventMachine as it doesn't have a flow control model for writes and will just accept an unbounded amount of data into its write buffer. This is a big problem if you have slow consumers.


Rails + SSE + Thin = a dream come true for me. Thank you for the hard work you're putting in!


The one thing I think Rack really messed up is the hard requirement for rewindable input. Try streaming an upload through to a back-end. Can't do it.


Very cool - thanks for the early look at this patch! Is this live streaming practical for high bandwidth I/O - say live video streaming?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: