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.
@@ -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