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

Handle missing flags in a valid JSON configuration request #38

Merged
merged 18 commits into from
Apr 5, 2024

Conversation

aarsilv
Copy link
Contributor

@aarsilv aarsilv commented Apr 4, 2024

Eppo Internal: 🎟️ Ticket: FF-1869 - crash in android app Attempt to invoke virtual method 'java.util.Set j$.util.concurrent.ConcurrentHashMap.keySet()' on a null object reference

If, for whatever reason, the request for a configuration is technically "successful," but the resulting JSON object is missing the flags property, it will result in a downstream error.

To resolve this--and allow future polls to set the flags--we treat it as having received an empty list of flags.

if (!lock.await(10000, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Request for RAC did not complete within timeout");
}
} catch (InterruptedException e) {
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 handling InterruptedException here and removing the throws from the signature, it cleaned up our test code a decent amount. 🧹

@@ -360,13 +356,76 @@ private List<JsonElement> getJSONAssignments(AssignmentTestCase testCase) {
return (List<JsonElement>) this.getAssignments(testCase, AssignmentValueType.JSON);
}

private static String getMockRandomizedAssignmentResponse() {
@Test
public void testInvalidConfigJSON() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test that reproduced the issue prior to the fix

Comment on lines +374 to +384
httpClientOverrideField = EppoClient.class.getDeclaredField("httpClientOverride");
httpClientOverrideField.setAccessible(true);
httpClientOverrideField.set(null, mockHttpClient);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mockito (and related packages) was not playing nicely with Android so I bailed to this less clean (but works!) solution of private static variables and reflection to inject a mock HTTP client without exposing the ability to in the "public" API.

}

@Test
public void testCachedBadResponseAllowsLaterFetching() {
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 made this test to check another possible cause of bad flags: a cached but invalid JSON file. It turns out it was handling things ok but I'll keep the test around to ensure this continues to work.

;
String result = EppoClient.getInstance().getStringAssignment("dummy subject", "dummy flag");
assertNull(result);
// Failure callback will have fired from cache read error, but configuration request will still be fired off on init
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Side note: I think we should consider reworking things so that we can use another callback on loadFromCache before firing off our request. The way things are now, if there is a cache file the initialization callback will be used regardless on whether the cache was used or not. So if the cache failed but we got a valid configuration 500ms later, onFailure() will be called, which I don't think fully represents what really happened.

Fixing that is outside the scope of this PR, but if we're interested in changing this we can file another ticket.

@@ -55,6 +56,10 @@ public boolean loadFromCache(InitializationCallback callback) {
InputStreamReader reader = cacheFile.getInputReader();
RandomizationConfigResponse configResponse = gson.fromJson(reader, RandomizationConfigResponse.class);
reader.close();
if (configResponse == null || configResponse.getFlags() == null) {
// Invalid cached configuration, initialize as an empty map and delete file
throw new JsonSyntaxException("Configuration file missing flags");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Make a more helpful error message just in case this ever does happen for whatever reason

Comment on lines 85 to 88
if (flags == null) {
Log.w(TAG, "Flags missing configuration response");
flags = new ConcurrentHashMap<>();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the fix

Comment on lines +39 to +40
// Useful for testing in situations where we want to mock the http client
private static EppoHttpClient httpClientOverride = null;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

My workaround for injecting an EppoHttpClient without Mockito magic

Comment on lines +85 to +88
if (flags == null) {
Log.w(TAG, "Flags missing in configuration response");
flags = new ConcurrentHashMap<>();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These four lines are the actual fix 😅

System.out.println("Sleeping for a bit to wait for cache population to complete");
Thread.sleep(10000);
// wait for a bit since cache file is loaded asynchronously
waitForNonNullAssignment();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test seemed flakey for me so I changed the hard-coded wait to use a checking one another test uses too

}
try {
initClient(TEST_HOST, false, false, false);
} catch (InterruptedException e) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Side note, I tried refactoring throws InterruptedException out of initClient() signature (example commit), but it seemed to (inexplicably) be what was breaking testCachedAssignments() the most 🤷

@leoromanovsky
Copy link
Member

bump versions if you want to publish ASAP.

@aarsilv aarsilv force-pushed the aaron/ff-1869/handle-null-flags branch from 094e451 to 83433fd Compare April 5, 2024 18:07
@aarsilv aarsilv merged commit 540f874 into main Apr 5, 2024
1 check passed
@aarsilv aarsilv deleted the aaron/ff-1869/handle-null-flags branch April 5, 2024 18:31
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.

2 participants