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

Save cache file as received rather than serializing it again #51

Merged
merged 3 commits into from
May 24, 2024

Conversation

aarsilv
Copy link
Contributor

@aarsilv aarsilv commented May 23, 2024

Eppo Internal:
🎟️ ticket: FF-2159 - Android SDK Needs a custom serializer to handle code minification

If ProGuard is used, then Gson won't serialize flag configurations with known field names. To get around this, we should just write the JSON file exactly as we received it.

Verified fix checking the logs of our example app, which has its source minified using ProGuard.

Before:

Loading from cache
Error loading from local cache
java.lang.NullPointerException: Attempt to invoke virtual method 'int com.google.gson.JsonElement.getAsInt()' on a null object reference

After:

Loading from cache
Cache loaded successfully
Initialized from cache

@@ -51,15 +50,15 @@
import cloud.eppo.android.dto.FlagConfig;
import cloud.eppo.android.dto.RandomizationConfigResponse;
import cloud.eppo.android.dto.SubjectAttributes;
import cloud.eppo.android.dto.deserializers.EppoValueAdapter;
import cloud.eppo.android.dto.adapters.EppoValueAdapter;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that I understand more about Adapters, I renamed this package back to adapters (how it was earlier)


public class EppoClientTest {
private static final String TAG = logTag(EppoClient.class);
private static final String DUMMY_API_KEY = "mock-api-key";
private static final String DUMMY_OTHER_API_KEY = "another-mock-api-key";
private static final String TEST_HOST = "https://us-central1-eppo-qa.cloudfunctions.net/serveGitHubRacTestFile";
private static final String INVALID_HOST = "https://thisisabaddomainforthistest.com";
private Gson gson = new GsonBuilder()
private final Gson gson = new GsonBuilder()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🧹 some cleanup suggested by Android Studio

@@ -266,7 +265,7 @@ public void testCachedAssignments() {
// First initialize successfully
initClient(TEST_HOST, true, true, false, DUMMY_API_KEY); // ensure cache is populated

// wait for a bit since cache file is loaded asynchronously
// wait for a bit since cache file is written asynchronously
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 really what we are waiting for

Comment on lines +27 to +31
// We have two at-bats to load the configuration: loading from cache and fetching
// The below variables help them keep track of each other's progress
AtomicBoolean cacheLoadInProgress = new AtomicBoolean(true);
AtomicReference<String> fetchErrorMessage = new AtomicReference<>(null);
// We only want to fire the callback off once; so track whether or not we have yet
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pulling back comments and tracking used in the UFC branch as the are more robust than what we have here.

Comment on lines +88 to +89
public void setFlagsFromJsonString(String jsonString) {
RandomizationConfigResponse config = gson.fromJson(jsonString, RandomizationConfigResponse.class);
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 now takes and passes to Gson a string instead of a reader.

This should be fine, provided our responses are relatively (<100mb) small, which they will be.

}

private void writeConfigToFile(RandomizationConfigResponse config) {
public void asyncWriteToCache(String jsonString) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Promoted this to an upstream public function, as it seemed a weird side effect of a function that sets flags.

Copy link
Contributor

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 is a weird side effect. This is essentially a write through cache, which is a very common pattern

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah that's a good point; I'm keeping that pattern just separating the responsibilities of the underlying functions

@@ -60,7 +52,11 @@ public void get(String path, RequestCallback callback) {
public void onResponse(Call call, Response response) {
if (response.isSuccessful()) {
Log.d(TAG, "Fetch successful");
callback.onSuccess(response.body().charStream());
try {
callback.onSuccess(response.body().string());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using string() to load the entire response into a string. This should be fine as long as its relatively small.


@Test
public void testDeserializingDouble() throws Exception {
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 do not actually throw any (known) Exception

AsyncTask.execute(() -> {
try {
synchronized (cacheFile) {
BufferedWriter writer = cacheFile.getWriter();
gson.toJson(config, writer);
writer.write(jsonString);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👉 CORE FIX 👈
Write the response body instead of re-serializing

@aarsilv aarsilv requested review from petzel and leoromanovsky May 23, 2024 21:54
@aarsilv aarsilv marked this pull request as ready for review May 23, 2024 21:54
@aarsilv aarsilv requested a review from greghuels May 23, 2024 22:01
Comment on lines -48 to -50
if (stringValue == "null") {
return EppoValue.valueOf();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

String equality uses .equals() so this was never true and we don't even do this shenanigans in other SDKs / UFC so just axing it

cacheLoadInProgress.set(false);
Log.d(TAG, "Did not initialize from cache");
// If cache loading failed, and fetching failed, fire off the failure callback if not yet done so
// Otherwise, if fetching has not failed yet, defer to it for firing off callbacks
Copy link
Member

Choose a reason for hiding this comment

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

What happens is about the same time the fetching exceptional handler checks cacheLoadInProgress, thinks it is in progress and returns? I guess this is covered right - we will eventually either get good data from the cache or empty data from the cache (if its empty). This is a good endpoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah so if cacheLoadInProgress is checked right before it's set to false, then the fetcher won't fire off the callbacks but the cache load completion will because the final check for callback called flag is an atomic check and a set

@aarsilv aarsilv assigned aarsilv and unassigned leoromanovsky May 24, 2024
@aarsilv aarsilv merged commit e7239ab into main May 24, 2024
1 check passed
@aarsilv aarsilv deleted the aaron/ff-2159/explicit-serialize branch May 24, 2024 04:25
@sameerank sameerank self-requested a review May 28, 2024 20:35
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.

3 participants