Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
bd2b15a
add base class: recurrence range, recurrence pattern
ivywei0125 Apr 27, 2024
56832a9
add object mapper in time window filter for parsing the config string…
ivywei0125 Apr 28, 2024
18551d6
implement the logic about recurrence evaluator
ivywei0125 Apr 28, 2024
21519ab
add validations
ivywei0125 Apr 28, 2024
bfb0d93
add test case for validator
ivywei0125 Apr 29, 2024
23f13f7
add test case for evaluator
ivywei0125 Apr 29, 2024
20825d7
add more test case
ivywei0125 May 7, 2024
c4f14c6
add more test case
ivywei0125 May 7, 2024
122541e
add calculateClosestStart method to calculate the closest occurrence
ivywei0125 May 7, 2024
15e7fa2
rename function
ivywei0125 May 8, 2024
c6fa94f
add cache logic
ivywei0125 May 8, 2024
449d482
update README.md
ivywei0125 May 9, 2024
6613249
update cache service
ivywei0125 May 9, 2024
3365d34
ci
ivywei0125 May 9, 2024
ac68532
ci
ivywei0125 May 9, 2024
c683ee8
move to parent path to make the package name shorter
ivywei0125 May 9, 2024
6bbbaaa
update copy right
ivywei0125 May 9, 2024
218cfa9
address comments: update README.md
ivywei0125 May 11, 2024
314c1a4
address comment: update the parameter to keep consistent
ivywei0125 May 14, 2024
c5b8334
address comment: update the sescription to be clearer
ivywei0125 May 14, 2024
374af49
address comment: update logic
ivywei0125 May 21, 2024
461b248
address comment: rename symbol to make easier understanding
ivywei0125 May 21, 2024
2c07dfe
address comment: extract common function to a util class
ivywei0125 May 21, 2024
00d4f69
address comment: rename symbol to make easier understanding
ivywei0125 May 21, 2024
0c49c89
address comment: simplify code
ivywei0125 May 21, 2024
77aaa0a
address comment: move to models folder
ivywei0125 May 21, 2024
fa7b99e
revert cache logic
ivywei0125 May 22, 2024
f694fbd
address comment: make the functions of RecurrenceEvaluator as static …
ivywei0125 May 22, 2024
bcc13df
address comment: move the validations to each property file
ivywei0125 May 23, 2024
89ed092
add test case in FeatureManagerTest
ivywei0125 May 23, 2024
57042e4
address comment: simplify code
ivywei0125 May 28, 2024
840b1af
address comment: throw exceptions directly
ivywei0125 May 28, 2024
5151259
address comment: move the related logic to the function to improve th…
ivywei0125 May 28, 2024
822f432
address comment: make as static method
ivywei0125 May 28, 2024
9b6199f
address comment: extract variables to avoid multi caller
ivywei0125 May 28, 2024
e703d7d
address comment: remove `convertToWeekDayNumber` method, no need any …
ivywei0125 May 29, 2024
bd614bb
address comment: simplify code
ivywei0125 May 29, 2024
c4138c5
address comment: remove unused code
ivywei0125 May 29, 2024
567b5c7
address comment: rename symbol
ivywei0125 May 29, 2024
65b98e6
address comment: add invalid settings check. Start and End must both …
ivywei0125 May 29, 2024
f5017a9
address comment: code style
ivywei0125 May 29, 2024
a52e3f7
address comment: add more test cases
ivywei0125 May 29, 2024
d6d815c
address comment: removed unused method
ivywei0125 May 30, 2024
2b928c9
address comment: update symbols
ivywei0125 May 30, 2024
59128ac
address comment: adjust spaces
ivywei0125 May 30, 2024
6ebe56c
address comment: add more test case
ivywei0125 May 30, 2024
dc659ad
address comment: update the README.md
ivywei0125 May 30, 2024
631b357
address comment: code style
ivywei0125 May 30, 2024
80c6011
address comment: typo error fix
ivywei0125 May 30, 2024
81da607
address comment: typo error fix
ivywei0125 May 30, 2024
e1c6007
address comment: update README.md
ivywei0125 May 30, 2024
f2ca9c7
address comment: update README.md
ivywei0125 May 30, 2024
22059be
Update sdk/spring/spring-cloud-azure-feature-management/README.md
ivywei0125 Jun 5, 2024
9e7f6a9
Update sdk/spring/spring-cloud-azure-feature-management/README.md
ivywei0125 Jun 5, 2024
59067dc
address comment: fix error in yaml example
ivywei0125 Jun 5, 2024
155074d
address comment: update README.md
ivywei0125 Jun 5, 2024
b59ae50
Update sdk/spring/spring-cloud-azure-feature-management/src/main/java…
ivywei0125 Jun 5, 2024
b68eb82
address comment: add @throws to java doc
ivywei0125 Jun 5, 2024
d957a1f
address comment: add "ignoreUnknown" annotation
ivywei0125 Jun 5, 2024
bad1e78
address comment: add year info to make it clearer
ivywei0125 Jun 5, 2024
cc86041
Merge branch 'yuwe/recurring-time-window-filter' of https://github.co…
ivywei0125 Jun 5, 2024
31c3bf7
address comment: add test case for time with RFC format
ivywei0125 Jun 5, 2024
a7c0271
address comment: fix build error
ivywei0125 Jun 6, 2024
f04780b
address comment: update the test case. Should use `assertThrows` inst…
ivywei0125 Jun 6, 2024
c5a31ff
address comment: update the error message to make it clearer
ivywei0125 Jun 13, 2024
688f252
update README file
ivywei0125 Jun 14, 2024
5bc512a
log error and then throw exception
ivywei0125 Jun 14, 2024
bc7da7d
address comment: update to support handler lower camel case
ivywei0125 Jun 17, 2024
d2d79f1
address comment: update code style
ivywei0125 Jun 17, 2024
3db57fb
address comment: update to use "Microsoft.TimeWindow"
ivywei0125 Jun 28, 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
132 changes: 129 additions & 3 deletions sdk/spring/spring-cloud-azure-feature-management/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ feature-management:

### TimeWindowFilter

This filter provides the capability to enable a feature based on a time window. If only `time-window-filter-setting-end` is specified, the feature will be considered on until that time. If only start is specified, the feature will be considered on at all points after that time. If both are specified the feature will be considered valid between the two times.
This filter provides the capability to enable a feature based on a time window. If only `End` is specified, the feature will be considered on until that time. If only `Start` is specified, the feature will be considered on at all points after that time.
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated

```yaml
feature-management:
Expand All @@ -192,10 +192,136 @@ feature-management:
-
name: TimeWindowFilter
parameters:
time-window-filter-setting-start: "Wed, 01 May 2019 13:59:59 GMT",
time-window-filter-setting-end: "Mon, 01 July 2019 00:00:00 GMT"
Start: "Wed, 01 May 2019 13:59:59 GMT",
End: "Mon, 01 July 2019 00:00:00 GMT"
```

The time window can be configured to recur periodically. This can be useful for the scenarios where one may need to turn on a feature during a low or high traffic period of a day or certain days of a week. To expand the individual time window to recurring time windows, the recurrence rule should be specified in the `Recurrence` parameter.

**Note:** `Start` and `End` must be both specified to enable `Recurrence`.

```yaml
feature-management:
feature-flags:
feature-v:
enabled-for:
-
name: TimeWindowFilter
parameters:
Comment thread
ivywei0125 marked this conversation as resolved.
start: "Fri, 22 Mar 2024 20:00:00 GMT",
end: "Mon, 01 July 2019 00:00:00 GMT",
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
recurrence:
pattern:
type: "Daily",
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
interval: 1
range:
type: "NoEnd"
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
```

The `Recurrence` settings is made up of two parts: `Pattern` (how often the time window will repeat) and `Range` (for how long the recurrence pattern will repeat).
#### Recurrence Pattern
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated

There are two possible recurrence pattern types: `Daily` and `Weekly`. For example, a time window could repeat "every day", "every 3 days", "every Monday" or "on Friday per 2 weeks".
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated

Depending on the type, certain fields of the `Pattern` are required, optional, or ignored.
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated

- `Daily`

The daily recurrence pattern causes the time window to repeat based on a number of days between each occurrence.

| Property | Relevance | Description |
|----------|-----------|-------------|
| **Type** | Required | Must be set to `Daily`. |
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
| **Interval** | Optional | Specifies the number of days between each occurrence. Default value is 1. |
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated

- `Weekly`

The weekly recurrence pattern causes the time window to repeat on the same day or days of the week, based on the number of weeks between each set of occurrences.

| Property | Relevance | Description |
|----------|-----------|-------------|
| **Type** | Required | Must be set to `Weekly`. |
| **DaysOfWeek** | Required | Specifies on which day(s) of the week the event occurs. |
| **Interval** | Optional | Specifies the number of weeks between each set of occurrences. Default value is 1. |
| **FirstDayOfWeek** | Optional | Specifies which day is considered the first day of the week. Default value is `Sunday`. |

The following example will repeat the time window every other Monday and Tuesday

```yaml
pattern:
type: "Weekly",
interval: 1,
daysOfWeek:
- Monday
- TuesDay
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
```
**Note:** `Start` must be a valid first occurrence which fits the recurrence pattern. Additionally, the duration of the time window cannot be longer than how frequently it occurs. For example, it is invalid to have a 25-hour time window recur every day.
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated

#### Recurrence Range
Comment thread
ivywei0125 marked this conversation as resolved.

There are three possible recurrence range type: `NoEnd`, `EndDate` and `Numbered`.

- `NoEnd`

The `NoEnd` range causes the recurrence to occur indefinitely.

| Property | Relevance | Description |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the way we are documenting this is confusing. It seems like each block of these are their own sets of fields. Where they are in-fact the same field with different values.

|----------|-----------|-------------|
| **Type** | Required | Must be set to `NoEnd`. |

- `EndDate`

The `EndDate` range causes the time window to occur on all days that fit the applicable pattern until the end date.

| Property | Relevance | Description |
|----------|-----------|-------------|
| **Type** | Required | Must be set to `EndDate`. |
| **EndDate** | Required | Specifies the date time to stop applying the pattern. Note that as long as the start time of the last occurrence falls before the end date, the end time of that occurrence is allowed to extend beyond it. |
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated

The following example will repeat the time window every day until the last occurrence happens on April 1st, 2024.

``` yaml
start: "Fri, 22 Mar 2024 18:00:00 GMT",
end: "Fri, 22 Mar 2024 20:00:00 GMT",
recurrence:
pattern:
type: "Daily",
interval: 1
range:
type: "EndDate",
endDate: "Mon, 1 Apr 2024 20:00:00 GMT"
```

- `Numbered`

The `Numbered` range causes the time window to occur a fixed number of times (based on the pattern).

| Property | Relevance | Description |
|----------|-----------|-------------|
| **Type** | Required | Must be set to `Numbered`. |
| **NumberOfOccurrences** | Required | Specifies the number of occurrences. |
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated

The following example will repeat the time window on Monday and Tuesday until the there are 3 occurrences, which respectively happens on April 1st(Mon), April 2nd(Tue) and April 8th(Mon).

``` yaml
start: "Mon, 1 Apr 2024 18:00:00 GMT",
end: "Mon, 1 Apr 2024 20:00:00 GMT",
recurrence:
pattern:
type: "Weekly",
interval: 1
daysOfWeek:
- Monday
- TuesDay
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
range:
type: "Numbered",
numberOfOccurrences: 3
```

To create a recurrence rule, you must specify both `Pattern` and `Range`. Any pattern type can work with any range type.

**Advanced:** The time zone offset of the `Start` property will apply to the recurrence settings.

### TargetingFilter

This filter provides the capability to enable a feature for a target audience. An in-depth explanation of targeting is explained in the targeting section below. The filter parameters include an audience object which describes users, groups, and a default percentage of the user base that should have access to the feature, and an exclusion object for users and groups that should never be targeted. Each group object that is listed in the target audience must also specify what percentage of the group's members should have access. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will have the feature enabled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
package com.azure.spring.cloud.feature.management;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -15,6 +18,7 @@
*/
@Configuration
@EnableConfigurationProperties({ FeatureManagementConfigProperties.class, FeatureManagementProperties.class })
@EnableCaching
class FeatureManagementConfiguration {

/**
Expand All @@ -30,4 +34,9 @@ FeatureManager featureManager(ApplicationContext context,
FeatureManagementProperties featureManagementConfigurations, FeatureManagementConfigProperties properties) {
return new FeatureManager(context, featureManagementConfigurations, properties);
}

@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("settings");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.filters;

import com.azure.spring.cloud.feature.management.implementation.timewindow.TimeWindowFilterSettings;
import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceCachedService;
import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.RecurrenceConstants;
import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collection;
import java.util.Map;

import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_END;
import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_RECURRENCE;
import static com.azure.spring.cloud.feature.management.models.FilterParameters.TIME_WINDOW_FILTER_SETTING_START;

/**
Expand All @@ -26,6 +33,8 @@ public TimeWindowFilter() {
}

private static final Logger LOGGER = LoggerFactory.getLogger(TimeWindowFilter.class);
private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();

/**
* Evaluates whether a feature is enabled based on a configurable time window.
Expand All @@ -35,38 +44,55 @@ public TimeWindowFilter() {
*/
@Override
public boolean evaluate(FeatureFilterEvaluationContext context) {
String start = (String) context.getParameters().get(TIME_WINDOW_FILTER_SETTING_START);
String end = (String) context.getParameters().get(TIME_WINDOW_FILTER_SETTING_END);
final Map<String, Object> parameters = context.getParameters();
final Object recurrenceObject = parameters.get(TIME_WINDOW_FILTER_SETTING_RECURRENCE);
if (recurrenceObject != null) {
final Map<String, Object> recurrenceParameters = (Map<String, Object>) recurrenceObject;
final Object patternObj = recurrenceParameters.get(RecurrenceConstants.RECURRENCE_PATTERN);
if (patternObj != null) {
updateValueFromMapToList((Map<String, Object>) patternObj, RecurrenceConstants.RECURRENCE_PATTERN_DAYS_OF_WEEK);
}
}

ZonedDateTime now = ZonedDateTime.now();
final TimeWindowFilterSettings settings = OBJECT_MAPPER.convertValue(context.getParameters(), TimeWindowFilterSettings.class);
final ZonedDateTime now = ZonedDateTime.now();

if (!StringUtils.hasText(start) && !StringUtils.hasText(end)) {
if (settings.getStart() == null && settings.getEnd() == null) {
LOGGER.warn("The {} feature filter is not valid for feature {}. It must specify either {}, {}, or both.",
Comment thread
ivywei0125 marked this conversation as resolved.
this.getClass().getSimpleName(), context.getName(), TIME_WINDOW_FILTER_SETTING_START,
TIME_WINDOW_FILTER_SETTING_END);
return false;
}

ZonedDateTime startTime = null;
ZonedDateTime endTime = null;

try {
startTime = StringUtils.hasText(start)
? ZonedDateTime.parse(start, DateTimeFormatter.ISO_DATE_TIME)
: null;
endTime = StringUtils.hasText(end)
? ZonedDateTime.parse(end, DateTimeFormatter.ISO_DATE_TIME)
: null;
} catch (DateTimeParseException e) {
startTime = StringUtils.hasText(start)
? ZonedDateTime.parse(start, DateTimeFormatter.RFC_1123_DATE_TIME)
: null;
endTime = StringUtils.hasText(end)
? ZonedDateTime.parse(end, DateTimeFormatter.RFC_1123_DATE_TIME)
: null;

if ((settings.getStart() == null || now.isAfter(settings.getStart()))
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
&& (settings.getEnd() == null || now.isBefore(settings.getEnd()))) {
return true;
}

if (settings.getRecurrence() != null) {
final Duration timeWindowSpan = Duration.between(settings.getStart(), settings.getEnd());
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
ZonedDateTime closestRecurrence = RecurrenceCachedService.getClosestTime(settings, now);

// Recalculate the closest recurrence if we have passed the cached time window.
if (now.isAfter(closestRecurrence.plus(timeWindowSpan))) {
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
closestRecurrence = RecurrenceCachedService.updateClosestTime(settings, now);
}

if (closestRecurrence == null || now.isBefore(closestRecurrence)) {
return false;
}

return now.isBefore(closestRecurrence.plus(timeWindowSpan));
}
return (!StringUtils.hasText(start) || now.isAfter(startTime))
&& (!StringUtils.hasText(end) || now.isBefore(endTime));

return false;
}

private void updateValueFromMapToList(Map<String, Object> parameters, String key) {
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
Object objectMap = parameters.get(key);
if (objectMap instanceof Map) {
Collection<Object> toType = ((Map<String, Object>) objectMap).values();
parameters.put(key, toType);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.implementation.timewindow;

import com.azure.spring.cloud.feature.management.implementation.timewindow.recurrence.Recurrence;

import java.time.ZonedDateTime;

public class TimeWindowFilterSettings {
/**
* An optional start time used to determine when a feature should be enabled.
* If no start time is specified the time window is considered to have already started.
* */
private ZonedDateTime start;
/**
* An optional end time used to determine when a feature should be disabled.
* If no end time is specified the time window is considered to never end.
* */
private ZonedDateTime end;
/**
* Add-on recurrence rule allows the time window defined by Start and End to recur.
* The rule specifies both how often the time window repeats and for how long.
* */
private Recurrence recurrence;

/**
* @param startTime the start time to determine when a feature should be enabled
* */
public void setStart(String startTime) {
this.start = TimeWindowUtils.convertStringToDate(startTime);
}

/**
* @return start time
* */
public ZonedDateTime getStart() {
return start;
}

/**
* @param endTime the end time to determine when a feature should be disabled
* */
public void setEnd(String endTime) {
this.end = TimeWindowUtils.convertStringToDate(endTime);
}

/**
* @return end time
* */
public ZonedDateTime getEnd() {
return end;
}

/**
* @param recurrence the recurrence rule to specify both how often the time window repeats and for how long
* */
public void setRecurrence(Recurrence recurrence) {
this.recurrence = recurrence;
}

/**
* @return the recurrence rule
* */
public Recurrence getRecurrence() {
return recurrence;
}

@Override
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
public String toString() {
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
return "TimeWindowFilterSettings{"
+ "start=" + start
+ ", end=" + end
+ ", recurrence=" + recurrence
Comment thread
ivywei0125 marked this conversation as resolved.
Outdated
+ '}';
}
}
Loading