kaching 0.0.3
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 +10 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +4 -0
- data/README.md +216 -0
- data/Rakefile +6 -0
- data/kaching.gemspec +28 -0
- data/lib/kaching.rb +13 -0
- data/lib/kaching/cache_counter.rb +89 -0
- data/lib/kaching/cache_counter_onmethod.rb +80 -0
- data/lib/kaching/cache_list.rb +194 -0
- data/lib/kaching/model_additions.rb +56 -0
- data/lib/kaching/railtie.rb +9 -0
- data/lib/kaching/storage_providers/memory.rb +42 -0
- data/lib/kaching/storage_providers/redis.rb +26 -0
- data/lib/kaching/version.rb +3 -0
- data/spec/lib/cache_counter_spec.rb +99 -0
- data/spec/lib/cache_list_spec.rb +135 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/support/db/connection.rb +4 -0
- data/spec/support/db/database.yml +6 -0
- data/spec/support/db/models.rb +69 -0
- data/spec/support/db/schema.rb +38 -0
- metadata +157 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
# kaching
|
2
|
+
|
3
|
+
[](http://travis-ci.org/elado/kaching)
|
4
|
+
|
5
|
+
Doesn't hit the DB for counters and existence of a many-to-many association.
|
6
|
+
|
7
|
+
## Quick Intro & Examples
|
8
|
+
|
9
|
+
**Q: How does it help me?**
|
10
|
+
|
11
|
+
A: The short answer -- Less DB hits. More, faster Redis hits.
|
12
|
+
|
13
|
+
### Countrs
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
# hits DB
|
17
|
+
author.articles.count
|
18
|
+
|
19
|
+
# first time hits DB and caches into Redis
|
20
|
+
author.articles_count
|
21
|
+
|
22
|
+
# triggers an after_commit that increases the counter on Redis, doesn't run a count query on the DB
|
23
|
+
author.articles.create!(title: "Hello")
|
24
|
+
|
25
|
+
# no DB hit. just reads from Redis
|
26
|
+
author.articles_count
|
27
|
+
```
|
28
|
+
|
29
|
+
### Lists
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
# writes to DB, updates Redis
|
33
|
+
user.add_like!(memento)
|
34
|
+
|
35
|
+
# no DB hit
|
36
|
+
user.likes_count
|
37
|
+
|
38
|
+
# no DB hit
|
39
|
+
user.has_like?(memento) # => true
|
40
|
+
# no DB hit
|
41
|
+
user.has_like?(inception) # => false
|
42
|
+
|
43
|
+
# writes to DB, updates Redis
|
44
|
+
user.add_like!(inception)
|
45
|
+
|
46
|
+
# no DB hit
|
47
|
+
user.has_like?(inception) # => true
|
48
|
+
|
49
|
+
# no DB hit
|
50
|
+
user.likes_count # => 2
|
51
|
+
|
52
|
+
# deletes from DB, updates Redis
|
53
|
+
user.remove_like(inception)
|
54
|
+
|
55
|
+
# no DB hit
|
56
|
+
user.has_like?(inception) # => false
|
57
|
+
|
58
|
+
# no DB hit
|
59
|
+
user.likes_count # => 1
|
60
|
+
```
|
61
|
+
|
62
|
+
|
63
|
+
## Installation
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
gem 'kaching'
|
67
|
+
```
|
68
|
+
|
69
|
+
Requires Ruby 1.9.2+.
|
70
|
+
|
71
|
+
## Storage options
|
72
|
+
|
73
|
+
### Redis
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
# config/initializers/redis.rb
|
77
|
+
$redis = Redis.new
|
78
|
+
|
79
|
+
Kaching::StorageProviders.Redis = $redis
|
80
|
+
```
|
81
|
+
|
82
|
+
## cache_counter
|
83
|
+
|
84
|
+
Cache counts of associations.
|
85
|
+
|
86
|
+
Adds a count method to a class, and adds an after_commit callback to the countable class.
|
87
|
+
|
88
|
+
### Usage
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
# article.rb
|
92
|
+
class Article < ActiveRecord::Base
|
93
|
+
belongs_to :user
|
94
|
+
end
|
95
|
+
|
96
|
+
# user.rb
|
97
|
+
class User < ActiveRecord::Base
|
98
|
+
has_many :articles
|
99
|
+
|
100
|
+
cache_counter :articles
|
101
|
+
end
|
102
|
+
|
103
|
+
# code
|
104
|
+
user = User.create!
|
105
|
+
|
106
|
+
user.articles_count # => 0
|
107
|
+
|
108
|
+
user.articles.create!(title: "Hello")
|
109
|
+
|
110
|
+
user.articles_count # => 1
|
111
|
+
```
|
112
|
+
|
113
|
+
`cache_counter` adds an after_commit to increase/decrease the counter directly on the data store.
|
114
|
+
|
115
|
+
|
116
|
+
#### More Options
|
117
|
+
|
118
|
+
Specify a `class_name` and provide a block to return a count for custom operations:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class User < ActiveRecord::Base
|
122
|
+
cache_counter :following_users, class_name: 'Follow' do |user|
|
123
|
+
Follow.where(user_id: user.id, item_type: 'User').count
|
124
|
+
end
|
125
|
+
|
126
|
+
cache_counter :follower_users, class_name: 'Follow', foreign_key: 'item_id' do |user|
|
127
|
+
Follow.where(item_id: user.id, item_type: 'User').count
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
## cache_list
|
133
|
+
|
134
|
+
Caches presence of items in a list that is belong to antoer item. Automatically adds `cache_counter` on that association.
|
135
|
+
|
136
|
+
Example: User can Like stuff. In order to check if a user likes an item, you can run a query like
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
Like.where(user_id: user.id, item_id: item.id, item_type: item.class.name).exists?
|
140
|
+
```
|
141
|
+
|
142
|
+
But with many items and checks, this process might take some precious time.
|
143
|
+
|
144
|
+
In order to solve that, `kaching` fetches once all liked items and stores them in cache.
|
145
|
+
|
146
|
+
`cache_list :likes` generates these methods:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
add_like!(item)
|
150
|
+
remove_like!(item)
|
151
|
+
has_like?(item)
|
152
|
+
likes_count
|
153
|
+
reset_list_cache_likes!
|
154
|
+
```
|
155
|
+
|
156
|
+
and from now on you can ask if `user.has_like?(item)`
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
# like.rb
|
160
|
+
class Like < ActiveRecord::Base
|
161
|
+
belongs_to :user
|
162
|
+
belongs_to :user, polymorphic: true
|
163
|
+
end
|
164
|
+
|
165
|
+
# movie.rb
|
166
|
+
class Movie < ActiveRecord::Base
|
167
|
+
end
|
168
|
+
|
169
|
+
# user.rb
|
170
|
+
class User < ActiveRecord::Base
|
171
|
+
has_many :likes
|
172
|
+
|
173
|
+
cache_list :likes
|
174
|
+
end
|
175
|
+
|
176
|
+
# code
|
177
|
+
user = User.create!
|
178
|
+
|
179
|
+
memento = Movie.create!(name: "memento")
|
180
|
+
inception = Movie.create!(name: "inception")
|
181
|
+
|
182
|
+
user.add_like!(memento) # add_like! is an auto generated method!
|
183
|
+
|
184
|
+
user.likes_count # => 1
|
185
|
+
|
186
|
+
user.has_like?(memento) # => true
|
187
|
+
user.has_like?(inception) # => false
|
188
|
+
|
189
|
+
user.add_like!(inception)
|
190
|
+
user.has_like?(inception) # => true
|
191
|
+
|
192
|
+
user.likes_count # => 2
|
193
|
+
|
194
|
+
user.remove_like(inception)
|
195
|
+
user.has_like?(inception) # => false
|
196
|
+
|
197
|
+
user.likes_count # => 1
|
198
|
+
```
|
199
|
+
|
200
|
+
The first time `has_like?` is called, it collects all IDs of likes items and stores them in cache.
|
201
|
+
|
202
|
+
The second time just asks the cache, and every creation/deletion of an item updates the cache.
|
203
|
+
|
204
|
+
|
205
|
+
### Options
|
206
|
+
|
207
|
+
|
208
|
+
```
|
209
|
+
item_key The item name on the list table (like 'movie' for 'movie_id')
|
210
|
+
polymorphic Whether list table is polymorphic (list table contains 'item_id' and 'item_type')
|
211
|
+
add_method_name Name of method to add. Can be customized, for example 'like!' instead of 'add_like!'
|
212
|
+
remove_method_name Name of method to remove. Can be customized, for example 'unlike!' instead of 'removes_like!'
|
213
|
+
exists_method_name Name of method to check if exists. Can be customized, for example 'likes?' instead of 'has_like?'
|
214
|
+
reset_list_cache_method_name Reset cache, after manual insertion
|
215
|
+
class_name Name of class of list table, in case it's different than default
|
216
|
+
```
|
data/Rakefile
ADDED
data/kaching.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "kaching/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "kaching"
|
7
|
+
s.version = Kaching::VERSION
|
8
|
+
s.authors = ["Elad Ossadon"]
|
9
|
+
s.email = ["elad@ossadon.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Cache counters and lists of Rails ActiveRecord in an external storage such as Redis.}
|
12
|
+
s.description = %q{Doesn't hit the DB for counters and existence of a many-to-many association.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "kaching"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# s.add_dependency 'rails', ">= 3.0.0"
|
22
|
+
s.add_dependency 'activerecord', ">= 3.0.0"
|
23
|
+
s.add_dependency 'redis'
|
24
|
+
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
s.add_development_dependency 'rspec'
|
27
|
+
s.add_development_dependency 'sqlite3'
|
28
|
+
end
|
data/lib/kaching.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'kaching/version'
|
2
|
+
require 'active_record'
|
3
|
+
require 'kaching/model_additions'
|
4
|
+
require 'kaching/storage_providers/memory'
|
5
|
+
require 'kaching/storage_providers/redis'
|
6
|
+
if defined? Rails
|
7
|
+
require 'kaching/railtie'
|
8
|
+
else
|
9
|
+
ActiveRecord::Base.send :include, Kaching::ModelAdditions
|
10
|
+
end
|
11
|
+
|
12
|
+
module Kaching
|
13
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Kaching
|
2
|
+
module CacheCounter
|
3
|
+
def cache_counter(attribute, options = {})
|
4
|
+
options = {}.merge(options || {})
|
5
|
+
|
6
|
+
container_class = self
|
7
|
+
|
8
|
+
count_method_name = options[:count_method_name] || "#{attribute.to_s}_count"
|
9
|
+
reset_count_method_name = "reset_count_cache_#{attribute.to_s}!"
|
10
|
+
|
11
|
+
Kaching.logger.info "DEFINE #{container_class.name}##{count_method_name}"
|
12
|
+
|
13
|
+
container_class.send :define_method, count_method_name do
|
14
|
+
value = Kaching.cache_store.fetch(kaching_key(attribute, :count)) {
|
15
|
+
value = (block_given? ? yield(self) : self.send(attribute))
|
16
|
+
value = value.count unless value.is_a?(Fixnum)
|
17
|
+
|
18
|
+
Kaching.logger.info "FIRST FETCH #{value}"
|
19
|
+
value
|
20
|
+
}.to_i
|
21
|
+
|
22
|
+
Kaching.logger.info "CALL #{container_class.name}##{count_method_name} = #{value}"
|
23
|
+
|
24
|
+
value
|
25
|
+
end
|
26
|
+
|
27
|
+
container_class.send :define_method, reset_count_method_name do
|
28
|
+
Kaching.cache_store.del(self.kaching_key(attribute, :count))
|
29
|
+
end
|
30
|
+
|
31
|
+
container_class.send(:after_commit) do
|
32
|
+
if self.destroyed?
|
33
|
+
Kaching.cache_store.del(self.kaching_key(attribute, :count))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
countable_class = options[:class_name].constantize if options[:class_name]
|
38
|
+
countable_class ||= attribute.to_s.singularize.classify.constantize
|
39
|
+
|
40
|
+
Kaching.logger.info "COUNTABLE DEFINE #{countable_class} after_commit"
|
41
|
+
|
42
|
+
foreign_key = Kaching._extract_foreign_key_from(attribute, options, container_class)
|
43
|
+
|
44
|
+
after_commit_method_name = "after_commit_#{count_method_name}"
|
45
|
+
countable_class.send(:define_method, after_commit_method_name) do |action|
|
46
|
+
begin
|
47
|
+
Kaching.logger.info "COUNTABLE #{countable_class.name} after_commit foreign_key = #{foreign_key}"
|
48
|
+
|
49
|
+
belongs_to_item = self.send(foreign_key)
|
50
|
+
|
51
|
+
return unless belongs_to_item
|
52
|
+
|
53
|
+
Kaching.logger.info " > COUNTABLE after_commit #{countable_class.name} belongs_to(#{foreign_key}) = #{belongs_to_item.class.name}##{belongs_to_item.id} | #{action.inspect}"
|
54
|
+
|
55
|
+
counter_key = belongs_to_item.kaching_key(attribute, :count)
|
56
|
+
|
57
|
+
if !Kaching.cache_store.exists(counter_key)
|
58
|
+
belongs_to_item.send(count_method_name) # get fresh count from db
|
59
|
+
else
|
60
|
+
case action
|
61
|
+
when :create
|
62
|
+
Kaching.logger.info " > COUNTABLE INCR #{counter_key}"
|
63
|
+
|
64
|
+
Kaching.cache_store.incr(counter_key)
|
65
|
+
when :destroy
|
66
|
+
Kaching.logger.info " > COUNTABLE DECR #{counter_key}"
|
67
|
+
|
68
|
+
Kaching.cache_store.decr(counter_key)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
rescue
|
72
|
+
puts $!.message
|
73
|
+
puts $!.backtrace.join("\n")
|
74
|
+
raise $!
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
countable_class.send(:after_commit) do
|
79
|
+
action = if self.send(:transaction_include_action?, :create)
|
80
|
+
:create
|
81
|
+
elsif self.destroyed?
|
82
|
+
:destroy
|
83
|
+
end
|
84
|
+
|
85
|
+
self.send(after_commit_method_name, action) if action
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Kaching
|
2
|
+
module CacheCounter
|
3
|
+
def cache_counter(attribute, options = {})
|
4
|
+
options = {}.merge(options || {})
|
5
|
+
|
6
|
+
container_class = self
|
7
|
+
|
8
|
+
count_method_name = options[:count_method_name] || "#{attribute.to_s}_count"
|
9
|
+
|
10
|
+
Kaching.logger.info "DEFINE #{container_class.name}##{count_method_name}"
|
11
|
+
|
12
|
+
container_class.send :define_method, count_method_name do
|
13
|
+
value = Kaching.cache_store.fetch(kaching_key(attribute, :count)) {
|
14
|
+
value = (block_given? ? yield(self) : self.send(attribute))
|
15
|
+
value = value.count unless value.is_a?(Fixnum)
|
16
|
+
|
17
|
+
Kaching.logger.info "FIRST FETCH #{value}"
|
18
|
+
value
|
19
|
+
}.to_i
|
20
|
+
|
21
|
+
Kaching.logger.info "CALL #{container_class.name}##{count_method_name} = #{value}"
|
22
|
+
|
23
|
+
value
|
24
|
+
end
|
25
|
+
|
26
|
+
container_class.send(:after_commit) do
|
27
|
+
if self.destroyed?
|
28
|
+
Kaching.cache_store.del(self.kaching_key(attribute, :count))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
countable_class = options[:class_name].constantize if options[:class_name]
|
33
|
+
countable_class ||= attribute.to_s.singularize.classify.constantize
|
34
|
+
|
35
|
+
Kaching.logger.info "COUNTABLE DEFINE #{countable_class} after_commit"
|
36
|
+
|
37
|
+
foreign_key = Kaching._extract_foreign_key_from(attribute, options, container_class)
|
38
|
+
|
39
|
+
countable_class.send(:after_create) do
|
40
|
+
Kaching.logger.info "COUNTABLE #{countable_class.name} after_create foreign_key = #{foreign_key}"
|
41
|
+
end
|
42
|
+
|
43
|
+
countable_class.send(:after_destroy) do
|
44
|
+
Kaching.logger.info "COUNTABLE #{countable_class.name} after_destroy foreign_key = #{foreign_key}"
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
countable_class.send(:after_commit) do
|
49
|
+
begin
|
50
|
+
Kaching.logger.info "COUNTABLE #{countable_class.name} after_commit foreign_key = #{foreign_key}"
|
51
|
+
|
52
|
+
belongs_to_item = self.send(foreign_key)
|
53
|
+
|
54
|
+
return unless belongs_to_item
|
55
|
+
|
56
|
+
created = self.send(:transaction_include_action?, :create)
|
57
|
+
destroyed = self.destroyed?
|
58
|
+
|
59
|
+
Kaching.logger.info " > COUNTABLE after_commit #{countable_class.name} belongs_to(#{foreign_key}) = #{belongs_to_item.class.name}##{belongs_to_item.id} | #{created ? 'created' : destroyed ? 'destroyed' : 'none'}"
|
60
|
+
|
61
|
+
counter_key = belongs_to_item.kaching_key(attribute, :count)
|
62
|
+
|
63
|
+
if created
|
64
|
+
Kaching.logger.info " > COUNTABLE INCR #{counter_key}"
|
65
|
+
|
66
|
+
Kaching.cache_store.incr(counter_key)
|
67
|
+
elsif destroyed
|
68
|
+
Kaching.logger.info " > COUNTABLE DECR #{counter_key}"
|
69
|
+
|
70
|
+
Kaching.cache_store.decr(counter_key)
|
71
|
+
end
|
72
|
+
rescue Exception => e
|
73
|
+
puts e.message
|
74
|
+
puts e.backtrace
|
75
|
+
raise
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|