Real-Time Rails


Brian Cardarella

@bcardarella




http://www.slid.es/bcardarella/real-time-rails


Who?


Owner of @DockYard


Co-Organizer for Boston OpenHack
Co-Organizer for Boston Ember.js
Organizer for Ember.js DC



Wicked Good Ruby


@wickedgoodruby
http://wickedgoodruby.com

Oct 12 - 13




What's the point?






Real-time is just one of the areas where Node.js has outpaced Rails






Rails was the innovator and is now playing catch-up






Can Rails Scale?






Can Rails Scale?

(its features)

Sockets


TCP Sockets


http://workingwithtcpsockets.com

by Jesse Storimer

TCP Sockets

Basics


  1. Data is broken into frames
  2. Frames are broken into packets
  3. Packets travel through the tubes
  4. Packets are constructed into frames
  5. Data is read from frames

Concurrency





  • Web Servers
  • Threads
  • Pub/Sub
  • Thread safety

Web Servers





Rails


Rails 4 is now fully concurrent, there is no full-stack lock on a request. If you use a concurrent server like Puma you can handle many requests at a time with a single process.


- Nick Gauthier

ngauthier.com/2013/03/websockets-in-rails-4.html

Threads




  • You will have to learn how
    to write threaded code in Ruby
  • Being aware thread safety issues in
    the code you write

Threads - 101



thread = Thread.new do
  100.times do
    puts 'Here!'
  end
end
puts 'Over here too!'


Simple race condition example

More Complex



  • Several "rooms"
  • Each room can only hold 2 visitors
  • Visitors are assigned to rooms as they join
  • When a room fills the next visitor
    is assigned to the next available room

Queues




  • Mutex#synchronize
  • External queue (Redis lists)


  • Push to the queue
  • Worker pops and run job
  • Ensures execution order

Pub/Sub



Will be your best friend

  • Postgres Notify/Listen
  • Redis Publish/Subscribe
  • ZeroMQ

Rails





  • Event Machine / Celluloid
  • Faye / Pusher.com

Rails


Long polling

poll = function() {
  $.ajax({
    url: '/chat.json',
    data: { last_time: getLastTime() }
  }).done(function(data) {
    // handle data
    setTimeout(poll, 1000);
  });
}

Rails 4


ActionController::Live



  • Proxy object for the socket
  • Stream response

Rails 4


Long Polling

class ChatController < ActionController::Base
  def index
    render json: { chat: Chat.history_from(params[:last_time]) }
  end
end

Rails 4



ActionController::Live

class ChatController < ActionController::Base
  include ActionController::Live
  
  def index
    Thread.new do
      ChatStreamer.run(response.stream)
    end
  end
end

Rails 4


The stream object is meant to quack like an IO object

class MyController < ActionController::Base
  include ActionController::Live
  
  def index
    100.times {
      response.stream.write "hello world\n"
    }
    response.stream.close
  end
end
- Aaron Patterson
tenderlovemaking.com/2012/07/30/is-it-live.html

Server Sent Events


  • One way streaming communication
  • Part of HTML5 Spec
def index
  100.times {
    response.headers['Content-Type'] = 'text/event-stream'
    response.stream.write "event: time\n"
    data = JSON.dump({ time: Time.now })
    response.stream.write "#data: {data}\n\n"
    sleep 1
  }
  response.stream.close
end
var source = new EventSource('/browser');
source.addEventListener('time', function(event) {
  // handle event
});

Rack.Hijack


New with Rack 1.5


env['rack.hijack?']
=> true

env['rack.hijack'].call
=> <TCPSocket:XXXX>

env['rack.hijack_io']
=> <TCPSocket:XXXX>

Rack.Hijack




Full Hijack

Partial Hijack

Rack.Hijack




Full Hijack

Partial Hijack

Rack.Hijack


  1. Request is made to the server
  2. TCP Socket is hijacked
  3. Socket is put into a thread
  4. Request completes as normal
  5. Thread is still alive and able to
    stream back to the client
  6. Up to the web server to allocate
    new sockets or watch hijacked
    socket and wait until free

Websockets



  • Perfect for rack.hijack
  • Two way communication
  • Websocket gem


gem 'websocket', github: 'imanel/websocket-ruby'
gem 'websocket-native'

Websockets


class ChatController < ActionController::Base
  def index
    socket = env['rack.hijack'].call
    # Sending the header
    handshake = WebSocket::Handshake::Server.new
    handshake.from_rack env
    socket.write handshake.to_s
    
    # Reading a frame
    buffer = WebSocket::Frame::Incoming::Server.new({
        version: handshake.version})
    raw_data = socket.recvfrom(2000).first
    buffer << raw_data
    while frame = buffer.next do
      // handle frame.data
    end
    
    # Writing a frame
    msg = "Hello"
    frame = WebSocket::Frame::Outgoing::Server.new({
        version: handshake.version, type: 'text', data: msg})
    socket.write frame.to_s
  end
end

WEbsockets



  1. Construct the header
  2. Write the header to the socket
  3. Write data to the socket
  4. Read data from the socket
  5. Close the socket

Websockets



ws = new WebSocket('ws://server:port/chat')
ws.onopen = function() {
  // do something upon connection
};
ws.onmessage = function(event) {
  // handle event.data
};
ws.send(data);
ws.onclose = function() {
  // teardown
}




Demos


http://railsconf2013.dockyard.com

Click on 'Chat'

A Wild Pattern Appears!




  1. Client captures events
  2. Event data is sent to the server
  3. Event data is normalized
  4. Event data is broadcast to all connections

TubeSock


Websockets + rack.hijack
github.com/ngauthier/tubesock

hijack do |tubesock|
  tubesock.onopen do
    tubesock.send_data message: "Hello, friend"
  end

  tubesock.onmessage do |data|
    tubesock.send_data message: "You said: #{data[:message]}"
  end
end

Challenges





  • No Heroku support
  • Nginx fights websockets

The future?




  • Integrate rack.hijack into ActionController::Live
  • Rails is (hopefully) moving towards the client
  • Frameworks like Ember (and others) are
    growing and need a strong backend
    solution

Thank you!



Work with DockYard
Hire DockYard

contact@dockyard.com
Made with Slides.com