deliveroo/routemaster-drain

View on GitHub
lib/routemaster/middleware/response_caching.rb

Summary

Maintainability
A
2 hrs
Test Coverage
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