From 561df2d705b7ae1ed9897bf1ee4538c739d0ef9f Mon Sep 17 00:00:00 2001 From: Christian Bernier Date: Mon, 13 Apr 2026 12:05:34 -0400 Subject: [PATCH 1/3] fix(firestore, android): catch RejectedExecutionException in sendOnSnapshotEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...tiveFirebaseFirestoreCollectionModule.java | 55 ++++++++++--------- ...NativeFirebaseFirestoreDocumentModule.java | 47 +++++++++------- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java index 64ebb0266d..df0287446b 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java @@ -407,31 +407,36 @@ private void sendOnSnapshotEvent( int listenerId, QuerySnapshot querySnapshot, MetadataChanges metadataChanges) { - Tasks.call( - getTransactionalExecutor(Integer.toString(listenerId)), - () -> - snapshotToWritableMap( - appName, databaseId, "onSnapshot", querySnapshot, metadataChanges)) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - WritableMap body = Arguments.createMap(); - body.putMap("snapshot", task.getResult()); - - ReactNativeFirebaseEventEmitter emitter = - ReactNativeFirebaseEventEmitter.getSharedInstance(); - - emitter.sendEvent( - new ReactNativeFirebaseFirestoreEvent( - ReactNativeFirebaseFirestoreEvent.COLLECTION_EVENT_SYNC, - body, - appName, - databaseId, - listenerId)); - } else { - sendOnSnapshotError(appName, databaseId, listenerId, task.getException()); - } - }); + try { + Tasks.call( + getTransactionalExecutor(Integer.toString(listenerId)), + () -> + snapshotToWritableMap( + appName, databaseId, "onSnapshot", querySnapshot, metadataChanges)) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + WritableMap body = Arguments.createMap(); + body.putMap("snapshot", task.getResult()); + + ReactNativeFirebaseEventEmitter emitter = + ReactNativeFirebaseEventEmitter.getSharedInstance(); + + emitter.sendEvent( + new ReactNativeFirebaseFirestoreEvent( + ReactNativeFirebaseFirestoreEvent.COLLECTION_EVENT_SYNC, + body, + appName, + databaseId, + listenerId)); + } else { + sendOnSnapshotError(appName, databaseId, listenerId, task.getException()); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + // Snapshot arrived after module invalidation shut down the executor. + // Safe to drop — the module is being torn down and no JS listener remains. + } } private void sendOnSnapshotError( diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java index 97d022c512..81473d98e0 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java @@ -300,27 +300,32 @@ public void documentBatch( private void sendOnSnapshotEvent( String appName, String databaseId, int listenerId, DocumentSnapshot documentSnapshot) { - Tasks.call(getExecutor(), () -> snapshotToWritableMap(appName, databaseId, documentSnapshot)) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - WritableMap body = Arguments.createMap(); - body.putMap("snapshot", task.getResult()); - - ReactNativeFirebaseEventEmitter emitter = - ReactNativeFirebaseEventEmitter.getSharedInstance(); - - emitter.sendEvent( - new ReactNativeFirebaseFirestoreEvent( - ReactNativeFirebaseFirestoreEvent.DOCUMENT_EVENT_SYNC, - body, - appName, - databaseId, - listenerId)); - } else { - sendOnSnapshotError(appName, databaseId, listenerId, task.getException()); - } - }); + try { + Tasks.call(getExecutor(), () -> snapshotToWritableMap(appName, databaseId, documentSnapshot)) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + WritableMap body = Arguments.createMap(); + body.putMap("snapshot", task.getResult()); + + ReactNativeFirebaseEventEmitter emitter = + ReactNativeFirebaseEventEmitter.getSharedInstance(); + + emitter.sendEvent( + new ReactNativeFirebaseFirestoreEvent( + ReactNativeFirebaseFirestoreEvent.DOCUMENT_EVENT_SYNC, + body, + appName, + databaseId, + listenerId)); + } else { + sendOnSnapshotError(appName, databaseId, listenerId, task.getException()); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + // Snapshot arrived after module invalidation shut down the executor. + // Safe to drop — the module is being torn down and no JS listener remains. + } } private void sendOnSnapshotError( From 9aa8c83539015117d1c032ce65469b4babcb01fb Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Sun, 3 May 2026 19:46:17 -0500 Subject: [PATCH 2/3] fix(database, android): remove RTDB listeners before shutting down executors Call super.invalidate() only after clearing query listeners so Firebase callbacks are less likely to run against a shut-down TaskExecutorService, matching the Firestore module invalidation order. --- .../database/ReactNativeFirebaseDatabaseQueryModule.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/database/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQueryModule.java b/packages/database/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQueryModule.java index 6a016b128c..d44d1b47eb 100644 --- a/packages/database/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQueryModule.java +++ b/packages/database/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQueryModule.java @@ -43,8 +43,6 @@ public class ReactNativeFirebaseDatabaseQueryModule extends ReactNativeFirebaseM @Override public void invalidate() { - super.invalidate(); - Iterator refIterator = queryMap.entrySet().iterator(); while (refIterator.hasNext()) { Map.Entry pair = (Map.Entry) refIterator.next(); @@ -53,6 +51,8 @@ public void invalidate() { databaseQuery.removeAllEventListeners(); refIterator.remove(); // avoids a ConcurrentModificationException } + + super.invalidate(); } /** From cfc02417bc325092d0a95c66602bc888408b00d6 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Sun, 3 May 2026 19:47:19 -0500 Subject: [PATCH 3/3] fix(android): catch RejectedExecutionException on executor-backed Tasks Wrap Tasks.call paths that run after Firebase callbacks or during bridge teardown: Realtime Database once listeners and streaming handleDatabaseEvent, Firestore query get, and Firestore transaction get document. Matches the Firestore snapshot listener handling when TaskExecutorService is shut down. --- ...eactNativeFirebaseDatabaseQueryModule.java | 183 ++++++++++-------- ...tiveFirebaseFirestoreCollectionModule.java | 24 ++- ...iveFirebaseFirestoreTransactionModule.java | 30 +-- 3 files changed, 135 insertions(+), 102 deletions(-) diff --git a/packages/database/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQueryModule.java b/packages/database/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQueryModule.java index d44d1b47eb..da7c0409b8 100644 --- a/packages/database/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQueryModule.java +++ b/packages/database/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQueryModule.java @@ -101,15 +101,19 @@ private void addOnceValueEventListener( new ValueEventListener() { @Override public void onDataChange(@Nonnull DataSnapshot dataSnapshot) { - Tasks.call(getExecutor(), () -> snapshotToMap(dataSnapshot)) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - promise.resolve(task.getResult()); - } else { - rejectPromiseWithExceptionMap(promise, task.getException()); - } - }); + try { + Tasks.call(getExecutor(), () -> snapshotToMap(dataSnapshot)) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + promise.resolve(task.getResult()); + } else { + rejectPromiseWithExceptionMap(promise, task.getException()); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + rejectPromiseWithExceptionMap(promise, e); + } } @Override @@ -139,17 +143,21 @@ private void addChildOnceEventListener( public void onChildAdded(@Nonnull DataSnapshot dataSnapshot, String previousChildName) { if ("child_added".equals(eventType)) { databaseQuery.removeEventListener(this); - Tasks.call( - getExecutor(), - () -> snapshotWithPreviousChildToMap(dataSnapshot, previousChildName)) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - promise.resolve(task.getResult()); - } else { - rejectPromiseWithExceptionMap(promise, task.getException()); - } - }); + try { + Tasks.call( + getExecutor(), + () -> snapshotWithPreviousChildToMap(dataSnapshot, previousChildName)) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + promise.resolve(task.getResult()); + } else { + rejectPromiseWithExceptionMap(promise, task.getException()); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + rejectPromiseWithExceptionMap(promise, e); + } } } @@ -157,17 +165,21 @@ public void onChildAdded(@Nonnull DataSnapshot dataSnapshot, String previousChil public void onChildChanged(@Nonnull DataSnapshot dataSnapshot, String previousChildName) { if ("child_changed".equals(eventType)) { databaseQuery.removeEventListener(this); - Tasks.call( - getExecutor(), - () -> snapshotWithPreviousChildToMap(dataSnapshot, previousChildName)) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - promise.resolve(task.getResult()); - } else { - rejectPromiseWithExceptionMap(promise, task.getException()); - } - }); + try { + Tasks.call( + getExecutor(), + () -> snapshotWithPreviousChildToMap(dataSnapshot, previousChildName)) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + promise.resolve(task.getResult()); + } else { + rejectPromiseWithExceptionMap(promise, task.getException()); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + rejectPromiseWithExceptionMap(promise, e); + } } } @@ -175,15 +187,19 @@ public void onChildChanged(@Nonnull DataSnapshot dataSnapshot, String previousCh public void onChildRemoved(@Nonnull DataSnapshot dataSnapshot) { if ("child_removed".equals(eventType)) { databaseQuery.removeEventListener(this); - Tasks.call(getExecutor(), () -> snapshotWithPreviousChildToMap(dataSnapshot, null)) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - promise.resolve(task.getResult()); - } else { - rejectPromiseWithExceptionMap(promise, task.getException()); - } - }); + try { + Tasks.call(getExecutor(), () -> snapshotWithPreviousChildToMap(dataSnapshot, null)) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + promise.resolve(task.getResult()); + } else { + rejectPromiseWithExceptionMap(promise, task.getException()); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + rejectPromiseWithExceptionMap(promise, e); + } } } @@ -191,17 +207,21 @@ public void onChildRemoved(@Nonnull DataSnapshot dataSnapshot) { public void onChildMoved(@Nonnull DataSnapshot dataSnapshot, String previousChildName) { if ("child_moved".equals(eventType)) { databaseQuery.removeEventListener(this); - Tasks.call( - getExecutor(), - () -> snapshotWithPreviousChildToMap(dataSnapshot, previousChildName)) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - promise.resolve(task.getResult()); - } else { - rejectPromiseWithExceptionMap(promise, task.getException()); - } - }); + try { + Tasks.call( + getExecutor(), + () -> snapshotWithPreviousChildToMap(dataSnapshot, previousChildName)) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + promise.resolve(task.getResult()); + } else { + rejectPromiseWithExceptionMap(promise, task.getException()); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + rejectPromiseWithExceptionMap(promise, e); + } } } @@ -315,34 +335,39 @@ private void handleDatabaseEvent( DataSnapshot dataSnapshot, @Nullable String previousChildName) { final String eventRegistrationKey = registration.getString("eventRegistrationKey"); - Tasks.call( - getTransactionalExecutor(eventRegistrationKey), - () -> { - if (eventType.equals("value")) { - return snapshotToMap(dataSnapshot); - } else { - return snapshotWithPreviousChildToMap(dataSnapshot, previousChildName); - } - }) - .addOnCompleteListener( - getExecutor(), - task -> { - if (task.isSuccessful()) { - WritableMap data = task.getResult(); - WritableMap event = Arguments.createMap(); - event.putMap("data", data); - event.putString("key", key); - event.putString("eventType", eventType); - event.putMap("registration", readableMapToWritableMap(registration)); - - ReactNativeFirebaseEventEmitter emitter = - ReactNativeFirebaseEventEmitter.getSharedInstance(); - - emitter.sendEvent( - new ReactNativeFirebaseDatabaseEvent( - ReactNativeFirebaseDatabaseEvent.EVENT_SYNC, event)); - } - }); + try { + Tasks.call( + getTransactionalExecutor(eventRegistrationKey), + () -> { + if (eventType.equals("value")) { + return snapshotToMap(dataSnapshot); + } else { + return snapshotWithPreviousChildToMap(dataSnapshot, previousChildName); + } + }) + .addOnCompleteListener( + getExecutor(), + task -> { + if (task.isSuccessful()) { + WritableMap data = task.getResult(); + WritableMap event = Arguments.createMap(); + event.putMap("data", data); + event.putString("key", key); + event.putString("eventType", eventType); + event.putMap("registration", readableMapToWritableMap(registration)); + + ReactNativeFirebaseEventEmitter emitter = + ReactNativeFirebaseEventEmitter.getSharedInstance(); + + emitter.sendEvent( + new ReactNativeFirebaseDatabaseEvent( + ReactNativeFirebaseDatabaseEvent.EVENT_SYNC, event)); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + // Event arrived after module invalidation shut down an executor. + // Safe to drop when tearing down; no JS listener will consume the event. + } } /** diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java index df0287446b..54a8c3c562 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java @@ -389,16 +389,20 @@ private void handleQueryOnSnapshot( private void handleQueryGet( ReactNativeFirebaseFirestoreQuery firestoreQuery, Source source, Promise promise) { - firestoreQuery - .get(getExecutor(), source) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - promise.resolve(task.getResult()); - } else { - rejectPromiseFirestoreException(promise, task.getException()); - } - }); + try { + firestoreQuery + .get(getExecutor(), source) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + promise.resolve(task.getResult()); + } else { + rejectPromiseFirestoreException(promise, task.getException()); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + rejectPromiseFirestoreException(promise, e); + } } private void sendOnSnapshotEvent( diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java index e22c0b4950..23fe331d5d 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java @@ -77,19 +77,23 @@ public void transactionGetDocument( FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path); - Tasks.call( - getTransactionalExecutor(), - () -> - snapshotToWritableMap( - appName, databaseId, transactionHandler.getDocument(documentReference))) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - promise.resolve(task.getResult()); - } else { - rejectPromiseWithExceptionMap(promise, task.getException()); - } - }); + try { + Tasks.call( + getTransactionalExecutor(), + () -> + snapshotToWritableMap( + appName, databaseId, transactionHandler.getDocument(documentReference))) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + promise.resolve(task.getResult()); + } else { + rejectPromiseWithExceptionMap(promise, task.getException()); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + rejectPromiseWithExceptionMap(promise, e); + } } @ReactMethod