Skip to content

feat: [SDK-5345] age out stale queued Requests to prevent bloat - #1759

Open
nan-li wants to merge 2 commits into
nan/sdk-5314from
nan/sdk-5345
Open

nan-li wants to merge 2 commits into
nan/sdk-5314from
nan/sdk-5345

Conversation

@nan-li

@nan-li nan-li commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Drop queued user Requests past an age limit. SDK-5345.

Details

Motivation

Under Identity Verification a Request that cannot be signed waits in its executor queue with no upper bound, and a property update or custom event delivered months late overwrites newer values or is misattributed.

Scope

  • OSRequestAging holds the limits: Update Properties 90 days, Custom Events 30 days, any Request owned by a user other than the current one 30 days. A current user's alias changes, Create User, Identify User, Fetch Identity By Subscription and the subscription Requests never age out.
  • The property, identity and custom events executors apply the check at uncache and at the start of every flush pass, before prepareForExecution, and rewrite their cache entry when anything is dropped.
  • A custom event waits as a Delta until its user has an onesignal_id, so the custom events executor ages its Deltas too, and a Request built from a Delta keeps the Delta's timestamp.
  • A timestamp ahead of the clock has no age.
  • The three executors take an optional nowProvider so tests can move the clock.
  • Aging applies whether or not Identity Verification is on. A dropped Update Properties also loses the session count, session time and purchases it carried.
  • Subscription Requests are exempt because a late one still describes the device. The Create Subscription success path writing the shared push model without a current-user check predates this PR and is not changed here.

Testing

Unit testing

ExecutorRequestAgingTests covers each limit one day either side, the current-user and future-timestamp exceptions, the Requests that never age out, a Request that crosses 90 days between two flushes, the identity and custom events flush paths, a user switch between flushes, and a custom event Delta that waits for an onesignal_id.

Manual testing

Full OneSignalUserTests bundle run serially on an iPhone 17 Pro simulator. Run on device.

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
  • 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
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

🤖 Generated with Claude Code

@nan-li
nan-li changed the base branch from 5.8-main to nan/sdk-5314 September 28, 2026 16:02
@nan-li nan-li changed the title fix: [SDK-5345] age out stale queued Requests fix: [SDK-5345] JWT age out stale queued Requests Sep 28, 2026
@nan-li nan-li changed the title fix: [SDK-5345] JWT age out stale queued Requests fix: [SDK-5345] JWT - age out stale queued Requests Sep 28, 2026
Update Properties Requests are dropped after 90 days, Custom Events after
30 days, and any Request owned by a user other than the current one after
30 days. Each executor checks at uncache and at the start of every flush
pass, before prepareForExecution, so a Request that ages past its limit
goes even if it could send by then. Create User, Fetch User and the
subscription Requests never age out.
A custom event stays a Delta until its user has an onesignal_id, and the
Request built later started at age zero, so events parked for a token
skipped both limits. The custom events executor now ages its Deltas by
their timestamp at uncache and on every pass, and a Request built from a
Delta keeps the Delta's timestamp.

Tests cover the identity and custom events flush paths, a user switch
between flushes, and the Delta case. The age clamp is gone since a
negative age never passes a positive limit, and the drop comments say
dropped rather than goes.
@nan-li nan-li changed the title fix: [SDK-5345] JWT - age out stale queued Requests feat: [SDK-5345] age out stale queued Requests Sep 28, 2026
@nan-li nan-li changed the title feat: [SDK-5345] age out stale queued Requests feat: [SDK-5345] age out stale queued Requests to prevent bloat Sep 28, 2026
@nan-li
nan-li marked this pull request as ready for review September 28, 2026 21:22

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Multi-model review (Opus 5.5, GPT 5.6 Sol, Grok 4.7) of the 609-line aging change.

Core checks look right: limits, uncache rewrite, Create/Subscription exemptions, and future timestamps having no age.

Act on (3/3)

  1. Property and identity Deltas reset their age when they become Requests. Custom events copy delta.timestamp; the other two keep Date() from OneSignalRequest.init. OSOperationRepo can hold Deltas while requirement == .unknown and across launches, so a months-old tag, session, or alias Delta is sent as age 0 — the late-overwrite case this PR is meant to stop.
  2. A nil owner never takes the 30-day cap (if let owner = owner). Alias Requests also pass typeLimit: nil, so they never age. That covers leftover anonymous work after login and pre-ownerExternalId cache blobs.

Consider (3/3) Tests miss Identify User, Fetch Identity By Subscription, a current-user alias well past 30 days, nil-owner vs an identified current user, and an old property Delta becoming a Request.

Noted Future timestamps stay unbounded until wall time catches up (documented). currentExternalId can briefly be nil during logout. An in-flight sentToClient Request can be logged as dropped while HTTP still completes.

Open in Web View Automation 

Sent by Cursor Automation: PR Reviews

Comment on lines +46 to +51
static func maxAge(owner: String?, typeLimit: TimeInterval?, currentExternalId: String?) -> TimeInterval? {
var limit = typeLimit
if let owner = owner, owner != currentExternalId {
limit = min(limit ?? .infinity, nonCurrentUserRequestMaxAge)
}
return limit

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act on (3/3): a nil owner never takes the 30-day cap. Alias Requests also pass typeLimit: nil, so they never age — leftover anonymous work after login, and cache blobs from before ownerExternalId (documented as nil). Properties still get 90 days instead of 30.

owner != currentExternalId without the if let would apply the cap when the current user is identified and the stamp is nil, while both-nil (anonymous still current) would stay uncapped.

ownerExternalId: combined.ownerExternalId
)
// Aged from the oldest event, not from when its user got an onesignal_id.
request.timestamp = combined.timestamp

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Act on (3/3): this stamp is what property and identity skip. They build OSRequestUpdateProperties / alias Requests from Deltas and leave timestamp as Date(), so a Delta that sat in OSOperationRepo (paused, or requirement == .unknown) or in the executor Delta cache restarts at age 0. Age those Deltas and copy the oldest timestamp the way this executor does.

@nan-li
nan-li requested a review from a team September 28, 2026 21:53
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.

1 participant