archan937/cached_record

View on GitHub
lib/cached_record/cache.rb

Summary

Maintainability
A
1 hr
Test Coverage
require "dalli"
require "redis"

module CachedRecord
  module Cache
    class Error < StandardError; end

    def self.setup(store, options = {})
      if valid_store? store
        send store, options
      end
    end

    def self.memcached(options = nil)
      if stores[:memcached].nil? || options
        options ||= {}
        host = options.delete(:host) || "localhost"
        port = options.delete(:port) || 11211
        stores[:memcached] = Dalli::Client.new "#{host}:#{port}", options
      end
      stores[:memcached]
    end

    def self.redis(options = nil)
      if stores[:redis].nil? || options
        options ||= {}
        stores[:redis] = Redis.new options
      end
      stores[:redis]
    end

    def self.cache
      @cache ||= {Dalli::Client => {}, Redis => {}}
    end

    def self.clear!
      @cache = nil
    end

    def self.get(klass, id)
      retained(klass, id) || begin
        cache_string = store(klass).get(klass.cache_key(id)) || begin
          return unless (instance = yield if block_given?)
          set instance
        end
        json, epoch_time = split_cache_string(cache_string)
        memoized(klass, id, epoch_time) do
          klass.load_cache_json JSON.parse(json)
        end
      end
    end

    def self.set(instance)
      "#{instance.to_cache_json}#{"@#{Time.now.to_i}" if instance.class.as_cache[:memoize]}".tap do |cache_string|
        store(instance.class).set instance.class.cache_key(instance.id), cache_string, instance.class.as_cache[:expire]
      end
    end

    def self.expire(instance)
      klass = instance.class
      cache_key = klass.cache_key instance.id
      store(klass).delete cache_key
      cache[store(klass).class].delete cache_key if klass.as_cache[:memoize]
      nil
    end

    def self.retained(klass, id)
      return unless klass.as_cache[:memoize] && klass.as_cache[:retain]
      cache_hash, cache_key = cache[store(klass).class], klass.cache_key(id)

      if (cache_entry = cache_hash[cache_key]) && (Time.now.to_i < cache_entry[:cache_hit])
        cache_entry[:instance]
      end
    end

    def self.memoized(klass, id, epoch_time)
      return yield unless klass.as_cache[:memoize]
      cache_hash, cache_key = cache[store(klass).class], klass.cache_key(id)
      cache_hit = klass.as_cache[:retain] ? {:cache_hit => (Time.now + klass.as_cache[:retain]).to_i} : {}

      if (cache_entry = cache_hash[cache_key]) && (epoch_time == cache_entry[:epoch_time])
        cache_entry.merge! cache_hit
        cache_entry[:instance]
      else
        yield.tap do |instance|
          cache_hash[cache_key] = {:instance => instance, :epoch_time => epoch_time}.merge(cache_hit) if instance
        end
      end
    end

  private

    def self.valid_store?(arg)
      [:memcached, :redis].include?(arg.to_sym)
    end

    def self.stores
      @stores ||= {}
    end

    def self.store(klass)
      store = klass.as_cache[:store] || begin
        if stores.size == 1
          stores.keys.first
        else
          raise Error, "Cannot determine default cache store (store size is not 1: #{@stores.keys.sort.inspect})"
        end
      end
      if valid_store?(store)
        send(store)
      else
        raise Error, "Invalid cache store :#{store} passed"
      end
    end

    def self.split_cache_string(string)
      reg_exp = /@(\d+)$/
      string.match(reg_exp)
      json = string.gsub(reg_exp, "")
      epoch_time = $1.to_i if $1
      [json, epoch_time]
    end

  end
end