debian-mirror-gitlab/lib/gitlab/utils/strong_memoize.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

148 lines
4.3 KiB
Ruby
Raw Normal View History

2019-02-15 15:39:39 +05:30
# frozen_string_literal: true
2018-03-17 18:26:18 +05:30
module Gitlab
module Utils
module StrongMemoize
# Instead of writing patterns like this:
#
# def trigger_from_token
# return @trigger if defined?(@trigger)
#
# @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
# end
#
# We could write it like:
#
# include Gitlab::Utils::StrongMemoize
#
# def trigger_from_token
2022-08-27 11:52:29 +05:30
# Ci::Trigger.find_by_token(params[:token].to_s)
# end
# strong_memoize_attr :trigger_from_token
#
# def enabled?
# Feature.enabled?(:some_feature)
# end
2023-03-17 16:20:25 +05:30
# strong_memoize_attr :enabled?
2022-08-27 11:52:29 +05:30
#
2018-03-17 18:26:18 +05:30
def strong_memoize(name)
2022-05-07 20:08:51 +05:30
key = ivar(name)
if instance_variable_defined?(key)
instance_variable_get(key)
2018-03-17 18:26:18 +05:30
else
2022-05-07 20:08:51 +05:30
instance_variable_set(key, yield)
2018-03-17 18:26:18 +05:30
end
end
2023-06-20 00:43:36 +05:30
# Works the same way as "strong_memoize" but takes
# a second argument - expire_in. This allows invalidate
# the data after specified number of seconds
def strong_memoize_with_expiration(name, expire_in)
key = ivar(name)
expiration_key = "#{key}_expired_at"
if instance_variable_defined?(expiration_key)
expire_at = instance_variable_get(expiration_key)
clear_memoization(name) if Time.current > expire_at
end
if instance_variable_defined?(key)
instance_variable_get(key)
else
value = instance_variable_set(key, yield)
instance_variable_set(expiration_key, Time.current + expire_in)
value
end
end
2023-01-13 00:05:48 +05:30
def strong_memoize_with(name, *args)
container = strong_memoize(name) { {} }
if container.key?(args)
container[args]
else
container[args] = yield
end
end
2019-12-04 20:38:33 +05:30
def strong_memoized?(name)
2023-03-17 16:20:25 +05:30
key = ivar(StrongMemoize.normalize_key(name))
instance_variable_defined?(key)
2019-12-04 20:38:33 +05:30
end
2018-03-17 18:26:18 +05:30
def clear_memoization(name)
2023-03-17 16:20:25 +05:30
key = ivar(StrongMemoize.normalize_key(name))
2022-05-07 20:08:51 +05:30
remove_instance_variable(key) if instance_variable_defined?(key)
2018-03-17 18:26:18 +05:30
end
2022-08-27 11:52:29 +05:30
module StrongMemoizeClassMethods
2023-03-17 16:20:25 +05:30
def strong_memoize_attr(method_name)
member_name = StrongMemoize.normalize_key(method_name)
2022-08-27 11:52:29 +05:30
2023-03-17 16:20:25 +05:30
StrongMemoize.send(:do_strong_memoize, self, method_name, member_name) # rubocop:disable GitlabSecurity/PublicSend
2022-08-27 11:52:29 +05:30
end
end
def self.included(base)
base.singleton_class.prepend(StrongMemoizeClassMethods)
end
2018-03-17 18:26:18 +05:30
private
2022-05-07 20:08:51 +05:30
# Convert `"name"`/`:name` into `:@name`
#
# Depending on a type ensure that there's a single memory allocation
2018-03-17 18:26:18 +05:30
def ivar(name)
2023-01-13 00:05:48 +05:30
case name
when Symbol
2022-05-07 20:08:51 +05:30
name.to_s.prepend("@").to_sym
2023-01-13 00:05:48 +05:30
when String
2022-05-07 20:08:51 +05:30
:"@#{name}"
else
raise ArgumentError, "Invalid type of '#{name}'"
end
2018-03-17 18:26:18 +05:30
end
2022-08-27 11:52:29 +05:30
2023-03-17 16:20:25 +05:30
class << self
def normalize_key(key)
return key unless key.end_with?('!', '?')
# Replace invalid chars like `!` and `?` with allowed Unicode codeparts.
key.to_s.tr('!?', "\uFF01\uFF1F")
end
2022-08-27 11:52:29 +05:30
private
def do_strong_memoize(klass, method_name, member_name)
method = klass.instance_method(method_name)
2023-03-04 22:38:38 +05:30
unless method.arity == 0
raise <<~ERROR
Using `strong_memoize_attr` on methods with parameters is not supported.
Use `strong_memoize_with` instead.
See https://docs.gitlab.com/ee/development/utilities.html#strongmemoize
ERROR
end
2022-08-27 11:52:29 +05:30
# Methods defined within a class method are already public by default, so we don't need to
# explicitly make them public.
scope = %i[private protected].find do |scope|
klass.send("#{scope}_instance_methods") # rubocop:disable GitlabSecurity/PublicSend
.include? method_name
end
2023-03-04 22:38:38 +05:30
klass.define_method(method_name) do |&block|
2022-08-27 11:52:29 +05:30
strong_memoize(member_name) do
2023-03-04 22:38:38 +05:30
method.bind_call(self, &block)
2022-08-27 11:52:29 +05:30
end
end
klass.send(scope, method_name) if scope # rubocop:disable GitlabSecurity/PublicSend
end
end
2018-03-17 18:26:18 +05:30
end
end
end