Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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,8 @@ public class ConfigAutoFetch {
private final ConfigUpdateListener retryCallback;
private final ScheduledExecutorService scheduledExecutorService;
private final Random random;
private final Clock clock;
private final ConfigSharedPrefsClient sharedPrefsClient;
private boolean isInBackground;

public ConfigAutoFetch(
Expand All @@ -62,7 +68,8 @@ public ConfigAutoFetch(
ConfigCacheClient activatedCache,
Set<ConfigUpdateListener> eventListeners,
ConfigUpdateListener retryCallback,
ScheduledExecutorService scheduledExecutorService) {
ScheduledExecutorService scheduledExecutorService,
ConfigSharedPrefsClient sharedPrefsClient) {
this.httpURLConnection = httpURLConnection;
this.configFetchHandler = configFetchHandler;
this.activatedCache = activatedCache;
Expand All @@ -71,6 +78,18 @@ public ConfigAutoFetch(
this.scheduledExecutorService = scheduledExecutorService;
this.random = new Random();
this.isInBackground = false;
this.sharedPrefsClient = sharedPrefsClient;
clock = DefaultClock.getInstance();
}

// Increase the backoff duration with a new end time based on Retry Interval
private synchronized void updateBackoffMetadataWithRetryInterval(int realtimeRetryInterval) {
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 @@ -190,6 +209,15 @@ private void handleNotifications(InputStream inputStream) throws IOException {
autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion);
}
}

// This field in the response indicates that the realtime request should retry after the
// specified interval to establish a long-lived connection. This interval extends the
// backoff duration without affecting the number of retries, so it will not enter an
// exponential backoff state.
if (jsonObject.has(REALTIME_RETRY_INTERVAL)) {
int realtimeRetryInterval = jsonObject.getInt(REALTIME_RETRY_INTERVAL);
updateBackoffMetadataWithRetryInterval(realtimeRetryInterval);
}
} 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 @@ -469,7 +469,8 @@ public void onError(@NonNull FirebaseRemoteConfigException error) {
activatedCache,
listeners,
retryCallback,
scheduledExecutorService);
scheduledExecutorService,
sharedPrefsClient);
}

// HTTP status code that the Realtime client should retry on.
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 @@ -351,7 +351,8 @@ public void onError(@NonNull FirebaseRemoteConfigException error) {
mockActivatedCache,
listeners,
mockRetryListener,
scheduledExecutorService);
scheduledExecutorService,
sharedPrefsClient);
configAutoFetch.setIsInBackground(false);
realtimeSharedPrefsClient =
new ConfigSharedPrefsClient(
Expand Down Expand Up @@ -1551,6 +1552,24 @@ public void realtimeStreamListen_andUnableToParseMessage() throws Exception {
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();

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

@Test
public void realtime_stream_listen_get_inputstream_fail() throws Exception {
InputStream inputStream = mock(InputStream.class);
Expand Down
Loading