module Kasket::ReadMixin

Public Class Methods

extended(base) click to toggle source
# File lib/kasket/read_mixin.rb, line 4
def self.extended(base)
  class << base
    alias_method :find_by_sql_without_kasket, :find_by_sql
    alias_method :find_by_sql, :find_by_sql_with_kasket
  end
end

Public Instance Methods

find_by_sql_with_kasket(sql, binds = [], *restargs, **kwargs, &blk) click to toggle source
# File lib/kasket/read_mixin.rb, line 11
def find_by_sql_with_kasket(sql, binds = [], *restargs, **kwargs, &blk)
  if use_kasket?
    query = if sql.respond_to?(:to_kasket_query)
      if ActiveRecord::VERSION::STRING < '5.2'
        sql.to_kasket_query(self, binds.map(&:value_for_database))
      else
        sql.to_kasket_query(self)
      end
    else
      kasket_parser.parse(sanitize_sql(sql))
    end
  end

  if query && has_kasket_index_on?(query[:index])
    if query[:key].is_a?(Array)
      filter_pending_records(find_by_sql_with_kasket_on_id_array(query[:key]), &blk)
    else
      if value = Kasket.cache.read(query[:key])
        # Identified a specific edge case where memcached server returns 0x00 binary protocol response with no data
        # when the node is being rebooted which causes the Dalli memcached client to return a TrueClass object instead of nil
        # see: https://github.com/petergoldstein/dalli/blob/31dabf19d3dd94b348a00a59fe5a7b8fa80ce3ad/lib/dalli/server.rb#L520
        # and: https://github.com/petergoldstein/dalli/issues/390
        #
        # The code in this first condition of TrueClass === true  will
        # skip the kasket cache for these specific objects and go directly to SQL for retrieval.
        result_set = if value.is_a?(TrueClass)
          find_by_sql_without_kasket(sql, binds, *restargs, **kwargs, &blk)
        elsif value.is_a?(Array)
          # The data from the Kasket cache is a list of keys to other Kasket entries.
          # This usually happens when we're trying to load a collection association,
          # e.g. a list of comments using their post_id in the query.
          # Do not report a cache hit yet, and defer it until we've verified that at
          # least one of the retrieved keys is actually in the cache.
          filter_pending_records(find_by_sql_with_kasket_on_id_array(value))
        else
          # Direct cache hit for the key.
          Events.report("cache_hit", self)
          filter_pending_records(Array.wrap(value).collect { |record| instantiate(record.dup, &blk) })
        end

        payload = {
          record_count: result_set.length,
          class_name: to_s
        }

        ActiveSupport::Notifications.instrument('instantiation.active_record', payload) { result_set }
      else
        store_in_kasket(query[:key], find_by_sql_without_kasket(sql, binds, *restargs, **kwargs, &blk))
      end
    end
  else
    find_by_sql_without_kasket(sql, binds, *restargs, **kwargs, &blk)
  end
end
find_by_sql_with_kasket_on_id_array(keys, &blk) click to toggle source
# File lib/kasket/read_mixin.rb, line 66
def find_by_sql_with_kasket_on_id_array(keys, &blk)
  key_attributes_map = Kasket.cache.read_multi(*keys)

  found_keys, missing_keys = keys.partition {|k| key_attributes_map[k] }
  # Only report a cache hit if at least some keys were found in the cache.
  Events.report("cache_hit", self) if found_keys.any?

  found_keys.each {|k| key_attributes_map[k] = instantiate(key_attributes_map[k].dup, &blk) }
  key_attributes_map.merge!(missing_records_from_db(missing_keys))

  key_attributes_map.values.compact
end

Protected Instance Methods

filter_pending_records(records) click to toggle source
# File lib/kasket/read_mixin.rb, line 81
def filter_pending_records(records)
  if pending_records = Kasket.pending_records
    records.map { |record| pending_records.fetch(record, record) }.compact
  else
    records
  end
end
missing_records_from_db(missing_keys) click to toggle source
# File lib/kasket/read_mixin.rb, line 89
def missing_records_from_db(missing_keys)
  return {} if missing_keys.empty?

  id_key_map = Hash[missing_keys.map {|key| [key.split('=').last.to_i, key] }]

  found = without_kasket { where(id: id_key_map.keys).to_a }
  found.each(&:store_in_kasket)
  Hash[found.map {|record| [id_key_map[record.id], record] }]
end
store_in_kasket(key, records) click to toggle source
# File lib/kasket/read_mixin.rb, line 99
def store_in_kasket(key, records)
  if records.size == 1
    records.first.store_in_kasket(key)
  elsif records.empty?
    ActiveRecord::Base.logger.debug("[KASKET] would have stored an empty resultset") if ActiveRecord::Base.logger
  elsif records.size <= Kasket::CONFIGURATION[:max_collection_size]
    if records.all?(&:kasket_cacheable?)
      instance_keys = records.map(&:store_in_kasket)
      Kasket.cache.write(key, instance_keys)
    end
  end
  records
end