Skip to content

Commit 1a39d8c

Browse files
authored
Allow firestore to recover quicker when a network change occurs (#217)
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 39408f5 commit 1a39d8c

File tree

7 files changed

+346
-7
lines changed

7 files changed

+346
-7
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Unreleased
2+
3+
# 18.0.1
24
- [fixed] Fixed an issue where Firestore would crash if handling write batches
35
larger than 2 MB in size (#208).
6+
- [changed] Firestore now recovers more quickly from long periods without
7+
network access (#217).
48

59
# 18.0.0
610
- [changed] The `timestampsInSnapshotsEnabled` setting is now enabled by
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.testutil.IntegrationTestUtil.waitFor;
18+
19+
import android.support.test.InstrumentationRegistry;
20+
import android.support.test.runner.AndroidJUnit4;
21+
import com.google.firebase.database.collection.ImmutableSortedSet;
22+
import com.google.firebase.firestore.auth.User;
23+
import com.google.firebase.firestore.core.OnlineState;
24+
import com.google.firebase.firestore.local.LocalStore;
25+
import com.google.firebase.firestore.local.MemoryPersistence;
26+
import com.google.firebase.firestore.local.Persistence;
27+
import com.google.firebase.firestore.model.DocumentKey;
28+
import com.google.firebase.firestore.model.mutation.MutationBatchResult;
29+
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
30+
import com.google.firebase.firestore.util.AsyncQueue;
31+
import com.google.firebase.firestore.util.Consumer;
32+
import io.grpc.Status;
33+
import java.util.concurrent.Semaphore;
34+
import org.junit.Test;
35+
import org.junit.runner.RunWith;
36+
37+
@RunWith(AndroidJUnit4.class)
38+
public class RemoteStoreTest {
39+
@Test
40+
public void testRemoteStoreStreamStopsWhenNetworkUnreachable() {
41+
AsyncQueue testQueue = new AsyncQueue();
42+
Datastore datastore =
43+
new Datastore(
44+
IntegrationTestUtil.testEnvDatabaseInfo(),
45+
testQueue,
46+
null,
47+
InstrumentationRegistry.getContext());
48+
Semaphore networkChangeSemaphore = new Semaphore(0);
49+
RemoteStore.RemoteStoreCallback callback =
50+
new RemoteStore.RemoteStoreCallback() {
51+
public void handleRemoteEvent(RemoteEvent remoteEvent) {}
52+
53+
public void handleRejectedListen(int targetId, Status error) {}
54+
55+
public void handleSuccessfulWrite(MutationBatchResult successfulWrite) {}
56+
57+
public void handleRejectedWrite(int batchId, Status error) {}
58+
59+
public void handleOnlineStateChange(OnlineState onlineState) {
60+
networkChangeSemaphore.release();
61+
}
62+
63+
public ImmutableSortedSet<DocumentKey> getRemoteKeysForTarget(int targetId) {
64+
return null;
65+
}
66+
};
67+
68+
FakeConnectivityMonitor connectivityMonitor = new FakeConnectivityMonitor();
69+
Persistence persistence = MemoryPersistence.createEagerGcMemoryPersistence();
70+
persistence.start();
71+
LocalStore localStore = new LocalStore(persistence, User.UNAUTHENTICATED);
72+
RemoteStore remoteStore =
73+
new RemoteStore(callback, localStore, datastore, testQueue, connectivityMonitor);
74+
75+
waitFor(testQueue.enqueue(() -> remoteStore.forceEnableNetwork()));
76+
drain(testQueue);
77+
networkChangeSemaphore.drainPermits();
78+
79+
connectivityMonitor.goOffline();
80+
waitFor(networkChangeSemaphore);
81+
drain(testQueue);
82+
83+
waitFor(testQueue.enqueue(() -> remoteStore.forceEnableNetwork()));
84+
networkChangeSemaphore.drainPermits();
85+
connectivityMonitor.goOnline();
86+
waitFor(networkChangeSemaphore);
87+
}
88+
89+
private void drain(AsyncQueue testQueue) {
90+
waitFor(testQueue.enqueue(() -> {}));
91+
}
92+
93+
class FakeConnectivityMonitor implements ConnectivityMonitor {
94+
private Consumer<NetworkStatus> callback = null;
95+
96+
@Override
97+
public void addCallback(Consumer<NetworkStatus> callback) {
98+
this.callback = callback;
99+
}
100+
101+
@Override
102+
public void shutdown() {}
103+
104+
public void goOffline() {
105+
if (callback != null) {
106+
callback.accept(ConnectivityMonitor.NetworkStatus.UNREACHABLE);
107+
}
108+
}
109+
110+
public void goOnline() {
111+
if (callback != null) {
112+
callback.accept(ConnectivityMonitor.NetworkStatus.REACHABLE);
113+
}
114+
}
115+
}
116+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
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.AndroidConnectivityMonitor;
48+
import com.google.firebase.firestore.remote.ConnectivityMonitor;
4749
import com.google.firebase.firestore.remote.Datastore;
4850
import com.google.firebase.firestore.remote.RemoteEvent;
4951
import com.google.firebase.firestore.remote.RemoteSerializer;
@@ -241,7 +243,8 @@ 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+
ConnectivityMonitor connectivityMonitor = new AndroidConnectivityMonitor(context);
247+
remoteStore = new RemoteStore(this, localStore, datastore, asyncQueue, connectivityMonitor);
245248

246249
syncEngine = new SyncEngine(localStore, remoteStore, user);
247250
eventManager = new EventManager(syncEngine);
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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 com.google.firebase.firestore.util.Consumer;
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
import javax.annotation.Nullable;
32+
33+
/**
34+
* Android implementation of ConnectivityMonitor. Parallel implementations exist for N+ and 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 AndroidConnectivityMonitor implements ConnectivityMonitor {
40+
41+
private final Context context;
42+
@Nullable private final ConnectivityManager connectivityManager;
43+
@Nullable private Runnable unregisterRunnable;
44+
private final List<Consumer<NetworkStatus>> callbacks = new ArrayList<>();
45+
46+
public AndroidConnectivityMonitor(Context context) {
47+
// This notnull restriction could be eliminated... the pre-N method doesn't
48+
// require a Context, and we could use that even on N+ if necessary.
49+
hardAssert(context != null, "Context must be non-null");
50+
this.context = context;
51+
52+
connectivityManager =
53+
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
54+
configureNetworkMonitoring();
55+
}
56+
57+
@Override
58+
public void addCallback(Consumer<NetworkStatus> callback) {
59+
callbacks.add(callback);
60+
}
61+
62+
@Override
63+
public void shutdown() {
64+
if (unregisterRunnable != null) {
65+
unregisterRunnable.run();
66+
unregisterRunnable = null;
67+
}
68+
}
69+
70+
private void configureNetworkMonitoring() {
71+
// Android N added the registerDefaultNetworkCallback API to listen to changes in the device's
72+
// default network. For earlier Android API levels, use the BroadcastReceiver API.
73+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
74+
final DefaultNetworkCallback defaultNetworkCallback = new DefaultNetworkCallback();
75+
connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
76+
unregisterRunnable =
77+
new Runnable() {
78+
@Override
79+
public void run() {
80+
connectivityManager.unregisterNetworkCallback(defaultNetworkCallback);
81+
}
82+
};
83+
} else {
84+
NetworkReceiver networkReceiver = new NetworkReceiver();
85+
IntentFilter networkIntentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
86+
context.registerReceiver(networkReceiver, networkIntentFilter);
87+
unregisterRunnable =
88+
new Runnable() {
89+
@Override
90+
public void run() {
91+
context.unregisterReceiver(networkReceiver);
92+
}
93+
};
94+
}
95+
}
96+
97+
/** Respond to changes in the default network. Only used on API levels 24+. */
98+
@TargetApi(Build.VERSION_CODES.N)
99+
private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback {
100+
@Override
101+
public void onAvailable(Network network) {
102+
for (Consumer<NetworkStatus> callback : callbacks) {
103+
callback.accept(NetworkStatus.REACHABLE);
104+
}
105+
}
106+
107+
@Override
108+
public void onLost(Network network) {
109+
for (Consumer<NetworkStatus> callback : callbacks) {
110+
callback.accept(NetworkStatus.UNREACHABLE);
111+
}
112+
}
113+
}
114+
115+
/** Respond to network changes. Only used on API levels < 24. */
116+
private class NetworkReceiver extends BroadcastReceiver {
117+
private boolean isConnected = false;
118+
119+
@Override
120+
public void onReceive(Context context, Intent intent) {
121+
ConnectivityManager conn =
122+
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
123+
NetworkInfo networkInfo = conn.getActiveNetworkInfo();
124+
boolean wasConnected = isConnected;
125+
isConnected = networkInfo != null && networkInfo.isConnected();
126+
if (isConnected && !wasConnected) {
127+
for (Consumer<NetworkStatus> callback : callbacks) {
128+
callback.accept(NetworkStatus.REACHABLE);
129+
}
130+
} else if (!isConnected && wasConnected) {
131+
for (Consumer<NetworkStatus> callback : callbacks) {
132+
callback.accept(NetworkStatus.UNREACHABLE);
133+
}
134+
}
135+
}
136+
}
137+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 com.google.firebase.firestore.util.Consumer;
18+
19+
/** Interface for monitoring changes in network connectivity/reachability. */
20+
public interface ConnectivityMonitor {
21+
enum NetworkStatus {
22+
UNREACHABLE,
23+
REACHABLE,
24+
// TODO(rsgowman): REACHABLE_VIA_CELLULAR.
25+
// Leaving this off for now, since (a) we don't need it, and (b) it's somewhat messy to
26+
// determine, and (c) we need two parallel implementations (for N+ and pre-N).
27+
};
28+
29+
// TODO(rsgowman): Skipping isNetworkReachable() until we need it.
30+
// boolean isNetworkReachable();
31+
32+
void addCallback(Consumer<NetworkStatus> callback);
33+
34+
/**
35+
* Stops monitoring connectivity. After this call completes, no further callbacks will be
36+
* triggered. After shutdown() is called, no further calls are allowed on this instance.
37+
*/
38+
void shutdown();
39+
}

0 commit comments

Comments
 (0)