object_cacheable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "http://www.rubygems.org"
2
+
3
+ gem 'minitest'
4
+ gem 'minitest-metadata'
5
+ gem 'minitest-reporters'
6
+ gem 'minitest-spec-context'
7
+ gem 'mocha'
8
+ gem 'spork'
9
+ gem 'spork-minitest'
@@ -0,0 +1,11 @@
1
+ production:
2
+ cache: 'Rails.cache'
3
+ logger: 'Rails.logger'
4
+
5
+ development:
6
+ cache: 'Rails.cache'
7
+ logger: 'Rails.logger'
8
+
9
+ test:
10
+ cache: 'Cacheable::Utils.test_cache'
11
+ logger: 'Cacheable::Utils.test_logger'
@@ -0,0 +1,41 @@
1
+ require 'yaml'
2
+ require "#{File.dirname(__FILE__)}/cache_utils"
3
+
4
+ module Cacheable
5
+ def self.cache
6
+ CacheConfiguration.cache
7
+ end
8
+
9
+ def self.logger
10
+ CacheConfiguration.logger
11
+ end
12
+
13
+ class CacheConfiguration
14
+ @@cache_instance = nil
15
+ @@logger_instance = nil
16
+
17
+ def self.load_config
18
+ configs = YAML.load_file("#{File.dirname(__FILE__)}/cache_configs.yml")
19
+ env = environment
20
+
21
+ @@cache_instance = eval(configs[env]['cache'])
22
+ @@logger_instance = eval(configs[env]['logger'])
23
+ end
24
+
25
+ def self.environment
26
+ Rails.env
27
+ rescue => error
28
+ 'test'
29
+ end
30
+
31
+ def self.cache
32
+ load_config if @@cache_instance.nil?
33
+ @@cache_instance
34
+ end
35
+
36
+ def self.logger
37
+ load_config if @@logger_instance.nil?
38
+ @@logger_instance
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ module Cacheable
2
+ class Utils
3
+ def self.test_cache
4
+ Cacheable::TestCache.instance
5
+ end
6
+
7
+ def self.test_logger
8
+ Cacheable::TestLogger.instance
9
+ end
10
+ end
11
+
12
+ class TestCache
13
+ @@cache_storage = {}
14
+ @@cache_instance = nil
15
+
16
+ def self.instance
17
+ @@cache_instance ||= TestCache.new
18
+ end
19
+
20
+ def fetch(key)
21
+ @@cache_storage[key]
22
+ end
23
+
24
+ def write(key, value, options = {})
25
+ @@cache_storage[key] = value
26
+ end
27
+
28
+ def delete(key)
29
+ @@cache_storage.delete(key)
30
+ end
31
+
32
+ def keys
33
+ @@cache_storage.keys
34
+ end
35
+ end
36
+
37
+ class TestLogger
38
+ @@logger_instance = nil
39
+
40
+ def self.instance
41
+ @@logger_instance ||= TestLogger.new
42
+ end
43
+
44
+ def info(message)
45
+ puts message
46
+ end
47
+
48
+ alias_method :warning, :info
49
+ alias_method :error, :info
50
+
51
+ end
52
+ end
data/lib/cacheable.rb ADDED
@@ -0,0 +1,154 @@
1
+ require "#{File.dirname(__FILE__)}/cache_configuration"
2
+
3
+ module Cacheable
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+
8
+ def self.method_missing(name, *args, &block)
9
+ query_attributes = []
10
+
11
+ if name.to_s.start_with? 'fetch_by_'
12
+ # puts("\n\nname = #{name}\nname['fetch_by_'.length, name.length] = #{name['fetch_by_'.length, name.length]}")
13
+
14
+ query_attributes = name['fetch_by_'.length, name.length].split('_and_')
15
+ return nil if query_attributes == []
16
+
17
+ # puts("\n\n Cacheable method_missing - query attributes = #{query_attributes}, arguments = #{args.inspect}\n\n")
18
+
19
+ find_in_cache(query_attributes,args) or refresh_cache(query_attributes, args)
20
+ else
21
+ super
22
+ end
23
+
24
+ end
25
+
26
+ # Use this method if you want to explicitly re-fetch the whole object with given attributes-values, and write the newly fetched object to the cache
27
+ def self.refresh_cache(attributes, args)
28
+
29
+ obj = fetch(attributes, args)
30
+ return nil if obj.nil?
31
+
32
+ obj.cached_by_attributes = attributes
33
+ return obj.update_cache
34
+
35
+ rescue => error
36
+ Cacheable::logger.error("\n\nError in Cacheable.refresh_cache error = #{error.message}\n#{error.backtrace.join("\n")}\n\n")
37
+ return nil
38
+ end
39
+
40
+ # This method must be implemented by subclasses, because only subclasses know how to do the actual data fetch
41
+ def self.fetch(attributes,args)
42
+ raise 'The method <fetch> must be implemented by subclass'
43
+ end
44
+
45
+ # including classes that want to have time-based expire can implement this method
46
+ # can be 5.minutes, 1.day ...etc...
47
+ # 0 is never expire
48
+ def self.cache_expired_duration
49
+ 600 # default to 10 minutes
50
+ end
51
+
52
+ private
53
+ def self.find_in_cache(attributes,args)
54
+ cache_key = build_cache_key(attributes,args)
55
+
56
+ obj = Cacheable::cache.fetch(cache_key)
57
+ if cache_expired_duration > 0
58
+ cached_time = Cacheable::cache.fetch(build_cached_time_key(attributes,args)) || 0
59
+ return refresh_cache(attributes, args) if ( (Time.now.to_i - cached_time > cache_expired_duration) && obj)
60
+ end
61
+
62
+ obj
63
+ end
64
+
65
+ def self.save_to_cache(attributes, args, obj)
66
+ cache_key = build_cache_key(attributes,args)
67
+ options = {}
68
+
69
+ if cache_expired_duration > 0
70
+ options = { expires_in: cache_expired_duration.to_i }
71
+ Cacheable::cache.write(build_cached_time_key(attributes,args), Time.now.to_i)
72
+ end
73
+
74
+ Cacheable::cache.write(cache_key, obj, options)
75
+ end
76
+
77
+ def self.build_cache_key(attributes,args)
78
+ "#{self.name}_#{attributes.join('_')}_#{args.map(&:to_s).join('_')}"
79
+ end
80
+
81
+ # store the time when the object is cached
82
+ def self.build_cached_time_key(attributes,args)
83
+ "#{build_cache_key(attributes,args)}_cached_time"
84
+ end
85
+
86
+ end
87
+ end # End of class methods
88
+
89
+ # Instance methods
90
+
91
+ # Delete cached object from cache
92
+ # For example if you have a cached current_profile, and you want to delete it out of cache use current_profile.delete_from_cache
93
+ def delete_from_cache
94
+ cached_key_attributes = cached_by_attributes
95
+ raise "This object has never been cached in the cache #{self.inspect}" if cached_key_attributes.nil?
96
+
97
+ cached_key_attributes.each do |attribute_keys|
98
+ attribute_values = build_attribute_values(attribute_keys)
99
+ cache_key = self.class.build_cache_key(attribute_keys,attribute_values)
100
+ Cacheable::cache.delete(cache_key)
101
+ end
102
+
103
+ self
104
+ end
105
+
106
+ # This method updates all instances of an object with all cached keys (cached by different attributes)
107
+ # If you have an instance of a cached object, you just modify it or partially modify it, and want to write it back to the cache
108
+ # then use this method. For example if you have a current_profile, and its info changed, use current_profile.update_cache
109
+ def update_cache
110
+ cached_key_attributes = cached_by_attributes
111
+ raise "This object has never been cached in the cache #{self.inspect}" if cached_key_attributes.nil?
112
+
113
+ cached_key_attributes.each do |attribute_keys|
114
+ attribute_values = build_attribute_values(attribute_keys)
115
+ self.class.save_to_cache(attribute_keys, attribute_values, self)
116
+ end
117
+
118
+ self
119
+
120
+ end
121
+
122
+
123
+ # All the attributes that instances of classes are cached by are dynamically created and store in cache
124
+ # in order to support multiple runtimes of cache server
125
+ def cached_by_attributes=(attributes)
126
+ all_attributes = all_cached_by_attributes
127
+ cached_attributes_for_class = all_attributes[self.class.name] || []
128
+
129
+ cached_attributes_for_class << attributes if !cached_attributes_for_class.include?(attributes)
130
+
131
+ all_attributes[self.class.name] = cached_attributes_for_class
132
+ Cacheable::cache.write('all_cached_by_attributes', all_attributes)
133
+ end
134
+
135
+ private
136
+ def cached_by_attributes
137
+ all_cached_by_attributes[self.class.name]
138
+ end
139
+
140
+ def all_cached_by_attributes
141
+ Cacheable::cache.fetch('all_cached_by_attributes') || {}
142
+ end
143
+
144
+ def build_attribute_values(attribute_keys)
145
+ attribute_values = []
146
+
147
+ attribute_keys.each do |attribute|
148
+ attribute_values << self.send(attribute)
149
+ end
150
+
151
+ attribute_values
152
+ end
153
+
154
+ end
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'object_cacheable'
3
+ s.version = '1.0.0'
4
+ s.summary = "Object-level caching"
5
+ s.description = "A gem for object-level caching."
6
+ s.authors = ['Linh Chau']
7
+ s.email = 'chauhonglinh@gmail.com'
8
+ s.files = [
9
+ './Gemfile', './object_cacheable.gemspec',
10
+ 'lib/cache_configs.yml', 'lib/cache_configuration.rb', 'lib/cache_utils.rb', 'lib/cacheable.rb',
11
+ 'test/models/cacheable_test.rb'
12
+ ]
13
+ s.homepage = 'https://github.com/linhchauatl/object_cacheable.git'
14
+ end
@@ -0,0 +1,191 @@
1
+ # Run test from the command line, gem root
2
+ # bundle exec ruby -Ilib test/models/cacheable_test.rb
3
+
4
+ require 'minitest/autorun'
5
+ require 'minitest-spec-context'
6
+ require 'cacheable'
7
+ require 'mocha'
8
+
9
+ class TestCacheable
10
+ include Cacheable
11
+ attr_accessor :id, :value_1, :value_2, :value_3
12
+
13
+ # This method mimics a real call to database or webservices
14
+ def self.fetch(attributes, args)
15
+ # Assume that the combination of (value_1 + value_2) will uniquely identify one instance - for testing purpose
16
+ # For testing convenience, this implementation returns the same value object for the following calls:
17
+ # obj1 = TestCacheable.fetch_by_id(1)
18
+ # obj2 = TestCacheable.fetch_by_value_1_and_value_2('value 1', 'value 2')
19
+
20
+ # The calls with different ids will return different objects
21
+
22
+ if ( (args.size == 1) or (args.size == 3) )
23
+ return TestCacheable.new(*args)
24
+ elsif (args.size == 2)
25
+ # Because the initilize always expects value 1 before value 2, so if we have attributes sent as ['value_2', 'value_1'], we have to reverse the args
26
+ created_args = (attributes == ['value_1', 'value_2'])? args : args.reverse
27
+
28
+ return (args.sort == ['value 1', 'value 2'])? TestCacheable.new(1, *created_args) : TestCacheable.new( (args[0].to_s.hash + args[1].to_s.hash), *created_args)
29
+ end
30
+
31
+ return TestCacheable.new(100, 'test value 1', 'test value 2')
32
+
33
+ end
34
+
35
+ def initialize(an_id, a_value_1 = 'value 1', a_value_2 = 'value 2')
36
+ self.id = an_id
37
+
38
+ self.value_1 = a_value_1
39
+ self.value_2 = a_value_2
40
+
41
+
42
+ self.value_3 = 'Original Value 3'
43
+ end
44
+
45
+ def ==(obj)
46
+ self.id == obj.id && self.value_1 == obj.value_1 && self.value_2 == obj.value_2 && self.value_3 == obj.value_3
47
+ end
48
+
49
+ end
50
+
51
+
52
+ describe Cacheable do
53
+
54
+ context 'fetch object' do
55
+ it 'calls the real fetch when the object is not in cache, then puts object in the cache' do
56
+
57
+ # First, make sure that there is nothing in cache
58
+ cached_obj = Cacheable::cache.fetch('TestCacheable_id_1')
59
+ cached_obj.must_be_nil
60
+
61
+ # Call the method that fetches the object and puts in cache
62
+ obj = TestCacheable.fetch_by_id(1)
63
+ obj.wont_be_nil
64
+
65
+ cached_obj = Cacheable::cache.fetch('TestCacheable_id_1')
66
+ cached_obj.must_equal obj
67
+
68
+ # Make sure that it doesn't make a real call to fetch the second time
69
+ TestCacheable.stubs(:fetch).returns(nil) # If this gets called, the result_obj will be nil
70
+ result_obj = TestCacheable.fetch_by_id(1)
71
+ result_obj.must_equal obj
72
+
73
+ # clean the cache after finish
74
+ obj.delete_from_cache
75
+ end
76
+
77
+ it 'fetches and caches different objects separately' do
78
+ obj3 = TestCacheable.fetch_by_id(3)
79
+ obj3.wont_be_nil
80
+
81
+ obj4 = TestCacheable.fetch_by_id(4)
82
+ obj4.wont_be_nil
83
+
84
+ # Fetched objects must be different
85
+ obj3.wont_equal obj4
86
+
87
+ #Cached objects must be different
88
+ cached_obj3 = Cacheable::cache.fetch('TestCacheable_id_3')
89
+ cached_obj3.must_equal obj3
90
+
91
+ cached_obj4 = Cacheable::cache.fetch('TestCacheable_id_4')
92
+ cached_obj4.must_equal obj4
93
+
94
+ cached_obj3.wont_equal cached_obj4
95
+
96
+ # clean the cache after finish
97
+ obj3.delete_from_cache
98
+ obj4.delete_from_cache
99
+ end
100
+
101
+ it 'fetches the same object with different ordered combinations of keys' do
102
+ obj1 = TestCacheable.fetch_by_value_1_and_value_2('key 1', 'key 2')
103
+ obj2 = TestCacheable.fetch_by_value_2_and_value_1('key 2', 'key 1')
104
+
105
+ obj1.must_equal obj2
106
+
107
+ cached_obj1 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_key 1_key 2')
108
+ cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_2_value_1_key 2_key 1')
109
+
110
+ cached_obj1.must_equal cached_obj2
111
+
112
+ # clean the cache after finish - make sure that obj1 and obj2 are both deleted from cache
113
+ obj1.delete_from_cache
114
+ cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_2_value_1_key 2_key 1')
115
+ cached_obj2.must_be_nil
116
+
117
+ end
118
+
119
+ it 'fetches two different objects with revert values for keys' do
120
+ # These 2 different objects have revert values for keys,
121
+ # for example [firstname, lastname] = [John, Smith] and [fistname, lastname] = [Smith, John]
122
+ # Unlikely to happen, but we will test anyway
123
+ obj1 = TestCacheable.fetch_by_value_1_and_value_2('value 1234', 'value 5678')
124
+ obj2 = TestCacheable.fetch_by_value_1_and_value_2('value 5678', 'value 1234')
125
+
126
+ # fetched objects are different
127
+ obj1.wont_equal obj2
128
+
129
+ cached_obj1 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_value 1234_value 5678')
130
+ cached_obj1.must_equal obj1
131
+
132
+ cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_value 5678_value 1234')
133
+ cached_obj2.must_equal obj2
134
+
135
+ # cached objects are different
136
+ cached_obj1.wont_equal cached_obj2
137
+
138
+ # clean the cache after finish
139
+ obj1.delete_from_cache
140
+ obj2.delete_from_cache
141
+ end
142
+
143
+ end
144
+
145
+ context 'update object' do
146
+ it 'updates all instances of object in the cache' do
147
+ obj1 = TestCacheable.fetch_by_id(1)
148
+ obj2 = TestCacheable.fetch_by_value_1_and_value_2('value 1', 'value 2')
149
+
150
+ # We expect that obj1 and obj2 are 2 cached instances of the same object:
151
+ obj1.must_equal obj2
152
+
153
+ # Now update obj1 and expect the value is changed in the cache
154
+ obj1.value_3 = 'New value 3'
155
+ obj1.update_cache
156
+
157
+ cached_obj1 = Cacheable::cache.fetch('TestCacheable_id_1')
158
+ cached_obj1.value_3.must_equal 'New value 3'
159
+
160
+ # We also expect that the obj_2 value_3 is also changed
161
+ cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_value 1_value 2')
162
+ cached_obj2.value_3.must_equal 'New value 3'
163
+
164
+ # clean the cache after finish
165
+ obj1.delete_from_cache
166
+
167
+ end
168
+ end
169
+
170
+ context 'delete object' do
171
+ it 'deletes all instances of object in the cache' do
172
+ obj1 = TestCacheable.fetch_by_id(1)
173
+ obj2 = TestCacheable.fetch_by_value_1_and_value_2('value 1', 'value 2')
174
+
175
+ # We expect that obj1 and obj2 are 2 cached instances of the same object:
176
+ obj1.must_equal obj2
177
+
178
+ # Delete obj1
179
+ obj1.delete_from_cache
180
+
181
+ cached_obj1 = Cacheable::cache.fetch('TestCacheable_id_1')
182
+ cached_obj1.must_be_nil
183
+
184
+ # The obj2 in the cache now must be nil too
185
+ cached_obj2 = Cacheable::cache.fetch('TestCacheable_value_1_value_2_value 1_value 2')
186
+ cached_obj2.must_be_nil
187
+
188
+ end
189
+ end
190
+
191
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: object_cacheable
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Linh Chau
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2013-03-28 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: A gem for object-level caching.
22
+ email: chauhonglinh@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - ./Gemfile
31
+ - ./object_cacheable.gemspec
32
+ - lib/cache_configs.yml
33
+ - lib/cache_configuration.rb
34
+ - lib/cache_utils.rb
35
+ - lib/cacheable.rb
36
+ - test/models/cacheable_test.rb
37
+ has_rdoc: true
38
+ homepage: https://github.com/linhchauatl/object_cacheable.git
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.6
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Object-level caching
67
+ test_files: []
68
+