summaryrefslogtreecommitdiff
path: root/lib/api/helpers/caching.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/api/helpers/caching.rb')
-rw-r--r--lib/api/helpers/caching.rb65
1 files changed, 62 insertions, 3 deletions
diff --git a/lib/api/helpers/caching.rb b/lib/api/helpers/caching.rb
index d0f22109879..f24ac7302c1 100644
--- a/lib/api/helpers/caching.rb
+++ b/lib/api/helpers/caching.rb
@@ -11,6 +11,11 @@ module API
# @return [ActiveSupport::Duration]
DEFAULT_EXPIRY = 1.day
+ # @return [Hash]
+ DEFAULT_CACHE_OPTIONS = {
+ race_condition_ttl: 5.seconds
+ }.freeze
+
# @return [ActiveSupport::Cache::Store]
def cache
Rails.cache
@@ -40,7 +45,7 @@ module API
# @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: DEFAULT_EXPIRY, **presenter_args)
+ def present_cached(obj_or_collection, with:, cache_context: -> (_) { current_user&.cache_key }, expires_in: DEFAULT_EXPIRY, **presenter_args)
json =
if obj_or_collection.is_a?(Enumerable)
cached_collection(
@@ -63,8 +68,59 @@ module API
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, **cache_opts)
+ json = cache.fetch(key, **apply_default_cache_options(cache_opts)) do
+ response = yield
+
+ if response.is_a?(Gitlab::Json::PrecompiledJson)
+ response.to_s
+ else
+ Gitlab::Json.dump(response.as_json)
+ end
+ 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
+
# Optionally uses a `Proc` to add context to a cache key
#
# @param object [Object] must respond to #cache_key
@@ -119,8 +175,11 @@ module API
objs.flatten!
map = multi_key_map(objs, context: context)
- cache.fetch_multi(*map.keys, **kwargs) do |key|
- yield map[key]
+ # TODO: `contextual_cache_key` should be constructed based on the guideline https://docs.gitlab.com/ee/development/redis.html#multi-key-commands.
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ cache.fetch_multi(*map.keys, **kwargs) do |key|
+ yield map[key]
+ end
end
end