cachable 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 70e62e1e63223309b99070e0b8d17707a93e2a1e
4
+ data.tar.gz: a43bad698ed24dfe8a86b26d5ba4120cdd8f93ea
5
+ SHA512:
6
+ metadata.gz: 299c3e2bd420d884196ed55883a9e2d6b4c745df9fdf90d4bfca1c24ad14b728920ae2c427f49f861087fc9bee421d3579eda032d0ca082b1840a0c7d34c07b7
7
+ data.tar.gz: 789bf412bf8b7d4a4a0cddd74fc235febebd29045b05f033a846c854aa5e465f5a30760ea53eada73d6ec116e3193fbb612cf2f25e66e534a8d52b1b3d4652eb
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Kai Marshland
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # Cachable
2
+ Caching is often a bother. This gem allows you to simply wrap your code in a block and have it be cached with redis.
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'cachable'
9
+ ```
10
+
11
+ You must also add redis to your app. On heroku, you can add [Heroku Redis](https://elements.heroku.com/addons/heroku-redis).
12
+
13
+ ## Usage
14
+ At its hearty, this gem is the `unless_cached` method.
15
+ Although no options are required, it accepts the following:
16
+ - key. If present, will be added to the base key to generate the full key. Defaults to the name of the caller.
17
+ - json. If true, will serialize and deserialize the result as json
18
+ - expiration Time for which to cache the result. Defaults to 1 day
19
+ - json_options. Options that get passed into json serialization and deserialization
20
+ - skip_cache. If true, will not populate the cache -- this can be used if you populate the cache elsewhere, but still want to check if there's something there.
21
+
22
+ ### Examples
23
+ In your model:
24
+
25
+ ```ruby
26
+ include Cachable
27
+
28
+ def your_method
29
+ unless_cached do
30
+ # ... output of this will be cached under the key model_name_id_updated_at_your_method
31
+ end
32
+ end
33
+ ```
34
+
35
+ Of course, `unless_cached` accepts a variety of options:
36
+ ```ruby
37
+ # opts[:key]. If present, will be added to the base key to generate the full key. Defaults to the name of the caller.
38
+ unless_cached(key: 'some_key') do
39
+ # will be stored under the key model_name_id_some_key
40
+ end
41
+
42
+
43
+ # opts[:json]. If true, will serialize and deserialize the result as json
44
+ unless_cached(json: true) do
45
+ {
46
+ json: 'object'
47
+ }
48
+ end
49
+
50
+ # opts[:expiration]. Time for which to cache the result. Defaults to 1 day
51
+ unless_cached(expiration: 30.minutes) do
52
+ # result will expire in half an hour
53
+ end
54
+
55
+
56
+ # opts[:json_options]. Options that get passed into json serialization and deserialization
57
+ unless_cached(json: true, json_options: {allow_nan: true}) do
58
+ {
59
+ value: Float::NAN
60
+ }
61
+ end
62
+ ```
63
+
64
+ ## Other features
65
+
66
+ ### Configuring the redis connection
67
+ It defaults to checking the `REDIS_URL` and the `HEROKU_REDIS_URL` environment variables to make a connection to redis.
68
+ However, you may wish to change the way you connect to redis. Create an initializer in `config/initializers/cachable.rb`.
69
+ ```ruby
70
+ Cachable::configure do |config|
71
+
72
+ # set a lambda to get a redis connection
73
+ config.redis_connection = -> {
74
+ Redis.new
75
+ }
76
+
77
+ # set the redis instance directly
78
+ config.redis_instance = Redis.new
79
+
80
+ # or set the redis url
81
+ config.redis_url = 'YOUR REDIS URL'
82
+
83
+ end
84
+ ```
85
+
86
+ ### Deleting from the cache
87
+ Sometimes you want to delete something from the cache.
88
+ This can be done using the `delete_from_cache` method, which takes in one or more keys.
89
+ These keys are either the
90
+ ```ruby
91
+ delete_from_cache(:key1, :key2)
92
+ ```
93
+
94
+ A special use case is after the changes have been committed.
95
+ For this, you can use the special `purge_cache` callback.
96
+ You must define the `tracked_cache_keys` method for this to work properly.
97
+ The following example will clear the cache for `cached_a` and `cached_b`, but not `cached_c`.
98
+ ```ruby
99
+ # in your model
100
+ after_commit :purge_cache
101
+
102
+ def tracked_cache_keys
103
+ [:cached_a, :cached_b]
104
+ end
105
+
106
+ def cached_a
107
+ unless_cached do
108
+ # ...
109
+ end
110
+ end
111
+
112
+ def cached_b
113
+ unless_cached do
114
+ # ...
115
+ end
116
+ end
117
+
118
+ def cached_c
119
+ unless_cached do
120
+ # ...
121
+ end
122
+ end
123
+ ```
124
+
125
+ If you want to use the `delete_from_cache` method directly in an `after_commit` callback, you must specify that in the options.
126
+ ```ruby
127
+ delete_from_cache(:key1, :key2, after_commit: true)
128
+ ```
129
+
130
+ ### Batch requests to the cache
131
+ Oftentimes, you want the output of a given method on an ActiveRecordCollection.
132
+ You could loop through each record in the collection, but if you expect the method to be cached,
133
+ it's much more efficient this gem's method, and avoid the memory overhead and extra database calls.
134
+
135
+ Imagine you have a model that looks as follows:
136
+ ```ruby
137
+ class Book < ActiveRecord::Base
138
+ include Cachable
139
+
140
+ def summary
141
+ unless_cached do
142
+ 'Some summary'
143
+ end
144
+ end
145
+ end
146
+ ```
147
+ To get the summaries of all fantasy books, you could run `Book.where(genre: 'fantasy').unless_cached(:summary)`.
148
+
149
+ Note that this will remove any ordering on the collection unless you set the slurp option to true or include `EachInOrder` in your model (this gem has not yet been released -- create an issue if you need me to release it). A full list of options is as follows:
150
+ - slurp. If true will not pull the records in in batches, which increases memory overhead but preserves order.
151
+ - force_cache. If true will add its own cache, regardless of what the underlying function does.
152
+ - cache_batches. If true will add another layer of caching outside each individual model. This caches a the result of up to 50 records at a time, which can drastically speed up the cache but will also increase the cache size.
153
+ - skip_result. If true will not return the result, but it will still prepopulate the cache, which can avoid memory overhead.
154
+ - clear_previous_batch. If true, and was also true previous times, it will remove the cached batches for the previous batch. This can be very useful when you are frequently adding more records and want to keep the redis memory usage down. Note that this was designed to have minimal overhead, and so was based on the total number of records, which may cause a small number of stale keys to stay in memory.
155
+ - You can also pass in all normal unless_cached options, such as expiration, json, and json_options.
156
+
157
+
158
+ ### Adding an additional redis key
159
+ Sometimes your cache depends on more than just the model.
160
+ For example, imagine you have a model book that `belongs_to` an author.
161
+ When the author is updated, you want the cache to become invalidated.
162
+ In your book model, you might have something like this:
163
+ ```ruby
164
+ def added_redis_key
165
+ self.author.updated_at.to_i
166
+ end
167
+
168
+ def self.added_redis_key
169
+ first = all.first
170
+ return '' if first.blank?
171
+
172
+ first.author.updated_at.to_i
173
+ end
174
+
175
+ ```
176
+
177
+ ### Using the cache outside of a specific instance
178
+ Sometimes you need to cache something in a static (class) method. It accepts the following options:
179
+ - json. If true, will serialize and deserialize the result as json
180
+ - expiration Time for which to cache the result. Defaults to 1 day
181
+ - json_options. Options that get passed into json serialization and deserialization
182
+ - skip_cache. If true, will not populate the cache -- this can be used if you populate the cache elsewhere, but still want to check if there's something there.
183
+ ```ruby
184
+ self.class.unless_cached_base("#{self.class.to_s.downcase}_key", json: true, expiration: 1.day) do
185
+ # this output will be cached under the key your_model_name_key
186
+ end
187
+ ```
188
+
189
+ ## License
190
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Cachable'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
data/lib/cachable.rb ADDED
@@ -0,0 +1,165 @@
1
+ require 'cachable/configuration'
2
+
3
+ module Cachable
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+
9
+ after_commit :purge_cache #try to purge the cache after anything changes
10
+
11
+ #Generates the basic redis key from id, updated_at, and whatever else we want
12
+ def base_redis_key(opts={})
13
+ added = ''
14
+ added = "_#{self.added_redis_key}" if self.respond_to? :added_redis_key
15
+
16
+ on_previous_changes = opts[:after_commit] && self.previous_changes['updated_at']
17
+ updated_at = on_previous_changes ? self.previous_changes['updated_at'].first : self.updated_at
18
+
19
+ "#{self.class.to_s.downcase}_#{self.id}_#{updated_at.to_i}#{added}"
20
+ end
21
+
22
+ #Clears the given keys from redis
23
+ def delete_from_cache(*keys, **opts)
24
+ base_key = self.base_redis_key opts
25
+ keys.each do |key|
26
+ Cachable::redis.del "#{base_key}_#{key}"
27
+ end
28
+ end
29
+
30
+ # Purges the cache for this record, if the client has defined the purge_cache method
31
+ def purge_cache
32
+ return unless self.respond_to? :tracked_cache_keys
33
+
34
+ self.delete_from_cache(*self.tracked_cache_keys, after_commit: true)
35
+ end
36
+
37
+ # If the is present in the cache, returns that; otherwise generates and caches the result
38
+ # opts[:key]. If present, will be added to the base key to generate the full key. Defaults to the name of the caller.
39
+ # opts[:json]. If true, will serialize and deserialize the result as json
40
+ # opts[:expiration]. Time for which to cache the result. Defaults to 1 day
41
+ # opts[:json_options]. Options that get passed into json serialization and deserialization
42
+ def unless_cached(opts={})
43
+ partial_key = opts[:key].present? ? opts[:key] : caller.first.match(/`[^']*/).to_s[1..-1]
44
+ key = "#{self.base_redis_key}_#{partial_key}"
45
+
46
+ self.class.unless_cached_base(key, opts) do
47
+ yield
48
+ end
49
+ end
50
+
51
+ # Calls unless cached on all records passed
52
+ # action
53
+ # opts[:slurp]. If true will not pull the records in in batches, which increases memory overhead but preserves order.
54
+ # opts[:force_cache]. If true will add its own cache, regardless of what the underlying function does
55
+ # opts[:cache_batches]. If true will add another layer of caching outside
56
+ # opts[:skip_result]. If true will not generate (useful for prepopulating the cache with lower overhead)
57
+ # opts[clear_previous_batch] If true, and was also true previous times, it will remove the cached batches for the previous batch. This can be very useful when you are frequently adding more records and want to keep the redis memory usage down.
58
+ # Can also pass in all normal unless_cached options
59
+ def self.unless_cached(action, opts={})
60
+ result = []
61
+
62
+ iterator = all.find_in_batches
63
+ iterator = all.each_in_order_in_batches if self.respond_to? :each_in_order_in_batches
64
+ iterator = all.each if opts[:slurp]
65
+
66
+ partial_key = opts[:key].present? ? opts[:key] : action
67
+ added_key = ''
68
+ added_key = "_#{self.added_redis_key}" if self.respond_to? :added_redis_key
69
+
70
+ batch_key_list = []
71
+ record_count = 0
72
+
73
+ opts[:skip_cache] = !opts[:force_cache]
74
+
75
+ iterator.each do |batch|
76
+ factors = batch.pluck(:id, :updated_at)
77
+ record_count += factors.length if opts[:clear_previous_batch]
78
+
79
+ if opts[:cache_batches]
80
+ batch_key = "#{self.to_s.downcase}_#{factors.flatten.join(',')}#{added_key}_#{partial_key}"
81
+ batch_key_list << batch_key if opts[:clear_previous_batch]
82
+
83
+ existing_result = Cachable::redis.get batch_key
84
+ if existing_result.present?
85
+ result.concat JSON(existing_result)
86
+ next
87
+ end
88
+ end
89
+
90
+ batch_result = factors.map do |id, updated_at|
91
+ key = "#{self.to_s.downcase}_#{id}_#{updated_at.to_i}#{added_key}_#{partial_key}"
92
+
93
+ self.unless_cached_base(key, opts) do
94
+ record = self.unscope(:order, :where, :offset).find id
95
+
96
+ block_given? ? (yield record) : record.send(action) if record.present?
97
+ end
98
+ end
99
+
100
+ result.concat batch_result unless opts[:skip_result]
101
+
102
+ if opts[:cache_batches]
103
+ Cachable::redis.set batch_key, JSON(batch_result)
104
+ expiration = opts[:expiration]
105
+ expiration = 15.minutes unless expiration.present?
106
+ Cachable::redis.expire batch_key, expiration unless expiration === false
107
+ end
108
+ end
109
+
110
+ if opts[:clear_previous_batch]
111
+
112
+ # store the keys for the current batch list
113
+ expiration = opts[:expiration]
114
+ expiration = 15.minutes unless expiration.present?
115
+
116
+ gen_key -> n {
117
+ "#{self.to_s.downcase}_keys_#{n}_#{added_key}_#{partial_key}"
118
+ }
119
+ batch_key = gen_key[record_count]
120
+
121
+ Cachable::redis.set(batch_key, JSON(batch_key_list))
122
+ Cachable::redis.expire(batch_key, expiration)
123
+
124
+ # delete the keys from the previous batch list
125
+ Cachable::redis.del(gen_key[record_count - 1])
126
+ Cachable::redis.del(gen_key[record_count + 1])
127
+ end
128
+
129
+ result
130
+ end
131
+
132
+ # Core implementation of unless cached.
133
+ # As well as options from above:
134
+ # opts[:skip_cache]. If true, will not populate the cache
135
+ def self.unless_cached_base(key, opts={})
136
+ opts[:json] = true if opts[:json].nil?
137
+
138
+ cached = Cachable::redis.get(key)
139
+ if cached.present?
140
+ cached = JSON.parse(cached, opts[:json_options]) if opts[:json]
141
+
142
+ return cached
143
+ end
144
+
145
+ result = yield
146
+
147
+ unless opts[:skip_cache]
148
+ if opts[:json]
149
+ Cachable::redis.set(key, JSON.generate(result, opts[:json_options]))
150
+ else
151
+ Cachable::redis.set(key, result)
152
+ end
153
+
154
+ expiration = opts[:expiration]
155
+ expiration = 1.day unless expiration.present?
156
+ Cachable::redis.expire(key, expiration) unless expiration === false
157
+ end
158
+
159
+ result
160
+
161
+ end
162
+
163
+ end
164
+
165
+ end
@@ -0,0 +1,31 @@
1
+
2
+ module Cachable
3
+ class Configuration
4
+ attr_accessor :redis_connection, :redis_instance, :redis_url
5
+
6
+ def redis
7
+ return @redis_connection.call if @redis_connection.present?
8
+ return @redis_instance if @redis_instance.present?
9
+
10
+
11
+ @redis_url = ENV['REDIS_URL'] || ENV['HEROKU_REDIS_URL']
12
+ raise 'No redis url provided' if @redis_url.blank?
13
+
14
+ uri = URI.parse(@redis_url)
15
+ @redis_instance = Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
16
+ end
17
+ end
18
+
19
+ def self.configuration
20
+ @config ||= Configuration.new
21
+ end
22
+
23
+ def self.configure
24
+ yield self.configuration
25
+ end
26
+
27
+ def self.redis
28
+ self.configuration.redis
29
+ end
30
+
31
+ end
@@ -0,0 +1,3 @@
1
+ module Cachable
2
+ VERSION = '0.5.3'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :cachable do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cachable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.3
5
+ platform: ruby
6
+ authors:
7
+ - Kai Marshland
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Easily add caching to models
56
+ email:
57
+ - kaimarshland@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - lib/cachable.rb
66
+ - lib/cachable/configuration.rb
67
+ - lib/cachable/version.rb
68
+ - lib/tasks/cachable_tasks.rake
69
+ homepage: https://github.com/KMarshland/cachable
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.5.1
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Easily add caching to models
93
+ test_files: []