-
Notifications
You must be signed in to change notification settings - Fork 40
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
Conversation
Signed-off-by: Ryan Prayogo <57620+ryanprayogo@users.noreply.github.com>
Codecov ReportAll modified and coverable lines are covered by tests ✅
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
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 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.
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
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. |
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.
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>
src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java
Outdated
Show resolved
Hide resolved
src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java
Show resolved
Hide resolved
@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)); | ||
} | ||
} |
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 a bit dubious of the value of this test. Did you actually see it fail at least sometimes without your 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.
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.
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 think this covers it! Thank you!
I'll wait a bit for a follow-up from @AndiHofi since their observations were pretty astute before.
Signed-off-by: Ryan Prayogo <57620+ryanprayogo@users.noreply.github.com>
|
||
provider.updateFlags(flags); | ||
|
||
verify(handler, times(1)) |
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.
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.
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 did this here since it was a quick change. I hope you don't mind @ryanprayogo
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 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?
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.
There's a default time limit (I think 5s?). After that point the test fails with a timeout.
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.
Ah yes, you're right 👌
Signed-off-by: Todd Baert <todd.baert@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.
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()); |
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 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.
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.
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 { |
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.
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.
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 not sure the test is unstable, but I do question it's value.
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Quality Gate passedIssues Measures |
@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!) |
Use ConcurrentHashMap for InMemoryProvider
ConcurrentHashMap
forflags
inInMemoryProvider
to make it thread-safeRelated Issues
Fixes #1054
Notes
Follow-up Tasks
How to test