Skip to content

Commit beb21f5

Browse files
feat: AccountStateProvider for testing (#265)
* feat: Add state provider interface * test: Provider interface tests * refactor: accountStateProvider
1 parent 8be9f7b commit beb21f5

File tree

6 files changed

+158
-141
lines changed

6 files changed

+158
-141
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.spotify.confidence;
2+
3+
/**
4+
* Functional interface for providing AccountState instances.
5+
*
6+
* <p>This interface allows custom implementations to provide AccountState data instead of using the
7+
* default FlagsAdminStateFetcher. This is useful for scenarios where flag data should be sourced
8+
* from custom locations or caching mechanisms.
9+
*
10+
* @since 0.2.4
11+
*/
12+
@FunctionalInterface
13+
public interface AccountStateProvider {
14+
15+
/**
16+
* Provides an AccountState instance.
17+
*
18+
* @return the AccountState containing flag configurations and metadata
19+
* @throws RuntimeException if the AccountState cannot be provided
20+
*/
21+
AccountState provide();
22+
}

openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java

Lines changed: 68 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
import com.google.common.annotations.VisibleForTesting;
55
import com.google.common.util.concurrent.ThreadFactoryBuilder;
66
import com.google.protobuf.Struct;
7+
import com.spotify.confidence.TokenHolder.Token;
78
import com.spotify.confidence.shaded.flags.admin.v1.FlagAdminServiceGrpc;
89
import com.spotify.confidence.shaded.flags.admin.v1.ResolverStateServiceGrpc;
10+
import com.spotify.confidence.shaded.flags.admin.v1.ResolverStateServiceGrpc.ResolverStateServiceBlockingStub;
911
import com.spotify.confidence.shaded.flags.resolver.v1.InternalFlagLoggerServiceGrpc;
1012
import com.spotify.confidence.shaded.flags.resolver.v1.Sdk;
1113
import com.spotify.confidence.shaded.iam.v1.AuthServiceGrpc;
12-
import com.spotify.confidence.shaded.iam.v1.ClientCredential;
14+
import com.spotify.confidence.shaded.iam.v1.AuthServiceGrpc.AuthServiceBlockingStub;
15+
import com.spotify.confidence.shaded.iam.v1.ClientCredential.ClientSecret;
1316
import io.grpc.Channel;
1417
import io.grpc.ClientInterceptors;
1518
import io.grpc.ManagedChannel;
@@ -18,6 +21,7 @@
1821
import java.time.Duration;
1922
import java.time.Instant;
2023
import java.util.List;
24+
import java.util.Map;
2125
import java.util.Optional;
2226
import java.util.concurrent.CompletableFuture;
2327
import java.util.concurrent.Executors;
@@ -26,6 +30,7 @@
2630
import java.util.concurrent.atomic.AtomicReference;
2731
import java.util.function.Supplier;
2832
import org.apache.commons.lang3.RandomStringUtils;
33+
import org.slf4j.LoggerFactory;
2934

3035
class LocalResolverServiceFactory implements ResolverServiceFactory {
3136

@@ -35,9 +40,7 @@ class LocalResolverServiceFactory implements ResolverServiceFactory {
3540
private final SwapWasmResolverApi wasmResolveApi;
3641
private final Supplier<Instant> timeSupplier;
3742
private final Supplier<String> resolveIdSupplier;
38-
private final ResolveLogger resolveLogger;
39-
private final AssignLogger assignLogger;
40-
private final boolean enableExposureLogs;
43+
private final FlagLogger flagLogger;
4144
private static final MetricRegistry metricRegistry = new MetricRegistry();
4245
private static final String CONFIDENCE_DOMAIN = "edge-grpc.spotify.com";
4346
private static final Duration ASSIGN_LOG_INTERVAL = Duration.ofSeconds(10);
@@ -60,30 +63,28 @@ private static ManagedChannel createConfidenceChannel() {
6063
}
6164

6265
static FlagResolverService from(ApiSecret apiSecret, String clientSecret, boolean isWasm) {
63-
return createFlagResolverService(apiSecret, clientSecret, isWasm, true);
66+
return createFlagResolverService(apiSecret, clientSecret, isWasm);
6467
}
6568

66-
static FlagResolverService from(
67-
ApiSecret apiSecret, String clientSecret, boolean isWasm, boolean enableExposureLogs) {
68-
return createFlagResolverService(apiSecret, clientSecret, isWasm, enableExposureLogs);
69+
static FlagResolverService from(AccountStateProvider accountStateProvider) {
70+
return createFlagResolverService(accountStateProvider);
6971
}
7072

7173
private static FlagResolverService createFlagResolverService(
72-
ApiSecret apiSecret, String clientSecret, boolean isWasm, boolean enableExposureLogs) {
74+
ApiSecret apiSecret, String clientSecret, boolean isWasm) {
7375
final var channel = createConfidenceChannel();
74-
final AuthServiceGrpc.AuthServiceBlockingStub authService =
75-
AuthServiceGrpc.newBlockingStub(channel);
76+
final AuthServiceBlockingStub authService = AuthServiceGrpc.newBlockingStub(channel);
7677
final TokenHolder tokenHolder =
7778
new TokenHolder(apiSecret.clientId(), apiSecret.clientSecret(), authService);
78-
final TokenHolder.Token token = tokenHolder.getToken();
79+
final Token token = tokenHolder.getToken();
7980
final Channel authenticatedChannel =
8081
ClientInterceptors.intercept(channel, new JwtAuthClientInterceptor(tokenHolder));
8182
final var flagLoggerStub = InternalFlagLoggerServiceGrpc.newBlockingStub(authenticatedChannel);
8283
final long assignLogCapacity =
8384
Optional.ofNullable(System.getenv("CONFIDENCE_ASSIGN_LOG_CAPACITY"))
8485
.map(Long::parseLong)
8586
.orElseGet(() -> (long) (Runtime.getRuntime().maxMemory() / 3.0));
86-
final ResolverStateServiceGrpc.ResolverStateServiceBlockingStub resolverStateService =
87+
final ResolverStateServiceBlockingStub resolverStateService =
8788
ResolverStateServiceGrpc.newBlockingStub(authenticatedChannel);
8889
final HealthStatusManager healthStatusManager = new HealthStatusManager();
8990
final HealthStatus healthStatus = new HealthStatus(healthStatusManager);
@@ -103,7 +104,7 @@ private static FlagResolverService createFlagResolverService(
103104
flagLoggerStub, ASSIGN_LOG_INTERVAL, metricRegistry, assignLogCapacity);
104105
final ResolveLogger resolveLogger =
105106
ResolveLogger.createStarted(() -> flagsAdminStub, RESOLVE_INFO_LOG_INTERVAL);
106-
final var flagLogger = getFlagLogger(resolveLogger, assignLogger, enableExposureLogs);
107+
final var flagLogger = getFlagLogger(resolveLogger, assignLogger);
107108
if (isWasm) {
108109
final SwapWasmResolverApi wasmResolverApi =
109110
new SwapWasmResolverApi(
@@ -117,96 +118,87 @@ private static FlagResolverService createFlagResolverService(
117118
pollIntervalSeconds,
118119
pollIntervalSeconds,
119120
TimeUnit.SECONDS);
120-
return new LocalResolverServiceFactory(
121-
wasmResolverApi,
122-
sidecarFlagsAdminFetcher.stateHolder(),
123-
resolveTokenConverter,
124-
resolveLogger,
125-
assignLogger,
126-
enableExposureLogs)
127-
.create(clientSecret);
121+
return request -> CompletableFuture.completedFuture(wasmResolverApi.resolve(request));
128122
} else {
129123
flagsFetcherExecutor.scheduleWithFixedDelay(
130124
sidecarFlagsAdminFetcher::reload,
131125
pollIntervalSeconds,
132126
pollIntervalSeconds,
133127
TimeUnit.SECONDS);
134128
return new LocalResolverServiceFactory(
135-
sidecarFlagsAdminFetcher.stateHolder(),
136-
resolveTokenConverter,
137-
resolveLogger,
138-
assignLogger,
139-
enableExposureLogs)
129+
sidecarFlagsAdminFetcher.stateHolder(), resolveTokenConverter, flagLogger)
140130
.create(clientSecret);
141131
}
142132
}
143133

144-
LocalResolverServiceFactory(
145-
SwapWasmResolverApi wasmResolveApi,
146-
AtomicReference<ResolverState> resolverStateHolder,
147-
ResolveTokenConverter resolveTokenConverter,
148-
ResolveLogger resolveLogger,
149-
AssignLogger assignLogger,
150-
boolean enableExposureLogs) {
151-
this(
152-
wasmResolveApi,
153-
resolverStateHolder,
154-
resolveTokenConverter,
155-
Instant::now,
156-
() -> RandomStringUtils.randomAlphanumeric(32),
157-
resolveLogger,
158-
assignLogger,
159-
enableExposureLogs);
160-
}
134+
private static FlagResolverService createFlagResolverService(
135+
AccountStateProvider accountStateProvider) {
136+
final long pollIntervalSeconds =
137+
Optional.ofNullable(System.getenv("CONFIDENCE_RESOLVER_POLL_INTERVAL_SECONDS"))
138+
.map(Long::parseLong)
139+
.orElse(Duration.ofMinutes(5).toSeconds());
140+
final AccountState initialAccountState = accountStateProvider.provide();
141+
final AtomicReference<ResolverState> stateHolder =
142+
new AtomicReference<>(
143+
new ResolverState(
144+
Map.of(initialAccountState.account().name(), initialAccountState),
145+
initialAccountState.secrets()));
146+
final AtomicReference<com.spotify.confidence.shaded.flags.admin.v1.ResolverState>
147+
rawStateHolder = new AtomicReference<>(stateHolder.get().toProto());
148+
final FlagLogger flagLogger = new NoopFlagLogger();
149+
final SwapWasmResolverApi wasmResolverApi =
150+
new SwapWasmResolverApi(flagLogger, rawStateHolder.get().toByteArray());
151+
flagsFetcherExecutor.scheduleAtFixedRate(
152+
() -> {
153+
try {
154+
final AccountState newAccountState = accountStateProvider.provide();
155+
final ResolverState newResolverState =
156+
new ResolverState(
157+
Map.of(newAccountState.account().name(), newAccountState),
158+
newAccountState.secrets());
159+
stateHolder.set(newResolverState);
161160

162-
LocalResolverServiceFactory(
163-
SwapWasmResolverApi wasmResolveApi,
164-
AtomicReference<ResolverState> resolverStateHolder,
165-
ResolveTokenConverter resolveTokenConverter,
166-
ResolveLogger resolveLogger,
167-
AssignLogger assignLogger) {
168-
this(
169-
wasmResolveApi,
170-
resolverStateHolder,
171-
resolveTokenConverter,
172-
Instant::now,
173-
() -> RandomStringUtils.randomAlphanumeric(32),
174-
resolveLogger,
175-
assignLogger,
176-
true);
161+
final com.spotify.confidence.shaded.flags.admin.v1.ResolverState newRawState =
162+
newResolverState.toProto();
163+
rawStateHolder.set(newRawState);
164+
wasmResolverApi.updateState(newRawState.toByteArray());
165+
} catch (Exception e) {
166+
LoggerFactory.getLogger(LocalResolverServiceFactory.class)
167+
.warn("Failed to refresh AccountState from provider, ignoring refresh", e);
168+
}
169+
},
170+
pollIntervalSeconds,
171+
pollIntervalSeconds,
172+
TimeUnit.SECONDS);
173+
174+
return request -> CompletableFuture.completedFuture(wasmResolverApi.resolve(request));
177175
}
178176

179177
LocalResolverServiceFactory(
180178
AtomicReference<ResolverState> resolverStateHolder,
181179
ResolveTokenConverter resolveTokenConverter,
182-
ResolveLogger resolveLogger,
183-
AssignLogger assignLogger,
184-
boolean enableExposureLogs) {
180+
FlagLogger flagLogger) {
185181
this(
186182
null,
187183
resolverStateHolder,
188184
resolveTokenConverter,
189185
Instant::now,
190186
() -> RandomStringUtils.randomAlphanumeric(32),
191-
resolveLogger,
192-
assignLogger,
193-
enableExposureLogs);
187+
flagLogger);
194188
}
195189

196190
LocalResolverServiceFactory(
191+
SwapWasmResolverApi wasmResolveApi,
197192
AtomicReference<ResolverState> resolverStateHolder,
198193
ResolveTokenConverter resolveTokenConverter,
199-
ResolveLogger resolveLogger,
200-
AssignLogger assignLogger) {
194+
FlagLogger flagLogger) {
201195
this(
202-
null,
196+
wasmResolveApi,
203197
resolverStateHolder,
204198
resolveTokenConverter,
205199
Instant::now,
206200
() -> RandomStringUtils.randomAlphanumeric(32),
207-
resolveLogger,
208-
assignLogger,
209-
true);
201+
flagLogger);
210202
}
211203

212204
LocalResolverServiceFactory(
@@ -215,17 +207,13 @@ private static FlagResolverService createFlagResolverService(
215207
ResolveTokenConverter resolveTokenConverter,
216208
Supplier<Instant> timeSupplier,
217209
Supplier<String> resolveIdSupplier,
218-
ResolveLogger resolveLogger,
219-
AssignLogger assignLogger,
220-
boolean enableExposureLogs) {
210+
FlagLogger flagLogger) {
221211
this.wasmResolveApi = wasmResolveApi;
222212
this.resolverStateHolder = resolverStateHolder;
223213
this.resolveTokenConverter = resolveTokenConverter;
224214
this.timeSupplier = timeSupplier;
225215
this.resolveIdSupplier = resolveIdSupplier;
226-
this.resolveLogger = resolveLogger;
227-
this.assignLogger = assignLogger;
228-
this.enableExposureLogs = enableExposureLogs;
216+
this.flagLogger = flagLogger;
229217
}
230218

231219
@VisibleForTesting
@@ -236,15 +224,11 @@ public void setState(byte[] state) {
236224
}
237225

238226
@Override
239-
public FlagResolverService create(ClientCredential.ClientSecret clientSecret) {
240-
if (wasmResolveApi != null) {
241-
return request -> CompletableFuture.completedFuture(wasmResolveApi.resolve(request));
242-
}
227+
public FlagResolverService create(ClientSecret clientSecret) {
243228
return createJavaFlagResolverService(clientSecret);
244229
}
245230

246-
private FlagResolverService createJavaFlagResolverService(
247-
ClientCredential.ClientSecret clientSecret) {
231+
private FlagResolverService createJavaFlagResolverService(ClientSecret clientSecret) {
248232
final ResolverState state = resolverStateHolder.get();
249233

250234
final AccountClient accountClient = state.secrets().get(clientSecret);
@@ -254,8 +238,6 @@ private FlagResolverService createJavaFlagResolverService(
254238
}
255239

256240
final AccountState accountState = state.accountStates().get(accountClient.accountName());
257-
final var flagLogger = getFlagLogger(resolveLogger, assignLogger, enableExposureLogs);
258-
259241
return new JavaFlagResolverService(
260242
accountState,
261243
accountClient,
@@ -265,31 +247,7 @@ private FlagResolverService createJavaFlagResolverService(
265247
resolveIdSupplier);
266248
}
267249

268-
private static FlagLogger getFlagLogger(
269-
ResolveLogger resolveLogger, AssignLogger assignLogger, boolean enableExposureLogs) {
270-
if (!enableExposureLogs) {
271-
return new FlagLogger() {
272-
@Override
273-
public void logResolve(
274-
String resolveId,
275-
Struct evaluationContext,
276-
Sdk sdk,
277-
AccountClient accountClient,
278-
List<ResolvedValue> values) {
279-
// Logging disabled - no-op
280-
}
281-
282-
@Override
283-
public void logAssigns(
284-
String resolveId,
285-
Sdk sdk,
286-
List<FlagToApply> flagsToApply,
287-
AccountClient accountClient) {
288-
// Logging disabled - no-op
289-
}
290-
};
291-
}
292-
250+
private static FlagLogger getFlagLogger(ResolveLogger resolveLogger, AssignLogger assignLogger) {
293251
return new FlagLogger() {
294252
@Override
295253
public void logResolve(
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.spotify.confidence;
2+
3+
import com.google.protobuf.Struct;
4+
import com.spotify.confidence.shaded.flags.resolver.v1.Sdk;
5+
import java.util.List;
6+
7+
public class NoopFlagLogger implements FlagLogger {
8+
9+
@Override
10+
public void logResolve(
11+
String resolveId,
12+
Struct evaluationContext,
13+
Sdk sdk,
14+
AccountClient accountClient,
15+
List<ResolvedValue> values) {
16+
// no-op
17+
}
18+
19+
@Override
20+
public void logAssigns(
21+
String resolveId, Sdk sdk, List<FlagToApply> flagsToApply, AccountClient accountClient) {
22+
// no-op
23+
}
24+
}

0 commit comments

Comments
 (0)