Post on 13-Jul-2015
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?