Accelerate Your Rails Site with Automatic Generation-Based Action Caching Rod Cope, CTO and Founder OpenLogic, Inc.
Goal Built-in caching is hard: Learn how to automate it to make the pain go away 2
Agenda Introduction Built-in caching Automatic generation-based caching Bonus material Conclusion & recommendations 3
Introduction Rod Cope CTO & Founder of OpenLogic 25 years of software development experience IBM Global Services, Anthem, Ericsson, many more OpenLogic, Inc. Governance, SLA support, security updates, and indemnification for over 500 Open Source packages Dozens of Fortune 500 customers OSS Census (osscensus.org) Global, community effort to catalog use of open source 4
No Spoon Feeding 5
Don t Panic scotduke.com 6
Reasons for Caching Lots of hits, speed is king Google 7
8
Reasons for Caching Lots of hits, speed is king Google Complex hits, richness is king OpenLogic s OLEX 9
10
Rails Caching Background Page caching Uberfast, little control Highly dynamic site? Move on. Action caching Fast, lots of control Expiration hell Fragment caching Expiration hell 11
Problems with Rails Caching Caching is easy to implement Site s borked again - turn off caching. Hard to clear (the right) caches when you re mixing and matching different types, styles, developers, plugins, etc. 12
Automatic Generation-Based Caching Generation-based Partition the cache so that every state change increments the generation Automatic Any action that could possibly change state bumps the count Let memcached overwrite old generations Very conservative No domain knowledge Don t cache errors, AJAX, redirects, flash notices, etc. 13
Overview Non-cached Cached Rails plumbing Rails plumbing Your code Cache helper Database Render View memcached 14
Cache = Hash Key Value /packages/1 <html><body>rails</body></html> /packages/2 <html><body>ruby</body></html>
Generational Cache Key Value /gen/1/packages/1 <html>rails</html> /gen/1/packages/2 <html>ruby</html>
Generational Cache Key Value /gen/1/packages/1 <html>rails</html> /gen/1/packages/2 <html>ruby</html> /gen/2/packages/1 <html>ruby on Rails</html>
memcached is your friend Very fast Don t worry about removing old entries memcached will automatically drop the oldest keys when it runs low on memory Run it on your web servers if you have the RAM 18
Cache Helper No cache-related code sprinkled throughout every model, view, and/or controller Use a global around filter Automatically increment (scoped) generation count upon POST, PUT, or DELETE Handle event recording and playback (optional) 19
Cache Helper Code (in around filter) key = make_auto_cache_key(request.request_uri) output = Rails.cache.read(key) if output render :text => output return else yield unless response.redirected_to flash[:notice] Rails.cache.write(key, response.body) end end 20
Cache Key def make_auto_cache_key(key) ol_gen = Rails.cache.fetch(OL_GEN) { "1" } "olex/#{ol_gen}" end 21
Clear the Cache POST, PUT, or DELETE clears the cache def maybe_clear_auto_cache return if request.get? gen = Rails.cache.fetch(OL_GEN) { "1" } Rails.cache.write(OL_GEN, (gen.to_i + 1).to_s) end 22
Controller Customization Some controllers may need special cache control (e.g., user-specific cache, disable cache for certain methods) olex_auto_cache_is_specific_to_user :only => :show Note: the cache may still need to be cleared when skipping cache usage! skip_filter :olex_auto_cache after_filter :maybe_clear_olex_auto_cache 23
Cache Partitioning Make it impossible for users of different corporations and permission levels to see each other s stuff, access the same cache, clear somebody else s cache, etc. Make sure any global changes clear all caches Use a cache hierarchy /olex/<gen #>/corp/<corp #>/<corp gen #>/roles/<role #s>/url MD5 the key (memcached has a 250 char limit) Extra credit: Use the key as an etag 24
Partitioned Cache Global generation /olex/13/corp/72/2/roles/2,7,19/packages/2 <html> Rails (unsupported) </html>
Partitioned Cache Corporate account ID /olex/13/corp/72/2/roles/2,7,19/packages/2 <html> Rails (unsupported) </html>
Partitioned Cache Corporation #72 s generation /olex/13/corp/72/2/roles/2,7,19/packages/2 <html> Rails (unsupported) </html>
Partitioned Cache Current user s role IDs /olex/13/corp/72/2/roles/2,7,19/packages/2 <html> Rails (unsupported) </html>
Partitioned Cache URL /olex/13/corp/72/2/roles/2,7,19/packages/2 <html> Rails (unsupported) </html>
Partitioned Cache /olex/13/corp/72/2/roles/2,7,19/packages/2 /olex/13/corp/72/2/roles/2,7,19,22/packages/2 /olex/14/corp/72/2/roles/2,7,19/packages/2 /olex/14/guest/packages/2 <html> Rails (unsupported) </html> <html> Rails (manager secret!) </html> <html> Rails (supported) </html> <html>rails</html>
Cache Key def make_auto_cache_key(key, user = nil) ol_gen = Rails.cache.fetch(OL_GEN) { "1" } ol_prefix = "olex/#{ol_gen}" if user.nil? && self.respond_to?("current_user") user = current_user end ca = get_corporate_account(user)... 31
Cache Key... if user.is_corp_user? corp_gen = Rails.cache.fetch(corp_gen_key(ca)){"1"} final_key = make_corp_key(user, ca, key, corp_gen) else final_key = "guest#{key}" end 32
Corporate Cache Key def make_corp_key(user, ca, key, corp_gen) roles = role_string(user) "/corp/#{ca.id}/#{corp_gen}/roles/#{roles}#{key}" end def role_string(user) Rails.cache.fetch("roles/#{user.id}") do user.role_string end end 33
Benefits Never have to explicitly expire anything Can t get an expired page Generation numbers also stored in memcached Cache hierarchy is like a directory structure Easy to grok Can t run out of disk space! 34
Does it really work? In production for over a year - a few minor issues See Gotchas Huge performance improvement for us, almost no developer pain e.g., needed cookie-based Welcome Joe! Great for passive caching Write caching code once, enjoy it forever Lots of room for more aggressive caching 35
Gotchas Easy to forget to make AJAX calls use REST method:'get','post', 'put', or 'delete' Test with and without caching, and test the caching itself! Caching doesn t help the first hit Mitigate by pre-caching when feasible 36
Recommendations The safety first caching implementation - use it It s still action caching, so don t expect page caching performance Measure the result - not all pages will benefit and there is some overhead 37
Bonus Asynchronous pre-caching Event recording and playback 38
Async pre-caching pages at logon Use Workling/Starling for async When user logs on, make async call Async call runs through list of URL s and hits them on behalf of the currently logged on user Need secret back-door logon in application.rb Use big scary session key in environment.rb 39
Event Recording Q: What if you want to record the fact that something happened even though the page was automatically cached and served? A: Watch it while being cached, record what happens in memcached, play it back later when serving cached version 40
Summary Rails built-in caching takes a lot of on-going work Easy to screw it up Automatic caching can be written once and enjoyed forever Much harder to screw up Still possible to use more aggressive techniques selectively 41
Any questions for Rod? 42
Resources memcached: http://www.danga.com/memcached/ Workling: http://github.com/purzelrakete/workling/tree/ master Starling: http://rubyforge.org/projects/starling/ OLEX: http://olex.openlogic.com 43
Credits Unless otherwise indicated, photos were licensed from BigStockPhoto.com 44
OpenLogic is hiring! Rails guru? Live near Denver, Colorado? Love Open Source? Come see me! 45