gitlab-org--gitlab-foss/lib/api/helpers/caching.rb

130 lines
4.6 KiB
Ruby

# frozen_string_literal: true
# Grape helpers for caching.
#
# This module helps introduce standardised caching into the Grape API
# in a similar manner to the standard Grape DSL.
module API
module Helpers
module Caching
include Gitlab::Cache::Helpers
# @return [Hash]
DEFAULT_CACHE_OPTIONS = {
race_condition_ttl: 5.seconds,
version: 1
}.freeze
# @return [Array]
PAGINATION_HEADERS = %w[X-Per-Page X-Page X-Next-Page X-Prev-Page Link X-Total X-Total-Pages].freeze
# This is functionally equivalent to the standard `#present` used in
# Grape endpoints, but the JSON for the object, or for each object of
# a collection, will be cached.
#
# With a collection all the keys will be fetched in a single call and the
# Entity rendered for those missing from the cache, which are then written
# back into it.
#
# Both the single object, and all objects inside a collection, must respond
# to `#cache_key`.
#
# To override the Grape formatter we return a custom wrapper in
# `Gitlab::Json::PrecompiledJson` which tells the `Gitlab::Json::GrapeFormatter`
# to export the string without conversion.
#
# A cache context can be supplied to add more context to the cache key. This
# defaults to including the `current_user` in every key for safety, unless overridden.
#
# @param obj_or_collection [Object, Enumerable<Object>] the object or objects to render
# @param with [Grape::Entity] the entity to use for rendering
# @param cache_context [Proc] a proc to call for each object to provide more context to the cache key
# @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry
# @param presenter_args [Hash] keyword arguments to be passed to the entity
# @return [Gitlab::Json::PrecompiledJson]
def present_cached(obj_or_collection, with:, cache_context: -> (_) { current_user&.cache_key }, expires_in: Gitlab::Cache::Helpers::DEFAULT_EXPIRY, **presenter_args)
json =
if obj_or_collection.is_a?(Enumerable)
cached_collection(
obj_or_collection,
presenter: with,
presenter_args: presenter_args,
context: cache_context,
expires_in: expires_in
)
else
cached_object(
obj_or_collection,
presenter: with,
presenter_args: presenter_args,
context: cache_context,
expires_in: expires_in
)
end
body Gitlab::Json::PrecompiledJson.new(json)
end
# Action caching implementation
#
# This allows you to wrap an entire API endpoint call in a cache, useful
# for short TTL caches to effectively rate-limit an endpoint. The block
# will be converted to JSON and cached, and returns a
# `Gitlab::Json::PrecompiledJson` object which will be exported without
# secondary conversion.
#
# @param key [Object] any object that can be converted into a cache key
# @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry
# @return [Gitlab::Json::PrecompiledJson]
def cache_action(key, **custom_cache_opts)
cache_opts = apply_default_cache_options(custom_cache_opts)
json, cached_headers = cache.fetch(key, **cache_opts) do
response = yield
cached_body = response.is_a?(Gitlab::Json::PrecompiledJson) ? response.to_s : Gitlab::Json.dump(response.as_json)
cached_headers = header.slice(*PAGINATION_HEADERS)
[cached_body, cached_headers]
end
cached_headers.each do |key, value|
next if header.key?(key)
header key, value
end
body Gitlab::Json::PrecompiledJson.new(json)
end
# Conditionally cache an action
#
# Perform a `cache_action` only if the conditional passes
def cache_action_if(conditional, *opts, **kwargs)
if conditional
cache_action(*opts, **kwargs) do
yield
end
else
yield
end
end
# Conditionally cache an action
#
# Perform a `cache_action` unless the conditional passes
def cache_action_unless(conditional, *opts, **kwargs)
cache_action_if(!conditional, *opts, **kwargs) do
yield
end
end
private
def apply_default_cache_options(opts = {})
DEFAULT_CACHE_OPTIONS.merge(opts)
end
end
end
end