Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
5 changes: 4 additions & 1 deletion firebase-config/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased

* [changed] This update introduces improvements to how the SDK handles real-time requests when a
Firebase project has exceeded its available quota for real-time services. Released in anticipation
of future quota enforcement, this change is designed to fetch the latest template even when the
quota is exhausted.

# 22.1.2
* [fixed] Fixed `NetworkOnMainThreadException` on Android versions below 8 by disconnecting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.VisibleForTesting;
import com.google.android.gms.common.util.Clock;
import com.google.android.gms.common.util.DefaultClock;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.remoteconfig.ConfigUpdate;
Expand All @@ -31,6 +33,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.util.Date;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
Expand All @@ -43,6 +46,7 @@ public class ConfigAutoFetch {
private static final int MAXIMUM_FETCH_ATTEMPTS = 3;
private static final String TEMPLATE_VERSION_KEY = "latestTemplateVersionNumber";
private static final String REALTIME_DISABLED_KEY = "featureDisabled";
private static final String REALTIME_RETRY_INTERVAL = "retryIntervalSeconds";

@GuardedBy("this")
private final Set<ConfigUpdateListener> eventListeners;
Expand All @@ -54,6 +58,7 @@ public class ConfigAutoFetch {
private final ConfigUpdateListener retryCallback;
private final ScheduledExecutorService scheduledExecutorService;
private final Random random;
private final Clock clock;
private boolean isInBackground;

public ConfigAutoFetch(
Expand All @@ -71,6 +76,18 @@ public ConfigAutoFetch(
this.scheduledExecutorService = scheduledExecutorService;
this.random = new Random();
this.isInBackground = false;
clock = DefaultClock.getInstance();
}

// Increase the backoff duration with a new end time based on Retry Interval
private synchronized void updateBackoffMetadataWithRetryInterval(
int realtimeRetryInterval, ConfigSharedPrefsClient sharedPrefsClient) {
Date currentTime = new Date(clock.currentTimeMillis());
long backoffDurationInMillis = realtimeRetryInterval * 1000L;
Date backoffEndTime = new Date(currentTime.getTime() + backoffDurationInMillis);

// Persist the new values to disk-backed metadata.
sharedPrefsClient.setRealtimeBackoffEndTime(backoffEndTime);
}

private synchronized void propagateErrors(FirebaseRemoteConfigException exception) {
Expand Down Expand Up @@ -106,7 +123,7 @@ private String parseAndValidateConfigUpdateMessage(String message) {

// Check connection and establish InputStream
@VisibleForTesting
public void listenForNotifications() {
public void listenForNotifications(ConfigSharedPrefsClient sharedPrefsClient) {
if (httpURLConnection == null) {
return;
}
Expand All @@ -116,7 +133,7 @@ public void listenForNotifications() {
InputStream inputStream = null;
try {
inputStream = httpURLConnection.getInputStream();
handleNotifications(inputStream);
handleNotifications(inputStream, sharedPrefsClient);
} catch (IOException ex) {
// If the real-time connection is at an unexpected lifecycle state when the app is
// backgrounded, it's expected closing the httpURLConnection will throw an exception.
Expand All @@ -138,7 +155,8 @@ public void listenForNotifications() {
}

// Auto-fetch new config and execute callbacks on each new message
private void handleNotifications(InputStream inputStream) throws IOException {
private void handleNotifications(
InputStream inputStream, ConfigSharedPrefsClient sharedPrefsClient) throws IOException {
BufferedReader reader = new BufferedReader((new InputStreamReader(inputStream, "utf-8")));
String partialConfigUpdateMessage;
String currentConfigUpdateMessage = "";
Expand Down Expand Up @@ -190,6 +208,11 @@ private void handleNotifications(InputStream inputStream) throws IOException {
autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion);
}
}

if (jsonObject.has(REALTIME_RETRY_INTERVAL)) {
int realtimeRetryInterval = jsonObject.getInt(REALTIME_RETRY_INTERVAL);
updateBackoffMetadataWithRetryInterval(realtimeRetryInterval, sharedPrefsClient);
}
} catch (JSONException ex) {
// Message was mangled up and so it was unable to be parsed. User is notified of this
// because it there could be a new configuration that needs to be fetched.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ public void beginRealtimeHttpStream() {

// Start listening for realtime notifications.
configAutoFetch = startAutoFetch(httpURLConnection);
configAutoFetch.listenForNotifications();
configAutoFetch.listenForNotifications(sharedPrefsClient);
}
} catch (IOException e) {
if (isInBackground) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,16 @@ void setRealtimeBackoffMetadata(int numFailedStreams, Date backoffEndTime) {
}
}

@VisibleForTesting
public void setRealtimeBackoffEndTime(Date backoffEndTime) {
synchronized (realtimeBackoffMetadataLock) {
frcSharedPrefs
.edit()
.putLong(REALTIME_BACKOFF_END_TIME_IN_MILLIS_KEY, backoffEndTime.getTime())
.apply();
}
}

void resetRealtimeBackoff() {
setRealtimeBackoffMetadata(NO_FAILED_REALTIME_STREAMS, NO_BACKOFF_TIME);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1286,7 +1286,7 @@ public void realtime_stream_listen_and_end_connection() throws Exception {
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, 1))
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.listenForNotifications();
configAutoFetch.listenForNotifications(sharedPrefsClient);

verify(inputStreamSpy, times(2)).close();
}
Expand All @@ -1299,7 +1299,7 @@ public void realtime_fetchesWithoutChangedParams_doesNotCallOnUpdate() throws Ex
"{ \"latestTemplateVersionNumber\": 1 }".getBytes(StandardCharsets.UTF_8)));
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(firstFetchedContainerResponse));
configAutoFetch.listenForNotifications();
configAutoFetch.listenForNotifications(sharedPrefsClient);

verifyNoInteractions(mockOnUpdateListener);
}
Expand Down Expand Up @@ -1339,7 +1339,7 @@ public void realtime_okStatusCode_startAutofetchAndRetries() throws Exception {
configRealtimeHttpClientSpy.beginRealtimeHttpStream();
flushScheduledTasks();

verify(mockConfigAutoFetch).listenForNotifications();
verify(mockConfigAutoFetch).listenForNotifications(any());
verify(configRealtimeHttpClientSpy).retryHttpConnectionWhenBackoffEnds();
}

Expand Down Expand Up @@ -1490,7 +1490,7 @@ public void realtime_stream_listen_and_failsafe_enabled() throws Exception {
new ByteArrayInputStream(
"{ \"featureDisabled\": true }".getBytes(StandardCharsets.UTF_8)));
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
configAutoFetch.listenForNotifications();
configAutoFetch.listenForNotifications(sharedPrefsClient);

verify(mockRetryListener).onError(any(FirebaseRemoteConfigServerException.class));
verify(mockFetchHandler, never()).fetch(0);
Expand All @@ -1508,7 +1508,7 @@ public void realtime_stream_listen_and_failsafe_disabled() throws Exception {
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, 1))
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.listenForNotifications();
configAutoFetch.listenForNotifications(sharedPrefsClient);

verify(mockUnavailableEventListener, never())
.onError(any(FirebaseRemoteConfigServerException.class));
Expand Down Expand Up @@ -1546,11 +1546,29 @@ public void realtimeStreamListen_andUnableToParseMessage() throws Exception {
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, 1))
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.listenForNotifications();
configAutoFetch.listenForNotifications(sharedPrefsClient);

verify(mockInvalidMessageEventListener).onError(any(FirebaseRemoteConfigClientException.class));
}

@Test
public void realtime_updatesBackoffMetadataWithProvidedRetryInterval() throws Exception {
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
when(mockHttpURLConnection.getResponseCode()).thenReturn(200);
int expectedRetryInterval = 240;
when(mockHttpURLConnection.getInputStream())
.thenReturn(
new ByteArrayInputStream(
String.format(
"{ \"latestTemplateVersionNumber\": 1, \"retryIntervalSeconds\": %d }",
expectedRetryInterval)
.getBytes(StandardCharsets.UTF_8)));
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
configAutoFetch.listenForNotifications(sharedPrefsClient);

verify(sharedPrefsClient, times(1)).setRealtimeBackoffEndTime(any());
}

@Test
public void realtime_stream_listen_get_inputstream_fail() throws Exception {
InputStream inputStream = mock(InputStream.class);
Expand All @@ -1561,7 +1579,7 @@ public void realtime_stream_listen_get_inputstream_fail() throws Exception {
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, 1))
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.listenForNotifications();
configAutoFetch.listenForNotifications(sharedPrefsClient);

verify(inputStream).close();
}
Expand All @@ -1571,7 +1589,7 @@ public void realtime_stream_listen_get_inputstream_exception_handling() throws E
InputStream inputStream = mock(InputStream.class);
when(mockHttpURLConnection.getResponseCode()).thenReturn(200);
when(mockHttpURLConnection.getInputStream()).thenThrow(IOException.class);
configAutoFetch.listenForNotifications();
configAutoFetch.listenForNotifications(sharedPrefsClient);

verify(mockHttpURLConnection, times(1)).getInputStream();
verify(inputStream, never()).close();
Expand Down
Loading