benny_cache 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 +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
|