Skip to content

Commit df6f237

Browse files
Add support scheduled rollout
Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org>
1 parent 119ecde commit df6f237

File tree

8 files changed

+187
-19
lines changed

8 files changed

+187
-19
lines changed
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
11
package dev.openfeature.contrib.providers.gofeatureflag.bean;
22

33
import java.util.List;
4-
import java.util.Map;
54
import lombok.Data;
5+
import lombok.EqualsAndHashCode;
66

77
/**
88
* Flag is a class that represents a feature flag for GO Feature Flag.
99
*/
10+
@EqualsAndHashCode(callSuper = true)
1011
@Data
11-
public class Flag {
12-
private Map<String, Object> variations;
13-
private List<Rule> targeting;
14-
private String bucketingKey;
15-
private Rule defaultRule;
16-
private ExperimentationRollout experimentation;
17-
private Boolean trackEvents;
18-
private Boolean disable;
19-
private String version;
20-
private Map<String, Object> metadata;
21-
// TODO: scheduledRollout
12+
public class Flag extends FlagBase {
13+
private List<ScheduledStep> scheduledRollout;
2214
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dev.openfeature.contrib.providers.gofeatureflag.bean;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import lombok.Data;
6+
7+
/**
8+
* FlagBase is a class that represents the base structure of a feature flag for GO Feature Flag.
9+
*/
10+
@Data
11+
public abstract class FlagBase {
12+
private Map<String, Object> variations;
13+
private List<Rule> targeting;
14+
private String bucketingKey;
15+
private Rule defaultRule;
16+
private ExperimentationRollout experimentation;
17+
private Boolean trackEvents;
18+
private Boolean disable;
19+
private String version;
20+
private Map<String, Object> metadata;
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package dev.openfeature.contrib.providers.gofeatureflag.bean;
2+
3+
import java.util.Date;
4+
import lombok.Data;
5+
import lombok.EqualsAndHashCode;
6+
7+
/**
8+
* ScheduledStep is a class that represents a scheduled step in the rollout of a feature flag.
9+
*/
10+
@EqualsAndHashCode(callSuper = true)
11+
@Data
12+
public class ScheduledStep extends FlagBase {
13+
private Date date;
14+
}

providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/service/EvaluationService.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
*/
2121
@AllArgsConstructor
2222
public class EvaluationService {
23-
/** The evaluator used to evaluate the flags. */
23+
/**
24+
* The evaluator used to evaluate the flags.
25+
*/
2426
private IEvaluator evaluator;
2527

2628
/**
@@ -33,12 +35,16 @@ public boolean isFlagTrackable(final String flagKey) {
3335
return this.evaluator.isFlagTrackable(flagKey);
3436
}
3537

36-
/** Init the evaluator. */
38+
/**
39+
* Init the evaluator.
40+
*/
3741
public void init() {
3842
this.evaluator.init();
3943
}
4044

41-
/** Destroy the evaluator. */
45+
/**
46+
* Destroy the evaluator.
47+
*/
4248
public void destroy() {
4349
this.evaluator.destroy();
4450
}
@@ -62,6 +68,16 @@ public <T> ProviderEvaluation<T> getEvaluation(
6268

6369
val goffResp = evaluator.evaluate(flagKey, defaultValue, evaluationContext);
6470

71+
// If we have an error code, we return the error directly.
72+
if (goffResp.getErrorCode() != null && !goffResp.getErrorCode().isEmpty()) {
73+
return ProviderEvaluation.<T>builder()
74+
.errorCode(mapErrorCode(goffResp.getErrorCode()))
75+
.errorMessage(goffResp.getErrorDetails())
76+
.reason(Reason.ERROR.name())
77+
.value(defaultValue)
78+
.build();
79+
}
80+
6581
if (Reason.DISABLED.name().equalsIgnoreCase(goffResp.getReason())) {
6682
// we don't set a variant since we are using the default value,
6783
// and we are not able to know which variant it is.

providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/util/Const.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.fasterxml.jackson.databind.DeserializationFeature;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.databind.util.StdDateFormat;
56
import java.time.Duration;
67

78
/**
@@ -24,6 +25,7 @@ public class Const {
2425
public static final ObjectMapper DESERIALIZE_OBJECT_MAPPER =
2526
new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
2627
public static final ObjectMapper SERIALIZE_OBJECT_MAPPER = new ObjectMapper();
27-
public static final ObjectMapper SERIALIZE_WASM_MAPPER =
28-
new ObjectMapper().setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL);
28+
public static final ObjectMapper SERIALIZE_WASM_MAPPER = new ObjectMapper()
29+
.setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)
30+
.setDateFormat(new StdDateFormat().withColonInTimeZone(true));
2931
}

providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,65 @@ void shouldIgnoreConfigurationIfEtagIsDifferentByLastModifiedIsOlder() {
537537
Thread.sleep(300L);
538538
assertFalse(configurationChangedCalled.get());
539539
}
540+
541+
@DisplayName("Should apply a scheduled rollout step")
542+
@SneakyThrows
543+
@Test
544+
void shouldApplyAScheduledRolloutStep() {
545+
try (val s = new MockWebServer()) {
546+
val goffAPIMock = new GoffApiMock(GoffApiMock.MockMode.SCHEDULED_ROLLOUT_FLAG_CONFIG);
547+
s.setDispatcher(goffAPIMock.dispatcher);
548+
GoFeatureFlagProvider provider = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder()
549+
.endpoint(s.url("").toString())
550+
.evaluationType(EvaluationType.IN_PROCESS)
551+
.timeout(1000)
552+
.build());
553+
OpenFeatureAPI.getInstance().setProviderAndWait(testName, provider);
554+
val client = OpenFeatureAPI.getInstance().getClient(testName);
555+
val got = client.getBooleanDetails("my-flag", false, TestUtils.defaultEvaluationContext);
556+
val want = FlagEvaluationDetails.<Boolean>builder()
557+
.value(true)
558+
.variant("enabled")
559+
.flagKey("my-flag")
560+
.reason(Reason.TARGETING_MATCH.name())
561+
.flagMetadata(ImmutableMetadata.builder()
562+
.addString("description", "this is a test flag")
563+
.addBoolean("defaultValue", false)
564+
.build())
565+
.build();
566+
assertEquals(want, got);
567+
}
568+
}
569+
570+
@DisplayName("Should not apply a scheduled rollout step if the date is in the future")
571+
@SneakyThrows
572+
@Test
573+
void shouldNotApplyAScheduledRolloutStepIfTheDateIsInTheFuture() {
574+
try (val s = new MockWebServer()) {
575+
val goffAPIMock = new GoffApiMock(GoffApiMock.MockMode.SCHEDULED_ROLLOUT_FLAG_CONFIG);
576+
s.setDispatcher(goffAPIMock.dispatcher);
577+
GoFeatureFlagProvider provider = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder()
578+
.endpoint(s.url("").toString())
579+
.evaluationType(EvaluationType.IN_PROCESS)
580+
.timeout(1000)
581+
.build());
582+
OpenFeatureAPI.getInstance().setProviderAndWait(testName, provider);
583+
val client = OpenFeatureAPI.getInstance().getClient(testName);
584+
val got = client.getBooleanDetails(
585+
"my-flag-scheduled-in-future", true, TestUtils.defaultEvaluationContext);
586+
val want = FlagEvaluationDetails.<Boolean>builder()
587+
.value(false)
588+
.variant("disabled")
589+
.flagKey("my-flag-scheduled-in-future")
590+
.reason(Reason.STATIC.name())
591+
.flagMetadata(ImmutableMetadata.builder()
592+
.addString("description", "this is a test flag")
593+
.addBoolean("defaultValue", false)
594+
.build())
595+
.build();
596+
assertEquals(want, got);
597+
}
598+
}
540599
}
541600

542601
@Nested
@@ -555,7 +614,7 @@ void shouldSendTheEvaluationInformationToTheDataCollector() {
555614
val client = OpenFeatureAPI.getInstance().getClient(testName);
556615
client.getIntegerDetails("integer_key", 1000, TestUtils.defaultEvaluationContext);
557616
client.getIntegerDetails("integer_key", 1000, TestUtils.defaultEvaluationContext);
558-
Thread.sleep(400L);
617+
Thread.sleep(250L);
559618
assertEquals(1, goffAPIMock.getCollectorRequestsHistory().size());
560619
}
561620

providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/util/GoffApiMock.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ public class GoffApiMock {
2424
private int collectorCallCount = 0;
2525

2626
private int configurationCallCount = 0;
27-
/** lastRequestBody contains the body of the last request. */
27+
/**
28+
* lastRequestBody contains the body of the last request.
29+
*/
2830
@Getter
2931
private String lastRequestBody = null;
3032

@@ -120,6 +122,9 @@ public MockResponse handleCollector(RecordedRequest request) {
120122
public MockResponse handleFlagConfiguration(RecordedRequest request) {
121123
var configLocation = "valid-all-types.json";
122124
switch (mode) {
125+
case SCHEDULED_ROLLOUT_FLAG_CONFIG:
126+
configLocation = "valid-scheduled-rollout.json";
127+
break;
123128
case ENDPOINT_ERROR_404:
124129
return new MockResponse().setResponseCode(404);
125130
case SERVE_OLD_CONFIGURATION:
@@ -187,5 +192,6 @@ public enum MockMode {
187192
SIMPLE_CONFIG,
188193
DEFAULT,
189194
SERVE_OLD_CONFIGURATION,
195+
SCHEDULED_ROLLOUT_FLAG_CONFIG,
190196
}
191197
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"flags": {
3+
"my-flag": {
4+
"variations": {
5+
"disabled": false,
6+
"enabled": true
7+
},
8+
"defaultRule": {
9+
"percentage": {
10+
"enabled": 0,
11+
"disabled": 100
12+
}
13+
},
14+
"metadata": {
15+
"description": "this is a test flag",
16+
"defaultValue": false
17+
},
18+
"scheduledRollout": [
19+
{
20+
"targeting": [
21+
{
22+
"query": "targetingKey eq \"d45e303a-38c2-11ed-a261-0242ac120002\"",
23+
"variation": "enabled"
24+
}
25+
],
26+
"date": "2022-07-31T22:00:00.100Z"
27+
}
28+
]
29+
},
30+
"my-flag-scheduled-in-future": {
31+
"variations": {
32+
"disabled": false,
33+
"enabled": true
34+
},
35+
"defaultRule": {
36+
"percentage": {
37+
"enabled": 0,
38+
"disabled": 100
39+
}
40+
},
41+
"metadata": {
42+
"description": "this is a test flag",
43+
"defaultValue": false
44+
},
45+
"scheduledRollout": [
46+
{
47+
"targeting": [
48+
{
49+
"query": "targetingKey eq \"d45e303a-38c2-11ed-a261-0242ac120002\"",
50+
"variation": "enabled"
51+
}
52+
],
53+
"date": "3022-07-31T22:00:00.100Z"
54+
}
55+
]
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)