Realtime Rails Four2012.rubyworld-conf.org/files/slides/rwc2012_A-6.pdfAllow concurrency (enable)...

Preview:

Citation preview

RealtimeRails Four

Rails FourFor you and for me!

Aaron Patterson@tenderlove

HI!! :-)

AT&T, AT&T logo and all AT&T related marks are trademarks of AT&T Intellectual Property and/or AT&T affiliated companies.

Rails FourFor you and me!!!

The Web!

Ruby

Rails

Web

Thread SafetyIn Rails

Delete config.threadsafe!

Why delete?

Always bethread safe!

Always bethread safe!

MyOpinion!

Simplify

Is it safe to remove?

What did it do?

threadsafe!

❤ Preload frameworks (enable)❤ Cache classes (enable)❤ Dependency loading (disable)❤ Allow concurrency (enable)

Loading Code isn’t Thread-safe*

Preload Frameworks

ENABLED

Cache classes

ENABLED

Dependency Loading

DISABLED

Allow Concurrency

ENABLED

Get Lock

Read from Socket

Process Stuff

Write to Socket

Release Lock

Rack::Lock

Thread 2 Get Lock

Read from Socket

Process Stuff

Write to Socket

Release Lock

Thread 1

Rack::Lock

Thread 2 Get Lock

Read from Socket

Process Stuff

Write to Socket

Release Lock

Rack::Lock

Thread 2

Get Lock

Read from Socket

Process Stuff

Write to Socket

Release Lock

Rack::Lock

Rack::Lock with Processes

Rack::Lock withThreads

Best Case:Extra Overhead

Worst Case:1 req at a time

IMPACT

Boot time increases (in prod)

Fewer middleware

Multi-Proc serversstay the same

Threaded serversJust Work™

Bug Fixes

100% Caching

Locking ||=

def some_method @value ||= some_calculationend

check then act

Is nil?

Calculate

Return

Set

check then act

Is nil?

Calculate

Return

Thread 1

Set

check then act

Is nil?

Calculate

Return

Thread 1

Set

Thread 2

check then act

Is nil?

Calculate

Return

Thread 1 Set

Thread 2

check then act

Is nil?

Calculate

Return

Set

Fix #1 (eager init)class Foo class << self def hard_calculation @calc end end

@calc = fib(34)end

p Foo.hard_calculation

Fix #2 (locking)class Foo @lock = Mutex.new

class << self def hard_calculation @lock.synchronize do @calc ||= fib(34) end end endend

p Foo.hard_calculation

Fix #2 (locking)class Foo @lock = Mutex.new

class << self def hard_calculation @lock.synchronize do @calc ||= fib(34) end end endend

p Foo.hard_calculation

Fix #2 (locking)class Foo @lock = Mutex.new

class << self def hard_calculation @lock.synchronize do @calc ||= fib(34) end end endend

p Foo.hard_calculation

Move methods to instances

Move to objectclass Foo def initialize @calc = fib(34) end

def hard_calculation @calc endend

Foo.new.hard_calculation

Lazy Objectclass Foo include Mutex_m

def hard_calculation synchronize do @calc ||= fib(34) end endend

Foo.new.hard_calculation

Maintain APIclass Foo include Mutex_m

def hard_calculation synchronize do @calc ||= fib(34) end end

Instance = new

def self.hard_calculation Instance.hard_calculation endend

Foo.hard_calculation

Hash.new { }class Foo def initialize @cache = Hash.new { |h,k| h[k] = [] } end

def some_value(key) @cache[key] endend

Fix #1 (lock)class Foo include Mutex_m

def initialize super

@cache = Hash.new { |h,k| h[k] = [] } end

def some_value(key) synchronize { @cache[key] } endend

Fix #2 thread_safe

https://github.com/headius/thread_safe

What about the App Level?

Thread SafetyIn Web Apps

Avoid shared data

Most people don’t type “Thread.new”

Look for things that are global.

Global Variables

$so_global = {}$so_global[:foo] ||= “bar”

Constants

SET_TWICE = 10SET_TWICE = 10

ALSO_GLOBAL = {}ALSO_GLOBAL[:foo] ||= “bar”

Warning

No Warning

Class methods

class MyModel def self.some_cache @foo ||= fib(34) endend

Avoid global data

Add locks

Streaming

Template Rendering Today

Templates Results are Buffered

Clients are blocked while Rails works

The entire page must fit in memory

Rack Encourages Buffering

class MyApplication def call(env) [200, {}, [‘my page’]] endend

We can do I/O and CPU in parallel.

So why buffer?

ActionController::Live

Exampleclass BrowserController < ApplicationController include ActionController::Live

def index 100.times do response.stream.write "hello!\n" end response.stream.close endend

Exampleclass BrowserController < ApplicationController include ActionController::Live

def index 100.times do response.stream.write "hello!\n" end response.stream.close endend

Mix in

Stream

response.stream acts likean I/O object

EverythingIs a File

How does it work?

Our APIdef index response.status = 200 response.headers[‘X-Whatever’] = ‘<3’ response.stream.write ‘hello’ response.stream.write ‘ world’ response.stream.closeend

Rack API

def call(env) return [200, {‘X-Whatever’ => ‘<3’}, [‘hello world’]]end

Wrapped Requestclass Response attr_accessor :status attr_reader :headers, :stream

def initialize @status = 200 @headers = {} @stream = StringIO.new endend

def call(env) res = Response.new controller.response = res controller.index [res.status, res.headers, res.stream]end

Threaded action

def call(env) res = Response.new controller.response = res Thread.new { controller.index } [res.status, res.headers, res.stream]end

Block until writedef call(env) res = Response.new controller.response = res Thread.new { controller.index } res.stream.await [res.status, res.headers, res.stream]end

Block until writedef call(env) res = Response.new controller.response = res Thread.new { controller.index } res.stream.await [res.status, res.headers, res.stream]end

Block

Blocking Bufferclass Buffer def initialize @latch = Latch.new @buffer = Queue.new end

def await # wait for write @latch.await end

def write(str) @latch.release @buffer << str endend

Blocking Bufferclass Buffer def initialize @latch = Latch.new @buffer = Queue.new end

def await # wait for write @latch.await end

def write(str) @latch.release @buffer << str endend

`call` blocks here

Blocking Bufferclass Buffer def initialize @latch = Latch.new @buffer = Queue.new end

def await # wait for write @latch.await end

def write(str) @latch.release @buffer << str endend

`write` unblocks

What can we do?

Rails Internals

Streaming ERB

View Source

# encoding: utf-8

require 'erb'

doc = ERB.new '<%= hello %> world'puts doc.src

Source

#coding:UTF-8_erbout = ''; _erbout.concat(( hello ).to_s); _erbout.concat " world"; _erbout.force_encoding(__ENCODING__)

Control Outputclass MyERB < ERB def set_eoutvar(compiler, eoutvar = '_erbout') compiler.put_cmd = "#{eoutvar}.write" compiler.insert_cmd = "#{eoutvar}.write" compiler.pre_cmd = [] compiler.post_cmd = [] endend

doc = MyERB.new '<%= hello %> world', nil, nil, '$stdout'puts doc.src

Source

#coding:UTF-8$stdout.write(( hello ).to_s); $stdout.write " world"

$ ruby test.rbhello world$

Web Apps

Infinite Streams

Server Sent Events

SSE ResponseHTTP/1.1 200 OKX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=blockX-Content-Type-Options: nosniffContent-Type: text/event-streamTransfer-Encoding: chunked

event: pingdata: {"ping":"2012-10-06T21:44:41-07:00"}

event: reloaddata: {"changed":["/Users/aaron/git/lolwut/app/views/users/"]}

SSE ResponseHTTP/1.1 200 OKX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=blockX-Content-Type-Options: nosniffContent-Type: text/event-streamTransfer-Encoding: chunked

event: pingdata: {"ping":"2012-10-06T21:44:41-07:00"}

event: reloaddata: {"changed":["/Users/aaron/git/lolwut/app/views/users/"]}

SSE ResponseHTTP/1.1 200 OKX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=blockX-Content-Type-Options: nosniffContent-Type: text/event-streamTransfer-Encoding: chunked

event: pingdata: {"ping":"2012-10-06T21:44:41-07:00"}

event: reloaddata: {"changed":["/Users/aaron/git/lolwut/app/views/users/"]}

SSE ResponseHTTP/1.1 200 OKX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=blockX-Content-Type-Options: nosniffContent-Type: text/event-streamTransfer-Encoding: chunked

event: pingdata: {"ping":"2012-10-06T21:44:41-07:00"}

event: reloaddata: {"changed":["/Users/aaron/git/lolwut/app/views/users/"]}

Client SidejQuery(document).ready(function() { setTimeout(function() { var source = new EventSource('/control');

// if we get a reload command, reload the page source.addEventListener('reload', function(e) { window.location.reload(); }); }, 1);});

Client SidejQuery(document).ready(function() { setTimeout(function() { var source = new EventSource('/control');

// if we get a reload command, reload the page source.addEventListener('reload', function(e) { window.location.reload(); }); }, 1);});

Client SidejQuery(document).ready(function() { setTimeout(function() { var source = new EventSource('/control');

// if we get a reload command, reload the page source.addEventListener('reload', function(e) { window.location.reload(); }); }, 1);});

Client SidejQuery(document).ready(function() { setTimeout(function() { var source = new EventSource('/control');

// if we get a reload command, reload the page source.addEventListener('reload', function(e) { window.location.reload(); }); }, 1);});

Real-Time BrowserCommunication

Puma

BrowserFS-Events

FS Events

Puma

BrowserFS-Events

FS Events

Puma

BrowserFS-Events

FS Events

Puma

BrowserFS-Events

FS Events

Puma

BrowserConsole

DRB

DB Events

Puma

BrowserConsole

DRB

DB Events

Socket

Other Input Sources

❤ Embedded systems (sausage box)❤ Telephony (twilio)❤ Other users (chat systems)

Thread Safety

P72NStreaming

Cores are increasing.

We need to utilize the entire machine

High latency clients are increasing.

Patience is decreasing.

Lie.Using cached data

Cheat.Updating partial parts of the page

Steal.Move computations to client side via JS

Lie.Cheat.Steal.

Be GoodEngineers

THANK YOU

Questions?

Recommended