Skip to content

feat: [SDK-5492] send sessions requests outside OperationRepo - #2785

Closed
fadi-george wants to merge 1 commit into
new-session-eventsfrom
fadi/sdk-5492
Closed

fadi-george wants to merge 1 commit into
new-session-eventsfrom
fadi/sdk-5492

Conversation

@fadi-george

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Send sessions API requests through their own persisted queue instead of OperationRepo.

Details

Motivation

OperationRepo is one ordered queue, so a failing sessions request blocks every user and subscription operation behind it. The fixes on #2777, #2780 and #2781 each added a session-only special case to that shared queue. Sessions are telemetry: they need persistence and retry, but not the repo's ordering or ID translation.

This replaces the session operations in #2777, #2780 and #2781.

Scope

  • New SessionRequestQueue:
    • Saves each request with its idempotency key before sending; clears it on success or a non-retryable 4xx.
    • Sends one request at a time, oldest ready first. A create waits for backend user and subscription IDs; an update waits for its session's server ID.
    • Unsent updates for a session collapse into one with the highest duration. An end replaces them, and later updates for that session are discarded. Capped at 100 requests (oldest dropped, along with its session's updates).
    • Retries in the same run with exponential backoff and jitter (5s to 5min), honoring a longer Retry-After. Drops after 5 failures the backend answered; offline failures don't count. A 404 for a just-created record is retried.
    • Retries immediately on foreground, and on network return while foregrounded.
    • A request backing off doesn't block other sessions' requests.
  • SessionService enqueues a create on session start and an end on session end. The background job waits up to 30s for the end to send. Existing local-to-backend ID translation also updates queued requests.
  • NewRecordsState methods are synchronized, since the repo and the queue use it from different threads.
  • No changes to OperationRepo.

Known gaps

  • Under identity verification, requests for an anonymous user that's never created sit until the cap evicts them.
  • Login before the anonymous user is created leaves that session's local IDs untranslated (same as the session model today).
  • Persistence uses the same buffered preference writes as OperationRepo.

Testing

Unit testing

  • SessionRequestQueueTests: ordering, coalescing, end handling, orphans, cap, retry with same key, drop after max attempts, non-retryable errors, offline retries, 404 retry window, replay after kill with the same key, and isolation (a stuck or backing-off request doesn't block others).
  • SessionServiceTests: create/end requests, cold start end, timeout, and ID translation forwarding.
  • Full core suite passes (1145 tests). Detekt failures in ThreadUtils, DeviceService and OneSignalImp already exist on new-session-events.

Manual testing

Not tested on a device yet.

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

Sessions API requests get their own persisted queue, so a sessions
outage never delays user or subscription operations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fadi-george
fadi-george requested a review from a team as a code owner October 12, 2026 21:37
@github-actions

Copy link
Copy Markdown
Contributor

📊 Diff Coverage Report

Diff Coverage Report (Changed Lines Only)

Gate: aggregate coverage on changed executable lines must be ≥ 80% (JaCoCo line data for lines touched in the diff).

Changed Files Coverage

  • ⚠️ SessionModule.kt: Not in coverage report (may not be compiled/tested)
  • SessionModel.kt: 3/3 touched executable lines (100.0%) (9 touched lines in diff)
  • SessionService.kt: 51/51 touched executable lines (100.0%) (89 touched lines in diff)
  • SessionRequest.kt: 40/40 touched executable lines (100.0%) (154 touched lines in diff)
  • SessionRequestStore.kt: 2/2 touched executable lines (100.0%) (7 touched lines in diff)
  • SessionRequestQueue.kt: 192/202 touched executable lines (95.0%) (408 touched lines in diff)

Overall (aggregate gate)

288/298 touched executable lines covered (96.6% — requires ≥ 80%)

📥 View workflow run

@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

Claude Opus 5.5, GPT 5.6 Sol, Grok 4.7. 10 files, +1433/−13 = 1446 (all Kotlin source/tests).

Act on (3/3): onCreated removes the persisted create before SessionModel.serverSessionId is written on serial IO. createdSessionIds is process-local. A kill in that window leaves the next end with no server ID and no queued create, so admitUpdate drops it as never-created.

Act on (3/3): The session does not pin appId. Ends use live config.appId; applyResolvedAppId does not clear session_requests. After an app-id change the end is orphaned against the old create, or PATCHed to apps/<new>/sessions/<old>.

Act on (3/3): A create 404 retries only when onesignalId is in the missing window. An existing user with a just-created subscription drops on the first 404; drop then deletes that session's queued end.

Consider (2/3): Local→backend translation is runOnSerialIO after the identity/config persist. A crash leaves queued local IDs; nothing reconciles them on start.

Consider (2/3): endSession runs off serial IO. A focus during the job can end the wrong or newly started session. Grok: isValid = false and activeDuration = 0 persist before the end is stored, so a kill there loses the end.

Consider (Opus): retryNow on every focus burns persisted failedAttempts, so five app opens during a sessions outage drop the queue.

Consider (2/3): A sessions Retry-After still sets HttpClient.delayNewRequestsUntil, so a 429 still blocks OperationRepo.

Consider (Opus): complete() holds the queue lock; BackgroundManager is Unconfined, so enqueueAndWait can resume under that lock.

Consider (Grok): Cap eviction can drop an in-flight create's queued end.

Noted: Author gaps (IV never-ready until cap; login-before-create). Coalesce unused in production (create+end only). 30s wait holds later background services. NewRecordsState process-local/unbounded. Uncapped Retry-After. Consent 0 retries forever. processForever is not restarted if the loop throws outside send.

Dismissed: OEM callback SecurityException (caught). SessionListener TrackSession* dual-write (out of this PR). Translate-under-lock persist order looks consistent.

Open in Web View Automation 

Sent by Cursor Automation: PR Reviews

Comment on lines +267 to +281
private fun onCreated(
create: SessionRequest,
serverSessionId: String,
) {
remove(create)
complete(create.id, sent = true)
newRecordsState.add(serverSessionId)
createdSessionIds[create.localSessionId] = serverSessionId
store.list().filter { !it.isCreate && it.isSameSession(create) }.forEach { it.serverSessionId = serverSessionId }

// Serialized with SessionService, which clears the server ID when the next session starts.
runOnSerialIO {
val session = sessionModelStore.model
if (session.sessionId == create.localSessionId) session.serverSessionId = serverSessionId
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Create success is removed from the store here, then SessionModel.serverSessionId is written later on serial IO. createdSessionIds is process-local. If the process dies in between, the next end has no server ID and no queued create, so admitUpdate discards it as never-created. Persist the local→server mapping (or keep the create) before removing this row.

Logging.debug("SessionService.backgroundRun: Session ended. activeDuration: $activeDuration")

session.isValid = false
val end = if (session.usesSessionsApi) session.toEndRequest(config?.appId) else null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This end uses live config.appId. The session does not pin an app id, and applyResolvedAppId does not clear session_requests. After an app-id change the end either fails isSameSession against the old create and is dropped as orphaned, or PATCHes apps/<new>/sessions/<old>.

ex: BackendException,
) {
val code = ex.statusCode
val missingId = if (request.isCreate) request.onesignalId else request.serverSessionId

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A create 404 is retried only when onesignalId is in the missing window. hasAccessibleIds already waits on both user and subscription. For an existing user with a just-created subscription, the first 404 is dropped and drop then deletes that session's queued end. Check every id the request depends on (subscription for create; session id for update), matching SubscriptionOperationExecutor.

@fadi-george
fadi-george marked this pull request as draft October 12, 2026 22:33
@fadi-george

Copy link
Copy Markdown
Contributor Author

Closing in favor of fixing OperationRepo retry for all ops (SDK-5535).

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