Skip to content

Conversation

@typotter
Copy link
Collaborator

@typotter typotter commented Jan 20, 2026

📚 Downstream SDK Extensibility Stacked Pull Requests 📚

☑️ Extract DTO interfaces for downstream SDK extensibility (#197)
👉 Add HTTP ETag-based caching for configuration fetches (this PR)
🔲 Extract IConfigurationSource interface for pluggable configuration loading (#203)
🔲 Make OkHttp a peer dependency for version flexibility (#204)

Eppo Internal

🎟️ Reduces bandwidth and CPU overhead for polling SDKs
📜 Design Doc: N/A - Standard HTTP caching implementation

Motivation and Context

Currently, every configuration poll fetches the full configuration JSON even when nothing has changed. This results in significant waste:

  1. High bandwidth usage: ~14-720MB/day per SDK instance (depending on config size, 1 poll/minute)
  2. Unnecessary CPU usage: JSON parsing, bandit model checks, callback notifications all triggered even when config is identical
  3. Wasted I/O: Configuration storage writes for unchanged data
  4. Increased server load: Full response generation and transmission for every poll

HTTP ETags are a standard caching mechanism (RFC 7232) that allows servers to signal when a resource hasn't changed via 304 Not Modified responses, eliminating redundant data transfer and processing.

With high-frequency polling (default 30 seconds) and typical cache hit rates of 70-90%, this optimization can reduce bandwidth and processing by an order of magnitude.

Description

Implement HTTP caching using ETags to optimize configuration fetching.

Added Configuration.flagsETag Field

public class Configuration {
  private final String flagsETag;  // Stores current eTag

  @Nullable
  public String getFlagsETag() { return flagsETag; }
}

public static class Builder {
  public Builder flagsETag(String flagsETag) { ... }
}
  • Immutable field storing current eTag value
  • Cleared on configuration reset (emptyConfig())
  • Persisted with configuration for subsequent requests

Created EppoHttpResponse Wrapper

public class EppoHttpResponse {
  private final byte[] body;
  private final int statusCode;
  @Nullable private final String eTag;

  public boolean isNotModified() { return statusCode == 304; }
  public boolean isSuccessful() { return statusCode >= 200 && statusCode < 300; }
}
  • Replaces raw byte[] returns from EppoHttpClient
  • Captures HTTP metadata (status code, eTag header)
  • Enables 304 detection

Updated EppoHttpClient for ETags

New Method Signatures:

  • EppoHttpResponse get(String path, String ifNoneMatch)
  • CompletableFuture<EppoHttpResponse> getAsync(String path, String ifNoneMatch)

Implementation:

  • Includes If-None-Match: {eTag} header when ifNoneMatch is non-null
  • Extracts ETag from response: response.header("ETag")
  • Handles 304 Not Modified: Returns response with empty body, status 304
  • Handles 200 OK: Returns response with full body, new eTag

Updated ConfigurationRequestor for 304 Handling

Fetch Flow:

  1. Get previous eTag: String previousETag = lastConfig.getFlagsETag()
  2. Fetch with eTag: EppoHttpResponse response = client.get(endpoint, previousETag)
  3. Check for 304: if (response.isNotModified()) { return; }
  4. Early exit - skip parsing, bandit fetch, callbacks, storage
  5. On 200: Parse config, store new eTag

Optimization:
When flags endpoint returns 304, bandit fetching is automatically skipped (no model version check needed).

How has this been documented?

  • Javadoc on Configuration.getFlagsETag() explains usage
  • Javadoc on EppoHttpResponse describes status codes and eTag handling
  • Code comments explain 304 early exit logic
  • No user-facing documentation needed (internal optimization, transparent to SDK users)

How has this been tested?

Automated Testing

./gradlew test

✅ All 249 tests passing

Test Updates

  • Updated TestUtils.mockHttpResponse() to return EppoHttpResponse instead of byte[]
  • Updated all mocks to handle 2-parameter signatures (get(path, ifNoneMatch))
  • Updated verify statements to match new method signatures
  • Added null safety for empty configurations
  • Modified 6 test files (ConfigurationRequestorTest, EppoHttpClientTest, ConfigurationBuilderTest, BaseEppoClientTest, TestUtils)

Functionality Verified

  1. ETag extraction: ETag properly extracted from HTTP response headers
  2. ETag storage: Stored in Configuration and retrieved correctly
  3. If-None-Match header: Sent when eTag exists
  4. 304 handling: Early exit skips all processing
  5. 200 handling: Normal processing, new eTag stored
  6. No eTag support: Works normally when servers don't send ETags (eTag = null)
  7. Concurrent fetches: eTag handling works correctly with polling

@typotter typotter changed the title feat: add HTTP ETag-based caching for configuration fetches Configuration Source Extraction part 2/4 Jan 20, 2026
@aarsilv aarsilv requested a review from Copilot January 20, 2026 18:15
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 implements HTTP ETag-based caching to optimize configuration polling by avoiding redundant data transfer and processing when configurations haven't changed.

Changes:

  • Added flagsETag field to Configuration class to store and persist ETag values across requests
  • Created EppoHttpResponse wrapper to capture HTTP metadata (status code, ETag header) alongside response body
  • Updated EppoHttpClient to support conditional requests via If-None-Match headers and handle 304 Not Modified responses
  • Modified ConfigurationRequestor to skip parsing, bandit fetching, and callbacks when receiving 304 responses

Reviewed changes

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

Show a summary per file
File Description
src/main/java/cloud/eppo/api/Configuration.java Added flagsETag field with getter/builder methods and updated equals/hashCode/toString
src/main/java/cloud/eppo/EppoHttpResponse.java New wrapper class for HTTP responses with status code, body, and ETag support
src/main/java/cloud/eppo/EppoHttpClient.java Updated to send If-None-Match headers, extract ETags, and return EppoHttpResponse objects
src/main/java/cloud/eppo/ConfigurationRequestor.java Added 304 handling to skip processing when config unchanged, storing ETags in configurations
src/test/java/cloud/eppo/ConfigurationRequestorTest.java Added comprehensive tests for ETag functionality including 304 handling, callback suppression, and round-trip scenarios
src/test/java/cloud/eppo/api/ConfigurationBuilderTest.java Updated constructor calls to include new flagsETag parameter
src/test/java/cloud/eppo/BaseEppoClientTest.java Updated mock to return EppoHttpResponse instead of raw bytes
src/test/java/cloud/eppo/helpers/TestUtils.java Updated mock helper to create EppoHttpResponse objects and handle both method signatures

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

Configuration.Builder configBuilder =
Configuration.builder(flagConfigurationJsonBytes, expectObfuscatedConfig)
.banditParametersFromConfig(lastConfig);
Configuration.builder(flagsResponse.getBody())
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The Configuration.builder() call is missing the expectObfuscatedConfig parameter that was present in the original code. This could cause incorrect parsing of obfuscated configurations.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

expectObfuscatedConfig was deprecated and now removed

@typotter typotter force-pushed the typo/etag branch 4 times, most recently from c97b24b to d1c6591 Compare January 21, 2026 06:40
Implement HTTP caching using ETags to reduce bandwidth and processing overhead.

Changes:
- Add flagsETag field to Configuration for storing current eTag
- Create EppoHttpResponse wrapper class (status code, body, eTag)
- Update EppoHttpClient to accept If-None-Match header and return EppoHttpResponse
- Update ConfigurationRequestor to handle 304 Not Modified responses
- Early exit on 304 - skip JSON parsing, bandit fetches, and callbacks

Benefits:
- ~80% bandwidth reduction (assuming 80% cache hit rate on polling)
- Reduced CPU usage - skip parsing and processing when config unchanged
- Fewer unnecessary bandit parameter fetches
- Reduced callback notifications

Implementation:
- Get previous eTag from Configuration.getFlagsETag()
- Include If-None-Match header in HTTP request if eTag exists
- Server returns 304 if config hasn't changed
- ConfigurationRequestor early returns (no processing) on 304
- Server returns 200 with new eTag if config changed
- New eTag stored in Configuration for next request

Tests:
- Updated all test mocks to return EppoHttpResponse
- Added null safety for empty configurations
- All 249 tests passing
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