Skip to content

[in_app_purchase_android] Defer reconnect off the platform-message upcall to fix an uncatchable SIGABRT - #12639

Open
eszter-varga wants to merge 1 commit into
flutter:mainfrom
eszter-varga:in-app-purchase-android-reconnect-defer
Open

eszter-varga wants to merge 1 commit into
flutter:mainfrom
eszter-varga:in-app-purchase-android-reconnect-defer

Conversation

@eszter-varga

@eszter-varga eszter-varga commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

BillingClientManager._connect issues startConnection synchronously from
inside the onBillingServiceDisconnected callback. That callback runs inside
an inbound platform-message JNI upcall, so the outbound startConnection send
happens re-entrantly on the same stack. When the billing service is flapping,
the Java side has a pending exception when the engine runs
FML_CHECK(CheckException(env)) in FlutterViewHandlePlatformMessage, and
the process aborts (SIGABRT) — uncatchable from Dart
(flutter/flutter#144954).

This PR yields one macrotask (await Future<void>.delayed(Duration.zero))
before the platform send, so startConnection always runs on a fresh task off
the upcall. A microtask would not suffice: microtasks drain before control
returns to the engine, so they do not escape the JNI upcall. _readyFuture is
still assigned synchronously, so runWithClient callers are unaffected aside
from a one-turn deferral; disposal during the deferral is re-checked before
sending.

Existing tests that asserted a synchronous reconnect now pump the event queue;
a new regression test asserts the send is not issued synchronously on the
disconnect callback.

Developed with AI assistance; tested and verified by the submitter.

Fixes flutter/flutter#191831

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2

@google-cla

google-cla Bot commented Aug 26, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@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 in_app_purchase_android package to version 0.5.3. It defers the billing client reconnection logic to a macrotask using Future.delayed(Duration.zero) inside BillingClientManager to prevent a synchronous outbound platform message from within the inbound onBillingServiceDisconnected callback. Additionally, the package's tests are updated to handle this asynchronous delay by using pumpEventQueue, and a regression test is added to verify the deferred reconnection behavior. There are no review comments, so no additional feedback is provided.

@eszter-varga
eszter-varga force-pushed the in-app-purchase-android-reconnect-defer branch from bd45782 to e251d29 Compare August 26, 2026 20:09
@eszter-varga
eszter-varga force-pushed the in-app-purchase-android-reconnect-defer branch from e251d29 to dc21f9d Compare August 28, 2026 12:55
@stuartmorgan-g stuartmorgan-g added the triage-android Should be looked at in Android triage label Sep 1, 2026
@jesswrd jesswrd added the CICD Run CI/CD label Sep 1, 2026
auto-submit Bot pushed a commit that referenced this pull request Oct 13, 2026
…cate listener responses (#12640)

The Play Billing library can invoke the response listener of a single async
call more than once (observed in production via its internal watchdog/timeout
path, `zzaf.run → zzan.run`, with the real response also arriving). A pigeon
reply may only be submitted once; the second completion throws
`IllegalStateException: Reply already submitted` on the main thread and kills
the process.

`startConnection` already guards against a double `onBillingSetupFinished`
with its `alreadyFinished` flag; this PR extends the same protection to the
other 9 async methods via a `replyOnce(methodName, callback)` wrapper — an
`AtomicBoolean.getAndSet(true)` single-shot around the pigeon callback. The
first completion wins; duplicates are dropped with a `Log.d`. Call sites bind
the pigeon parameter as `rawCallback` and rebind `callback` to the wrapper, so
the method bodies are otherwise unchanged and every completion path
(null-client error, null-activity error, listener response, RuntimeException
catch) is single-shot. Listener side effects (`updateCachedProducts`) still
run on dropped duplicates, so if an error response lands first and the real
one second, the product cache still receives real data for a later
`launchBillingFlow`.

Two JUnit regression tests fire a captured listener twice with distinct
results and assert exactly one pigeon completion holding the first result.

*Developed with AI assistance; tested and verified by the submitter.*

Fixes flutter/flutter#191832
Related: flutter/flutter#103432, flutter/flutter#134967

## Pre-Review Checklist

Note: this PR and #12639 both bump the package to 0.5.3; whichever lands
second will be rebased and re-bumped.

[^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
…call to fix an uncatchable SIGABRT

BillingClientManager._connect issued startConnection synchronously from
inside the onBillingServiceDisconnected callback, which runs on an inbound
platform-message JNI upcall; a pending Java exception at that moment trips
FML_CHECK in FlutterViewHandlePlatformMessage and aborts the process,
uncatchably from Dart. Yield one macrotask before the platform send so it
always lands on a fresh task off the upcall.

Claude-Session: https://claude.ai/code/session_012juAFafaPDBSvR7pcqTLMd
@eszter-varga
eszter-varga force-pushed the in-app-purchase-android-reconnect-defer branch from dc21f9d to 4dd2767 Compare October 18, 2026 22:36
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 18, 2026
@eszter-varga

eszter-varga commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Rebased onto main (now versioned 0.5.5). The only conflict was the CHANGELOG.md entry after #12640 landed as 0.5.4; the code change is unchanged — one macrotask yield before startConnection, so the reconnect never runs synchronously inside the onBillingServiceDisconnected platform-message upcall (flutter/flutter#191831). Locally: package tests 82/82, flutter analyze and dart format clean.

@gmackall @camsim99 — you reviewed the sibling #12640 (the duplicate-reply guard, released in 0.5.4); could one of you take a look at this one too, and re-apply the CICD label? Thanks!

@gmackall gmackall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm a bit skeptical that deferring _connect by Duration.zero will prevent this SIGABRT. While Dart does invoke FlutterViewHandlePlatformMessage synchronously inside onBillingServiceDisconnected, the Java side of startConnection isn't actually executed synchronously there: DartMessenger.handleMessageFromDart already defers the channel handler to a later Looper turn via PlatformTaskQueue.dispatch (handler.post). Since Java can't enter the inbound JNI call with an already-pending exception, running FlutterViewHandlePlatformMessage one macrotask later on the same thread seems likely to hit the same CheckException failure.

That said, the deferral and _isDisposed check themselves seem harmless, so I'm fine landing this if we either:

  1. Remove the SIGABRT code comments in billing_client_manager.dart / billing_client_manager_test.dart (and update the CHANGELOG/PR description so it doesn't claim to fix the SIGABRT), or
  2. Have reproduction/logcat evidence (e.g., the Java exception logged by CheckException right before the abort) showing how deferring off the upcall prevents the SIGABRT.

@gmackall gmackall added the CICD Run CI/CD label Oct 20, 2026
@flutter-dashboard

Copy link
Copy Markdown

This pull request is not mergeable in its current state, likely because of a merge conflict. Pre-submit CI jobs were not triggered. Pushing a new commit to this branch that resolves the issue will result in pre-submit jobs being scheduled.

@tarrinneal tarrinneal added the waiting for response The Flutter team cannot make further progress on this PR until the author responds label Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD p: in_app_purchase platform-android triage-android Should be looked at in Android triage waiting for response The Flutter team cannot make further progress on this PR until the author responds

Projects

None yet

5 participants