Skip to content

fix: concurrency groundwork for the multi-user client and features repository - #234

Open
madhuchavva wants to merge 9 commits into
mainfrom
mc/concurrency-groundwork
Open

madhuchavva wants to merge 9 commits into
mainfrom
mc/concurrency-groundwork

Conversation

@madhuchavva

@madhuchavva madhuchavva commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Concurrency groundwork for the multi-user client and features repository

Summary

  • Fixes for thread-safety bugs
  • adds a compile-time guard against post-Java-8 API usage.
  • This is the groundwork PR for the async client work. No API changes.

What's included

  • InMemoryStickyBucketServiceImpl — new no-arg constructor backed by ConcurrentHashMap, used by Options.setInMemoryStickyBucketService(). The previous plain HashMap is a structural data race (not just a lost update) when one client's concurrent evaluations save assignments in parallel. The map-injecting constructor is unchanged; its Javadoc now states the thread-safety requirement.
  • GrowthBookClientassigned is now a ConcurrentHashMap and callbacks a CopyOnWriteArrayList (both were plain collections mutated per run()/subscribe()). The assigned-variation change-check and publish in fireSubscriptions are now one atomic compute step, so two concurrent run() calls can no longer both observe a stale value and double-fire subscriptions; callbacks still run outside the map lock.
  • GBFeaturesRepository callback dispatchrefreshCallbacks was a plain ArrayList: adds were synchronized but the dispatch iteration on the poll/SSE/retry background threads was not, so registering a callback during a refresh could throw ConcurrentModificationException. Now CopyOnWriteArrayList (the fix NativeJavaGbFeatureRepository already uses).
  • GBFeaturesRepository polling thread — the SWR poll scheduler used the default (non-daemon) thread factory: the only non-daemon thread in the SDK, and it keeps the JVM alive if an application exits without calling shutdown(). Now a named daemon thread (growthbook-feature-poll), matching the SSE retry factory.
  • FeatureSnapshotfeaturesJson/savedGroupsJson/parsedFeatures/parsedSavedGroups were four independently assigned volatile fields, read separately by GrowthBookClient.buildGlobalContext, so a refresh landing between two reads produced a context with new features + old saved groups. They are now captured together in one immutable FeatureSnapshot swapped through a single AtomicReference; existing getters delegate, and buildGlobalContext reads the snapshot once. New public getFeatureSnapshot().

What's NOT changed:

  • Legacy single-user GrowthBook internals (single-threaded by design; ).

This branch has not been deployed

No deployments
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