Skip to content

Fix ConcurrentModificationException in decide() when setAttribute() is called concurrently (BUG-8620)#529

Draft
Copilot wants to merge 1 commit into
masterfrom
copilot/bug-8620-investigate-issue
Draft

Fix ConcurrentModificationException in decide() when setAttribute() is called concurrently (BUG-8620)#529
Copilot wants to merge 1 commit into
masterfrom
copilot/bug-8620-investigate-issue

Conversation

Copy link
Copy Markdown

Copilot AI commented May 14, 2026

Summary

Fixes BUG-8620 / GitHub issue #507: a ConcurrentModificationException thrown from OptimizelyUserContext.decide() when setAttribute() is called from another thread concurrently.

Root Cause

The exception originates in this call chain:

decide() → copy() → new OptimizelyUserContext(...) → new HashMap<>(attributes)

attributes is stored as a Collections.synchronizedMap. That wrapper makes individual operations (put/get/remove) thread-safe, but bulk iteration (such as the copy-constructor new HashMap<>(attributes)) is explicitly NOT covered — it requires the caller to hold the map's mutex during the iteration.

When setAttribute() runs on another thread at the same time as copy(), the iterator detects the concurrent modification and throws ConcurrentModificationException.

The same latent hazard exists for qualifiedSegments, which is stored as a Collections.synchronizedList.

Fix

Override copy() in OptimizelyUserContextAndroid to hold the synchronized-map/list mutex for the entire duration of super.copy():

@Override
public OptimizelyUserContext copy() {
    Map<String, Object> attrs = getAttributes();
    List<String> segs = getQualifiedSegments();
    synchronized (attrs) {
        if (segs == null) {
            return super.copy();
        }
        synchronized (segs) {
            return super.copy();
        }
    }
}

Collections.synchronizedMap uses the wrapper object itself as its mutex (mutex = this). So synchronized(getAttributes()) acquires exactly the same lock that setAttribute() uses, blocking any concurrent modification for the duration of the copy.

Changes

  • OptimizelyUserContextAndroid.java — adds copy() override with proper synchronization
  • OptimizelyUserContextAndroidTest.java — adds concurrent regression test (testCopy_noConcurrentModificationExceptionWithConcurrentSetAttribute) that runs 10 writer threads calling setAttribute() and 10 reader threads calling copy() simultaneously
  • CHANGELOG.md — documents the fix under 5.2.0

…ute() is called concurrently (BUG-8620)

Agent-Logs-Url: https://github.com/optimizely/android-sdk/sessions/9734d248-0b4a-4815-a88f-e54529087a55

Co-authored-by: muzahidul-opti <129880873+muzahidul-opti@users.noreply.github.com>
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