Skip to content

Commit 2244194

Browse files
authored
Firestore: Fix spurious "Backend didn't respond within 10 seconds" errors when network just slow (#8145)
1 parent 84f9ff0 commit 2244194

File tree

9 files changed

+92
-5
lines changed

9 files changed

+92
-5
lines changed

.changeset/early-tomatoes-occur.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/firestore': patch
3+
'firebase': patch
4+
---
5+
6+
Prevent spurious "Backend didn't respond within 10 seconds" errors when network is indeed responding, just slowly.

packages/firestore/src/platform/browser/webchannel_connection.ts

+1
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ export class WebChannelConnection extends RestConnection {
306306
LOG_TAG,
307307
`RPC '${rpcName}' stream ${streamId} transport opened.`
308308
);
309+
streamBridge.callOnConnected();
309310
}
310311
});
311312

packages/firestore/src/platform/node/grpc_connection.ts

+6
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,15 @@ export class GrpcConnection implements Connection {
286286
}
287287
});
288288

289+
let onConnectedSent = false;
289290
grpcStream.on('data', (msg: Resp) => {
290291
if (!closed) {
291292
logDebug(LOG_TAG, `RPC '${rpcName}' stream ${streamId} received:`, msg);
293+
// Emulate the "onConnected" event that WebChannelConnection sends.
294+
if (!onConnectedSent) {
295+
stream.callOnConnected();
296+
onConnectedSent = true;
297+
}
292298
stream.callOnMessage(msg);
293299
}
294300
});

packages/firestore/src/remote/connection.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,14 @@ export interface Connection {
109109
* A bidirectional stream that can be used to send an receive messages.
110110
*
111111
* A stream can be closed locally with close() or can be closed remotely or
112-
* through network errors. onClose is guaranteed to be called. onOpen will only
113-
* be called if the stream successfully established a connection.
112+
* through network errors. onClose is guaranteed to be called. onOpen will be
113+
* called once the stream is ready to send messages (which may or may not be
114+
* before an actual connection to the backend has been established). The
115+
* onConnected event is called when an actual, physical connection with the
116+
* backend has been established, and may occur before or after the onOpen event.
114117
*/
115118
export interface Stream<I, O> {
119+
onConnected(callback: () => void): void;
116120
onOpen(callback: () => void): void;
117121
onClose(callback: (err?: FirestoreError) => void): void;
118122
onMessage(callback: (msg: O) => void): void;

packages/firestore/src/remote/persistent_stream.ts

+8
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ const enum PersistentStreamState {
125125
* events by the concrete implementation classes.
126126
*/
127127
export interface PersistentStreamListener {
128+
/**
129+
* Called after receiving an acknowledgement from the server, confirming that
130+
* we are able to connect to it.
131+
*/
132+
onConnected: () => Promise<void>;
128133
/**
129134
* Called after the stream was established and can accept outgoing
130135
* messages
@@ -483,6 +488,9 @@ export abstract class PersistentStream<
483488
const dispatchIfNotClosed = this.getCloseGuardedDispatcher(this.closeCount);
484489

485490
this.stream = this.startRpc(authToken, appCheckToken);
491+
this.stream.onConnected(() => {
492+
dispatchIfNotClosed(() => this.listener!.onConnected());
493+
});
486494
this.stream.onOpen(() => {
487495
dispatchIfNotClosed(() => {
488496
debugAssert(

packages/firestore/src/remote/remote_store.ts

+9
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,13 @@ function cleanUpWatchStreamState(remoteStoreImpl: RemoteStoreImpl): void {
403403
remoteStoreImpl.watchChangeAggregator = undefined;
404404
}
405405

406+
async function onWatchStreamConnected(
407+
remoteStoreImpl: RemoteStoreImpl
408+
): Promise<void> {
409+
// Mark the client as online since we got a "connected" notification.
410+
remoteStoreImpl.onlineStateTracker.set(OnlineState.Online);
411+
}
412+
406413
async function onWatchStreamOpen(
407414
remoteStoreImpl: RemoteStoreImpl
408415
): Promise<void> {
@@ -923,6 +930,7 @@ function ensureWatchStream(
923930
remoteStoreImpl.datastore,
924931
remoteStoreImpl.asyncQueue,
925932
{
933+
onConnected: onWatchStreamConnected.bind(null, remoteStoreImpl),
926934
onOpen: onWatchStreamOpen.bind(null, remoteStoreImpl),
927935
onClose: onWatchStreamClose.bind(null, remoteStoreImpl),
928936
onWatchChange: onWatchStreamChange.bind(null, remoteStoreImpl)
@@ -969,6 +977,7 @@ function ensureWriteStream(
969977
remoteStoreImpl.datastore,
970978
remoteStoreImpl.asyncQueue,
971979
{
980+
onConnected: () => Promise.resolve(),
972981
onOpen: onWriteStreamOpen.bind(null, remoteStoreImpl),
973982
onClose: onWriteStreamClose.bind(null, remoteStoreImpl),
974983
onHandshakeComplete: onWriteHandshakeComplete.bind(

packages/firestore/src/remote/stream_bridge.ts

+17
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Stream } from './connection';
2626
* interface. The stream callbacks are invoked with the callOn... methods.
2727
*/
2828
export class StreamBridge<I, O> implements Stream<I, O> {
29+
private wrappedOnConnected: (() => void) | undefined;
2930
private wrappedOnOpen: (() => void) | undefined;
3031
private wrappedOnClose: ((err?: FirestoreError) => void) | undefined;
3132
private wrappedOnMessage: ((msg: O) => void) | undefined;
@@ -38,6 +39,14 @@ export class StreamBridge<I, O> implements Stream<I, O> {
3839
this.closeFn = args.closeFn;
3940
}
4041

42+
onConnected(callback: () => void): void {
43+
debugAssert(
44+
!this.wrappedOnConnected,
45+
'Called onConnected on stream twice!'
46+
);
47+
this.wrappedOnConnected = callback;
48+
}
49+
4150
onOpen(callback: () => void): void {
4251
debugAssert(!this.wrappedOnOpen, 'Called onOpen on stream twice!');
4352
this.wrappedOnOpen = callback;
@@ -61,6 +70,14 @@ export class StreamBridge<I, O> implements Stream<I, O> {
6170
this.sendFn(msg);
6271
}
6372

73+
callOnConnected(): void {
74+
debugAssert(
75+
this.wrappedOnConnected !== undefined,
76+
'Cannot call onConnected because no callback was set'
77+
);
78+
this.wrappedOnConnected();
79+
}
80+
6481
callOnOpen(): void {
6582
debugAssert(
6683
this.wrappedOnOpen !== undefined,

packages/firestore/test/integration/browser/webchannel.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ describeFn('WebChannel', () => {
6363
}
6464
};
6565

66+
// Register an "onConnected" callback since it's required, even though we
67+
// don't care about this event.
68+
stream.onConnected(() => {});
69+
6670
// Once the stream is open, send an "add_target" request
6771
stream.onOpen(() => {
6872
stream.send(payload);

packages/firestore/test/integration/remote/stream.test.ts

+35-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import {
2222
Token
2323
} from '../../../src/api/credentials';
2424
import { SnapshotVersion } from '../../../src/core/snapshot_version';
25+
import { Target } from '../../../src/core/target';
26+
import { TargetData, TargetPurpose } from '../../../src/local/target_data';
2527
import { MutationResult } from '../../../src/model/mutation';
28+
import { ResourcePath } from '../../../src/model/path';
2629
import {
2730
newPersistentWatchStream,
2831
newPersistentWriteStream
@@ -57,7 +60,8 @@ type StreamEventType =
5760
| 'mutationResult'
5861
| 'watchChange'
5962
| 'open'
60-
| 'close';
63+
| 'close'
64+
| 'connected';
6165

6266
const SINGLE_MUTATION = [setMutation('docs/1', { foo: 'bar' })];
6367

@@ -117,6 +121,10 @@ class StreamStatusListener implements WatchStreamListener, WriteStreamListener {
117121
return this.resolvePending('watchChange');
118122
}
119123

124+
onConnected(): Promise<void> {
125+
return this.resolvePending('connected');
126+
}
127+
120128
onOpen(): Promise<void> {
121129
return this.resolvePending('open');
122130
}
@@ -148,6 +156,14 @@ describe('Watch Stream', () => {
148156
});
149157
});
150158
});
159+
160+
it('gets connected event before first message', () => {
161+
return withTestWatchStream(async (watchStream, streamListener) => {
162+
await streamListener.awaitCallback('open');
163+
watchStream.watch(sampleTargetData());
164+
await streamListener.awaitCallback('connected');
165+
});
166+
});
151167
});
152168

153169
class MockAuthCredentialsProvider extends EmptyAuthCredentialsProvider {
@@ -190,6 +206,7 @@ describe('Write Stream', () => {
190206
'Handshake must be complete before writing mutations'
191207
);
192208
writeStream.writeHandshake();
209+
await streamListener.awaitCallback('connected');
193210
await streamListener.awaitCallback('handshakeComplete');
194211

195212
// Now writes should succeed
@@ -205,9 +222,10 @@ describe('Write Stream', () => {
205222
return withTestWriteStream((writeStream, streamListener, queue) => {
206223
return streamListener
207224
.awaitCallback('open')
208-
.then(() => {
225+
.then(async () => {
209226
writeStream.writeHandshake();
210-
return streamListener.awaitCallback('handshakeComplete');
227+
await streamListener.awaitCallback('connected');
228+
await streamListener.awaitCallback('handshakeComplete');
211229
})
212230
.then(() => {
213231
writeStream.markIdle();
@@ -228,6 +246,7 @@ describe('Write Stream', () => {
228246
return withTestWriteStream(async (writeStream, streamListener, queue) => {
229247
await streamListener.awaitCallback('open');
230248
writeStream.writeHandshake();
249+
await streamListener.awaitCallback('connected');
231250
await streamListener.awaitCallback('handshakeComplete');
232251

233252
// Mark the stream idle, but immediately cancel the idle timer by issuing another write.
@@ -336,3 +355,16 @@ export async function withTestWatchStream(
336355
streamListener.verifyNoPendingCallbacks();
337356
});
338357
}
358+
359+
function sampleTargetData(): TargetData {
360+
const target: Target = {
361+
path: ResourcePath.emptyPath(),
362+
collectionGroup: null,
363+
orderBy: [],
364+
filters: [],
365+
limit: null,
366+
startAt: null,
367+
endAt: null
368+
};
369+
return new TargetData(target, 1, TargetPurpose.Listen, 1);
370+
}

0 commit comments

Comments
 (0)