forumwarz and rjs: a love/hate affair

Post on 13-Jul-2015

1.199 Views

Category:

Business

1 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Forumwarz and RJSA love/hate affair

An internet-based gameabout the internet

LOL WHUT?

The Internet is a wonderful, magical place!

But it’s also terrible.

Very, very terrible.

John Gabriel’s Greater Internet Fuckwad Theory

Exhibit A

Have you heard of it?• One of the most popular forums in the

world

• Almost all its users are anonymous

• Unfortunately, it’s hilarious.

The anonymity of the Internet means you can

be anything...

You can be another ethnicity!

You can be another species!

You can even be beautiful!

(Even if you’re not.)

Are you going to explainwhat forumwarz is, orjust show us hackneyedimage macros all night?

Role-Play an Internet User!

Camwhore Emo Kid Troll

• Each player class features unique abilities and attacks

• Many different ways to play

• A detailed story line ties it all together

Role-Play an Internet User!

An interface heavy in RJS

(please endure this short demo)

Let’s get technical

Forumwarz Technology

Some of our stats• ~30,000 user accounts since we launched

one month ago

• 2 million dynamic requests per day (25-55 req/s)

• Static requests (images, stylesheets, js) are infrequent, since they’re set to expire in the far future

• About 25GB of bandwidth per day

Deployment• Single server: 3.0Ghz quad-core Xeon, 3GB

of RAM

• Nginx proxy to pack of 16 evented mongrels

• Modest-sized memcached daemon

Thank you AJAX!

• One reason we can handle so many requests off a single server is because they’re tiny

• We try to let the request get in and out as quickly as possible

• RJS makes writing Javascript ridiculously simple

why we rjs

A simple exampleExample View

Example Controller

#battle_log There's a large monster in front of you. = link_to_remote "Attack Monster!", :action => 'attack'

def attack @monster = Monster.find(session[:current_monster_id]) @player = Player.find(session[:current_player_id]) @player.attack(@monster) update_page_tag do |page| page.insert_html :top, 'battle_log', :partial => 'attack_result' end end

Pretty cool eh?

• Without writing a line of javascript we’ve made a controller respond to an AJAX request

• It’s fast. No need to request a full page for such a small update

• It works great*

* but it can haunt you

Problem #1: Double Clicks

• Often, people will click twice (or more!) in rapid succession

• Your server gets two requests

• If you’re lucky they will occur serially

A Solution?

var ClickRegistry = { clicks : $H(), can_click_on : function(click_id) { return (this.clicks.get(click_id) == null) }, clicked_on : function(click_id) { this.clicks.set(click_id, true) }, done_call : function(click_id) { this.clicks.unset(click_id) }}

• Use some javascript to prevent multiple clicks on the client side

A Solution?

def link_once_remote(name, options = {}, html_options = {}) click_id = html_options[:id] || Useful.unique_id options[:condition] = "ClickRegistry.can_click_on('#{click_id}')" prev_before = options[:before] options[:before] = "ClickRegistry.clicked_on('#{click_id}')" options[:before] << "; #{prev_before}" if prev_before prev_complete = options[:complete] options[:complete] = "ClickRegistry.done_call('#{click_id}')" options[:complete] << "; #{prev_complete}" if prev_complete link_to_remote(name, options, html_options) end

• Add a helper, link_once_remote

Our Example: v.2Example View

#battle_log There's a large monster in front of you. = link_once_remote "Attack Monster!", :action => 'attack'

Surprise!

It doesn’t work!

Why not?• Proxies or download “accelerators”

• Browser add-ons might disagree with the javascript

Also, it’s client validated!

• Let’s face it: You can never, ever trust client validated data

• Even if the Javascript worked perfectly, people would create greasemonkey scripts or bots to exploit it

• Our users have already been doing this :(

Server Side Validation• It’s the Rails way

• If it fails, we can choose how to deal with the invalid request

• Sometimes it makes sense to just ignore a request

• Other times you might want to alert the user

Problem #2: Validations• ActiveRecord validations can break during

concurrency

• In particular, the validates_uniqueness_of validation

The Uniqueness Life-Cycle

select * from battle_turns where turn = 1 and user_id = 1;

if no rows returned

insert into battle_turns (...)

else

return errors collection

Transactions don’t help

• With default isolation levels, reads aren’t locked

• Assuming you have indexed the columns in your database you will get a DB error

• So much for reporting errors to the user nicely!

A solution?• Could monkey patch ActiveRecord to lock

the tables

• That’s fine if you don’t mind slowing your database to a crawl and a ridiculous amount of deadlocks

A different solution?• You can rescue the DB error, and check to

see if it’s a unique constraint that’s failing

• This is what we did. It works, but it ties you to a particular database

def save_with_catching_duplicates(*args) begin return save_without_catching_duplicates(*args) rescue ActiveRecord::StatementInvalid => error if error.to_s.include?("Mysql::Error: Duplicate entry") # Do what you want with the error. In our case we raise a # custom exception that we catch and deal with how we want end endend

alias_method_chain :save, :catching_duplicates

Problem #3: Animation• script.aculo.us has some awesome

animation effects, and we use them often.

• RJS gives you the great visual_effect helper method to do this:page.visual_effect :fade, 'toolbar'page.visual_effect :shake, 'score'

When order matters• Often you’ll want to perform animation in

order

• RJS executes visual effects in parallel

• There are two ways around this

Effect Queues• You can queue together visual effects by

assigning a name to a visual effect and a position in the queue.

• Works great when all you are doing is animating

• Does not work when you want to call custom Javascript at any point in the queue

• Unfortunately we do this, in particular to deal with our toolbar

page.delaypage.visual_effect :fade, 'toolbar', :duration => 1.5page.delay(1.5) do page.call 'Toolbar.maintenance' page.visual_effect :shake, 'score'end

• Executes a block after a delay

• If paired with :duration, you can have the block execute after a certain amount of time

It’s me again!

This also doesn’t work!

Durations aren’t guaranteed• Your timing is at the whim of your client’s

computer

• Your effects can step on each other, preventing the animation from completing!

• They will email you complaining that your app has “locked up”

A solution?def visual_effect_with_callback_generation(name, id = false, options = {}) options.each do |key,value| if value.is_a?(Proc) js = update_page(&value) options[key] = "function() { #{js} }" end end visual_effect_without_callback_generation(name, id, options)end

alias_method_chain :visual_effect, :callback_generation

Thanks to skidooer on the SA forums for this idea!

And then, in RJSpage.visual_effect :fade, 'toolbar', :duration => 1.5, :afterFinish => lambda do |step2| step2.call 'Toolbar.maintenance' step2.visual_effect :shake, 'score'end

• The lambda only gets executed after the visual effect has finished

• Doesn’t matter if the computer takes longer than 1.5s

in Conclusion

Nobody’s Perfect!

Nobody’s Perfect!

• We love RJS despite its flaws

• It really does make your life easier, most of these issues would never be a problem in a low traffic app or admin interface

• The solutions we came up with are easy to implement

this presentation was brought to you by

this presentation was brought to you by

Any questions?

top related