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.
@@ -0,0 +1,19 @@
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
18
+ .idea
19
+ benny_cache.iml
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in benny_cache.gemspec
4
+ gemspec
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.
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ require "benny_cache/version"
2
+ require "benny_cache/cache"
3
+ require "benny_cache/config"
4
+ require "benny_cache/base"
5
+ require "benny_cache/model"
6
+ require "benny_cache/related"
7
+
@@ -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