object_cacheable 1.0.0

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