Skip to content

[SDK-499] Synchronize EmbeddedSessionManager to fix embedded session races - #1083

Open
franco-zalamena-iterable wants to merge 6 commits into
masterfrom
feature/SDK-499-embedded-session-thread-safety
Open

[SDK-499] Synchronize EmbeddedSessionManager to fix embedded session races#1083
franco-zalamena-iterable wants to merge 6 commits into
masterfrom
feature/SDK-499-embedded-session-thread-safety

Conversation

@franco-zalamena-iterable

@franco-zalamena-iterable franco-zalamena-iterable commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

📝 Summary

Makes EmbeddedSessionManager internally thread-safe, fixing a reported NullPointerException plus two further races in the same class.

🎟️ Jira Ticket: SDK-499

📖 Description

EmbeddedSessionManager had no synchronization at all, and its impression API has no SDK-internal callers — it is driven entirely by integrator code, so it can be entered from any thread. Three races: the reported NPE in updateDisplayCountAndDuration() (null-check then !! on start, which pauseImpression() nulls in between), concurrent modification of the impression map, and duplicate session tracking when endSession() raced with itself.

All access to session, impressions and the impression fields now goes through one private lock. A single class-wide lock instead of a concurrent map because the invariants span several fields — endSession() has to end all impressions, snapshot the list and reset both fields as one step. trackEmbeddedSession() is called after the lock is released, since it re-enters IterableApi and integrator code.

endSession() still leaves the session running when there are no impressions. That is pre-existing behavior, changing it changes what gets reported, so it is tracked separately in SDK-701. A test pins the current behavior so that change is explicit when SDK-701 lands.

No public API change and no behavior change on the single-threaded path.

This development was started by contributor @Shamyyoun

🧪 How to test?

./gradlew :iterableapi:testDebugUnitTest --tests "com.iterable.iterableapi.EmbeddedSessionManagerThreadSafetyTest"

concurrentSessionAndImpressionUpdatesDoNotThrow races 8 threads over the session and impression API. Reverting the two source files to master reproduces the reported crash:

java.lang.AssertionError: concurrent access failed: [java.lang.NullPointerException] expected:<0> but was:<1>

Full iterableapi suite passed locally.

🧾 Changelog

  • Fixed a NullPointerException in EmbeddedSessionManager.updateDisplayCountAndDuration() that could crash apps calling embedded session methods off the main thread. EmbeddedSessionManager is now internally synchronized, which also fixes concurrent modification of its impression map and duplicate session tracking when endSession() raced with itself. Thanks to @Shamyyoun for the report and initial fix.

📹 Loom recording if applicable

N/A

🐞 Github Issues solved

Addresses #1052, supersedes #1053. Deliberately not using a closing keyword yet so merging doesn't auto-close the issue before we reply to the reporter.

📚 Docs PR if applicable

N/A

Shamyyoun and others added 5 commits August 21, 2026 17:54
The NPE fix on start closed the reported crash but left the rest of the
class open to the same threading. Callers reach EmbeddedSessionManager
from arbitrary threads (issue #1052 reports Dispatchers.Default), and
impressions is a plain LinkedHashMap: startImpression writes to it while
endSession iterates it and then reassigns the field, so concurrent use
could also throw ConcurrentModificationException or lose entries. The
session field had the same check-then-act shape as start — two threads
could both pass isTracking() and each track a session.

Guard every access to impressions, session, and the impression fields
with one private lock. The per-impression synchronized block is now
redundant and removed; @volatile on start stays as visibility defence,
but the class lock is the invariant. trackEmbeddedSession runs after the
lock is released, since it calls back into IterableApi.

endSession keeps its existing behaviour of doing nothing — not even
resetting the session — when there are no impressions.

Adds a test racing 8 threads over startImpression, pauseImpression,
startSession and endSession, which reproduces the reported
NullPointerException without this change, plus two tests pinning the
endSession-with-no-impressions behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@franco-zalamena-iterable
franco-zalamena-iterable requested a review from a team as a code owner August 24, 2026 11:21
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.

NullPointerException in EmbeddedSessionManager.updateDisplayCountAndDuration() — thread-safety race condition

2 participants