Skip to content

Commit 6fc0b90

Browse files
feat: error resolution flow control without exceptions (#1095)
* Allowing flowcontrol with out exceptions So far we used exception to handle our flowcontrol, but Exceptions are costly. In the end we enriched our evaluation Details with errorCode and errorMessage. This can be also handled by the providers if desired, to reduce the execution footprint in errornous cases, which do not have to be exceptions. Eg FlagNotFound - it might be the case, but in performance critical environments, an exception rather than a normal return, can cause overhead and can be already too costly. Signed-off-by: Simon Schrottner <[email protected]> * fix: adding reason, and removing stacktraces from errors Signed-off-by: Simon Schrottner <[email protected]> * Update src/main/java/dev/openfeature/sdk/exceptions/TypeMismatchError.java Co-authored-by: Justin Abrahms <[email protected]> Signed-off-by: Simon Schrottner <[email protected]> --------- Signed-off-by: Simon Schrottner <[email protected]> Co-authored-by: Justin Abrahms <[email protected]>
1 parent 29901b8 commit 6fc0b90

File tree

7 files changed

+110
-16
lines changed

7 files changed

+110
-16
lines changed

Diff for: src/main/java/dev/openfeature/sdk/OpenFeatureClient.java

+12-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
* You should not instantiate this or reference this class.
2222
* Use the dev.openfeature.sdk.Client interface instead.
2323
* @see Client
24-
*
24+
*
2525
* @deprecated // TODO: eventually we will make this non-public. See issue #872
2626
*/
2727
@Slf4j
@@ -132,7 +132,11 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
132132

133133
details = FlagEvaluationDetails.from(providerEval, key);
134134
if (details.getErrorCode() != null) {
135-
throw ExceptionUtils.instantiateErrorByErrorCode(details.getErrorCode(), details.getErrorMessage());
135+
OpenFeatureError error = ExceptionUtils.instantiateErrorByErrorCode(
136+
details.getErrorCode(),
137+
details.getErrorMessage());
138+
enrichDetailsWithErrorDefaults(defaultValue, details);
139+
hookSupport.errorHooks(type, afterHookContext, error, mergedHooks, hints);
136140
} else {
137141
hookSupport.afterHooks(type, afterHookContext, details, mergedHooks, hints);
138142
}
@@ -146,8 +150,7 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
146150
details.setErrorCode(ErrorCode.GENERAL);
147151
}
148152
details.setErrorMessage(e.getMessage());
149-
details.setValue(defaultValue);
150-
details.setReason(Reason.ERROR.toString());
153+
enrichDetailsWithErrorDefaults(defaultValue, details);
151154
hookSupport.errorHooks(type, afterHookContext, e, mergedHooks, hints);
152155
} finally {
153156
hookSupport.afterAllHooks(type, afterHookContext, mergedHooks, hints);
@@ -156,6 +159,11 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
156159
return details;
157160
}
158161

162+
private static <T> void enrichDetailsWithErrorDefaults(T defaultValue, FlagEvaluationDetails<T> details) {
163+
details.setValue(defaultValue);
164+
details.setReason(Reason.ERROR.toString());
165+
}
166+
159167
/**
160168
* Merge invocation contexts with API, transaction and client contexts.
161169
* Does not merge before context.

Diff for: src/main/java/dev/openfeature/sdk/exceptions/FlagNotFoundError.java

+4-7
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@
44
import lombok.Getter;
55
import lombok.experimental.StandardException;
66

7-
@SuppressWarnings("checkstyle:MissingJavadocType")
7+
@SuppressWarnings({"checkstyle:MissingJavadocType", "squid:S110"})
88
@StandardException
9-
public class FlagNotFoundError extends OpenFeatureError {
9+
public class FlagNotFoundError extends OpenFeatureErrorWithoutStacktrace {
1010
private static final long serialVersionUID = 1L;
11-
@Getter private final ErrorCode errorCode = ErrorCode.FLAG_NOT_FOUND;
11+
@Getter
12+
private final ErrorCode errorCode = ErrorCode.FLAG_NOT_FOUND;
1213

13-
@Override
14-
public synchronized Throwable fillInStackTrace() {
15-
return this;
16-
}
1714
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package dev.openfeature.sdk.exceptions;
2+
3+
import lombok.experimental.StandardException;
4+
5+
@SuppressWarnings("checkstyle:MissingJavadocType")
6+
@StandardException
7+
public abstract class OpenFeatureErrorWithoutStacktrace extends OpenFeatureError {
8+
private static final long serialVersionUID = 1L;
9+
10+
@Override
11+
public synchronized Throwable fillInStackTrace() {
12+
return this;
13+
}
14+
}

Diff for: src/main/java/dev/openfeature/sdk/exceptions/ProviderNotReadyError.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import lombok.Getter;
55
import lombok.experimental.StandardException;
66

7-
@SuppressWarnings("checkstyle:MissingJavadocType")
7+
@SuppressWarnings({"checkstyle:MissingJavadocType", "squid:S110"})
88
@StandardException
9-
public class ProviderNotReadyError extends OpenFeatureError {
9+
public class ProviderNotReadyError extends OpenFeatureErrorWithoutStacktrace {
1010
private static final long serialVersionUID = 1L;
1111
@Getter private final ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY;
1212
}

Diff for: src/main/java/dev/openfeature/sdk/exceptions/TypeMismatchError.java

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/**
88
* The type of the flag value does not match the expected type.
99
*/
10+
@SuppressWarnings({"checkstyle:MissingJavadocType", "squid:S110"})
1011
@StandardException
1112
public class TypeMismatchError extends OpenFeatureError {
1213
private static final long serialVersionUID = 1L;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package dev.openfeature.sdk;
2+
3+
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
4+
5+
public class AlwaysBrokenWithDetailsProvider implements FeatureProvider {
6+
7+
@Override
8+
public Metadata getMetadata() {
9+
return () -> {
10+
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
11+
};
12+
}
13+
14+
@Override
15+
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
16+
return ProviderEvaluation.<Boolean>builder()
17+
.errorMessage(TestConstants.BROKEN_MESSAGE)
18+
.errorCode(ErrorCode.FLAG_NOT_FOUND)
19+
.build();
20+
}
21+
22+
@Override
23+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
24+
return ProviderEvaluation.<String>builder()
25+
.errorMessage(TestConstants.BROKEN_MESSAGE)
26+
.errorCode(ErrorCode.FLAG_NOT_FOUND)
27+
.build();
28+
}
29+
30+
@Override
31+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
32+
return ProviderEvaluation.<Integer>builder()
33+
.errorMessage(TestConstants.BROKEN_MESSAGE)
34+
.errorCode(ErrorCode.FLAG_NOT_FOUND)
35+
.build();
36+
}
37+
38+
@Override
39+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
40+
return ProviderEvaluation.<Double>builder()
41+
.errorMessage(TestConstants.BROKEN_MESSAGE)
42+
.errorCode(ErrorCode.FLAG_NOT_FOUND)
43+
.build();
44+
}
45+
46+
@Override
47+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext invocationContext) {
48+
return ProviderEvaluation.<Value>builder()
49+
.errorMessage(TestConstants.BROKEN_MESSAGE)
50+
.errorCode(ErrorCode.FLAG_NOT_FOUND)
51+
.build();
52+
}
53+
}

Diff for: src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java

+24-3
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
113113
OpenFeatureAPI.getInstance().setProvider(providerName, provider);
114114
assertThat(api.getProvider(providerName).getState()).isEqualTo(ProviderState.NOT_READY);
115115
Client client = OpenFeatureAPI.getInstance().getClient(providerName);
116-
assertEquals(ErrorCode.PROVIDER_NOT_READY, client.getBooleanDetails("return_error_when_not_initialized", false).getErrorCode());
116+
FlagEvaluationDetails<Boolean> details = client.getBooleanDetails("return_error_when_not_initialized", false);
117+
assertEquals(ErrorCode.PROVIDER_NOT_READY, details.getErrorCode());
118+
assertEquals(Reason.ERROR.toString(), details.getReason());
117119
}
118120

119121
@Specification(number="1.1.5", text="The API MUST provide a function for retrieving the metadata field of the configured provider.")
@@ -259,10 +261,29 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
259261
@Test void broken_provider() {
260262
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
261263
Client c = api.getClient();
262-
assertFalse(c.getBooleanValue("key", false));
263-
FlagEvaluationDetails<Boolean> details = c.getBooleanDetails("key", false);
264+
boolean defaultValue = false;
265+
assertFalse(c.getBooleanValue("key", defaultValue));
266+
FlagEvaluationDetails<Boolean> details = c.getBooleanDetails("key", defaultValue);
264267
assertEquals(ErrorCode.FLAG_NOT_FOUND, details.getErrorCode());
265268
assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage());
269+
assertEquals(Reason.ERROR.toString(), details.getReason());
270+
assertEquals(defaultValue, details.getValue());
271+
}
272+
273+
@Specification(number="1.4.8", text="In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.")
274+
@Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")
275+
@Specification(number="1.4.10", text="Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")
276+
@Specification(number="1.4.13", text="In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.")
277+
@Test void broken_provider_withDetails() {
278+
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenWithDetailsProvider());
279+
Client c = api.getClient();
280+
boolean defaultValue = false;
281+
assertFalse(c.getBooleanValue("key", defaultValue));
282+
FlagEvaluationDetails<Boolean> details = c.getBooleanDetails("key", defaultValue);
283+
assertEquals(ErrorCode.FLAG_NOT_FOUND, details.getErrorCode());
284+
assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage());
285+
assertEquals(Reason.ERROR.toString(), details.getReason());
286+
assertEquals(defaultValue, details.getValue());
266287
}
267288

268289
@Specification(number="1.4.11", text="Methods, functions, or operations on the client SHOULD NOT write log messages.")

0 commit comments

Comments
 (0)