Skip to content

Commit 8f51379

Browse files
fix(firestore, android): catch RejectedExecutionException in sendOnSnapshotEvent
When the Firestore native module is invalidated, snapshot listener callbacks that are already queued on the main thread's Handler can still fire after super.invalidate() has shut down the executor. This causes a fatal RejectedExecutionException crash. The existing invalidate() ordering (remove listeners before super.invalidate()) reduces but does not eliminate this race, because AsyncEventListener dispatches via Handler.post() — the callback may already be enqueued before invalidation begins. Wrapping Tasks.call() in sendOnSnapshotEvent with a try-catch for RejectedExecutionException is safe because: - The module is being torn down; no JS listener will process the event - The snapshot data is stale/irrelevant at this point - This matches the standard pattern for handling executor shutdown races Applies to both CollectionModule and DocumentModule.
1 parent c8c1fc1 commit 8f51379

2 files changed

Lines changed: 56 additions & 46 deletions

File tree

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -396,31 +396,36 @@ private void sendOnSnapshotEvent(
396396
int listenerId,
397397
QuerySnapshot querySnapshot,
398398
MetadataChanges metadataChanges) {
399-
Tasks.call(
400-
getTransactionalExecutor(Integer.toString(listenerId)),
401-
() ->
402-
snapshotToWritableMap(
403-
appName, databaseId, "onSnapshot", querySnapshot, metadataChanges))
404-
.addOnCompleteListener(
405-
task -> {
406-
if (task.isSuccessful()) {
407-
WritableMap body = Arguments.createMap();
408-
body.putMap("snapshot", task.getResult());
409-
410-
ReactNativeFirebaseEventEmitter emitter =
411-
ReactNativeFirebaseEventEmitter.getSharedInstance();
412-
413-
emitter.sendEvent(
414-
new ReactNativeFirebaseFirestoreEvent(
415-
ReactNativeFirebaseFirestoreEvent.COLLECTION_EVENT_SYNC,
416-
body,
417-
appName,
418-
databaseId,
419-
listenerId));
420-
} else {
421-
sendOnSnapshotError(appName, databaseId, listenerId, task.getException());
422-
}
423-
});
399+
try {
400+
Tasks.call(
401+
getTransactionalExecutor(Integer.toString(listenerId)),
402+
() ->
403+
snapshotToWritableMap(
404+
appName, databaseId, "onSnapshot", querySnapshot, metadataChanges))
405+
.addOnCompleteListener(
406+
task -> {
407+
if (task.isSuccessful()) {
408+
WritableMap body = Arguments.createMap();
409+
body.putMap("snapshot", task.getResult());
410+
411+
ReactNativeFirebaseEventEmitter emitter =
412+
ReactNativeFirebaseEventEmitter.getSharedInstance();
413+
414+
emitter.sendEvent(
415+
new ReactNativeFirebaseFirestoreEvent(
416+
ReactNativeFirebaseFirestoreEvent.COLLECTION_EVENT_SYNC,
417+
body,
418+
appName,
419+
databaseId,
420+
listenerId));
421+
} else {
422+
sendOnSnapshotError(appName, databaseId, listenerId, task.getException());
423+
}
424+
});
425+
} catch (java.util.concurrent.RejectedExecutionException e) {
426+
// Snapshot arrived after module invalidation shut down the executor.
427+
// Safe to drop — the module is being torn down and no JS listener remains.
428+
}
424429
}
425430

426431
private void sendOnSnapshotError(

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -291,27 +291,32 @@ public void documentBatch(
291291

292292
private void sendOnSnapshotEvent(
293293
String appName, String databaseId, int listenerId, DocumentSnapshot documentSnapshot) {
294-
Tasks.call(getExecutor(), () -> snapshotToWritableMap(appName, databaseId, documentSnapshot))
295-
.addOnCompleteListener(
296-
task -> {
297-
if (task.isSuccessful()) {
298-
WritableMap body = Arguments.createMap();
299-
body.putMap("snapshot", task.getResult());
300-
301-
ReactNativeFirebaseEventEmitter emitter =
302-
ReactNativeFirebaseEventEmitter.getSharedInstance();
303-
304-
emitter.sendEvent(
305-
new ReactNativeFirebaseFirestoreEvent(
306-
ReactNativeFirebaseFirestoreEvent.DOCUMENT_EVENT_SYNC,
307-
body,
308-
appName,
309-
databaseId,
310-
listenerId));
311-
} else {
312-
sendOnSnapshotError(appName, databaseId, listenerId, task.getException());
313-
}
314-
});
294+
try {
295+
Tasks.call(getExecutor(), () -> snapshotToWritableMap(appName, databaseId, documentSnapshot))
296+
.addOnCompleteListener(
297+
task -> {
298+
if (task.isSuccessful()) {
299+
WritableMap body = Arguments.createMap();
300+
body.putMap("snapshot", task.getResult());
301+
302+
ReactNativeFirebaseEventEmitter emitter =
303+
ReactNativeFirebaseEventEmitter.getSharedInstance();
304+
305+
emitter.sendEvent(
306+
new ReactNativeFirebaseFirestoreEvent(
307+
ReactNativeFirebaseFirestoreEvent.DOCUMENT_EVENT_SYNC,
308+
body,
309+
appName,
310+
databaseId,
311+
listenerId));
312+
} else {
313+
sendOnSnapshotError(appName, databaseId, listenerId, task.getException());
314+
}
315+
});
316+
} catch (java.util.concurrent.RejectedExecutionException e) {
317+
// Snapshot arrived after module invalidation shut down the executor.
318+
// Safe to drop — the module is being torn down and no JS listener remains.
319+
}
315320
}
316321

317322
private void sendOnSnapshotError(

0 commit comments

Comments
 (0)