debian-mirror-gitlab/app/experiments/application_experiment.rb

121 lines
4 KiB
Ruby
Raw Normal View History

2021-02-22 17:27:13 +05:30
# frozen_string_literal: true
2021-03-11 19:13:27 +05:30
class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/NamespacedClass
def enabled?
return false if Feature::Definition.get(feature_flag_name).nil? # there has to be a feature flag yaml file
return false unless Gitlab.dev_env_or_com? # we're in an environment that allows experiments
Feature.get(feature_flag_name).state != :off # rubocop:disable Gitlab/AvoidFeatureGet
end
2021-02-22 17:27:13 +05:30
def publish(_result)
track(:assignment) # track that we've assigned a variant for this context
Gon.global.push({ experiment: { name => signature } }, true) # push to client
end
def track(action, **event_args)
2021-03-11 19:13:27 +05:30
return unless should_track? # no events for opted out actors or excluded subjects
2021-02-22 17:27:13 +05:30
Gitlab::Tracking.event(name, action.to_s, **event_args.merge(
context: (event_args[:context] || []) << SnowplowTracker::SelfDescribingJson.new(
'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', signature
)
))
end
2021-03-11 19:13:27 +05:30
def rollout_strategy
# no-op override in inherited class as desired
end
def variants
# override as desired in inherited class with all variants + control
# %i[variant1 variant2 control]
#
# this will make sure we supply variants as these go together - rollout_strategy of :round_robin must have variants
raise NotImplementedError, "Inheriting class must supply variants as an array if :round_robin strategy is used" if rollout_strategy == :round_robin
end
2021-02-22 17:27:13 +05:30
private
2021-03-11 19:13:27 +05:30
def feature_flag_name
name.tr('/', '_')
end
2021-02-22 17:27:13 +05:30
def resolve_variant_name
2021-03-11 19:13:27 +05:30
case rollout_strategy
when :round_robin
round_robin_rollout
else
percentage_rollout
end
end
def round_robin_rollout
Strategy::RoundRobin.new(feature_flag_name, variants).execute
end
def percentage_rollout
return variant_names.first if Feature.enabled?(feature_flag_name, self, type: :experiment, default_enabled: :yaml)
2021-03-08 18:12:59 +05:30
nil # Returning nil vs. :control is important for not caching and rollouts.
2021-02-22 17:27:13 +05:30
end
# Cache is an implementation on top of Gitlab::Redis::SharedState that also
# adheres to the ActiveSupport::Cache::Store interface and uses the redis
# hash data type.
#
# Since Gitlab::Experiment can use any type of caching layer, utilizing the
# long lived shared state interface here gives us an efficient way to store
# context keys and the variant they've been assigned -- while also giving us
# a simple way to clean up an experiments data upon resolution.
#
# The data structure:
# key: experiment.name
# fields: context key => variant name
#
# The keys are expected to be `experiment_name:context_key`, which is the
# default cache key strategy. So running `cache.fetch("foo:bar", "value")`
# would create/update a hash with the key of "foo", with a field named
# "bar" that has "value" assigned to it.
2021-03-11 19:13:27 +05:30
class Cache < ActiveSupport::Cache::Store # rubocop:disable Gitlab/NamespacedClass
2021-02-22 17:27:13 +05:30
# Clears the entire cache for a given experiment. Be careful with this
# since it would reset all resolved variants for the entire experiment.
def clear(key:)
key = hkey(key)[0] # extract only the first part of the key
pool do |redis|
case redis.type(key)
when 'hash', 'none' then redis.del(key)
else raise ArgumentError, 'invalid call to clear a non-hash cache key'
end
end
end
private
def pool
raise ArgumentError, 'missing block' unless block_given?
Gitlab::Redis::SharedState.with { |redis| yield redis }
end
def hkey(key)
2021-03-08 18:12:59 +05:30
key.to_s.split(':') # this assumes the default strategy in gitlab-experiment
2021-02-22 17:27:13 +05:30
end
def read_entry(key, **options)
value = pool { |redis| redis.hget(*hkey(key)) }
value.nil? ? nil : ActiveSupport::Cache::Entry.new(value)
end
def write_entry(key, entry, **options)
return false if entry.value.blank? # don't cache any empty values
pool { |redis| redis.hset(*hkey(key), entry.value) }
end
def delete_entry(key, **options)
pool { |redis| redis.hdel(*hkey(key)) }
end
end
end