Skip to content

Commit 48246ec

Browse files
authored
refactor: replace remote resolve with remote materialization store (#209)
1 parent cdcfc86 commit 48246ec

12 files changed

+805
-107
lines changed

openfeature-provider/java/README.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ Configure the provider behavior using environment variables:
121121

122122
- `CONFIDENCE_RESOLVER_POLL_INTERVAL_SECONDS`: How often to poll Confidence to get updates (default: `30` seconds)
123123
- `CONFIDENCE_NUMBER_OF_WASM_INSTANCES`: How many WASM instances to create (this defaults to `Runtime.getRuntime().availableProcessors()` and will affect the performance of the provider)
124+
- `CONFIDENCE_MATERIALIZATION_READ_TIMEOUT_SECONDS`: Timeout for materialization read operations when using remote materialization store (default: `2` seconds)
125+
- `CONFIDENCE_MATERIALIZATION_WRITE_TIMEOUT_SECONDS`: Timeout for materialization write operations when using remote materialization store (default: `5` seconds)
124126

125127
##### Deprecated in favour of a custom ChannelFactory:
126128
- `CONFIDENCE_DOMAIN`: Override the default Confidence service endpoint (default: `edge-grpc.spotify.com`)
@@ -161,17 +163,54 @@ The provider supports **materializations** for two key use cases:
161163

162164
1. **Custom Targeting via Materialized Segments**: Efficiently target precomputed sets of identifiers from datasets. Instead of evaluating complex targeting rules at runtime, materializations allow for fast lookups of whether a unit (user, session, etc.) is included in a target segment.
163165

164-
**By default, materializations are managed by Confidence servers.** When sticky assignment data is needed, the provider makes a network call to Confidence, which maintains the sticky repository server-side with automatic 90-day TTL management. This requires no additional setup.
166+
### Materialization Storage Options
165167

166-
### Custom Materialization Storage
168+
The provider offers three options for managing materialization data:
167169

168-
Optionally, you can implement a custom `MaterializationStore` to manage materialization data in your own storage (Redis, database, etc.) to eliminate network calls and improve latency:
170+
#### 1. No Materialization Support (Default)
171+
172+
By default, materializations are not supported. If a flag requires materialization data (sticky assignments or custom targeting), the evaluation will fail and return the default value.
173+
174+
```java
175+
// Default behavior - no materialization support
176+
OpenFeatureLocalResolveProvider provider =
177+
new OpenFeatureLocalResolveProvider("your-client-secret");
178+
```
179+
180+
#### 2. Remote Materialization Store
181+
182+
Enable remote materialization storage to have Confidence manage materialization data server-side. When sticky assignment data or materialized segment targeting data is needed, the provider makes a gRPC call to Confidence:
183+
184+
```java
185+
// Enable remote materialization storage
186+
LocalProviderConfig config = LocalProviderConfig.builder()
187+
.useRemoteMaterializationStore(true)
188+
.build();
189+
190+
OpenFeatureLocalResolveProvider provider =
191+
new OpenFeatureLocalResolveProvider(config, "your-client-secret");
192+
```
193+
194+
**⚠️ Important Performance Impact**: This option fundamentally changes how the provider operates:
195+
- **Without materialization (default)**: Flag evaluation requires **zero network calls** - all evaluations happen locally with minimal latency
196+
- **With remote materialization**: Flag evaluation **requires network calls** to Confidence for materialization reads/writes, adding latency to each evaluation
197+
198+
This option:
199+
- Requires network calls for materialization reads/writes during flag evaluation
200+
- Automatically handles TTL and cleanup
201+
- Requires no additional infrastructure
202+
- Suitable for production use cases where sticky assignments or materialized segment targeting are required
203+
204+
#### 3. Custom Materialization Storage
205+
206+
For advanced use cases requiring minimal latency, implement a custom `MaterializationStore` to manage materialization data in your own infrastructure (Redis, database, etc.):
169207

170208
```java
171-
// Optional: Custom storage for materialization data
209+
// Custom storage for materialization data
172210
MaterializationStore store = new RedisMaterializationStore(jedisPool);
173211
OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider(
174-
clientSecret,
212+
new LocalProviderConfig(),
213+
"your-client-secret",
175214
store
176215
);
177216
```

openfeature-provider/java/src/main/java/com/spotify/confidence/ConfidenceGrpcFlagResolver.java

Lines changed: 0 additions & 34 deletions
This file was deleted.

openfeature-provider/java/src/main/java/com/spotify/confidence/LocalProviderConfig.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
public class LocalProviderConfig {
44
private final ChannelFactory channelFactory;
55
private final HttpClientFactory httpClientFactory;
6+
private final boolean useRemoteMaterializationStore;
67

78
public LocalProviderConfig() {
89
this(null, null);
@@ -16,6 +17,17 @@ public LocalProviderConfig(ChannelFactory channelFactory, HttpClientFactory http
1617
this.channelFactory = channelFactory != null ? channelFactory : new DefaultChannelFactory();
1718
this.httpClientFactory =
1819
httpClientFactory != null ? httpClientFactory : new DefaultHttpClientFactory();
20+
this.useRemoteMaterializationStore = false;
21+
}
22+
23+
public LocalProviderConfig(
24+
ChannelFactory channelFactory,
25+
HttpClientFactory httpClientFactory,
26+
boolean useRemoteMaterializationStore) {
27+
this.channelFactory = channelFactory != null ? channelFactory : new DefaultChannelFactory();
28+
this.httpClientFactory =
29+
httpClientFactory != null ? httpClientFactory : new DefaultHttpClientFactory();
30+
this.useRemoteMaterializationStore = useRemoteMaterializationStore;
1931
}
2032

2133
public ChannelFactory getChannelFactory() {
@@ -25,4 +37,38 @@ public ChannelFactory getChannelFactory() {
2537
public HttpClientFactory getHttpClientFactory() {
2638
return httpClientFactory;
2739
}
40+
41+
public boolean isUseRemoteMaterializationStore() {
42+
return useRemoteMaterializationStore;
43+
}
44+
45+
public static Builder builder() {
46+
return new Builder();
47+
}
48+
49+
public static class Builder {
50+
private ChannelFactory channelFactory;
51+
private HttpClientFactory httpClientFactory;
52+
private boolean useRemoteMaterializationStore;
53+
54+
public Builder channelFactory(ChannelFactory channelFactory) {
55+
this.channelFactory = channelFactory;
56+
return this;
57+
}
58+
59+
public Builder httpClientFactory(HttpClientFactory httpClientFactory) {
60+
this.httpClientFactory = httpClientFactory;
61+
return this;
62+
}
63+
64+
public Builder useRemoteMaterializationStore(boolean useRemoteMaterializationStore) {
65+
this.useRemoteMaterializationStore = useRemoteMaterializationStore;
66+
return this;
67+
}
68+
69+
public LocalProviderConfig build() {
70+
return new LocalProviderConfig(
71+
channelFactory, httpClientFactory, useRemoteMaterializationStore);
72+
}
73+
}
2874
}

openfeature-provider/java/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ public class OpenFeatureLocalResolveProvider implements FeatureProvider {
6060
private final AtomicReference<ProviderState> state =
6161
new AtomicReference<>(ProviderState.NOT_READY);
6262
private final ChannelFactory channelFactory;
63-
private final RemoteResolver grpcFlagResolver;
6463

6564
private static long getPollIntervalSeconds() {
6665
return Optional.ofNullable(System.getenv("CONFIDENCE_RESOLVER_POLL_INTERVAL_SECONDS"))
@@ -115,7 +114,12 @@ public OpenFeatureLocalResolveProvider(String clientSecret) {
115114
* authentication
116115
*/
117116
public OpenFeatureLocalResolveProvider(LocalProviderConfig config, String clientSecret) {
118-
this(config, clientSecret, new UnsupportedMaterializationStore());
117+
this(
118+
config,
119+
clientSecret,
120+
config.isUseRemoteMaterializationStore()
121+
? new RemoteMaterializationStore(clientSecret, config.getChannelFactory())
122+
: new UnsupportedMaterializationStore());
119123
}
120124

121125
/**
@@ -160,7 +164,6 @@ public OpenFeatureLocalResolveProvider(
160164
final var wasmFlagLogger = new GrpcWasmFlagLogger(clientSecret, config.getChannelFactory());
161165
this.wasmResolveApi = new ThreadLocalSwapWasmResolverApi(wasmFlagLogger, materializationStore);
162166
this.channelFactory = config.getChannelFactory();
163-
this.grpcFlagResolver = new ConfidenceGrpcFlagResolver(this.channelFactory);
164167
}
165168

166169
/**
@@ -179,13 +182,11 @@ public OpenFeatureLocalResolveProvider(
179182
AccountStateProvider accountStateProvider,
180183
String clientSecret,
181184
MaterializationStore materializationStore,
182-
WasmFlagLogger wasmFlagLogger,
183-
RemoteResolver remoteResolver) {
185+
WasmFlagLogger wasmFlagLogger) {
184186
this.materializationStore = materializationStore;
185187
this.clientSecret = clientSecret;
186188
this.stateProvider = accountStateProvider;
187189
this.wasmResolveApi = new ThreadLocalSwapWasmResolverApi(wasmFlagLogger, materializationStore);
188-
this.grpcFlagResolver = remoteResolver;
189190
this.channelFactory = new LocalProviderConfig().getChannelFactory();
190191
}
191192

@@ -293,7 +294,6 @@ public void shutdown() {
293294
log.debug("Shutting down scheduled executors");
294295
flagsFetcherExecutor.shutdown();
295296
logPollExecutor.shutdown();
296-
this.grpcFlagResolver.close();
297297

298298
try {
299299
if (!flagsFetcherExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
@@ -311,6 +311,11 @@ public void shutdown() {
311311
Thread.currentThread().interrupt();
312312
}
313313

314+
// if we created the materialization store ourselves we are responsible for shutting it down
315+
if (materializationStore instanceof RemoteMaterializationStore remoteMaterializationStore) {
316+
remoteMaterializationStore.shutdown();
317+
}
318+
314319
// wasmResolveApi.close() flushes logs and calls flagLogger.shutdown() which waits for pending
315320
// writes
316321
this.wasmResolveApi.close();
@@ -350,19 +355,15 @@ public ProviderEvaluation<Value> getObjectEvaluation(
350355
.build())
351356
.build();
352357

353-
try {
354-
resolveFlagResponse =
355-
wasmResolveApi
356-
.resolveWithSticky(
357-
ResolveWithStickyRequest.newBuilder()
358-
.setResolveRequest(req)
359-
.setFailFastOnSticky(false)
360-
.build())
361-
.toCompletableFuture()
362-
.get();
363-
} catch (MaterializationNotSupportedException e) {
364-
resolveFlagResponse = remoteResolve(req).get();
365-
}
358+
resolveFlagResponse =
359+
wasmResolveApi
360+
.resolveWithSticky(
361+
ResolveWithStickyRequest.newBuilder()
362+
.setResolveRequest(req)
363+
.setFailFastOnSticky(false)
364+
.build())
365+
.toCompletableFuture()
366+
.get();
366367

367368
if (resolveFlagResponse.getResolvedFlagsList().isEmpty()) {
368369
log.warn("No active flag '{}' was found", flagPath.getFlag());
@@ -413,13 +414,6 @@ public ProviderEvaluation<Value> getObjectEvaluation(
413414
}
414415
}
415416

416-
private CompletableFuture<ResolveFlagsResponse> remoteResolve(ResolveFlagsRequest req) {
417-
if (req.getFlagsList().isEmpty()) {
418-
return CompletableFuture.completedFuture(ResolveFlagsResponse.newBuilder().build());
419-
}
420-
return grpcFlagResolver.resolve(req);
421-
}
422-
423417
private static void handleStatusRuntimeException(StatusRuntimeException e) {
424418
if (e.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED) {
425419
log.error("Deadline exceeded when calling provider backend", e);

0 commit comments

Comments
 (0)