From add0abb5055141c5a39beeb01c8eb1b3129b147c Mon Sep 17 00:00:00 2001
From: Simon Tomlinson <stomlinson@gitlab.com>
Date: Wed, 4 Sep 2024 16:23:08 -0500
Subject: [PATCH] Flipper per-pod actor

Adds Feature.current_pod, a flipper actor that is the same for a given
kubernetes pod.
---
 lib/feature.rb           | 16 +++++++++++++
 spec/lib/feature_spec.rb | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 68 insertions(+)

diff --git a/lib/feature.rb b/lib/feature.rb
index b9307ce66dd1a..e2b2039a04f7e 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -44,6 +44,16 @@ def flipper_id
     end
   end
 
+  # Generates the same flipper_id for a given kubernetes pod,
+  # or for the entire gitlab application if deployed on a single host.
+  class FlipperPod
+    attr_reader :flipper_id
+
+    def initialize
+      @flipper_id = "FlipperPod:#{Socket.gethostname}".freeze
+    end
+  end
+
   # Generates a unique flipper_id for the current GitLab instance.
   class FlipperGitlabInstance
     attr_reader :flipper_id
@@ -253,6 +263,10 @@ def current_request
       end
     end
 
+    def current_pod
+      @flipper_pod ||= FlipperPod.new
+    end
+
     def gitlab_instance
       @flipper_gitlab_instance ||= FlipperGitlabInstance.new
     end
@@ -290,6 +304,8 @@ def sanitized_thing(thing)
         gitlab_instance
       when :request, :current_request
         current_request
+      when :pod, :current_pod
+        current_pod
       else
         thing
       end
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index acf39fbbe5350..b2bc24970a1df 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -55,6 +55,29 @@ def wrap_all_methods_with_flag_check(lb, flag)
     end
   end
 
+  describe '.current_pod' do
+    it 'returns a FlipperPod with a flipper_id' do
+      expect(described_class.current_pod).to respond_to(:flipper_id)
+    end
+
+    it 'is the same flipper_id within a process' do
+      previous_id = described_class.current_pod.flipper_id
+
+      expect(previous_id).to eq(described_class.current_pod.flipper_id)
+    end
+
+    it 'is a different flipper_id in a new host' do
+      previous_id = described_class.current_pod.flipper_id
+
+      # Simulate a new process by changing host,
+      previous_host = Socket.gethostname
+      allow(Socket).to receive(:gethostname).and_return("#{previous_host}-1")
+
+      new_id = Feature::FlipperPod.new.flipper_id # Bypass caching
+      expect(previous_id).not_to eq(new_id)
+    end
+  end
+
   describe '.gitlab_instance' do
     it 'returns a FlipperGitlabInstance with a flipper_id' do
       flipper_request = described_class.gitlab_instance
@@ -407,6 +430,35 @@ def wrap_all_methods_with_flag_check(lb, flag)
       end
     end
 
+    context 'with :pod actor' do
+      before do
+        stub_feature_flag_definition(:enabled_feature_flag)
+      end
+
+      it 'returns the same value in the same host' do
+        described_class.enable(:enabled_feature_flag, :current_pod)
+
+        expect(described_class.enabled?(:enabled_feature_flag, :current_pod)).to be_truthy
+      end
+
+      it 'returns different values in different hosts' do
+        number_of_times = 1_000
+        percentage = 50
+        described_class.enable_percentage_of_actors(:enabled_feature_flag, percentage)
+        results = { true => 0, false => 0 }
+        original_hostname = Socket.gethostname
+        number_of_times.times do |i|
+          allow(Socket).to receive(:gethostname).and_return("#{original_hostname}-#{i}")
+          flipper_thing = Feature::FlipperPod.new # Create a new one to bypass caching, we are simulating many different pods
+          result = described_class.enabled?(:enabled_feature_flag, flipper_thing)
+          results[result] += 1
+        end
+
+        percent_true = (results[true].to_f / (results[true] + results[false])) * 100
+        expect(percent_true).to be_within(5).of(percentage)
+      end
+    end
+
     context 'with a group member' do
       let(:key) { :awesome_feature }
       let(:guinea_pigs) { create_list(:user, 3) }
-- 
GitLab