Skip to content

Commit ba787b8

Browse files
committed
Allow firestore to recover quicker when a network change occurs
Eg going from airplane mode to wifi enabled. Previously, firestore would use an exponential backoff to determine when to attempt a reconnect. That backoff is now reset and the connections are retried immediately upon a network change.
1 parent 58b2928 commit ba787b8

File tree

6 files changed

+303
-3
lines changed

6 files changed

+303
-3
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.remote;
16+
17+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
18+
19+
import android.support.test.InstrumentationRegistry;
20+
import android.support.test.runner.AndroidJUnit4;
21+
import com.google.android.gms.tasks.Task;
22+
import com.google.firebase.database.collection.ImmutableSortedSet;
23+
import com.google.firebase.firestore.auth.EmptyCredentialsProvider;
24+
import com.google.firebase.firestore.core.OnlineState;
25+
import com.google.firebase.firestore.model.DocumentKey;
26+
import com.google.firebase.firestore.model.mutation.MutationBatchResult;
27+
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
28+
import com.google.firebase.firestore.util.AsyncQueue;
29+
import io.grpc.Status;
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
import java.util.concurrent.Semaphore;
33+
import org.junit.Test;
34+
import org.junit.runner.RunWith;
35+
36+
class MockCredentialsProvider extends EmptyCredentialsProvider {
37+
@Override
38+
public Task<String> getToken() {
39+
states.add("getToken");
40+
return super.getToken();
41+
}
42+
43+
@Override
44+
public void invalidateToken() {
45+
states.add("invalidateToken");
46+
super.invalidateToken();
47+
}
48+
49+
public List<String> observedStates() {
50+
return states;
51+
}
52+
53+
private final List<String> states = new ArrayList<>();
54+
}
55+
56+
@RunWith(AndroidJUnit4.class)
57+
public class RemoteStoreTest {
58+
@Test
59+
public void testRemoteStoreStreamStopsWhenNetworkUnreachable() {
60+
AsyncQueue testQueue = new AsyncQueue();
61+
MockCredentialsProvider mockCredentialsProvider = new MockCredentialsProvider();
62+
Datastore datastore =
63+
new Datastore(
64+
IntegrationTestUtil.testEnvDatabaseInfo(),
65+
testQueue,
66+
mockCredentialsProvider,
67+
InstrumentationRegistry.getContext());
68+
Semaphore networkChangeSemaphore = new Semaphore(0);
69+
RemoteStore.RemoteStoreCallback callbacks =
70+
new RemoteStore.RemoteStoreCallback() {
71+
public void handleRemoteEvent(RemoteEvent remoteEvent) {}
72+
73+
public void handleRejectedListen(int targetId, Status error) {}
74+
75+
public void handleSuccessfulWrite(MutationBatchResult successfulWrite) {}
76+
77+
public void handleRejectedWrite(int batchId, Status error) {}
78+
79+
public void handleOnlineStateChange(OnlineState onlineState) {
80+
networkChangeSemaphore.release();
81+
}
82+
83+
public ImmutableSortedSet<DocumentKey> getRemoteKeysForTarget(int targetId) {
84+
return null;
85+
}
86+
};
87+
88+
FakeNetworkReachabilityMonitor nrm = new FakeNetworkReachabilityMonitor();
89+
RemoteStore remoteStore = new RemoteStore(callbacks, null, datastore, testQueue, nrm);
90+
waitForIdle(testQueue);
91+
92+
nrm.goOffline();
93+
waitFor(networkChangeSemaphore);
94+
}
95+
96+
private void waitForIdle(AsyncQueue testQueue) {
97+
waitFor(testQueue.enqueue(() -> {}));
98+
}
99+
100+
class FakeNetworkReachabilityMonitor implements NetworkReachabilityMonitor {
101+
private NetworkReachabilityCallback callback = null;
102+
103+
public void onNetworkReachabilityChange(NetworkReachabilityCallback callback) {
104+
this.callback = callback;
105+
}
106+
107+
public void goOffline() {
108+
if (callback != null) {
109+
callback.onChange(NetworkReachabilityMonitor.Reachability.UNREACHABLE);
110+
}
111+
}
112+
113+
public void goOnline() {
114+
if (callback != null) {
115+
callback.onChange(NetworkReachabilityMonitor.Reachability.REACHABLE);
116+
}
117+
}
118+
}
119+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@
4444
import com.google.firebase.firestore.model.NoDocument;
4545
import com.google.firebase.firestore.model.mutation.Mutation;
4646
import com.google.firebase.firestore.model.mutation.MutationBatchResult;
47+
import com.google.firebase.firestore.remote.AndroidNetworkReachabilityMonitor;
4748
import com.google.firebase.firestore.remote.Datastore;
49+
import com.google.firebase.firestore.remote.NetworkReachabilityMonitor;
4850
import com.google.firebase.firestore.remote.RemoteEvent;
4951
import com.google.firebase.firestore.remote.RemoteSerializer;
5052
import com.google.firebase.firestore.remote.RemoteStore;
@@ -241,7 +243,10 @@ private void initialize(Context context, User user, boolean usePersistence, long
241243
}
242244

243245
Datastore datastore = new Datastore(databaseInfo, asyncQueue, credentialsProvider, context);
244-
remoteStore = new RemoteStore(this, localStore, datastore, asyncQueue);
246+
NetworkReachabilityMonitor networkReachabilityMonitor =
247+
new AndroidNetworkReachabilityMonitor(context);
248+
remoteStore =
249+
new RemoteStore(this, localStore, datastore, asyncQueue, networkReachabilityMonitor);
245250

246251
syncEngine = new SyncEngine(localStore, remoteStore, user);
247252
eventManager = new EventManager(syncEngine);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.remote;
16+
17+
import static com.google.firebase.firestore.util.Assert.hardAssert;
18+
19+
import android.annotation.TargetApi;
20+
import android.content.BroadcastReceiver;
21+
import android.content.Context;
22+
import android.content.Intent;
23+
import android.content.IntentFilter;
24+
import android.net.ConnectivityManager;
25+
import android.net.Network;
26+
import android.net.NetworkInfo;
27+
import android.os.Build;
28+
import java.util.ArrayList;
29+
import java.util.List;
30+
import javax.annotation.Nullable;
31+
32+
/**
33+
* Android implementation of NetworkReachabilityMonitor. Parallel implementations exist for N+ and
34+
* pre-N.
35+
*
36+
* <p>Implementation note: Most of the code here was shamelessly stolen from
37+
* https://github.com/grpc/grpc-java/blob/master/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java
38+
*/
39+
public final class AndroidNetworkReachabilityMonitor implements NetworkReachabilityMonitor {
40+
41+
private final Context context;
42+
@Nullable private final ConnectivityManager connectivityManager;
43+
private final List<NetworkReachabilityCallback> callbacks = new ArrayList<>();
44+
45+
public AndroidNetworkReachabilityMonitor(Context context) {
46+
// This notnull restriction could be eliminated... the pre-N method doesn't
47+
// require a Context, and we could use that even on N+ if necessary.
48+
hardAssert(context != null, "Context must be non-null");
49+
this.context = context;
50+
51+
connectivityManager =
52+
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
53+
configureNetworkMonitoring();
54+
}
55+
56+
@Override
57+
public void onNetworkReachabilityChange(NetworkReachabilityCallback callback) {
58+
callbacks.add(callback);
59+
}
60+
61+
private void configureNetworkMonitoring() {
62+
// Android N added the registerDefaultNetworkCallback API to listen to changes in the device's
63+
// default network. For earlier Android API levels, use the BroadcastReceiver API.
64+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
65+
DefaultNetworkCallback defaultNetworkCallback = new DefaultNetworkCallback();
66+
connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
67+
} else {
68+
NetworkReceiver networkReceiver = new NetworkReceiver();
69+
IntentFilter networkIntentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
70+
context.registerReceiver(networkReceiver, networkIntentFilter);
71+
}
72+
73+
// TODO(rsgowman): We should handle unregistering the listener (via
74+
// ConnectivityManager.unregisterNetworkCallback() or Context.unregisterReciver()). But we
75+
// don't support tearing down firestore itself, so it would never be called.
76+
}
77+
78+
/** Respond to changes in the default network. Only used on API levels 24+. */
79+
@TargetApi(Build.VERSION_CODES.N)
80+
private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback {
81+
@Override
82+
public void onAvailable(Network network) {
83+
for (NetworkReachabilityCallback callback : callbacks) {
84+
callback.onChange(Reachability.REACHABLE);
85+
}
86+
}
87+
88+
@Override
89+
public void onLost(Network network) {
90+
for (NetworkReachabilityCallback callback : callbacks) {
91+
callback.onChange(Reachability.UNREACHABLE);
92+
}
93+
}
94+
}
95+
96+
/** Respond to network changes. Only used on API levels < 24. */
97+
private class NetworkReceiver extends BroadcastReceiver {
98+
private boolean isConnected = false;
99+
100+
@Override
101+
public void onReceive(Context context, Intent intent) {
102+
ConnectivityManager conn =
103+
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
104+
NetworkInfo networkInfo = conn.getActiveNetworkInfo();
105+
boolean wasConnected = isConnected;
106+
isConnected = networkInfo != null && networkInfo.isConnected();
107+
if (isConnected && !wasConnected) {
108+
for (NetworkReachabilityCallback callback : callbacks) {
109+
callback.onChange(Reachability.REACHABLE);
110+
}
111+
} else if (!isConnected && wasConnected) {
112+
for (NetworkReachabilityCallback callback : callbacks) {
113+
callback.onChange(Reachability.UNREACHABLE);
114+
}
115+
}
116+
}
117+
}
118+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.remote;
16+
17+
/** Interface for monitoring changes in network connectivity/reachability. */
18+
public interface NetworkReachabilityMonitor {
19+
enum Reachability {
20+
UNREACHABLE,
21+
REACHABLE,
22+
};
23+
24+
interface NetworkReachabilityCallback {
25+
void onChange(Reachability networkStatus);
26+
}
27+
28+
// TODO(rsgowman): Skipping isNetworkReachable() until we need it.
29+
// boolean isNetworkReachable();
30+
31+
void onNetworkReachabilityChange(NetworkReachabilityCallback callback);
32+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.firebase.firestore.model.mutation.MutationBatch;
2929
import com.google.firebase.firestore.model.mutation.MutationBatchResult;
3030
import com.google.firebase.firestore.model.mutation.MutationResult;
31+
import com.google.firebase.firestore.remote.NetworkReachabilityMonitor.Reachability;
3132
import com.google.firebase.firestore.remote.WatchChange.DocumentChange;
3233
import com.google.firebase.firestore.remote.WatchChange.ExistenceFilterWatchChange;
3334
import com.google.firebase.firestore.remote.WatchChange.WatchTargetChange;
@@ -146,7 +147,8 @@ public RemoteStore(
146147
RemoteStoreCallback remoteStoreCallback,
147148
LocalStore localStore,
148149
Datastore datastore,
149-
AsyncQueue workerQueue) {
150+
AsyncQueue workerQueue,
151+
NetworkReachabilityMonitor networkReachabilityMonitor) {
150152
this.remoteStoreCallback = remoteStoreCallback;
151153
this.localStore = localStore;
152154
this.datastore = datastore;
@@ -201,6 +203,26 @@ public void onClose(Status status) {
201203
handleWriteStreamClose(status);
202204
}
203205
});
206+
207+
networkReachabilityMonitor.onNetworkReachabilityChange(
208+
(Reachability reachability) -> {
209+
switch (reachability) {
210+
case REACHABLE:
211+
workerQueue.enqueueAndForget(
212+
() -> {
213+
disableNetwork();
214+
enableNetwork();
215+
});
216+
break;
217+
218+
case UNREACHABLE:
219+
workerQueue.enqueueAndForget(
220+
() -> {
221+
disableNetwork();
222+
});
223+
break;
224+
}
225+
});
204226
}
205227

206228
/** Re-enables the network. Only to be called as the counterpart to disableNetwork(). */

firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@
5656
import com.google.firebase.firestore.model.mutation.Mutation;
5757
import com.google.firebase.firestore.model.mutation.MutationBatchResult;
5858
import com.google.firebase.firestore.model.mutation.MutationResult;
59+
import com.google.firebase.firestore.remote.AndroidNetworkReachabilityMonitor;
5960
import com.google.firebase.firestore.remote.ExistenceFilter;
6061
import com.google.firebase.firestore.remote.MockDatastore;
62+
import com.google.firebase.firestore.remote.NetworkReachabilityMonitor;
6163
import com.google.firebase.firestore.remote.RemoteEvent;
6264
import com.google.firebase.firestore.remote.RemoteStore;
6365
import com.google.firebase.firestore.remote.RemoteStore.RemoteStoreCallback;
@@ -266,7 +268,9 @@ private void initClient() {
266268
// Set up the sync engine and various stores.
267269
datastore = new MockDatastore(queue, RuntimeEnvironment.application);
268270

269-
remoteStore = new RemoteStore(this, localStore, datastore, queue);
271+
NetworkReachabilityMonitor networkReachabilityMonitor =
272+
new AndroidNetworkReachabilityMonitor(RuntimeEnvironment.application);
273+
remoteStore = new RemoteStore(this, localStore, datastore, queue, networkReachabilityMonitor);
270274
syncEngine = new SyncEngine(localStore, remoteStore, currentUser);
271275
eventManager = new EventManager(syncEngine);
272276
localStore.start();

0 commit comments

Comments
 (0)