Skip to content

feat: improve sticky bucketing#167

Open
vazarkevych wants to merge 7 commits into
growthbook:feature/fix-cachingfrom
vazarkevych:feature/improve-stickybucketing
Open

feat: improve sticky bucketing#167
vazarkevych wants to merge 7 commits into
growthbook:feature/fix-cachingfrom
vazarkevych:feature/improve-stickybucketing

Conversation

@vazarkevych

@vazarkevych vazarkevych commented Aug 18, 2025

Copy link
Copy Markdown
Collaborator

In this pr we:

  • Added FileStickyBucketServiceImpl that implements StickyBucketService using GbCacheManager to persist all sticky bucket assignments in a single file.
  • Supports asynchronous fetching and saving via CompletableFuture.

@madhuchavva madhuchavva left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@vazarkevych Thoughtful contribution on providing File-based storage. But it involves a lot of risks that are needed to be taken care such as Concurrency bottlenecks (read-modify-write), Performance issues (full file read/write per operation) and Corruption risks (if the process crashes midway, the data is lost and the file isn't readable anymore).

On top of it, This PR replaces the current synchronous behavior for StickyBucketingService interface with async methods, which breaks the existing/custom implementations and concurrent access to a shared file needs synchronization.

Can you provide more details about this feature request?

@vazarkevych

Copy link
Copy Markdown
Collaborator Author

@vazarkevych Thoughtful contribution on providing File-based storage. But it involves a lot of risks that are needed to be taken care such as Concurrency bottlenecks (read-modify-write), Performance issues (full file read/write per operation) and Corruption risks (if the process crashes midway, the data is lost and the file isn't readable anymore).

On top of it, This PR replaces the current synchronous behavior for StickyBucketingService interface with async methods, which breaks the existing/custom implementations and concurrent access to a shared file needs synchronization.

Can you provide more details about this feature request?

Thank you for the review!
I fully agree with the concerns regarding interface consistency and the risks associated with file I/O.
I’ve made substantial changes to address all the issues raised, while still preserving the purpose of the PR - providing a safe file-based storage.

1. Contract Violation - resolved

Changing StickyBucketService to an asynchronous contract (CompletableFuture) was indeed a mistake, as it introduced a breaking change.

Fix:
The StickyBucketService interface has been fully restored to its original synchronous form (returning StickyAssignmentsDocument or Map<...>). This maintains backward compatibility with all existing implementations.

2. File Storage Risks & Concurrency

I improved FileStickyBucketServiceImpl to minimize concurrency issues and corruption risks:

✔️ Concurrency

To avoid race conditions during file-level "read-modify-write" operations, I added key-based locking using a ConcurrentHashMap.
This ensures that read/write operations for the same file are thread-safe and properly synchronized.

✔️ Safety & Integrity

The implementation now operates within a synchronized block to ensure controlled access to the file system.

3. 💡 Feature Request Background

This request originated from community needs analysis, particularly similar to the Swift SDK (Issue #116), where asynchronous support for StickyBucketService was added. This is needed when users want to avoid blocking the main application thread during slow I/O operations. Also, the TypeScript SDK's base StickyBucketService is also fundamentally asynchronous (returning Promises) to natively support remote persistence solutions

Although in this PR we reverted to a synchronous interface for compatibility, the FileStickyBucketServiceImpl implementation shows how I/O-based storage can be used safely in a synchronous environment.

@vazarkevych vazarkevych changed the title Feature/improve stickybucketing feat: improve sticky bucketing Jul 2, 2026
…ded concurrency)

Address the reliability/security risks of persisting sticky bucket assignments
to the filesystem:

FileCachingManagerImpl
- Atomic writes: write to a temp file then ATOMIC_MOVE over the target (fallback
  to REPLACE_EXISTING), so a crash mid-write can no longer leave a corrupted /
  half-written cache entry. Benefits the feature cache too.
- Explicit UTF-8 for read and write (was platform default); read whole file so
  content round-trips exactly.

FileStickyBucketServiceImpl
- Path-traversal safe: the user-controlled attribute value is no longer used as a
  file name. The logical key is hashed with SHA-256, giving a fixed-length hex
  name that cannot escape the cache dir or collide on separator chars.
- Bounded concurrency: striped ReadWriteLocks (fixed count) instead of an
  unbounded per-key lock map that grew with the number of distinct users.
- Read latency: a bounded (LRU) in-memory cache fronts the store so repeated
  evaluations for the same user avoid disk I/O, with constant memory use.

Tests
- New FileStickyBucketServiceImplHardeningTest: cross-instance persistence,
  path-traversal safety, concurrent distinct-key and same-key access, and
  graceful handling of corrupted entries.
- Update FileCachingManagerImplTest write-failure case to a deterministic,
  permission-independent scenario consistent with atomic writes.
- Centralize the "attributeName||attributeValue" document key format in
  StickyAssignmentsDocument (KEY_SEPARATOR + key()/getKey()); remove the four
  duplicated/hardcoded "||" usages across both service impls and GrowthBookUtils.
- Extract AbstractStickyBucketService with the shared getAllAssignments so the
  in-memory and file backends only implement getAssignments/saveAssignments.
- Add StickyBucketVariation.notFound()/blocked()/found() factories (+ NOT_FOUND
  constant) and use them in getStickyBucketVariation.
- Rewrite GrowthBookUtils.getStickyBucketAssignments for readability (named
  locals, no duplicate map lookups) while preserving exact behavior.
- Guard deriveStickyBucketIdentifierAttributes against a missing "features" key
  (was an NPE on jsonObject.get("features").toString()).
- Tidy StickyBucketService / model javadoc.
@vazarkevych
vazarkevych requested a review from madhuchavva July 2, 2026 17:19
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