-
Notifications
You must be signed in to change notification settings - Fork 112
/
TimeWindowFilter.cs
149 lines (124 loc) · 6.24 KB
/
TimeWindowFilter.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
namespace Microsoft.FeatureManagement.FeatureFilters
{
/// <summary>
/// A feature filter that can be used to activate a feature based on a time window.
/// The time window can be configured to recur periodically.
/// </summary>
[FilterAlias(Alias)]
public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder
{
private readonly TimeSpan CacheSlidingExpiration = TimeSpan.FromMinutes(5);
private readonly TimeSpan CacheAbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1);
private const string Alias = "Microsoft.TimeWindow";
private readonly ILogger _logger;
/// <summary>
/// Creates a time window based feature filter.
/// </summary>
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
public TimeWindowFilter(ILoggerFactory loggerFactory = null)
{
_logger = loggerFactory?.CreateLogger<TimeWindowFilter>();
}
/// <summary>
/// The application memory cache to store the start time of the closest active time window. By caching this time, the time window can minimize redundant computations when evaluating recurrence.
/// </summary>
public IMemoryCache Cache { get; set; }
/// <summary>
/// This property allows the time window filter in our test suite to use simulated time.
/// </summary>
internal ISystemClock SystemClock { get; set; }
/// <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)
{
var settings = filterParameters.Get<TimeWindowFilterSettings>() ?? new TimeWindowFilterSettings();
if (!RecurrenceValidator.TryValidateSettings(settings, out string paramName, out string reason))
{
throw new ArgumentException(reason, paramName);
}
return settings;
}
/// <summary>
/// Evaluates whether a feature is enabled based on the <see cref="TimeWindowFilterSettings"/> specified in the configuration.
/// </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)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
//
// Check if prebound settings available, otherwise bind from parameters.
TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings ?? (TimeWindowFilterSettings)BindParameters(context.Parameters);
DateTimeOffset now = SystemClock?.UtcNow ?? DateTimeOffset.UtcNow;
if (!settings.Start.HasValue && !settings.End.HasValue)
{
_logger?.LogWarning($"The '{Alias}' feature filter is not valid for feature '{context.FeatureName}'. It must specify either '{nameof(settings.Start)}', '{nameof(settings.End)}', or both.");
return Task.FromResult(false);
}
//
// Hit the first occurrence of the time window
if ((!settings.Start.HasValue || now >= settings.Start.Value) && (!settings.End.HasValue || now < settings.End.Value))
{
return Task.FromResult(true);
}
if (settings.Recurrence != null)
{
//
// The reference of the object will be used for cache key.
// If there is no pre-bounded settings attached to the context, there will be no cached filter settings and each call will have a unique settings object.
// In this case, the cache for recurrence settings won't work.
if (Cache == null || context.Settings == null)
{
return Task.FromResult(RecurrenceEvaluator.IsMatch(now, settings));
}
//
// The start time of the closest active time window. It could be null if the recurrence range surpasses its end.
DateTimeOffset? closestStart;
TimeSpan activeDuration = settings.End.Value - settings.Start.Value;
//
// Recalculate the closest start if not yet calculated,
// Or if we have passed the cached time window.
if (!Cache.TryGetValue(settings, out closestStart) ||
(closestStart.HasValue && now >= closestStart.Value + activeDuration))
{
closestStart = ReloadClosestStart(settings);
}
if (!closestStart.HasValue || now < closestStart.Value)
{
return Task.FromResult(false);
}
return Task.FromResult(now < closestStart.Value + activeDuration);
}
return Task.FromResult(false);
}
private DateTimeOffset? ReloadClosestStart(TimeWindowFilterSettings settings)
{
DateTimeOffset now = SystemClock?.UtcNow ?? DateTimeOffset.UtcNow;
DateTimeOffset? closestStart = RecurrenceEvaluator.CalculateClosestStart(now, settings);
Cache.Set(
settings,
closestStart,
new MemoryCacheEntryOptions
{
SlidingExpiration = CacheSlidingExpiration,
AbsoluteExpirationRelativeToNow = CacheAbsoluteExpirationRelativeToNow,
Size = 1
});
return closestStart;
}
}
}