Skip to content

Commit 45d44ff

Browse files
Don't crash if Target cannot be persisted
1 parent a36b51b commit 45d44ff

File tree

7 files changed

+110
-34
lines changed

7 files changed

+110
-34
lines changed

packages/firestore/.idea/runConfigurations/Unit_Tests.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Unreleased
2+
- [fixed] Firestore now rejects `onSnapshot()` listeners if they cannot be
3+
registered in IndexedDB. Previously, these errors crashed the client.
24
- [fixed] Firestore now rejects write operations if they cannot be persisted
35
in IndexedDB. Previously, these errors crashed the client.
46
- [fixed] Fixed a source of IndexedDB-related crashes for tabs that receive

packages/firestore/src/core/event_manager.ts

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@ import { ObjectMap } from '../util/obj_map';
2121
import { Query } from './query';
2222
import { SyncEngine, SyncEngineListener } from './sync_engine';
2323
import { OnlineState, TargetId } from './types';
24-
import { DocumentViewChange, ChangeType, ViewSnapshot } from './view_snapshot';
24+
import { ChangeType, DocumentViewChange, ViewSnapshot } from './view_snapshot';
25+
import { logError } from '../util/log';
26+
import { Code, FirestoreError } from '../util/error';
27+
28+
const LOG_TAG = 'EventManager';
2529

2630
/**
2731
* Holds the listeners and the last received ViewSnapshot for a query being
2832
* tracked by EventManager.
2933
*/
3034
class QueryListenersInfo {
31-
viewSnap: ViewSnapshot | null = null;
35+
viewSnap: ViewSnapshot | undefined = undefined;
3236
targetId: TargetId = 0;
3337
listeners: QueryListener[] = [];
3438
}
@@ -59,16 +63,37 @@ export class EventManager implements SyncEngineListener {
5963
this.syncEngine.subscribe(this);
6064
}
6165

62-
listen(listener: QueryListener): Promise<TargetId> {
66+
async listen(listener: QueryListener): Promise<void> {
6367
const query = listener.query;
6468
let firstListen = false;
6569

6670
let queryInfo = this.queries.get(query);
6771
if (!queryInfo) {
6872
firstListen = true;
6973
queryInfo = new QueryListenersInfo();
70-
this.queries.set(query, queryInfo);
7174
}
75+
76+
if (firstListen) {
77+
try {
78+
const { targetId, snapshot } = await this.syncEngine.listen(query);
79+
queryInfo.targetId = targetId;
80+
queryInfo.viewSnap = snapshot;
81+
} catch (e) {
82+
if (e.name === 'IndexedDbTransactionError') {
83+
logError(LOG_TAG, `Failed to register query: ${e}`);
84+
listener.onError(
85+
new FirestoreError(
86+
Code.UNAVAILABLE,
87+
`Failed to register query: ${e}`
88+
)
89+
);
90+
return;
91+
}
92+
throw e;
93+
}
94+
}
95+
96+
this.queries.set(query, queryInfo!);
7297
queryInfo.listeners.push(listener);
7398

7499
// Run global snapshot listeners if a consistent snapshot has been emitted.
@@ -84,15 +109,6 @@ export class EventManager implements SyncEngineListener {
84109
this.raiseSnapshotsInSyncEvent();
85110
}
86111
}
87-
88-
if (firstListen) {
89-
return this.syncEngine.listen(query).then(targetId => {
90-
queryInfo!.targetId = targetId;
91-
return targetId;
92-
});
93-
} else {
94-
return Promise.resolve(queryInfo.targetId);
95-
}
96112
}
97113

98114
async unlisten(listener: QueryListener): Promise<void> {

packages/firestore/src/core/sync_engine.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ class QueryView {
102102
) {}
103103
}
104104

105+
/** Result type returned during initial query registration. */
106+
export interface QueryRegistration {
107+
targetId: TargetId;
108+
snapshot?: ViewSnapshot;
109+
}
110+
105111
/** Tracks a limbo resolution. */
106112
class LimboResolution {
107113
constructor(public key: DocumentKey) {}
@@ -216,7 +222,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
216222
* server. All the subsequent view snapshots or errors are sent to the
217223
* subscribed handlers. Returns the targetId of the query.
218224
*/
219-
async listen(query: Query): Promise<TargetId> {
225+
async listen(query: Query): Promise<QueryRegistration> {
220226
this.assertSubscribed('listen()');
221227

222228
let targetId;
@@ -250,8 +256,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
250256
}
251257
}
252258

253-
this.syncEngineListener!.onWatchChange([viewSnapshot]);
254-
return targetId;
259+
return { targetId, snapshot: viewSnapshot };
255260
}
256261

257262
/**

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { describeSpec, specTest } from './describe_spec';
1919
import { client, spec } from './spec_builder';
2020
import { TimerId } from '../../../src/util/async_queue';
2121
import { Query } from '../../../src/core/query';
22+
import { Code } from '../../../src/util/error';
2223
import { doc, path } from '../../util/helpers';
2324

2425
describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
@@ -162,4 +163,37 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
162163
.watchAcksFull(query, 2, doc1, doc3)
163164
.expectEvents(query, { metadata: [doc1, doc3] });
164165
});
166+
167+
specTest('Fails targets that cannot be allocated', [], () => {
168+
const query1 = Query.atPath(path('collection1'));
169+
const query2 = Query.atPath(path('collection2'));
170+
const query3 = Query.atPath(path('collection3'));
171+
return spec()
172+
.userListens(query1)
173+
.watchAcksFull(query1, 1)
174+
.expectEvents(query1, {})
175+
.failDatabase()
176+
.userListens(query2)
177+
.expectEvents(query2, { errorCode: Code.UNKNOWN })
178+
.recoverDatabase()
179+
.userListens(query3)
180+
.watchAcksFull(query3, 1)
181+
.expectEvents(query3, {});
182+
});
183+
184+
specTest('Can re-add failed target', [], () => {
185+
const query1 = Query.atPath(path('collection1'));
186+
const query2 = Query.atPath(path('collection2'));
187+
return spec()
188+
.userListens(query1)
189+
.watchAcksFull(query1, 1)
190+
.expectEvents(query1, {})
191+
.failDatabase()
192+
.userListens(query2)
193+
.expectEvents(query2, { errorCode: Code.UNKNOWN })
194+
.recoverDatabase()
195+
.userListens(query2)
196+
.watchAcksFull(query2, 1)
197+
.expectEvents(query2, {});
198+
});
165199
});

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

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export class ClientMemoryState {
8989
limboMapping: LimboMap = {};
9090

9191
limboIdGenerator: TargetIdGenerator = TargetIdGenerator.forSyncEngine();
92+
injectFailures = false;
9293

9394
constructor() {
9495
this.reset();
@@ -191,6 +192,14 @@ export class SpecBuilder {
191192
return this.clientState.activeTargets;
192193
}
193194

195+
private get injectFailures(): boolean {
196+
return this.clientState.injectFailures;
197+
}
198+
199+
private set injectFailures(injectFailures: boolean) {
200+
this.clientState.injectFailures = injectFailures;
201+
}
202+
194203
/**
195204
* Exports the spec steps as a JSON object that be used in the spec runner.
196205
*/
@@ -232,18 +241,26 @@ export class SpecBuilder {
232241

233242
const target = query.toTarget();
234243
let targetId: TargetId = 0;
235-
if (this.queryMapping.has(target)) {
236-
targetId = this.queryMapping.get(target)!;
244+
245+
if (this.injectFailures) {
246+
// Return a `userListens()` step but don't advance the target IDs.
247+
this.currentStep = {
248+
userListen: [targetId, SpecBuilder.queryToSpec(query)]
249+
};
237250
} else {
238-
targetId = this.queryIdGenerator.next(target);
239-
}
251+
if (this.queryMapping.has(target)) {
252+
targetId = this.queryMapping.get(target)!;
253+
} else {
254+
targetId = this.queryIdGenerator.next(target);
255+
}
240256

241-
this.queryMapping.set(target, targetId);
242-
this.addQueryToActiveTargets(targetId, query, resumeToken);
243-
this.currentStep = {
244-
userListen: [targetId, SpecBuilder.queryToSpec(query)],
245-
expectedState: { activeTargets: { ...this.activeTargets } }
246-
};
257+
this.queryMapping.set(target, targetId);
258+
this.addQueryToActiveTargets(targetId, query, resumeToken);
259+
this.currentStep = {
260+
userListen: [targetId, SpecBuilder.queryToSpec(query)],
261+
expectedState: { activeTargets: { ...this.activeTargets } }
262+
};
263+
}
247264
return this;
248265
}
249266

@@ -421,6 +438,7 @@ export class SpecBuilder {
421438
/** Fails all database operations until `recoverDatabase()` is called. */
422439
failDatabase(): this {
423440
this.nextStep();
441+
this.injectFailures = true;
424442
this.currentStep = {
425443
failDatabase: true
426444
};
@@ -430,6 +448,7 @@ export class SpecBuilder {
430448
/** Stops failing database operations. */
431449
recoverDatabase(): this {
432450
this.nextStep();
451+
this.injectFailures = false;
433452
this.currentStep = {
434453
failDatabase: false
435454
};

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,6 @@ abstract class TestRunner {
597597
}
598598

599599
private async doListen(listenSpec: SpecUserListen): Promise<void> {
600-
const expectedTargetId = listenSpec[0];
601600
const querySpec = listenSpec[1];
602601
const query = parseQuery(querySpec);
603602
const aggregator = new EventAggregator(query, this.pushEvent.bind(this));
@@ -609,14 +608,15 @@ abstract class TestRunner {
609608
const queryListener = new QueryListener(query, aggregator, options);
610609
this.queryListeners.set(query, queryListener);
611610

612-
await this.queue.enqueue(async () => {
613-
const targetId = await this.eventManager.listen(queryListener);
614-
expect(targetId).to.equal(
615-
expectedTargetId,
616-
'targetId assigned to listen'
617-
);
611+
await this.queue.enqueue(() => {
612+
return this.eventManager.listen(queryListener);
618613
});
619614

615+
if (this.persistence.injectFailures) {
616+
// The Watch stream won't open if we are injecting failures.
617+
return;
618+
}
619+
620620
// Skip the backoff that may have been triggered by a previous call to
621621
// `watchStreamCloses()`.
622622
if (

0 commit comments

Comments
 (0)