Skip to content
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

Adds 'Settings' to Context and uses it as a cache of parameters #229

Merged
merged 17 commits into from
Jun 2, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
91617b2
Adds 'Settings' to Context and uses it as a cache of parameters
rossgrambo Apr 19, 2023
69a3f8d
Resolves comments
rossgrambo May 4, 2023
a3d3a2b
Changes the default cache period to 0
rossgrambo May 4, 2023
79c0206
Handling edge case where AbsoluteExpirationRelative doesn't accept Ti…
rossgrambo May 4, 2023
ed9834c
Renamed SettingsCachePeriod to FilterSettingsCacheTtl and adjusted su…
rossgrambo May 4, 2023
87a3799
Removing unrequired dependency
rossgrambo May 4, 2023
95abd68
Adds an Error type for invalid options and throws it for negative cac…
rossgrambo May 5, 2023
20e6b2c
Changed options validation error to an ArgumentException
rossgrambo May 5, 2023
c1a7240
Merge branch 'main' into rossgrambo/cache-filter-parameters
rossgrambo May 5, 2023
576b762
Removes cache timespan as an option, sets a sliding expiration to 5 m…
rossgrambo May 13, 2023
541bfc0
Merge branch 'rossgrambo/cache-filter-parameters' of https://github.c…
rossgrambo May 13, 2023
c44de57
Adds FilterIndex to FeatureFilterEvaluationContext. Uses it in the ca…
rossgrambo May 13, 2023
d2209ae
Changes the interface to be private and adds an assemblyinfo file tha…
rossgrambo May 15, 2023
c18384c
Pulling out constants and adding notes to summary
rossgrambo May 23, 2023
5cce4d3
Merge branch 'main' into rossgrambo/cache-filter-parameters
rossgrambo May 30, 2023
e1044ef
Removes FilterIndex from FeatureFilterEvaluationContext object and cl…
rossgrambo Jun 1, 2023
c15d820
Merge branch 'rossgrambo/cache-filter-parameters' of https://github.c…
rossgrambo Jun 1, 2023
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 @@ -179,7 +179,7 @@ We support
enabledFor.Add(new FeatureFilterConfiguration()
{
Name = section[nameof(FeatureFilterConfiguration.Name)],
Parameters = section.GetSection(nameof(FeatureFilterConfiguration.Parameters))
Parameters = new ConfigurationWrapper(section.GetSection(nameof(FeatureFilterConfiguration.Parameters)))
});
}
}
Expand Down
38 changes: 38 additions & 0 deletions src/Microsoft.FeatureManagement/ConfigurationWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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.
/// </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
Expand Up @@ -19,5 +19,11 @@ public class FeatureFilterEvaluationContext
/// The settings provided for the feature filter to use when evaluating whether the feature should be enabled.
/// </summary>
public IConfiguration Parameters { get; set; }

/// <summary>
/// A settings object, if any, that has been pre-bound from <see cref="Parameters"/>.
/// The settings are made available for <see cref="IFeatureFilter"/>s that implement <see cref="IFilterParametersBinder"/>.
/// </summary>
public object Settings { get; set; }
}
}
14 changes: 12 additions & 2 deletions src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters
/// A feature filter that can be used to activate a feature based on a random percentage.
/// </summary>
[FilterAlias(Alias)]
public class PercentageFilter : IFeatureFilter
public class PercentageFilter : IFeatureFilter, IFilterParametersBinder
{
private const string Alias = "Microsoft.Percentage";
private readonly ILogger _logger;
Expand All @@ -26,14 +26,24 @@ public PercentageFilter(ILoggerFactory loggerFactory)
_logger = loggerFactory.CreateLogger<PercentageFilter>();
}

/// <summary>
/// Binds configuration representing filter parameters to <see cref="PercentageFilterSettings"/>.
/// </summary>
/// <param name="filterParameters">The configuration representing filter parameters that should be bound to <see cref="PercentageFilterSettings"/>.</param>
/// <returns><see cref="PercentageFilterSettings"/> that can later be used in feature evaluation.</returns>
public object BindParameters(IConfiguration filterParameters)
{
return filterParameters.Get<PercentageFilterSettings>() ?? new PercentageFilterSettings();
}

/// <summary>
/// Performs a percentage based evaluation to determine whether a feature is enabled.
/// </summary>
/// <param name="context">The feature evaluation context.</param>
/// <returns>True if the feature is enabled, false otherwise.</returns>
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
PercentageFilterSettings settings = context.Parameters.Get<PercentageFilterSettings>() ?? new PercentageFilterSettings();
PercentageFilterSettings settings = (PercentageFilterSettings)context.Settings;

bool result = true;

Expand Down
14 changes: 12 additions & 2 deletions src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters
/// A feature filter that can be used to activate a feature based on a time window.
/// </summary>
[FilterAlias(Alias)]
public class TimeWindowFilter : IFeatureFilter
public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder
{
private const string Alias = "Microsoft.TimeWindow";
private readonly ILogger _logger;
Expand All @@ -26,14 +26,24 @@ public TimeWindowFilter(ILoggerFactory loggerFactory)
_logger = loggerFactory.CreateLogger<TimeWindowFilter>();
}

/// <summary>
/// Binds configuration representing filter parameters to <see cref="TimeWindowFilterSettings"/>.
/// </summary>
/// <param name="filterParameters">The configuration representing filter parameters that should be bound to <see cref="TimeWindowFilterSettings"/>.</param>
/// <returns><see cref="TimeWindowFilterSettings"/> that can later be used in feature evaluation.</returns>
public object BindParameters(IConfiguration filterParameters)
{
return filterParameters.Get<TimeWindowFilterSettings>() ?? new TimeWindowFilterSettings();
}

/// <summary>
/// Evaluates whether a feature is enabled based on a configurable time window.
/// </summary>
/// <param name="context">The feature evaluation context.</param>
/// <returns>True if the feature is enabled, false otherwise.</returns>
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
TimeWindowFilterSettings settings = context.Parameters.Get<TimeWindowFilterSettings>() ?? new TimeWindowFilterSettings();
TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings;

DateTimeOffset now = DateTimeOffset.UtcNow;

Expand Down
11 changes: 11 additions & 0 deletions src/Microsoft.FeatureManagement/FeatureManagementOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System;
rossgrambo marked this conversation as resolved.
Show resolved Hide resolved

namespace Microsoft.FeatureManagement
{
/// <summary>
Expand All @@ -22,5 +24,14 @@ public class FeatureManagementOptions
/// The default value is true.
/// </summary>
public bool IgnoreMissingFeatures { get; set; } = true;

/// <summary>
/// Controls the cache lifetime of settings bound by <see cref="IFilterParametersBinder"/> in the feature management cache.
/// By default, this cache is off, with a ttl of <see cref="TimeSpan.Zero"/>.
/// To enable caching of filter parameters, a non-zero ttl should be provided. A recommendation is five seconds.
/// Increasing the value may cause an observed increase in memory footprint as items live longer.
/// Lowering the value will decrease performance benefits yielded by caching bound parameters.
/// </summary>
public TimeSpan FilterSettingsCacheTtl { get; set; } = TimeSpan.Zero;
}
}
80 changes: 74 additions & 6 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
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;
Expand All @@ -14,7 +16,7 @@ namespace Microsoft.FeatureManagement
/// <summary>
/// Used to evaluate whether a feature is enabled or disabled.
/// </summary>
class FeatureManager : IFeatureManager
class FeatureManager : IFeatureManager, IDisposable
{
private readonly IFeatureDefinitionProvider _featureDefinitionProvider;
private readonly IEnumerable<IFeatureFilterMetadata> _featureFilters;
Expand All @@ -23,6 +25,14 @@ class FeatureManager : IFeatureManager
private readonly ConcurrentDictionary<string, IFeatureFilterMetadata> _filterMetadataCache;
private readonly ConcurrentDictionary<string, ContextualFeatureFilterEvaluator> _contextualFeatureFilterCache;
private readonly FeatureManagementOptions _options;
private readonly IMemoryCache _cache;

private class ConfigurationCacheItem
{
public IConfiguration Parameters { get; set; }

public object Settings { get; set; }
}

public FeatureManager(
IFeatureDefinitionProvider featureDefinitionProvider,
Expand All @@ -38,6 +48,11 @@ public FeatureManager(
_filterMetadataCache = new ConcurrentDictionary<string, IFeatureFilterMetadata>();
_contextualFeatureFilterCache = new ConcurrentDictionary<string, ContextualFeatureFilterEvaluator>();
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_cache = new MemoryCache(
new MemoryCacheOptions
{
ExpirationScanFrequency = _options.FilterSettingsCacheTtl
});
}

public Task<bool> IsEnabledAsync(string feature)
Expand All @@ -58,6 +73,11 @@ public async IAsyncEnumerable<string> GetFeatureNamesAsync()
}
}
rossgrambo marked this conversation as resolved.
Show resolved Hide resolved

public void Dispose()
{
_cache.Dispose();
}

private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appContext, bool useAppContext)
{
foreach (ISessionManager sessionManager in _sessionManagers)
Expand Down Expand Up @@ -144,7 +164,9 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo
{
ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, typeof(TContext));

if (contextualFilter != null &&
BindSettings(filter, context);

if (contextualFilter != null &&
await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) == targetEvaluation)
{
enabled = targetEvaluation;
Expand All @@ -155,12 +177,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);

break;
if (await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation) {
enabled = targetEvaluation;

break;
}
}
}
}
Expand All @@ -187,6 +212,49 @@ await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvalua
return enabled;
}

private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context)
{
IFilterParametersBinder binder = filter as IFilterParametersBinder;

if (binder == null)
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about having a separate short circuit like

if (!(_featureDefinitionProvider is IFeatureDefinitionProviderCacheable))
{
    context.Settings = binder.BindParameters(context.Parameters);

    return;
}

Suggesting it because any interaction with cache key or cache doesn't make sense if the definition provider isn't cachable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}

object settings;

ConfigurationCacheItem cacheItem;

//
// Check if settings already bound from configuration or the parameters have changed
if (!_cache.TryGetValue(context.FeatureName, out cacheItem) ||
cacheItem.Parameters != context.Parameters)
zhenlan marked this conversation as resolved.
Show resolved Hide resolved
{
settings = binder.BindParameters(context.Parameters);

if (_options.FilterSettingsCacheTtl > TimeSpan.Zero)
{
_cache.Set(
context.FeatureName,
new ConfigurationCacheItem
{
Settings = settings,
Parameters = context.Parameters
},
new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _options.FilterSettingsCacheTtl
avanigupta marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
else
{
settings = cacheItem.Settings;
}

context.Settings = settings;
}

private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName)
{
const string filterSuffix = "filter";
Expand Down
21 changes: 21 additions & 0 deletions src/Microsoft.FeatureManagement/IFilterParametersBinder.cs
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.1.23" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters
/// A feature filter that can be used to activate features for targeted audiences.
/// </summary>
[FilterAlias(Alias)]
public class ContextualTargetingFilter : IContextualFeatureFilter<ITargetingContext>
public class ContextualTargetingFilter : IContextualFeatureFilter<ITargetingContext>, IFilterParametersBinder
{
private const string Alias = "Microsoft.Targeting";
private readonly TargetingEvaluationOptions _options;
Expand All @@ -37,6 +37,16 @@ public ContextualTargetingFilter(IOptions<TargetingEvaluationOptions> options, I
private StringComparison ComparisonType => _options.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
private StringComparer ComparerType => _options.IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;

/// <summary>
/// Binds configuration representing filter parameters to <see cref="TargetingFilterSettings"/>.
/// </summary>
/// <param name="filterParameters">The configuration representing filter parameters that should be bound to <see cref="TargetingFilterSettings"/>.</param>
/// <returns><see cref="TargetingFilterSettings"/> that can later be used in targeting.</returns>
public object BindParameters(IConfiguration filterParameters)
{
return filterParameters.Get<TargetingFilterSettings>() ?? new TargetingFilterSettings();
}

/// <summary>
/// Performs a targeting evaluation using the provided <see cref="TargetingContext"/> to determine if a feature should be enabled.
/// </summary>
Expand All @@ -56,7 +66,7 @@ public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context, ITargeti
throw new ArgumentNullException(nameof(targetingContext));
}

TargetingFilterSettings settings = context.Parameters.Get<TargetingFilterSettings>() ?? new TargetingFilterSettings();
TargetingFilterSettings settings = (TargetingFilterSettings)context.Settings;

if (!TryValidateSettings(settings, out string paramName, out string message))
{
Expand Down
13 changes: 12 additions & 1 deletion src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
Expand All @@ -12,7 +13,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters
/// A feature filter that can be used to activate features for targeted audiences.
/// </summary>
[FilterAlias(Alias)]
public class TargetingFilter : IFeatureFilter
public class TargetingFilter : IFeatureFilter, IFilterParametersBinder
{
private const string Alias = "Microsoft.Targeting";
private readonly ITargetingContextAccessor _contextAccessor;
Expand All @@ -32,6 +33,16 @@ public TargetingFilter(IOptions<TargetingEvaluationOptions> options, ITargetingC
_logger = loggerFactory?.CreateLogger<TargetingFilter>() ?? throw new ArgumentNullException(nameof(loggerFactory));
}

/// <summary>
/// Binds configuration representing filter parameters to <see cref="TargetingFilterSettings"/>.
/// </summary>
/// <param name="filterParameters">The configuration representing filter parameters that should be bound to <see cref="TargetingFilterSettings"/>.</param>
/// <returns><see cref="TargetingFilterSettings"/> that can later be used in targeting.</returns>
public object BindParameters(IConfiguration filterParameters)
{
return filterParameters.Get<TargetingFilterSettings>() ?? new TargetingFilterSettings();
}

/// <summary>
/// Performs a targeting evaluation using the current <see cref="TargetingContext"/> to determine if a feature should be enabled.
/// </summary>
Expand Down
Loading