Skip to content

Commit f30be09

Browse files
authored
Offline get() improvements. (#1197)
1. Changed MAX_WATCH_STREAM_FAILURES to 1 per conversation with @wilhuff and @jdimond. 2. Fixed a "race" where when a get() triggered a watch stream error, we'd restart the stream, then get() would remove its listener, and so we had no listener left once the watch stream was "opened". Without a listen to send we'd be stuck in Offline state (even though we should be Unknown whenever we have no listens to send), resulting in the next get() returning data from cache without even attempting to reach the backend.
1 parent ad2d178 commit f30be09

File tree

8 files changed

+88
-40
lines changed

8 files changed

+88
-40
lines changed

packages/firestore/CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
# Unreleased
1+
# 0.7.3 (Unreleased)
2+
- [fixed] Fixed an issue where the first `get()` call made after being offline
3+
could incorrectly return cached data without attempting to reach the backend.
4+
- [changed] Changed `get()` to only make 1 attempt to reach the backend before
5+
returning cached data, potentially reducing delays while offline. Previously
6+
it would make 2 attempts, to work around a backend bug.
7+
8+
# 0.7.2
29
- [fixed] Fixed a regression that prevented use of Firestore on ReactNative's
310
Expo platform (#1138).
411

packages/firestore/src/remote/online_state_tracker.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ const LOG_TAG = 'OnlineStateTracker';
2525

2626
// To deal with transient failures, we allow multiple stream attempts before
2727
// giving up and transitioning from OnlineState.Unknown to Offline.
28-
const MAX_WATCH_STREAM_FAILURES = 2;
28+
// TODO(mikelehen): This used to be set to 2 as a mitigation for b/66228394.
29+
// @jdimond thinks that bug is sufficiently fixed so that we can set this back
30+
// to 1. If that works okay, we could potentially remove this logic entirely.
31+
const MAX_WATCH_STREAM_FAILURES = 1;
2932

3033
// To deal with stream attempts that don't succeed or fail in a timely manner,
3134
// we have a timeout for OnlineState to reach Online or Offline.

packages/firestore/src/remote/remote_store.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,16 @@ export class RemoteStore implements TargetMetadataProvider {
243243
delete this.listenTargets[targetId];
244244
if (this.watchStream.isOpen()) {
245245
this.sendUnwatchRequest(targetId);
246-
if (objUtils.isEmpty(this.listenTargets)) {
246+
}
247+
248+
if (objUtils.isEmpty(this.listenTargets)) {
249+
if (this.watchStream.isOpen()) {
247250
this.watchStream.markIdle();
251+
} else if (this.canUseNetwork()) {
252+
// Revert to OnlineState.Unknown if the watch stream is not open and we
253+
// have no listeners, since without any listens to send we cannot
254+
// confirm if the stream is healthy and upgrade to OnlineState.Online.
255+
this.onlineStateTracker.set(OnlineState.Unknown);
248256
}
249257
}
250258
}

packages/firestore/test/integration/api/database.test.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17+
import * as chai from 'chai';
18+
import * as chaiAsPromised from 'chai-as-promised';
19+
1720
import { expect } from 'chai';
1821
import * as firestore from '@firebase/firestore-types';
1922

@@ -32,6 +35,8 @@ import { query } from '../../util/api_helpers';
3235
import { fail } from '../../../src/util/assert';
3336
import { Code } from '../../../src/util/error';
3437

38+
chai.use(chaiAsPromised);
39+
3540
const Timestamp = firebase.firestore!.Timestamp;
3641
const FieldValue = firebase.firestore!.FieldValue;
3742

@@ -884,26 +889,25 @@ apiDescribe('Database', persistence => {
884889
});
885890
});
886891

887-
it('can get documents while offline', () => {
888-
return withTestDoc(persistence, docRef => {
892+
it('can get documents while offline', async () => {
893+
await withTestDoc(persistence, async docRef => {
889894
const firestore = docRef.firestore;
890895

891-
return firestore.disableNetwork().then(() => {
892-
const writePromise = docRef.set({ foo: 'bar' });
893-
894-
return docRef
895-
.get()
896-
.then(doc => {
897-
expect(doc.metadata.fromCache).to.be.true;
898-
return firestore.enableNetwork();
899-
})
900-
.then(() => writePromise)
901-
.then(() => docRef.get())
902-
.then(doc => {
903-
expect(doc.metadata.fromCache).to.be.false;
904-
expect(doc.data()).to.deep.equal({ foo: 'bar' });
905-
});
906-
});
896+
await firestore.disableNetwork();
897+
await expect(docRef.get()).to.eventually.be.rejectedWith(
898+
'Failed to get document because the client is offline.'
899+
);
900+
901+
const writePromise = docRef.set({ foo: 'bar' });
902+
const doc = await docRef.get();
903+
expect(doc.metadata.fromCache).to.be.true;
904+
905+
await firestore.enableNetwork();
906+
await writePromise;
907+
908+
const doc2 = await docRef.get();
909+
expect(doc2.metadata.fromCache).to.be.false;
910+
expect(doc2.data()).to.deep.equal({ foo: 'bar' });
907911
});
908912
});
909913

packages/firestore/test/unit/specs/offline_spec.test.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ describeSpec('Offline:', [], () => {
2828
return (
2929
spec()
3030
.userListens(query)
31-
// second error triggers event
32-
.watchStreamCloses(Code.UNAVAILABLE)
3331
.watchStreamCloses(Code.UNAVAILABLE)
3432
.expectEvents(query, {
3533
fromCache: true,
@@ -49,8 +47,7 @@ describeSpec('Offline:', [], () => {
4947
.watchAcks(query)
5048
// first error triggers unknown state
5149
.watchStreamCloses(Code.UNAVAILABLE)
52-
// getting two more errors triggers offline state
53-
.watchStreamCloses(Code.UNAVAILABLE)
50+
// second error triggers offline state
5451
.watchStreamCloses(Code.UNAVAILABLE)
5552
.expectEvents(query, {
5653
fromCache: true,
@@ -71,8 +68,7 @@ describeSpec('Offline:', [], () => {
7168
return (
7269
spec()
7370
.userListens(query)
74-
// getting two errors triggers offline state
75-
.watchStreamCloses(Code.UNAVAILABLE)
71+
// error triggers offline state
7672
.watchStreamCloses(Code.UNAVAILABLE)
7773
.expectEvents(query, {
7874
fromCache: true,
@@ -83,11 +79,10 @@ describeSpec('Offline:', [], () => {
8379
// If the next (already scheduled) connection attempt fails, we'll move
8480
// to unknown since there are no listeners, and stop trying to connect.
8581
.watchStreamCloses(Code.UNAVAILABLE)
86-
// Suppose sometime later we listen again, it should take two failures
82+
// Suppose sometime later we listen again, it should take one failure
8783
// before we get cached data.
8884
.userListens(query)
8985
.watchStreamCloses(Code.UNAVAILABLE)
90-
.watchStreamCloses(Code.UNAVAILABLE)
9186
.expectEvents(query, {
9287
fromCache: true,
9388
hasPendingWrites: false
@@ -107,8 +102,7 @@ describeSpec('Offline:', [], () => {
107102
// first error triggers unknown state
108103
.watchStreamCloses(Code.UNAVAILABLE)
109104
.restoreListen(query, 'resume-token-1000')
110-
// getting two more errors triggers offline state and fromCache: true
111-
.watchStreamCloses(Code.UNAVAILABLE)
105+
// second error triggers offline state and fromCache: true
112106
.watchStreamCloses(Code.UNAVAILABLE)
113107
.expectEvents(query, { fromCache: true })
114108
// Going online and getting a CURRENT message triggers fromCache: false
@@ -136,8 +130,7 @@ describeSpec('Offline:', [], () => {
136130
// first error triggers unknown state
137131
.watchStreamCloses(Code.UNAVAILABLE)
138132
.restoreListen(query, 'resume-token-1001')
139-
// getting two more errors triggers offline state.
140-
.watchStreamCloses(Code.UNAVAILABLE)
133+
// second error triggers offline state.
141134
.watchStreamCloses(Code.UNAVAILABLE)
142135
.watchAcksFull(query, 1001)
143136
.watchAcksFull(limboQuery, 1001)
@@ -191,10 +184,9 @@ describeSpec('Offline:', [], () => {
191184
return (
192185
spec()
193186
.userListens(query1)
194-
// 2 Failures should mark the client offline and trigger an empty
187+
// After failure, we mark the client offline and trigger an empty
195188
// fromCache event.
196189
.watchStreamCloses(Code.UNAVAILABLE)
197-
.watchStreamCloses(Code.UNAVAILABLE)
198190
.expectEvents(query1, { fromCache: true })
199191

200192
// A new query should immediately return from cache.
@@ -223,4 +215,26 @@ describeSpec('Offline:', [], () => {
223215
);
224216
}
225217
);
218+
219+
// TODO(b/114055812): This shouldn't really need to be marked eager-gc
220+
specTest(
221+
'Queries return from cache when network disabled',
222+
['eager-gc'],
223+
() => {
224+
const query = Query.atPath(path('collection'));
225+
return (
226+
spec()
227+
.disableNetwork()
228+
.userListens(query)
229+
.expectEvents(query, { fromCache: true })
230+
.userUnlistens(query)
231+
232+
// There was once a bug where removing the last listener accidentally
233+
// reverted us to OnlineState.Unknown, so make sure it works a second time
234+
.userListens(query)
235+
.expectEvents(query, { fromCache: true })
236+
.userUnlistens(query)
237+
);
238+
}
239+
);
226240
});

packages/firestore/test/unit/specs/remote_store_spec.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ describeSpec('Remote store:', [], () => {
7777
// Close before we get an ack, this should reset our pending
7878
// target counts.
7979
.watchStreamCloses(Code.UNAVAILABLE)
80-
// This should work now.
80+
.expectEvents(query, { fromCache: true })
81+
8182
.watchAcksFull(query, 1001, doc1)
8283
.expectEvents(query, { added: [doc1] })
8384
);
@@ -97,6 +98,7 @@ describeSpec('Remote store:', [], () => {
9798
// close the stream (this should trigger retry with backoff; but don't
9899
// run it in an attempt to reproduce b/74749605).
99100
.watchStreamCloses(Code.UNAVAILABLE, { runBackoffTimer: false })
101+
.expectEvents(query, { fromCache: true })
100102

101103
// Because we didn't let the backoff timer run and restart the watch
102104
// stream, there will be no active targets.

packages/firestore/test/unit/specs/spec_test_runner.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,8 @@ abstract class TestRunner {
375375
[targetId: number]: { query: SpecQuery; resumeToken: string };
376376
};
377377

378+
private networkEnabled = true;
379+
378380
private datastore: Datastore;
379381
private localStore: LocalStore;
380382
private remoteStore: RemoteStore;
@@ -603,7 +605,7 @@ abstract class TestRunner {
603605
);
604606
}
605607

606-
if (this.isPrimaryClient) {
608+
if (this.isPrimaryClient && this.networkEnabled) {
607609
// Open should always have happened after a listen
608610
await this.connection.waitForWatchOpen();
609611
}
@@ -851,6 +853,7 @@ abstract class TestRunner {
851853
}
852854

853855
private async doDisableNetwork(): Promise<void> {
856+
this.networkEnabled = false;
854857
// Make sure to execute all writes that are currently queued. This allows us
855858
// to assert on the total number of requests sent before shutdown.
856859
await this.remoteStore.fillWritePipeline();
@@ -862,6 +865,7 @@ abstract class TestRunner {
862865
}
863866

864867
private async doEnableNetwork(): Promise<void> {
868+
this.networkEnabled = true;
865869
await this.syncEngine.enableNetwork();
866870
}
867871

@@ -1020,7 +1024,7 @@ abstract class TestRunner {
10201024
}
10211025

10221026
private async validateActiveTargets(): Promise<void> {
1023-
if (!this.isPrimaryClient) {
1027+
if (!this.isPrimaryClient || !this.networkEnabled) {
10241028
expect(this.connection.activeTargets).to.be.empty;
10251029
return;
10261030
}

packages/rxfire/firestore/collection/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ function processIndividualChange(
7575
}
7676
break;
7777
case 'modified':
78-
if (combined[change.oldIndex] == null || combined[change.oldIndex].doc.id == change.doc.id) {
78+
if (
79+
combined[change.oldIndex] == null ||
80+
combined[change.oldIndex].doc.id == change.doc.id
81+
) {
7982
// When an item changes position we first remove it
8083
// and then add it's new position
8184
if (change.oldIndex !== change.newIndex) {
@@ -87,7 +90,10 @@ function processIndividualChange(
8790
}
8891
break;
8992
case 'removed':
90-
if (combined[change.oldIndex] && combined[change.oldIndex].doc.id == change.doc.id) {
93+
if (
94+
combined[change.oldIndex] &&
95+
combined[change.oldIndex].doc.id == change.doc.id
96+
) {
9197
combined.splice(change.oldIndex, 1);
9298
}
9399
break;

0 commit comments

Comments
 (0)