Browse files

Uploading first version (0.8) with local per request cache and single…

… connection for cache and session stores.
  • Loading branch information...
1 parent abae10d commit e7d83f10eddb84839d2619c28dfe826dbb926e9e @terrcin committed Nov 9, 2008
Showing with 298 additions and 0 deletions.
  1. +21 −0 MIT-LICENSE
  2. +96 −0 README
  3. +7 −0 change.log
  4. +4 −0 init.rb
  5. +17 −0 lib/spandex_dispatcher_callback.rb
  6. +153 −0 lib/spandex_mem_cache_store.rb
View
21 MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2008 Nahum Wild
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
View
96 README
@@ -0,0 +1,96 @@
+Spandex MemCache Store
+======================
+
+Description:
+
+A enhanced version of and replacement for the MemCacheStore shipping with rails.
+
+
+Features:
+
+* Local cache used to buffer duplicate gets per request
+
+Standard caching techniques involve reading from the cache within the action to see if the view
+is cached, if not then execute expensive code as the view will need to be rendered.
+
+There is a rare situation on websites with good levels of traffic where the cache is populated
+when the action checks, but before the view is executed a separate request expires/deletes
+that cache entry resulting in the view being rendered rather than pulled from the cache. In
+this case the action hasn't does what it's needed todo for the view, so lots of weird errors
+about instance vars not being nil etc... appear. This is a very frustrating problem to have
+once you have figured out whats actually causing it.
+
+SpandexMemCacheStore caches the result from the action's cache read and returns that to the
+view without going to memcache a second time. This also occurs for session reads, Rails does
+a minimum of two reads per request, this local caching cuts that down to one.
+
+For a dynamic web 2.0 site which has several layers of fragment caching this can halve the
+number of reads from memcache per request with a populated cache.
+
+The SpandexMemCacheStore's local cache is cleared before each request is executed.
+
+* Easy configuration and single connection to memcache per app
+
+IMO the rails session MemCacheStore is broken, if you simply specify :mem_cache_store it will
+only talk to a memcache server running on localhost at the default port of 11211. To get it to
+work properly you have to duck punch the ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS
+constant like this:
+
+CACHE = MemCache.new('192.168.0.34', {:namespace => "mywebsite-#{RAILS_ENV}"})
+ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.merge!({'cache' => CACHE})
+
+To stop this from happening SpandexMemcacheStore re-uses the connection to memcache established
+through with the caching session store.
+
+
+Installation:
+
+Install the plugin under vendor/plugins and then in environment.rb within the Initializer area
+place the following:
+
+config.action_controller.session_store = :spandex_mem_cache_store
+config.cache_store = :spandex_mem_cache_store, '127.0.0.1', {:namespace => "mywebsite-#{RAILS_ENV}"}
+
+
+Usage:
+
+Nothing new or special yet. Just drop it in as above and it'll instantly start do the things
+described above.
+
+
+Requirements:
+
+Rails >= 2.1
+
+
+Recommended Plugins:
+
+* XML Cache
+http://code.google.com/p/xmlcache/
+
+
+Future Plans:
+
+* Make this a Gem
+* Conditional caching
+* Multi-Get
+* Dynamic key helpers
+* Make the code more awesome
+
+Latest details and dicussion can be found at a dedicated page on my blog:
+
+www.motionstandingstill.com/spandex-mem-cache-store
+
+
+Credit:
+
+The local caching concept I first encountered when using the ExtendedFragmentCache plugin
+(http://rubyforge.org/projects/zventstools/) back in 2006. I then re-factored that for
+Ponoko.com when I was working there. This plugin is a completely new implementation.
+
+
+Contact Details:
+
+Nahum Wild
+email: nahum.wild@gmail.com
+blog: www.motionstandingstill.com
View
7 change.log
@@ -0,0 +1,7 @@
+Version 0.8
+-----------
+
+* Initial Version
+* Local per request cache
+* Session store re-uses the cache store
+
View
4 init.rb
@@ -0,0 +1,4 @@
+require 'spandex_mem_cache_store'
+require 'spandex_dispatcher_callback'
+
+Dispatcher.send :include, SpandexDispatcherCallback
View
17 lib/spandex_dispatcher_callback.rb
@@ -0,0 +1,17 @@
+require 'dispatcher'
+
+module SpandexDispatcherCallback
+
+ def self.included(base)
+
+ base.class_eval <<-EOF
+ before_dispatch :clear_local_cache
+
+ def clear_local_cache
+ Rails.cache.clear_local
+ end
+ EOF
+
+ end
+
+end
View
153 lib/spandex_mem_cache_store.rb
@@ -0,0 +1,153 @@
+class SpandexMemCacheStore < ActiveSupport::Cache::Store
+ # this allows caching of the fact that there is nothing in the cache
+ CACHED_NULL = 'spandex:cached_null'
+
+ def initialize(*addresses)
+ @hash = Hash.new
+ @memcache_store = ActiveSupport::Cache::MemCacheStore.new(*addresses)
+ end
+
+ def read(key, options = nil)
+ value = @hash[key]
+ if value == CACHED_NULL
+ value = nil
+ elsif value.nil?
+ value = @memcache_store.read(key, options)
+ @hash[key] = value || CACHED_NULL
+ end
+ value
+ end
+
+ # Set key = value. Pass :unless_exist => true if you don't
+ # want to update the cache if the key is already set.
+ def write(key, value, options = nil)
+ @memcache_store.write(key, (@hash[key] = value || CACHED_NULL), options)
+ end
+
+ def delete(key, options = nil)
+ @hash[key] = CACHED_NULL
+ @memcache_store.delete(key, options)
+ end
+
+ def exist?(key, options = nil)
+ # memcache_store just does a read here, so lets just do that, and cache the result
+ !read(key, options).nil?
+ end
+
+ def increment(key, amount = 1)
+ # don't do any local caching at present, just pass through
+ @memcache_store.increment(key, amount)
+ end
+
+ def decrement(key, amount = 1)
+ # don't do any local caching at present, just pass through
+ @memcache_store.decrement(key, amount)
+ end
+
+ def delete_matched(matcher, options = nil)
+ # don't do any local caching at present, just pass through.
+ # memcache_store doesn't support this so it throws an error
+ @memcache_store.delete_matched(matcher, options)
+ end
+
+ def clear_local
+ @hash = Hash.new
+ end
+
+ def clear
+ clear_local
+ @memcache_store.clear
+ end
+
+ def stats
+ @memcache_store.stats
+ end
+
+end
+
+class CGI
+ class Session
+
+ class SpandexMemCacheStore #< class #Cgi::Session
+
+ # MemCache-based session storage class.
+ #
+ # This builds upon the top-level MemCache class provided by the
+ # library file memcache.rb. Session data is marshalled and stored
+ # in a memcached cache.
+
+ def check_id(id) #:nodoc:#
+ /[^0-9a-zA-Z]+/ =~ id.to_s ? false : true
+ end
+
+ # Create a new CGI::Session::MemCache instance
+ #
+ # This constructor is used internally by CGI::Session. The
+ # user does not generally need to call it directly.
+ #
+ # +session+ is the session for which this instance is being
+ # created. The session id must only contain alphanumeric
+ # characters; automatically generated session ids observe
+ # this requirement.
+ #
+ # +options+ is a hash of options for the initializer. The
+ # following options are recognized:
+ #
+ # cache:: an instance of a MemCache client to use as the
+ # session cache.
+ #
+ # expires:: an expiry time value to use for session entries in
+ # the session cache. +expires+ is interpreted in seconds
+ # relative to the current time if itís less than 60*60*24*30
+ # (30 days), or as an absolute Unix time (e.g., Time#to_i) if
+ # greater. If +expires+ is +0+, or not passed on +options+,
+ # the entry will never expire.
+ #
+ # This session's memcache entry will be created if it does
+ # not exist, or retrieved if it does.
+ def initialize(session, options = {})
+ id = session.session_id
+ unless check_id(id)
+ raise ArgumentError, "session_id '%s' is invalid" % id
+ end
+ #@cache = Rails.cache #options['cache'] || MemCache.new('localhost')
+ @expires = options['expires'] || 0
+ @session_key = "session:#{id}"
+ @session_data = {}
+ # Add this key to the store if haven't done so yet
+ unless Rails.cache.read(@session_key)
+ update
+ end
+ end
+
+ # Restore session state from the session's memcache entry.
+ #
+ # Returns the session state as a hash.
+ def restore
+ @session_data = Rails.cache.read(@session_key) || {}
+ end
+
+ # Save session state to the session's memcache entry.
+ def update
+ Rails.cache.write(@session_key, @session_data, {:expires_in => @expires})
+ end
+
+ # Update and close the session's memcache entry.
+ def close
+ update
+ end
+
+ # Delete the session's memcache entry.
+ def delete
+ Rails.cache.delete(@session_key)
+ @session_data = {}
+ end
+
+ def data
+ @session_data
+ end
+
+ end
+
+ end
+end

0 comments on commit e7d83f1

Please sign in to comment.