Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9069aad
support app config server side schema
zhiyuanliang-ms Nov 22, 2023
1547ee5
avoid naming with server-side
zhiyuanliang-ms Nov 23, 2023
8a8ca97
fix bug
zhiyuanliang-ms Nov 23, 2023
e4f89f7
testcase added
zhiyuanliang-ms Nov 24, 2023
b5b859d
reorg the code
zhiyuanliang-ms Nov 24, 2023
7933989
update the naming
zhiyuanliang-ms Nov 28, 2023
cd3b3f5
fix typo
zhiyuanliang-ms Nov 28, 2023
1270e37
adjust indentation
zhiyuanliang-ms Nov 28, 2023
da89f81
resolve comments
zhiyuanliang-ms Nov 29, 2023
bae3270
revert unexpected change
zhiyuanliang-ms Nov 30, 2023
8c13231
adjust the logic of get feature flag definition section
zhiyuanliang-ms Nov 30, 2023
f54b66a
revert changes
zhiyuanliang-ms Nov 30, 2023
c4cfcf3
fix bug
zhiyuanliang-ms Nov 30, 2023
8baa4c7
resolve comments
zhiyuanliang-ms Dec 1, 2023
7d73b06
resolved comments
zhiyuanliang-ms Dec 2, 2023
dca69d3
update
zhiyuanliang-ms Dec 2, 2023
9d21987
update
zhiyuanliang-ms Dec 4, 2023
729af66
make schema selection thread-safe
zhiyuanliang-ms Dec 4, 2023
1e41ac1
update testcases
zhiyuanliang-ms Dec 4, 2023
66e3841
update naming
zhiyuanliang-ms Dec 4, 2023
646be65
update
zhiyuanliang-ms Dec 4, 2023
ed5ba39
resolve comments
zhiyuanliang-ms Dec 5, 2023
fb75dfa
thread-safe
zhiyuanliang-ms Dec 6, 2023
5048f80
Merge branch 'main' into zhiyuanliang/support-server-side-schema
zhiyuanliang-ms Dec 7, 2023
a4805a3
improvement
zhiyuanliang-ms Dec 8, 2023
421eb69
resolve comments
zhiyuanliang-ms Dec 9, 2023
0f4a889
use lock
zhiyuanliang-ms Dec 12, 2023
1a565a1
resolve comments
zhiyuanliang-ms Dec 13, 2023
29a6aed
Merge branch 'main' into zhiyuanliang/support-server-side-schema
zhiyuanliang-ms Dec 13, 2023
c0e9db6
Merge branch 'main' into zhiyuanliang/support-server-side-schema
zhiyuanliang-ms Dec 19, 2023
b54142c
Merge branch 'main' into zhiyuanliang/support-server-side-schema
zhiyuanliang-ms Jan 23, 2024
c12feef
rename to Microsoft Feature Flag schema
zhiyuanliang-ms Jan 23, 2024
ebf9bd2
Merge branch 'zhiyuanliang/support-server-side-schema' of https://git…
zhiyuanliang-ms Jan 23, 2024
05dd60a
Merge branch 'main' into zhiyuanliang/support-server-side-schema
zhiyuanliang-ms Jan 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP
// IFeatureDefinitionProviderCacheable interface is only used to mark this provider as cacheable. This allows our test suite's
// provider to be marked for caching as well.

private const string FeatureFiltersSectionName = "EnabledFor";
private const string RequirementTypeKeyword = "RequirementType";
private const string FeatureManagementSectionName = "FeatureManagement";
private readonly IConfiguration _configuration;
private readonly ConcurrentDictionary<string, FeatureDefinition> _definitions;
private IDisposable _changeSubscription;
private int _stale = 0;
private long _initialized = 0;
private bool _microsoftFeatureFlagSchemaEnabled;
private readonly object _lock = new object();

/// <summary>
/// Creates a configuration feature definition provider.
Expand Down Expand Up @@ -81,6 +81,8 @@ public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
throw new ArgumentException($"The value '{ConfigurationPath.KeyDelimiter}' is not allowed in the feature name.", nameof(featureName));
}

EnsureInit();

if (Interlocked.Exchange(ref _stale, 0) != 0)
{
_definitions.Clear();
Expand All @@ -104,6 +106,8 @@ public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
#pragma warning restore CS1998
{
EnsureInit();

if (Interlocked.Exchange(ref _stale, 0) != 0)
{
_definitions.Clear();
Expand All @@ -113,16 +117,55 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
// Iterate over all features registered in the system at initial invocation time
foreach (IConfigurationSection featureSection in GetFeatureDefinitionSections())
{
string featureName = GetFeatureName(featureSection);

if (string.IsNullOrEmpty(featureName))
{
continue;
}

//
// Underlying IConfigurationSection data is dynamic so latest feature definitions are returned
yield return _definitions.GetOrAdd(featureSection.Key, (_) => ReadFeatureDefinition(featureSection));
yield return _definitions.GetOrAdd(featureName, (_) => ReadFeatureDefinition(featureSection));
}
}

private void EnsureInit()
{
if (_initialized == 0)
{
IConfiguration featureManagementConfigurationSection = _configuration
.GetChildren()
.FirstOrDefault(section =>
string.Equals(
section.Key,
ConfigurationFields.FeatureManagementSectionName,
StringComparison.OrdinalIgnoreCase));

if (featureManagementConfigurationSection == null && RootConfigurationFallbackEnabled)
{
featureManagementConfigurationSection = _configuration;
}

bool hasMicrosoftFeatureFlagSchema = featureManagementConfigurationSection != null &&
HasMicrosoftFeatureFlagSchema(featureManagementConfigurationSection);

lock (_lock)
{
if (Interlocked.Read(ref _initialized) == 0)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lock can sure only one thread can enter this block, what is the reason for using Interlocked.Read(ref _initialized) to atomically get the value?

Mark int32 type _initialized as volatile then judge _initialized == 0 is fine. Am I missing anything?

Copy link

@RichardChen820 RichardChen820 Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or use

if (Interlocked.CompareExchange(ref _initialized, 1, 0))==0 {
_azureAppConfigurationFeatureFlagSchemaEnabled = hasAzureAppConfigurationFeatureFlagSchema; }

is also fine.

Copy link
Member

@jimmyca15 jimmyca15 Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interlocked exchange does perform an atomic swap, but it also has the added benefit of forcing a read from memory. Here, interlocked avoids cached read from CPU. Marking the variable as volatile will have lasting effect throughout the lifetime of the application whereas interlocked usage in the init check only happens during initialization.

{
_microsoftFeatureFlagSchemaEnabled = hasMicrosoftFeatureFlagSchema;

Interlocked.Exchange(ref _initialized, 1);
}
}
}
}

private FeatureDefinition ReadFeatureDefinition(string featureName)
{
IConfigurationSection configuration = GetFeatureDefinitionSections()
.FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase));
.FirstOrDefault(section => string.Equals(GetFeatureName(section), featureName, StringComparison.OrdinalIgnoreCase));

if (configuration == null)
{
Expand All @@ -133,6 +176,16 @@ private FeatureDefinition ReadFeatureDefinition(string featureName)
}

private FeatureDefinition ReadFeatureDefinition(IConfigurationSection configurationSection)
{
if (_microsoftFeatureFlagSchemaEnabled)
{
return ParseMicrosoftFeatureDefinition(configurationSection);
}

return ParseFeatureDefinition(configurationSection);
}

private FeatureDefinition ParseFeatureDefinition(IConfigurationSection configurationSection)
{
/*

Expand Down Expand Up @@ -167,15 +220,17 @@ We support

*/

RequirementType requirementType = RequirementType.Any;
string featureName = GetFeatureName(configurationSection);

var enabledFor = new List<FeatureFilterConfiguration>();

RequirementType requirementType = RequirementType.Any;

string val = configurationSection.Value; // configuration[$"{featureName}"];

if (string.IsNullOrEmpty(val))
{
val = configurationSection[FeatureFiltersSectionName];
val = configurationSection[ConfigurationFields.FeatureFiltersSectionName];
}

if (!string.IsNullOrEmpty(val) && bool.TryParse(val, out bool result) && result)
Expand All @@ -193,64 +248,215 @@ We support
}
else
{
string rawRequirementType = configurationSection[RequirementTypeKeyword];
string rawRequirementType = configurationSection[ConfigurationFields.RequirementType];

//
// If requirement type is specified, parse it and set the requirementType variable
if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType))
{
throw new FeatureManagementException(
FeatureManagementError.InvalidConfigurationSetting,
$"Invalid requirement type '{rawRequirementType}' for feature '{configurationSection.Key}'.");
FeatureManagementError.InvalidConfigurationSetting,
$"Invalid value '{rawRequirementType}' for field '{ConfigurationFields.RequirementType}' of feature '{featureName}'.");
}

IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren();
IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(ConfigurationFields.FeatureFiltersSectionName).GetChildren();

foreach (IConfigurationSection section in filterSections)
{
//
// Arrays in json such as "myKey": [ "some", "values" ]
// Are accessed through the configuration system by using the array index as the property name, e.g. "myKey": { "0": "some", "1": "values" }
if (int.TryParse(section.Key, out int i) && !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)]))
if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword]))
{
enabledFor.Add(new FeatureFilterConfiguration()
{
Name = section[nameof(FeatureFilterConfiguration.Name)],
Parameters = new ConfigurationWrapper(section.GetSection(nameof(FeatureFilterConfiguration.Parameters)))
Name = section[ConfigurationFields.NameKeyword],
Parameters = new ConfigurationWrapper(section.GetSection(ConfigurationFields.FeatureFilterConfigurationParameters))
});
}
}
}

return new FeatureDefinition()
{
Name = configurationSection.Key,
Name = featureName,
EnabledFor = enabledFor,
RequirementType = requirementType
};
}

private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections()
private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection configurationSection)
{
/*

If Microsoft feature flag schema is enabled, we support

FeatureFlags: [
{
id: "myFeature",
enabled: true,
conditions: {
client_filters: ["myFeatureFilter1", "myFeatureFilter2"],
requirement_type: "All",
}
},
{
id: "myAlwaysEnabledFeature",
enabled: true,
conditions: {
client_filters: [],
}
},
{
id: "myAlwaysDisabledFeature",
enabled: false,
}
]

*/

string featureName = GetFeatureName(configurationSection);

var enabledFor = new List<FeatureFilterConfiguration>();

RequirementType requirementType = RequirementType.Any;

IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureFlagFields.Conditions);

string rawRequirementType = conditions[MicrosoftFeatureFlagFields.RequirementType];

//
// Look for feature definitions under the "FeatureManagement" section
IConfigurationSection featureManagementConfigurationSection = _configuration.GetSection(FeatureManagementSectionName);
// If requirement type is specified, parse it and set the requirementType variable
if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType))
{
throw new FeatureManagementException(
FeatureManagementError.InvalidConfigurationSetting,
$"Invalid value '{rawRequirementType}' for field '{MicrosoftFeatureFlagFields.RequirementType}' of feature '{featureName}'.");
}

string rawEnabled = configurationSection[MicrosoftFeatureFlagFields.Enabled];

if (featureManagementConfigurationSection.Exists())
bool enabled = false;

if (!string.IsNullOrEmpty(rawEnabled) && !bool.TryParse(rawEnabled, out enabled))
{
return featureManagementConfigurationSection.GetChildren();
throw new FeatureManagementException(
FeatureManagementError.InvalidConfigurationSetting,
$"Invalid value '{rawEnabled}' for field '{MicrosoftFeatureFlagFields.Enabled}' of feature '{featureName}'.");
}

//
// There is no "FeatureManagement" section in the configuration
if (RootConfigurationFallbackEnabled)
if (enabled)
{
return _configuration.GetChildren();
IEnumerable<IConfigurationSection> filterSections = conditions.GetSection(MicrosoftFeatureFlagFields.ClientFilters).GetChildren();

if (filterSections.Any())
{
foreach (IConfigurationSection section in filterSections)
{
//
// Arrays in json such as "myKey": [ "some", "values" ]
// Are accessed through the configuration system by using the array index as the property name, e.g. "myKey": { "0": "some", "1": "values" }
if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[MicrosoftFeatureFlagFields.Name]))
{
enabledFor.Add(new FeatureFilterConfiguration()
{
Name = section[MicrosoftFeatureFlagFields.Name],
Parameters = new ConfigurationWrapper(section.GetSection(MicrosoftFeatureFlagFields.Parameters))
});
}
}
}
else
{
enabledFor.Add(new FeatureFilterConfiguration
{
Name = "AlwaysOn"
});
}
}

Logger?.LogDebug($"No configuration section named '{FeatureManagementSectionName}' was found.");
return new FeatureDefinition()
{
Name = featureName,
EnabledFor = enabledFor,
RequirementType = requirementType
};
}

private string GetFeatureName(IConfigurationSection section)
{
if (_microsoftFeatureFlagSchemaEnabled)
{
return section[MicrosoftFeatureFlagFields.Id];
}

return section.Key;
}

private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections()
{
if (!_configuration.GetChildren().Any())
{
Logger?.LogDebug($"Configuration is empty.");

return Enumerable.Empty<IConfigurationSection>();
}

IConfiguration featureManagementConfigurationSection = _configuration
.GetChildren()
.FirstOrDefault(section =>
string.Equals(
section.Key,
ConfigurationFields.FeatureManagementSectionName,
StringComparison.OrdinalIgnoreCase));

if (featureManagementConfigurationSection == null)
{
if (RootConfigurationFallbackEnabled)
{
featureManagementConfigurationSection = _configuration;
}
else
{
Logger?.LogDebug($"No configuration section named '{ConfigurationFields.FeatureManagementSectionName}' was found.");

return Enumerable.Empty<IConfigurationSection>();
}
}

if (_microsoftFeatureFlagSchemaEnabled)
{
IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(MicrosoftFeatureFlagFields.FeatureFlagsSectionName);

return featureFlagsSection.GetChildren();
}

return featureManagementConfigurationSection.GetChildren();
}

private static bool HasMicrosoftFeatureFlagSchema(IConfiguration featureManagementConfiguration)
{
IConfigurationSection featureFlagsConfigurationSection = featureManagementConfiguration
.GetChildren()
.FirstOrDefault(section =>
string.Equals(
section.Key,
MicrosoftFeatureFlagFields.FeatureFlagsSectionName,
StringComparison.OrdinalIgnoreCase));

if (featureFlagsConfigurationSection != null)
{
if (!string.IsNullOrEmpty(featureFlagsConfigurationSection.Value))
{
return false;
}

IEnumerable<IConfigurationSection> featureFlagsChildren = featureFlagsConfigurationSection.GetChildren();

return featureFlagsChildren.Any() && featureFlagsChildren.All(section => int.TryParse(section.Key, out int _));
}

return Enumerable.Empty<IConfigurationSection>();
return false;
}
}
}
20 changes: 20 additions & 0 deletions src/Microsoft.FeatureManagement/ConfigurationFields.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//

namespace Microsoft.FeatureManagement
{
internal static class ConfigurationFields
{
// Enum keywords
public const string RequirementType = "RequirementType";

// Feature filters keywords
public const string FeatureFiltersSectionName = "EnabledFor";
public const string FeatureFilterConfigurationParameters = "Parameters";

// Other keywords
public const string NameKeyword = "Name";
public const string FeatureManagementSectionName = "FeatureManagement";
}
}
Loading