Module: IdentityCache

Defined in:
lib/identity_cache.rb,
lib/belongs_to_caching.rb,
lib/memoized_cache_proxy.rb,
lib/identity_cache/version.rb

Defined Under Namespace

Modules: BelongsToCaching, ClassMethods Classes: AlreadyIncludedError, InverseAssociationError, MemoizedCacheProxy

Constant Summary collapse

CACHED_NIL =
:idc_cached_nil
VERSION =
"0.0.2"

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.cacheObject (readonly)

Returns the value of attribute cache.



13
14
15
# File 'lib/identity_cache.rb', line 13

def cache
  @cache
end

.loggerObject

Returns the value of attribute logger.



12
13
14
# File 'lib/identity_cache.rb', line 12

def logger
  @logger
end

.readonlyObject

Returns the value of attribute readonly.



12
13
14
# File 'lib/identity_cache.rb', line 12

def readonly
  @readonly
end

Class Method Details

.cache_backend=(cache_adaptor) ⇒ Object

Sets the cache adaptor IdentityCache will be using

Parameters

cache_adaptor - A ActiveSupport::Cache::Store



21
22
23
# File 'lib/identity_cache.rb', line 21

def cache_backend=(cache_adaptor)
  cache.memcache = cache_adaptor
end

.fetch(key, &block) ⇒ Object

Cache retrieval and miss resolver primitive; given a key it will try to retrieve the associated value from the cache otherwise it will return the value of the execution of the block.

Parameters

key A cache key string



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/identity_cache.rb', line 44

def fetch(key, &block)
  result = cache.read(key) if should_cache?

  if result.nil?
    if block_given?
      ActiveRecord::Base.connection.with_master do
        result = yield
      end
      result = map_cached_nil_for(result)

      if should_cache?
        cache.write(key, result)
      end
    end
    logger.debug "[IdentityCache] cache miss for #{key}"
  else
    logger.debug "[IdentityCache] cache hit for #{key}"
  end

  unmap_cached_nil_for(result)
end

.fetch_multi(*keys, &block) ⇒ Object

Same as fetch, except that it will try a collection of keys, using the multiget operation of the cache adaptor

Parameters

keys A collection of key strings



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/identity_cache.rb', line 80

def fetch_multi(*keys, &block)
  return {} if keys.size == 0
  result = {}
  result = cache.read_multi(*keys) if should_cache?

  missed_keys = keys - result.select {|key, value| value.present? }.keys

  if missed_keys.size > 0
    if block_given?
      replacement_results = nil
      ActiveRecord::Base.connection.with_master do
        replacement_results = yield missed_keys
      end
      missed_keys.zip(replacement_results) do |(key, replacement_result)|
        if should_cache?
          replacement_result  = map_cached_nil_for(replacement_result )
          cache.write(key, replacement_result)
          logger.debug "[IdentityCache] cache miss for #{key} (multi)"
        end
        result[key] = replacement_result
      end
    end
  else
    result.keys.each do |key|
      logger.debug "[IdentityCache] cache hit for #{key} (multi)"
    end
  end

  result.keys.each do |key|
    result[key] = unmap_cached_nil_for(result[key])
  end

  result
end

.included(base) ⇒ Object

:nodoc:



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/identity_cache.rb', line 119

def included(base) #:nodoc:
  raise AlreadyIncludedError if base.respond_to? :cache_indexes

  unless ActiveRecord::Base.connection.respond_to?(:with_master)
    ActiveRecord::Base.connection.class.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
      def with_master
        yield
      end
    CODE
  end

  base.send(:include, ArTransactionChanges) unless base.include?(ArTransactionChanges)
  base.send(:include, IdentityCache::BelongsToCaching)
  base.after_commit :expire_cache
  base.after_touch  :expire_cache
  base.class_attribute :cache_indexes
  base.class_attribute :cache_attributes
  base.class_attribute :cached_has_manys
  base.class_attribute :cached_has_ones
  base.class_attribute :embedded_schema_hashes
  base.send(:extend, ClassMethods)

  base.cached_has_manys = {}
  base.cached_has_ones = {}
  base.embedded_schema_hashes = {}
  base.cache_attributes = []
  base.cache_indexes = []

  base.private_class_method :require_if_necessary, :build_normalized_has_many_cache, :build_denormalized_association_cache, :add_parent_expiry_hook,
    :identity_cache_multiple_value_dynamic_fetcher, :identity_cache_single_value_dynamic_fetcher


  base.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
    private :expire_cache, :was_new_record?, :fetch_denormalized_cached_association, :populate_denormalized_cached_association
  CODE
end

.map_cached_nil_for(value) ⇒ Object



66
67
68
# File 'lib/identity_cache.rb', line 66

def map_cached_nil_for(value)
  value.nil? ? IdentityCache::CACHED_NIL : value
end

.memcache_hash(key) ⇒ Object

:nodoc:



156
157
158
# File 'lib/identity_cache.rb', line 156

def memcache_hash(key) #:nodoc:
  CityHash.hash64(key)
end

.schema_to_string(columns) ⇒ Object



115
116
117
# File 'lib/identity_cache.rb', line 115

def schema_to_string(columns)
  columns.sort_by(&:name).map {|c| "#{c.name}:#{c.type}"} * ","
end

.should_cache?Boolean

:nodoc:

Returns:

  • (Boolean)


33
34
35
# File 'lib/identity_cache.rb', line 33

def should_cache? # :nodoc:
  !readonly && ActiveRecord::Base.connection.open_transactions == 0
end

.unmap_cached_nil_for(value) ⇒ Object



71
72
73
# File 'lib/identity_cache.rb', line 71

def unmap_cached_nil_for(value)
  value == IdentityCache::CACHED_NIL ? nil : value
end

Instance Method Details

#attribute_cache_key_for_attribute_and_previous_values(attribute, fields) ⇒ Object

:nodoc:



674
675
676
# File 'lib/identity_cache.rb', line 674

def attribute_cache_key_for_attribute_and_previous_values(attribute, fields) # :nodoc:
  self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields))
end

#expire_attribute_indexesObject

:nodoc:



719
720
721
722
723
# File 'lib/identity_cache.rb', line 719

def expire_attribute_indexes # :nodoc:
  cache_attributes.try(:each) do |(attribute, fields)|
    IdentityCache.cache.delete(attribute_cache_key_for_attribute_and_previous_values(attribute, fields)) unless was_new_record?
  end
end

#expire_cacheObject

:nodoc:



725
726
727
728
729
730
# File 'lib/identity_cache.rb', line 725

def expire_cache # :nodoc:
  expire_primary_index
  expire_secondary_indexes
  expire_attribute_indexes
  true
end

#expire_primary_indexObject

:nodoc:



691
692
693
694
695
696
697
698
699
700
701
# File 'lib/identity_cache.rb', line 691

def expire_primary_index # :nodoc:
  extra_keys = if respond_to? :updated_at
    old_updated_at = old_values_for_fields([:updated_at]).first
    "expiring_last_updated_at=#{old_updated_at}"
  else
    ""
  end
  IdentityCache.logger.debug "[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}"

  IdentityCache.cache.delete(primary_cache_index_key)
end

#expire_secondary_indexesObject

:nodoc:



703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
# File 'lib/identity_cache.rb', line 703

def expire_secondary_indexes # :nodoc:
  cache_indexes.try(:each) do |fields|
    if self.destroyed?
      IdentityCache.cache.delete(secondary_cache_index_key_for_previous_values(fields))
    else
      new_cache_index_key = secondary_cache_index_key_for_current_values(fields)
      IdentityCache.cache.delete(new_cache_index_key)

      if !was_new_record?
        old_cache_index_key = secondary_cache_index_key_for_previous_values(fields)
        IdentityCache.cache.delete(old_cache_index_key) unless old_cache_index_key == new_cache_index_key
      end
    end
  end
end

#fetch_denormalized_cached_association(ivar_name, association_name) ⇒ Object

:nodoc:



628
629
630
631
632
633
634
635
636
# File 'lib/identity_cache.rb', line 628

def fetch_denormalized_cached_association(ivar_name, association_name) # :nodoc:
  ivar_full_name = :"@#{ivar_name}"
  if IdentityCache.should_cache?
    populate_denormalized_cached_association(ivar_name, association_name)
    IdentityCache.unmap_cached_nil_for(instance_variable_get(ivar_full_name))
  else
    send(association_name.to_sym)
  end
end

#old_values_for_fields(fields) ⇒ Object

:nodoc:



678
679
680
681
682
683
684
685
686
687
688
689
# File 'lib/identity_cache.rb', line 678

def old_values_for_fields(fields) # :nodoc:
  fields.map do |field|
    field_string = field.to_s
    if destroyed? && transaction_changed_attributes.has_key?(field_string)
      transaction_changed_attributes[field_string]
    elsif persisted? && transaction_changed_attributes.has_key?(field_string)
      transaction_changed_attributes[field_string]
    else
      self.send(field)
    end
  end
end

#populate_association_cachesObject

:nodoc:



616
617
618
619
620
621
622
623
624
625
626
# File 'lib/identity_cache.rb', line 616

def populate_association_caches # :nodoc:
  self.class.all_cached_associations_needing_population.each do |cached_association, options|
    send(options[:population_method_name])
    reflection = options[:embed] && self.class.reflect_on_association(cached_association)
    if reflection && reflection.klass.respond_to?(:cached_has_manys)
      child_objects = Array.wrap(send(options[:cached_accessor_name]))
      child_objects.each(&:populate_association_caches)
    end
  end
  self.clear_association_cache if self.respond_to?(:clear_association_cache)
end

#populate_denormalized_cached_association(ivar_name, association_name) ⇒ Object

:nodoc:



638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
# File 'lib/identity_cache.rb', line 638

def populate_denormalized_cached_association(ivar_name, association_name) # :nodoc:
  ivar_full_name = :"@#{ivar_name}"
  schema_hash_ivar = :"@#{ivar_name}_schema_hash"
  reflection = association(association_name)

  current_schema_hash = self.class.embedded_schema_hashes[association_name] ||= begin
    IdentityCache.memcache_hash(IdentityCache.schema_to_string(reflection.klass.columns))
  end

  saved_schema_hash = instance_variable_get(schema_hash_ivar)

  if saved_schema_hash == current_schema_hash
    value = instance_variable_get(ivar_full_name)
    return value unless value.nil?
  end

  reflection.load_target unless reflection.loaded?

  loaded_association = send(association_name)

  instance_variable_set(schema_hash_ivar, current_schema_hash)
  instance_variable_set(ivar_full_name, IdentityCache.map_cached_nil_for(loaded_association))
end

#primary_cache_index_keyObject

:nodoc:



662
663
664
# File 'lib/identity_cache.rb', line 662

def primary_cache_index_key # :nodoc:
  self.class.rails_cache_key(id)
end

#secondary_cache_index_key_for_current_values(fields) ⇒ Object

:nodoc:



666
667
668
# File 'lib/identity_cache.rb', line 666

def secondary_cache_index_key_for_current_values(fields) # :nodoc:
  self.class.rails_cache_index_key_for_fields_and_values(fields, fields.collect {|field| self.send(field)})
end

#secondary_cache_index_key_for_previous_values(fields) ⇒ Object

:nodoc:



670
671
672
# File 'lib/identity_cache.rb', line 670

def secondary_cache_index_key_for_previous_values(fields) # :nodoc:
  self.class.rails_cache_index_key_for_fields_and_values(fields, old_values_for_fields(fields))
end

#was_new_record?Boolean

:nodoc:

Returns:

  • (Boolean)


732
733
734
# File 'lib/identity_cache.rb', line 732

def was_new_record? # :nodoc:
  !destroyed? && transaction_changed_attributes.has_key?('id') && transaction_changed_attributes['id'].nil?
end