diff --git a/firebase-config/CHANGELOG.md b/firebase-config/CHANGELOG.md index fc00b486a87..743dd399729 100644 --- a/firebase-config/CHANGELOG.md +++ b/firebase-config/CHANGELOG.md @@ -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 diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java index a93b1dc5784..6607a822011 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java @@ -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; @@ -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; @@ -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 eventListeners; @@ -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( @@ -62,7 +68,8 @@ public ConfigAutoFetch( ConfigCacheClient activatedCache, Set eventListeners, ConfigUpdateListener retryCallback, - ScheduledExecutorService scheduledExecutorService) { + ScheduledExecutorService scheduledExecutorService, + ConfigSharedPrefsClient sharedPrefsClient) { this.httpURLConnection = httpURLConnection; this.configFetchHandler = configFetchHandler; this.activatedCache = activatedCache; @@ -71,6 +78,19 @@ public ConfigAutoFetch( this.scheduledExecutorService = scheduledExecutorService; this.random = new Random(); this.isInBackground = false; + this.sharedPrefsClient = sharedPrefsClient; + this.clock = DefaultClock.getInstance(); + } + + // Increase the backoff duration with a new end time based on Retry Interval + private synchronized void updateBackoffMetadataWithRetryInterval( + int realtimeRetryIntervalInSeconds) { + Date currentTime = new Date(clock.currentTimeMillis()); + long backoffDurationInMillis = realtimeRetryIntervalInSeconds * 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) { @@ -190,6 +210,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 realtimeRetryIntervalInSeconds = jsonObject.getInt(REALTIME_RETRY_INTERVAL); + updateBackoffMetadataWithRetryInterval(realtimeRetryIntervalInSeconds); + } } 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. diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java index 7be3ef97136..552508dda16 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java @@ -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. diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java index 7ce24bc44f6..b19431fa5a1 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java @@ -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); } diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index 9e8f65c767e..076dafff6fe 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -50,6 +50,8 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.common.util.Clock; +import com.google.android.gms.common.util.DefaultClock; import com.google.android.gms.shadows.common.internal.ShadowPreconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; @@ -104,6 +106,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -200,6 +203,7 @@ public final class FirebaseRemoteConfigTest { private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + private final Clock clock = DefaultClock.getInstance(); @Before public void setUp() throws Exception { @@ -351,7 +355,8 @@ public void onError(@NonNull FirebaseRemoteConfigException error) { mockActivatedCache, listeners, mockRetryListener, - scheduledExecutorService); + scheduledExecutorService, + sharedPrefsClient); configAutoFetch.setIsInBackground(false); realtimeSharedPrefsClient = new ConfigSharedPrefsClient( @@ -1551,6 +1556,34 @@ 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 expectedRetryIntervalInSeconds = 240; + when(mockHttpURLConnection.getInputStream()) + .thenReturn( + new ByteArrayInputStream( + String.format( + "{ \"latestTemplateVersionNumber\": 1, \"retryIntervalSeconds\": %d }", + expectedRetryIntervalInSeconds) + .getBytes(StandardCharsets.UTF_8))); + when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L); + configAutoFetch.listenForNotifications(); + + ArgumentMatcher backoffEndTimeWithinTolerance = + argument -> { + Date currentTime = new Date(clock.currentTimeMillis()); + long backoffDurationInMillis = expectedRetryIntervalInSeconds * 1000L; + Date expectedBackoffEndTime = new Date(currentTime.getTime() + backoffDurationInMillis); + return Math.abs(argument.getTime() - expectedBackoffEndTime.getTime()) + <= TimeUnit.SECONDS.toSeconds(1); + }; + + verify(sharedPrefsClient, times(1)) + .setRealtimeBackoffEndTime(argThat(backoffEndTimeWithinTolerance)); + } + @Test public void realtime_stream_listen_get_inputstream_fail() throws Exception { InputStream inputStream = mock(InputStream.class);