You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cacheable is a gem which intends to add method caching in an [aspect-oriented programming (AOP)](https://en.wikipedia.org/wiki/Aspect-oriented_programming)fashion in Ruby. Its core goals are:
5
+
Cacheable is a gem which adds method caching in Ruby following an [aspect-oriented programming (AOP)](https://en.wikipedia.org/wiki/Aspect-oriented_programming)paradigm. Its core goals are:
6
6
7
7
* ease of use (method annotation)
8
8
* flexibility (simple adaptability for any cache backend)
9
9
* portability (plain Ruby for use with any framework)
10
10
11
-
While Rails is not a requirement, Cacheable was built inside a mature Rails app and later extracted. This first release will seamlessly work in Rails and only includes an adapter for an in-memory cache backed by a simple Hash. This may be enough for your needs but it is more likely that additional cache adapters will need to be written.
11
+
While using Ruby on Rails is not a requirement, Cacheable was built inside a mature Rails app and later extracted. The current release is designed for drop-in support in Rails, and includes an adapter for an in-memory cache backed by a simple hash. This may be enough for your needs, but it's more likely that additional cache adapters will need to be written for other projects.
12
12
13
13
See more about [Cache Adapters](cache-adapters.md).
Cacheable is designed to work seamlessly with your already existing codebase. Consider the following contrived class:
35
+
Cacheable is designed to work seamlessly with your already existing codebase. Consider the following example where we fetch the star count for Cacheable from GitHub's API. Feel free to copy/paste it into your IRB console or use the code in `examples/simple_example.rb`.
**That's it!** There's some complex Ruby magic going on under the hood but to the end user you can simply call `expensive_calculation` and the result will be retrieved from the cache, if available, or generated and placed into the cache. To confirm it is working, fire up an IRB console try the following:
75
+
**That's it!** There's some complex Ruby magic going on under the hood but to the end user you can simply call `star_count` and the result will be retrieved from the cache, if available, or fetched from the network and placed into the cache. To confirm it is working, fire up an IRB console try the following:
66
76
67
77
```irb
68
-
> s = SimpleExample.new
69
-
> s.expensive_calculation
70
-
beginning expensive method
71
-
=> "my_result"
72
-
> s.expensive_calculation
73
-
=> "my_result"
78
+
> a = GitHubApiAdapter.new
79
+
> a.star_count
80
+
Fetching data from GitHub
81
+
=> 2
82
+
> a.star_count
83
+
=> 2
74
84
75
85
# Notice that the `puts` was not output the 2nd time the method was invoked.
86
+
# The network call and result parsing would also not be performed again.
76
87
```
77
88
78
89
### Additional Methods
@@ -84,97 +95,153 @@ Cacheable also adds two useful methods to your class.
84
95
The cache can intentionally be skipped by appending `_without_cache` to the method name. This invocation will neither check the cache nor populate it. It is as if you called the original method and never used Cacheable.
85
96
86
97
```irb
87
-
> s = SimpleExample.new
88
-
> s.expensive_calculation_without_cache
89
-
beginning expensive method
90
-
=> "my_result"
91
-
> s.expensive_calculation_without_cache
92
-
beginning expensive method
93
-
=> "my_result"
94
-
```
98
+
> a = GitHubApiAdapter.new
99
+
> a.star_count
100
+
Fetching data from GitHub
101
+
=> 2
102
+
> a.star_count_without_cache
103
+
Fetching data from GitHub
104
+
=> 2
105
+
> a.star_count
106
+
=> 2
107
+
```
95
108
96
109
#### Remove the Value via `clear_#{method}_cache`
97
110
98
111
The cached value can be cleared at any time by calling `clear_#{your_method_name}_cache`.
99
112
100
113
```irb
101
-
> s = SimpleExample.new
102
-
> s.expensive_calculation
103
-
beginning expensive method
104
-
=> "my_result"
105
-
> s.expensive_calculation
106
-
=> "my_result"
107
-
108
-
> s.clear_expensive_calculation_cache
114
+
> a = GitHubApiAdapter.new
115
+
> a.star_count
116
+
Fetching data from GitHub
117
+
=> 2
118
+
> a.star_count
119
+
=> 2
120
+
121
+
> a.clear_star_count_cache
109
122
=> true
110
-
> s.expensive_calculation
111
-
beginning expensive method
112
-
=> "my_result"
123
+
> a.star_count
124
+
Fetching data from GitHub
125
+
=> 2
113
126
```
114
127
115
128
## Additional Configuration
116
129
117
-
### Cache Invalidation
130
+
### Cache Keys
118
131
119
132
#### Default
120
133
121
-
One of the hardest things to do correctly is cache invalidation. Cacheable handles this in a variety of ways. By default Cacheable will construct key a key in the format `[cache_key || class_name, method_name]`.
134
+
By default, Cacheable will construct key a key in the format `[cache_key || class_name, method_name]` without using method arguments.
122
135
123
136
If the object responds to `cache_key` its return value will be the first element in the array. `ActiveRecord` provides [`cache_key`](https://api.rubyonrails.org/classes/ActiveRecord/Integration.html#method-i-cache_key) but it can be added to any Ruby object or overwritten. If the object does not respond to it, the name of the class will be used instead. The second element will be the name of the method as a symbol.
124
137
125
138
It is up to the cache adapter what to do with this array. For example, Rails will turn `[SomeClass, :some_method]` into `"SomeClass/some_method"`. For more information see the documentation on [Cache Adapters](cache-adapters.md)
126
139
127
140
#### Set Your Own
128
141
129
-
If (re)defining `cache_key` does not provide enough flexibility you can pass a proc to the `key_format:` option of `cacheable`.
142
+
If (re)defining `cache_key` does not provide enough flexibility, you can pass a proc to the `key_format:` option of `cacheable`.
130
143
131
144
```ruby
132
-
classCustomKeyExample
145
+
# From examples/custom_key_example.rb
146
+
147
+
require'cacheable'
148
+
require'json'
149
+
require'net/http'
150
+
151
+
classGitHubApiAdapter
133
152
includeCacheable
134
153
135
-
cacheable :my_method, key_format:-> (target, method_name, method_args) do
*`target` is the object the method is being called on (`#<CustomKeyExample:0x0…0>`)
147
-
*`method_name` is the name of the method being cached (`:my_method`)
148
-
*`method_args` is an array of arguments being passed to the method (`[arg1]`)
167
+
*`target` is the object the method is being called on (`#<GitHubApiAdapter:0x0…0>`)
168
+
*`method_name` is the name of the method being cached (`:star_count`)
169
+
*`method_args` is an array of arguments being passed to the method (`[params]`)
149
170
150
-
So if we called `CustomKeyExample.new.my_method(123)`we would get the cache key
171
+
Including the method argument(s) allows you to cache different calls to the same method. Without the arguments in the cache key, a call to `star_count('cacheable')` would populate the cache and `star_count('tokenautocomplete')` would return the number of stars for Cacheable instead of what you want.
151
172
152
-
`"my_method called on #<CustomKeyExample:0x0…0> with Integer::123"`.
173
+
In addition, we're including the current date in the cache key so calling this method tomorrow will return an updated value.
153
174
154
-
### Conditional Caching
175
+
```irb
176
+
> a = GitHubApiAdapter.new
177
+
> a.star_count('cacheable')
178
+
Fetching data from GitHub for cacheable
179
+
=> 2
180
+
> a.star_count('cacheable')
181
+
=> 2
182
+
> a.star_count('tokenautocomplete')
183
+
Fetching data from GitHub for tokenautocomplete
184
+
=> 1142
185
+
> a.star_count('tokenautocomplete')
186
+
=> 1142
187
+
188
+
# In this example the follow cache keys are generated:
You can control if a method should be cached by supplying a proc to the `unless:` option which will get the same arguments as `key_format:`. Alternatively this method can be defined on the class and a symbol of the name of the method can be passed. **Note**: When using a symbol, the first argument will not be passed but will be available in the method as `self`. The following example will not cache the value if the first argument to the method is `false`.
193
+
### Conditional Caching
157
194
195
+
You can control if a method should be cached by supplying a proc to the `unless:` option which will get the same arguments as `key_format:`. This logic can be defined in a method on the class and the name of the method as a symbol can be passed as well. **Note**: When using a symbol, the first argument, `target`, will not be passed but will be available as `self`.
158
196
159
197
```ruby
160
-
classConditionalCachingExample
198
+
# From examples/conditional_example.rb
199
+
200
+
require'cacheable'
201
+
require'json'
202
+
require'net/http'
203
+
204
+
classGitHubApiAdapter
161
205
includeCacheable
162
206
163
-
cacheable :maybe_cache, unless::should_not_cache?
207
+
cacheable :star_count, unless::growing_fast?, key_format:-> (target, method_name, method_args) do
Cacheable is new so we don't want to cache the number of stars it has as we expect it to change quickly.
225
+
226
+
```irb
227
+
> a = GitHubApiAdapter.new
228
+
> a.star_count('tokenautocomplete')
229
+
Fetching data from GitHub for tokenautocomplete
230
+
=> 1142
231
+
a.star_count('tokenautocomplete')
232
+
=> 1142
233
+
234
+
> a.star_count('cacheable')
235
+
Fetching data from GitHub for cacheable
236
+
=> 2
237
+
> a.star_count('cacheable')
238
+
Fetching data from GitHub for cacheable
239
+
=> 2
240
+
```
241
+
175
242
### Cache Options
176
243
177
-
If your cache backend supports options you can pass them as the `cache_options:` option. This will be passed though untouched to the cache's `fetch` method.
244
+
If your cache backend supports options, you can pass them as the `cache_options:` option. This will be passed through untouched to the cache's `fetch` method.
You can cache class methods just as easily as a Ruby class is just an instance of `Class`. You simply need to `include Cacheable` within the `class << self` block. Methods can be defined in this block or outside using the `def self.` syntax.
261
+
You can cache static (class) methods as well by including Cacheable in your class' [eigenclass](https://en.wikipedia.org/wiki/Metaclass#In_Ruby). This is because all Ruby classes are instances of the `Class` class. Understanding how Ruby's class structure works is powerful and useful, however, further explanation is beyond the scope of this README and not necessary to proceed.
262
+
263
+
Simply put `include Cacheable` and the `cacheable` directive within a `class << self` block as in the example below. The methods you want to cache can be defined in this block or outside using the `def self.#{method_name}` syntax.
- Q: How does Cacheable handle cache invalidation?
315
+
- A: Cacheable takes Rails' cue and sidesteps the difficult problem of cache invalidation in favor of [key-based expiration](https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works). As DHH mentions in the blog post, `ActiveRecord`'s `cache_key` uses the `updated_at` timestamp so the cache is recalculated as the object changes. This results in new cache values being calculated, and your cache implementation can be configured to expire least recently used (LRU) values. In other applications, care must be taken to include a mechanism of key-based expiration in the `cache_key` method or [`key_format` proc](#set-your-own) or you risk serving stale data. Alternatively the generated [cache clearing](#remove-the-value-via-clear_method_cache) method can be used to explicitly invalidate the cache.
0 commit comments