Skip to content

feat: add means of awaiting event emission, fix flaky build #1463

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

Conversation

chrfwow
Copy link
Contributor

@chrfwow chrfwow commented May 28, 2025

This PR

  • adds the ability to await event emissions by returning a new construct
  • uses this construct in tests

⚠️ in rare cases, this could be a breaking change, but the events API is currently experimental, so we will not do a major version ubmp

Related Issues

Fixes #1449

@chrfwow chrfwow requested review from a team as code owners May 28, 2025 12:04
final TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> localOnEmit = this.onEmit;
if (localOnEmit != null) {
emitterExecutor.submit(() -> localOnEmit.accept(this, event, details));
try (var ignored = OpenFeatureAPI.lock.readLockAutoCloseable()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not nice to access the static lock that's clearly only exposed for testing, but we need to use this very lock to protect against concurrent reads/writes to the underlying map.

I removed the call to the emitterExecutor, so now all listeners are invoked sequentially on the calling thread. This was needed as several tests expect the state of the provider to be set immediately after calling this method.
If we consider this a problem, I could work around this issue by returning something our tests can wait for. In this case, we would need to call both the localOnEmit and the localEventProviderListener in the emitterExecutor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By moving the call from the emitterExecutor to the current thread, I reintroduced a race condition that was already fixed.
Therefore I added the executor again. This means that some tests will fail that expect the state of the provider to be updated immediately after a call to emit. This is why I now return an Awaitable, for which those tests (or users) can wait.

@@ -21,7 +21,6 @@
@Slf4j
public abstract class EventProvider implements FeatureProvider {
private EventProviderListener eventProviderListener;
private final ExecutorService emitterExecutor = Executors.newCachedThreadPool();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not needed anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wrong, we do need it

Copy link
Member

@aepfli aepfli left a comment

Choose a reason for hiding this comment

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

Thank you! I really would love to have the same detail and knowledge as you have for concurrency. Thank you!

@chrfwow chrfwow requested a review from aepfli June 2, 2025 13:02
@chrfwow chrfwow requested a review from a team June 2, 2025 13:03
Copy link

codecov bot commented Jun 3, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 93.40%. Comparing base (6cca721) to head (0228a70).
Report is 1 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1463      +/-   ##
============================================
+ Coverage     92.88%   93.40%   +0.51%     
- Complexity      483      490       +7     
============================================
  Files            45       46       +1     
  Lines          1167     1182      +15     
  Branches        101      103       +2     
============================================
+ Hits           1084     1104      +20     
+ Misses           53       48       -5     
  Partials         30       30              
Flag Coverage Δ
unittests 93.40% <100.00%> (+0.51%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chrfwow
Copy link
Contributor Author

chrfwow commented Jun 3, 2025

@toddbaert in theory, this is a breaking change. The state of the provider is not set synchronously when calling the emit() method, but only after the emitterExecutor runs. Users can wait for the state change to be propagated by calling await() on the return value of emit().

@chrfwow chrfwow requested a review from a team June 4, 2025 14:55
@chrfwow chrfwow requested a review from toddbaert June 5, 2025 08:15
/**
* A class to help with synchronization.
*/
public class Awaitable {
Copy link
Member

Choose a reason for hiding this comment

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

I like this.

handlerMap.put(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, new ArrayList<>());
handlerMap.put(ProviderEvent.PROVIDER_ERROR, new ArrayList<>());
handlerMap.put(ProviderEvent.PROVIDER_STALE, new ArrayList<>());
handlerMap.put(ProviderEvent.PROVIDER_READY, new ConcurrentLinkedQueue<>());
Copy link
Member

Choose a reason for hiding this comment

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

Good idea.

@@ -93,8 +110,8 @@ public void emit(ProviderEvent event, ProviderEventDetails details) {
*
* @param details The details of the event
*/
public void emitProviderReady(ProviderEventDetails details) {
emit(ProviderEvent.PROVIDER_READY, details);
public Awaitable emitProviderReady(ProviderEventDetails details) {
Copy link
Member

@toddbaert toddbaert Jun 9, 2025

Choose a reason for hiding this comment

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

This is a public class people can override to implement a provider that supports events. I like this API and I think it's handy to have.

I was trying to see if there was any way this could be considered breaking. I tried adding this at the top of the class:

private BiConsumer<ProviderEvent, ProviderEventDetails> doesThisCompile = this::emit;

And (surprisingly to me) before and after your change, this still compiles even though BIcConsumer returns void. Apparently, return values of method references are ignored when assigned to void functional interfaces!

EDIT:

Oh - I just realized, this could break implementations that override these methods... I think that would be relatively rare but it's possible. What do you think @aepfli @chrfwow ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think this would be much of a problem. Providers can just return Awaitable.FINISHED if they do this synchronously, otherwise it's probably best to return something to get informed about the state update anyway.

@toddbaert toddbaert changed the title fix: Flaky build due to a possible race condition feat: add means of awaiting event emission, fix flaky build Jun 10, 2025
Copy link

@toddbaert toddbaert merged commit 3dd7d5d into open-feature:main Jun 10, 2025
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Flaky build due to a possible race condition
4 participants