From 47d420cf959731439868307a2284e20d08f901e8 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 24 Apr 2025 13:25:54 -0700 Subject: [PATCH 1/5] in progress change start index for flags --- .../FeatureManagement/FeatureManagementKeyValueAdapter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs index 76ab04e4..882b0e97 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs @@ -18,7 +18,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage internal class FeatureManagementKeyValueAdapter : IKeyValueAdapter { private FeatureFlagTracing _featureFlagTracing; - private int _featureFlagIndex = 0; + private int _featureFlagIndex = 1000000; public FeatureManagementKeyValueAdapter(FeatureFlagTracing featureFlagTracing) { @@ -74,7 +74,7 @@ public void OnChangeDetected(ConfigurationSetting setting = null) public void OnConfigUpdated() { - _featureFlagIndex = 0; + _featureFlagIndex = _instanceStartIndex; return; } From fe00d90d74bd0bbd110f03ae26bdab62c07f4e01 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 25 Apr 2025 12:59:53 -0700 Subject: [PATCH 2/5] edit --- .../FeatureManagement/FeatureManagementKeyValueAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs index 882b0e97..b7425a6b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs @@ -74,7 +74,7 @@ public void OnChangeDetected(ConfigurationSetting setting = null) public void OnConfigUpdated() { - _featureFlagIndex = _instanceStartIndex; + _featureFlagIndex = ; return; } From 6c5a8c9b4c13caa9c397c45872fb3ac7d62e0dd3 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 25 Apr 2025 15:05:48 -0700 Subject: [PATCH 3/5] progress --- .../FeatureManagementKeyValueAdapter.cs | 38 ++++- .../FeatureManagementTests.cs | 140 +++++++++--------- 2 files changed, 103 insertions(+), 75 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs index b7425a6b..12f9f844 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs @@ -18,11 +18,20 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage internal class FeatureManagementKeyValueAdapter : IKeyValueAdapter { private FeatureFlagTracing _featureFlagTracing; - private int _featureFlagIndex = 1000000; + private static int _globalFeatureFlagCounter = 1000000; + private readonly int _startFeatureFlagIndex; + private int _currentFeatureFlagIndex; + private int _processedFlagsCount; + private int _maxProcessedFlagsPerRefresh; public FeatureManagementKeyValueAdapter(FeatureFlagTracing featureFlagTracing) { _featureFlagTracing = featureFlagTracing ?? throw new ArgumentNullException(nameof(featureFlagTracing)); + + _startFeatureFlagIndex = Interlocked.Add(ref _globalFeatureFlagCounter, 0); + _currentFeatureFlagIndex = _startFeatureFlagIndex; + _processedFlagsCount = 0; + _maxProcessedFlagsPerRefresh = 0; } public Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken) @@ -44,6 +53,24 @@ public Task>> ProcessKeyValue(Configura return Task.FromResult>>(keyValues); } + // Track a new feature flag being processed and update the global counter + private void TrackProcessedFlag() + { + _processedFlagsCount++; + + // Update the max flags count if we have more flags in this cycle + if (_processedFlagsCount > _maxProcessedFlagsPerRefresh) + { + _maxProcessedFlagsPerRefresh = _processedFlagsCount; + + // Update the global counter to reserve enough space for our maximum observed flags + Interlocked.CompareExchange( + ref _globalFeatureFlagCounter, + _startFeatureFlagIndex + _maxProcessedFlagsPerRefresh, + _globalFeatureFlagCounter); + } + } + public bool CanProcess(ConfigurationSetting setting) { if (setting == null || @@ -74,8 +101,8 @@ public void OnChangeDetected(ConfigurationSetting setting = null) public void OnConfigUpdated() { - _featureFlagIndex = ; - + _currentFeatureFlagIndex = _startFeatureFlagIndex; + _processedFlagsCount = 0; return; } @@ -141,9 +168,10 @@ private List> ProcessMicrosoftSchemaFeatureFlag(Fea return keyValues; } - string featureFlagPath = $"{FeatureManagementConstants.FeatureManagementSectionName}:{FeatureManagementConstants.FeatureFlagsSectionName}:{_featureFlagIndex}"; + string featureFlagPath = $"{FeatureManagementConstants.FeatureManagementSectionName}:{FeatureManagementConstants.FeatureFlagsSectionName}:{_currentFeatureFlagIndex}"; - _featureFlagIndex++; + _currentFeatureFlagIndex++; + TrackProcessedFlag(); keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.Id}", featureFlag.Id)); diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs index 6756b949..b9e9fd94 100644 --- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs +++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs @@ -1955,66 +1955,66 @@ public void WithVariants() }) .Build(); - Assert.Equal("VariantsFeature1", config["feature_management:feature_flags:0:id"]); - Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]); - Assert.Equal("Big", config["feature_management:feature_flags:0:variants:0:name"]); - Assert.Equal("600px", config["feature_management:feature_flags:0:variants:0:configuration_value"]); - Assert.Equal("Small", config["feature_management:feature_flags:0:variants:1:name"]); - Assert.Equal("Disabled", config["feature_management:feature_flags:0:variants:1:status_override"]); - Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:default_when_disabled"]); - Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:default_when_enabled"]); - Assert.Equal("Big", config["feature_management:feature_flags:0:allocation:user:0:variant"]); - Assert.Equal("Marsha", config["feature_management:feature_flags:0:allocation:user:0:users:0"]); - Assert.Equal("John", config["feature_management:feature_flags:0:allocation:user:0:users:1"]); - Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:user:1:variant"]); - Assert.Equal("Alice", config["feature_management:feature_flags:0:allocation:user:1:users:0"]); - Assert.Equal("Bob", config["feature_management:feature_flags:0:allocation:user:1:users:1"]); - Assert.Equal("Big", config["feature_management:feature_flags:0:allocation:group:0:variant"]); - Assert.Equal("Ring1", config["feature_management:feature_flags:0:allocation:group:0:groups:0"]); - Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:group:1:variant"]); - Assert.Equal("Ring2", config["feature_management:feature_flags:0:allocation:group:1:groups:0"]); - Assert.Equal("Ring3", config["feature_management:feature_flags:0:allocation:group:1:groups:1"]); - Assert.Equal("Big", config["feature_management:feature_flags:0:allocation:percentile:0:variant"]); - Assert.Equal("0", config["feature_management:feature_flags:0:allocation:percentile:0:from"]); - Assert.Equal("50", config["feature_management:feature_flags:0:allocation:percentile:0:to"]); - Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:percentile:1:variant"]); - Assert.Equal("50", config["feature_management:feature_flags:0:allocation:percentile:1:from"]); - Assert.Equal("100", config["feature_management:feature_flags:0:allocation:percentile:1:to"]); - Assert.Equal("13992821", config["feature_management:feature_flags:0:allocation:seed"]); - - Assert.Equal("VariantsFeature2", config["feature_management:feature_flags:1:id"]); - Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]); - Assert.Equal("ObjectVariant", config["feature_management:feature_flags:1:variants:0:name"]); - Assert.Equal("Value1", config["feature_management:feature_flags:1:variants:0:configuration_value:Key1"]); - Assert.Equal("Value2", config["feature_management:feature_flags:1:variants:0:configuration_value:Key2:InsideKey2"]); - Assert.Equal("NumberVariant", config["feature_management:feature_flags:1:variants:1:name"]); - Assert.Equal("100", config["feature_management:feature_flags:1:variants:1:configuration_value"]); - Assert.Equal("NullVariant", config["feature_management:feature_flags:1:variants:2:name"]); - Assert.Equal("", config["feature_management:feature_flags:1:variants:2:configuration_value"]); + Assert.Equal("VariantsFeature1", config["feature_management:feature_flags:1000000:id"]); + Assert.Equal("True", config["feature_management:feature_flags:1000000:enabled"]); + Assert.Equal("Big", config["feature_management:feature_flags:1000000:variants:0:name"]); + Assert.Equal("600px", config["feature_management:feature_flags:1000000:variants:0:configuration_value"]); + Assert.Equal("Small", config["feature_management:feature_flags:1000000:variants:1:name"]); + Assert.Equal("Disabled", config["feature_management:feature_flags:1000000:variants:1:status_override"]); + Assert.Equal("Small", config["feature_management:feature_flags:1000000:allocation:default_when_disabled"]); + Assert.Equal("Small", config["feature_management:feature_flags:1000000:allocation:default_when_enabled"]); + Assert.Equal("Big", config["feature_management:feature_flags:1000000:allocation:user:0:variant"]); + Assert.Equal("Marsha", config["feature_management:feature_flags:1000000:allocation:user:0:users:0"]); + Assert.Equal("John", config["feature_management:feature_flags:1000000:allocation:user:0:users:1"]); + Assert.Equal("Small", config["feature_management:feature_flags:1000000:allocation:user:1:variant"]); + Assert.Equal("Alice", config["feature_management:feature_flags:1000000:allocation:user:1:users:0"]); + Assert.Equal("Bob", config["feature_management:feature_flags:1000000:allocation:user:1:users:1"]); + Assert.Equal("Big", config["feature_management:feature_flags:1000000:allocation:group:0:variant"]); + Assert.Equal("Ring1", config["feature_management:feature_flags:1000000:allocation:group:0:groups:0"]); + Assert.Equal("Small", config["feature_management:feature_flags:1000000:allocation:group:1:variant"]); + Assert.Equal("Ring2", config["feature_management:feature_flags:1000000:allocation:group:1:groups:0"]); + Assert.Equal("Ring3", config["feature_management:feature_flags:1000000:allocation:group:1:groups:1"]); + Assert.Equal("Big", config["feature_management:feature_flags:1000000:allocation:percentile:0:variant"]); + Assert.Equal("0", config["feature_management:feature_flags:1000000:allocation:percentile:0:from"]); + Assert.Equal("50", config["feature_management:feature_flags:1000000:allocation:percentile:0:to"]); + Assert.Equal("Small", config["feature_management:feature_flags:1000000:allocation:percentile:1:variant"]); + Assert.Equal("50", config["feature_management:feature_flags:1000000:allocation:percentile:1:from"]); + Assert.Equal("100", config["feature_management:feature_flags:1000000:allocation:percentile:1:to"]); + Assert.Equal("13992821", config["feature_management:feature_flags:1000000:allocation:seed"]); + + Assert.Equal("VariantsFeature2", config["feature_management:feature_flags:1000001:id"]); + Assert.Equal("False", config["feature_management:feature_flags:1000001:enabled"]); + Assert.Equal("ObjectVariant", config["feature_management:feature_flags:1000001:variants:0:name"]); + Assert.Equal("Value1", config["feature_management:feature_flags:1000001:variants:0:configuration_value:Key1"]); + Assert.Equal("Value2", config["feature_management:feature_flags:1000001:variants:0:configuration_value:Key2:InsideKey2"]); + Assert.Equal("NumberVariant", config["feature_management:feature_flags:1000001:variants:1:name"]); + Assert.Equal("100", config["feature_management:feature_flags:1000001:variants:1:configuration_value"]); + Assert.Equal("NullVariant", config["feature_management:feature_flags:1000001:variants:2:name"]); + Assert.Equal("", config["feature_management:feature_flags:1000001:variants:2:configuration_value"]); Assert.True(config - .GetSection("feature_management:feature_flags:1:variants:2") + .GetSection("feature_management:feature_flags:1000001:variants:2") .AsEnumerable() .ToDictionary(x => x.Key, x => x.Value) - .ContainsKey("feature_management:feature_flags:1:variants:2:configuration_value")); - Assert.Equal("MissingValueVariant", config["feature_management:feature_flags:1:variants:3:name"]); - Assert.Null(config["feature_management:feature_flags:1:variants:3:configuration_value"]); + .ContainsKey("feature_management:feature_flags:1000001:variants:2:configuration_value")); + Assert.Equal("MissingValueVariant", config["feature_management:feature_flags:1000001:variants:3:name"]); + Assert.Null(config["feature_management:feature_flags:1000001:variants:3:configuration_value"]); Assert.False(config - .GetSection("feature_management:feature_flags:1:variants:3") + .GetSection("feature_management:feature_flags:1000001:variants:3") .AsEnumerable() .ToDictionary(x => x.Key, x => x.Value) - .ContainsKey("feature_management:feature_flags:1:variants:3:configuration_value")); - Assert.Equal("BooleanVariant", config["feature_management:feature_flags:1:variants:4:name"]); - Assert.Equal("True", config["feature_management:feature_flags:1:variants:4:configuration_value"]); - Assert.Equal("ObjectVariant", config["feature_management:feature_flags:1:allocation:default_when_disabled"]); - Assert.Equal("ObjectVariant", config["feature_management:feature_flags:1:allocation:default_when_enabled"]); - - Assert.Equal("VariantsFeature3", config["feature_management:feature_flags:2:id"]); - Assert.Equal("True", config["feature_management:feature_flags:2:enabled"]); - Assert.Equal("NumberVariant", config["feature_management:feature_flags:2:allocation:default_when_enabled"]); - Assert.Equal("1", config["feature_management:feature_flags:2:variants:0:configuration_value"]); - Assert.Equal("2", config["feature_management:feature_flags:2:variants:1:configuration_value"]); - Assert.Equal("Other", config["feature_management:feature_flags:2:variants:2:configuration_value"]); - Assert.Equal("NumberVariant", config["feature_management:feature_flags:2:allocation:default_when_enabled"]); + .ContainsKey("feature_management:feature_flags:1000001:variants:3:configuration_value")); + Assert.Equal("BooleanVariant", config["feature_management:feature_flags:1000001:variants:4:name"]); + Assert.Equal("True", config["feature_management:feature_flags:1000001:variants:4:configuration_value"]); + Assert.Equal("ObjectVariant", config["feature_management:feature_flags:1000001:allocation:default_when_disabled"]); + Assert.Equal("ObjectVariant", config["feature_management:feature_flags:1000001:allocation:default_when_enabled"]); + + Assert.Equal("VariantsFeature3", config["feature_management:feature_flags:1000002:id"]); + Assert.Equal("True", config["feature_management:feature_flags:1000002:enabled"]); + Assert.Equal("NumberVariant", config["feature_management:feature_flags:1000002:allocation:default_when_enabled"]); + Assert.Equal("1", config["feature_management:feature_flags:1000002:variants:0:configuration_value"]); + Assert.Equal("2", config["feature_management:feature_flags:1000002:variants:1:configuration_value"]); + Assert.Equal("Other", config["feature_management:feature_flags:1000002:variants:2:configuration_value"]); + Assert.Equal("NumberVariant", config["feature_management:feature_flags:1000002:allocation:default_when_enabled"]); Assert.Equal("True", config["FeatureManagement:VariantsFeature4"]); } @@ -2037,11 +2037,11 @@ public void WithTelemetry() }) .Build(); - Assert.Equal("True", config["feature_management:feature_flags:0:telemetry:enabled"]); - Assert.Equal("TelemetryFeature1", config["feature_management:feature_flags:0:id"]); - Assert.Equal("Tag1Value", config["feature_management:feature_flags:0:telemetry:metadata:Tags.Tag1"]); - Assert.Equal("Tag2Value", config["feature_management:feature_flags:0:telemetry:metadata:Tags.Tag2"]); - Assert.Equal("c3c231fd-39a0-4cb6-3237-4614474b92c1", config["feature_management:feature_flags:0:telemetry:metadata:ETag"]); + Assert.Equal("True", config["feature_management:feature_flags:1000000:telemetry:enabled"]); + Assert.Equal("TelemetryFeature1", config["feature_management:feature_flags:1000000:id"]); + Assert.Equal("Tag1Value", config["feature_management:feature_flags:1000000:telemetry:metadata:Tags.Tag1"]); + Assert.Equal("Tag2Value", config["feature_management:feature_flags:1000000:telemetry:metadata:Tags.Tag2"]); + Assert.Equal("c3c231fd-39a0-4cb6-3237-4614474b92c1", config["feature_management:feature_flags:1000000:telemetry:metadata:ETag"]); byte[] featureFlagIdHash; @@ -2055,12 +2055,12 @@ public void WithTelemetry() .Replace('+', '-') .Replace('/', '_'); - Assert.Equal(featureFlagId, config["feature_management:feature_flags:0:telemetry:metadata:FeatureFlagId"]); - Assert.Equal($"{TestHelpers.PrimaryConfigStoreEndpoint}kv/{FeatureManagementConstants.FeatureFlagMarker}TelemetryFeature1?label=label", config["feature_management:feature_flags:0:telemetry:metadata:FeatureFlagReference"]); + Assert.Equal(featureFlagId, config["feature_management:feature_flags:1000000:telemetry:metadata:FeatureFlagId"]); + Assert.Equal($"{TestHelpers.PrimaryConfigStoreEndpoint}kv/{FeatureManagementConstants.FeatureFlagMarker}TelemetryFeature1?label=label", config["feature_management:feature_flags:1000000:telemetry:metadata:FeatureFlagReference"]); - Assert.Equal("True", config["feature_management:feature_flags:1:telemetry:enabled"]); - Assert.Equal("TelemetryFeature2", config["feature_management:feature_flags:1:id"]); - Assert.Equal("Tag2Value", config["feature_management:feature_flags:1:telemetry:metadata:Tags.Tag1"]); + Assert.Equal("True", config["feature_management:feature_flags:1000001:telemetry:enabled"]); + Assert.Equal("TelemetryFeature2", config["feature_management:feature_flags:1000001:id"]); + Assert.Equal("Tag2Value", config["feature_management:feature_flags:1000001:telemetry:metadata:Tags.Tag1"]); } [Fact] @@ -2100,12 +2100,12 @@ public void WithRequirementType() }) .Build(); - Assert.Null(config["feature_management:feature_flags:0:requirement_type"]); - Assert.Equal("Feature_NoFilters", config["feature_management:feature_flags:0:id"]); - Assert.Equal("All", config["feature_management:feature_flags:1:conditions:requirement_type"]); - Assert.Equal("Feature_RequireAll", config["feature_management:feature_flags:1:id"]); - Assert.Equal("Any", config["feature_management:feature_flags:2:conditions:requirement_type"]); - Assert.Equal("Feature_RequireAny", config["feature_management:feature_flags:2:id"]); + Assert.Null(config["feature_management:feature_flags:1000000:requirement_type"]); + Assert.Equal("Feature_NoFilters", config["feature_management:feature_flags:1000000:id"]); + Assert.Equal("All", config["feature_management:feature_flags:1000001:conditions:requirement_type"]); + Assert.Equal("Feature_RequireAll", config["feature_management:feature_flags:1000001:id"]); + Assert.Equal("Any", config["feature_management:feature_flags:1000002:conditions:requirement_type"]); + Assert.Equal("Feature_RequireAny", config["feature_management:feature_flags:1000002:id"]); } [Fact] From 6740506aa95869054469defb7202873d1dfd9279 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 28 Apr 2025 16:09:37 -0700 Subject: [PATCH 4/5] fix comments --- .../FeatureManagement/FeatureManagementKeyValueAdapter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs index 12f9f844..6212304f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs @@ -53,12 +53,11 @@ public Task>> ProcessKeyValue(Configura return Task.FromResult>>(keyValues); } - // Track a new feature flag being processed and update the global counter private void TrackProcessedFlag() { _processedFlagsCount++; - // Update the max flags count if we have more flags in this cycle + // Update the max flags count if we have more flags this time compared to last load/refresh if (_processedFlagsCount > _maxProcessedFlagsPerRefresh) { _maxProcessedFlagsPerRefresh = _processedFlagsCount; @@ -102,7 +101,9 @@ public void OnChangeDetected(ConfigurationSetting setting = null) public void OnConfigUpdated() { _currentFeatureFlagIndex = _startFeatureFlagIndex; + _processedFlagsCount = 0; + return; } From ab6071a23beadf17fd086e9d842b13baa387502a Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 30 Apr 2025 15:13:11 -0700 Subject: [PATCH 5/5] add test and fix adapter logic --- .../FeatureManagementKeyValueAdapter.cs | 29 +------ .../FeatureManagementTests.cs | 83 +++++++++++++++++++ 2 files changed, 86 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs index 6212304f..eea0b7ce 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs @@ -18,20 +18,17 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage internal class FeatureManagementKeyValueAdapter : IKeyValueAdapter { private FeatureFlagTracing _featureFlagTracing; - private static int _globalFeatureFlagCounter = 1000000; + private static int _globalFeatureFlagCounter = 1_000_000; + private const int _reservedIndexSpace = 100_000; private readonly int _startFeatureFlagIndex; private int _currentFeatureFlagIndex; - private int _processedFlagsCount; - private int _maxProcessedFlagsPerRefresh; public FeatureManagementKeyValueAdapter(FeatureFlagTracing featureFlagTracing) { _featureFlagTracing = featureFlagTracing ?? throw new ArgumentNullException(nameof(featureFlagTracing)); - _startFeatureFlagIndex = Interlocked.Add(ref _globalFeatureFlagCounter, 0); + _startFeatureFlagIndex = Interlocked.Add(ref _globalFeatureFlagCounter, _reservedIndexSpace) - _reservedIndexSpace; _currentFeatureFlagIndex = _startFeatureFlagIndex; - _processedFlagsCount = 0; - _maxProcessedFlagsPerRefresh = 0; } public Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken) @@ -53,23 +50,6 @@ public Task>> ProcessKeyValue(Configura return Task.FromResult>>(keyValues); } - private void TrackProcessedFlag() - { - _processedFlagsCount++; - - // Update the max flags count if we have more flags this time compared to last load/refresh - if (_processedFlagsCount > _maxProcessedFlagsPerRefresh) - { - _maxProcessedFlagsPerRefresh = _processedFlagsCount; - - // Update the global counter to reserve enough space for our maximum observed flags - Interlocked.CompareExchange( - ref _globalFeatureFlagCounter, - _startFeatureFlagIndex + _maxProcessedFlagsPerRefresh, - _globalFeatureFlagCounter); - } - } - public bool CanProcess(ConfigurationSetting setting) { if (setting == null || @@ -102,8 +82,6 @@ public void OnConfigUpdated() { _currentFeatureFlagIndex = _startFeatureFlagIndex; - _processedFlagsCount = 0; - return; } @@ -172,7 +150,6 @@ private List> ProcessMicrosoftSchemaFeatureFlag(Fea string featureFlagPath = $"{FeatureManagementConstants.FeatureManagementSectionName}:{FeatureManagementConstants.FeatureFlagsSectionName}:{_currentFeatureFlagIndex}"; _currentFeatureFlagIndex++; - TrackProcessedFlag(); keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.Id}", featureFlag.Id)); diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs index b9e9fd94..f387c268 100644 --- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs +++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs @@ -2155,6 +2155,89 @@ public void ThrowsOnIncorrectJsonTypes() } } + [Fact] + public void MultipleFeatureManagementConfigurationsIndexCorrectly() + { + // Create two separate collections of feature flags to represent different providers + var provider1FeatureFlags = new List + { + ConfigurationModelFactory.ConfigurationSetting( + key: FeatureManagementConstants.FeatureFlagMarker + "Provider1Feature", + value: @" + { + ""id"": ""Provider1Feature"", + ""enabled"": true, + ""variants"": [ + { + ""name"": ""VariantA"", + ""configuration_value"": ""Value1"" + } + ] + } + ", + label: default, + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8", + eTag: new ETag("provider1-etag")) + }; + + var provider2FeatureFlags = new List + { + ConfigurationModelFactory.ConfigurationSetting( + key: FeatureManagementConstants.FeatureFlagMarker + "Provider2Feature", + value: @" + { + ""id"": ""Provider2Feature"", + ""enabled"": true, + ""variants"": [ + { + ""name"": ""VariantC"", + ""configuration_value"": ""Value3"" + } + ] + } + ", + label: default, + contentType: FeatureManagementConstants.ContentType + ";charset=utf-8", + eTag: new ETag("provider2-etag")) + }; + + var mockClient1 = new Mock(MockBehavior.Strict); + var mockClient2 = new Mock(MockBehavior.Strict); + + mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(provider1FeatureFlags)); + + mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(provider2FeatureFlags)); + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options1 => + { + options1.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient1.Object); + options1.UseFeatureFlags(ff => ff.Select("Provider1Feature")); + }) + .AddAzureAppConfiguration(options2 => + { + options2.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient2.Object); + options2.UseFeatureFlags(ff => ff.Select("Provider2Feature")); + }) + .Build(); + + int index1 = 1_000_000; + int index2 = 1_100_000; + + // Verify the configuration values match + Assert.Equal("Provider1Feature", config[$"feature_management:feature_flags:{index1}:id"]); + Assert.Equal("True", config[$"feature_management:feature_flags:{index1}:enabled"]); + Assert.Equal("VariantA", config[$"feature_management:feature_flags:{index1}:variants:0:name"]); + Assert.Equal("Value1", config[$"feature_management:feature_flags:{index1}:variants:0:configuration_value"]); + + Assert.Equal("Provider2Feature", config[$"feature_management:feature_flags:{index2}:id"]); + Assert.Equal("True", config[$"feature_management:feature_flags:{index2}:enabled"]); + Assert.Equal("VariantC", config[$"feature_management:feature_flags:{index2}:variants:0:name"]); + Assert.Equal("Value3", config[$"feature_management:feature_flags:{index2}:variants:0:configuration_value"]); + } + Response GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken) { return Response.FromValue(FirstKeyValue, new MockResponse(200));