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,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