-
Notifications
You must be signed in to change notification settings - Fork 38
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
feat: make provider interface "stateless"; SDK maintains provider state #1096
feat: make provider interface "stateless"; SDK maintains provider state #1096
Conversation
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
@@ -94,7 +138,7 @@ public void emitProviderStale(ProviderEventDetails details) { | |||
/** | |||
* Emit a {@link ProviderEvent#PROVIDER_ERROR} event. | |||
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)} | |||
* | |||
* | |||
* @param details The details of the event | |||
*/ | |||
public void emitProviderError(ProviderEventDetails details) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should there also be a method like emitFatalProviderError
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would take the approach of just checking if the error is a Fatal error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can I do that? The ProviderEventDetails
parameter does not have a field containing a potential error code. If I can get it from getEventMetadata()
, what key
is the error code associated with?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I think that's another thing we need to add. I might have missed it in the issue, but it's in the spec: https://openfeature.dev/specification/types#event-details
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it queried like this?
String errorCode = details.getEventMetadata().getString("error code");
if("FATAL".equals(errorCode)){
...
}
I can't see how this should work from the documentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm saying the event details should have a first class member representing the error code (not in the event data), if it's an error event.
OpenFeatureAPI api = OpenFeatureAPI.getInstance(); | ||
api.setProvider(new NoOpProvider()); | ||
api.setProvider(TestEventsProvider.initialized()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of the tests had to be changed, as now an uninitialized provider will not allow the lookup of a value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to use setProviderAndWait
which will initialize the provider before continuing.
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
Thanks a lot for this! I will try to review tomorrow! 🙏 |
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1096 +/- ##
============================================
- Coverage 95.12% 93.91% -1.22%
- Complexity 401 430 +29
============================================
Files 39 40 +1
Lines 924 1019 +95
Branches 56 72 +16
============================================
+ Hits 879 957 +78
- Misses 24 39 +15
- Partials 21 23 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
throw new ProviderNotReadyError("provider not yet initialized"); | ||
} | ||
if (ProviderState.FATAL.equals(getState())) { | ||
throw new FatalError("provider in fatal error state"); | ||
} | ||
throw new GeneralError("unknown error"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The behaviour here is different than that of the OpenFeatureClient
, which would not throw this GeneralError
. Which bevaviour is the expected one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be able to delete this code entirely since initialization is tracked in the SDK, but things should also work fine if we don't touch this class at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so do you suggest to generate a Second deprecated InMemoryProvider to always ensure backwards compatibility and run the tests for both? or do you think that is overkill?
@Override | ||
public final void initialize(EvaluationContext evaluationContext) throws Exception { | ||
try { | ||
doInitialization(evaluationContext); | ||
providerState = ProviderState.READY; | ||
} catch (OpenFeatureError openFeatureError) { | ||
if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) { | ||
providerState = ProviderState.FATAL; | ||
} else { | ||
providerState = ProviderState.ERROR; | ||
} | ||
throw openFeatureError; | ||
} catch (Exception e) { | ||
providerState = ProviderState.ERROR; | ||
throw new GeneralError(e); | ||
} | ||
} | ||
|
||
protected void doInitialization(EvaluationContext evaluationContext) throws Exception { | ||
// Intentionally left blank, to be implemented by inheritors | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation is logically correct, but this won't work retroactively with existing implementations (many of which can be found here, though there's others maintained by vendors, etc).
We need to do this in a non-breaking way, without breaking any existing contracts or behvaiors. In C#, I accomplished this by creating a new package-private ProviderState field on the provider which the SDK updates after it has successfully (or unsuccessfully) run the initialize
method, and basically ignoring the previous provider state field.
This allows existing providers to work without breaking changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@toddbaert I wonder if there can be some additional automated step to see if the sdk-contrib repository providers are not breaking, possibly as warning / non-mandatory. There is the InMemoryProvider on this repository which is a good sanity, maybe checking the other providers as well can help. Or is too much? What do you think ?
* @param version a version identifier | ||
* @return a new client instance | ||
*/ | ||
public Client getClient(String domain, String version) { | ||
return new OpenFeatureClient(this, | ||
return new OpenFeatureClient( | ||
() -> providerRepository.getProvider(domain), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inside the ProviderRespository
, you will have to modify this private method to call the initialize function on the provider if it's not yet initialized or in the process of being initialized, then update a new package-private status member indicating readiness.
/** | ||
* Initializes the provider. | ||
* @param evaluationContext evaluation context | ||
* @throws Exception on error | ||
*/ | ||
@Override | ||
public void initialize(EvaluationContext evaluationContext) throws Exception { | ||
super.initialize(evaluationContext); | ||
state = ProviderState.READY; | ||
log.debug("finished initializing provider, state: {}", state); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a proof that nothing is breaking, you should be able to get away with making no changes to this provider.
if (ProviderEvent.PROVIDER_ERROR.equals(event)) { | ||
providerState = ProviderState.ERROR; | ||
} else if (ProviderEvent.PROVIDER_STALE.equals(event)) { | ||
providerState = ProviderState.STALE; | ||
} else if (ProviderEvent.PROVIDER_READY.equals(event)) { | ||
providerState = ProviderState.READY; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a specific test for this? It relates to specification requirement 5.3.5, so you can use our @Specification
annotation for tracking compliance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really good first pass! I've left some comments. The most important is here.
If you need more help or clarification, please reach out on slack.
@Override | ||
public final void shutdown() { | ||
providerState = ProviderState.NOT_READY; | ||
doShutdown(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why this split is needed ?
without it, provider can implement shutdown() same as today and calling super.shutdown() in it.
providers which were implemented this way, can continue to work as is.
similar with initialize().
@@ -35,8 +26,10 @@ public TestEventsProvider(int initTimeoutMs, boolean initError, String initError | |||
this.initErrorMessage = initErrorMessage; | |||
} | |||
|
|||
public TestEventsProvider(ProviderState initialState) { | |||
this.state = initialState; | |||
public static TestEventsProvider initialized() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to something like newTestEventsProvider() ?
class DeveloperExperienceTest implements HookFixtures { | ||
transient String flagKey = "mykey"; | ||
|
||
@Test void simpleBooleanFlag() { | ||
@Test void simpleBooleanFlag() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can use @SneakyThrows.
also at other places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ Apparently, that's someones username 😄
@@ -94,8 +90,12 @@ class MutatingHook implements Hook { | |||
|
|||
@Override | |||
// change the provider during a before hook - this should not impact the evaluation in progress | |||
public Optional before(HookContext ctx, Map hints) { | |||
FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider()); | |||
public Optional before(HookContext ctx, Map hints) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can use @SneakyThrows and remove try/catch ?
public void initialize(EvaluationContext evaluationContext) throws Exception { | ||
Awaitility.await().wait(3000); | ||
protected void doInitialization(EvaluationContext evaluationContext) throws Exception { | ||
Thread.sleep(10000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why change the implementation and wait that much longer ?
inMemoryProvider.initialize(null); | ||
inMemoryProvider.emitProviderError(ProviderEventDetails.builder().build()); | ||
|
||
// ErrorCode.GENERAL should be returned when evaluated via the client |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why would it return error after provider error event ?
any reference to this behavior ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reference to this behavior ?
No, but this is the current behaviour. See #1096 (comment) and the implementation
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
…ke-provider-stateless
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have one request for change: I think the @Delegate
annotation is a leftover from the Decorator approach and can be removed.
src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java
Outdated
Show resolved
Hide resolved
*/ | ||
@Deprecated() // TODO: eventually we will make this non-public. See issue #872 | ||
public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String version) { | ||
public OpenFeatureClient( | ||
ProviderAccessor providerAccessor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProviderAccessor
is exposed outside its defined visibility scope. OpenFeatureClient
and the constructor are public, but the ProviderAccessor
is package privet.
Today I learned... I would have expected the compiler to not allow such constructs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How should I handle this? Should I make the constructor package private, as it is deprecated anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add a package internal accessor to getProviderStateManager(String domain)
in OpenFeatureAPI
. Then you can access the state manager in the client and you can remove the ´ProviderAccessor` interface completely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done: 45e9ba2
} | ||
if (ProviderState.FATAL.equals(state)) { | ||
throw new FatalError("provider is in an irrecoverable error state"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We had some effort in parallel to handle evaluation errors without exceptions in #1095.
@toddbaert does this also apply to unexpected Provider states?
In general, how do we handle provider state here? If it's now managed by the SDK, does it only forward the evaluation calls to the provider, if it is in READY
state, or in any state except NOT_READY
and FATAL
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently we only forward calls if the provider is not in error (or fatal), which differs from the implementation of the InMemoryProvider
, where they are only forwarded when the provider is READY
. Is this what we want?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently we only forward calls if the provider is not in error (or fatal), which differs from the implementation of the InMemoryProvider, where they are only forwarded when the provider is READY. Is this what we want?
Yes, this is what we want. Some providers may be in an ERROR state, and still work in some sense (they may send error telemetry, or read from an out-of-date cache, or even try to reconnect before returning). The only states that should short-circuit are FATAL and NOT_READY. In these cases we can be sure the provider hasn't connected at all or is in such a bad state (FATAL) that it's pointless to even use and will never recover (perhaps a bad credential or something like that). This is defined in the spec (and implemented in other SDKs):
https://openfeature.dev/specification/sections/flag-evaluation#requirement-177
https://openfeature.dev/specification/sections/flag-evaluation#requirement-178
As far as throwing errors, yes - it would be better if we could instead return error resolutions but still run the error hooks, as we do in #1095 , to avoid the cost of creating the stack trace and exception.
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
…ke-provider-stateless
return providerRepository.getProviderState(domain); | ||
} | ||
|
||
public ProviderState getProviderState(FeatureProvider provider) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know I've requested this, but maybe it's better to not expose this on the public API surface unless someone requests it. We could also get rid of the additional code in the ProviderRepository
that handles this request again.
Sorry for being too greedy 🙈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This overload would return a null
state if the provider is not registered, which is what I would expect (or an exception?), but it's still different to the behavior of other getProviderState()
overloads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I guess the only reason this was done was for this assertion which we can avoid. I agree we should remove these methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done: 45e9ba2
@@ -82,12 +78,12 @@ void getApiInstance() { | |||
@Test void providerAndWait() { | |||
FeatureProvider provider = new TestEventsProvider(500); | |||
OpenFeatureAPI.getInstance().setProviderAndWait(provider); | |||
assertThat(api.getProvider().getState()).isEqualTo(ProviderState.READY); | |||
assertThat(api.getProviderState()).isEqualTo(ProviderState.READY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can instead get a client (even before we set the provider) and then check the Provider state there, so we can remove this public getProviderState
as @guidobrei suggests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done: 45e9ba2
@chrfwow I think I agree with @guidobrei 's points. Thanks for being so patient with all these reviews! I think we're almost there but I think the last few changes seem small. |
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
I pushed another changeset to resolve @guidobrei 's suggestions: 45e9ba2 |
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
57ef876
to
5816732
Compare
Quality Gate passedIssues Measures |
Thanks @chrfwow for all your help with this! It works great in the contrib repo so far. |
It was a pleasure! |
This PR
Providers no longer maintain their own state: the state for each provider is maintained in the SDK automatically, and updated according to the success/failures of lifecycle methods (init/shutdown) or events emitted from providers spontaneously.
Related Issues
#844
Please remove
getState
in implementing providers (the interface method has been marked as deprecated).