kaching 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ *.gem
2
+ .bundle
3
+ .rvmrc
4
+ Gemfile.lock
5
+ gemfiles/*.lock
6
+ pkg/*
7
+ *.rbc
8
+ tmp/*
9
+ .DS_Store
10
+ test.sqlite3
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=documentation
@@ -0,0 +1,13 @@
1
+ ## v0.0.3
2
+
3
+ * Rename to "kaching", gem release
4
+ * Bug fixes
5
+ * Better specs
6
+
7
+ ## v0.0.2
8
+
9
+ * Cache list
10
+
11
+ ## v0.0.1
12
+
13
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in kaching.gemspec
4
+ gemspec
@@ -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
+ ```
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -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
@@ -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