Skip to content

Commit 8328a85

Browse files
committed
chore: fixup format and merge conflicts
1 parent eb74ad4 commit 8328a85

11 files changed

+277
-248
lines changed

openfeature-provider/java/pom.xml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,6 @@
132132
</dependency>
133133

134134
<!-- Test dependencies -->
135-
<dependency>
136-
<groupId>org.slf4j</groupId>
137-
<artifactId>slf4j-simple</artifactId>
138-
<version>${slf4j.version}</version>
139-
<scope>test</scope>
140-
</dependency>
141135
<dependency>
142136
<groupId>ch.qos.logback</groupId>
143137
<artifactId>logback-classic</artifactId>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.spotify.confidence;
2+
3+
import java.io.IOException;
4+
import java.net.HttpURLConnection;
5+
import java.net.URL;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
/**
10+
* Default implementation of HttpClientFactory that creates standard HTTP connections.
11+
*
12+
* <p>This factory:
13+
*
14+
* <ul>
15+
* <li>Creates HttpURLConnection instances for the given URLs
16+
* <li>Uses default timeouts and settings from the JVM
17+
* <li>Can be extended or replaced for testing or custom behavior
18+
* </ul>
19+
*/
20+
public class DefaultHttpClientFactory implements HttpClientFactory {
21+
private static final Logger logger = LoggerFactory.getLogger(DefaultHttpClientFactory.class);
22+
23+
@Override
24+
public HttpURLConnection create(String url) throws IOException {
25+
return (HttpURLConnection) new URL(url).openConnection();
26+
}
27+
28+
@Override
29+
public void shutdown() {
30+
// HTTP connections are stateless and don't require cleanup
31+
logger.debug("DefaultHttpClientFactory shutdown called (no-op for stateless HTTP)");
32+
}
33+
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.io.IOException;
44
import java.io.InputStream;
55
import java.net.HttpURLConnection;
6-
import java.net.URL;
76
import java.nio.charset.StandardCharsets;
87
import java.security.MessageDigest;
98
import java.security.NoSuchAlgorithmException;
@@ -26,15 +25,17 @@ class FlagsAdminStateFetcher implements AccountStateProvider {
2625
"https://confidence-resolver-state-cdn.spotifycdn.com/";
2726

2827
private final String clientSecret;
28+
private final HttpClientFactory httpClientFactory;
2929
// ETag for conditional GETs of resolver state
3030
private final AtomicReference<String> etagHolder = new AtomicReference<>();
3131
private final AtomicReference<byte[]> rawResolverStateHolder =
3232
new AtomicReference<>(
3333
com.spotify.confidence.flags.admin.v1.ResolverState.newBuilder().build().toByteArray());
3434
private String accountId = "";
3535

36-
public FlagsAdminStateFetcher(String clientSecret) {
36+
public FlagsAdminStateFetcher(String clientSecret, HttpClientFactory httpClientFactory) {
3737
this.clientSecret = clientSecret;
38+
this.httpClientFactory = httpClientFactory;
3839
}
3940

4041
public AtomicReference<byte[]> rawStateHolder() {
@@ -64,7 +65,7 @@ private void fetchAndUpdateStateIfChanged() {
6465
// Build CDN URL using SHA256 hash of client secret
6566
final var cdnUrl = CDN_BASE_URL + sha256Hex(clientSecret);
6667
try {
67-
final HttpURLConnection conn = (HttpURLConnection) new URL(cdnUrl).openConnection();
68+
final HttpURLConnection conn = httpClientFactory.create(cdnUrl);
6869
final String previousEtag = etagHolder.get();
6970
if (previousEtag != null) {
7071
conn.setRequestProperty("if-none-match", previousEtag);

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

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,56 +32,36 @@ public class GrpcWasmFlagLogger implements WasmFlagLogger {
3232
private static final int MAX_FLAG_ASSIGNED_PER_CHUNK = 1000;
3333
private static final Duration DEFAULT_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10);
3434
private final InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceBlockingStub stub;
35-
private final String clientSecret;
3635
private final ExecutorService executorService;
3736
private final FlagLogWriter writer;
3837
private final Duration shutdownTimeout;
3938

4039
@VisibleForTesting
4140
public GrpcWasmFlagLogger(String clientSecret, FlagLogWriter writer) {
42-
this.stub = createStub(new DefaultChannelFactory());
43-
this.clientSecret = clientSecret;
41+
this.stub = createAuthStub(new DefaultChannelFactory(), clientSecret);
4442
this.executorService = Executors.newCachedThreadPool();
4543
this.writer = writer;
4644
this.shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT;
4745
}
4846

47+
@VisibleForTesting
48+
public GrpcWasmFlagLogger(String clientSecret, FlagLogWriter writer, Duration shutdownTimeout) {
49+
this.stub = createAuthStub(new DefaultChannelFactory(), clientSecret);
50+
this.executorService = Executors.newCachedThreadPool();
51+
this.writer = writer;
52+
this.shutdownTimeout = shutdownTimeout;
53+
}
54+
4955
public GrpcWasmFlagLogger(String clientSecret, ChannelFactory channelFactory) {
50-
this.stub = createStub(channelFactory);
51-
this.clientSecret = clientSecret;
56+
this.stub = createAuthStub(channelFactory, clientSecret);
5257
this.executorService = Executors.newCachedThreadPool();
5358
this.shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT;
5459
this.writer =
5560
request ->
5661
executorService.submit(
5762
() -> {
5863
try {
59-
// Create a stub with authorization header interceptor
60-
InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceBlockingStub
61-
stubWithAuth =
62-
stub.withInterceptors(
63-
new ClientInterceptor() {
64-
@Override
65-
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
66-
MethodDescriptor<ReqT, RespT> method,
67-
CallOptions callOptions,
68-
Channel next) {
69-
return new ForwardingClientCall.SimpleForwardingClientCall<
70-
ReqT, RespT>(next.newCall(method, callOptions)) {
71-
@Override
72-
public void start(
73-
Listener<RespT> responseListener, Metadata headers) {
74-
Metadata.Key<String> authKey =
75-
Metadata.Key.of(
76-
"authorization", Metadata.ASCII_STRING_MARSHALLER);
77-
headers.put(authKey, "ClientSecret " + clientSecret);
78-
super.start(responseListener, headers);
79-
}
80-
};
81-
}
82-
});
83-
84-
stubWithAuth.clientWriteFlagLogs(request);
64+
stub.clientWriteFlagLogs(request);
8565
logger.debug(
8666
"Successfully sent flag log with {} entries",
8767
request.getFlagAssignedCount());
@@ -91,10 +71,10 @@ public void start(
9171
});
9272
}
9373

94-
private static InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceBlockingStub createStub(
95-
ChannelFactory channelFactory) {
74+
private static InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceBlockingStub createAuthStub(
75+
ChannelFactory channelFactory, String clientSecret) {
9676
final var channel = createConfidenceChannel(channelFactory);
97-
return InternalFlagLoggerServiceGrpc.newBlockingStub(channel);
77+
return addAuthInterceptor(InternalFlagLoggerServiceGrpc.newBlockingStub(channel), clientSecret);
9878
}
9979

10080
@Override
@@ -187,7 +167,7 @@ public void writeSync(WriteFlagLogsRequest request) {
187167

188168
private void sendSync(WriteFlagLogsRequest request) {
189169
try {
190-
stub.writeFlagLogs(request);
170+
stub.clientWriteFlagLogs(request);
191171
logger.debug("Synchronously sent flag log with {} entries", request.getFlagAssignedCount());
192172
} catch (Exception e) {
193173
logger.error("Failed to write flag logs synchronously", e);
@@ -217,4 +197,28 @@ public void shutdown() {
217197
Thread.currentThread().interrupt();
218198
}
219199
}
200+
201+
private static InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceBlockingStub
202+
addAuthInterceptor(
203+
InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceBlockingStub stub,
204+
String clientSecret) {
205+
// Create a stub with authorization header interceptor
206+
return stub.withInterceptors(
207+
new ClientInterceptor() {
208+
@Override
209+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
210+
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
211+
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
212+
next.newCall(method, callOptions)) {
213+
@Override
214+
public void start(Listener<RespT> responseListener, Metadata headers) {
215+
Metadata.Key<String> authKey =
216+
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
217+
headers.put(authKey, "ClientSecret " + clientSecret);
218+
super.start(responseListener, headers);
219+
}
220+
};
221+
}
222+
});
223+
}
220224
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.spotify.confidence;
2+
3+
import java.io.IOException;
4+
import java.net.HttpURLConnection;
5+
6+
/**
7+
* HttpClientFactory is an advanced/testing hook allowing callers to customize how HTTP connections
8+
* are created. The provider will pass the URL that needs to be fetched.
9+
*
10+
* <p>Implementations may modify request properties, change URLs, or replace the connection creation
11+
* mechanism entirely. This is particularly useful for:
12+
*
13+
* <ul>
14+
* <li>Unit testing: inject mock HTTP responses
15+
* <li>Integration testing: point to local mock HTTP servers
16+
* <li>Production customization: custom timeouts, proxies, headers
17+
* <li>Debugging: add custom logging or request tracking
18+
* </ul>
19+
*
20+
* <p><strong>Lifecycle:</strong> The factory is responsible for managing any resources it creates.
21+
* When {@link #shutdown()} is called, it should clean up any resources that were allocated.
22+
*/
23+
public interface HttpClientFactory {
24+
/**
25+
* Creates an HTTP connection for the given URL.
26+
*
27+
* @param url the URL to connect to (e.g.,
28+
* "https://confidence-resolver-state-cdn.spotifycdn.com/...")
29+
* @return a configured HttpURLConnection
30+
* @throws IOException if an I/O error occurs while opening the connection
31+
*/
32+
HttpURLConnection create(String url) throws IOException;
33+
34+
/**
35+
* Shuts down this factory and cleans up any resources. This method should be called when the
36+
* provider is shutting down to ensure proper resource cleanup.
37+
*
38+
* <p>Implementations should clean up any resources that were created and wait for them to
39+
* terminate gracefully if applicable.
40+
*/
41+
void shutdown();
42+
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@
22

33
public class LocalProviderConfig {
44
private final ChannelFactory channelFactory;
5+
private final HttpClientFactory httpClientFactory;
56

67
public LocalProviderConfig() {
7-
this(null);
8+
this(null, null);
89
}
910

1011
public LocalProviderConfig(ChannelFactory channelFactory) {
12+
this(channelFactory, null);
13+
}
14+
15+
public LocalProviderConfig(ChannelFactory channelFactory, HttpClientFactory httpClientFactory) {
1116
this.channelFactory = channelFactory != null ? channelFactory : new DefaultChannelFactory();
17+
this.httpClientFactory =
18+
httpClientFactory != null ? httpClientFactory : new DefaultHttpClientFactory();
1219
}
1320

1421
public ChannelFactory getChannelFactory() {
1522
return channelFactory;
1623
}
24+
25+
public HttpClientFactory getHttpClientFactory() {
26+
return httpClientFactory;
27+
}
1728
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public OpenFeatureLocalResolveProvider(
160160
StickyResolveStrategy stickyResolveStrategy) {
161161
this.clientSecret = clientSecret;
162162
this.stickyResolveStrategy = stickyResolveStrategy;
163-
this.stateProvider = new FlagsAdminStateFetcher(clientSecret);
163+
this.stateProvider = new FlagsAdminStateFetcher(clientSecret, config.getHttpClientFactory());
164164
final var wasmFlagLogger = new GrpcWasmFlagLogger(clientSecret, config.getChannelFactory());
165165
this.wasmResolveApi = new ThreadLocalSwapWasmResolverApi(wasmFlagLogger, stickyResolveStrategy);
166166
this.channelFactory = config.getChannelFactory();
@@ -285,12 +285,12 @@ private <T> ProviderEvaluation<T> getCastedEvaluation(
285285
.build();
286286
}
287287

288-
@Override
289-
public void shutdown() {
290-
state.set(ProviderState.NOT_READY);
291-
log.debug("Shutting down scheduled executors");
292-
flagsFetcherExecutor.shutdown();
293-
logPollExecutor.shutdown();
288+
@Override
289+
public void shutdown() {
290+
state.set(ProviderState.NOT_READY);
291+
log.debug("Shutting down scheduled executors");
292+
flagsFetcherExecutor.shutdown();
293+
logPollExecutor.shutdown();
294294

295295
try {
296296
if (!flagsFetcherExecutor.awaitTermination(1, TimeUnit.SECONDS)) {

openfeature-provider/java/src/test/java/com/spotify/confidence/CapturingWasmFlagLogger.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public void write(WriteFlagLogsRequest request) {
4040
capturedRequests.add(request);
4141
}
4242

43+
@Override
44+
public void writeSync(WriteFlagLogsRequest request) {
45+
capturedRequests.add(request);
46+
}
47+
4348
@Override
4449
public void shutdown() {
4550
shutdownCalled = true;

0 commit comments

Comments
 (0)