feat: add precomputed configuration storage#239
Merged
leoromanovsky merged 8 commits intomainfrom Feb 3, 2026
Merged
Conversation
Add the foundational data structures and utilities for the precomputed client feature: - ObfuscationUtils: MD5 hashing for flag key obfuscation - PrecomputedFlag: DTO for precomputed flag assignments - PrecomputedBandit: DTO for precomputed bandit assignments - PrecomputedConfigurationResponse: Wire protocol response parsing - BanditResult: Result container for bandit action lookups - MissingSubjectKeyException: Validation exception Includes comprehensive unit tests for serialization round-trips and MD5 hash consistency.
sameerank
approved these changes
Jan 31, 2026
sameerank
left a comment
There was a problem hiding this comment.
The empty-vs-null behavior returned by loadConfigFromCache is the main one that was confusing to me. The rest are nits. Looks good overall!
eppo/src/main/java/cloud/eppo/android/PrecomputedConfigurationStore.java
Outdated
Show resolved
Hide resolved
eppo/src/main/java/cloud/eppo/android/dto/PrecomputedConfigurationResponse.java
Show resolved
Hide resolved
| return CompletableFuture.completedFuture(null); | ||
| } | ||
| return cacheLoadFuture = | ||
| CompletableFuture.supplyAsync( |
There was a problem hiding this comment.
Nothing to fix here, but just noting that there's a race condition due to a sync check and async read. You've handled it by ensuring that exceptions are caught so it's not dangerous.
Member
Author
There was a problem hiding this comment.
Yea absolutely thanks for mentioning; I'm being kind of lazy and copying the same pattern as exists in the local-eval configuration store.
eppo/src/main/java/cloud/eppo/android/dto/PrecomputedConfigurationResponse.java
Outdated
Show resolved
Hide resolved
8f4e45c to
e80e6f9
Compare
Add md5HexPrefix() method that only converts the bytes needed for a given prefix length, avoiding unnecessary work when only a prefix is required (e.g., cache file naming uses first 8 chars). Includes unrolled loop for the common 4-byte (8 hex char) case to help compiler optimization, following iOS SDK PR #93 approach.
Extract common file caching functionality from ConfigCacheFile into a new BaseCacheFile base class. This enables reuse for the upcoming precomputed configuration cache without code duplication. - Add BaseCacheFile with common read/write/delete operations - Refactor ConfigCacheFile to extend BaseCacheFile - No functional changes to existing behavior
Add the storage layer for precomputed flag configurations: - PrecomputedCacheFile: Disk cache file extending BaseCacheFile - PrecomputedConfigurationStore: In-memory + disk storage with async save/load operations and proper thread synchronization - Updates in-memory config even if disk write fails for resilience Also adds test data files to Makefile for integration testing. Includes unit tests for cache operations and failure scenarios.
e80e6f9 to
dd5309f
Compare
- Return null consistently in loadConfigFromCache for both file-not-found and read-error cases - Use static EMPTY singleton in PrecomputedConfigurationResponse.empty() - Use Collections.singletonMap() in getEnvironment() for memory efficiency
leoromanovsky
added a commit
that referenced
this pull request
Feb 4, 2026
* feat: add obfuscation utilities and precomputed DTOs Add the foundational data structures and utilities for the precomputed client feature: - ObfuscationUtils: MD5 hashing for flag key obfuscation - PrecomputedFlag: DTO for precomputed flag assignments - PrecomputedBandit: DTO for precomputed bandit assignments - PrecomputedConfigurationResponse: Wire protocol response parsing - BanditResult: Result container for bandit action lookups - MissingSubjectKeyException: Validation exception Includes comprehensive unit tests for serialization round-trips and MD5 hash consistency. * perf: optimize MD5 hex conversion with lookup table Replace Integer.toHexString() with a pre-computed hex character lookup table for byte-to-hex conversion. This avoids creating intermediate String objects for each byte, reducing allocations. Mirrors optimization from iOS SDK PR #91/#93. * perf: add md5HexPrefix for efficient partial hash Add md5HexPrefix() method that only converts the bytes needed for a given prefix length, avoiding unnecessary work when only a prefix is required (e.g., cache file naming uses first 8 chars). Includes unrolled loop for the common 4-byte (8 hex char) case to help compiler optimization, following iOS SDK PR #93 approach. * refactor: extract BaseCacheFile for reuse Extract common file caching functionality from ConfigCacheFile into a new BaseCacheFile base class. This enables reuse for the upcoming precomputed configuration cache without code duplication. - Add BaseCacheFile with common read/write/delete operations - Refactor ConfigCacheFile to extend BaseCacheFile - No functional changes to existing behavior * feat: add precomputed configuration storage Add the storage layer for precomputed flag configurations: - PrecomputedCacheFile: Disk cache file extending BaseCacheFile - PrecomputedConfigurationStore: In-memory + disk storage with async save/load operations and proper thread synchronization - Updates in-memory config even if disk write fails for resilience Also adds test data files to Makefile for integration testing. Includes unit tests for cache operations and failure scenarios. * style: apply spotless formatting * fix: address PR #239 feedback - Return null consistently in loadConfigFromCache for both file-not-found and read-error cases - Use static EMPTY singleton in PrecomputedConfigurationResponse.empty() - Use Collections.singletonMap() in getEnvironment() for memory efficiency * feat: add EppoPrecomputedClient Add the main precomputed client implementation with: - Server-side precomputed flag assignments with instant lookups - Support for all flag types: string, boolean, integer, numeric, JSON - Bandit action support with attribute decoding - Builder pattern with extensive configuration options - Offline mode with initial configuration support - Background polling with configurable interval and jitter - Assignment and bandit logging with deduplication caches - Graceful error handling mode The client fetches precomputed assignments from the edge endpoint, eliminating client-side flag evaluation overhead. Includes comprehensive instrumented tests covering: - All flag type assignments - Assignment logging and deduplication - Bandit actions - Offline mode - SDK test data integration * perf: use md5HexPrefix for cache file naming * fix: address PR review feedback for EppoPrecomputedClient - Make polling fields volatile for thread safety - Return user's default value on parse failure instead of hardcoded 0 - Extract magic string to NO_ACTION_CACHE_KEY constant - Fix banditActions serialization to match JS SDK wire format - Add comments for hash length and jitter calculation - Rename misleading test to testNonGracefulModeCanBeConfigured * feat: derive precomputed client base URL from SDK key Extract environment prefix from SDK key to automatically construct the correct edge endpoint URL (e.g., https://5qhpgd.fs-edge-assignment.eppo.cloud). This removes the need for users to manually configure the base URL. * chore: remove self-evident comments in UtilsTest Address PR review feedback by removing redundant inline comments that simply restated what the test assertions already conveyed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The precomputed client needs to store configuration both in-memory for fast access and on disk for offline/startup scenarios. This PR adds the storage layer that manages this caching.
Changes
Storage:
PrecomputedCacheFile- Disk cache file extendingBaseCacheFilewith precomputed-specific namingPrecomputedConfigurationStore- Thread-safe storage manager with:Build:
Makefileto download precomputed test data files from sdk-test-dataTests:
PrecomputedConfigurationStoreTest- Unit tests for:Decisions
volatilefor configuration field to ensure visibility across threads without locking on readscacheLock) and load future management (cacheLoadLock)PR Stack
This PR is part of the precomputed client feature, split for easier review:
Why this structure: The storage layer depends on both the base cache infrastructure (PR 1) and the DTOs (PR 2). It's isolated from the client implementation, making it easier to review the caching logic independently.
Merge order: PR 1 and PR 2 must be merged first, then this PR's base updated to
main.