class ApiCallCache

Constants

DEFAULT_ACCESS_TOKEN
VERSION

Attributes

_acc_log_entry[RW]

Public Class Methods

api_cache_redis_instance(redis_inst) click to toggle source
# File lib/api_call_cache.rb, line 18
def self.api_cache_redis_instance(redis_inst)
  raise ArgumentError, 'requires a redis instance' unless redis_inst
  @@redis_inst = redis_inst 
end
api_get(base_url_key, rel_path, req_params = {}, req_opts = {}) click to toggle source

Class methods

# File lib/api_call_cache.rb, line 69
def self.api_get(base_url_key, rel_path, req_params = {}, req_opts = {})
  req_opts = req_opts.merge({base_url_key: base_url_key})
  cached_api_call(:get, rel_path, req_params, req_opts)    
end
base_urls(base_urls_hash) click to toggle source
# File lib/api_call_cache.rb, line 27
def self.base_urls(base_urls_hash)
  @@acc_base_urls = Hashie.symbolize_keys(base_urls_hash)
end
cached_api_call(req_type, rel_path, req_params = {}, req_opts = {}) click to toggle source
# File lib/api_call_cache.rb, line 74
def self.cached_api_call(req_type, rel_path, req_params = {}, req_opts = {})
  obj = self.new
  obj.cached_api_call_core(req_type, rel_path, req_params, req_opts)
rescue Exception => exp
  obj.acc_log_entry[:status] = 'exception'
  obj.acc_log_entry[:desc] = exp.message
  raise
ensure
  @@acc_logger.info obj.acc_log_entry.to_a.flatten.join("\t") if @@acc_logger
end
configure() { |self| ... } click to toggle source
# File lib/api_call_cache.rb, line 14
def self.configure
  yield self
end
get_redis() click to toggle source
# File lib/api_call_cache.rb, line 54
def self.get_redis
  @@redis_inst
end
logger_path(log_file = 'log/api_call_cache.log', rotation = 'monthly') click to toggle source
# File lib/api_call_cache.rb, line 23
def self.logger_path(log_file = 'log/api_call_cache.log', rotation = 'monthly')
  @@acc_logger = Logger.new(log_file, rotation)
end
make_api_call(req_type, url, access_token, body=nil) click to toggle source
# File lib/api_call_cache.rb, line 85
def self.make_api_call(req_type, url, access_token, body=nil)
  self.new.make_api_call(req_type, url, access_token, body)
end
req_defaults() click to toggle source
# File lib/api_call_cache.rb, line 36
def self.req_defaults
  @req_opt_defaults ||= {
                          base_url_key: '',
                          access_token: DEFAULT_ACCESS_TOKEN,
                          from_cache: true,
                          override_cache_expiry: nil,     # seconds. Overrides Cache-Control max-age value
                          append_dot_json: true,
                          override_params_cache_key: nil, # If provided, this value will be used
                                                          # as the 'params' part of the cache key,
                                                          # instead of manually 'hashed_params'
                                                          #
                                                          # Useful if the api doesn't accept explicit
                                                          # params, but is distinguished solely on the
                                                          # basis of some implicit data like
                                                          # access_token etc
                        }
end
salt(salt_str) click to toggle source
# File lib/api_call_cache.rb, line 31
def self.salt(salt_str)
   raise ArgumentError, 'requires a string as salt' if salt_str.empty?
  @@acc_salt = salt_str
end
set_tz_offset(offset = '+05:30') click to toggle source
# File lib/api_call_cache.rb, line 62
def self.set_tz_offset(offset = '+05:30')
  @@tz_offset = offset
end

Public Instance Methods

acc_log_entry() click to toggle source
# File lib/api_call_cache.rb, line 251
def acc_log_entry
  @_acc_log_entry ||= {
    time:         log_time,
    status:       '', # hit/miss/exception/ignore
    api:          '',
    resp_time:    '',
    resp_code:    '',
    write_back:   'false',
    user_ttl:     '', # Suggested by the internal user for overriding
    src_ttl:      '', # Suggested by the external server
    act_ttl:      '', # Actual TTL after dithering
    params_h:     '', # hashed relative path
    params_f:     '', # full form relative path
    cache_key:    '',
    desc:         '',
  }
end
cached_api_call_core(req_type, rel_path, req_params, req_opts) click to toggle source

Instance methods

# File lib/api_call_cache.rb, line 92
def cached_api_call_core(req_type, rel_path, req_params, req_opts)

  Hashie.symbolize_keys!(req_opts)
  req_opts = self.class.req_defaults.merge(req_opts)
  req_type = req_type.to_s.downcase.to_sym
  
  # Get from cache if required
  cached_body = nil

  base_url_key = req_opts[:base_url_key].to_s
  raise 'Base URL KEY not found' if base_url_key.empty?

  acc_log_entry[:api] = [base_url_key.to_s, rel_path.to_s].join('/')

  base_url = @@acc_base_urls[base_url_key.to_sym].to_s
  raise "Base URL not found for #{base_url_key}" if base_url.empty?

  cache_key       = gen_api_call_cache_key(req_type, base_url_key, 
                                           rel_path, req_params,
                                           req_opts[:override_params_cache_key])

  try_from_cache  = (req_type == :get) && req_opts[:from_cache]
  write_to_cache  = (req_type == :get)
  acc_log_entry[:cache_key] = cache_key
  cache_miss  = false

  if try_from_cache
    cached_info   = get_redis.hgetall(cache_key)
    cached_body   = cached_info['body']
    cached_status = cached_info['status'].to_i

    cache_miss = cached_body.nil?  # NOTE: 'nil?' used here on purpose instead of 'blank?'
    acc_log_entry[:status] = cache_miss ? 'miss' : 'hit'

    ttl = get_redis.ttl(cache_key).to_i.seconds
  else
    acc_log_entry[:status] = 'ignore'
  end

  if cache_miss
    
    rel_url = gen_api_call_rel_url(rel_path, req_params, req_opts[:append_dot_json])

    acc_log_entry[:params_f] = req_params.to_s

    tic = ::Time.now
    api_resp = make_api_call(req_type, "#{base_url}/#{rel_url}", 
                             req_opts[:access_token])
    toc = ::Time.now

    acc_log_entry[:resp_time] = ((toc - tic)*1000).round.to_s
    acc_log_entry[:resp_code] = api_resp.status

    result_body   = api_resp.body
    result_status = api_resp.status.to_i

    if write_to_cache && api_resp.try(:ok?)
      ttl = write_to_api_cache(cache_key, api_resp, result_body, result_status,
                         req_opts[:override_cache_expiry])

      acc_log_entry[:expires_at] = (ttl.to_i.seconds.from_now).to_time.localtime(@@tz_offset).to_s
    end
  else
    # Found in cache. Using it.
    result_body   = cached_body
    result_status = cached_status
    acc_log_entry[:expires_at] = (ttl.seconds.from_now).to_time.localtime(@@tz_offset).to_s
  end

  Hashie::Mash.new(status:  result_status, 
                   body:    result_body, 
                   source:  (try_from_cache && !cache_miss) ? :cache : :api_call,
                   ok:      ::HTTP::Status::SUCCESSFUL_STATUS.include?(result_status))
end
gen_api_call_cache_key(_req_type, base_url_key, rel_path, req_params, hashed_params = nil) click to toggle source
# File lib/api_call_cache.rb, line 204
def gen_api_call_cache_key(_req_type, base_url_key, rel_path, req_params, hashed_params = nil)

  if hashed_params.nil? || hashed_params.empty?
    # Since params hash key hasn't been overridden using
    plain = req_params.sort.to_h.to_query # sort req_params alphabetically
    hashed_params = OpenSSL::HMAC.hexdigest('sha256', @@acc_salt, plain)
  end

  acc_log_entry[:params_h] = hashed_params

  ['api_call_cache', 'api', base_url_key, rel_path, hashed_params].join(':')
end
gen_api_call_rel_url(rel_path, req_params, app_dot_json) click to toggle source
# File lib/api_call_cache.rb, line 199
def gen_api_call_rel_url(rel_path, req_params, app_dot_json)
  rel_path += '.json' if app_dot_json
  req_params.empty? ? rel_path : [rel_path, req_params.to_query].join('?')
end
get_cache_expiry(response, user_expiry = nil) click to toggle source
# File lib/api_call_cache.rb, line 218
def get_cache_expiry(response, user_expiry = nil)
  # Extract server defined TTL
  ext_ttl = nil
  if user_expiry.nil?
    cache_ctrl_hdr = response.headers['Api-Cache-Control'].to_s
    cache_ctrl_hdr = response.headers['Cache-Control'] if cache_ctrl_hdr.empty?
    cache_ctrl_hdr.split(',').map(&:strip).each do |opt|
      opts = opt.split('=')
      next if opts.first != 'max-age'
      ext_ttl = opts.last.to_i
      break
    end
  end

  expiry = user_expiry || ext_ttl || FALLBACK_TTL

  acc_log_entry[:user_ttl] =  user_expiry.to_s
  acc_log_entry[:src_ttl]  =   ext_ttl.to_s

  # Dither cache expiry
  dither_factor = 0.1

  # final expiry = given expiry + 10% variance <max 2 minutes>
  expiry += rand * ([expiry * dither_factor, 5.minutes].min)
  expiry = expiry.round

  acc_log_entry[:act_ttl] =  expiry.to_s

  expiry
end
get_redis() click to toggle source
# File lib/api_call_cache.rb, line 58
def get_redis
  self.class.get_redis
end
log_time() click to toggle source
# File lib/api_call_cache.rb, line 269
def log_time
  time = Time.now.localtime(@@tz_offset)
  usec = time.usec.to_s.rjust(6, '0')
  time.strftime "%Y-%m-%d %H:%M:%S.#{usec} %z"
end
make_api_call(req_type, url, access_token, body = nil) click to toggle source
# File lib/api_call_cache.rb, line 181
def make_api_call(req_type, url, access_token, body = nil)
  access_token = Rack::OAuth2::AccessToken::Bearer.new(access_token: access_token)
  return access_token.send(req_type, url, body)
rescue HTTPClient::ReceiveTimeoutError => exp
  return Hashie::Mash.new(status: 408, 
                          body:   {}.to_json, 
                          ok?:    false,
                          timeout?: true)
rescue SocketError => exp
  return Hashie::Mash.new(status: 400, 
                          body:   {error: exp.message}.to_json, 
                          ok?:    false)
rescue Errno::ECONNREFUSED => exp
  return Hashie::Mash.new(status: 403, 
                          body:   {error: exp.message}.to_json, 
                          ok?:    false)
end
write_to_api_cache(cache_key, api_resp, cache_body, cache_status, override_cache_expiry) click to toggle source
# File lib/api_call_cache.rb, line 167
def write_to_api_cache(cache_key, api_resp, cache_body, cache_status, override_cache_expiry)
  expiry = get_cache_expiry(api_resp, override_cache_expiry)

  acc_log_entry[:write_back] = 'true'

  if !expiry.zero?
    get_redis.hmset(cache_key, 'body', cache_body, 
                               'status', cache_status)
  end

  get_redis.expire(cache_key, expiry)
  expiry
end