lib/routemaster/middleware/response_caching.rb
require 'wisper'
require 'routemaster/event_index'
require 'routemaster/cache_key'
module Routemaster
module Middleware
class ResponseCaching
BODY_FIELD_TEMPLATE = 'v:{version},l:{locale},body'.freeze
HEADERS_FIELD_TEMPLATE = 'v:{version},l:{locale},headers'.freeze
VERSION_REGEX = /application\/json;v=(?<version>\S*)/
RESPONSE_CACHING_OPT_HEADER = 'X-routemaster_drain.opt_cache'.freeze
def initialize(app, options = {})
@app = app
@cache = options.fetch(:cache) { Config.cache_redis }
@expiry = Config.cache_expiry
@listener = options[:listener]
end
def call(env)
@cache.del(cache_key(env)) if %i(patch delete).include?(env.method)
return @app.call(env) if env.method != :get
fetch_from_cache(env) || fetch_from_service(env, event_index(env))
end
private
def fetch_from_service(env, event_index)
@app.call(env).on_complete do |response_env|
response = response_env.response
if response.success? && cache_enabled?(env)
namespaced_key = "#{@cache.namespace}:#{cache_key(env)}"
@cache.redis.node_for(namespaced_key).multi do |node|
if Config.logger.debug?
Config.logger.debug("DRAIN: Saving #{url(env)} with a event index of #{event_index}")
end
node.hmset(namespaced_key,
body_cache_field(env), response.body,
headers_cache_field(env), Marshal.dump(response.headers),
:most_recent_index, event_index)
node.expire(namespaced_key, @expiry)
end
@listener._publish(:cache_miss, url(env)) if @listener
end
end
end
def fetch_from_cache(env)
return nil unless cache_enabled?(env)
body, headers, most_recent_index, current_index = currently_cached_content(env)
unless most_recent_index.to_i == current_index.to_i && body && headers
Config.logger.debug("DRAIN: Cache miss #{url(env)} - index_recent: #{most_recent_index.to_i}") if Config.logger.debug?
return nil
end
Config.logger.debug("DRAIN: Cache hit #{url(env)} - index_recent: #{most_recent_index.to_i}") if Config.logger.debug?
@listener._publish(:cache_hit, url(env)) if @listener
Faraday::Response.new(status: 200,
body: body,
response_headers: Marshal.load(headers),
request: {})
end
def body_cache_field(env)
BODY_FIELD_TEMPLATE
.gsub('{version}', version(env).to_s)
.gsub('{locale}', locale(env).to_s)
end
def headers_cache_field(env)
HEADERS_FIELD_TEMPLATE
.gsub('{version}', version(env).to_s)
.gsub('{locale}', locale(env).to_s)
end
def cache_key(env)
CacheKey.url_key(url(env))
end
def url(env)
env.url.to_s
end
def version(env)
(env.request_headers['Accept'] || '')[VERSION_REGEX, 1]
end
def locale(env)
env.request_headers['Accept-Language']
end
def cache_enabled?(env)
env.request_headers[RESPONSE_CACHING_OPT_HEADER].to_s == 'true'
end
def event_index(env)
Routemaster::EventIndex.new(url(env)).current
end
def currently_cached_content(env)
@cache.hmget(cache_key(env),
body_cache_field(env),
headers_cache_field(env),
:most_recent_index,
:current_index)
end
end
end
end