Skip to content

Conversation

@leoromanovsky
Copy link
Member

Motivation

The precomputed client provides a new way to use Eppo feature flags where all assignments are computed server-side for a specific subject. This eliminates client-side evaluation overhead and provides instant lookups.

Changes

Client (EppoPrecomputedClient):

Assignment Methods:

  • getStringAssignment(), getBooleanAssignment(), getIntegerAssignment(), getNumericAssignment(), getJSONAssignment()
  • getBanditAction() - Returns variation + recommended action

Initialization:

  • Builder pattern with extensive configuration options
  • Supports offline mode with initial configuration bytes
  • Loads from disk cache on startup (races with network for speed on slow devices)
  • Fetches fresh configuration from edge endpoint

Polling:

  • Background polling with configurable interval and jitter
  • Polling starts after initial fetch completes (not during)
  • Pause/resume/stop lifecycle methods

Logging:

  • Assignment and bandit logging with deduplication caches
  • Decodes Base64-encoded extra logging fields
  • Includes SDK metadata (version, language, obfuscation flag)

Error Handling:

  • Graceful mode (default): returns defaults on errors
  • Non-graceful mode: throws exceptions

Tests (EppoPrecomputedClientTest):

  • Builder validation (missing API key, subject key)
  • All flag type assignments with mock data
  • Assignment logging and cache deduplication
  • Bandit actions
  • Type mismatch handling
  • Offline mode
  • Integration with sdk-test-data files

Decisions

  • Singleton pattern: Matches existing EppoClient API
  • Subject key required at init: Precomputed assignments are subject-specific
  • Polling after fetch: Prevents concurrent fetches during initialization
  • Null safety in bandit decoding: Handles malformed responses gracefully
  • No API key in logs: Redacted for security
  • In-memory update on disk failure: Ensures client works even with storage issues

PR Stack

This PR is part of the precomputed client feature, split for easier review:

main
├── precomputed/1-base-cache-file
│   └─┬─ precomputed/3-storage-layer ⬅ (base)
│     │   └── 👉 precomputed/4-client (this PR)
│     │       └── precomputed/5-example-app
│     │
└── precomputed/2-dtos-and-utils ──────┘

Why this structure: The client is the largest piece (~1000 lines) but depends on all the infrastructure built in previous PRs. Reviewing it separately allows focus on the client logic without being distracted by DTO definitions or storage implementation.

Merge order: PRs 1, 2, and 3 must be merged first.

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.
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.
@leoromanovsky leoromanovsky force-pushed the precomputed/3-storage-layer branch from 8f4e45c to e80e6f9 Compare February 2, 2026 20:10
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.
@leoromanovsky leoromanovsky force-pushed the precomputed/3-storage-layer branch from e80e6f9 to dd5309f Compare February 2, 2026 20:13
- 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
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
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces EppoPrecomputedClient, a new client for Eppo feature flags that receives precomputed assignments from the server rather than performing client-side evaluation. This eliminates evaluation overhead and provides instant flag lookups.

Changes:

  • Implements EppoPrecomputedClient with builder pattern initialization, assignment methods for all flag types (string, boolean, integer, numeric, JSON), bandit action support, and configurable polling
  • Adds comprehensive test suite covering offline mode, assignment logging, cache deduplication, type mismatches, and integration with sdk-test-data files
  • Supports both online (fetch + poll) and offline modes, graceful/non-graceful error handling, and disk cache persistence with network racing for fast initialization

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
EppoPrecomputedClient.java Core client implementation with assignment retrieval, HTTP fetching, polling lifecycle, logging, and builder-based initialization
EppoPrecomputedClientTest.java Test suite covering builder validation, flag assignments, logging, caching, and sdk-test-data integration tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Base automatically changed from precomputed/3-storage-layer to main February 3, 2026 02:48
- 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
@leoromanovsky leoromanovsky marked this pull request as ready for review February 3, 2026 03:06
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.
// No dot separator
assertNull(Utils.getEnvironmentFromSdkKey("invalidKeyWithoutDot"));

// Null key

Choose a reason for hiding this comment

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

A nit: would clean up these comments that are stating what is self-evident in the assertions

Comment on lines +528 to +530
metaData.put("obfuscated", "true");
metaData.put("sdkLanguage", "android");
metaData.put("sdkLibVersion", BuildConfig.EPPO_VERSION);

Choose a reason for hiding this comment

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

Oh interesting, this metaData differs from iOS (which has obfuscated, sdkName, and sdkVersion).

But looks consistent with Android's EppoClient

expectedMeta.put("obfuscated", "true");
expectedMeta.put("sdkLanguage", "android");
expectedMeta.put("sdkLibVersion", BuildConfig.EPPO_VERSION);

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.

3 participants