summaryrefslogtreecommitdiff
path: root/app/experiments/members/invite_email_experiment.rb
blob: 893061e34f3e0b4df2aef9ce447d9d489d127bbf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# frozen_string_literal: true

module Members
  class InviteEmailExperiment < ApplicationExperiment
    exclude { context.actor.created_by.blank? }
    exclude { context.actor.created_by.avatar_url.nil? }

    INVITE_TYPE = 'initial_email'

    def self.initial_invite_email?(invite_type)
      invite_type == INVITE_TYPE
    end

    def resolve_variant_name
      RoundRobin.new(feature_flag_name, %i[activity control]).execute
    end
  end

  class RoundRobin
    CacheError = Class.new(StandardError)

    COUNTER_EXPIRE_TIME = 86400 # one day

    def initialize(key, variants)
      @key = key
      @variants = variants
    end

    def execute
      increment_counter
      resolve_variant_name
    end

    # When the counter would expire
    #
    # @api private Used internally by SRE and debugging purpose
    # @return [Integer] Number in seconds until expiration or false if never
    def counter_expires_in
      Gitlab::Redis::SharedState.with do |redis|
        redis.ttl(key)
      end
    end

    # Return the actual counter value
    #
    # @return [Integer] value
    def counter_value
      Gitlab::Redis::SharedState.with do |redis|
        (redis.get(key) || 0).to_i
      end
    end

    # Reset the counter
    #
    # @private Used internally by SRE and debugging purpose
    # @return [Boolean] whether reset was a success
    def reset!
      redis_cmd do |redis|
        redis.del(key)
      end
    end

    private

    attr_reader :key, :variants

    # Increase the counter
    #
    # @return [Boolean] whether operation was a success
    def increment_counter
      redis_cmd do |redis|
        redis.incr(key)
        redis.expire(key, COUNTER_EXPIRE_TIME)
      end
    end

    def resolve_variant_name
      remainder = counter_value % variants.size

      variants[remainder]
    end

    def redis_cmd
      Gitlab::Redis::SharedState.with { |redis| yield(redis) }

      true
    rescue CacheError => e
      Gitlab::AppLogger.warn("GitLab: An unexpected error occurred in writing to Redis: #{e}")

      false
    end
  end
end