Skip to content
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

fix: Use ConcurrentHashMap for InMemoryProvider #1057

Merged
merged 8 commits into from
Aug 22, 2024

Conversation

ryanprayogo
Copy link
Contributor

Use ConcurrentHashMap for InMemoryProvider

  • Use ConcurrentHashMap for flags in InMemoryProvider to make it thread-safe

Related Issues

Fixes #1054

Notes

Follow-up Tasks

How to test

Signed-off-by: Ryan Prayogo <57620+ryanprayogo@users.noreply.github.com>
Copy link

codecov bot commented Aug 21, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 95.25%. Comparing base (a81957f) to head (b62f4ea).
Report is 1 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1057      +/-   ##
============================================
+ Coverage     95.04%   95.25%   +0.21%     
- Complexity      392      393       +1     
============================================
  Files            38       38              
  Lines           888      886       -2     
  Branches         54       54              
============================================
  Hits            844      844              
+ Misses           24       23       -1     
+ Partials         20       19       -1     
Flag Coverage Δ
unittests 95.25% <100.00%> (+0.21%) ⬆️

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.

Copy link

@AndiHofi AndiHofi left a comment

Choose a reason for hiding this comment

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

This is still not thread-safe.

Either the flags field has to be final and modified in-place.

Or use a normal HashMap and a lock for each access to the map.

Or an AtomicReference and the feature flags are held in an immutable Map (does not need to be immutable, but it prevents from changing the map accidentally after setting it to the AtomicReference.

The simplest option is to make the flags field final and update it with a normal put(key, value)

Also, in updateFlags(Map flags) the flagsChanged set should only contain the keys of the changed flags, not all flags the provider. That bug existed earlier already.

The test is likely unstable, independent if the InMemoryProvider is threadsafe or not. There is no guarantee that any of the scheduled tasks even started execution when the assertion for that flag runs.

@toddbaert toddbaert self-requested a review August 21, 2024 14:57
@toddbaert
Copy link
Member

This is still not thread-safe.

Either the flags field has to be final and modified in-place.

Or use a normal HashMap and a lock for each access to the map.

Or an AtomicReference and the feature flags are held in an immutable Map (does not need to be immutable, but it prevents from changing the map accidentally after setting it to the AtomicReference.

The simplest option is to make the flags field final and update it with a normal put(key, value)

Also, in updateFlags(Map flags) the flagsChanged set should only contain the keys of the changed flags, not all flags the provider. That bug existed earlier already.

I believe this is all correct. The use of the concurrent map is safer but there are still thread safety issues with this new implementaiton

The test is likely unstable, independent if the InMemoryProvider is threadsafe or not. There is no guarantee that any of the scheduled tasks even started execution when the assertion for that flag runs.

Yes, I generally find this sort of thing difficult to test, which is why I don't think testing this is a hard requirement. As long as there's a consensus on the thread safety I think we're OK to merge.

Copy link
Member

@toddbaert toddbaert left a comment

Choose a reason for hiding this comment

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

An improvement, certainly! but still a bit to go: #1057 (comment)

🙏

Signed-off-by: Ryan Prayogo <57620+ryanprayogo@users.noreply.github.com>
Signed-off-by: Ryan Prayogo <57620+ryanprayogo@users.noreply.github.com>
Comment on lines 133 to 154
@Test
void multithreadedTest() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(100);
List<Callable<Void>> updates = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
String flagKey = "multithreadedFlag" + i;
updates.add(() -> {
provider.updateFlag(flagKey, Flag.builder()
.variant("on", true)
.variant("off", false)
.defaultVariant("on")
.build());
return null;
});
}

executorService.invokeAll(updates);

for (int i = 0; i < 10000; i++) {
assertTrue(client.getBooleanValue("multithreadedFlag" + i, false));
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit dubious of the value of this test. Did you actually see it fail at least sometimes without your changes?

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 did write this test before making any changes and did see it had failed sometimes.

After my change, I have not observed any failures in this test.

Copy link
Member

@toddbaert toddbaert left a comment

Choose a reason for hiding this comment

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

I think this covers it! Thank you!

I'll wait a bit for a follow-up from @AndiHofi since their observations were pretty astute before.

@toddbaert toddbaert requested a review from AndiHofi August 21, 2024 19:08
Signed-off-by: Ryan Prayogo <57620+ryanprayogo@users.noreply.github.com>

provider.updateFlags(flags);

verify(handler, times(1))
Copy link
Member

@toddbaert toddbaert Aug 22, 2024

Choose a reason for hiding this comment

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

Oh, I think this test is a bit flaky. You probably will need to use Awaitility to make this assertion within some time frame, since handlers run asynchronously on another thread. If you search for await in this repo you'll see a bunch of similar examples.

Copy link
Member

Choose a reason for hiding this comment

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

I did this here since it was a quick change. I hope you don't mind @ryanprayogo

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@toddbaert I do not mind, thank you.

Would it be worth adding an .atMost(...) so the test doesn't hang forever in the case that the assertion doesn't pass?

Copy link
Member

Choose a reason for hiding this comment

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

There's a default time limit (I think 5s?). After that point the test fails with a timeout.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes, you're right 👌

Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Copy link

@AndiHofi AndiHofi left a comment

Choose a reason for hiding this comment

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

See comments in code

this.flags = new HashMap<>(flags);
public void updateFlags(Map<String, Flag<?>> newFlags) {
Set<String> flagsChanged = new HashSet<>(newFlags.keySet());
flagsChanged.removeAll(this.flags.keySet());

Choose a reason for hiding this comment

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

This is not needed.

flagsChanged has to be new HashSet(newFlags.keySet())
otherwise it would be "flagsAdded" ... that value is used to notify listeners that a flag value may have changed and that they need to read the feature flag value again.

Then it matches the behavior of updateFlag(String flagKey, Flag newFlag) that also sends the update event for the modified flag in all cases.

What would be the perfect solution is something like this: (pseudo-code)

Set flagsChanged = new HashSet(); // set of all flag keys that got modified, or added.
for (Map.Entry changed : newFlags.entrySet()) {
  if (!Objects.equals(changed.getValue(), this.flags.get(changed.getKey())) {
    flagsChanged.add(changed.getKey());
  }
}

I am not sure, if Flag implements proper equals and hashcode, and the InMemoryProvider is likely used by tests only, therefore sending too many keys is likely OK; it is OK for my use-cases at least, so the "complex" solution is optional.

Missing flag keys in the changed event are a problem though.

Copy link
Member

@toddbaert toddbaert Aug 22, 2024

Choose a reason for hiding this comment

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

Oh, nice catch.

Ya I think it's fine just to list all flags here. This is just a testing util.

@ryanprayogo you can mention that all flags will be put into the change notification. This is a testing util primarily, and anyway providers get to decide what constitutes "changed" for their purposes.

}

@Test
void multithreadedTest() throws InterruptedException {

Choose a reason for hiding this comment

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

Please remove this test. It is likely not stable and would cause build problems for others.

It is now a thing wrapper over ConcurrentHashMap (with some event handling), there is little value in testing thread-safety of ConcurrentHashMap.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure the test is unstable, but I do question it's value.

Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Copy link

@toddbaert
Copy link
Member

@ryanprayogo I've made the small changes recommended by @AndiHofi . I'm sure you could have done them, but I want to get this into the next release I was hoping to do today! Thanks for fixing this (and also for your other Java 21 fix!)

@toddbaert toddbaert merged commit b7ed041 into open-feature:main Aug 22, 2024
10 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.

Thread safe in-memory provider
5 participants