Skip to content

BillingClient reconnection permanently stops when the service disconnects during an in-flight connection attempt; queued requests and getOfferings pend forever #3922

Description

@aelaguiz

Describe the bug

If the Play billing service disconnects while a BillingClient.startConnection() attempt is already in flight, the SDK's reconnection ladder is never re-armed and never runs again. From that state:

  • The in-flight connection attempt is killed with the service, so onBillingSetupFinished(OK) never arrives for it.
  • A concurrent startConnection call during that window gets response code 5 ("Client is already in the process of connecting to billing service."). BillingWrapper.onBillingSetupFinished handles code 5 (DEVELOPER_ERROR) with a deliberate no-op ("Billing service is already trying to connect. Don't do anything."), relying on the in-flight attempt to complete. It never does.
  • BillingWrapper.onBillingServiceDisconnected() only logs and tracks a diagnostics event. It schedules nothing.

Result: no code path ever calls startConnection again. The exponential backoff machinery (RECONNECT_TIMER_START_MILLISECONDS = 1s with the 15 minute cap in BillingWrapper, RETRY_TIMER_START = 878ms in BillingClientUseCase) never engages because nothing schedules a retry from this state. Queued service requests (pending_request_count=2 in the SDK's own diagnostics) wait indefinitely, so Purchases.getOfferings() pends silently: no error, no timeout, no retry, against a Play Store that is healthy again seconds later. The only release we have observed is an app background/foreground cycle, whose lifecycle hook calls startConnection and drains the queue within seconds.

We hit this in production behind a paywall: getOfferings() pends indefinitely with no callback, the user sees a spinner forever. The likely production trigger is Play Store churn (for example a Play self-update) while a billing operation is in flight.

  1. Environment

    1. Platform: Android. We consume the SDK from a Flutter app in production via purchases_flutter 10.8.0 / purchases-hybrid-common 18.29.0, but the defect is entirely in the native Android SDK and reproduces at the native layer.
    2. SDK version: reproduced identically on purchases-android 10.16.0 and 10.16.1 (BillingClient 8.3.0).
    3. OS version: Android 17 (physical Google Pixel 10a, build CP2A.260705.006) and Android 16 (stock emulator with a Google Play image, no Google account signed in; the wedge is store-account-independent).
    4. Android Studio version: n/a (Gradle CLI builds).
    5. How widespread: low frequency, high severity. Our fleet telemetry shows roughly one silent paywall stall per week in this failure class. Once wedged, the device does not recover until the app is backgrounded and foregrounded.
  2. Debug logs

Verbose SDK logs are forwarded through our Flutter log handler; the payloads are the SDK's own lines. Wedge anatomy from the physical Pixel 10a on 10.16.0 (device local time):

08-08 08:44:06.980  RC debug: Starting connection for com.android.billingclient.api.BillingClientImpl@3182b3c
08-08 08:44:07.008  ActivityManager: Start proc 10200:com.android.vending ... for bound-service {com.android.vending/com.google.android.finsky.billing.iab.InAppBillingService}
08-08 08:44:07.073  W/BillingClient: Client is already in the process of connecting to billing service.
08-08 08:44:07.074  RC debug: Starting connection for com.android.billingclient.api.BillingClientImpl@3182b3c
08-08 08:44:07.132  ActivityManager: Force stopping service ServiceRecord{... com.android.vending/com.google.android.finsky.billing.iab.InAppBillingService}
08-08 08:44:07.137  W/BillingClient: Billing service died.
08-08 08:44:08.092  RC diagnostics: google_billing_setup_finished {"billing_response_code":5,"billing_debug_message":"Client is already in the process of connecting to billing service.","pending_request_count":2}
08-08 08:44:08.092  RC warn: Billing Service disconnected for com.android.billingclient.api.BillingClientImpl@3182b3c

Then nothing. Zero "Starting connection" lines from 08:44:09 through 08:57:08 (grep-proven across the full logcat), even though the force-stop loop ended at ~08:44:56 and the Play Store was healthy from ~08:45:40. The pending getOfferings sat silent for 13m03s. At 08:57:09 we backgrounded and foregrounded the app:

08-08 08:57:09.450  RC debug: Starting connection for com.android.billingclient.api.BillingClientImpl@3182b3c
08-08 08:57:09.665  RC debug: Billing Service Setup finished for com.android.billingclient.api.BillingClientImpl@3182b3c

and the queued work completed about 3 seconds later.

The same anatomy reproduced on the emulator on both 10.16.0 and 10.16.1. Example from 10.16.1:

08-08 11:51:04.654  RC debug: Starting connection for com.android.billingclient.api.BillingClientImpl@a01c04a
08-08 11:51:04.873  ActivityManager: Force stopping service ... InAppBillingService
08-08 11:51:04.905  W/BillingClient: Billing service died.
08-08 11:51:04.928  RC warn: Billing Service disconnected for com.android.billingclient.api.BillingClientImpl@a01c04a
[zero further "Starting connection" lines until a background/foreground at 11:54:15]
  1. Steps to reproduce, with a description of expected vs. actual behavior

Reproduces on a stock Android emulator with a Google Play system image and no Google account, so it should be easy to run anywhere:

  1. Configure the SDK with verbose logs and open a screen that calls Purchases.getOfferings().
  2. Simulate sustained Play Store churn from the host: run adb shell am force-stop com.android.vending in a loop every ~300ms for ~110 seconds.
  3. While the loop is running, trigger a getOfferings() call so the SDK starts a billing connection.
  4. The wedge forms when the loop kills the billing service during an in-flight connection attempt. Signature: "Starting connection", then code 5 "already in the process of connecting" with pending_request_count > 0 in the google_billing_setup_finished diagnostics entry, then "Billing service died", then permanent silence.
  5. Stop the loop. Wait several minutes with a healthy Play Store.
  6. Background and foreground the app.

Expected: after the service churn ends, the exponential backoff reconnect ladder re-establishes the connection within seconds to minutes, and the pending getOfferings completes or fails with an error.

Actual: no reconnection attempt is ever scheduled. getOfferings() pends silently and indefinitely (observed 13m03s on the Pixel, and 201.8s / 192.7s / 203.3s on three emulator runs where the pend length was simply how long we waited before foregrounding). The lifecycle foreground handler is the only thing that ever calls startConnection again.

Timing note: a single force-stop does not reproduce this (the SDK rebinds in ~400ms). The disconnect has to land inside the in-flight connection window, which the sustained loop makes reliable (3 out of 3 attempts across emulator runs, plus the Pixel run).

  1. Other information

Our reading of the current code, consistent with all observed logs:

  • BillingWrapper.onBillingServiceDisconnected() is a no-op apart from logging and diagnostics, so a disconnect never re-arms the retry ladder by itself.
  • The DEVELOPER_ERROR (code 5) branch of onBillingSetupFinished intentionally does nothing, trusting the in-flight attempt to deliver a terminal callback. When the service dies mid-connect, BillingClient never delivers that callback, so the trust is misplaced and both paths drop the ball.
  • retryBillingServiceConnectionWithExponentialBackoff() is only invoked from terminal onBillingSetupFinished error codes, which never arrive in this scenario.

We have a fix in progress and intend to submit a PR (re-arming reconnection when a disconnect lands while requests are pending / a connection attempt is in flight).

Related: we found a second, independent way getOfferings() pends forever (HTTP layer, hung socket not bounded by any timeout) while investigating the same production stall. Filed separately as #3923.

Additional context

Production impact for us: a paywall that silently never loads offerings, with no error surfaced to the app, so the app cannot even show a retry affordance based on SDK signals. The pending_request_count growth in consecutive diagnostics entries (2, then 4, then 6 in our logs as more calls queued) makes the wedge visible in the SDK's own diagnostics stream.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions