Skip to content

Commit 2886218

Browse files
Improve Real-time response Handling for Remote Config (#7044)
This change prepares the Firebase SDK for future real-time quota enforcement, addressing potential user starvation. It ensures the SDK can fetch the latest template even when the real-time quota is exceeded. We now include a retryInterval in the response when a connection hits a quota limit. This allows clients to get the latest template immediately and retry the connection after the specified interval. This approach guarantees users always have access to the latest data and can efficiently resume real-time services once their quota is restored.
1 parent acf250d commit 2886218

File tree

5 files changed

+80
-4
lines changed

5 files changed

+80
-4
lines changed

firebase-config/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
2-
2+
* [changed] This update introduces improvements to how the SDK handles real-time requests when a
3+
Firebase project has exceeded its available quota for real-time services. Released in anticipation
4+
of future quota enforcement, this change is designed to fetch the latest template even when the
5+
quota is exhausted.
36

47
# 22.1.2
58
* [fixed] Fixed `NetworkOnMainThreadException` on Android versions below 8 by disconnecting

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import android.util.Log;
2020
import androidx.annotation.GuardedBy;
2121
import androidx.annotation.VisibleForTesting;
22+
import com.google.android.gms.common.util.Clock;
23+
import com.google.android.gms.common.util.DefaultClock;
2224
import com.google.android.gms.tasks.Task;
2325
import com.google.android.gms.tasks.Tasks;
2426
import com.google.firebase.remoteconfig.ConfigUpdate;
@@ -31,6 +33,7 @@
3133
import java.io.InputStream;
3234
import java.io.InputStreamReader;
3335
import java.net.HttpURLConnection;
36+
import java.util.Date;
3437
import java.util.Random;
3538
import java.util.Set;
3639
import java.util.concurrent.ScheduledExecutorService;
@@ -43,6 +46,7 @@ public class ConfigAutoFetch {
4346
private static final int MAXIMUM_FETCH_ATTEMPTS = 3;
4447
private static final String TEMPLATE_VERSION_KEY = "latestTemplateVersionNumber";
4548
private static final String REALTIME_DISABLED_KEY = "featureDisabled";
49+
private static final String REALTIME_RETRY_INTERVAL = "retryIntervalSeconds";
4650

4751
@GuardedBy("this")
4852
private final Set<ConfigUpdateListener> eventListeners;
@@ -54,6 +58,8 @@ public class ConfigAutoFetch {
5458
private final ConfigUpdateListener retryCallback;
5559
private final ScheduledExecutorService scheduledExecutorService;
5660
private final Random random;
61+
private final Clock clock;
62+
private final ConfigSharedPrefsClient sharedPrefsClient;
5763
private boolean isInBackground;
5864

5965
public ConfigAutoFetch(
@@ -62,7 +68,8 @@ public ConfigAutoFetch(
6268
ConfigCacheClient activatedCache,
6369
Set<ConfigUpdateListener> eventListeners,
6470
ConfigUpdateListener retryCallback,
65-
ScheduledExecutorService scheduledExecutorService) {
71+
ScheduledExecutorService scheduledExecutorService,
72+
ConfigSharedPrefsClient sharedPrefsClient) {
6673
this.httpURLConnection = httpURLConnection;
6774
this.configFetchHandler = configFetchHandler;
6875
this.activatedCache = activatedCache;
@@ -71,6 +78,19 @@ public ConfigAutoFetch(
7178
this.scheduledExecutorService = scheduledExecutorService;
7279
this.random = new Random();
7380
this.isInBackground = false;
81+
this.sharedPrefsClient = sharedPrefsClient;
82+
this.clock = DefaultClock.getInstance();
83+
}
84+
85+
// Increase the backoff duration with a new end time based on Retry Interval
86+
private synchronized void updateBackoffMetadataWithRetryInterval(
87+
int realtimeRetryIntervalInSeconds) {
88+
Date currentTime = new Date(clock.currentTimeMillis());
89+
long backoffDurationInMillis = realtimeRetryIntervalInSeconds * 1000L;
90+
Date backoffEndTime = new Date(currentTime.getTime() + backoffDurationInMillis);
91+
92+
// Persist the new values to disk-backed metadata.
93+
sharedPrefsClient.setRealtimeBackoffEndTime(backoffEndTime);
7494
}
7595

7696
private synchronized void propagateErrors(FirebaseRemoteConfigException exception) {
@@ -190,6 +210,15 @@ private void handleNotifications(InputStream inputStream) throws IOException {
190210
autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion);
191211
}
192212
}
213+
214+
// This field in the response indicates that the realtime request should retry after the
215+
// specified interval to establish a long-lived connection. This interval extends the
216+
// backoff duration without affecting the number of retries, so it will not enter an
217+
// exponential backoff state.
218+
if (jsonObject.has(REALTIME_RETRY_INTERVAL)) {
219+
int realtimeRetryIntervalInSeconds = jsonObject.getInt(REALTIME_RETRY_INTERVAL);
220+
updateBackoffMetadataWithRetryInterval(realtimeRetryIntervalInSeconds);
221+
}
193222
} catch (JSONException ex) {
194223
// Message was mangled up and so it was unable to be parsed. User is notified of this
195224
// because it there could be a new configuration that needs to be fetched.

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,8 @@ public void onError(@NonNull FirebaseRemoteConfigException error) {
469469
activatedCache,
470470
listeners,
471471
retryCallback,
472-
scheduledExecutorService);
472+
scheduledExecutorService,
473+
sharedPrefsClient);
473474
}
474475

475476
// HTTP status code that the Realtime client should retry on.

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,16 @@ void setRealtimeBackoffMetadata(int numFailedStreams, Date backoffEndTime) {
394394
}
395395
}
396396

397+
@VisibleForTesting
398+
public void setRealtimeBackoffEndTime(Date backoffEndTime) {
399+
synchronized (realtimeBackoffMetadataLock) {
400+
frcSharedPrefs
401+
.edit()
402+
.putLong(REALTIME_BACKOFF_END_TIME_IN_MILLIS_KEY, backoffEndTime.getTime())
403+
.apply();
404+
}
405+
}
406+
397407
void resetRealtimeBackoff() {
398408
setRealtimeBackoffMetadata(NO_FAILED_REALTIME_STREAMS, NO_BACKOFF_TIME);
399409
}

firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
import android.os.Bundle;
5151
import androidx.annotation.NonNull;
5252
import androidx.test.core.app.ApplicationProvider;
53+
import com.google.android.gms.common.util.Clock;
54+
import com.google.android.gms.common.util.DefaultClock;
5355
import com.google.android.gms.shadows.common.internal.ShadowPreconditions;
5456
import com.google.android.gms.tasks.Task;
5557
import com.google.android.gms.tasks.TaskCompletionSource;
@@ -104,6 +106,7 @@
104106
import org.junit.Test;
105107
import org.junit.runner.RunWith;
106108
import org.mockito.ArgumentCaptor;
109+
import org.mockito.ArgumentMatcher;
107110
import org.mockito.Mock;
108111
import org.mockito.MockitoAnnotations;
109112
import org.robolectric.RobolectricTestRunner;
@@ -200,6 +203,7 @@ public final class FirebaseRemoteConfigTest {
200203

201204
private final ScheduledExecutorService scheduledExecutorService =
202205
Executors.newSingleThreadScheduledExecutor();
206+
private final Clock clock = DefaultClock.getInstance();
203207

204208
@Before
205209
public void setUp() throws Exception {
@@ -351,7 +355,8 @@ public void onError(@NonNull FirebaseRemoteConfigException error) {
351355
mockActivatedCache,
352356
listeners,
353357
mockRetryListener,
354-
scheduledExecutorService);
358+
scheduledExecutorService,
359+
sharedPrefsClient);
355360
configAutoFetch.setIsInBackground(false);
356361
realtimeSharedPrefsClient =
357362
new ConfigSharedPrefsClient(
@@ -1551,6 +1556,34 @@ public void realtimeStreamListen_andUnableToParseMessage() throws Exception {
15511556
verify(mockInvalidMessageEventListener).onError(any(FirebaseRemoteConfigClientException.class));
15521557
}
15531558

1559+
@Test
1560+
public void realtime_updatesBackoffMetadataWithProvidedRetryInterval() throws Exception {
1561+
ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient);
1562+
when(mockHttpURLConnection.getResponseCode()).thenReturn(200);
1563+
int expectedRetryIntervalInSeconds = 240;
1564+
when(mockHttpURLConnection.getInputStream())
1565+
.thenReturn(
1566+
new ByteArrayInputStream(
1567+
String.format(
1568+
"{ \"latestTemplateVersionNumber\": 1, \"retryIntervalSeconds\": %d }",
1569+
expectedRetryIntervalInSeconds)
1570+
.getBytes(StandardCharsets.UTF_8)));
1571+
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
1572+
configAutoFetch.listenForNotifications();
1573+
1574+
ArgumentMatcher<Date> backoffEndTimeWithinTolerance =
1575+
argument -> {
1576+
Date currentTime = new Date(clock.currentTimeMillis());
1577+
long backoffDurationInMillis = expectedRetryIntervalInSeconds * 1000L;
1578+
Date expectedBackoffEndTime = new Date(currentTime.getTime() + backoffDurationInMillis);
1579+
return Math.abs(argument.getTime() - expectedBackoffEndTime.getTime())
1580+
<= TimeUnit.SECONDS.toSeconds(1);
1581+
};
1582+
1583+
verify(sharedPrefsClient, times(1))
1584+
.setRealtimeBackoffEndTime(argThat(backoffEndTimeWithinTolerance));
1585+
}
1586+
15541587
@Test
15551588
public void realtime_stream_listen_get_inputstream_fail() throws Exception {
15561589
InputStream inputStream = mock(InputStream.class);

0 commit comments

Comments
 (0)