Permalink
Browse files

use Marshal.dump instead of #hash to digest instance state and args

  • Loading branch information...
1 parent 11ceaad commit 56d7b5321936bbf6b51245aaaf416b8611f7a8c0 @seamusabshere committed Mar 1, 2012
View
@@ -1,7 +1,6 @@
*.gem
-.bundle
Gemfile.lock
-pkg/*
-rdoc/*
-secret.sh
.DS_Store
+.yardoc/
+doc/
+.bundle/
View
@@ -0,0 +1,24 @@
+0.2.0 / 2012-02-29
+
+* API changes:
+
+ * #clear_method_cache -> #cache_method_clear
+ * #method_cache_hash -> #as_cache_key (see README)
+ * doesn't rely on #hash at all any more (http://numbers.brighterplanet.com/2012/02/29/beware-of-string-hash-being-used-for-cache-keys/)
+ * doesn't try to mitigate differences between Ruby 1.8 and 1.9 splats (http://www.ruby-forum.com/topic/98106)
+
+* Enhancements
+
+ * clearer method naming, I think
+ * safer determination of cache keys - previously, in effect, args.map(&:to_s) was being used
+ * no more autoload
+ * remove direct dependency on activesupport
+ * tests now run with dalli, green on MRI 1.8, MRI 1.9, and jruby 1.6.7
+
+0.1.7 / yanked!
+
+* Bug: used args.map(&:hash) instead of args.map(&:to_s)... which was actually worse because #hash is different across processes.
+
+0.1.6 / November 17, 2011
+
+* Last release before CHANGELOG in place
View
@@ -1,4 +1,9 @@
-source "http://rubygems.org"
+source :rubygems
# Specify your gem's dependencies in cache_method.gemspec
gemspec
+
+# development dependencies
+gem 'rake'
+gem 'dalli'
+gem 'yard'
@@ -1,14 +1,14 @@
-= cache_method
+# cache_method
-It's like <tt>alias_method</tt>, but it's <tt>cache_method</tt>!
+It's like `alias_method`, but it's `cache_method`!
Lets you cache the results of calling methods given their arguments. Like memoization, but stored in Memcached, Redis, etc. so that the cached results can be shared between processes and hosts.
-== Real-world usage
+## Real-world usage
-In production use at {impact.brighterplanet.com}[http://impact.brighterplanet.com] and {data.brighterplanet.com}[http://data.brighterplanet.com].
+In production use at [impact.brighterplanet.com](http://impact.brighterplanet.com) and [data.brighterplanet.com](http://data.brighterplanet.com).
-== Example
+## Example
require 'cache_method'
class Blog
@@ -25,12 +25,11 @@ In production use at {impact.brighterplanet.com}[http://impact.brighterplanet.co
end
cache_method :get_latest_entries
- # cache_method defaults to using the #hash method
- # It's a "hash code" (integer! always integer!) representing the internal state of an instance.
- # If you need to customize it, you can define #method_cache_hash.
- # In that case, it's recommended that you construct a String or a Hash and then call #hash on it (because you should return an integer)
- def method_cache_hash
- { :name => name, :url => url }.hash
+ # By default, cache_method derives the cache key for an instance by getting the SHA1 hash of the Marshal.dump
+ # If you need to customize how an instance is recognized, you can define #as_cache_key.
+ # Marshal.load will be called on the result.
+ def as_cache_key
+ { :name => name, :url => url }
end
end
@@ -41,11 +40,11 @@ Then you can do
And clear them too
- my_blog.clear_method_cache :get_latest_entries
+ my_blog.cache_method_clear :get_latest_entries
(which doesn't delete the rest of your cache)
-== Configuration (and supported cache clients)
+## Configuration (and supported cache clients)
You need to set where the cache will be stored:
@@ -59,9 +58,9 @@ or this might even work...
CacheMethod.config.storage = Rails.cache
-See <tt>Config</tt> for the full list of supported caches.
+See `Config` for the full list of supported caches.
-== Defining a #hash method (not the same as #to_hash)
+== Defining a #as_cache_key method
Since we're not pure functional programmers, sometimes cache hits depend on object state in addition to method arguments. To illustrate:
@@ -71,25 +70,17 @@ get_latest_entries doesn't take any arguments, so it must depend on my_blog.url
class Blog
# [...]
- def method_cache_hash
- { :name => name, :url => url }.hash
+ def as_cache_key
+ { :name => name, :url => url }
end
# [...]
end
-You should follow Ruby convention and have <tt>#hash</tt> return a <tt>Fixnum</tt>.
+If you don't define `#as_cache_key`, then `cache_method` will `Marshal.dump` an instance.
-Ideally, you should try to make a <tt>String</tt> or a <tt>Hash</tt> and call the standard <tt>#hash</tt> on that.
+## Module methods
-Note: this is NOT the same thing as <tt>#to_hash</tt>! That returns a <b><tt>Hash</tt></b>. What we want is an integer "hash code."
-
-== Using #method_cache_hash instead of #hash
-
-If you don't want to modify #hash, use #method_cache_hash instead.
-
-== Module methods
-
-You can put <tt>#cache_method</tt> right into your module declarations:
+You can put `#cache_method` right into your module declarations:
module MyModule
def my_module_method(args)
@@ -106,21 +97,21 @@ You can put <tt>#cache_method</tt> right into your module declarations:
extend MyModule
end
-Rest assured that <tt>Tiger.my_module_method</tt> and <tt>Lion.my_module_method</tt> will be cached correctly and separately. This, on the other hand, won't work:
+Rest assured that `Tiger.my_module_method` and `Lion.my_module_method` will be cached correctly and separately. This, on the other hand, won't work:
class Tiger
extend MyModule
# wrong - will raise NameError Exception: undefined method `my_module_method' for class `Tiger'
# cache_method :my_module_method
end
-== Rationale
+## Rationale
-* It should be easy to cache a method using memcached, dalli (if you're on heroku), redis, etc. (that's why I made the {cache gem}[https://rubygems.org/gems/cache])
+* It should be easy to cache a method using memcached, dalli (if you're on heroku), redis, etc. (that's why I made the [cache gem](https://rubygems.org/gems/cache))
* It should be easy to uncache a method without clearing the whole cache
* It should be easy to cache instance methods
-* It should be easy to cache methods that depend on object state
+* It should be easy to cache methods that depend on object state (hence `#as_cache_key`)
-== Copyright
+## Copyright
-Copyright 2011 Seamus Abshere
+Copyright 2012 Seamus Abshere
View
@@ -1,5 +1,5 @@
-require 'bundler'
-Bundler::GemHelper.install_tasks
+#!/usr/bin/env rake
+require "bundler/gem_tasks"
require 'rake'
require 'rake/testtask'
@@ -11,12 +11,5 @@ end
task :default => :test
-require 'rake/rdoctask'
-Rake::RDocTask.new do |rdoc|
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
-
- rdoc.rdoc_dir = 'rdoc'
- rdoc.title = "cache_method #{version}"
- rdoc.rdoc_files.include('README*')
- rdoc.rdoc_files.include('lib/**/*.rb')
-end
+require 'yard'
+YARD::Rake::YardocTask.new
@@ -18,11 +18,4 @@ Gem::Specification.new do |s|
s.require_paths = ["lib"]
s.add_dependency 'cache', '>=0.2.1'
- s.add_development_dependency 'memcached'
- s.add_development_dependency 'rake'
- # if RUBY_VERSION >= '1.9'
- # s.add_development_dependency 'ruby-debug19'
- # else
- # s.add_development_dependency 'ruby-debug'
- # end
end
View
@@ -1,10 +1,9 @@
-require 'cache_method/version'
+require 'cache_method/config'
+require 'cache_method/cached_result'
+require 'cache_method/generation'
+
# See the README.rdoc for more info!
module CacheMethod
- autoload :Config, 'cache_method/config'
- autoload :CachedResult, 'cache_method/cached_result'
- autoload :Generation, 'cache_method/generation'
-
def self.config #:nodoc:
Config.instance
end
@@ -21,19 +20,19 @@ def self.method_signature(obj, method_id) #:nodoc:
[ klass_name(obj), method_id ].join method_delimiter(obj)
end
- # All Objects, including instances and Classes, get the <tt>#clear_method_cache</tt> method.
+ # All Objects, including instances and Classes, get the <tt>#cache_method_clear</tt> method.
module InstanceMethods
# Clear the cache for a particular method.
#
- # Note: Remember to define <tt>#hash</tt> on any object whose instance methods might get cached.
+ # Note: Remember to define <tt>#as_cache_key</tt> on any object whose instance methods might get cached.
#
# Example:
- # my_blog.clear_method_cache :get_latest_entries
- def clear_method_cache(method_id)
+ # my_blog.cache_method_clear :get_latest_entries
+ def cache_method_clear(method_id)
if ::CacheMethod.config.generational?
::CacheMethod::Generation.new(self, method_id).mark_passing
else
- raise ::RuntimeError, "[cache_method] clear_method_cache called, but you have disabled generational caching. Check your setting for CacheMethod.config.generational"
+ raise ::RuntimeError, "[cache_method] cache_method_clear called, but you have disabled generational caching. Check your setting for CacheMethod.config.generational"
end
end
end
@@ -42,7 +41,7 @@ def clear_method_cache(method_id)
module ClassMethods
# Cache a method. TTL in seconds, defaults to whatever's in CacheMethod.config.default_ttl
#
- # Note: Remember to define <tt>#hash</tt> on any object whose instance methods might get cached.
+ # Note: Remember to define <tt>#as_cache_key</tt> on any object whose instance methods might get cached.
#
# Note 2: Check out CacheMethod.config.default_ttl... the default is 24 hours.
#
@@ -2,7 +2,6 @@
module CacheMethod
class CachedResult #:nodoc: all
CACHE_KEY_JOINER = ','
- ARG_HASH_JOINER = '/'
def initialize(obj, method_id, original_method_id, ttl, args)
@obj = obj
@@ -36,20 +35,20 @@ def cache_key
if obj.is_a?(::Class) or obj.is_a?(::Module)
[ 'CacheMethod', 'CachedResult', method_signature, current_generation, args_digest ].compact.join CACHE_KEY_JOINER
else
- [ 'CacheMethod', 'CachedResult', method_signature, obj_hash, current_generation, args_digest ].compact.join CACHE_KEY_JOINER
+ [ 'CacheMethod', 'CachedResult', method_signature, obj_digest, current_generation, args_digest ].compact.join CACHE_KEY_JOINER
end
end
def method_signature
@method_signature ||= ::CacheMethod.method_signature(obj, method_id)
end
- def obj_hash
- @obj_hash ||= obj.respond_to?(:method_cache_hash) ? obj.method_cache_hash : obj.hash
+ def obj_digest
+ @obj_digest ||= ::Digest::SHA1.hexdigest(::Marshal.dump(obj.respond_to?(:as_cache_key) ? obj.as_cache_key : obj))
end
def args_digest
- @args_digest ||= args.empty? ? 'empty' : calculate_args_digest
+ @args_digest ||= args.empty? ? 'empty' : ::Digest::SHA1.hexdigest(::Marshal.dump(args))
end
def current_generation
@@ -58,20 +57,8 @@ def current_generation
end
end
- private
-
- def calculate_args_digest
- # equality ruby 1.8 and 1.9 splat behavior
- # FIXME i don't think cache_method should handle this, really
- hashes = args.map do |arg|
- case arg
- when ::Array
- arg.map { |subarg| subarg.hash }.join(ARG_HASH_JOINER)
- else
- arg.hash
- end
- end
- ::Digest::SHA1.hexdigest hashes.join(ARG_HASH_JOINER)
+ def arity
+ @arity ||= obj.method(original_method_id).arity
end
end
end
@@ -1,3 +1,4 @@
+require 'digest/sha1'
module CacheMethod
class Generation #:nodoc: all
class << self
@@ -18,15 +19,15 @@ def method_signature
@method_signature ||= ::CacheMethod.method_signature(obj, method_id)
end
- def obj_hash
- @obj_hash ||= obj.respond_to?(:method_cache_hash) ? obj.method_cache_hash : obj.hash
+ def obj_digest
+ @obj_digest ||= ::Digest::SHA1.hexdigest(::Marshal.dump(obj.respond_to?(:as_cache_key) ? obj.as_cache_key : obj))
end
def cache_key
- if obj.is_a? ::Class or obj.is_a? ::Module
+ if obj.is_a?(::Class) or obj.is_a?(::Module)
[ 'CacheMethod', 'Generation', method_signature ].join ','
else
- [ 'CacheMethod', 'Generation', method_signature, obj_hash ].join ','
+ [ 'CacheMethod', 'Generation', method_signature, obj_digest ].join ','
end
end
@@ -1,3 +1,3 @@
module CacheMethod
- VERSION = "0.1.7"
+ VERSION = "0.2.0"
end
View
@@ -25,30 +25,20 @@ def echo_count
# representation."
def echo(*args)
self.echo_count += 1
- if RUBY_VERSION >= '1.9'
- if args.empty?
- return nil
- elsif args.length == 1
- return args[0]
- else
- return args
- end
- else
- return *args
- end
+ return *args
end
- def hash
- raise "Used hash"
+ def marshal_dump
+ raise "Used marshal_dump"
end
- def method_cache_hash
- name.hash
+ def as_cache_key
+ name
end
cache_method :echo
end
class CopyCat1a < CopyCat1
- def method_cache_hash
- raise "Used method_cache_hash"
+ def as_cache_key
+ raise "Used as_cache_key"
end
end
@@ -113,8 +103,8 @@ def get_latest_entries2
["voo vaa #{name}"]
end
cache_method :get_latest_entries2, 1 # second
- def hash
- { :name => name, :url => url }.hash
+ def as_cache_key
+ { :name => name, :url => url }
end
end
def new_instance_of_my_blog
Oops, something went wrong. Retry.

0 comments on commit 56d7b53

Please sign in to comment.