-
Notifications
You must be signed in to change notification settings - Fork 120
Adds 'Settings' to Context and uses it as a cache of parameters #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
91617b2
69a3f8d
a3d3a2b
79c0206
ed9834c
87a3799
95abd68
20e6b2c
c1a7240
576b762
541bfc0
c44de57
d2209ae
c18384c
5cce4d3
e1044ef
c15d820
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
// | ||
using System.Runtime.CompilerServices; | ||
|
||
// Tests | ||
[assembly: InternalsVisibleTo("Tests.FeatureManagement,PublicKey=" + | ||
"0024000004800000940000000602000000240000525341310004000001000100895524f60b44ff" + | ||
"3ae70fbea5662f61dd9d640de2205b7bd5359a43dda006e51d83d1f5f7a7d3f849267a0a28676d" + | ||
"cf49727a32487d4c75c4aacd5febb0069e1adc66ec63bbd18ec2276091a0e3c1326aa626c9e4db" + | ||
"800714a134f2a81e405f35752b55220021923429cb61776cd2fa66d25c335f8dc27bb92292905a" + | ||
"3798d896")] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
// | ||
using System; | ||
using System.Collections.Generic; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace Microsoft.FeatureManagement | ||
{ | ||
/// <summary> | ||
/// Wraps an instance of IConfiguration. This allows the reference to be updated when the underlying IConfiguration is updated. | ||
/// This is useful for cache busting based on the reference. | ||
/// </summary> | ||
class ConfigurationWrapper : IConfiguration | ||
rossgrambo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
private readonly IConfiguration _configuration; | ||
|
||
public ConfigurationWrapper(IConfiguration configuration) | ||
{ | ||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); | ||
} | ||
|
||
public string this[string key] | ||
{ | ||
get => _configuration[key]; | ||
set => _configuration[key] = value; | ||
} | ||
|
||
public IEnumerable<IConfigurationSection> GetChildren() => | ||
_configuration.GetChildren(); | ||
|
||
public IChangeToken GetReloadToken() => | ||
_configuration.GetReloadToken(); | ||
|
||
public IConfigurationSection GetSection(string key) => | ||
_configuration.GetSection(key); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
// | ||
using Microsoft.Extensions.Caching.Memory; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using System; | ||
|
@@ -14,15 +16,26 @@ namespace Microsoft.FeatureManagement | |
/// <summary> | ||
/// Used to evaluate whether a feature is enabled or disabled. | ||
/// </summary> | ||
class FeatureManager : IFeatureManager | ||
class FeatureManager : IFeatureManager, IDisposable | ||
{ | ||
private readonly TimeSpan ParametersCacheSlidingExpiration = TimeSpan.FromMinutes(5); | ||
private readonly TimeSpan ParametersCacheAbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1); | ||
|
||
private readonly IFeatureDefinitionProvider _featureDefinitionProvider; | ||
private readonly IEnumerable<IFeatureFilterMetadata> _featureFilters; | ||
private readonly IEnumerable<ISessionManager> _sessionManagers; | ||
private readonly ILogger _logger; | ||
private readonly ConcurrentDictionary<string, IFeatureFilterMetadata> _filterMetadataCache; | ||
private readonly ConcurrentDictionary<string, ContextualFeatureFilterEvaluator> _contextualFeatureFilterCache; | ||
private readonly FeatureManagementOptions _options; | ||
private readonly IMemoryCache _parametersCache; | ||
|
||
private class ConfigurationCacheItem | ||
{ | ||
public IConfiguration Parameters { get; set; } | ||
|
||
public object Settings { get; set; } | ||
} | ||
|
||
public FeatureManager( | ||
IFeatureDefinitionProvider featureDefinitionProvider, | ||
|
@@ -38,6 +51,7 @@ public FeatureManager( | |
_filterMetadataCache = new ConcurrentDictionary<string, IFeatureFilterMetadata>(); | ||
_contextualFeatureFilterCache = new ConcurrentDictionary<string, ContextualFeatureFilterEvaluator>(); | ||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options)); | ||
_parametersCache = new MemoryCache(new MemoryCacheOptions()); | ||
} | ||
|
||
public Task<bool> IsEnabledAsync(string feature) | ||
|
@@ -58,6 +72,11 @@ public async IAsyncEnumerable<string> GetFeatureNamesAsync() | |
} | ||
} | ||
rossgrambo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public void Dispose() | ||
{ | ||
_parametersCache.Dispose(); | ||
} | ||
|
||
private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appContext, bool useAppContext) | ||
{ | ||
foreach (ISessionManager sessionManager in _sessionManagers) | ||
|
@@ -99,10 +118,16 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo | |
// We iterate until we hit our target evaluation | ||
bool targetEvaluation = !enabled; | ||
|
||
// | ||
// Keep track of the index of the filter we are evaluating | ||
int filterIndex = -1; | ||
|
||
// | ||
// For all enabling filters listed in the feature's state, evaluate them according to requirement type | ||
foreach (FeatureFilterConfiguration featureFilterConfiguration in featureDefinition.EnabledFor) | ||
{ | ||
filterIndex++; | ||
|
||
// | ||
// Handle AlwaysOn filters | ||
if (string.Equals(featureFilterConfiguration.Name, "AlwaysOn", StringComparison.OrdinalIgnoreCase)) | ||
|
@@ -144,7 +169,9 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo | |
{ | ||
ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, typeof(TContext)); | ||
|
||
if (contextualFilter != null && | ||
BindSettings(filter, context, filterIndex); | ||
|
||
if (contextualFilter != null && | ||
await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) == targetEvaluation) | ||
{ | ||
enabled = targetEvaluation; | ||
|
@@ -155,12 +182,15 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) | |
|
||
// | ||
// IFeatureFilter | ||
if (filter is IFeatureFilter featureFilter && | ||
await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation) | ||
if (filter is IFeatureFilter featureFilter) | ||
{ | ||
enabled = targetEvaluation; | ||
BindSettings(filter, context, filterIndex); | ||
|
||
if (await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation) { | ||
enabled = targetEvaluation; | ||
|
||
break; | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -187,6 +217,56 @@ await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvalua | |
return enabled; | ||
} | ||
|
||
private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) | ||
{ | ||
IFilterParametersBinder binder = filter as IFilterParametersBinder; | ||
|
||
if (binder == null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about having a separate short circuit like
Suggesting it because any interaction with cache key or cache doesn't make sense if the definition provider isn't cachable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea, makes the cache if statement a little cleaner as a result. |
||
{ | ||
return; | ||
} | ||
|
||
if (!(_featureDefinitionProvider is IFeatureDefinitionProviderCacheable)) | ||
{ | ||
context.Settings = binder.BindParameters(context.Parameters); | ||
|
||
return; | ||
} | ||
|
||
object settings; | ||
|
||
ConfigurationCacheItem cacheItem; | ||
|
||
string cacheKey = $"{context.FeatureName}.{filterIndex}"; | ||
|
||
// | ||
// Check if settings already bound from configuration or the parameters have changed | ||
if (!_parametersCache.TryGetValue(cacheKey, out cacheItem) || | ||
cacheItem.Parameters != context.Parameters) | ||
zhenlan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
settings = binder.BindParameters(context.Parameters); | ||
|
||
_parametersCache.Set( | ||
cacheKey, | ||
new ConfigurationCacheItem | ||
{ | ||
Settings = settings, | ||
Parameters = context.Parameters | ||
}, | ||
new MemoryCacheEntryOptions | ||
{ | ||
SlidingExpiration = ParametersCacheSlidingExpiration, | ||
AbsoluteExpirationRelativeToNow = ParametersCacheAbsoluteExpirationRelativeToNow | ||
}); | ||
} | ||
else | ||
{ | ||
settings = cacheItem.Settings; | ||
} | ||
|
||
context.Settings = settings; | ||
} | ||
|
||
private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName) | ||
{ | ||
const string filterSuffix = "filter"; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
// | ||
|
||
namespace Microsoft.FeatureManagement | ||
{ | ||
/// <summary> | ||
/// An interface that marks this provider's parameters are cacheable. This was implemented to allow the provider in our test suite to be cacheable. | ||
/// </summary> | ||
internal interface IFeatureDefinitionProviderCacheable | ||
{ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
// | ||
using Microsoft.Extensions.Configuration; | ||
|
||
namespace Microsoft.FeatureManagement | ||
{ | ||
/// <summary> | ||
/// An interface used by the feature management system to pre-bind feature filter parameters to a settings type. | ||
/// <see cref="IFeatureFilter"/>s can implement this interface to take advantage of caching of settings by the feature management system. | ||
/// </summary> | ||
public interface IFilterParametersBinder | ||
jimmyca15 marked this conversation as resolved.
Show resolved
Hide resolved
jimmyca15 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
/// <summary> | ||
/// Binds a set of feature filter parameters to a settings object. | ||
/// </summary> | ||
/// <param name="parameters">The configuration representing filter parameters to bind to a settings object</param> | ||
/// <returns>A settings object that is understood by the implementer of <see cref="IFilterParametersBinder"/>.</returns> | ||
object BindParameters(IConfiguration parameters); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like if we have some comment explaining that
IFeatureDefinitionProviderCacheable
is around purely for satisfying test consumption.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps also in that types file as well.