kaching 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://secure.travis-ci.org/elado/kaching.png)](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
|