feat: thread-safe user attribute update tracking and safe reading/updating of evaluations#242
feat: thread-safe user attribute update tracking and safe reading/updating of evaluations#242duyhungtnn wants to merge 16 commits intomainfrom
Conversation
… using a unique ID to prevent race conditions.
…onditions when clearing user attribute updates in evaluation storage.
…InteractorCaptureErrorTests.kt` and remove a trailing blank line in `copilot-instructions.md`.
…storage implementation and tests.
There was a problem hiding this comment.
Pull request overview
Adds a versioned, thread-safe mechanism for tracking user attribute updates to prevent race conditions during evaluation fetch/clear flows in the Bucketeer Android SDK.
Changes:
- Introduces
UserAttributesState(flag + version) and updatesEvaluationStorageAPI/implementation to clear updates only when versions match. - Updates
EvaluationInteractorto capture state for safe clearing after fetch operations. - Adds/updates unit tests (including a concurrency test) and synchronizes
MemCacheaccess.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| bucketeer/src/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/UserAttributesState.kt | Adds a new state object (updated flag + version) for concurrency-safe tracking. |
| bucketeer/src/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorage.kt | Updates storage interface to support versioned clear + state retrieval. |
| bucketeer/src/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImpl.kt | Implements versioned user-attributes tracking with synchronized access. |
| bucketeer/src/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/EvaluationInteractor.kt | Captures user attribute state and uses it when clearing update flags after fetches. |
| bucketeer/src/main/kotlin/io/bucketeer/sdk/android/internal/cache/MemCache.kt | Adds synchronization to in-memory cache set/get for thread safety. |
| bucketeer/src/test/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImplTest.kt | Updates tests to validate version-aware clearing behavior. |
| bucketeer/src/test/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageConcurrencyTest.kt | Adds concurrency-focused tests for version increments and race-condition behavior. |
| bucketeer/src/test/kotlin/io/bucketeer/sdk/android/internal/evaluation/EvaluationInteractorCaptureErrorTests.kt | Updates mock storage to satisfy new interface contract. |
| .github/workflows/copilot-instructions.md | Adds AI assistant instructions documentation. |
Comments suppressed due to low confidence (2)
.github/workflows/copilot-instructions.md:162
- The last lines include leftover markup (
</content>,<parameter name="filePath">...) and a developer-specific absolute path. This looks like an accidental paste and makes the instructions file misleading/noisy; please remove these artifacts. Also, if the intent is to provide GitHub Copilot custom instructions, the expected location is typically.github/copilot-instructions.md(not underworkflows/).
- `.github/workflows/build.yml` - Main build pipeline
- `.github/workflows/e2e.yml` - End-to-end testing</content>
bucketeer/src/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/EvaluationInteractor.kt:70
getUserAttributesUpdated()andgetUserAttributesState()are read in two separate calls. IfsetUserAttributesUpdated()is invoked between them, the request can be sent withuserAttributesUpdated=falsewhile the captured state reflects the newer update, reintroducing the race this PR is trying to fix. Prefer a single atomic read: fetchuserAttributesStatefirst (synchronized) and deriveuserAttributesUpdatedfromuserAttributesState.userAttributesUpdatedfor the request condition, using the same captured state for clearing.
val currentEvaluationsId = evaluationStorage.getCurrentEvaluationId()
val evaluatedAt = evaluationStorage.getEvaluatedAt()
val userAttributesUpdated = evaluationStorage.getUserAttributesUpdated()
val userAttributesState = evaluationStorage.getUserAttributesState()
val condition =
UserEvaluationCondition(
evaluatedAt = evaluatedAt,
userAttributesUpdated = userAttributesUpdated,
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...lin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageConcurrencyTest.kt
Outdated
Show resolved
Hide resolved
...rc/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImpl.kt
Outdated
Show resolved
Hide resolved
...est/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImplTest.kt
Show resolved
Hide resolved
...rc/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImpl.kt
Outdated
Show resolved
Hide resolved
...lin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageConcurrencyTest.kt
Outdated
Show resolved
Hide resolved
...lin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageConcurrencyTest.kt
Outdated
Show resolved
Hide resolved
...lin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageConcurrencyTest.kt
Outdated
Show resolved
Hide resolved
Update the comment in EvaluationStorageImpl to describe using a version number (userAttributesVersion) rather than an id to avoid the race condition. The comment now explains that userAttributesVersion is incremented when setUserAttributesUpdated is called and that fetchEvaluations only clears userAttributesUpdated if the version matches the request state.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
bucketeer/src/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/EvaluationInteractor.kt
Show resolved
Hide resolved
...rc/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImpl.kt
Outdated
Show resolved
Hide resolved
...rc/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImpl.kt
Outdated
Show resolved
Hide resolved
...lin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageConcurrencyTest.kt
Outdated
Show resolved
Hide resolved
...lin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageConcurrencyTest.kt
Outdated
Show resolved
Hide resolved
…te updates, resolving concurrency issues, and update related tests.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...rc/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImpl.kt
Outdated
Show resolved
Hide resolved
.../src/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/UserAttributesState.kt
Show resolved
Hide resolved
bucketeer/src/main/kotlin/io/bucketeer/sdk/android/internal/cache/MemCache.kt
Outdated
Show resolved
Hide resolved
…tion in `MemCache`.
…emove a blank line in `MemCache`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...est/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImplTest.kt
Show resolved
Hide resolved
…uationStorageImpl` to prevent external lock contention.
…ulation in concurrency test
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...lin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageConcurrencyTest.kt
Show resolved
Hide resolved
...rc/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImpl.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...lin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageConcurrencyTest.kt
Outdated
Show resolved
Hide resolved
...rc/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImpl.kt
Show resolved
Hide resolved
...est/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImplTest.kt
Outdated
Show resolved
Hide resolved
...est/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImplTest.kt
Show resolved
Hide resolved
...rc/main/kotlin/io/bucketeer/sdk/android/internal/evaluation/storage/EvaluationStorageImpl.kt
Show resolved
Hide resolved
Clean up unused imports in MemCacheConcurrencyTest.kt by removing redundant imports of MemCache and Executors. This eliminates unnecessary dependencies and compiler warnings; no logic changes.
…sion` from `Int` to `Long` to prevent potential overflow.
Minor test cleanup: remove two extra blank lines in EvaluationStorageConcurrencyTest.kt and remove a redundant assert in EvaluationStorageImplTest.kt. No behavioral changes—just tidying up test code.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@cre8ivejp please help me to take a look |
| override fun getEvaluatedAt(): String = evaluationSharedPrefs.evaluatedAt | ||
|
|
||
| override fun getUserAttributesUpdated(): Boolean = evaluationSharedPrefs.userAttributesUpdated | ||
| private var userAttributesVersion: Long = 0 |
There was a problem hiding this comment.
The word version seems confusing, and since we don't have version control and it is always reset when the SDK initializes, I was thinking it would be better if you used something like updatedAt instead.
WDYT?
There was a problem hiding this comment.
I agree version is confusing here since it's not persisted and resets on SDK init, and we don't have version control on it.
However, I think updatedAt might also be misleading because this value is a counter (0, 1, 2...) rather than a timestamp.
What about userAttributesUpdateSequence (or just updateSequence in the state object)? This makes it clear that:
- It's a sequence number for ordering updates
- It's session-scoped (resets on init)
- It's used for optimistic concurrency control
WDYT?
This pull request introduces a version-based concurrency mechanism to track user attribute updates in the evaluation storage system. The main goal is to prevent race conditions that could cause user attribute updates to be lost during concurrent evaluation fetches and updates. The implementation replaces the previous boolean flag with a versioned state object, ensures thread-safety with explicit locking, and updates all related interfaces, implementations, and tests accordingly.
Concurrency and Race Condition Handling:
UserAttributesStatedata class containing both a boolean flag and a version number to track user attribute updates, ensuring that updates are not lost during concurrent operations. [1] [2]EvaluationStorageImplto use a private lock and version counter for user attribute updates, only clearing the update flag if the version matches the state captured at the start of a fetch, thus preventing race conditions.EvaluationStorageConcurrencyTestto verify correct versioning and to ensure that updates are not lost during concurrent fetches and updates.API and Interface Changes:
EvaluationStorageinterface: replacedgetUserAttributesUpdated()withgetUserAttributesState(), and madeclearUserAttributesUpdated()accept aUserAttributesStateparameter for version-aware clearing.EvaluationInteractorand related classes to use the new versioned state API, passing the captured state toclearUserAttributesUpdated()and usinggetUserAttributesState()instead of the old boolean method. [1] [2] [3]Thread Safety Improvements:
MemCacheImplandEvaluationStorageImplto use private lock objects for synchronization, avoiding potential deadlocks or lock contention by not synchronizing onthis. [1] [2]Test Updates and Coverage:
UserAttributesStateAPI and added assertions for the versioned state. [1] [2] [3] [4] [5] [6] [7]These changes collectively make user attribute update tracking robust against concurrency issues and ensure correctness in multi-threaded scenarios.