benny_cache 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +281 -0
- data/Rakefile +15 -0
- data/benny_cache.gemspec +23 -0
- data/lib/benny_cache.rb +7 -0
- data/lib/benny_cache/base.rb +36 -0
- data/lib/benny_cache/cache.rb +44 -0
- data/lib/benny_cache/config.rb +21 -0
- data/lib/benny_cache/model.rb +264 -0
- data/lib/benny_cache/related.rb +58 -0
- data/lib/benny_cache/version.rb +3 -0
- data/spec/models/cache_spec.rb +59 -0
- data/spec/models/config_spec.rb +15 -0
- data/spec/models/model_method_cache_spec.rb +85 -0
- data/spec/models/model_spec.rb +116 -0
- data/spec/models/related_spec.rb +63 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/test_classes.rb +73 -0
- metadata +137 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Steven Hilton
|
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,281 @@
|
|
1
|
+
# BennyCache
|
2
|
+
|
3
|
+
BennyCache is a model, data and method caching library that uses the ActiveRecord API, but does not try to get in between you and
|
4
|
+
ActiveRecord. The main motivation for creating BennyCache was to make it possible to implicitly and efficiently
|
5
|
+
clear the cache of one record when changes to related records are made.
|
6
|
+
|
7
|
+
For example, suppose an Agent has a set of Items in its Inventory. We have an agent's data is populated in the cache,
|
8
|
+
and we want to change the agent's inventory, either add a new item, or update the remaining ammo of a weapon.
|
9
|
+
With BennyCache, we can update an individual Item and the Agent's inventory cache is cleared. We do not have
|
10
|
+
to load the agent into memory, and the agent's basic information remains unchanged in the cache -- only the Inventory
|
11
|
+
data for that Agent is flushed.
|
12
|
+
|
13
|
+
BennyCache uses Rails.cache if available, or you can provide your own caching engine. Otherwise, it uses an
|
14
|
+
internal memory cache by default. The internal memory cache is meant for testing and evaluation purposes only.
|
15
|
+
|
16
|
+
|
17
|
+
### Contrasting BennyCache with other similar caching tools:
|
18
|
+
|
19
|
+
* [CacheFu](https://github.com/defunkt/cache_fu) or [Rails 3 compatible fork](https://github.com/kreetitech/cache_fu)
|
20
|
+
|
21
|
+
* [CacheMoney](https://github.com/nkallen/cache-money)
|
22
|
+
|
23
|
+
* [CacheMethod](https://github.com/seamusabshere/cache_method)
|
24
|
+
|
25
|
+
__Differences__
|
26
|
+
|
27
|
+
- BennyCache is marginally aware of ActiveRecord, but doesn't touch the internals, so it should be
|
28
|
+
forward-compatible, or as much as it can be.
|
29
|
+
- Usage of BennyCache is explicit: It doesn't try to do hide itself from the code.
|
30
|
+
- Method caching in BennyCache was a bit inspired by CacheMethod. CacheMethod is a more robust solution for method
|
31
|
+
caching, especially if you are passing complex data structures as parameters.
|
32
|
+
|
33
|
+
## Installation
|
34
|
+
|
35
|
+
Add this line to your application's Gemfile:
|
36
|
+
|
37
|
+
gem 'benny_cache'
|
38
|
+
|
39
|
+
And then execute:
|
40
|
+
|
41
|
+
$ bundle
|
42
|
+
|
43
|
+
Or install it yourself as:
|
44
|
+
|
45
|
+
$ gem install benny_cache
|
46
|
+
|
47
|
+
## Usage
|
48
|
+
|
49
|
+
BennyCache will cache three separate but related types of information.
|
50
|
+
|
51
|
+
* __Model Cache__ Model Caches are a cached representation of a model
|
52
|
+
* __Data Cache__ Data Caches are cached representation of data related to a model, but not the model itself.
|
53
|
+
* __Method Cache__ Method Caches are cached result of a call to model method.
|
54
|
+
|
55
|
+
Model, data and method caches are independent of each other. A model is cached and uncached independently
|
56
|
+
of the data cache or method cache related ot the model. You can cache a model method without
|
57
|
+
caching the model itself.
|
58
|
+
|
59
|
+
Another important concept in BennyCache is the __Related Index__ When related indexes are defined, a link is
|
60
|
+
created between one model and another model's data or method caches. For example, when a "skill" (an instance
|
61
|
+
of the Skill class) is upgraded, the related Robot will automatically have its skills method cache cleared,
|
62
|
+
without having to directly reference a Robot model. The relationship is created by defining a method_index in
|
63
|
+
the Robot class, and a related_index in the Skill class.
|
64
|
+
|
65
|
+
### Defining the cache store
|
66
|
+
|
67
|
+
By default, BennyCache uses Rails.cache store if available. You can explicitly set the cache store by calling:
|
68
|
+
|
69
|
+
BennyCache::Config.store = Rails.cache
|
70
|
+
|
71
|
+
The cache is expected to support the `#read`, `#write`, `#delete` and `#fetch` methods of the
|
72
|
+
`ActiveSuport::Cache::Store` interface.
|
73
|
+
|
74
|
+
If Rails.cache is not defined, and you don't explicitly initialize a cache, BennyCache uses an internal
|
75
|
+
`BennyCache::Cache` object, which is just an in-memory key-value hash. The internal cache
|
76
|
+
object is not intended for production use.
|
77
|
+
|
78
|
+
|
79
|
+
### Model Indexes
|
80
|
+
|
81
|
+
Include `BennyCache::Model` into your ActiveRecord model and declare your indexes:
|
82
|
+
|
83
|
+
class Robot < ActiveRecord::Base
|
84
|
+
include BennyCache::Model
|
85
|
+
benny_model_index :user_id
|
86
|
+
end
|
87
|
+
|
88
|
+
class Location < ActiveRecord::Base
|
89
|
+
include BennyCache::Model
|
90
|
+
benny_model_index [:x, :y]
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
Once that is done, loading item from the cache is easy:
|
95
|
+
|
96
|
+
robot = Robot.benny_model_cache(user_id: current_user.id)
|
97
|
+
|
98
|
+
location = Location.benny_model_cache(123) # cached by primary key
|
99
|
+
location = Location.benny_model_cache(x: 30, y: 50) # cached by coordinates
|
100
|
+
|
101
|
+
Calling these will populate the my_model object in the cache with different keys. If you
|
102
|
+
change `location` and call `#save()` or `#destroy()`, all instances will be cleared from the cache.
|
103
|
+
|
104
|
+
|
105
|
+
### Data Indexes
|
106
|
+
|
107
|
+
A data index is a piece of data related to a specific model that is created by a block of code. Data caches
|
108
|
+
are maintained separate from the model cache itself. Data caches are useful for data about a model
|
109
|
+
that is expensive to load and/or calculate, or changes infrequently in relation to the model itself.
|
110
|
+
|
111
|
+
Data caches and model caches are independent of each other. A model cache is maintained independently
|
112
|
+
of any data caches related to the model.
|
113
|
+
|
114
|
+
To use data indexes, you must first declare a data index key. When loading the key, you also pass a block that
|
115
|
+
is used to populate the cache.
|
116
|
+
|
117
|
+
class MyModel < ActiveRecord::Base
|
118
|
+
include BennyCache::Model
|
119
|
+
|
120
|
+
benny_data_index :my_data_index
|
121
|
+
end
|
122
|
+
|
123
|
+
Then, when using the model...
|
124
|
+
|
125
|
+
my_model.benny_data_cache(:my_data_index) {
|
126
|
+
self.expensive_data_to_calculate()
|
127
|
+
}
|
128
|
+
|
129
|
+
The return value of self.expensive_data_to_calculate() is used to populate the cache for the :my_data_index key.
|
130
|
+
Further calls my_model.benny_data_cache(:my_data_index) will return the cached value until the cache is cleared.
|
131
|
+
Usually, some external process will invalidate a data cache.
|
132
|
+
|
133
|
+
|
134
|
+
#### Clearing data index cache
|
135
|
+
To manually clear a data index cache for a model, you need the primary key
|
136
|
+
of the model and use a class method. You do not need to instantiate the model.
|
137
|
+
|
138
|
+
MyModel.benny_data_cache_delete(123, :my_data_index)
|
139
|
+
|
140
|
+
If changes to one model might need to invalidate the data caches of another mother, this can be managed automatically
|
141
|
+
with the `BennyCache::Related` mixin describe below.
|
142
|
+
|
143
|
+
### Method Indexes
|
144
|
+
|
145
|
+
A method index is a cached result of a call to a model method. Like Data caches, Method caches
|
146
|
+
are created and deleted separate from the model cache itself. Also like Data caches, Method caches are useful
|
147
|
+
for caching data about a model that is expensive to load or calculate, or changes at different intervals in
|
148
|
+
relation to the model itself.
|
149
|
+
|
150
|
+
To use method indexes, declare a method index. Method indexes use ruby method aliasing, so the
|
151
|
+
source method must be defined *before* declaring the method index.
|
152
|
+
|
153
|
+
class MyModel < ActiveRecord::Base
|
154
|
+
include BennyCache::Model
|
155
|
+
|
156
|
+
def method_name # source method first
|
157
|
+
[expensive_code]
|
158
|
+
end
|
159
|
+
benny_method_index :method_name # index second
|
160
|
+
end
|
161
|
+
|
162
|
+
Arguments passed to a cached method are hashed to create a unique signature per parameter list, and the
|
163
|
+
cached value is based on the method name and the args hash sig. In the following example:
|
164
|
+
|
165
|
+
rv1 = agent.method_name :foo
|
166
|
+
rv2 = agent.method_name :bar
|
167
|
+
|
168
|
+
If agent#method_name is declared as a method_index, rv1 and and rv2 will be two different cached values.
|
169
|
+
|
170
|
+
#### Local caching
|
171
|
+
|
172
|
+
When a BennyCache method index is called, benny cache keeps an model-specific copy of the cache in local memory,
|
173
|
+
so multiple calls to the same method return the same object with the same object_id.
|
174
|
+
This supports in process updates to the data. For example:
|
175
|
+
|
176
|
+
rv1 = agent.method_name #=> [:a, :b, :c]
|
177
|
+
rv1.push :d
|
178
|
+
rv2 = agent.method_name #=> [:a, :b, :c, :d]
|
179
|
+
|
180
|
+
rv1.size == 4 #=> true
|
181
|
+
rv1.object_id == rv2.object_id #=> true
|
182
|
+
|
183
|
+
This behavior works for my needs, but may not suit all users. I may add the ability to change this behavior.
|
184
|
+
|
185
|
+
I have not fully tested the benny_method_index functionality with all of the ActiveRelation's varied functionality.
|
186
|
+
Using the two together and exercising different parts of ActiveRelation may have unexpected results. However, in my
|
187
|
+
simple case, where I use basic :has_many relationships and don't use #where, #include, etc, it works the way I need
|
188
|
+
it to work.
|
189
|
+
|
190
|
+
Simple use cases should work without issue, but passing complex data structures to cached methods may
|
191
|
+
confuse BennyCache. For more robust method caching, checkout out [CacheMethod](https://github.com/seamusabshere/cache_method).
|
192
|
+
|
193
|
+
|
194
|
+
#### Clearing model index cache
|
195
|
+
Method indexes will cache data on per-args_hash basis, but clearing the cache for a model index is more of a shotgun
|
196
|
+
approach: clearing a model index cache will clear _all_ cached data for all args hashes.
|
197
|
+
|
198
|
+
To manually clear a method index cache for a model, you need the primary key of the model, the method name, and use a
|
199
|
+
class method. You do not need to instantiate the model.
|
200
|
+
|
201
|
+
MyModel.benny_method_cache_delete(123, :method_name)
|
202
|
+
|
203
|
+
If changes to one model might need to invalidate the data caches of another mother, this can be managed
|
204
|
+
with the `BennyCache::Related` mixin.
|
205
|
+
|
206
|
+
|
207
|
+
### Related Indexes
|
208
|
+
|
209
|
+
Related indexes are used when you know that a change to one model will need to invalidate
|
210
|
+
a data or method cache of another model. Defining a related index will make the cache invalidation automatic.
|
211
|
+
|
212
|
+
BennyCache::Related installs and after_save/after_destroy callback to clear the related data indexes of other models.
|
213
|
+
|
214
|
+
Defined the two classes like so, a class that uses BennyCache::Model with a data_index, and a class that
|
215
|
+
uses BennyCache::Related that defines a benny_related_index that points to the main class's date_index
|
216
|
+
|
217
|
+
class MainModel < ActiveRecord::Base
|
218
|
+
has_many :related_models
|
219
|
+
|
220
|
+
include BennyCache::Model
|
221
|
+
|
222
|
+
benny_data_index :my_related_items # data cache
|
223
|
+
benny_model_index :my_related_method # method cache
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
class RelatedModel < ActiveRecord::Base
|
228
|
+
belongs_to :main_model
|
229
|
+
|
230
|
+
include BennyCache::Related
|
231
|
+
benny_related_index ":main_model_id/MainModel/my_related_items"
|
232
|
+
benny_related_method ":main_model_id/MainModel/my_related_method"
|
233
|
+
|
234
|
+
end
|
235
|
+
|
236
|
+
The benny_related_index call sets up all RelatedModel instances to clear the `:my_related_items` data index of the
|
237
|
+
MainModel instance withe a primary key of `related_model.main_model_id` whenever the RelatedModel instance is created,
|
238
|
+
updated, or deleted.
|
239
|
+
|
240
|
+
The benny_related_method call sets up all RelatedModel instances to clear the `:my_related_method` method cache data of the
|
241
|
+
MainModel instance withe a primary key of `related_model.main_model_id` whenever the RelatedModel instance is created,
|
242
|
+
updated, or deleted.
|
243
|
+
|
244
|
+
### Cache namespacing
|
245
|
+
|
246
|
+
Internally, BennyCache uses the class name of the classes using BennyCache::Model as part of the key name for
|
247
|
+
caching. Sometimes that might not be what you want, and will need to explicitly declare the namespace. This happens
|
248
|
+
when you use classes that take advantage of ActiveRecord's single table inheritance:
|
249
|
+
|
250
|
+
|
251
|
+
class Location < ActiveRecord::Base
|
252
|
+
include BennyCache::Model
|
253
|
+
benny_cache_ns 'Location'
|
254
|
+
end
|
255
|
+
|
256
|
+
class IndustrialComplex < Location
|
257
|
+
|
258
|
+
end
|
259
|
+
|
260
|
+
class RecreationArea < Location
|
261
|
+
|
262
|
+
end
|
263
|
+
|
264
|
+
By declaring the above namespace, these two calls reference the same key:
|
265
|
+
|
266
|
+
l = Location.benny_cache_model 123
|
267
|
+
l = RecreationArea.benny_cache_model 123
|
268
|
+
|
269
|
+
|
270
|
+
## Bugs
|
271
|
+
|
272
|
+
There are probably bugs. Until there is an issue tracking system, send email to
|
273
|
+
`mshiltonj@gmail.com` and put 'BennyCache' in the subject.
|
274
|
+
|
275
|
+
## Contributing
|
276
|
+
|
277
|
+
1. Fork it
|
278
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
279
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
280
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
281
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require 'rspec'
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new('spec')
|
8
|
+
|
9
|
+
namespace :spec do
|
10
|
+
desc "Create rspec coverage"
|
11
|
+
task :coverage do
|
12
|
+
ENV['COVERAGE'] = 'true'
|
13
|
+
Rake::Task['spec'].execute
|
14
|
+
end
|
15
|
+
end
|
data/benny_cache.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/benny_cache/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Steven Hilton"]
|
6
|
+
gem.email = ["mshiltonj@gmail.com"]
|
7
|
+
gem.description = %q{A model caching library with indirect cached clearing}
|
8
|
+
gem.summary = %q{A model caching library with indirect cached clearing}
|
9
|
+
gem.homepage = "https://github.com/mshiltonj/benny_cache"
|
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 = "benny_cache"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = BennyCache::VERSION
|
17
|
+
|
18
|
+
|
19
|
+
gem.add_development_dependency('rspec')
|
20
|
+
gem.add_development_dependency('mocha')
|
21
|
+
gem.add_development_dependency('ZenTest')
|
22
|
+
gem.add_development_dependency('simplecov')
|
23
|
+
end
|
data/lib/benny_cache.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module BennyCache
|
2
|
+
module Base
|
3
|
+
def self.included(base) #:nodoc:
|
4
|
+
base.extend(BennyCache::Base::ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def benny_constantize(string) #:nodoc:
|
8
|
+
if string.respond_to?(:constantize)
|
9
|
+
# use ActiveSupport directly if possible
|
10
|
+
string.constantize
|
11
|
+
else
|
12
|
+
names = string.split('::')
|
13
|
+
names.shift if names.empty? || names.first.empty?
|
14
|
+
constant = Object
|
15
|
+
names.each do |name|
|
16
|
+
constant = constant.const_get(name)
|
17
|
+
end
|
18
|
+
constant
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
def benny_model_ns(ns)
|
26
|
+
self.class_variable_set(:@@BENNY_MODEL_NS, ns.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_benny_model_ns #:nodoc:
|
30
|
+
ns = self.class_variable_defined?(:@@BENNY_MODEL_NS) ? self.class_variable_get(:@@BENNY_MODEL_NS) : self.to_s
|
31
|
+
"Benny/#{ns}"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module BennyCache
|
2
|
+
class Cache
|
3
|
+
def initialize
|
4
|
+
@cache = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def fetch(key, options = nil, &block)
|
8
|
+
val = @cache[key]
|
9
|
+
|
10
|
+
if val.nil? && block_given?
|
11
|
+
val = block.call()
|
12
|
+
@cache[key] = val
|
13
|
+
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
val = val.dup unless val.nil?
|
17
|
+
rescue TypeError
|
18
|
+
#okay
|
19
|
+
end
|
20
|
+
val
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def read(key, options = nil)
|
25
|
+
val = @cache[key]
|
26
|
+
val = val.dup unless val.nil?
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def write(key, val, options = nil)
|
31
|
+
@cache[key] = val.dup
|
32
|
+
return true
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(key, options = nil)
|
36
|
+
@cache.delete(key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def clear(options = nil)
|
40
|
+
@cache = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|