Permalink
Browse files

first commit for the gem

  • Loading branch information...
0 parents commit dc7b76048d116f9aee1d4f26b4312026ffbfd6ee @linhchauatl committed Mar 22, 2013
Showing with 493 additions and 0 deletions.
  1. BIN .DS_Store
  2. +9 −0 Gemfile
  3. +21 −0 README.md
  4. +11 −0 lib/cache_configs.yml
  5. +41 −0 lib/cache_configuration.rb
  6. +52 −0 lib/cache_utils.rb
  7. +154 −0 lib/cacheable.rb
  8. +14 −0 object_cacheable.gemspec
  9. +191 −0 test/models/cacheable_test.rb
BIN .DS_Store
Binary file not shown.
@@ -0,0 +1,9 @@
+source "http://www.rubygems.org"
+
+gem 'minitest'
+gem 'minitest-metadata'
+gem 'minitest-reporters'
+gem 'minitest-spec-context'
+gem 'mocha'
+gem 'spork'
+gem 'spork-minitest'
@@ -0,0 +1,21 @@
+This is a gem that provides Ruby objects with the capability of object caching. This gem is framework-agnostic, so it can be used in all the types of Ruby applications.
+
+Object that wants to be cached must include Cacheable,
+ class Person
+ include Cacheable
+ end
+
+and implement the method:
+ def self.fetch(attributes,args)
+ # Your code to do the real object retrieval here
+ # It can be database call, webservices call or complex object builder
+ # The first argument attributes is an array of attribute names that we want to retrieve the object by
+ # The second argument args is an array of values of the attributes
+ # for example Person.fetch(['first_name', 'last_name'], ['Linh', 'Chau'])
+ # But the combinations of attributes must be able to identify a unique object
+ end
+.
+When a Class includes Cacheable and defines the method "fetch", it's instances will become cacheable, and all the cache operations will be handled by the module Cacheable.
+
+Testing
+`bundle exec ruby -Ilib test/models/cacheable_test.rb`
@@ -0,0 +1,11 @@
+production:
+ cache: 'Rails.cache'
+ logger: 'Rails.logger'
+
+development:
+ cache: 'Rails.cache'
+ logger: 'Rails.logger'
+
+test:
+ cache: 'Cacheable::Utils.test_cache'
+ logger: 'Cacheable::Utils.test_logger'
@@ -0,0 +1,41 @@
+require 'yaml'
+require "#{File.dirname(__FILE__)}/cache_utils"
+
+module Cacheable
+ def self.cache
+ CacheConfiguration.cache
+ end
+
+ def self.logger
+ CacheConfiguration.logger
+ end
+
+ class CacheConfiguration
+ @@cache_instance = nil
+ @@logger_instance = nil
+
+ def self.load_config
+ configs = YAML.load_file("#{File.dirname(__FILE__)}/cache_configs.yml")
+ env = environment
+
+ @@cache_instance = eval(configs[env]['cache'])
+ @@logger_instance = eval(configs[env]['logger'])
+ end
+
+ def self.environment
+ Rails.env
+ rescue => error
+ 'test'
+ end
+
+ def self.cache
+ load_config if @@cache_instance.nil?
+ @@cache_instance
+ end
+
+ def self.logger
+ load_config if @@logger_instance.nil?
+ @@logger_instance
+ end
+ end
+end
@@ -0,0 +1,52 @@
+module Cacheable
+ class Utils
+ def self.test_cache
+ Cacheable::TestCache.instance
+ end
+
+ def self.test_logger
+ Cacheable::TestLogger.instance
+ end
+ end
+
+ class TestCache
+ @@cache_storage = {}
+ @@cache_instance = nil
+
+ def self.instance
+ @@cache_instance ||= TestCache.new
+ end
+
+ def fetch(key)
+ @@cache_storage[key]
+ end
+
+ def write(key, value, options = {})
+ @@cache_storage[key] = value
+ end
+
+ def delete(key)
+ @@cache_storage.delete(key)
+ end
+
+ def keys
+ @@cache_storage.keys
+ end
+ end
+
+ class TestLogger
+ @@logger_instance = nil
+
+ def self.instance
+ @@logger_instance ||= TestCache.new
+ end
+
+ def info(message)
+ puts message
+ end
+
+ alias_method :warning, :info
+ alias_method :error, :info
+
+ end
+end
@@ -0,0 +1,154 @@
+require "#{File.dirname(__FILE__)}/cache_configuration"
+
+module Cacheable
+
+ def self.included(base)
+ base.class_eval do
+
+ def self.method_missing(name, *args, &block)
+ query_attributes = []
+
+ if name.to_s.start_with? 'fetch_by_'
+ # puts("\n\nname = #{name}\nname['fetch_by_'.length, name.length] = #{name['fetch_by_'.length, name.length]}")
+
+ query_attributes = name['fetch_by_'.length, name.length].split('_and_')
+ return nil if query_attributes == []
+
+ # puts("\n\n Cacheable method_missing - query attributes = #{query_attributes}, arguments = #{args.inspect}\n\n")
+
+ find_in_cache(query_attributes,args) or refresh_cache(query_attributes, args)
+ else
+ super
+ end
+
+ end
+
+ # 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
+ def self.refresh_cache(attributes, args)
+
+ obj = fetch(attributes, args)
+ return nil if obj.nil?
+
+ obj.cached_by_attributes = attributes
+ return obj.update_cache
+
+ rescue => error
+ Cacheable::logger.error("\n\nError in Cacheable.refresh_cache error = #{error.message}\n#{error.backtrace.join("\n")}\n\n")
+ return nil
+ end
+
+ # This method must be implemented by subclasses, because only subclasses know how to do the actual data fetch
+ def self.fetch(attributes,args)
+ raise 'The method <fetch> must be implemented by subclass'
+ end
+
+ # including classes that want to have time-based expire can implement this method
+ # can be 5.minutes, 1.day ...etc...
+ # 0 is never expire
+ def self.cache_expired_duration
+ 600 # default to 10 minutes
+ end
+
+ private
+ def self.find_in_cache(attributes,args)
+ cache_key = build_cache_key(attributes,args)
+
+ obj = Cacheable::cache.fetch(cache_key)
+ if cache_expired_duration > 0
+ cached_time = Cacheable::cache.fetch(build_cached_time_key(attributes,args)) || 0
+ return refresh_cache(attributes, args) if ( (Time.now.to_i - cached_time > cache_expired_duration) && obj)
+ end
+
+ obj
+ end
+
+ def self.save_to_cache(attributes, args, obj)
+ cache_key = build_cache_key(attributes,args)
+ options = {}
+
+ if cache_expired_duration > 0
+ options = { expires_in: cache_expired_duration.to_i }
+ Cacheable::cache.write(build_cached_time_key(attributes,args), Time.now.to_i)
+ end
+
+ Cacheable::cache.write(cache_key, obj, options)
+ end
+
+ def self.build_cache_key(attributes,args)
+ "#{self.name}_#{attributes.join('_')}_#{args.map(&:to_s).join('_')}"
+ end
+
+ # store the time when the object is cached
+ def self.build_cached_time_key(attributes,args)
+ "#{build_cache_key(attributes,args)}_cached_time"
+ end
+
+ end
+ end # End of class methods
+
+ # Instance methods
+
+ # Delete cached object from cache
+ # For example if you have a cached current_profile, and you want to delete it out of cache use current_profile.delete_from_cache
+ def delete_from_cache
+ cached_key_attributes = cached_by_attributes
+ raise "This object has never been cached in the cache #{self.inspect}" if cached_key_attributes.nil?
+
+ cached_key_attributes.each do |attribute_keys|
+ attribute_values = build_attribute_values(attribute_keys)
+ cache_key = self.class.build_cache_key(attribute_keys,attribute_values)
+ Cacheable::cache.delete(cache_key)
+ end
+
+ self
+ end
+
+ # This method updates all instances of an object with all cached keys (cached by different attributes)
+ # 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
+ # then use this method. For example if you have a current_profile, and its info changed, use current_profile.update_cache
+ def update_cache
+ cached_key_attributes = cached_by_attributes
+ raise "This object has never been cached in the cache #{self.inspect}" if cached_key_attributes.nil?
+
+ cached_key_attributes.each do |attribute_keys|
+ attribute_values = build_attribute_values(attribute_keys)
+ self.class.save_to_cache(attribute_keys, attribute_values, self)
+ end
+
+ self
+
+ end
+
+
+ # All the attributes that instances of classes are cached by are dynamically created and store in cache
+ # in order to support multiple runtimes of cache server
+ def cached_by_attributes=(attributes)
+ all_attributes = all_cached_by_attributes
+ cached_attributes_for_class = all_attributes[self.class.name] || []
+
+ cached_attributes_for_class << attributes if !cached_attributes_for_class.include?(attributes)
+
+ all_attributes[self.class.name] = cached_attributes_for_class
+ Cacheable::cache.write('all_cached_by_attributes', all_attributes)
+ end
+
+ private
+ def cached_by_attributes
+ all_cached_by_attributes[self.class.name]
+ end
+
+ def all_cached_by_attributes
+ Cacheable::cache.fetch('all_cached_by_attributes') || {}
+ end
+
+ def build_attribute_values(attribute_keys)
+ attribute_values = []
+
+ attribute_keys.each do |attribute|
+ attribute_values << self.send(attribute)
+ end
+
+ attribute_values
+ end
+
+end
@@ -0,0 +1,14 @@
+Gem::Specification.new do |s|
+ s.name = 'object_cacheable'
+ s.version = '1.0.0'
+ s.summary = "Object-level caching"
+ s.description = "A gem for object-level caching."
+ s.authors = ['Linh Chau']
+ s.email = 'chauhonglinh@gmail.com'
+ s.files = [
+ './Gemfile', './object_cacheable.gemspec',
+ 'lib/cache_configs.yml', 'lib/cache_configuration.rb', 'lib/cache_utils.rb', 'lib/cacheable.rb',
+ 'test/models/cacheable_test.rb'
+ ]
+ s.homepage = 'https://github.com/linhchauatl/object_cacheable.git'
+end
Oops, something went wrong.

0 comments on commit dc7b760

Please sign in to comment.