Skip to content

Rc realtime dev #3743

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

Merged
merged 68 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
488e8e7
Creates Realtime Http client file. Includes ConfigUpdate Listener & R…
qdpham13 May 13, 2022
fc01819
Creates Realtime Http client file. Includes ConfigUpdate Listener & R…
qdpham13 May 13, 2022
41898e7
Merge remote-tracking branch 'origin/rc-realtime-dev' into rc-realtim…
qdpham13 May 13, 2022
a00ce0c
Update stream name
qdpham13 May 16, 2022
31af37a
Add copyright info to top of file.
qdpham13 May 16, 2022
187864b
Update comments
qdpham13 May 16, 2022
a9d3a97
Format file.
qdpham13 May 17, 2022
3642424
Creates Realtime Http client file. Includes ConfigUpdate Listener & R…
qdpham13 May 13, 2022
19956a8
Update stream name
qdpham13 May 16, 2022
93512af
Add copyright info to top of file.
qdpham13 May 16, 2022
fa56069
Update comments
qdpham13 May 16, 2022
50da87e
Format file.
qdpham13 May 17, 2022
99c166c
Merge remote-tracking branch 'origin/rc-realtime-dev' into rc-realtim…
qdpham13 May 18, 2022
567aa14
Moved ConfigUpdateListener and ConfigUpdateListenerRegistration to pu…
qdpham13 May 18, 2022
4c16e92
Moved ConfigUpdateListener and ConfigUpdateListenerRegistration to pu…
qdpham13 May 18, 2022
e9e28b2
Format and update comments
qdpham13 May 18, 2022
10fefb5
Uodate api.txt
qdpham13 May 18, 2022
31777f5
Add nonnull and nullable to params
qdpham13 May 18, 2022
51bf244
Change registration to interface.
qdpham13 May 18, 2022
b5c4a96
Add author
qdpham13 May 18, 2022
7394492
Update api.txt
qdpham13 May 18, 2022
c35e1bc
Dependabot High severity alerts (#3734)
daymxn May 18, 2022
a4e4d1a
Do not attempt to parse empty json files in MetaDataStore (#3735)
mrober May 18, 2022
3630787
Disable javadoc for undocumented sdks. (#3738)
vkryachko May 19, 2022
83c6d8f
Creates Realtime Http client file. Includes ConfigUpdate Listener & R…
qdpham13 May 13, 2022
4eca1dc
Merge remote-tracking branch 'origin/master' into rc-realtime-dev
qdpham13 May 19, 2022
32e980a
Reupdate files.
qdpham13 May 19, 2022
527ae0c
Make changes based on PR comments;
qdpham13 May 19, 2022
a6f69c2
Remove synchronized set and add explicit synchonized to methods.
qdpham13 May 19, 2022
dd9d76f
Add http connection logic
qdpham13 May 19, 2022
9ae0be3
Add autofetch logic
qdpham13 May 19, 2022
4ba117e
Add http stream connection and async autofetching. Also add template …
qdpham13 May 20, 2022
a22e5d3
Merge remote-tracking branch 'origin/master' into rc-realtime-dev
qdpham13 May 20, 2022
ca5ce12
Merge remote-tracking branch 'origin/master' into rc-realtime-dev-2
qdpham13 May 20, 2022
8334dfb
Add message decoding
qdpham13 May 23, 2022
0bd4c2b
Merge remote-tracking branch 'origin/master' into rc-realtime-dev
qdpham13 May 23, 2022
2bdb734
Merge remote-tracking branch 'origin/master' into rc-realtime-dev-2
qdpham13 May 23, 2022
07488b4
add unit tests and Realtime based exceptions
qdpham13 May 23, 2022
2f9a558
Merge branch 'rc-realtime-dev-2' into rc-realtime-dev
qdpham13 May 23, 2022
8854eec
Merge branch 'realtime-rc-merge' into rc-realtime-dev
qdpham13 May 23, 2022
59c6ac8
Update api.txt
qdpham13 May 24, 2022
d634bc8
Fix check failures
qdpham13 May 24, 2022
bbb0d68
Format files
qdpham13 May 24, 2022
feed939
Update changes based on PR comments
qdpham13 May 24, 2022
7d8e4a8
Add back template version number check
qdpham13 May 24, 2022
ba2867f
Alter template version number comment to reflect changes
qdpham13 May 25, 2022
d9bc0cd
Revert template version number extraction
qdpham13 May 25, 2022
f8fdb8e
Update template version number comment
qdpham13 May 25, 2022
b4290aa
Add thread safe for autofetch listeners
qdpham13 May 25, 2022
df35b7a
Add thread safe for autofetch listeners
qdpham13 May 25, 2022
7522ddc
add stream unit tests and thread safe for http connection object
qdpham13 May 25, 2022
622db09
finish autofetch testing
qdpham13 May 26, 2022
7cb6e69
Merge branch 'master' into rc-realtime-dev
qdpham13 May 26, 2022
5b230d7
Merge branch 'realtime-rc-merge' into rc-realtime-dev
qdpham13 May 26, 2022
a20e5ef
Post release version updates (#3752)
vkryachko May 26, 2022
0f583a6
Ignore appdistro-api since it's still in beta. (#3751)
vkryachko May 26, 2022
33ed2b2
Merge branch 'master' into rc-realtime-dev
qdpham13 May 27, 2022
41d69da
Merge branch 'realtime-rc-merge' into rc-realtime-dev
qdpham13 May 27, 2022
8d4335f
Upgrade Gradle to 6.9 (#3744)
daymxn May 30, 2022
dd2c9d5
Upgrade kotlin version to 1.6.20 (#3762)
vkryachko May 30, 2022
e1ec000
Merge branch 'master' into rc-realtime-dev
qdpham13 May 31, 2022
3865f23
Remove extra log
qdpham13 May 31, 2022
4236508
Merge branch 'realtime-rc-merge' into rc-realtime-dev
qdpham13 May 31, 2022
f01c894
Merge master
qdpham13 Jun 2, 2022
3d8519a
Merge master
qdpham13 Jun 3, 2022
cdbd833
Merge branch 'master' into rc-realtime-dev
qdpham13 Jun 6, 2022
cd03a2e
Merge branch 'realtime-rc-merge' into rc-realtime-dev
qdpham13 Jun 6, 2022
7d5f792
Merge branch 'realtime-rc-merge' into rc-realtime-dev
qdpham13 Jun 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion firebase-config/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
package com.google.firebase.remoteconfig {

public interface ConfigUpdateListener {
method public void onError(Exception);
method public void onError(@NonNull Exception);
method public void onEvent();
}

Expand Down Expand Up @@ -65,6 +65,16 @@ package com.google.firebase.remoteconfig {
method public int getLastFetchStatus();
}

public class FirebaseRemoteConfigRealtimeUpdateFetchException extends com.google.firebase.remoteconfig.FirebaseRemoteConfigException {
ctor public FirebaseRemoteConfigRealtimeUpdateFetchException(@NonNull String);
ctor public FirebaseRemoteConfigRealtimeUpdateFetchException(@NonNull String, @Nullable Throwable);
}

public class FirebaseRemoteConfigRealtimeUpdateStreamException extends com.google.firebase.remoteconfig.FirebaseRemoteConfigException {
Comment on lines +69 to +73
Copy link
Member

Choose a reason for hiding this comment

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

What is the semantic difference between these 2 exceptions? Do we expect devs to understand this difference or is it part of the implementation detail?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The difference is that the fetch exception is meant to indicate to the user that we got a new config update signal but we were unable to retrieve the latest config version that we were expecting. And the second one indicated that the stream was unable to open. We'll let users know about these exceptions so that they can expect them when writing their onError callback. We'll include these exceptions as part of our docs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Just for reference, do you have an example of different actions that would make sense to take depending on which exception is thrown?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the stream exception some things they could do is remove their listener, fetch again, or add a new listener to trigger another round of http stream retries.
And for the fetch exception users could try and fetch again or activate their default configs.

Copy link
Member

Choose a reason for hiding this comment

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

For the stream exception some things they could do is remove their listener, fetch again, or add a new listener to trigger another round of http stream retries

This sounds like it should be done internally by the SDK as opposed to having devs do it manually, wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And yeah, with the next PR that includes the retry methods we would do this for them already. This exception would be more of an indication to the user that Realtime won't be able to work at all due to the stream issue. This may enable them to revert to base RC as a fallback or some other mitigation where they are not reliant on realtime.

Copy link
Member

Choose a reason for hiding this comment

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

Realtime won't be able to work at all due to the stream issue

What's still unclear to me is why would we expose this as an exception to the developer as opposed to retrying internally until we succeed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mainly so user's can fall back onto something else and know when to not rely on Realtime. When we retry we're only going to have limited attempts b/c we don't want to use up the devices bandwidth/battery making an unlimited amount of retries. Especially in scenarios when our endpoint is down or the device has no network connection and so it won't successfully connect for a while. Do you think we should just fail quietly and not pass anything back if we can't establish a stream?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated. PTAL @vkryachko

ctor public FirebaseRemoteConfigRealtimeUpdateStreamException(@NonNull String);
ctor public FirebaseRemoteConfigRealtimeUpdateStreamException(@NonNull String, @Nullable Throwable);
}

public class FirebaseRemoteConfigServerException extends com.google.firebase.remoteconfig.FirebaseRemoteConfigException {
ctor public FirebaseRemoteConfigServerException(int, @NonNull String);
ctor public FirebaseRemoteConfigServerException(int, @NonNull String, @Nullable Throwable);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.remoteconfig;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
* A Firebase Remote Config exception caused by the auto-fetch component of Realtime config updates.
*
* @author Quan Pham
*/
public class FirebaseRemoteConfigRealtimeUpdateFetchException
extends FirebaseRemoteConfigException {
/** Creates a Firebase Remote Config Realtime fetch exception with the given message. */
public FirebaseRemoteConfigRealtimeUpdateFetchException(@NonNull String detailMessage) {
super(detailMessage);
}

/** Creates a Firebase Remote Config Realtime fetch exception with the given message and cause. */
public FirebaseRemoteConfigRealtimeUpdateFetchException(
@NonNull String detailMessage, @Nullable Throwable cause) {
super(detailMessage, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.remoteconfig;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
* A Firebase Remote Config exception caused by the stream component of Realtime config updates.
*
* @author Quan Pham
*/
public class FirebaseRemoteConfigRealtimeUpdateStreamException
extends FirebaseRemoteConfigException {
/** Creates a Firebase Remote Config Realtime stream exception with the given message. */
public FirebaseRemoteConfigRealtimeUpdateStreamException(@NonNull String detailMessage) {
super(detailMessage);
}

/**
* Creates a Firebase Remote Config Realtime stream exception with the given message and cause.
*/
public FirebaseRemoteConfigRealtimeUpdateStreamException(
@NonNull String detailMessage, @Nullable Throwable cause) {
super(detailMessage, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
public final class RemoteConfigConstants {
public static final String FETCH_REGEX_URL =
"https://firebaseremoteconfig.googleapis.com/v1/projects/%s/namespaces/%s:fetch";
public static final String REALTIME_REGEX_URL =
"https://firebaseremoteconfig.googleapis.com/v1/projects/%s/namespaces/%s:streamFetchInvalidations";

/**
* Keys of fields in the Fetch request body that the client sends to the Firebase Remote Config
Expand Down Expand Up @@ -73,14 +75,16 @@ public final class RemoteConfigConstants {
ResponseFieldKey.ENTRIES,
ResponseFieldKey.EXPERIMENT_DESCRIPTIONS,
ResponseFieldKey.PERSONALIZATION_METADATA,
ResponseFieldKey.STATE
ResponseFieldKey.STATE,
ResponseFieldKey.TEMPLATE_VERSION_NUMBER
})
@Retention(RetentionPolicy.SOURCE)
public @interface ResponseFieldKey {
String ENTRIES = "entries";
String EXPERIMENT_DESCRIPTIONS = "experimentDescriptions";
String PERSONALIZATION_METADATA = "personalizationMetadata";
String STATE = "state";
String TEMPLATE_VERSION_NUMBER = "templateVersion";
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.remoteconfig.internal;

import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.TAG;

import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.VisibleForTesting;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.remoteconfig.ConfigUpdateListener;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigRealtimeUpdateFetchException;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigRealtimeUpdateStreamException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.json.JSONException;
import org.json.JSONObject;

public class ConfigAutoFetch {

private static final int FETCH_RETRY = 3;

@GuardedBy("this")
private final Set<ConfigUpdateListener> eventListeners;
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be thread-safe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated. And yeah, with the next PR that includes the retry methods we would do this for them already. This exception would be more of an indication to the user that Realtime won't be able to work at all due to the stream issue. This may enable them to revert to base RC as a fallback or some other mitigation where they are not reliant on realtime.


@GuardedBy("this")
private final HttpURLConnection httpURLConnection;

private final ConfigFetchHandler configFetchHandler;
private final ConfigUpdateListener retryCallback;
private final ScheduledExecutorService scheduledExecutorService;
private final Random random;
private final Executor executor;

public ConfigAutoFetch(
HttpURLConnection httpURLConnection,
ConfigFetchHandler configFetchHandler,
Set<ConfigUpdateListener> eventListeners,
ConfigUpdateListener retryCallback,
Executor executor,
ScheduledExecutorService scheduledExecutorService) {
this.httpURLConnection = httpURLConnection;
this.configFetchHandler = configFetchHandler;
this.eventListeners = eventListeners;
this.retryCallback = retryCallback;
this.scheduledExecutorService = scheduledExecutorService;
this.random = new Random();
this.executor = executor;
}

public void beginAutoFetch() {
executor.execute(
new Runnable() {
@Override
public void run() {
listenForNotifications();
}
});
}

private synchronized void propagateErrors(FirebaseRemoteConfigException exception) {
for (ConfigUpdateListener listener : eventListeners) {
listener.onError(exception);
}
}

private synchronized void executeAllListenerCallbacks() {
for (ConfigUpdateListener listener : eventListeners) {
listener.onEvent();
}
}

// Check connection and establish InputStream
@VisibleForTesting
public synchronized void listenForNotifications() {
if (httpURLConnection != null) {
try {
int responseCode = httpURLConnection.getResponseCode();
if (responseCode == 200) {
InputStream inputStream = httpURLConnection.getInputStream();
handleNotifications(inputStream);
inputStream.close();
} else {
propagateErrors(
new FirebaseRemoteConfigRealtimeUpdateStreamException(
"Http connection responded with error: " + responseCode));
}
} catch (IOException ex) {
propagateErrors(
new FirebaseRemoteConfigRealtimeUpdateFetchException(
"Error handling stream messages while fetching.", ex.getCause()));
}
}
retryCallback.onEvent();
}

// Auto-fetch new config and execute callbacks on each new message
private void handleNotifications(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader((new InputStreamReader(inputStream, "utf-8")));
String message;
while ((message = reader.readLine()) != null) {
long targetTemplateVersion = configFetchHandler.getTemplateVersionNumber();
try {
JSONObject jsonObject = new JSONObject(message);
if (jsonObject.has("latestTemplateVersionNumber")) {
targetTemplateVersion = jsonObject.getLong("latestTemplateVersionNumber");
}
} catch (JSONException ex) {
Log.i(TAG, "Unable to parse latest config update message.");
}

autoFetch(FETCH_RETRY, targetTemplateVersion);
}
reader.close();
}

private void autoFetch(int remainingAttempts, long targetVersion) {
if (remainingAttempts == 0) {
propagateErrors(
new FirebaseRemoteConfigRealtimeUpdateFetchException("Unable to fetch latest version."));
return;
}

// Needs fetch to occur between 2 - 12 seconds. Randomize to not cause ddos alerts in backend
int timeTillFetch = random.nextInt(6) + 1;
scheduledExecutorService.schedule(
new Runnable() {
@Override
public void run() {
fetchLatestConfig(remainingAttempts, targetVersion);
}
},
timeTillFetch,
TimeUnit.SECONDS);
}

@VisibleForTesting
public synchronized void fetchLatestConfig(int remainingAttempts, long targetVersion) {
Task<ConfigFetchHandler.FetchResponse> fetchTask = configFetchHandler.fetch(0L);
fetchTask.onSuccessTask(
(fetchResponse) -> {
long newTemplateVersion = 0;
if (fetchResponse.getFetchedConfigs() != null) {
newTemplateVersion = fetchResponse.getFetchedConfigs().getTemplateVersionNumber();
}

if (newTemplateVersion >= targetVersion) {
executeAllListenerCallbacks();
} else {
Log.i(
TAG,
"Fetched template version is the same as SDK's current version."
+ " Retrying fetch.");
// Continue fetching until template version number if greater then current.
autoFetch(remainingAttempts - 1, targetVersion);
}
return Tasks.forResult(null);
});
}
}
Loading