Skip to content

fix(app, android): discard tasks posted to shut-down transactional executors - #9205

Open
ottob wants to merge 2 commits into
invertase:mainfrom
ottob:fix/task-executor-shutdown-rejection
Open

fix(app, android): discard tasks posted to shut-down transactional executors#9205
ottob wants to merge 2 commits into
invertase:mainfrom
ottob:fix/task-executor-shutdown-rejection

Conversation

@ottob

@ottob ottob commented Aug 20, 2026

Copy link
Copy Markdown

Description

TaskExecutorService.getNewExecutor() returns a bare Executors.newSingleThreadExecutor() for transactional executors. When ReactNativeFirebaseModule.invalidate() runs shutdownNow() while a play-services Task is still in flight — an App Check Play Integrity attestation or an Auth network call racing React instance teardown (app swiped away, headless task ending, reload) — the Task's completion listener posts its continuation to the now-dead executor. The default AbortPolicy throws an uncaught RejectedExecutionException on a gms pool thread and crashes the process:

Exception java.util.concurrent.RejectedExecutionException:
  at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution (ThreadPoolExecutor.java:2050)
  at java.util.concurrent.ThreadPoolExecutor.reject (ThreadPoolExecutor.java:797)
  at java.util.concurrent.ThreadPoolExecutor.execute (ThreadPoolExecutor.java:1338)
  at java.util.concurrent.Executors$DelegatedExecutorService.execute (Executors.java:643)
  at com.google.android.gms.tasks.zzj.zza (play-services-tasks@@18.4.0)
  at com.google.android.gms.tasks.zzr.zzb
  at com.google.android.gms.tasks.zzw.zzc
  at com.google.android.gms.tasks.zzx.run

d70520d ("catch RejectedExecutionException on executor-backed Tasks") guarded the database/firestore call sites, but the generic path through any module's transactional executor is still exposed — we see this in production via app-check/auth, where no Tasks.call wrapper exists to catch it (the throw happens inside play-services internals, not in RNFB code).

This PR replaces the bare single-thread executor with an equivalent ThreadPoolExecutor(1, 1, 0ms, LinkedBlockingQueue) carrying a RejectedExecutionHandler that silently discards work arriving after shutdown — the same treatment executeInFallback already gives the pooled executor. FIFO single-thread semantics are unchanged; a rejection on a live executor (unreachable with an unbounded queue) still throws to preserve AbortPolicy behaviour. Discarded continuations belong to a React instance that no longer exists, so nothing awaits them.

Related issues

Related: #4047, #4118 (long-standing reports of this crash class). Follow-up to d70520d.

Release Summary

fix(app, android): discard tasks posted to shut-down transactional executors instead of crashing with RejectedExecutionException

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
  • My change includes tests;
    • e2e tests added or updated in packages/\*\*/e2e
    • jest tests added or updated in packages/\*\*/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • No

Test Plan

We hit this crash in production (Google Play Console, Android vitals) in an app using only app, app-check (Play Integrity provider) and auth — reproduced by killing the app immediately after launch while an App Check attestation is in flight. No automated test is included: the race needs a live play-services Task completing after invalidate(), which isn't reachable from jest, and e2e teardown timing is nondeterministic. The change is semantically equivalent to the existing pooled-executor rejection handling. We're shipping this same change as a local patch via patch-package/bun patch and will report back with before/after crash-rate data from Play Console.

…ecutors

The transactional executor returned by TaskExecutorService is a bare
Executors.newSingleThreadExecutor(), so when module invalidation calls
shutdownNow() while a play-services Task is still in flight (App Check
attestation, Auth network calls racing React instance teardown), the
Task's completion listener posts to the dead executor and the default
AbortPolicy crashes the process with an uncaught
RejectedExecutionException thrown on a gms pool thread.

d70520d guarded the database/firestore call sites, but the generic path
through any module's transactional executor is still exposed. Give the
transactional executor the same discard-after-shutdown behaviour the
pooled executor already has via its RejectedExecutionHandler, keeping
single-thread FIFO semantics identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Aug 20, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@russellwheatley

Copy link
Copy Markdown
Member

@ottob -

This seems right to me. The transactional executor is the generic path d70520d couldn't cover, and swapping Executors.newSingleThreadExecutor() for an equivalent pool with a discard-after-shutdown handler matches what executeInFallback already does for the pooled executor.

A couple of things that are worth updating:

  1. We do have Java unit tests. A Robolectric unit test around TaskExecutorService itself. The full App Check / Auth race isn't something e2e can pin down, but the executor behaviour is: get a transactional executor, shutdown(), submit work, assert it does not throw. Same package as packages/app/android/src/test/java/io/invertase/firebase/common/ReactNativeFirebaseEventEmitterTest.java.
  2. Collapse the rejection-handler guard to executor.isShutdown(). isTerminated() / isTerminating() are already true only after shutdown, so the extra checks don't change anything. Please do that in both discardAfterShutdown and the existing executeInFallback handler, and add a short comment so the next person doesn't re-expand it.

Providing CI goes green (I've just ran it) and @mikehardy doesn't have any further changes he'd like to see, this looks good to me.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.89%. Comparing base (12a52d4) to head (bb82650).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9205      +/-   ##
============================================
+ Coverage     68.41%   69.89%   +1.49%     
============================================
  Files           516      377     -139     
  Lines         37853    26996   -10857     
  Branches       5183     3632    -1551     
============================================
- Hits          25892    18867    -7025     
+ Misses        10176     7057    -3119     
+ Partials       1785     1072     -713     
Flag Coverage Δ
android-native ?
e2e-ts-android ?
e2e-ts-ios 53.51% <ø> (ø)
e2e-ts-macos 49.67% <ø> (-<0.01%) ⬇️
ios-native 68.78% <ø> (-<0.01%) ⬇️
ios-ruby 100.00% <ø> (ø)
jest 45.48% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ction guards

Collapse both rejection handlers' guards to isShutdown() - it remains
true through the terminating and terminated states, so the extra checks
were redundant - and add Robolectric coverage: work submitted before
shutdown runs, and work submitted after shutdown is discarded without
throwing on both the transactional and pooled executors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ottob

ottob commented Aug 20, 2026

Copy link
Copy Markdown
Author

Thanks for the review @russellwheatley — both addressed in bb82650:

  1. Added TaskExecutorServiceTest (Robolectric, same package as ReactNativeFirebaseEventEmitterTest): work submitted before shutdown runs; work submitted after shutdown is discarded without throwing, covered for both the transactional executor (discardAfterShutdown) and the pooled executor (executeInFallback's guard).
  2. Collapsed both handlers' guards to executor.isShutdown() with a comment noting it stays true through the terminating/terminated states.

One honest caveat: I couldn't run the Gradle test suite locally (no JDK on this machine — we consume RNFB through Expo/EAS), so I'm relying on your CI run for execution. Happy to adjust if anything's red.

@mikehardy mikehardy self-assigned this Aug 20, 2026
@mikehardy mikehardy added Workflow: Needs Review Pending feedback or review from a maintainer. and removed Needs Attention labels Aug 20, 2026
@mikehardy

Copy link
Copy Markdown
Collaborator

I can pick up the testing work - the unit test infra is very new, I just put it in there, and I've got a JDK on the machine I'll be doing final review / qualification on already - so I'll shepherd this to merge - thanks for the fix!

@mikehardy

mikehardy commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

The CI android test problem is in fact related to the newness of the Java unit test infrastructure here - I have a fix coming for it in unrelated PR #9093 - I will merge that when ready, then rebase this PR and push it back out with the 9093 fix plus some small tweaks here

The shape of your fix is correct though and if you are already using it locally it should work fine. We'll have it merged here and released before too long though

Thanks for the PR!

@mikehardy mikehardy added the blocked: do-not-merge Do not merge this issue without approval by the person who labelled this issue as Do Not Merge label Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked: do-not-merge Do not merge this issue without approval by the person who labelled this issue as Do Not Merge Workflow: Needs Review Pending feedback or review from a maintainer.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants