Skip to content

fix(messaging,android): release the FCM run wake lock off the main thread - #18867

Open
SelaseKay wants to merge 2 commits into
mainfrom
fix/messaging-android-wakelock-release-anr
Open

SelaseKay wants to merge 2 commits into
mainfrom
fix/messaging-android-wakelock-release-anr

Conversation

@SelaseKay

@SelaseKay SelaseKay commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Description

On Android, high-priority background messages run through a wakeful JobIntentService. When that service is destroyed, onDestroy runs on the main thread and released the run wake lock there. WakeLock.release() is a Binder call into PowerManager, so a slow system power service becomes an ANR. That matches the Crashlytics stack in #18866.

This PR posts the run-wake-lock release onto a background executor so teardown does not wait on PowerManager. The start-path acquire stays on the main thread; moving it would change the window where enqueueWork can startService relative to the run lock being held. Each processing session gets its own run wake lock, so a delayed release cannot drop a lock acquired for a later message. The launch-lock handoff stays under the same lock as the session flags.

ComponentNameWithWakeful now implements equals/hashCode, so enqueueWork and the service share one CompatWorkEnqueuer. Without that, the launch-lock handoff in serviceProcessingFinished never ran, each high-priority message leaked a 60s launch lock, and sClassWorkEnqueuer grew without bound. The launch-lock acquire on that path has no extra try/catch; it matches enqueueWork. Android unit tests cache ~/.m2/repository/org/robolectric so CI does not re-download the sdk=28 android-all jar on every run.

Related Issues

Fixes #18866

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]).
This will ensure a smooth and quick review process. Updating the pubspec.yaml and changelogs is not required.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • My PR includes unit or integration tests for all changed/updated/fixed behaviors (See Contributor Guide).
  • All existing and new tests are passing.
  • I updated/added relevant documentation (doc comments with ///).
  • The analyzer (melos run analyze) does not report any problems on this PR.
  • I read and followed the Flutter Style Guide.
  • I signed the CLA.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require plugin users to manually update their apps to accommodate your change?

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

Test plan

The ANR is a PowerManager Binder delay during Service.onDestroy. It is not observable through Dart firebase_messaging APIs, and this repo has no FCM-push e2e that can stall IPowerManager.releaseWakeLock. Coverage is a Robolectric unit test that keeps the production executor and uses a WakeLock shadow whose release() blocks, so the old inline release() would hang serviceProcessingFinished / onDestroy:

  1. serviceProcessingFinished returns while the run lock is still held, and release() runs on the executor thread.
  2. A delayed release of session 1's run lock does not drop session 2's run lock.
  3. Finishing while a new enqueue is in flight re-acquires the launch lock, then the next session takes it; a delayed run-lock release does not take it back.
  4. onDestroy returns while the run lock is still held; the lock is released afterwards, and onHandleWork is allowed to finish.
  5. getWorkEnqueuer returns the same instance for the same component and wakeful flag, including the instance used from onCreate.

Ran locally:

cd tests && flutter build apk --debug --config-only
cd android && ./gradlew :firebase_messaging:testDebugUnitTest

JobIntentServiceWakeLockTest: 6 tests, 0 failures.

…read

A slow PowerManager Binder call during background-service teardown was ANRing the main thread after high-priority messages.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Android JobIntentService implementation to release wake locks asynchronously on a background executor, preventing potential Application Not Responding (ANR) issues during service teardown. It also adds Robolectric unit tests to verify this non-blocking behavior. The review feedback suggests using a custom ThreadFactory to name the background threads, which would improve debuggability and thread dump analysis.


// WakeLock.release() is a Binder call into PowerManager. Doing it on the main
// thread during service teardown ANRs when the system power service is slow.
static volatile Executor wakeLockExecutor = Executors.newSingleThreadExecutor();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

It is a best practice to provide a custom ThreadFactory to name background threads. This makes debugging, profiling, and analyzing thread dumps much easier (e.g., identifying which thread is responsible for releasing wake locks).

  static volatile Executor wakeLockExecutor =
      Executors.newSingleThreadExecutor(
          new java.util.concurrent.ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
              Thread thread = new Thread(r, "firebase-messaging-wakelock-release");
              thread.setDaemon(true);
              return thread;
            }
          });

@Lyokone Lyokone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach looks right to me. The run lock is read under the lock, each session gets its own lock, and releases are serialized, so a late release can't drop a later session's lock. Going back to one shared run lock makes delayedReleaseDoesNotDropLaterSessionLock and delayedReleaseDoesNotReacquireLaunchLock fail, as intended.

Main points inline:

  • The start path still does a main-thread acquire.
  • A pre-existing enqueuer-cache bug makes the launch-lock handling unreachable.
  • The tests check the executor hand-off rather than non-blocking.

On the PR description:

  • "Without this change the first two tests time out": without the change the test file doesn't compile, because wakeLockExecutor doesn't exist. With only the field added and the release left inline, all four tests fail, and they fail on the release latches rather than on the condition in each test's name.
  • The numbered test list doesn't match the order of the tests in the file.
  • Item 4 ("finishing while a new enqueue is in flight") can't happen in production today because of the enqueuer-cache issue (see the comment on serviceProcessingFinished).

// as if we had enqueued work without the service running.
if (mLaunchingService) {
try {
mLaunchWakeLock.acquire(60 * 1000);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this try/catch needed? It's unrelated to the ANR, it isn't tested, and the branch can't run today (see above). I'd leave it out of this PR.

Comment thread packages/firebase_messaging/firebase_messaging/android/build.gradle
…ake lock release

ComponentNameWithWakeful lacked equals/hashCode, so enqueueWork and the
service never shared a CompatWorkEnqueuer and the launch-lock handoff
never ran. Tests now stall WakeLock.release on the production executor
instead of swapping it, and CI caches Robolectric's android-all jars.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[firebase_messaging]: Android ANR during background service destruction — main thread blocked in releaseWakeLock

2 participants