Skip to content

Commit

Permalink
Uploading first version (0.8) with local per request cache and single…
Browse files Browse the repository at this point in the history
… connection for cache and session stores.
  • Loading branch information
Nahum Wild committed Nov 9, 2008
1 parent abae10d commit e7d83f1
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 0 deletions.
21 changes: 21 additions & 0 deletions 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.

96 changes: 96 additions & 0 deletions 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
7 changes: 7 additions & 0 deletions change.log
@@ -0,0 +1,7 @@
Version 0.8
-----------

* Initial Version
* Local per request cache
* Session store re-uses the cache store

4 changes: 4 additions & 0 deletions init.rb
@@ -0,0 +1,4 @@
require 'spandex_mem_cache_store'
require 'spandex_dispatcher_callback'

Dispatcher.send :include, SpandexDispatcherCallback
17 changes: 17 additions & 0 deletions 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
153 changes: 153 additions & 0 deletions 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.