Skip to content

Conversation

@vazarkevych
Copy link
Collaborator

@vazarkevych vazarkevych commented Nov 21, 2025

In the old SDK version, the getFeatureValue method was not protected from concurrent access:

  1. No thread synchronization: The getFeatureValue method directly accessed the shared evalContext without any synchronization.

  2. Mutation of shared state: The getEvalContext() method mutated the existing evalContext.stackContext directly:

    evalContext.stackContext = StackContext()  // Mutating shared object
    evalContext.globalContext.features = gbContext.features
  3. Race condition on evaluatedFeatures: When getFeatureValue was called concurrently from different threads, multiple threads simultaneously tried to modify StackContext.evaluatedFeatures (which is a Set<String>). This led to:

    • Simultaneous read and write operations on the same data structure
    • Violation of Set invariants
    • Attempts to access already deallocated memory (EXC_BAD_ACCESS)
  4. No protection during features update: The featuresFetchedSuccessfully method updated evalContext without synchronization, which could happen simultaneously with reads in getFeatureValue.

Solution

In the new version, the following changes were made:

  1. Added serial queue for synchronization:

    private let syncQueue = DispatchQueue(label: "com.growthbook.sdk.sync", qos: .userInitiated)
  2. All operations with shared state wrapped in syncQueue:

    • getFeatureValue uses syncQueue.sync for synchronous access
    • featuresFetchedSuccessfully uses syncQueue.async with [weak self] for asynchronous updates
    • All read/write methods for evalContext and gbContext.features are protected
  3. Creating new context instead of mutation:
    Instead of mutating the existing evalContext, the getEvalContext() method now creates a new EvalContext with a new StackContext():

    let newStackContext = StackContext()  // New object for each call
    let newEvalContext = EvalContext(...)  // New context
    return newEvalContext

    This ensures that each thread works with its own isolated context, avoiding race conditions.

  4. Atomic reading of shared state:
    All reads of evalContext and gbContext.features occur atomically within syncQueue.sync, preventing situations where one thread reads data while another modifies it.

# Conflicts:
#	Sources/CommonMain/GrowthBookSDK.swift
Copy link

@tungfam tungfam left a comment

Choose a reason for hiding this comment

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

Sorry for the delays! Thank you!

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