Skip to content

fix: [SDK-4874] single-flight UpdateSubscription to stop in-flight PATCH races#1689

Merged
nan-li merged 3 commits into
mainfrom
nan/sdk-4874-inflight-request
Jul 20, 2026
Merged

fix: [SDK-4874] single-flight UpdateSubscription to stop in-flight PATCH races#1689
nan-li merged 3 commits into
mainfrom
nan/sdk-4874-inflight-request

Conversation

@nan-li

@nan-li nan-li commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Serialize UpdateSubscription PATCHes per subscription so an in-flight stale -19 cannot race a later grant and leave users Never Subscribed.

Details

Motivation

#1687 fixed the pending stale UpdateSubscription case by refreshing the PATCH body from the live model before send and coalescing unsent updates. A residual window remains: if a pre-permission update (enabled: false, notification_types: -19) is already on the wire when the user accepts, a concurrent post-grant PATCH can reorder on the server and stick the subscription in Never Subscribed.

This follow-up closes that window for the escalating SDK-4874 customer symptom. Related: SDK-4874.

Scope

  • At most one in-flight OSRequestUpdateSubscription per subscription model; later updates stay queued (existing coalesce) and are drained after success or non-retryable failure.
  • On retryable failure, clear sentToClient so the single-flight gate does not block the model until app restart.
  • Test-only: MockOneSignalClient can hold responses mid-flight.
  • Does not change public APIs or skip sending -19. Broader OperationRepo queue-safety refactor tracked separately in SDK-4892.

Testing

Unit testing

Extended SubscriptionUpdateRaceTests:

  • In-flight -19 blocks the grant follow-up until the first response completes, then sends live subscribed state.
  • Retryable 500 does not leave the gate locked; a later grant update still sends.

Ran OneSignalUserTests/SubscriptionUpdateRaceTests locally (all pass).

Manual testing

Covered by unit tests with held mock responses. Full CI pending on this PR.

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
    • If it is hard to explain how any codes changes are related to each other then it most likely needs to be more than one PR
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
    • Local SubscriptionUpdateRaceTests pass; full CI pending on this PR.
  • I have personally tested this on my device, or explained why that is not possible
    • Unit coverage with mid-flight held responses; device retest not required for this follow-up.

Final pass

  • Code is as readable as possible.
    • Simplify with less code, followed by splitting up code into well named functions and variables, followed by adding comments to the code.
  • I have reviewed this PR myself, ensuring it meets each checklist item
    • WIP (Work In Progress) is ok, but explain what is still in progress and what you would like feedback on. Start the PR title with "WIP" to indicate this.

Made with Cursor

…TCH races

Gate concurrent updates per subscription model, drain pending after completion, and clear sentToClient on retryable failure so the gate does not stick.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nan-li
nan-li requested review from a team, fadi-george and sherwinski July 20, 2026 21:37
@fadi-george

Copy link
Copy Markdown
Collaborator

The single-flight fix looks good overall. A few concerns:

  • Retryable failures may cause repeated retries without proper backoff.
  • Draining the next request may reorder RYW token updates or use the wrong background state.
  • The mock hold/release logic has a race that can strand callbacks.

Could we address or add coverage for these cases?

nan-li and others added 2 commits July 20, 2026 15:24
Keep RYW token updates on the executor queue ahead of single-flight drain so chained PATCHes don't let an older token overwrite a newer one.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hold check and enqueue share one lock, and releaseHeldResponses clears the hold flag so in-flight test callbacks cannot be stranded.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nan-li

nan-li commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author
  • Retryable failures may cause repeated retries without proper backoff.

On retryable failure the new code only clears sentToClient and does not drain. Retry still waits for the next OperationRepo flush (POLL_INTERVAL_MS = 5s production). That matches other executors.

  • Draining the next request may reorder RYW token updates or use the wrong background state.

RYW ordering is a nit, IAM only needs a “we have a subscription-update token for this onesignalId” to unblock, any successful PATCH’s token is enough. We can still clean it up for clarity.

  • The mock hold/release logic has a race that can strand callbacks.

This is valid, so we will fix it underr one lock: check flag + enqueue

Pushed 2 commits to address (2) and (3)

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.

2 participants