patella 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in patella.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Keenan Brock
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Patella
2
+
3
+ Gem for the RailsConf 2012 talk Patella: It's Memoization into Memcached calculated in the background with Resque.
4
+ [www.slideshare.net/jdwyah/patella-railsconf-2012](http://www.slideshare.net/jdwyah/patella-railsconf-2012)
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'patella'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install patella
19
+
20
+ ## Usage
21
+
22
+ ```ruby
23
+ def self.my_slow_method(user_id)
24
+ all_notifications_for(User.find(user_id))
25
+ end
26
+ patella_reflex :notification_unread_count, :expires_in => 3.minutes, :class_method => true
27
+ ```
28
+
29
+ See the tests for more [examples](https://github.com/kbrock/patella/blob/master/test/patella_test.rb)
30
+
31
+ ## Contributing
32
+
33
+ 1. Fork it
34
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
35
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
36
+ 4. Push to the branch (`git push origin my-new-feature`)
37
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |test|
7
+ test.libs << 'lib' << 'test'
8
+ test.pattern = 'test/*_test.rb'
9
+ end
10
+
11
+ task :default => :test
data/lib/patella.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "patella/version"
2
+ require "patella/patella"
3
+ require "patella/send_later"
4
+ require "patella/send_later_worker"
5
+
6
+ module Patella
7
+ # Your code goes here...
8
+ end
@@ -0,0 +1,130 @@
1
+ require 'active_support'
2
+ require 'json'
3
+
4
+ module Patella::Patella
5
+
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ base.send :include, Patella::SendLater
9
+ end
10
+
11
+ def patella_key(symbol, args)
12
+ id_param = respond_to?(:id) ? self.id : nil
13
+ "patella/#{self.class.to_s}/#{id_param}/#{symbol}/#{Digest::MD5.hexdigest(args.to_json)}"
14
+ end
15
+
16
+ module ClassMethods
17
+ def patella_key(symbol, args)
18
+ "patella/#{self.to_s}//#{symbol}/#{Digest::MD5.hexdigest(args.to_json)}"
19
+ end
20
+
21
+ def patella_reflex(symbol, options = {})
22
+ options[:expires_in] ||= 30*60
23
+ options[:soft_expiration] ||= 0
24
+ options[:no_backgrounding] ||= false
25
+ is_class = options[:class_method]
26
+
27
+ original_method = :"_unpatellaed_#{symbol}"
28
+
29
+ method_definitions = <<-EOS, __FILE__, __LINE__ + 1
30
+
31
+ if method_defined?(:#{original_method})
32
+ raise "Already patella'd #{symbol}"
33
+ end
34
+ alias #{original_method} #{symbol}
35
+
36
+ def caching_#{symbol}(args)
37
+ cache_key = self.patella_key('#{symbol}',args)
38
+ result = args.any? ? #{original_method}(*args) : #{original_method}()
39
+ json = {'result' => result, 'soft_expiration' => Time.now + #{options[:expires_in]} - #{ options[:soft_expiration]}}.to_json
40
+ Rails.cache.write(cache_key, json, :expires_in => #{options[:expires_in]})
41
+ result
42
+ end
43
+
44
+ def clear_#{symbol}(*args)
45
+ cache_key = self.patella_key('#{symbol}',args)
46
+ Rails.cache.delete(cache_key)
47
+ end
48
+
49
+ def #{symbol}(*args)
50
+ patella_#{symbol}(args, {:no_backgrounding => #{options[:no_backgrounding]}})
51
+ end
52
+
53
+ def patella_#{symbol}(args, opts)
54
+ cache_key = self.patella_key('#{symbol}',args)
55
+ promise = { 'promise' => true }
56
+
57
+ json = Rails.cache.fetch(cache_key, :expires_in => #{options[:expires_in]}, :force => !Rails.caching?) do
58
+ if opts[:no_backgrounding]
59
+ promise['result'] = self.send(:caching_#{symbol}, args)
60
+ promise.delete('promise')
61
+ else
62
+ promise['result'] = self.send_later(:caching_#{symbol}, args) #send_later sends_later when Rails.caching? otherwise sends_now
63
+ promise.delete('promise') unless Rails.caching?
64
+ end
65
+ promise.to_json
66
+ end
67
+
68
+ if promise['promise'] && opts[:no_backgrounding]
69
+ promise['result'] = self.send(:caching_#{symbol}, args)
70
+ promise.delete('promise')
71
+ json = promise.to_json
72
+ end
73
+
74
+ val = JSON.parse(json)
75
+ if val and !val['promise']
76
+ loading = false
77
+ soft_expiration = Time.parse(val['soft_expiration']) rescue nil
78
+ json_val = val
79
+ val = val['result']
80
+ else
81
+ val = promise
82
+ loading = true
83
+ end
84
+
85
+ if !loading and soft_expiration and Time.now > soft_expiration
86
+ expires = #{options[:soft_expiration]} + 10*60
87
+ json_val['soft_expiration'] = (Time.now - expires).to_s
88
+ Rails.cache.write(cache_key, json_val, :expires_in => expires)
89
+ self.send_later(:caching_#{symbol}, args)
90
+ end
91
+
92
+ PatellaResult.new val, loading
93
+ end
94
+
95
+ if private_method_defined?(#{original_method.inspect}) # if private_method_defined?(:_unmemoized_mime_type)
96
+ private #{symbol.inspect} # private :mime_type
97
+ elsif protected_method_defined?(#{original_method.inspect}) # elsif protected_method_defined?(:_unmemoized_mime_type)
98
+ protected #{symbol.inspect} # protected :mime_type
99
+ end # end
100
+ EOS
101
+
102
+ if is_class
103
+ (class << self; self; end).class_eval *method_definitions
104
+ else
105
+ class_eval *method_definitions
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ class PatellaResult < ActiveSupport::BasicObject
112
+
113
+ def initialize(target=nil, loading=false)
114
+ @target = target
115
+ @loading = loading
116
+ end
117
+
118
+ def loading?
119
+ @loading
120
+ end
121
+
122
+ def loaded?
123
+ !@loading
124
+ end
125
+
126
+ def method_missing(method, *args, &block)
127
+ @target.send(method, *args, &block)
128
+ end
129
+ end
130
+
@@ -0,0 +1,26 @@
1
+ module Patella::SendLater
2
+
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ def send_later method_name, *args
8
+ #in dev mode, run the command in process
9
+ if Rails.caching?
10
+ Patella::SendLaterWorker.perform_later self.class.to_s, self.id, method_name, *args
11
+ else
12
+ self.send method_name, *args
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def send_later method_name, *args
18
+ if Rails.caching?
19
+ Patella::SendLaterWorker.perform_later self.to_s, nil, method_name, *args
20
+ else
21
+ self.send method_name, *args
22
+ end
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,18 @@
1
+ module Patella
2
+ class SendLaterWorker
3
+ @queue = :send_later
4
+
5
+ def self.perform_later *args
6
+ self.enqueue *args
7
+ end
8
+
9
+ def self.perform(class_name, instance_id, method_name, *args)
10
+ o = class_name.constantize
11
+ o = o.find_by_id instance_id if instance_id
12
+
13
+ o.send(method_name, *args)
14
+ rescue => e
15
+ raise e, "#{e.message} \nWith SendLater: #{class_name}#{':'+instance_id.to_s if instance_id}##{method_name}(#{args.map(&:to_s).join(',')})", e.backtrace
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Patella
2
+ VERSION = "0.0.1" unless defined?(Patella::VERSION)
3
+ end
data/patella.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/patella/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jeff Dwyer"]
6
+ gem.email = ["jdwyer@patientslikeme.com"]
7
+ gem.description = "It's Memoization into Memcached calculated in the background with Resque."
8
+ gem.summary = "DSL to add memoization on any method into Memcached calculated in the background with Resque."
9
+ gem.homepage = "https://github.com/patientslikeme/patella"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "patella"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Patella::VERSION
17
+ gem.add_dependency 'resque', '~>1.16'
18
+ gem.add_dependency 'activesupport', '~>2.3' #, :require => 'active_support'
19
+ gem.add_dependency 'rails', '~>2.3' #, :require => 'active_support'
20
+ gem.add_dependency 'json'
21
+ gem.add_development_dependency 'bundler'
22
+ gem.add_development_dependency 'rake'
23
+ gem.add_development_dependency 'mocha', "~>0.9.8"
24
+ gem.add_development_dependency 'bourne'
25
+ end
@@ -0,0 +1,144 @@
1
+ require 'test_helper'
2
+
3
+ class Dummy
4
+ include Patella::Patella
5
+ include Patella::SendLater
6
+
7
+ attr_accessor :id
8
+ def initialize id
9
+ self.id = id
10
+ end
11
+
12
+ def self.find_by_id id
13
+ new id
14
+ end
15
+
16
+ def foo
17
+ 5
18
+ end
19
+ patella_reflex :foo
20
+
21
+ def bar(a,b)
22
+ a+b
23
+ end
24
+ patella_reflex :bar
25
+
26
+ def baz(add)
27
+ self.id + add
28
+ end
29
+ patella_reflex :baz
30
+
31
+ def self.bing
32
+ 3
33
+ end
34
+ patella_reflex :bing, :class_method => true
35
+
36
+ def no_background_add(a,b)
37
+ a + b
38
+ end
39
+ patella_reflex :no_background_add, :no_backgrounding => true
40
+ end
41
+
42
+ class PatellaTest < ActiveSupport::TestCase
43
+
44
+ def test_patella_basics
45
+ # foreground
46
+ f = Dummy.new 6
47
+ f.stubs :caching_foo => 5
48
+ assert_equal 5, f.foo
49
+ assert_received(f, :caching_foo)
50
+
51
+ #background
52
+ with_caching do
53
+ Patella::SendLaterWorker.stubs :perform_later => 'loading'
54
+ f1 = Dummy.new 5
55
+ assert f1.foo.loading?
56
+ assert_received(Patella::SendLaterWorker, :perform_later) do |ex|
57
+ ex.once
58
+ ex.with 'Dummy', f1.id, :caching_foo, []
59
+ end
60
+ end
61
+ end
62
+
63
+ def test_turning_off_background
64
+ #background
65
+ with_caching do
66
+ Patella::SendLaterWorker.expects(:perform_later).never
67
+ Dummy.any_instance.expects('caching_no_background_add').once.returns(9)
68
+ d = Dummy.new(1)
69
+ result = d.no_background_add(4,5)
70
+ assert result.loaded?
71
+ assert_equal 9, result
72
+ end
73
+ end
74
+
75
+ def cache_clearing
76
+ d = Dummy.new(1)
77
+ result = d.bar(4,5) #load it by turning caching off
78
+ assert result.loaded?
79
+ assert_equal 9, result
80
+
81
+ with_caching do
82
+ result = d.bar(4,5) #still here
83
+ assert result.loaded?
84
+ assert_equal 9, result
85
+
86
+ d.clear_bar(4,5)
87
+ result = d.bar(4,5) #cleared
88
+ assert result.loading?
89
+
90
+ end
91
+ end
92
+
93
+ def test_patella_for_instance_objs
94
+ four = Dummy.new 4
95
+ assert_equal(8, four.baz(4))
96
+ assert_equal(13, four.baz(9))
97
+ end
98
+
99
+ def test_patella_for_class_methods
100
+ assert Dummy.bing.loaded?
101
+ assert_equal(3, Dummy.bing)
102
+ end
103
+
104
+ def test_keys
105
+ d = Dummy.new 2
106
+ assert_equal "patella/Dummy/2/foo/#{md5 [].to_json}", d.patella_key(:foo,[])
107
+ assert_equal "patella/Dummy/2/foo/#{md5 [1].to_json}", d.patella_key(:foo,[1])
108
+ assert_equal "patella/Dummy/2/foo/#{md5 [1,3].to_json}", d.patella_key(:foo,[1,3])
109
+ assert_equal "patella/Dummy/2/foo/#{md5 [1,"asdf"].to_json}", d.patella_key(:foo,[1,"asdf"])
110
+ assert_equal "patella/Dummy/2/foo/#{md5 [1,"asdf"*1000].to_json}", d.patella_key(:foo,[1, ("asdf"*1000)])
111
+
112
+ d3 = Dummy.new 3
113
+ assert_equal "patella/Dummy/3/foo/#{md5 [].to_json}", d3.patella_key(:foo,[])
114
+ assert_equal "patella/Dummy/3/foo/#{md5 [1].to_json}", d3.patella_key(:foo,[1])
115
+ assert_equal "patella/Dummy/3/foo/#{md5 [1,3].to_json}", d3.patella_key(:foo,[1,3])
116
+ assert_equal "patella/Dummy/3/foo/#{md5 [1,"asdf" * 1000].to_json}", d3.patella_key(:foo,[1,"asdf" * 1000])
117
+ end
118
+
119
+ def test_soft_expiration
120
+ dummy = Dummy.new 1
121
+ ::Rails.cache.stubs( :fetch => { 'result' => "loads of data", 'soft_expiration' => Time.now-1.minute }.to_json )
122
+ ::Rails.cache.stubs( :write => true )
123
+ dummy.stubs :send_later => 'loading'
124
+ dummy.foo
125
+ assert_received(Rails.cache, :write) { |ex| ex.once }
126
+ assert_received(dummy, :send_later) { |ex| ex.once }
127
+ end
128
+
129
+ private
130
+ def md5(str)
131
+ Digest::MD5.hexdigest(str)
132
+ end
133
+
134
+ def with_caching(&block)
135
+ #previous_caching = ActionController::Base.perform_caching
136
+ begin
137
+ Rails.stubs(:caching? => true)
138
+ #ActionController::Base.perform_caching = true
139
+ yield
140
+ ensure
141
+ #ActionController::Base.perform_caching = previous_caching
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ Bundler.require
5
+ require 'patella'
6
+ require 'test/unit'
7
+ require 'active_support'
8
+ require 'active_support/test_case'
9
+ require 'json'
10
+ require 'mocha'
11
+ require 'bourne'
12
+
13
+ unless defined?(Rails)
14
+ module Rails
15
+ class MockCache
16
+ def fetch(*args)
17
+ yield
18
+ end
19
+ def write(*args)
20
+ end
21
+ end
22
+
23
+ def self.caching=(value)
24
+ @caching=value
25
+ end
26
+
27
+ def self.caching?
28
+ @caching
29
+ end
30
+ def self.cache
31
+ @cache ||= MockCache.new
32
+ end
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: patella
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jeff Dwyer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: resque
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.16'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.16'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '2.3'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '2.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rails
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '2.3'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: json
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: bundler
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rake
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: mocha
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 0.9.8
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: 0.9.8
126
+ - !ruby/object:Gem::Dependency
127
+ name: bourne
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ description: It's Memoization into Memcached calculated in the background with Resque.
143
+ email:
144
+ - jdwyer@patientslikeme.com
145
+ executables: []
146
+ extensions: []
147
+ extra_rdoc_files: []
148
+ files:
149
+ - .gitignore
150
+ - Gemfile
151
+ - LICENSE
152
+ - README.md
153
+ - Rakefile
154
+ - lib/patella.rb
155
+ - lib/patella/patella.rb
156
+ - lib/patella/send_later.rb
157
+ - lib/patella/send_later_worker.rb
158
+ - lib/patella/version.rb
159
+ - patella.gemspec
160
+ - test/patella_test.rb
161
+ - test/test_helper.rb
162
+ homepage: https://github.com/patientslikeme/patella
163
+ licenses: []
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ none: false
176
+ requirements:
177
+ - - ! '>='
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubyforge_project:
182
+ rubygems_version: 1.8.24
183
+ signing_key:
184
+ specification_version: 3
185
+ summary: DSL to add memoization on any method into Memcached calculated in the background
186
+ with Resque.
187
+ test_files:
188
+ - test/patella_test.rb
189
+ - test/test_helper.rb