18. Role-Play an Internet User!
• Each player class features unique abilities
and attacks
• Many different ways to play
• A detailed story line ties it all together
22. 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
23. Deployment
• Single server: 3.0Ghz quad-core Xeon, 3GB
of RAM
• Nginx proxy to pack of 16 evented
mongrels
• Modest-sized memcached daemon
24. 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
26. A simple example
Example View
#battle_log
There's a large monster in front of you.
= link_to_remote quot;Attack Monster!quot;, :action => 'attack'
Example Controller
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
27. 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*
29. 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
30. A Solution?
• Use some javascript to prevent multiple
clicks on the client side
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)
}
}
32. Our Example: v.2
Example View
#battle_log
There's a large monster in front of you.
= link_once_remote quot;Attack Monster!quot;, :action => 'attack'
34. Why not?
• Proxies or download “accelerators”
• Browser add-ons might disagree with the
javascript
35. 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 :(
36. 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
38. 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
39. 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!
40. 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
41. 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?(quot;Mysql::Error: Duplicate entryquot;)
# 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
end
end
alias_method_chain :save, :catching_duplicates
42. 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'
43. When order matters
• Often you’ll want to perform animation in
order
• RJS executes visual effects in parallel
• There are two ways around this
44. 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
45. page.delay
page.visual_effect :fade, 'toolbar', :duration => 1.5
page.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
47. 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”
48. 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] = quot;function() { #{js} }quot;
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!
49. And then, in RJS
page.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
52. 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