Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Uploading first version (0.8) with local per request cache and single…
… connection for cache and session stores.
- Loading branch information
Nahum Wild
committed
Nov 9, 2008
1 parent
abae10d
commit e7d83f1
Showing
6 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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. | |||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,7 @@ | |||
Version 0.8 | |||
----------- | |||
|
|||
* Initial Version | |||
* Local per request cache | |||
* Session store re-uses the cache store | |||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,4 @@ | |||
require 'spandex_mem_cache_store' | |||
require 'spandex_dispatcher_callback' | |||
|
|||
Dispatcher.send :include, SpandexDispatcherCallback |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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 |