Skip to content

Commit 5a35536

Browse files
Handle IndexedDB errors in unsubscribe callback (#3162)
1 parent 13dfc2f commit 5a35536

File tree

3 files changed

+86
-26
lines changed

3 files changed

+86
-26
lines changed

.changeset/smart-cars-doubt.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"firebase": patch
3+
"@firebase/firestore": patch
4+
---
5+
6+
The SDK no longer crashes if an IndexedDB failure occurs when unsubscribing from a Query.

packages/firestore/src/local/local_store.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ class LocalStoreImpl implements LocalStore {
917917
}
918918
}
919919

920-
releaseTarget(
920+
async releaseTarget(
921921
targetId: number,
922922
keepPersistedTargetData: boolean
923923
): Promise<void> {
@@ -928,21 +928,34 @@ class LocalStoreImpl implements LocalStore {
928928
);
929929

930930
const mode = keepPersistedTargetData ? 'readwrite' : 'readwrite-primary';
931-
return this.persistence
932-
.runTransaction('Release target', mode, txn => {
933-
if (!keepPersistedTargetData) {
931+
932+
try {
933+
if (!keepPersistedTargetData) {
934+
await this.persistence.runTransaction('Release target', mode, txn => {
934935
return this.persistence.referenceDelegate.removeTarget(
935936
txn,
936937
targetData!
937938
);
938-
} else {
939-
return PersistencePromise.resolve();
940-
}
941-
})
942-
.then(() => {
943-
this.targetDataByTarget = this.targetDataByTarget.remove(targetId);
944-
this.targetIdByTarget.delete(targetData!.target);
945-
});
939+
});
940+
}
941+
} catch (e) {
942+
if (isIndexedDbTransactionError(e)) {
943+
// All `releaseTarget` does is record the final metadata state for the
944+
// target, but we've been recording this periodically during target
945+
// activity. If we lose this write this could cause a very slight
946+
// difference in the order of target deletion during GC, but we
947+
// don't define exact LRU semantics so this is acceptable.
948+
logDebug(
949+
LOG_TAG,
950+
`Failed to update sequence numbers for target ${targetId}: ${e}`
951+
);
952+
} else {
953+
throw e;
954+
}
955+
}
956+
957+
this.targetDataByTarget = this.targetDataByTarget.remove(targetId);
958+
this.targetIdByTarget.delete(targetData!.target);
946959
}
947960

948961
executeQuery(

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

+55-14
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,10 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
569569
);
570570
});
571571

572-
specTest('Recovers when watch rejection cannot be persisted', [], () => {
572+
specTest('Handles rejections that cannot be persisted', [], () => {
573+
// This test verifies that the client ignores failures during the
574+
// 'Release target' transaction.
575+
573576
const doc1Query = Query.atPath(path('collection/key1'));
574577
const doc2Query = Query.atPath(path('collection/key2'));
575578
const doc1a = doc('collection/key1', 1000, { foo: 'a' });
@@ -593,25 +596,22 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
593596
doc1Query,
594597
new RpcError(Code.PERMISSION_DENIED, 'Simulated target error')
595598
)
596-
// `failDatabaseTransactions()` causes us to go offline.
597-
.expectActiveTargets()
598-
.expectEvents(doc1Query, { fromCache: true })
599-
.expectEvents(doc2Query, { fromCache: true })
599+
.expectEvents(doc1Query, { errorCode: Code.PERMISSION_DENIED })
600600
.recoverDatabase()
601-
.runTimer(TimerId.AsyncQueueRetry)
602-
.expectActiveTargets(
603-
{ query: doc1Query, resumeToken: 'resume-token-1000' },
604-
{ query: doc2Query, resumeToken: 'resume-token-2000' }
605-
)
606-
.watchAcksFull(doc1Query, 3000)
607-
.expectEvents(doc1Query, {})
608601
.watchRemoves(
609602
doc2Query,
610603
new RpcError(Code.PERMISSION_DENIED, 'Simulated target error')
611604
)
612605
.expectEvents(doc2Query, { errorCode: Code.PERMISSION_DENIED })
613-
.watchSends({ affects: [doc1Query] }, doc1b)
614-
.watchSnapshots(4000)
606+
// Verify that `doc1Query` can be listened to again. Note that the
607+
// resume token is slightly outdated since we failed to persist the
608+
// target update during the release.
609+
.userListens(doc1Query, 'resume-token-1000')
610+
.expectEvents(doc1Query, {
611+
added: [doc1a],
612+
fromCache: true
613+
})
614+
.watchAcksFull(doc1Query, 4000, doc1b)
615615
.expectEvents(doc1Query, {
616616
modified: [doc1b]
617617
})
@@ -797,4 +797,45 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
797797
);
798798
}
799799
);
800+
801+
specTest('Unlisten succeeds when target release fails', [], () => {
802+
const query = Query.atPath(path('collection'));
803+
const doc1 = doc('collection/key1', 1, { foo: 'a' });
804+
return spec()
805+
.userListens(query)
806+
.watchAcksFull(query, 1000, doc1)
807+
.expectEvents(query, {
808+
added: [doc1]
809+
})
810+
.failDatabaseTransactions('Release target')
811+
.userUnlistens(query)
812+
.expectActiveTargets();
813+
});
814+
815+
specTest('Can re-listen to query when unlisten fails', [], () => {
816+
const query = Query.atPath(path('collection'));
817+
const doc1 = doc('collection/key1', 1, { foo: 'a' });
818+
const doc2 = doc('collection/key2', 2, { foo: 'b' });
819+
return spec()
820+
.withGCEnabled(false)
821+
.userListens(query)
822+
.watchAcksFull(query, 1000, doc1)
823+
.expectEvents(query, {
824+
added: [doc1]
825+
})
826+
.failDatabaseTransactions('Release target')
827+
.userUnlistens(query)
828+
.watchRemoves(query)
829+
.recoverDatabase()
830+
.userListens(query, 'resume-token-1000')
831+
.expectEvents(query, {
832+
added: [doc1],
833+
fromCache: true
834+
})
835+
.watchAcksFull(query, 2000, doc2)
836+
.expectEvents(query, {
837+
added: [doc2]
838+
})
839+
.userUnlistens(query);
840+
});
800841
});

0 commit comments

Comments
 (0)