kaching 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,194 @@
1
+ module Kaching
2
+ module CacheList
3
+ def cache_list(attribute, options = {})
4
+ container_class = self
5
+
6
+ if block_given?
7
+ self.cache_counter attribute, options do |item|
8
+ yield(self)
9
+ end
10
+ else
11
+ self.cache_counter attribute, options
12
+ end
13
+
14
+ options = {
15
+ method_verb: nil,
16
+ list_method_name: attribute.to_s,
17
+ item_key: :item,
18
+ polymorphic: false,
19
+ add_alter_methods: true,
20
+ reset_cache_method_name: "reset_list_cache_#{attribute}!",
21
+ reset_count_cache_method_name: "reset_count_cache_#{attribute}!"
22
+ }.merge(options || {})
23
+
24
+ if options[:method_verb]
25
+ options = {
26
+ add_method_name: "#{options[:method_verb]}!",
27
+ remove_method_name: "un#{options[:method_verb]}!",
28
+ exists_method_name: "#{options[:method_verb]}s?",
29
+ toggle_method_name: "toggle_#{options[:method_verb]}!",
30
+ }.merge(options)
31
+ else
32
+ singularized_attribute = attribute.to_s.singularize
33
+ options = {
34
+ add_method_name: "add_#{singularized_attribute}!",
35
+ remove_method_name: "remove_#{singularized_attribute}!",
36
+ exists_method_name: "has_#{singularized_attribute}?",
37
+ toggle_method_name: "toggle_#{singularized_attribute}!",
38
+ }.merge(options)
39
+ end
40
+
41
+ Kaching.logger.info { "LIST NEW #{self.name} #{options}" }
42
+
43
+ list_class = options[:class_name].constantize if options[:class_name]
44
+ list_class ||= attribute.to_s.singularize.classify.constantize
45
+
46
+ item_key_id = "#{options[:item_key]}_id"
47
+
48
+ if options[:polymorphic]
49
+ item_key_type = "#{options[:item_key]}_type"
50
+ end
51
+
52
+ if options[:add_alter_methods]
53
+ container_class.send :define_method, options[:add_method_name] do |*args|
54
+ item, values = args
55
+ Kaching.logger.info { "LIST ADD #{self.class.name}##{self.id} #{item.class.name}##{item.id}" }
56
+
57
+ values = values ? values.dup : {}
58
+
59
+ values.merge!(options[:item_key] => item)
60
+ values.merge!(options[:create_options]) if options[:create_options]
61
+
62
+ return nil if self.send(options[:exists_method_name], item)
63
+
64
+ list_item = list_class.new(values)
65
+ self.send(options[:list_method_name]) << list_item
66
+ list_item
67
+ end
68
+
69
+ container_class.send :define_method, options[:remove_method_name] do |item|
70
+ Kaching.logger.info { "LIST REMOVE #{self.class.name}##{self.id} #{item.class.name}##{item.id}" }
71
+
72
+ where = { item_key_id => item.id }
73
+
74
+ if options[:polymorphic]
75
+ where[item_key_type] = item.class.name
76
+ end
77
+
78
+ self.send(options[:list_method_name]).where(where).destroy_all
79
+
80
+ nil
81
+ end
82
+
83
+ container_class.send(:define_method, options[:toggle_method_name]) do |*args|
84
+ item, state = args
85
+
86
+ return unless item
87
+
88
+ if state.nil?
89
+ state = !self.send(options[:exists_method_name], item)
90
+ end
91
+
92
+ if state
93
+ self.send(options[:add_method_name], item)
94
+ else
95
+ self.send(options[:remove_method_name], item)
96
+ end
97
+ end
98
+ end
99
+
100
+ container_class.send(:define_method, options[:exists_method_name]) do |item|
101
+ return false if item == self
102
+
103
+ hash_key = self.kaching_key(attribute, :list)
104
+
105
+ if Kaching.cache_store.hexists(hash_key, "created")
106
+ Kaching.logger.info { "USING CREATED #{hash_key}" }
107
+ Kaching.cache_store.hexists hash_key, item.id.to_s
108
+ else
109
+ Kaching.logger.info { "NOT CREATED - CREATING #{hash_key}" }
110
+
111
+ # fetch all ids
112
+ query = block_given? ? yield(self) : self.send(options[:list_method_name])
113
+ all_ids = container_class.connection.select_values(query.select(item_key_id).to_sql)
114
+
115
+ Kaching.logger.info { "FETCH all_ids = #{all_ids.length} | #{all_ids}" }
116
+
117
+ no_ids = all_ids.empty?
118
+
119
+ ids_with_values_for_hash = all_ids.map { |id|
120
+ [ id, 1 ]
121
+ }.flatten
122
+
123
+ ids_with_values_for_hash << "created" << 1
124
+
125
+ # store them with "created" key. "created" means that hash was created and it's not only empty.
126
+ Kaching.cache_store.send :hmset, *[hash_key, ids_with_values_for_hash].flatten
127
+
128
+ if no_ids
129
+ false
130
+ else
131
+ all_ids.include?(item.id)
132
+ end
133
+ end
134
+ end
135
+
136
+ container_class.send :define_method, options[:reset_cache_method_name] do
137
+ Kaching.cache_store.del(self.kaching_key(attribute, :list))
138
+
139
+ self.send(options[:reset_count_cache_method_name]) if self.respond_to?(options[:reset_count_cache_method_name])
140
+ end
141
+
142
+ container_class.send(:after_commit) do
143
+ if self.destroyed?
144
+ Kaching.cache_store.del(self.kaching_key(attribute, :list))
145
+ end
146
+ end
147
+
148
+ foreign_key = Kaching._extract_foreign_key_from(attribute, options, container_class)
149
+
150
+ after_commit_method_name = "after_commit_list_#{attribute}"
151
+ list_class.send(:define_method, after_commit_method_name) do |action|
152
+ begin
153
+ Kaching.logger.info { "LIST #{list_class.name} after_commit foreign_key = #{foreign_key}" }
154
+
155
+ belongs_to_item = self.send(foreign_key)
156
+
157
+ return unless belongs_to_item
158
+
159
+ hash_key = belongs_to_item.kaching_key(attribute, :list)
160
+
161
+ # don't bother managing the list because it's not created yet. let the has_item?() build it with all items, and then on creation it'll update the list
162
+ return unless Kaching.cache_store.hexists(hash_key, "created")
163
+
164
+ Kaching.logger.info { " > LIST after_commit #{list_class.name} belongs_to(#{foreign_key}) = #{belongs_to_item.class.name}##{belongs_to_item.id} #{action.inspect}" }
165
+
166
+ case action
167
+ when :create
168
+ Kaching.logger.info { " > LIST SET #{hash_key} #{self.class.name}##{self.id} type = #{Kaching.cache_store.type(hash_key)}" }
169
+
170
+ Kaching.cache_store.hset(hash_key, self.send(item_key_id), 1)
171
+ when :destroy
172
+ Kaching.logger.info { " > LIST DEL #{hash_key}" }
173
+
174
+ Kaching.cache_store.hdel(hash_key, self.send(item_key_id))
175
+ end
176
+ rescue
177
+ puts $!.message
178
+ puts $!.backtrace.join("\n")
179
+ raise $!
180
+ end
181
+ end
182
+
183
+ list_class.send(:after_commit) do
184
+ action = if self.send(:transaction_include_action?, :create)
185
+ :create
186
+ elsif self.destroyed?
187
+ :destroy
188
+ end
189
+
190
+ self.send(after_commit_method_name, action) if action
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,56 @@
1
+ require 'logger'
2
+ require 'kaching/cache_counter'
3
+ require 'kaching/cache_list'
4
+
5
+ module Kaching
6
+ # def self.attributes
7
+ # @attributes ||= []
8
+ # end
9
+
10
+ def self.cache_store
11
+ Kaching::StorageProviders.Redis
12
+ end
13
+
14
+ def self.logger
15
+ @logger ||= begin
16
+ logger = Logger.new(ENV['ATTRIBUTE_CACHE_LOG'] ? $stdout : '/dev/null')
17
+ logger.level = Logger::INFO
18
+ logger
19
+ end
20
+ end
21
+
22
+ def self._extract_foreign_key_from(attribute, options, container_class)
23
+ foreign_key = options[:foreign_key]
24
+
25
+ if !foreign_key
26
+ reflection = container_class.reflections[attribute]
27
+ foreign_key = reflection.options[:foreign_key] if reflection
28
+ end
29
+
30
+ foreign_key = if foreign_key
31
+ foreign_key.gsub(/_id$/, "")
32
+ else
33
+ container_class.name.underscore.singularize
34
+ end
35
+
36
+ foreign_key
37
+ end
38
+
39
+ module ModelAdditions
40
+ module ClassMethods
41
+ include CacheCounter
42
+ include CacheList
43
+ end
44
+
45
+ module InstanceMethods
46
+ def kaching_key(attribute, type)
47
+ "Kaching::#{type}::#{self.class.name.underscore.singularize}::#{self.id}::#{attribute}"
48
+ end
49
+ end
50
+
51
+ def self.included(receiver)
52
+ receiver.extend ClassMethods
53
+ receiver.send :include, InstanceMethods
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,9 @@
1
+ module Kaching
2
+ class Railtie < Rails::Railtie
3
+ initializer 'attrubute_cache.model_additions' do
4
+ ActiveSupport.on_load :active_record do
5
+ include ModelAdditions
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ module Kaching
2
+ module StorageProviders
3
+ class Memory
4
+ class << self
5
+ def data
6
+ @data ||= {}
7
+ end
8
+
9
+ def get(key)
10
+ Kaching.logger.info "CACHE read #{key} = #{data[key]}"
11
+ data[key]
12
+ end
13
+
14
+ def set(key, value)
15
+ Kaching.logger.info "CACHE write #{key} = #{value}"
16
+ data[key] = value
17
+ end
18
+
19
+ def del(key)
20
+ data.delete(key)
21
+ end
22
+
23
+ def fetch(key, &block)
24
+ Kaching.logger.info "CACHE fetch #{key} = #{data[key].inspect}"
25
+ get(key) || set(key, block.call)
26
+ end
27
+
28
+ def incr(key)
29
+ data[key] ||= 0
30
+ Kaching.logger.info "CACHE inc #{key} = #{data[key] + 1}"
31
+ data[key] += 1
32
+ end
33
+
34
+ def decr(key)
35
+ data[key] ||= 0
36
+ Kaching.logger.info "CACHE dec #{key} = #{data[key] - 1}"
37
+ data[key] -= 1
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
1
+ require 'redis'
2
+
3
+ module Kaching
4
+ module StorageProviders
5
+ def self.Redis
6
+ if @redis && !@redis.respond_to?(:fetch)
7
+ def @redis.fetch(key, &block)
8
+ value = get(key)
9
+
10
+ if !value
11
+ value = block.call
12
+ set(key, value)
13
+ end
14
+
15
+ value
16
+ end
17
+ end
18
+
19
+ @redis
20
+ end
21
+
22
+ def self.Redis=(adapter)
23
+ @redis = adapter
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module Kaching
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ describe Kaching::CacheCounter do
4
+ let(:user) { User.create }
5
+
6
+ it "should create method on user" do
7
+ user.should respond_to(:articles_count)
8
+ user.should respond_to(:reset_count_cache_articles!)
9
+ end
10
+
11
+ it "should return up-to-date count with method" do
12
+ user.articles_count.should == 0
13
+ user.articles << Article.new
14
+ user.articles_count.should == 1
15
+ user.articles << Article.new
16
+ user.articles_count.should == 2
17
+ end
18
+
19
+ it "should update count" do
20
+ user.articles_count.should == 0
21
+ user.articles << Article.new
22
+
23
+ Kaching.cache_store.get(user.kaching_key(:articles, :count)).to_i.should == 1
24
+
25
+ user.reset_count_cache_articles!
26
+
27
+ Kaching.cache_store.get(user.kaching_key(:articles, :count)).should be_blank
28
+
29
+ user.articles_count.should == user.articles_count
30
+
31
+ Kaching.cache_store.get(user.kaching_key(:articles, :count)).to_i.should == 1
32
+ end
33
+
34
+ it "should return up-to-date count for user with items" do
35
+ user.articles << Article.new
36
+ user.articles << Article.new
37
+ user.articles_count.should == 2
38
+
39
+ id = user.id
40
+
41
+ user = User.find(id)
42
+ user.articles_count.should == 2
43
+
44
+ user.articles << Article.new
45
+ user.articles_count.should == 3
46
+ end
47
+
48
+ it "should fetch count where not exists yet (_count method wasn't called and inserted new record)" do
49
+ user.articles << Article.new
50
+ user.articles << Article.new
51
+ user.articles_count.should == 2
52
+ user.reset_count_cache_articles!
53
+
54
+ user.articles << Article.new
55
+ user.articles_count.should == 3
56
+ end
57
+
58
+ it "should use count blocks if passed" do
59
+ user.following_users_count.should == 0
60
+ Follow.create(user_id: user.id, item: User.create)
61
+ user.following_users_count.should == 1
62
+ Follow.create(user_id: user.id, item: User.create)
63
+ user.following_users_count.should == 2
64
+ end
65
+
66
+ context "with blocks" do
67
+ it "should use count blocks if passed and show up-to-date count for existing user" do
68
+ Follow.create!(user_id: user.id, item: User.create)
69
+ Follow.create!(user_id: user.id, item: User.create)
70
+ user.following_users_count.should == 2
71
+
72
+ id = user.id
73
+
74
+ user = User.find(id)
75
+ user.following_users_count.should == 2
76
+
77
+ Follow.create!(user_id: user.id, item: User.create)
78
+ user.following_users_count.should == 3
79
+ end
80
+
81
+ it "should use count blocks if passed, other side of association" do
82
+ follower = User.create
83
+
84
+ user.follower_users_count.should == 0
85
+
86
+ Follow.create(user_id: follower.id, item: user)
87
+ user.follower_users_count.should == 1
88
+ end
89
+ end
90
+
91
+ context "custom options" do
92
+ it "should find belongs_to by options" do
93
+ user.cars_count.should == 0
94
+ car = Car.create!(driver: user)
95
+ user.cars.count.should == 1
96
+ user.cars_count.should == 1
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ describe Kaching::CacheList do
4
+ let(:user) { User.create }
5
+
6
+ context "non-polymorphic" do
7
+ it "should add/remove/has" do
8
+ movie1 = Movie.create!
9
+ movie2 = Movie.create!
10
+
11
+ user.has_user_movie?(movie1).should be_false
12
+ user.add_user_movie!(movie1)
13
+
14
+ user.has_user_movie?(movie1).should be_true
15
+
16
+ user.add_user_movie!(movie2)
17
+ user.has_user_movie?(movie2).should be_true
18
+
19
+ user.remove_user_movie!(movie1)
20
+ user.has_user_movie?(movie1).should be_false
21
+ user.has_user_movie?(movie2).should be_true
22
+
23
+ id = user.id
24
+
25
+ user = User.find(id)
26
+ user.has_user_movie?(movie1).should be_false
27
+ user.has_user_movie?(movie2).should be_true
28
+ end
29
+ end
30
+
31
+ context "polymorphic" do
32
+ it "should create method on user" do
33
+ user.should respond_to(:like!)
34
+ user.should respond_to(:unlike!)
35
+ user.should respond_to(:likes?)
36
+ user.should respond_to(:likes)
37
+ user.should respond_to(:likes_count)
38
+ end
39
+
40
+ it "should add/remove/has" do
41
+ Movie.create!
42
+
43
+ movie1 = Movie.create!
44
+ movie2 = Movie.create!
45
+
46
+ user.likes_count.should == 0
47
+ user.likes?(movie1).should be_false
48
+ user.like!(movie1)
49
+ user.like!(movie2)
50
+ user.likes_count.should == 2
51
+ user.likes?(movie1).should be_true
52
+ user.likes?(movie2).should be_true
53
+ user.unlike!(movie1)
54
+ user.likes_count.should == 1
55
+ user.likes?(movie1).should be_false
56
+
57
+ id = user.id
58
+
59
+ user = User.find(id)
60
+ user.likes?(movie1).should be_false
61
+ user.likes?(movie2).should be_true
62
+ end
63
+
64
+ it "should support creation/has w/o built-in methods" do
65
+ movie1 = Movie.create!
66
+ movie2 = Movie.create!
67
+
68
+ Like.connection.execute "INSERT INTO likes (user_id, item_id, item_type) VALUES (#{user.id}, #{movie1.id}, 'Movie')"
69
+ Like.create!(user: user, item: movie2)
70
+
71
+ user.likes?(movie1).should be_true
72
+ user.likes?(movie2).should be_true
73
+ end
74
+
75
+ it "should toggle" do
76
+ movie1 = Movie.create!
77
+
78
+ user.likes?(movie1).should be_false
79
+ user.toggle_like!(movie1)
80
+ user.likes?(movie1).should be_true
81
+
82
+ user.toggle_like!(movie1)
83
+ user.likes?(movie1).should be_false
84
+
85
+ user.like!(movie1)
86
+ user.likes?(movie1).should be_true
87
+
88
+ user.toggle_like!(movie1, false)
89
+ user.likes?(movie1).should be_false
90
+
91
+ user.toggle_like!(movie1, true)
92
+ user.likes?(movie1).should be_true
93
+ end
94
+
95
+ it "should support creation/has w/o built-in methods, by resetting cache after manual insertion" do
96
+ movie1 = Movie.create!
97
+
98
+ # this creates the cache
99
+ user.likes?(movie1).should be_false
100
+
101
+ # this doesn't update cache
102
+ Like.connection.execute "INSERT INTO likes (user_id, item_id, item_type) VALUES (#{user.id}, #{movie1.id}, 'Movie')"
103
+
104
+ user.reset_list_cache_likes!
105
+ user.likes?(movie1).should be_true
106
+ end
107
+ end
108
+
109
+ context "two relationships on same model" do
110
+ it "should manage counts" do
111
+ follower_user = User.create!
112
+ followed_user = User.create!
113
+
114
+ follower_user.should respond_to(:follower_users_count)
115
+ follower_user.should respond_to(:following_users_count)
116
+
117
+ follower_user.follower_users_count.should == 0
118
+ followed_user.following_users_count.should == 0
119
+
120
+ expect { expect { expect { expect {
121
+ follower_user.follow! followed_user
122
+ }.to change(followed_user, :follower_users_count).by(1)
123
+ }.to change(follower_user, :following_users_count).by(1)
124
+ }.to_not change(follower_user, :follower_users_count)
125
+ }.to_not change(followed_user, :following_users_count)
126
+
127
+ expect { expect { expect { expect {
128
+ follower_user.unfollow! followed_user
129
+ }.to change(followed_user, :follower_users_count).by(-1)
130
+ }.to change(follower_user, :following_users_count).by(-1)
131
+ }.to_not change(follower_user, :follower_users_count)
132
+ }.to_not change(followed_user, :following_users_count)
133
+ end
134
+ end
135
+ end