From ec21c0ee2bff00fb1998b321fea093046a22cc75 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 13 Oct 2025 13:41:39 +0200 Subject: [PATCH 1/8] feat: Remove native local provider --- .../java/com/spotify/confidence/Account.java | 4 - .../java/com/spotify/confidence/GrpcUtil.java | 20 ++ .../confidence/GrpcWasmFlagLogger.java | 21 +- .../LocalResolverServiceFactory.java | 214 +++--------------- .../spotify/confidence/NoopFlagLogger.java | 24 -- .../OpenFeatureLocalResolveProvider.java | 41 +--- .../com/spotify/confidence/ResolveTest.java | 2 +- .../java/com/spotify/confidence/TestBase.java | 11 +- 8 files changed, 59 insertions(+), 278 deletions(-) delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/NoopFlagLogger.java diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Account.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Account.java index 9644bf10..8e366987 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Account.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/Account.java @@ -11,8 +11,4 @@ record Account(String name, Optional region) { Account(String name) { this(name, Optional.empty()); } - - Region regionOrThrow() { - return region.orElseThrow(); - } } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/GrpcUtil.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/GrpcUtil.java index 8eb8c5b0..c18e3e05 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/GrpcUtil.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/GrpcUtil.java @@ -4,6 +4,10 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import java.time.Duration; +import java.util.Optional; import java.util.concurrent.CompletableFuture; /** @@ -12,6 +16,8 @@ */ final class GrpcUtil { + private static final String CONFIDENCE_DOMAIN = "edge-grpc.spotify.com"; + private GrpcUtil() {} static CompletableFuture toCompletableFuture(final ListenableFuture listenableFuture) { @@ -39,4 +45,18 @@ public void onFailure(Throwable t) { MoreExecutors.directExecutor()); return completableFuture; } + + static ManagedChannel createConfidenceChannel() { + final String confidenceDomain = + Optional.ofNullable(System.getenv("CONFIDENCE_DOMAIN")).orElse(CONFIDENCE_DOMAIN); + final boolean useGrpcPlaintext = + Optional.ofNullable(System.getenv("CONFIDENCE_GRPC_PLAINTEXT")) + .map(Boolean::parseBoolean) + .orElse(false); + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(confidenceDomain); + if (useGrpcPlaintext) { + builder = builder.usePlaintext(); + } + return builder.intercept(new DefaultDeadlineClientInterceptor(Duration.ofMinutes(1))).build(); + } } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/GrpcWasmFlagLogger.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/GrpcWasmFlagLogger.java index fcc3e1ec..8293c837 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/GrpcWasmFlagLogger.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/GrpcWasmFlagLogger.java @@ -1,17 +1,15 @@ package com.spotify.confidence; +import static com.spotify.confidence.GrpcUtil.createConfidenceChannel; + import com.google.common.annotations.VisibleForTesting; import com.spotify.confidence.shaded.flags.resolver.v1.InternalFlagLoggerServiceGrpc; import com.spotify.confidence.shaded.flags.resolver.v1.WriteFlagLogsRequest; import com.spotify.confidence.shaded.iam.v1.AuthServiceGrpc; import io.grpc.Channel; import io.grpc.ClientInterceptors; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import java.time.Duration; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.slf4j.Logger; @@ -23,7 +21,6 @@ interface FlagLogWriter { } public class GrpcWasmFlagLogger implements WasmFlagLogger { - private static final String CONFIDENCE_DOMAIN = "edge-grpc.spotify.com"; private static final Logger logger = LoggerFactory.getLogger(GrpcWasmFlagLogger.class); // Max number of flag_assigned entries per chunk to avoid exceeding gRPC max message size private static final int MAX_FLAG_ASSIGNED_PER_CHUNK = 1000; @@ -137,18 +134,4 @@ private void sendAsync(WriteFlagLogsRequest request) { public void shutdown() { executorService.shutdown(); } - - private static ManagedChannel createConfidenceChannel() { - final String confidenceDomain = - Optional.ofNullable(System.getenv("CONFIDENCE_DOMAIN")).orElse(CONFIDENCE_DOMAIN); - final boolean useGrpcPlaintext = - Optional.ofNullable(System.getenv("CONFIDENCE_GRPC_PLAINTEXT")) - .map(Boolean::parseBoolean) - .orElse(false); - ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(confidenceDomain); - if (useGrpcPlaintext) { - builder = builder.usePlaintext(); - } - return builder.intercept(new DefaultDeadlineClientInterceptor(Duration.ofMinutes(1))).build(); - } } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java index e7e7f06f..20bf4519 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java @@ -1,75 +1,38 @@ package com.spotify.confidence; -import com.codahale.metrics.MetricRegistry; +import static com.spotify.confidence.GrpcUtil.createConfidenceChannel; + import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import com.google.protobuf.Struct; import com.spotify.confidence.TokenHolder.Token; -import com.spotify.confidence.shaded.flags.admin.v1.FlagAdminServiceGrpc; -import com.spotify.confidence.shaded.flags.resolver.v1.InternalFlagLoggerServiceGrpc; import com.spotify.confidence.shaded.flags.resolver.v1.ResolverStateServiceGrpc; import com.spotify.confidence.shaded.flags.resolver.v1.ResolverStateServiceGrpc.ResolverStateServiceBlockingStub; -import com.spotify.confidence.shaded.flags.resolver.v1.Sdk; import com.spotify.confidence.shaded.flags.resolver.v1.WriteFlagLogsRequest; import com.spotify.confidence.shaded.iam.v1.AuthServiceGrpc; import com.spotify.confidence.shaded.iam.v1.AuthServiceGrpc.AuthServiceBlockingStub; import com.spotify.confidence.shaded.iam.v1.ClientCredential.ClientSecret; import io.grpc.Channel; import io.grpc.ClientInterceptors; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; import io.grpc.protobuf.services.HealthStatusManager; import java.time.Duration; -import java.time.Instant; -import java.util.List; import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import org.apache.commons.lang3.RandomStringUtils; class LocalResolverServiceFactory implements ResolverServiceFactory { - - private final AtomicReference resolverStateHolder; - private final ResolveTokenConverter resolveTokenConverter; - private final ResolverApi wasmResolveApi; - private final Supplier timeSupplier; - private final Supplier resolveIdSupplier; - private final FlagLogger flagLogger; - private static final MetricRegistry metricRegistry = new MetricRegistry(); - private static final String CONFIDENCE_DOMAIN = "edge-grpc.spotify.com"; - private static final Duration ASSIGN_LOG_INTERVAL = Duration.ofSeconds(10); private static final Duration POLL_LOG_INTERVAL = Duration.ofSeconds(10); private static final ScheduledExecutorService flagsFetcherExecutor = Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder().setDaemon(true).build()); - private static final Duration RESOLVE_INFO_LOG_INTERVAL = Duration.ofMinutes(1); private final StickyResolveStrategy stickyResolveStrategy; private static final ScheduledExecutorService logPollExecutor = Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder().setDaemon(true).build()); - private static ManagedChannel createConfidenceChannel() { - final String confidenceDomain = - Optional.ofNullable(System.getenv("CONFIDENCE_DOMAIN")).orElse(CONFIDENCE_DOMAIN); - final boolean useGrpcPlaintext = - Optional.ofNullable(System.getenv("CONFIDENCE_GRPC_PLAINTEXT")) - .map(Boolean::parseBoolean) - .orElse(false); - ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(confidenceDomain); - if (useGrpcPlaintext) { - builder = builder.usePlaintext(); - } - return builder.intercept(new DefaultDeadlineClientInterceptor(Duration.ofMinutes(1))).build(); - } - static FlagResolverService from( - ApiSecret apiSecret, - String clientSecret, - boolean isWasm, - StickyResolveStrategy stickyResolveStrategy) { - return createFlagResolverService(apiSecret, clientSecret, isWasm, stickyResolveStrategy); + ApiSecret apiSecret, StickyResolveStrategy stickyResolveStrategy) { + return createFlagResolverService(apiSecret, stickyResolveStrategy); } static FlagResolverService from( @@ -79,11 +42,8 @@ static FlagResolverService from( return createFlagResolverService(accountStateProvider, accountId, stickyResolveStrategy); } - private static FlagResolverService createFlagResolverService( - ApiSecret apiSecret, - String clientSecret, - boolean isWasm, - StickyResolveStrategy stickyResolveStrategy) { + static FlagResolverService createFlagResolverService( + ApiSecret apiSecret, StickyResolveStrategy stickyResolveStrategy) { final var channel = createConfidenceChannel(); final AuthServiceBlockingStub authService = AuthServiceGrpc.newBlockingStub(channel); final TokenHolder tokenHolder = @@ -101,68 +61,29 @@ private static FlagResolverService createFlagResolverService( Optional.ofNullable(System.getenv("CONFIDENCE_RESOLVER_POLL_INTERVAL_SECONDS")) .map(Long::parseLong) .orElse(Duration.ofMinutes(5).toSeconds()); - - final ResolveTokenConverter resolveTokenConverter = new PlainResolveTokenConverter(); - sidecarFlagsAdminFetcher.reload(); - final var wasmFlagLogger = new GrpcWasmFlagLogger(apiSecret); - if (isWasm) { - final ResolverApi wasmResolverApi = - new ThreadLocalSwapWasmResolverApi( - wasmFlagLogger, - sidecarFlagsAdminFetcher.rawStateHolder().get().toByteArray(), - sidecarFlagsAdminFetcher.accountId, - stickyResolveStrategy); - flagsFetcherExecutor.scheduleAtFixedRate( - sidecarFlagsAdminFetcher::reload, - pollIntervalSeconds, - pollIntervalSeconds, - TimeUnit.SECONDS); + final ResolverApi wasmResolverApi = + new ThreadLocalSwapWasmResolverApi( + wasmFlagLogger, + sidecarFlagsAdminFetcher.rawStateHolder().get().toByteArray(), + sidecarFlagsAdminFetcher.accountId, + stickyResolveStrategy); + flagsFetcherExecutor.scheduleAtFixedRate( + sidecarFlagsAdminFetcher::reload, + pollIntervalSeconds, + pollIntervalSeconds, + TimeUnit.SECONDS); - logPollExecutor.scheduleAtFixedRate( - () -> { + logPollExecutor.scheduleAtFixedRate( + () -> wasmResolverApi.updateStateAndFlushLogs( sidecarFlagsAdminFetcher.rawStateHolder().get().toByteArray(), - sidecarFlagsAdminFetcher.accountId); - }, - POLL_LOG_INTERVAL.getSeconds(), - POLL_LOG_INTERVAL.getSeconds(), - TimeUnit.SECONDS); - - return new WasmFlagResolverService(wasmResolverApi, stickyResolveStrategy); - } else { - flagsFetcherExecutor.scheduleWithFixedDelay( - sidecarFlagsAdminFetcher::reload, - pollIntervalSeconds, - pollIntervalSeconds, - TimeUnit.SECONDS); - // create java native local resolver - final var flagLoggerStub = - InternalFlagLoggerServiceGrpc.newBlockingStub(authenticatedChannel); - final long assignLogCapacity = - Optional.ofNullable(System.getenv("CONFIDENCE_ASSIGN_LOG_CAPACITY")) - .map(Long::parseLong) - .orElseGet(() -> (long) (Runtime.getRuntime().maxMemory() / 3.0)); - - final var flagsAdminStub = FlagAdminServiceGrpc.newBlockingStub(authenticatedChannel); - final AssignLogger assignLogger = - AssignLogger.createStarted( - flagLoggerStub, ASSIGN_LOG_INTERVAL, metricRegistry, assignLogCapacity); - final ResolveLogger resolveLogger = - ResolveLogger.createStarted(() -> flagsAdminStub, RESOLVE_INFO_LOG_INTERVAL); - final var flagLogger = getFlagLogger(resolveLogger, assignLogger); - - return new LocalResolverServiceFactory( - sidecarFlagsAdminFetcher.stateHolder(), - resolveTokenConverter, - flagLogger, - stickyResolveStrategy) - .create(clientSecret); - } - } + sidecarFlagsAdminFetcher.accountId), + POLL_LOG_INTERVAL.getSeconds(), + POLL_LOG_INTERVAL.getSeconds(), + TimeUnit.SECONDS); - private static boolean getFailFast(StickyResolveStrategy stickyResolveStrategy) { - return stickyResolveStrategy instanceof ResolverFallback; + return new WasmFlagResolverService(wasmResolverApi, stickyResolveStrategy); } private static FlagResolverService createFlagResolverService( @@ -204,51 +125,13 @@ public void shutdown() {} return new WasmFlagResolverService(wasmResolverApi, stickyResolveStrategy); } - LocalResolverServiceFactory( - AtomicReference resolverStateHolder, - ResolveTokenConverter resolveTokenConverter, - FlagLogger flagLogger, - StickyResolveStrategy stickyResolveStrategy) { - this( - null, - resolverStateHolder, - resolveTokenConverter, - Instant::now, - () -> RandomStringUtils.randomAlphanumeric(32), - flagLogger, - stickyResolveStrategy); - } - - LocalResolverServiceFactory( - ResolverApi wasmResolveApi, - AtomicReference resolverStateHolder, - ResolveTokenConverter resolveTokenConverter, - FlagLogger flagLogger, - StickyResolveStrategy stickyResolveStrategy) { - this( - wasmResolveApi, - resolverStateHolder, - resolveTokenConverter, - Instant::now, - () -> RandomStringUtils.randomAlphanumeric(32), - flagLogger, - stickyResolveStrategy); + LocalResolverServiceFactory(StickyResolveStrategy stickyResolveStrategy) { + this(null, stickyResolveStrategy); } LocalResolverServiceFactory( - ResolverApi wasmResolveApi, - AtomicReference resolverStateHolder, - ResolveTokenConverter resolveTokenConverter, - Supplier timeSupplier, - Supplier resolveIdSupplier, - FlagLogger flagLogger, - StickyResolveStrategy stickyResolveStrategy) { + ResolverApi wasmResolveApi, StickyResolveStrategy stickyResolveStrategy) { this.wasmResolveApi = wasmResolveApi; - this.resolverStateHolder = resolverStateHolder; - this.resolveTokenConverter = resolveTokenConverter; - this.timeSupplier = timeSupplier; - this.resolveIdSupplier = resolveIdSupplier; - this.flagLogger = flagLogger; this.stickyResolveStrategy = stickyResolveStrategy; } @@ -261,47 +144,6 @@ public void setState(byte[] state, String accountId) { @Override public FlagResolverService create(ClientSecret clientSecret) { - if (wasmResolveApi != null) { - return new WasmFlagResolverService(wasmResolveApi, stickyResolveStrategy); - } - return createJavaFlagResolverService(clientSecret); - } - - private FlagResolverService createJavaFlagResolverService(ClientSecret clientSecret) { - final ResolverState state = resolverStateHolder.get(); - - final AccountClient accountClient = state.secrets().get(clientSecret); - if (accountClient == null) { - throw new UnauthenticatedException("client secret not found"); - } - - final AccountState accountState = state.accountStates().get(accountClient.accountName()); - return new JavaFlagResolverService( - accountState, - accountClient, - flagLogger, - resolveTokenConverter, - timeSupplier, - resolveIdSupplier); - } - - private static FlagLogger getFlagLogger(ResolveLogger resolveLogger, AssignLogger assignLogger) { - return new FlagLogger() { - @Override - public void logResolve( - String resolveId, - Struct evaluationContext, - Sdk sdk, - AccountClient accountClient, - List values) { - resolveLogger.logResolve(resolveId, evaluationContext, accountClient, values); - } - - @Override - public void logAssigns( - String resolveId, Sdk sdk, List flagsToApply, AccountClient accountClient) { - assignLogger.logAssigns(resolveId, sdk, flagsToApply, accountClient); - } - }; + return new WasmFlagResolverService(wasmResolveApi, stickyResolveStrategy); } } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/NoopFlagLogger.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/NoopFlagLogger.java deleted file mode 100644 index 02ae9b4d..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/NoopFlagLogger.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.spotify.confidence; - -import com.google.protobuf.Struct; -import com.spotify.confidence.shaded.flags.resolver.v1.Sdk; -import java.util.List; - -public class NoopFlagLogger implements FlagLogger { - - @Override - public void logResolve( - String resolveId, - Struct evaluationContext, - Sdk sdk, - AccountClient accountClient, - List values) { - // no-op - } - - @Override - public void logAssigns( - String resolveId, Sdk sdk, List flagsToApply, AccountClient accountClient) { - // no-op - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java index 098345c9..207bf1d4 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java @@ -22,24 +22,9 @@ /** * OpenFeature provider for Confidence feature flags using local resolution. * - *

This provider evaluates feature flags locally using either a WebAssembly (WASM) resolver or a - * pure Java implementation. It periodically syncs flag configurations from the Confidence service - * and caches them locally for fast, low-latency flag evaluation. - * - *

The provider supports two resolution modes: - * - *

    - *
  • WASM mode (default): Uses a WebAssembly resolver - *
  • Java mode: Uses a pure Java resolver - *
- * - *

Resolution mode can be controlled via the {@code LOCAL_RESOLVE_MODE} environment variable: - * - *

    - *
  • {@code LOCAL_RESOLVE_MODE=WASM} - Forces WASM mode - *
  • {@code LOCAL_RESOLVE_MODE=JAVA} - Forces Java mode - *
  • Not set - Defaults to WASM mode - *
+ *

This provider evaluates feature flags locally using either a WebAssembly (WASM) resolver . It + * periodically syncs flag configurations from the Confidence service and caches them locally for + * fast, low-latency flag evaluation. * *

Usage Example: * @@ -78,9 +63,6 @@ public class OpenFeatureLocalResolveProvider implements FeatureProvider { * which provides fallback to the remote Confidence service when the WASM resolver encounters * missing materializations. By default, no retry strategy is applied. * - *

The provider will automatically determine the resolution mode (WASM or Java) based on the - * {@code LOCAL_RESOLVE_MODE} environment variable, defaulting to WASM mode. - * * @param apiSecret the API credentials containing client ID and client secret for authenticating * with the Confidence service. Create using {@code new ApiSecret("client-id", * "client-secret")} @@ -97,8 +79,7 @@ public OpenFeatureLocalResolveProvider(ApiSecret apiSecret, String clientSecret) * Creates a new OpenFeature provider for local flag resolution with full configuration control. * *

This is the primary constructor that allows full control over the provider configuration, - * including retry strategy. The provider will automatically determine the resolution mode (WASM - * or Java) based on the {@code LOCAL_RESOLVE_MODE} environment variable, defaulting to WASM mode. + * including retry strategy. * * @param apiSecret the API credentials containing client ID and client secret for authenticating * with the Confidence service. Create using {@code new ApiSecret("client-id", @@ -111,19 +92,9 @@ public OpenFeatureLocalResolveProvider(ApiSecret apiSecret, String clientSecret) */ public OpenFeatureLocalResolveProvider( ApiSecret apiSecret, String clientSecret, StickyResolveStrategy stickyResolveStrategy) { - final var env = System.getenv("LOCAL_RESOLVE_MODE"); - if (env != null && env.equals("WASM")) { - this.flagResolverService = - LocalResolverServiceFactory.from(apiSecret, clientSecret, true, stickyResolveStrategy); - } else if (env != null && env.equals("JAVA")) { - this.flagResolverService = - LocalResolverServiceFactory.from(apiSecret, clientSecret, false, stickyResolveStrategy); - } else { - this.flagResolverService = - LocalResolverServiceFactory.from(apiSecret, clientSecret, true, stickyResolveStrategy); - } - this.stickyResolveStrategy = stickyResolveStrategy; + this.flagResolverService = LocalResolverServiceFactory.from(apiSecret, stickyResolveStrategy); this.clientSecret = clientSecret; + this.stickyResolveStrategy = stickyResolveStrategy; } /** diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java index adf41004..6b5cc9f1 100644 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java +++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java @@ -182,7 +182,7 @@ abstract class ResolveTest extends TestBase { } protected ResolveTest(boolean isWasm) { - super(exampleState, isWasm); + super(exampleState); } @BeforeAll diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java index b881dba1..1c82cb42 100644 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java +++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java @@ -40,10 +40,8 @@ public class TestBase { .setClientSecret(secret) .build())); - protected TestBase(ResolverState state, boolean isWasm) { + protected TestBase(ResolverState state) { this.desiredState = state; - final ResolveTokenConverter resolveTokenConverter = new PlainResolveTokenConverter(); - if (isWasm) { final var wasmResolverApi = new SwapWasmResolverApi( new WasmFlagLogger() { @@ -58,12 +56,7 @@ public void shutdown() {} mockFallback); resolverServiceFactory = new LocalResolverServiceFactory( - wasmResolverApi, resolverState, resolveTokenConverter, mock(), mockFallback); - } else { - resolverServiceFactory = - new LocalResolverServiceFactory( - resolverState, resolveTokenConverter, mock(), mockFallback); - } + wasmResolverApi, mockFallback); } protected static void setup() {} From 3e40d3b50bb35a663f108df6e6a558723385938e Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 13 Oct 2025 13:55:07 +0200 Subject: [PATCH 2/8] feat: Clean and organize expressions --- .../spotify/confidence/AccountResolver.java | 1 + .../com/spotify/confidence/AccountState.java | 1 + .../confidence/FlagsAdminStateFetcher.java | 4 - .../confidence/JavaFlagResolverService.java | 116 ------------------ .../spotify/confidence/SemanticVersion.java | 4 +- .../java/com/spotify/confidence/Util.java | 13 +- .../confidence/{ => expressions}/And.java | 10 +- .../confidence/{ => expressions}/AndOr.java | 2 +- .../confidence/{ => expressions}/Eq.java | 4 +- .../confidence/{ => expressions}/Expr.java | 4 +- .../{ => expressions}/ExprNormalizer.java | 10 +- .../confidence/{ => expressions}/False.java | 2 +- .../confidence/{ => expressions}/Not.java | 2 +- .../confidence/{ => expressions}/Or.java | 2 +- .../confidence/{ => expressions}/Ord.java | 5 +- .../confidence/{ => expressions}/Ref.java | 2 +- .../{ => expressions}/TargetingExpr.java | 20 +-- .../confidence/{ => expressions}/True.java | 2 +- 18 files changed, 48 insertions(+), 156 deletions(-) delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/JavaFlagResolverService.java rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/And.java (80%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/AndOr.java (96%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/Eq.java (96%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/Expr.java (93%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/ExprNormalizer.java (91%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/False.java (87%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/Not.java (94%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/Or.java (94%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/Ord.java (95%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/Ref.java (85%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/TargetingExpr.java (82%) rename openfeature-provider-local/src/main/java/com/spotify/confidence/{ => expressions}/True.java (87%) diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountResolver.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountResolver.java index 6bcdf170..2a398ab7 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountResolver.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountResolver.java @@ -2,6 +2,7 @@ import com.google.protobuf.Struct; import com.google.protobuf.Value; +import com.spotify.confidence.expressions.TargetingExpr; import com.spotify.confidence.shaded.flags.admin.v1.Flag; import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule; import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule.Assignment; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountState.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountState.java index df2b22a9..6a7dd8d0 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountState.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountState.java @@ -2,6 +2,7 @@ import static com.spotify.confidence.Randomizer.MEGA_SALT; +import com.spotify.confidence.expressions.TargetingExpr; import com.spotify.confidence.shaded.flags.admin.v1.Flag; import com.spotify.confidence.shaded.flags.admin.v1.Segment; import com.spotify.confidence.shaded.iam.v1.ClientCredential.ClientSecret; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagsAdminStateFetcher.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagsAdminStateFetcher.java index 94b3604b..1f14bf6b 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagsAdminStateFetcher.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagsAdminStateFetcher.java @@ -58,10 +58,6 @@ public FlagsAdminStateFetcher( this.accountName = accountName; } - public AtomicReference stateHolder() { - return stateHolder; - } - public AtomicReference rawStateHolder() { return rawResolverStateHolder; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/JavaFlagResolverService.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/JavaFlagResolverService.java deleted file mode 100644 index 14552eb2..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/JavaFlagResolverService.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.spotify.confidence; - -import static com.spotify.confidence.ResolveTokenConverter.toAssignedFlag; - -import com.google.protobuf.ByteString; -import com.google.protobuf.Struct; -import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsRequest; -import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse; -import com.spotify.confidence.shaded.flags.resolver.v1.ResolvedFlag; -import com.spotify.confidence.shaded.flags.types.v1.FlagSchema; -import java.time.Instant; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -record JavaFlagResolverService( - AccountState accountState, - AccountClient accountClient, - FlagLogger flagLogger, - ResolveTokenConverter resolveTokenConverter, - Supplier timeSupplier, - Supplier resolveIdSupplier) - implements FlagResolverService { - private static final Logger logger = LoggerFactory.getLogger(JavaFlagResolverService.class); - - @Override - public CompletableFuture resolveFlags(ResolveFlagsRequest request) { - final Instant now = timeSupplier.get(); - - final AccountResolver resolver = getAccountResolver(request.getEvaluationContext()); - return resolver - .resolveFlags(request.getFlagsList()) - .thenApply( - resolvedValues -> { - final String resolveId = generateResolveId(); - final ResolveFlagsResponse.Builder responseBuilder = - ResolveFlagsResponse.newBuilder() - .setResolveToken(ByteString.EMPTY) - .setResolveId(resolveId); - - resolvedValues.stream() - .map(this::toResolvedFlag) - .forEach(responseBuilder::addResolvedFlags); - - if (request.getApply()) { - if (!resolvedValues.isEmpty()) { - flagLogger.logAssigns( - resolveId, - request.getSdk(), - toFlagsToApply(resolvedValues, now), - resolver.getClient()); - } - } else { - final ByteString resolveToken = - resolveTokenConverter.createResolveToken( - accountState.account().name(), - resolveId, - resolvedValues, - resolver.getEvaluationContext()); - responseBuilder.setResolveToken(resolveToken); - } - try { - flagLogger.logResolve( - resolveId, - resolver.getEvaluationContext(), - request.getSdk(), - resolver.getClient(), - resolvedValues); - } catch (Exception ex) { - logger.warn("Could not send the log resolve", ex); - } - return responseBuilder.build(); - }); - } - - private List toFlagsToApply(List resolvedValues, Instant now) { - return resolvedValues.stream() - .map(resolvedValue -> new FlagToApply(now, toAssignedFlag(resolvedValue))) - .toList(); - } - - @Override - public void close() {} - - private ResolvedFlag toResolvedFlag(ResolvedValue resolvedValue) { - final var builder = - ResolvedFlag.newBuilder() - .setFlag(resolvedValue.flag().getName()) - .setReason(resolvedValue.reason()); - - if (resolvedValue.matchedAssignment().isEmpty()) { - return builder.build(); - } - - final AssignmentMatch match = resolvedValue.matchedAssignment().get(); - - return builder - .setVariant(match.variant().orElse("")) - .setValue(match.value().orElse(Struct.getDefaultInstance())) - .setFlagSchema( - match.value().isPresent() - ? resolvedValue.flag().getSchema() - : FlagSchema.StructFlagSchema.getDefaultInstance()) - .build(); - } - - private AccountResolver getAccountResolver(Struct evaluationContext) { - return new AccountResolver(accountClient, accountState, evaluationContext, logger); - } - - private String generateResolveId() { - return resolveIdSupplier.get(); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java index 2cfa3c67..c10abb84 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java @@ -5,7 +5,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -class SemanticVersion implements Comparable { +public class SemanticVersion implements Comparable { private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d{1,3})(\\.\\d{1,3})(\\.\\d{1,3})?(\\.\\d{1,10})?$"); @@ -29,7 +29,7 @@ private SemanticVersion(int major, int minor, int patch, int tag) { * @return instance of Semantic version * @throws IllegalArgumentException in case an invalid Semver is provided */ - static SemanticVersion fromVersionString(final String version) { + public static SemanticVersion fromVersionString(final String version) { if (version == null || version.isEmpty()) { throw new IllegalArgumentException("Invalid version, version must be non-empty and not null"); } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java index c80ccf58..9e5f7c68 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java @@ -1,5 +1,11 @@ package com.spotify.confidence; +import static com.spotify.confidence.expressions.Expr.Type.AND; +import static com.spotify.confidence.expressions.Expr.Type.FALSE; +import static com.spotify.confidence.expressions.Expr.Type.NOT; +import static com.spotify.confidence.expressions.Expr.Type.OR; +import static com.spotify.confidence.expressions.Expr.Type.REF; +import static com.spotify.confidence.expressions.Expr.Type.TRUE; import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.EndCase.END_EXCLUSIVE; import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.EndCase.END_INCLUSIVE; import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.EndCase.END_NOT_SET; @@ -8,6 +14,9 @@ import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.StartCase.START_NOT_SET; import static java.util.stream.Collectors.toList; +import com.spotify.confidence.expressions.Eq; +import com.spotify.confidence.expressions.Expr; +import com.spotify.confidence.expressions.Ord; import com.spotify.confidence.shaded.flags.types.v1.Targeting.EqRule; import com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule; import com.spotify.confidence.shaded.flags.types.v1.Targeting.SetRule; @@ -17,11 +26,11 @@ import java.util.Set; import java.util.stream.Stream; -final class Util { +public final class Util { private Util() {} - static boolean evalExpression(Expr expr, Set trueRefs) { + public static boolean evalExpression(Expr expr, Set trueRefs) { return switch (expr.type()) { case TRUE -> true; case FALSE -> false; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/And.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/And.java similarity index 80% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/And.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/And.java index 478c1297..fd680166 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/And.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/And.java @@ -1,4 +1,4 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import static java.util.stream.Collectors.toSet; @@ -15,17 +15,17 @@ public Type type() { @Override public Expr simplify() { final Set reduced = - reduceNegatedPairsTo(F) + reduceNegatedPairsTo(Expr.F) .filter(o -> !o.isTrue()) .flatMap(o -> o.isAnd() ? o.operands().stream() : Stream.of(o)) .collect(toSet()); - if (reduced.contains(F)) { - return F; + if (reduced.contains(Expr.F)) { + return Expr.F; } else if (reduced.size() == 1) { return reduced.iterator().next(); } else if (reduced.isEmpty()) { - return T; + return Expr.T; } return Expr.and(reduced); } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/AndOr.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/AndOr.java similarity index 96% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/AndOr.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/AndOr.java index 58d20cd2..abfa8001 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/AndOr.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/AndOr.java @@ -1,4 +1,4 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toSet; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Eq.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Eq.java similarity index 96% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/Eq.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Eq.java index 8ef88057..3b2e10d1 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Eq.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Eq.java @@ -1,11 +1,11 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import com.spotify.confidence.shaded.flags.types.v1.Targeting; import java.math.BigDecimal; import java.util.List; import java.util.OptionalLong; -interface Eq { +public interface Eq { double EPSILON = 0.00000001d; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Expr.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Expr.java similarity index 93% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/Expr.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Expr.java index d66480b0..a74bc59a 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Expr.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Expr.java @@ -1,4 +1,4 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import static java.util.Collections.unmodifiableSet; import static java.util.Objects.requireNonNull; @@ -11,7 +11,7 @@ import java.util.TreeSet; /* Expr ADT written with Java 15 sealed interfaces and records */ -sealed interface Expr extends Comparable permits AndOr, False, Not, Ref, True { +public sealed interface Expr extends Comparable permits AndOr, False, Not, Ref, True { Expr T = new True(); Expr F = new False(); diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ExprNormalizer.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/ExprNormalizer.java similarity index 91% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/ExprNormalizer.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/ExprNormalizer.java index 8fa837f9..b4bf1e42 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ExprNormalizer.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/ExprNormalizer.java @@ -1,11 +1,11 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; -import static com.spotify.confidence.Expr.and; -import static com.spotify.confidence.Expr.not; -import static com.spotify.confidence.Expr.or; +import static com.spotify.confidence.expressions.Expr.and; +import static com.spotify.confidence.expressions.Expr.not; +import static com.spotify.confidence.expressions.Expr.or; import static java.util.stream.Collectors.toList; -import com.spotify.confidence.Expr.Type; +import com.spotify.confidence.expressions.Expr.Type; import java.util.ArrayList; import java.util.List; import java.util.Set; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/False.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/False.java similarity index 87% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/False.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/False.java index 0b9c4264..bb14c202 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/False.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/False.java @@ -1,4 +1,4 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import java.util.Set; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Not.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Not.java similarity index 94% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/Not.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Not.java index b63b3c75..7d9ef130 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Not.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Not.java @@ -1,4 +1,4 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import java.util.Set; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Or.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Or.java similarity index 94% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/Or.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Or.java index de348c5a..ea8d6bfb 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Or.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Or.java @@ -1,4 +1,4 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import static java.util.stream.Collectors.toSet; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Ord.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ord.java similarity index 95% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/Ord.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ord.java index 231fb37e..8ea85652 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Ord.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ord.java @@ -1,10 +1,11 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import static com.google.protobuf.util.Timestamps.toNanos; +import com.spotify.confidence.SemanticVersion; import com.spotify.confidence.shaded.flags.types.v1.Targeting; -interface Ord { +public interface Ord { boolean lt(Targeting.Value a, Targeting.Value b); diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Ref.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ref.java similarity index 85% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/Ref.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ref.java index 7eb9bb09..ceaee295 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Ref.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ref.java @@ -1,4 +1,4 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import java.util.Set; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/TargetingExpr.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/TargetingExpr.java similarity index 82% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/TargetingExpr.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/TargetingExpr.java index 44fb893a..83aa3bfc 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/TargetingExpr.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/TargetingExpr.java @@ -1,11 +1,11 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; -import static com.spotify.confidence.Expr.T; -import static com.spotify.confidence.Expr.and; -import static com.spotify.confidence.Expr.not; -import static com.spotify.confidence.Expr.or; -import static com.spotify.confidence.Expr.ref; import static com.spotify.confidence.Util.evalExpression; +import static com.spotify.confidence.expressions.Expr.T; +import static com.spotify.confidence.expressions.Expr.and; +import static com.spotify.confidence.expressions.Expr.not; +import static com.spotify.confidence.expressions.Expr.or; +import static com.spotify.confidence.expressions.Expr.ref; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; @@ -17,7 +17,7 @@ import java.util.Map; import java.util.Set; -class TargetingExpr { +public class TargetingExpr { private final Expr expression; private final Map refs; @@ -30,11 +30,11 @@ class TargetingExpr { this.refs = requireNonNull(refs); } - Map refs() { + public Map refs() { return refs; } - boolean eval(Set trueRefs) { + public boolean eval(Set trueRefs) { return evalExpression(expression, trueRefs); } @@ -43,7 +43,7 @@ public String toString() { return "TargetingExpr{" + "expression=" + expression + '}'; } - static TargetingExpr fromTargeting(final Targeting targeting) { + public static TargetingExpr fromTargeting(final Targeting targeting) { final Expr expr = ExprNormalizer.normalize(convert(targeting.getExpression())); final Set includedRefs = new HashSet<>(); collectRefs(expr, includedRefs); diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/True.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/True.java similarity index 87% rename from openfeature-provider-local/src/main/java/com/spotify/confidence/True.java rename to openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/True.java index 763d0297..0d4d52cc 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/True.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/True.java @@ -1,4 +1,4 @@ -package com.spotify.confidence; +package com.spotify.confidence.expressions; import java.util.Set; From 87538d4972945d19abbb608e256cc9747ffad7de Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 13 Oct 2025 14:12:43 +0200 Subject: [PATCH 3/8] feat: Further cleanups of native resolver --- openfeature-provider-local/README.md | 18 +- openfeature-provider-local/pom.xml | 1 + .../spotify/confidence/AccountResolver.java | 465 ------------------ .../PlainResolveTokenConverter.java | 27 - .../confidence/ResolveFlagListener.java | 31 -- .../com/spotify/confidence/ResolveLogger.java | 267 ---------- .../confidence/ResolveTokenConverter.java | 65 --- .../java/com/spotify/confidence/TestBase.java | 26 +- 8 files changed, 14 insertions(+), 886 deletions(-) delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/AccountResolver.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/PlainResolveTokenConverter.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveFlagListener.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveLogger.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveTokenConverter.java diff --git a/openfeature-provider-local/README.md b/openfeature-provider-local/README.md index bf848375..ca62c8c7 100644 --- a/openfeature-provider-local/README.md +++ b/openfeature-provider-local/README.md @@ -6,7 +6,7 @@ A high-performance OpenFeature provider for [Confidence](https://confidence.spot ## Features -- **Local Resolution**: Evaluates feature flags locally using WebAssembly (WASM) or pure Java +- **Local Resolution**: Evaluates feature flags locally using WebAssembly (WASM) - **Low Latency**: No network calls during flag evaluation - **Automatic Sync**: Periodically syncs flag configurations from Confidence - **Exposure Logging**: Fully supported exposure logging (and other resolve analytics) @@ -49,22 +49,6 @@ String value = client.getStringValue("my-flag", "default-value"); ## Configuration -### Resolution Modes - -The provider supports two resolution modes: - -- **WASM mode** (default): Uses WebAssembly resolver -- **Java mode**: Pure Java implementation of the resolver - -Control the mode with the `LOCAL_RESOLVE_MODE` environment variable: - -```bash -# Force WASM mode -export LOCAL_RESOLVE_MODE=WASM - -# Force Java mode -export LOCAL_RESOLVE_MODE=JAVA -``` ### Exposure Logging diff --git a/openfeature-provider-local/pom.xml b/openfeature-provider-local/pom.xml index 6c76d8bd..ee44a9fc 100644 --- a/openfeature-provider-local/pom.xml +++ b/openfeature-provider-local/pom.xml @@ -147,6 +147,7 @@ org.apache.commons commons-lang3 3.17.0 + test io.dropwizard.metrics diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountResolver.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountResolver.java deleted file mode 100644 index 2a398ab7..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountResolver.java +++ /dev/null @@ -1,465 +0,0 @@ -package com.spotify.confidence; - -import com.google.protobuf.Struct; -import com.google.protobuf.Value; -import com.spotify.confidence.expressions.TargetingExpr; -import com.spotify.confidence.shaded.flags.admin.v1.Flag; -import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule; -import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule.Assignment; -import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule.Assignment.AssignmentCase; -import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule.AssignmentSpec; -import com.spotify.confidence.shaded.flags.admin.v1.Flag.State; -import com.spotify.confidence.shaded.flags.admin.v1.Flag.Variant; -import com.spotify.confidence.shaded.flags.admin.v1.Segment; -import com.spotify.confidence.shaded.flags.resolver.v1.ResolveReason; -import com.spotify.confidence.shaded.flags.types.v1.Targeting; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.Criterion; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.Criterion.AttributeCriterion; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.InnerRule; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.ListValue; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.Value.ValueCase; -import com.spotify.futures.CompletableFutures; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import org.slf4j.Logger; - -class AccountResolver { - static final String TARGETING_KEY = "targeting_key"; - private static final int MAX_NO_OF_FLAGS_TO_BATCH_RESOLVE = 200; - private final AccountClient client; - private final AccountState state; - private final Struct evaluationContext; - private final Logger logger; - - private static final ResolveFlagListener NO_OP_RESOLVE_FLAG_LISTENER = - new ResolveFlagListener() {}; - - AccountResolver( - AccountClient client, AccountState state, Struct evaluationContext, Logger logger) { - this.client = client; - this.state = state; - this.evaluationContext = evaluationContext; - this.logger = logger; - } - - AccountClient getClient() { - return client; - } - - Struct getEvaluationContext() { - return evaluationContext; - } - - CompletableFuture> resolveFlags(List flagNames) { - return resolveFlags(flagNames, NO_OP_RESOLVE_FLAG_LISTENER); - } - - CompletableFuture> resolveFlags( - List flagNames, ResolveFlagListener listener) { - return resolveFlags(flagNames, listener, false); - } - - CompletableFuture> resolveFlags( - List flagNames, ResolveFlagListener listener, boolean strict) { - final List flagsToResolve = - state.flags().values().stream() - .filter(flag -> flag.getClientsList().contains(client.client().getName())) - .filter(flag -> flag.getState() == State.ACTIVE) - .filter(flag -> flagNames.isEmpty() || flagNames.contains(flag.getName())) - .toList(); - - if (strict && flagsToResolve.isEmpty() && !flagNames.isEmpty()) { - // explicitly declared flags to resolve but no active flags found - throw new BadRequestException( - "No active flags found for the client %s with the names %s" - .formatted(client.client().getName(), flagNames)); - } - if (flagsToResolve.size() > MAX_NO_OF_FLAGS_TO_BATCH_RESOLVE) { - throw new BadRequestException( - "Max %d flags allowed in a single resolve request, this request would return %d flags." - .formatted(MAX_NO_OF_FLAGS_TO_BATCH_RESOLVE, flagsToResolve.size())); - } - final ConcurrentMap convertValueCache = - new ConcurrentHashMap<>(); - - final var futures = - flagsToResolve.stream() - .map( - flag -> { - try { - return resolveFlag(flag, listener, convertValueCache); - } catch (BadRequestException badRequestException) { - return CompletableFuture.failedFuture(badRequestException); - } catch (RuntimeException error) { - logger.error("Error during resolve", error); - return CompletableFuture.completedFuture( - new ResolvedValue(flag).withReason(ResolveReason.RESOLVE_REASON_ERROR)); - } - }) - .toList(); - return CompletableFutures.allAsList(futures); - } - - private CompletableFuture resolveFlag( - final Flag flag, - ResolveFlagListener listener, - ConcurrentMap convertValueCache) { - ResolvedValue resolvedValue = new ResolvedValue(flag); - - if (flag.getState() == State.ARCHIVED) { - return CompletableFuture.completedFuture( - resolvedValue.withReason(ResolveReason.RESOLVE_REASON_FLAG_ARCHIVED)); - } - - for (Rule rule : flag.getRulesList()) { - if (rule.hasMaterializationSpec()) { - logger.warn( - "Flag {} has a sticky rule, sticky assignments are not supported in the local resolve", - flag.getName()); - continue; - } - if (!rule.getEnabled()) { - listener.markRuleEvaluationReason( - rule.getName(), ResolveFlagListener.RuleEvaluationReason.RULE_NOT_ENABLED); - continue; - } - - final String segmentName = rule.getSegment(); - final Segment segment = state.segments().get(segmentName); - if (segment == null) { - logger.warn("Segment {} not found among active segments", rule.getSegment()); - listener.markRuleEvaluationReason( - rule.getName(), - ResolveFlagListener.RuleEvaluationReason.SEGMENT_NOT_FOUND_OR_NOT_ACTIVE); - continue; - } - - final String targetingKey = - rule.getTargetingKeySelector().isBlank() ? TARGETING_KEY : rule.getTargetingKeySelector(); - final Value unitValue = EvalUtil.getAttributeValue(evaluationContext, targetingKey); - if (unitValue.hasNullValue()) { - listener.markRuleEvaluationReason( - rule.getName(), ResolveFlagListener.RuleEvaluationReason.SEGMENT_NOT_MATCHED); - listener.markSegmentEvaluationReason( - flag.getName(), - segment.getName(), - ResolveFlagListener.SegmentEvaluationReason.TARGETING_KEY_MISSING); - continue; - } - - final String unit; - if (unitValue.getKindCase() == Value.KindCase.STRING_VALUE) { - unit = unitValue.getStringValue(); - } else if (unitValue.getKindCase() == Value.KindCase.NUMBER_VALUE) { - // Cast integer values to string for targeting key compatibility - final double numberValue = unitValue.getNumberValue(); - // Only support integers (no fractional part) - if (numberValue == Math.floor(numberValue) && !Double.isInfinite(numberValue)) { - unit = String.valueOf((long) numberValue); - } else { - return CompletableFuture.completedFuture( - resolvedValue.withReason(ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR)); - } - } else { - return CompletableFuture.completedFuture( - resolvedValue.withReason(ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR)); - } - if (unit.length() > 100) { - throw new BadRequestException("Targeting key is too larger, max 100 characters."); - } - - if (!segmentMatches( - segment, segmentName, unit, flag.getName(), listener, convertValueCache)) { - listener.markRuleEvaluationReason( - rule.getName(), ResolveFlagListener.RuleEvaluationReason.SEGMENT_NOT_MATCHED); - continue; - } - - listener.markSegmentEvaluationReason( - flag.getName(), - segment.getName(), - ResolveFlagListener.SegmentEvaluationReason.SEGMENT_MATCHED); - - final AssignmentSpec spec = rule.getAssignmentSpec(); - final int bucketCount = spec.getBucketCount(); - - // hash bucket for targetingKey with salt from segment - final String variantSalt = segmentName.split("/")[1]; - final long bucket = Randomizer.getBucket(unit, variantSalt, bucketCount); - - final Optional matchedAssignmentOpt = - spec.getAssignmentsList().stream() - .filter(variant -> Randomizer.coversBucket(variant, bucket)) - .findFirst(); - - if (matchedAssignmentOpt.isPresent()) { - final Assignment matchedAssignment = matchedAssignmentOpt.get(); - if (matchedAssignment.getAssignmentCase() == AssignmentCase.FALLTHROUGH) { - listener.markRuleEvaluationReason( - rule.getName(), ResolveFlagListener.RuleEvaluationReason.RULE_MATCHED_FALLTHROUGH); - resolvedValue = - resolvedValue.attributeFallthroughRule( - rule, matchedAssignment.getAssignmentId(), unit); - continue; - } - - if (matchedAssignment.getAssignmentCase() == AssignmentCase.CLIENT_DEFAULT) { - return CompletableFuture.completedFuture( - resolvedValue.withClientDefaultMatch( - matchedAssignment.getAssignmentId(), unit, segment, rule)); - } - - if (matchedAssignment.getAssignmentCase() == AssignmentCase.VARIANT) { - return CompletableFuture.completedFuture( - variantMatch(resolvedValue, matchedAssignment, unit, segment, rule, flag)); - } - } else { - listener.markRuleEvaluationReason( - rule.getName(), - ResolveFlagListener.RuleEvaluationReason.RULE_EVALUATED_NO_VARIANT_MATCH); - } - } - - return CompletableFuture.completedFuture( - resolvedValue.withReason(ResolveReason.RESOLVE_REASON_NO_SEGMENT_MATCH)); - } - - private ResolvedValue variantMatch( - ResolvedValue resolvedValue, - Assignment matchedAssignment, - String unit, - Segment segment, - Rule rule, - Flag flag) { - final String variantName = matchedAssignment.getVariant().getVariant(); - final Flag.Variant variant = findVariantOrThrow(flag.getVariantsList(), variantName); - - return resolvedValue.withMatch( - matchedAssignment.getAssignmentId(), variantName, unit, variant.getValue(), segment, rule); - } - - private boolean isTargetingMatch( - Segment segment, - final String unit, - Set visitedSegments, - final String flag, - ResolveFlagListener listener, - ConcurrentMap convertValueCache) { - final TargetingExpr targetingExpr = state.getTargetingExpr(segment); - final Set satisfiedRefs = new HashSet<>(targetingExpr.refs().size()); - for (var entry : targetingExpr.refs().entrySet()) { - final boolean matches = - switch (entry.getValue().getCriterionCase()) { - case ATTRIBUTE -> { - final AttributeCriterion criterion = entry.getValue().getAttribute(); - final ValueCase expectedPrimitiveType = - getExpectedPrimitiveType(criterion, segment, entry); - final Value attributeValue = - EvalUtil.getAttributeValue(evaluationContext, criterion.getAttributeName()); - - if (attributeValue.hasNullValue()) { - listener.addEvalContextMissingValue( - flag, segment.getName(), criterion.getAttributeName()); - } - - final ListValue convertedValueList = - convertValueCache.computeIfAbsent( - new ConvertValueCacheKey(attributeValue, expectedPrimitiveType), - ignored -> - listWrapper( - EvalUtil.convertToTargetingValue( - attributeValue, expectedPrimitiveType))); - yield resolveAttributeCriterion(segment, entry, criterion, convertedValueList); - } - case SEGMENT -> { - final var segmentName = entry.getValue().getSegment().getSegment(); - yield segmentMatches( - state.segments().get(segmentName), - segmentName, - unit, - visitedSegments, - flag, - listener, - convertValueCache); - } - default -> throw new UnsupportedOperationException(); - }; - if (matches) { - satisfiedRefs.add(entry.getKey()); - } - } - return targetingExpr.eval(satisfiedRefs); - } - - private static boolean resolveAttributeCriterion( - Segment segment, - Entry entry, - AttributeCriterion criterion, - ListValue convertedValueList) { - return switch (criterion.getRuleCase()) { - case EQ_RULE -> convertedValueList.getValuesList().contains(criterion.getEqRule().getValue()); - case SET_RULE -> - convertedValueList.getValuesList().stream() - .anyMatch(v -> criterion.getSetRule().getValuesList().contains(v)); - case RANGE_RULE -> - convertedValueList.getValuesList().stream() - .anyMatch(v -> EvalUtil.isInRange(criterion.getRangeRule(), v)); - case ANY_RULE -> - convertedValueList.getValuesList().stream() - .anyMatch( - v -> - resolveAttributeCriterion( - segment, entry, criterion.getAnyRule().getRule(), v)); - case ALL_RULE -> - convertedValueList.getValuesList().stream() - .allMatch( - v -> - resolveAttributeCriterion( - segment, entry, criterion.getAllRule().getRule(), v)); - default -> - throw new BadRequestException( - "Targeting rule %s in %s is invalid".formatted(entry.getValue(), segment.getName())); - }; - } - - private static boolean resolveAttributeCriterion( - Segment segment, - Entry entry, - InnerRule rule, - Targeting.Value convertedValue) { - return switch (rule.getRuleCase()) { - case EQ_RULE -> rule.getEqRule().getValue().equals(convertedValue); - case SET_RULE -> rule.getSetRule().getValuesList().contains(convertedValue); - case RANGE_RULE -> EvalUtil.isInRange(rule.getRangeRule(), convertedValue); - default -> - throw new BadRequestException( - "Targeting rule %s in %s is invalid".formatted(entry.getValue(), segment.getName())); - }; - } - - private static ListValue listWrapper(Targeting.Value value) { - if (value == null) { - return ListValue.getDefaultInstance(); - } else if (value.hasListValue()) { - return value.getListValue(); - } else { - return ListValue.newBuilder().addValues(value).build(); - } - } - - private boolean segmentMatches( - Segment segment, - String segmentName, - String unit, - String flag, - ResolveFlagListener listener, - ConcurrentMap convertValueCache) { - return segmentMatches( - segment, segmentName, unit, new HashSet<>(), flag, listener, convertValueCache); - } - - private boolean segmentMatches( - Segment segment, - String segmentName, - String unit, - Set visitedSegments, - String flag, - ResolveFlagListener listener, - ConcurrentMap convertValueCache) { - if (visitedSegments.contains(segmentName)) { - throw new InternalServerException( - "Segment %s has a circular dependency".formatted(segmentName)); - } - visitedSegments.add(segmentName); - - // handle targeting - try { - final boolean targetingMatch = - isTargetingMatch(segment, unit, visitedSegments, flag, listener, convertValueCache); - if (!targetingMatch) { - listener.markSegmentEvaluationReason( - flag, segmentName, ResolveFlagListener.SegmentEvaluationReason.TARGETING_NOT_MATCHED); - return false; - } - } catch (RuntimeException error) { - logger.error("Error during targeting", error); - return false; - } - - final boolean inBitset = Randomizer.inBitset(state, segmentName, unit); - - if (!inBitset) { - listener.markSegmentEvaluationReason( - flag, segmentName, ResolveFlagListener.SegmentEvaluationReason.BITSET_NOT_MATCHED); - } - - return inBitset; - } - - private ValueCase getExpectedPrimitiveType( - AttributeCriterion criterion, Segment segment, Entry entry) { - return switch (criterion.getRuleCase()) { - case EQ_RULE -> criterion.getEqRule().getValue().getValueCase(); - case SET_RULE -> - criterion.getSetRule().getValuesList().isEmpty() - ? ValueCase.VALUE_NOT_SET - : criterion.getSetRule().getValuesList().get(0).getValueCase(); - case RANGE_RULE -> getExpectedPrimitiveType(criterion.getRangeRule()); - case ANY_RULE -> getExpectedPrimitiveType(criterion.getAnyRule().getRule(), segment, entry); - case ALL_RULE -> getExpectedPrimitiveType(criterion.getAllRule().getRule(), segment, entry); - default -> - throw new BadRequestException( - "Targeting rule %s in %s has invalid type" - .formatted(entry.getValue(), segment.getName())); - }; - } - - private ValueCase getExpectedPrimitiveType( - InnerRule rule, Segment segment, Entry entry) { - return switch (rule.getRuleCase()) { - case EQ_RULE -> rule.getEqRule().getValue().getValueCase(); - case SET_RULE -> - rule.getSetRule().getValuesList().isEmpty() - ? ValueCase.VALUE_NOT_SET - : rule.getSetRule().getValuesList().get(0).getValueCase(); - case RANGE_RULE -> getExpectedPrimitiveType(rule.getRangeRule()); - default -> - throw new BadRequestException( - "Targeting rule %s in %s has invalid type" - .formatted(entry.getValue(), segment.getName())); - }; - } - - private static ValueCase getExpectedPrimitiveType(Targeting.RangeRule rangeRule) { - if (rangeRule.hasStartInclusive()) { - return rangeRule.getStartInclusive().getValueCase(); - } else if (rangeRule.hasStartExclusive()) { - return rangeRule.getStartExclusive().getValueCase(); - } else if (rangeRule.hasEndInclusive()) { - return rangeRule.getEndInclusive().getValueCase(); - } else if (rangeRule.hasEndExclusive()) { - return rangeRule.getEndExclusive().getValueCase(); - } else { - return ValueCase.VALUE_NOT_SET; - } - } - - private static Flag.Variant findVariantOrThrow( - Collection variantsList, String searchedVariant) { - return variantsList.stream() - .filter(variant -> searchedVariant.equals(variant.getName())) - .findFirst() - .orElseThrow( - () -> - new BadRequestException("Assigned flag variant " + searchedVariant + " not found")); - } - - private record ConvertValueCacheKey(Value value, Targeting.Value.ValueCase expectedType) {} -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/PlainResolveTokenConverter.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/PlainResolveTokenConverter.java deleted file mode 100644 index 8bcdc707..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/PlainResolveTokenConverter.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.spotify.confidence; - -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; -import com.spotify.confidence.shaded.flags.resolver.v1.ResolveToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -class PlainResolveTokenConverter extends ResolveTokenConverter { - - private static final Logger logger = LoggerFactory.getLogger(PlainResolveTokenConverter.class); - - @Override - public ByteString convertResolveToken(ResolveToken resolveToken) { - return resolveToken.toByteString(); - } - - @Override - public ResolveToken readResolveToken(ByteString tokenBytes) { - try { - return ResolveToken.parseFrom(tokenBytes); - } catch (InvalidProtocolBufferException e) { - logger.warn("Got InvalidProtocolBufferException when reading resolve token", e); - throw new BadRequestException("Unable to parse resolve token"); - } - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveFlagListener.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveFlagListener.java deleted file mode 100644 index 016b6133..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveFlagListener.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.spotify.confidence; - -interface ResolveFlagListener { - - enum RuleEvaluationReason { - RULE_MATCHED, - RULE_EVALUATED_NO_VARIANT_MATCH, - RULE_NOT_ENABLED, - RULE_NOT_EVALUATED, - SEGMENT_NOT_FOUND_OR_NOT_ACTIVE, - SEGMENT_NOT_MATCHED, - RULE_MATCHED_FALLTHROUGH, - MATERIALIZATION_NOT_MATCHED, - MATERIALIZATION_AND_SEGMENT_NOT_MATCHED - } - - enum SegmentEvaluationReason { - SEGMENT_MATCHED, - SEGMENT_NOT_EVALUATED, - TARGETING_NOT_MATCHED, - BITSET_NOT_MATCHED, - TARGETING_KEY_MISSING, - } - - default void markRuleEvaluationReason(String rule, RuleEvaluationReason reason) {} - - default void markSegmentEvaluationReason( - String flag, String segment, SegmentEvaluationReason reason) {} - - default void addEvalContextMissingValue(String flag, String segment, String fieldName) {} -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveLogger.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveLogger.java deleted file mode 100644 index 68177af8..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveLogger.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.spotify.confidence; - -import com.google.protobuf.Struct; -import com.spotify.confidence.shaded.flags.admin.v1.ClientResolveInfo; -import com.spotify.confidence.shaded.flags.admin.v1.FlagAdminServiceGrpc; -import com.spotify.confidence.shaded.flags.admin.v1.WriteResolveInfoRequest; -import java.io.Closeable; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.Supplier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -class ResolveLogger implements Closeable { - private static final Logger logger = LoggerFactory.getLogger(ResolveLogger.class); - private final Supplier adminStub; - - private final AtomicReference stateRef = - new AtomicReference<>(new ResolveInfoState()); - private final Timer timer; - - private ResolveLogger( - Supplier adminStub, Timer timer) { - this.adminStub = adminStub; - this.timer = timer; - } - - static ResolveLogger createStarted( - Supplier adminStub, - Duration checkpointInterval) { - final Timer timer = new Timer("resolve-logger-timer", true); - final ResolveLogger resolveLogger = new ResolveLogger(adminStub, timer); - - timer.scheduleAtFixedRate( - new TimerTask() { - @Override - public void run() { - try { - resolveLogger.checkpoint(); - } catch (Exception ex) { - logger.error("Could not checkpoint", ex); - } - } - }, - checkpointInterval.toMillis(), - checkpointInterval.toMillis()); - return resolveLogger; - } - - private void checkpoint() { - final var state = stateRef.getAndSet(new ResolveInfoState()); - final var lock = state.readWriteLock.writeLock(); - try { - lock.lock(); - if (state.isEmpty()) { - return; - } - adminStub - .get() - .writeResolveInfo( - WriteResolveInfoRequest.newBuilder() - .addAllClientResolveInfo( - state.clientResolveInfo().entrySet().stream() - .map( - entry -> - ClientResolveInfo.newBuilder() - .setClient(extractClient(entry.getKey())) - .setClientCredential(entry.getKey()) - .addAllSchema( - entry.getValue().schemas().stream() - .map( - s -> - ClientResolveInfo - .EvaluationContextSchemaInstance - .newBuilder() - .putAllSchema(s.fields()) - .putAllSemanticTypes(s.semanticTypes()) - .build()) - .toList()) - .build()) - .toList()) - .addAllFlagResolveInfo( - state.flagResolveInfo().entrySet().stream() - .map( - entry -> - com.spotify.confidence.shaded.flags.admin.v1.FlagResolveInfo - .newBuilder() - .setFlag(entry.getKey()) - .addAllRuleResolveInfo( - entry.getValue().ruleResolveInfo().entrySet().stream() - .map( - ruleInfo -> - com.spotify.confidence.shaded.flags.admin.v1 - .FlagResolveInfo.RuleResolveInfo - .newBuilder() - .setRule(ruleInfo.getKey()) - .setCount( - ruleInfo.getValue().count().get()) - .addAllAssignmentResolveInfo( - ruleInfo - .getValue() - .assignmentCounts() - .entrySet() - .stream() - .map( - assignmentEntry -> - com.spotify.confidence - .shaded.flags.admin.v1 - .FlagResolveInfo - .AssignmentResolveInfo - .newBuilder() - .setAssignmentId( - assignmentEntry - .getKey()) - .setCount( - assignmentEntry - .getValue() - .get()) - .build()) - .toList()) - .build()) - .toList()) - .addAllVariantResolveInfo( - entry.getValue().variantResolveInfo().entrySet().stream() - .map( - variantInfo -> - com.spotify.confidence.shaded.flags.admin.v1 - .FlagResolveInfo.VariantResolveInfo - .newBuilder() - .setVariant(variantInfo.getKey()) - .setCount( - variantInfo.getValue().count().get()) - .build()) - .toList()) - .build()) - .toList()) - .build()); - - } finally { - lock.unlock(); - } - } - - private String extractClient(String key) { - final String[] split = key.split("/"); - return split[0] + "/" + split[1]; - } - - void logResolve( - String resolveId, - Struct evaluationContext, - AccountClient accountClient, - List values) { - ResolveInfoState state = stateRef.get(); - Lock lock = state.readWriteLock.readLock(); - try { - if (!lock.tryLock()) { - // If we failed to lock it means that the checkpoint is currently running, which means that - // the state has just been reset, so we can take the new state and lock on that - state = stateRef.get(); - lock = state.readWriteLock.readLock(); - lock.lock(); - } - - final SchemaFromEvaluationContext.DerivedClientSchema derivedSchema = - SchemaFromEvaluationContext.getSchema(evaluationContext); - state - .clientResolveInfo(accountClient.clientCredential().getName()) - .schemas() - .add(derivedSchema); - - for (ResolvedValue value : values) { - final var flagState = state.flagResolveInfo(value.flag().getName()); - for (var fallthrough : value.fallthroughAssignments()) { - flagState.ruleResolveInfo(fallthrough.getRule()).increment(fallthrough.getAssignmentId()); - } - - if (value.matchedAssignment().isEmpty()) { - flagState.variantResolveInfo("").increment(); - } else { - final var match = value.matchedAssignment().get(); - flagState.variantResolveInfo(match.variant().orElse("")).increment(); - flagState.ruleResolveInfo(match.matchedRule().getName()).increment(match.assignmentId()); - } - } - } finally { - lock.unlock(); - } - } - - @Override - public void close() { - timer.cancel(); - checkpoint(); - } - - private record ResolveInfoState( - ConcurrentMap flagResolveInfo, - ConcurrentMap clientResolveInfo, - ReadWriteLock readWriteLock) { - ResolveInfoState() { - this(new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), new ReentrantReadWriteLock()); - } - - FlagResolveInfo flagResolveInfo(String flag) { - return flagResolveInfo.computeIfAbsent(flag, ignore -> new FlagResolveInfo()); - } - - ClientResolveInfoState clientResolveInfo(String clientCredential) { - return clientResolveInfo.computeIfAbsent( - clientCredential, ignore -> new ClientResolveInfoState()); - } - - boolean isEmpty() { - return flagResolveInfo.isEmpty() && clientResolveInfo.isEmpty(); - } - } - - private record ClientResolveInfoState( - Set schemas) { - ClientResolveInfoState() { - this(ConcurrentHashMap.newKeySet()); - } - } - - private record FlagResolveInfo( - ConcurrentMap variantResolveInfo, - ConcurrentMap ruleResolveInfo) { - FlagResolveInfo() { - this(new ConcurrentHashMap<>(), new ConcurrentHashMap<>()); - } - - VariantResolveInfo variantResolveInfo(String s) { - return variantResolveInfo.computeIfAbsent( - s, ignore -> new VariantResolveInfo(new AtomicLong())); - } - - RuleResolveInfo ruleResolveInfo(String s) { - return ruleResolveInfo.computeIfAbsent( - s, ignore -> new RuleResolveInfo(new AtomicLong(), new ConcurrentHashMap<>())); - } - } - - private record VariantResolveInfo(AtomicLong count) { - void increment() { - count.incrementAndGet(); - } - } - - private record RuleResolveInfo(AtomicLong count, Map assignmentCounts) { - void increment(String assignmentId) { - count.incrementAndGet(); - assignmentCounts.computeIfAbsent(assignmentId, a -> new AtomicLong()).incrementAndGet(); - } - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveTokenConverter.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveTokenConverter.java deleted file mode 100644 index aa32917c..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveTokenConverter.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.spotify.confidence; - -import com.google.protobuf.ByteString; -import com.google.protobuf.Struct; -import com.spotify.confidence.shaded.flags.resolver.v1.ResolveToken; -import com.spotify.confidence.shaded.flags.resolver.v1.ResolveTokenV1; -import com.spotify.confidence.shaded.flags.resolver.v1.ResolveTokenV1.AssignedFlag; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -abstract class ResolveTokenConverter { - - public ByteString createResolveToken( - String accountName, - String resolveId, - List resolvedValues, - Struct evaluationContext) { - final Map assignedFlags = - resolvedValues.stream() - .map(ResolveTokenConverter::toAssignedFlag) - .collect(Collectors.toMap(AssignedFlag::getFlag, Function.identity())); - - final ResolveToken resolveToken = - ResolveToken.newBuilder() - .setTokenV1( - ResolveTokenV1.newBuilder() - .setAccount(accountName) - .setResolveId(resolveId) - .setEvaluationContext(evaluationContext) - .putAllAssignments(assignedFlags) - .build()) - .build(); - - return convertResolveToken(resolveToken); - } - - abstract ByteString convertResolveToken(ResolveToken resolveToken); - - abstract ResolveToken readResolveToken(ByteString token); - - static AssignedFlag toAssignedFlag(ResolvedValue value) { - final var builder = - AssignedFlag.newBuilder() - .setFlag(value.flag().getName()) - .setReason(value.reason()) - .addAllFallthroughAssignments(value.fallthroughAssignments()); - - if (value.matchedAssignment().isEmpty()) { - return builder.build(); - } - - final AssignmentMatch match = value.matchedAssignment().get(); - - return builder - .setAssignmentId(match.assignmentId()) - .setRule(match.matchedRule().getName()) - .setSegment(match.segment().getName()) - .setVariant(match.variant().orElse("")) - .setTargetingKey(match.targetingKey()) - .setTargetingKeySelector(match.matchedRule().getTargetingKeySelector()) - .build(); - } -} diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java index 1c82cb42..9396fc7e 100644 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java +++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java @@ -42,21 +42,19 @@ public class TestBase { protected TestBase(ResolverState state) { this.desiredState = state; - final var wasmResolverApi = - new SwapWasmResolverApi( - new WasmFlagLogger() { - @Override - public void write(WriteFlagLogsRequest request) {} + final var wasmResolverApi = + new SwapWasmResolverApi( + new WasmFlagLogger() { + @Override + public void write(WriteFlagLogsRequest request) {} - @Override - public void shutdown() {} - }, - desiredState.toProto().toByteArray(), - "", - mockFallback); - resolverServiceFactory = - new LocalResolverServiceFactory( - wasmResolverApi, mockFallback); + @Override + public void shutdown() {} + }, + desiredState.toProto().toByteArray(), + "", + mockFallback); + resolverServiceFactory = new LocalResolverServiceFactory(wasmResolverApi, mockFallback); } protected static void setup() {} From 58ee3d2b6428d33176c44478a49f6a0637b066d0 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 13 Oct 2025 14:37:16 +0200 Subject: [PATCH 4/8] refactor: Small refactor --- .../com/spotify/confidence/LocalResolverServiceFactory.java | 6 +----- .../spotify/confidence/OpenFeatureLocalResolveProvider.java | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java index 20bf4519..490c995d 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java @@ -42,7 +42,7 @@ static FlagResolverService from( return createFlagResolverService(accountStateProvider, accountId, stickyResolveStrategy); } - static FlagResolverService createFlagResolverService( + private static FlagResolverService createFlagResolverService( ApiSecret apiSecret, StickyResolveStrategy stickyResolveStrategy) { final var channel = createConfidenceChannel(); final AuthServiceBlockingStub authService = AuthServiceGrpc.newBlockingStub(channel); @@ -125,10 +125,6 @@ public void shutdown() {} return new WasmFlagResolverService(wasmResolverApi, stickyResolveStrategy); } - LocalResolverServiceFactory(StickyResolveStrategy stickyResolveStrategy) { - this(null, stickyResolveStrategy); - } - LocalResolverServiceFactory( ResolverApi wasmResolveApi, StickyResolveStrategy stickyResolveStrategy) { this.wasmResolveApi = wasmResolveApi; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java index 207bf1d4..199976a2 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java @@ -22,7 +22,7 @@ /** * OpenFeature provider for Confidence feature flags using local resolution. * - *

This provider evaluates feature flags locally using either a WebAssembly (WASM) resolver . It + *

This provider evaluates feature flags locally using either a WebAssembly (WASM) resolver. It * periodically syncs flag configurations from the Confidence service and caches them locally for * fast, low-latency flag evaluation. * @@ -56,8 +56,8 @@ public class OpenFeatureLocalResolveProvider implements FeatureProvider { private final StickyResolveStrategy stickyResolveStrategy; /** - * Creates a new OpenFeature provider for local flag resolution with default fallback strategy and - * no retry. + * Creates a new OpenFeature provider for local flag resolution with sticky default fallback + * strategy and no retry. * *

This constructor uses {@link RemoteResolverFallback} as the default sticky resolve strategy, * which provides fallback to the remote Confidence service when the WASM resolver encounters From b67ef41c778c935f3a8e3b6eeb642afdf57b0159 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 13 Oct 2025 15:24:30 +0200 Subject: [PATCH 5/8] reefactor: Remove AccountState --- .../com/spotify/confidence/AccountState.java | 67 --- .../java/com/spotify/confidence/EvalUtil.java | 36 -- .../confidence/FlagsAdminStateFetcher.java | 126 +----- .../LocalResolverServiceFactory.java | 4 +- .../com/spotify/confidence/Randomizer.java | 55 --- .../com/spotify/confidence/ResolverState.java | 103 ----- .../java/com/spotify/confidence/Util.java | 386 ------------------ .../spotify/confidence/expressions/And.java | 37 -- .../spotify/confidence/expressions/AndOr.java | 44 -- .../spotify/confidence/expressions/Eq.java | 65 --- .../spotify/confidence/expressions/Expr.java | 103 ----- .../expressions/ExprNormalizer.java | 97 ----- .../spotify/confidence/expressions/False.java | 26 -- .../spotify/confidence/expressions/Not.java | 47 --- .../spotify/confidence/expressions/Or.java | 37 -- .../spotify/confidence/expressions/Ord.java | 67 --- .../spotify/confidence/expressions/Ref.java | 21 - .../confidence/expressions/TargetingExpr.java | 88 ---- .../spotify/confidence/expressions/True.java | 26 -- 19 files changed, 22 insertions(+), 1413 deletions(-) delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/AccountState.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/Randomizer.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/ResolverState.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/And.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/AndOr.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Eq.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Expr.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/ExprNormalizer.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/False.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Not.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Or.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ord.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ref.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/TargetingExpr.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/True.java diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountState.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountState.java deleted file mode 100644 index 6a7dd8d0..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/AccountState.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.spotify.confidence; - -import static com.spotify.confidence.Randomizer.MEGA_SALT; - -import com.spotify.confidence.expressions.TargetingExpr; -import com.spotify.confidence.shaded.flags.admin.v1.Flag; -import com.spotify.confidence.shaded.flags.admin.v1.Segment; -import com.spotify.confidence.shaded.iam.v1.ClientCredential.ClientSecret; -import java.util.BitSet; -import java.util.Map; -import java.util.stream.Collectors; - -record AccountState( - Account account, - Map flags, - Map segments, - Map bitsets, - Map secrets, - String stateFileHash, - Map targeting, - String saltForAccount) { - AccountState { - flags = - flags.entrySet().stream() - .collect( - Collectors.toMap( - Map.Entry::getKey, - e -> { - final var builder = e.getValue().toBuilder(); - for (var variant : builder.getVariantsBuilderList()) { - variant.setValue( - EvalUtil.expandToSchema(variant.getValue(), e.getValue().getSchema())); - } - return builder.build(); - })); - } - - AccountState( - Account account, - Map flags, - Map segments, - Map bitsets, - Map secrets, - String stateFileHash) { - this( - account, - flags, - segments, - bitsets, - secrets, - stateFileHash, - segments.entrySet().stream() - .collect( - Collectors.toMap( - Map.Entry::getKey, - segment -> TargetingExpr.fromTargeting(segment.getValue().getTargeting()))), - "%s-%s".formatted(MEGA_SALT, account.name().split("/")[1])); - } - - TargetingExpr getTargetingExpr(String segmentName) { - return targeting.get(segmentName); - } - - TargetingExpr getTargetingExpr(Segment segment) { - return getTargetingExpr(segment.getName()); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/EvalUtil.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/EvalUtil.java index cc98bdaf..634932c4 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/EvalUtil.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/EvalUtil.java @@ -30,29 +30,6 @@ final class EvalUtil { private EvalUtil() {} - static Value getAttributeValue(Struct struct, String attributeName) { - final String[] attributePath = attributeName.split("\\."); - return getAttributeValue(struct, attributePath, 0); - } - - private static Value getAttributeValue(Struct struct, String[] attributePath, int pos) { - final Map fieldsMap = struct.getFieldsMap(); - final String fieldName = attributePath[pos]; - if (!fieldsMap.containsKey(fieldName)) { - return Values.ofNull(); - } - - final Value value = fieldsMap.get(fieldName); - if (pos == attributePath.length - 1) { - return value; - } else if (value.hasStructValue()) { - return getAttributeValue(value.getStructValue(), attributePath, pos + 1); - } - - // non-struct value addressed with .-operator - return Values.ofNull(); - } - /** * Tries to parse the a {@link Value} to an expected targeting type value. If the expected type is * not set, or there is no meaningful conversion to the expected type, this function will return @@ -147,19 +124,6 @@ static Targeting.Value timestampValue(final Instant instant) { .build(); } - static boolean isInRange(Targeting.RangeRule rangeRule, Targeting.Value value) { - if (value == null) { - return false; - } - - try { - return Util.hasOverlap(value, rangeRule); - } catch (IllegalArgumentException e) { - // the supplied value type might not match the type in the rule - return false; - } - } - static Struct expandToSchema(Struct struct, FlagSchema.StructFlagSchema schema) { final Struct.Builder builder = struct.toBuilder(); for (Map.Entry schemaEntry : schema.getSchemaMap().entrySet()) { diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagsAdminStateFetcher.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagsAdminStateFetcher.java index 1f14bf6b..2d327032 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagsAdminStateFetcher.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagsAdminStateFetcher.java @@ -1,18 +1,9 @@ package com.spotify.confidence; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; - -import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream; -import com.google.protobuf.ByteString; import com.google.protobuf.Timestamp; -import com.spotify.confidence.shaded.flags.admin.v1.Flag; -import com.spotify.confidence.shaded.flags.admin.v1.Segment; import com.spotify.confidence.shaded.flags.resolver.v1.ResolverStateServiceGrpc; import com.spotify.confidence.shaded.flags.resolver.v1.ResolverStateUriRequest; import com.spotify.confidence.shaded.flags.resolver.v1.ResolverStateUriResponse; -import com.spotify.confidence.shaded.iam.v1.Client; -import com.spotify.confidence.shaded.iam.v1.ClientCredential; import io.grpc.health.v1.HealthCheckResponse; import java.io.IOException; import java.io.InputStream; @@ -20,13 +11,7 @@ import java.net.URL; import java.time.Duration; import java.time.Instant; -import java.util.BitSet; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import java.util.zip.GZIPInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,13 +22,13 @@ class FlagsAdminStateFetcher { private final ResolverStateServiceGrpc.ResolverStateServiceBlockingStub resolverStateService; private final HealthStatus healthStatus; private final String accountName; - // Source of truth for resolver state, shared with GrpcFlagResolverService - private final AtomicReference stateHolder = - new AtomicReference<>(new ResolverState(Map.of(), Map.of())); - private final AtomicReference - rawResolverStateHolder = - new AtomicReference<>( - com.spotify.confidence.shaded.flags.admin.v1.ResolverState.newBuilder().build()); + // ETag for conditional GETs of resolver state + private final AtomicReference etagHolder = new AtomicReference<>(); + private final AtomicReference rawResolverStateHolder = + new AtomicReference<>( + com.spotify.confidence.shaded.flags.admin.v1.ResolverState.newBuilder() + .build() + .toByteArray()); private final AtomicReference resolverStateUriResponse = new AtomicReference<>(); private final AtomicReference refreshTimeHolder = new AtomicReference<>(); @@ -58,27 +43,16 @@ public FlagsAdminStateFetcher( this.accountName = accountName; } - public AtomicReference - rawStateHolder() { + public AtomicReference rawStateHolder() { return rawResolverStateHolder; } public void reload() { - final ResolverState currentState = stateHolder.get(); - final Map newAccountStates = - new LinkedHashMap<>(currentState.accountStates()); - final Map secrets = new HashMap<>(); - try { - final AccountState newAccountState = fetchState(); - newAccountStates.put(accountName, newAccountState); - secrets.putAll(newAccountState.secrets()); + fetchAndUpdateStateIfChanged(); } catch (Exception e) { logger.warn("Failed to reload, ignoring reload", e); - return; } - - stateHolder.set(new ResolverState(newAccountStates, secrets)); healthStatus.setStatus(HealthCheckResponse.ServingStatus.SERVING); } @@ -99,87 +73,27 @@ private Instant toInstant(Timestamp time) { return Instant.ofEpochSecond(time.getSeconds(), time.getNanos()); } - private AccountState fetchState() { + private void fetchAndUpdateStateIfChanged() { final var response = getResolverFileUri(); this.accountId = response.getAccount(); final var uri = response.getSignedUri(); - final com.spotify.confidence.shaded.flags.admin.v1.ResolverState state; - final String etag; try { final HttpURLConnection conn = (HttpURLConnection) new URL(uri).openConnection(); - if (stateHolder.get() != null && stateHolder.get().accountStates().get(accountName) != null) { - conn.setRequestProperty( - "if-none-match", stateHolder.get().accountStates().get(accountName).stateFileHash()); + final String previousEtag = etagHolder.get(); + if (previousEtag != null) { + conn.setRequestProperty("if-none-match", previousEtag); } if (conn.getResponseCode() == 304) { // Not modified - return stateHolder.get().accountStates().get(accountName); + return; } - etag = conn.getHeaderField("etag"); - try (final var stream = conn.getInputStream()) { - state = com.spotify.confidence.shaded.flags.admin.v1.ResolverState.parseFrom(stream); + final String etag = conn.getHeaderField("etag"); + try (final InputStream stream = conn.getInputStream()) { + final byte[] bytes = stream.readAllBytes(); + rawResolverStateHolder.set(bytes); + etagHolder.set(etag); } - } catch (IOException e) { - throw new RuntimeException(e); - } - final List flags = state.getFlagsList(); - final Map flagsIndex = flags.stream().collect(toMap(Flag::getName, identity())); - - final Map bitsetsBySegment = - state.getBitsetsList().stream() - .collect( - toMap( - com.spotify.confidence.shaded.flags.admin.v1.ResolverState.PackedBitset - ::getSegment, - bitset -> - switch (bitset.getBitsetCase()) { - case GZIPPED_BITSET -> unzipBitset(bitset.getGzippedBitset()); - case FULL_BITSET -> Randomizer.FULL_BITSET; - case BITSET_NOT_SET -> throw new UnsupportedOperationException(); - })); - - final Map segmentsIndex = - state.getSegmentsNoBitsetsList().stream().collect(toMap(Segment::getName, c -> c)); - - final List clients = state.getClientsList(); - final Map secrets = new HashMap<>(); - for (Client client : clients) { - final List credentials = - state.getClientCredentialsList().stream() - .filter(c -> c.getName().startsWith(client.getName())) - .toList(); - final Map credentialsIndex = - credentials.stream() - .collect( - toMap( - ClientCredential::getClientSecret, - credential -> new AccountClient(accountName, client, credential))); - - secrets.putAll(credentialsIndex); - } - - logger.info( - "Loaded {} flags, {} segments, {} clients, {} secrets for {}", - flags.size(), - segmentsIndex.size(), - clients.size(), - secrets.size(), - accountName); - - rawResolverStateHolder.set(state); - return new AccountState( - new Account(accountName), flagsIndex, segmentsIndex, bitsetsBySegment, secrets, etag); - } - - private BitSet unzipBitset(ByteString gzippedBitset) { - return BitSet.valueOf(uncompressGZIP(gzippedBitset).asReadOnlyByteBuffer()); - } - - public static ByteString uncompressGZIP(final ByteString data) { - try { - final InputStream stream = - new GZIPInputStream(new ByteBufferBackedInputStream(data.asReadOnlyByteBuffer())); - return ByteString.readFrom(stream); + logger.info("Loaded resolver state for {}, etag={}", accountName, etag); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java index 490c995d..2c405fea 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java @@ -65,7 +65,7 @@ private static FlagResolverService createFlagResolverService( final ResolverApi wasmResolverApi = new ThreadLocalSwapWasmResolverApi( wasmFlagLogger, - sidecarFlagsAdminFetcher.rawStateHolder().get().toByteArray(), + sidecarFlagsAdminFetcher.rawStateHolder().get(), sidecarFlagsAdminFetcher.accountId, stickyResolveStrategy); flagsFetcherExecutor.scheduleAtFixedRate( @@ -77,7 +77,7 @@ private static FlagResolverService createFlagResolverService( logPollExecutor.scheduleAtFixedRate( () -> wasmResolverApi.updateStateAndFlushLogs( - sidecarFlagsAdminFetcher.rawStateHolder().get().toByteArray(), + sidecarFlagsAdminFetcher.rawStateHolder().get(), sidecarFlagsAdminFetcher.accountId), POLL_LOG_INTERVAL.getSeconds(), POLL_LOG_INTERVAL.getSeconds(), diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Randomizer.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Randomizer.java deleted file mode 100644 index 027b51b3..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Randomizer.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.spotify.confidence; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.base.Joiner; -import com.google.common.hash.HashCode; -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; -import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule.Assignment; -import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule.BucketRange; -import java.util.BitSet; - -class Randomizer { - - static final String MEGA_SALT = "MegaSalt"; - static final int BR_BUCKET_COUNT = 1_000_000; - static final BitSet FULL_BITSET = new BitSet(BR_BUCKET_COUNT); - - static { - for (int i = 0; i < BR_BUCKET_COUNT; i++) { - FULL_BITSET.set(i); - } - } - - private static final HashFunction HASH_FUNCTION = Hashing.murmur3_128(); - private static final Joiner PIPE_JOINER = Joiner.on('|'); - private static final long LONG_SCALE = 0x0FFF_FFFF_FFFF_FFFFL; - - static boolean inBitset(AccountState state, String segmentName, String unit) { - if (!state.bitsets().containsKey(segmentName) || state.bitsets().get(segmentName).isEmpty()) { - return false; - } - - final BitSet bitset = state.bitsets().get(segmentName); - final long bucket = getBucket(unit, state.saltForAccount(), BR_BUCKET_COUNT); - - return bitset.get((int) bucket); - } - - static long getBucket(String unit, String salt, int bucketCount) { - final String[] data = new String[] {salt, unit}; - final String unitString = PIPE_JOINER.join(data); - final HashCode hashCode = HASH_FUNCTION.hashString(unitString, UTF_8); - return ((hashCode.asLong() >> 4) & LONG_SCALE) % bucketCount; - } - - static boolean coversBucket(Assignment assignment, long bucket) { - for (BucketRange range : assignment.getBucketRangesList()) { - if (range.getLower() <= bucket && bucket < range.getUpper()) { - return true; - } - } - return false; - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolverState.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolverState.java deleted file mode 100644 index 785964be..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolverState.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.spotify.confidence; - -import com.spotify.confidence.shaded.iam.v1.Client; -import com.spotify.confidence.shaded.iam.v1.ClientCredential; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.BitSet; -import java.util.List; -import java.util.Map; -import java.util.zip.GZIPOutputStream; - -record ResolverState( - Map accountStates, - Map secrets) { - - public com.spotify.confidence.shaded.flags.admin.v1.ResolverState toProto() { - final com.spotify.confidence.shaded.flags.admin.v1.ResolverState.Builder builder = - com.spotify.confidence.shaded.flags.admin.v1.ResolverState.newBuilder(); - - // Collect all flags from all account states - final List allFlags = new ArrayList<>(); - final List allSegments = - new ArrayList<>(); - final List allBitsets = - new ArrayList<>(); - final List allClients = new ArrayList<>(); - final List allClientCredentials = - new ArrayList<>(); - - // Process each account state - for (AccountState accountState : accountStates.values()) { - // Add flags - allFlags.addAll(accountState.flags().values()); - - // Add segments (without bitsets) - allSegments.addAll(accountState.segments().values()); - - // Add bitsets as packed bitsets - for (Map.Entry entry : accountState.bitsets().entrySet()) { - final String segmentName = entry.getKey(); - final BitSet bitSet = entry.getValue(); - - final com.spotify.confidence.shaded.flags.admin.v1.ResolverState.PackedBitset.Builder - bitsetBuilder = - com.spotify.confidence.shaded.flags.admin.v1.ResolverState.PackedBitset.newBuilder() - .setSegment(segmentName); - - // Check if it's a full bitset (all bits set) - if (isFullBitset(bitSet)) { - bitsetBuilder.setFullBitset(true); - } else { - // Compress the bitset - final byte[] compressed = compressBitset(bitSet); - bitsetBuilder.setGzippedBitset(com.google.protobuf.ByteString.copyFrom(compressed)); - } - - allBitsets.add(bitsetBuilder.build()); - } - } - - // Process secrets to extract clients and client credentials - for (AccountClient accountClient : secrets.values()) { - allClients.add(accountClient.client()); - allClientCredentials.add(accountClient.clientCredential()); - } - - // Set all collected data - builder.addAllFlags(allFlags); - builder.addAllSegmentsNoBitsets(allSegments); - builder.addAllBitsets(allBitsets); - builder.addAllClients(allClients); - builder.addAllClientCredentials(allClientCredentials); - - // Set region if available (default to UNSPECIFIED) - builder.setRegion( - com.spotify.confidence.shaded.flags.admin.v1.ResolverState.Region.REGION_UNSPECIFIED); - - return builder.build(); - } - - private boolean isFullBitset(BitSet bitSet) { - // Check if all bits in the bitset are set - final int cardinality = bitSet.cardinality(); - final int length = bitSet.length(); - return cardinality == length && length > 0; - } - - private byte[] compressBitset(BitSet bitSet) { - try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - GZIPOutputStream gzipStream = new GZIPOutputStream(byteStream)) { - - // Convert BitSet to byte array - final byte[] bytes = bitSet.toByteArray(); - gzipStream.write(bytes); - gzipStream.finish(); - - return byteStream.toByteArray(); - } catch (IOException e) { - throw new RuntimeException("Failed to compress bitset", e); - } - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java deleted file mode 100644 index 9e5f7c68..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java +++ /dev/null @@ -1,386 +0,0 @@ -package com.spotify.confidence; - -import static com.spotify.confidence.expressions.Expr.Type.AND; -import static com.spotify.confidence.expressions.Expr.Type.FALSE; -import static com.spotify.confidence.expressions.Expr.Type.NOT; -import static com.spotify.confidence.expressions.Expr.Type.OR; -import static com.spotify.confidence.expressions.Expr.Type.REF; -import static com.spotify.confidence.expressions.Expr.Type.TRUE; -import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.EndCase.END_EXCLUSIVE; -import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.EndCase.END_INCLUSIVE; -import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.EndCase.END_NOT_SET; -import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.StartCase.START_EXCLUSIVE; -import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.StartCase.START_INCLUSIVE; -import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.StartCase.START_NOT_SET; -import static java.util.stream.Collectors.toList; - -import com.spotify.confidence.expressions.Eq; -import com.spotify.confidence.expressions.Expr; -import com.spotify.confidence.expressions.Ord; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.EqRule; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.SetRule; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.Value; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.stream.Stream; - -public final class Util { - - private Util() {} - - public static boolean evalExpression(Expr expr, Set trueRefs) { - return switch (expr.type()) { - case TRUE -> true; - case FALSE -> false; - case REF -> trueRefs.contains(expr.name()); - case NOT -> !evalExpression(expr.operands().iterator().next(), trueRefs); - case AND -> expr.operands().stream().allMatch(op -> evalExpression(op, trueRefs)); - case OR -> expr.operands().stream().anyMatch(op -> evalExpression(op, trueRefs)); - }; - } - - // eq - static boolean hasOverlap(EqRule r1, EqRule r2) { - ensureSameType(r1.getValue(), r2.getValue()); - final Eq eq = Eq.getEq(r1.getValue().getValueCase()); - - return eq.eq(r1.getValue(), r2.getValue()); - } - - // eq, contains - static boolean hasOverlap(EqRule r1, SetRule r2) { - r2.getValuesList().forEach(v -> ensureSameType(r1.getValue(), v)); - final Eq eq = Eq.getEq(r1.getValue().getValueCase()); - - return eq.contains(r2.getValuesList(), r1.getValue()); - } - - static boolean hasOverlap(EqRule r1, RangeRule r2) { - return hasOverlap(r1.getValue(), r2); - } - - // lt, lte - static boolean hasOverlap(Value value, RangeRule r2) { - ensureSameType(value, r2); - final Ord ord = Ord.getOrd(value); - - final boolean afterStart; - final boolean beforeEnd; - - afterStart = - switch (r2.getStartCase()) { - case START_INCLUSIVE -> ord.lte(r2.getStartInclusive(), value); - case START_EXCLUSIVE -> ord.lt(r2.getStartExclusive(), value); - default -> false; - }; - - beforeEnd = - switch (r2.getEndCase()) { - case END_INCLUSIVE -> ord.lte(value, r2.getEndInclusive()); - case END_EXCLUSIVE -> ord.lt(value, r2.getEndExclusive()); - default -> false; - }; - - return (r2.getStartCase() == START_NOT_SET || afterStart) - && (r2.getEndCase() == END_NOT_SET || beforeEnd); - } - - // eq, intersect - static boolean hasOverlap(SetRule r1, SetRule r2) { - r1.getValuesList().forEach(v1 -> r2.getValuesList().forEach(v2 -> ensureSameType(v1, v2))); - - final Eq eq = Eq.getEq(r1.getValuesList().get(0).getValueCase()); - return eq.overlap(r1.getValuesList(), r2.getValuesList()); - } - - static boolean hasOverlapNeg(SetRule r1, SetRule r2Negated) { - r1.getValuesList() - .forEach(v1 -> r2Negated.getValuesList().forEach(v2 -> ensureSameType(v1, v2))); - - final Eq eq = Eq.getEq(r1.getValuesList().get(0).getValueCase()); - return eq.overlapNeg(r1.getValuesList(), r2Negated.getValuesList()); - } - - static boolean hasOverlap(SetRule r1, RangeRule r2) { - return r1.getValuesList().stream().anyMatch(value -> hasOverlap(value, r2)); - } - - // lt, lte - static boolean hasOverlap(RangeRule r1, RangeRule r2) { - ensureSameType(r1, r2); - - if (r1.getStartCase() == START_NOT_SET && r1.getEndCase() == END_NOT_SET) { - return true; - } - - if (r2.getStartCase() == START_NOT_SET && r2.getEndCase() == END_NOT_SET) { - return true; - } - - final boolean x1y2E = r1.getStartCase() == START_EXCLUSIVE || r2.getEndCase() == END_EXCLUSIVE; - final boolean y1x2E = r2.getStartCase() == START_EXCLUSIVE || r1.getEndCase() == END_EXCLUSIVE; - - final Value x1 = - r1.getStartCase() == START_INCLUSIVE ? r1.getStartInclusive() : r1.getStartExclusive(); - final Value x2 = r1.getEndCase() == END_INCLUSIVE ? r1.getEndInclusive() : r1.getEndExclusive(); - final Value y1 = - r2.getStartCase() == START_INCLUSIVE ? r2.getStartInclusive() : r2.getStartExclusive(); - final Value y2 = r2.getEndCase() == END_INCLUSIVE ? r2.getEndInclusive() : r2.getEndExclusive(); - - // .....] - // [....] - // or - // [....] - // [..... - if (r1.getStartCase() == START_NOT_SET && r2.getStartCase() != START_NOT_SET - || r2.getEndCase() == END_NOT_SET && r1.getEndCase() != END_NOT_SET) { - if (y1x2E) { - return Ord.getOrd(y1).lt(y1, x2); - } else { - return Ord.getOrd(y1).lte(y1, x2); - } - } - - // [..... - // [....] - // or - // [....] - // .....] - if (r1.getEndCase() == END_NOT_SET && r2.getEndCase() != END_NOT_SET - || r2.getStartCase() == START_NOT_SET && r1.getStartCase() != START_NOT_SET) { - if (x1y2E) { - return Ord.getOrd(x1).lt(x1, y2); - } else { - return Ord.getOrd(x1).lte(x1, y2); - } - } - - final Ord ord = Ord.getOrd(x1); - if (x1y2E && y1x2E) { - return ord.lt(x1, y2) && ord.lt(y1, x2); - } else if (x1y2E) { - return ord.lt(x1, y2) && ord.lte(y1, x2); - } else if (y1x2E) { - return ord.lte(x1, y2) && ord.lt(y1, x2); - } else { - return ord.lte(x1, y2) && ord.lte(y1, x2); - } - } - - static SetRule intersectSets(SetRule set1, SetRule set2) { - final List values1 = set1.getValuesList(); - final List values2 = set2.getValuesList(); - values1.forEach(v1 -> values2.forEach(v2 -> ensureSameType(v1, v2))); - - if (values1.isEmpty() || values2.isEmpty()) { - return SetRule.getDefaultInstance(); - } - - final Eq eq = Eq.getEq(values1.get(0).getValueCase()); - final List intersection = - values1.stream().filter(v -> eq.contains(values2, v)).collect(toList()); - return SetRule.newBuilder().addAllValues(intersection).build(); - } - - static SetRule unionSets(SetRule set1, SetRule set2) { - final List values1 = set1.getValuesList(); - final List values2 = set2.getValuesList(); - values1.forEach(v1 -> values2.forEach(v2 -> ensureSameType(v1, v2))); - - if (values1.isEmpty() && values2.isEmpty()) { - return SetRule.getDefaultInstance(); - } - - if (values1.isEmpty()) { - return set2; - } - - if (values2.isEmpty()) { - return set1; - } - - final Eq eq = Eq.getEq(values1.get(0).getValueCase()); - final List filter = new ArrayList<>(); - final List union = - Stream.concat(values1.stream(), values2.stream()) - .filter( - v -> { - final boolean contained = eq.contains(filter, v); - filter.add(v); - return !contained; - }) - .collect(toList()); - return SetRule.newBuilder().addAllValues(union).build(); - } - - static SetRule subtractSet(SetRule set, SetRule subtract) { - final List values1 = set.getValuesList(); - final List values2 = subtract.getValuesList(); - values1.forEach(v1 -> values2.forEach(v2 -> ensureSameType(v1, v2))); - - if (values1.isEmpty()) { - return SetRule.getDefaultInstance(); - } - - final Eq eq = Eq.getEq(values1.get(0).getValueCase()); - final List subtracted = - values1.stream().filter(v -> !eq.contains(values2, v)).collect(toList()); - return SetRule.newBuilder().addAllValues(subtracted).build(); - } - - static RangeRule intersectRanges(RangeRule range1, RangeRule range2) { - ensureSameType(range1, range2); - - final RangeRule.Builder builder = RangeRule.newBuilder(); - - // max of start values - if (range1.getStartCase() != START_NOT_SET && range2.getStartCase() == START_NOT_SET) { - if (range1.hasStartInclusive()) { - builder.setStartInclusive(range1.getStartInclusive()); - } else if (range1.hasStartExclusive()) { - builder.setStartExclusive(range1.getStartExclusive()); - } - } else if (range1.getStartCase() == START_NOT_SET && range2.getStartCase() != START_NOT_SET) { - if (range2.hasStartInclusive()) { - builder.setStartInclusive(range2.getStartInclusive()); - } else if (range2.hasStartExclusive()) { - builder.setStartExclusive(range2.getStartExclusive()); - } - } else if (range1.getStartCase() != START_NOT_SET) { // both starts must be set - final Value v1 = - range1.hasStartInclusive() ? range1.getStartInclusive() : range1.getStartExclusive(); - final Value v2 = - range2.hasStartInclusive() ? range2.getStartInclusive() : range2.getStartExclusive(); - - final Ord ord = Ord.getOrd(v1); - if (ord.lt(v1, v2)) { - if (range2.hasStartInclusive()) { - builder.setStartInclusive(v2); - } else if (range2.hasStartExclusive()) { - builder.setStartExclusive(v2); - } - } else if (ord.lt(v2, v1)) { - if (range1.hasStartInclusive()) { - builder.setStartInclusive(v1); - } else if (range1.hasStartExclusive()) { - builder.setStartExclusive(v1); - } - } else { // equal - if (range1.hasStartExclusive() || range2.hasStartExclusive()) { - builder.setStartExclusive(v1); - } else { - builder.setStartInclusive(v1); - } - } - } - - // min of end values - if (range1.getEndCase() != END_NOT_SET && range2.getEndCase() == END_NOT_SET) { - if (range1.hasEndInclusive()) { - builder.setEndInclusive(range1.getEndInclusive()); - } else if (range1.hasEndExclusive()) { - builder.setEndExclusive(range1.getEndExclusive()); - } - } else if (range1.getEndCase() == END_NOT_SET && range2.getEndCase() != END_NOT_SET) { - if (range2.hasEndInclusive()) { - builder.setEndInclusive(range2.getEndInclusive()); - } else if (range2.hasEndExclusive()) { - builder.setEndExclusive(range2.getEndExclusive()); - } - } else if (range1.getEndCase() != END_NOT_SET) { // both starts must be set - final Value v1 = - range1.hasEndInclusive() ? range1.getEndInclusive() : range1.getEndExclusive(); - final Value v2 = - range2.hasEndInclusive() ? range2.getEndInclusive() : range2.getEndExclusive(); - - final Ord ord = Ord.getOrd(v1); - if (ord.lt(v1, v2)) { - if (range1.hasEndInclusive()) { - builder.setEndInclusive(v1); - } else if (range1.hasEndExclusive()) { - builder.setEndExclusive(v1); - } - } else if (ord.lt(v2, v1)) { - if (range2.hasEndInclusive()) { - builder.setEndInclusive(v2); - } else if (range2.hasEndExclusive()) { - builder.setEndExclusive(v2); - } - } else { // equal - if (range1.hasEndExclusive() || range2.hasEndExclusive()) { - builder.setEndExclusive(v1); - } else { - builder.setEndInclusive(v1); - } - } - } - - return builder.build(); - } - - static SetRule filterSetByRange(SetRule set, RangeRule range) { - final List values = set.getValuesList(); - values.forEach(v -> ensureSameType(v, range)); - // todo check all values are of same type in set - - final List filtered = - values.stream().filter(v -> hasOverlap(v, range)).collect(toList()); - return SetRule.newBuilder().addAllValues(filtered).build(); - } - - static boolean emptyRange(RangeRule range) { - if (range.getStartCase() != START_NOT_SET && range.getEndCase() != END_NOT_SET) { - final Value startVal = - range.hasStartInclusive() ? range.getStartInclusive() : range.getStartExclusive(); - final Value endVal = - range.hasEndInclusive() ? range.getEndInclusive() : range.getEndExclusive(); - - final Ord ord = Ord.getOrd(startVal); - if (ord.lt(endVal, startVal)) { - return true; - } else if (ord.lt(startVal, endVal)) { - return false; - } else { - return range.hasStartExclusive() || range.hasEndExclusive(); - } - } - return false; - } - - private static void ensureSameType(RangeRule range1, RangeRule range2) { - if (range1.getStartCase() == START_INCLUSIVE) { - ensureSameType(range1.getStartInclusive(), range2); - } else if (range1.getStartCase() == START_EXCLUSIVE) { - ensureSameType(range1.getStartExclusive(), range2); - } - - if (range1.getEndCase() == END_INCLUSIVE) { - ensureSameType(range1.getEndInclusive(), range2); - } else if (range1.getEndCase() == END_EXCLUSIVE) { - ensureSameType(range1.getEndExclusive(), range2); - } - } - - private static void ensureSameType(Value value, RangeRule range) { - if (range.getStartCase() == START_INCLUSIVE) { - ensureSameType(value, range.getStartInclusive()); - } else if (range.getStartCase() == START_EXCLUSIVE) { - ensureSameType(value, range.getStartExclusive()); - } - - if (range.getEndCase() == END_INCLUSIVE) { - ensureSameType(value, range.getEndInclusive()); - } else if (range.getEndCase() == END_EXCLUSIVE) { - ensureSameType(value, range.getEndExclusive()); - } - } - - private static void ensureSameType(Value v1, Value v2) { - if (v1.getValueCase() != v2.getValueCase()) { - throw new IllegalArgumentException( - "Non-matching types " + v1.getValueCase() + " != " + v2.getValueCase()); - } - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/And.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/And.java deleted file mode 100644 index fd680166..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/And.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.spotify.confidence.expressions; - -import static java.util.stream.Collectors.toSet; - -import java.util.Set; -import java.util.stream.Stream; - -record And(Set operands) implements AndOr { - - @Override - public Type type() { - return Type.AND; - } - - @Override - public Expr simplify() { - final Set reduced = - reduceNegatedPairsTo(Expr.F) - .filter(o -> !o.isTrue()) - .flatMap(o -> o.isAnd() ? o.operands().stream() : Stream.of(o)) - .collect(toSet()); - - if (reduced.contains(Expr.F)) { - return Expr.F; - } else if (reduced.size() == 1) { - return reduced.iterator().next(); - } else if (reduced.isEmpty()) { - return Expr.T; - } - return Expr.and(reduced); - } - - @Override - public String toString() { - return name(); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/AndOr.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/AndOr.java deleted file mode 100644 index abfa8001..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/AndOr.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.spotify.confidence.expressions; - -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toSet; -import static java.util.stream.Stream.concat; - -import java.util.List; -import java.util.Set; -import java.util.stream.Stream; - -sealed interface AndOr extends Expr permits And, Or { - - @Override - default String name() { - return operands().stream() - .map(Expr::name) - .collect(joining(delimiter(), isOr() ? "(" : "", isOr() ? ")" : "")); - } - - private String delimiter() { - return type() == Type.AND ? "·" : " + "; - } - - default Stream reduceNegatedPairsTo(Expr substitute) { - final List negatedExpressions = - operandsOfType(Type.NOT).stream() - .map(not -> not.operands().iterator().next()) - .map(Expr::simplify) - .toList(); - final Set rest = - operandsExcludingType(Type.NOT).stream().map(Expr::simplify).collect(toSet()); - - final Stream matched = - rest.stream().map(o -> negatedExpressions.contains(o) ? substitute : o); - - final Stream nonMatched = - negatedExpressions.stream() - .filter(o -> !rest.contains(o)) - .map(Expr::not) // wrap in not operator again; - .map(Expr::simplify); - - return concat(matched, nonMatched); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Eq.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Eq.java deleted file mode 100644 index 3b2e10d1..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Eq.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.spotify.confidence.expressions; - -import com.spotify.confidence.shaded.flags.types.v1.Targeting; -import java.math.BigDecimal; -import java.util.List; -import java.util.OptionalLong; - -public interface Eq { - - double EPSILON = 0.00000001d; - - boolean eq(Targeting.Value a, Targeting.Value b); - - default boolean contains(List a, Targeting.Value b) { - return a.stream().anyMatch(v -> eq(v, b)); - } - - default boolean overlap(List a, List b) { - return a.stream().anyMatch(v -> contains(b, v)); - } - - default boolean overlapNeg(List a, List bNeg) { - return a.stream().anyMatch(v -> !contains(bNeg, v)); - } - - /** - * not sure if we need this, we could just compare the proto value itself, it already has defined - * equality - */ - static Eq getEq(Targeting.Value.ValueCase valueCase) { - switch (valueCase) { - case BOOL_VALUE: - return (a, b) -> a.getBoolValue() == b.getBoolValue(); - case NUMBER_VALUE: - return (a, b) -> { - final double aValue = a.getNumberValue(); - final double bValue = b.getNumberValue(); - final OptionalLong aLong = tryGetLong(aValue); - final OptionalLong bLong = tryGetLong(bValue); - - if (aLong.isPresent() && bLong.isPresent()) { - return aLong.getAsLong() == bLong.getAsLong(); - } else { - return Math.abs(aValue - bValue) < EPSILON; - } - }; - case STRING_VALUE: - return (a, b) -> a.getStringValue().equals(b.getStringValue()); - case TIMESTAMP_VALUE: - return (a, b) -> a.getTimestampValue().equals(b.getTimestampValue()); - case VERSION_VALUE: - return (a, b) -> a.getVersionValue().getVersion().equals(b.getVersionValue().getVersion()); - default: - throw new UnsupportedOperationException(); - } - } - - private static OptionalLong tryGetLong(double value) { - try { - return OptionalLong.of(BigDecimal.valueOf(value).longValueExact()); - } catch (ArithmeticException e) { - return OptionalLong.empty(); - } - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Expr.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Expr.java deleted file mode 100644 index a74bc59a..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Expr.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.spotify.confidence.expressions; - -import static java.util.Collections.unmodifiableSet; -import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toCollection; - -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - -/* Expr ADT written with Java 15 sealed interfaces and records */ -public sealed interface Expr extends Comparable permits AndOr, False, Not, Ref, True { - - Expr T = new True(); - Expr F = new False(); - - // this also defines the sort order of operands - enum Type { - TRUE, - FALSE, - REF, - NOT, - AND, - OR - } - - Type type(); - - String name(); - - Set operands(); - - static Expr ref(String name) { - return new Ref(name); - } - - static Expr not(Expr expr) { - return new Not(expr); - } - - static Expr and(Expr... operands) { - return and(List.of(operands)); - } - - static Expr and(Collection operands) { - return new And(sort(operands)); - } - - static Expr or(Expr... operands) { - return or(List.of(operands)); - } - - static Expr or(Collection operands) { - return new Or(sort(operands)); - } - - default Set operandsOfType(Type type) { - return operands().stream().filter(o -> o.type() == type).collect(toCollection(TreeSet::new)); - } - - default Set operandsExcludingType(Type type) { - return operands().stream().filter(o -> o.type() != type).collect(toCollection(TreeSet::new)); - } - - default boolean isTrue() { - return this instanceof True; - } - - default boolean isFalse() { - return this instanceof False; - } - - default boolean isRef() { - return this instanceof Ref; - } - - default boolean isNot() { - return this instanceof Not; - } - - default boolean isAnd() { - return this instanceof And; - } - - default boolean isOr() { - return this instanceof Or; - } - - default Expr simplify() { - return this; - } - - @Override - default int compareTo(Expr other) { - return Comparator.comparing(Expr::type).thenComparing(Expr::name).compare(this, other); - } - - static Set sort(Collection operands) { - return unmodifiableSet(new TreeSet<>(requireNonNull(operands))); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/ExprNormalizer.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/ExprNormalizer.java deleted file mode 100644 index b4bf1e42..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/ExprNormalizer.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.spotify.confidence.expressions; - -import static com.spotify.confidence.expressions.Expr.and; -import static com.spotify.confidence.expressions.Expr.not; -import static com.spotify.confidence.expressions.Expr.or; -import static java.util.stream.Collectors.toList; - -import com.spotify.confidence.expressions.Expr.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -/** Normalizes arbitrary {@link Expr} instances into a Sum of Products form. */ -class ExprNormalizer { - - static Expr normalize(final Expr expression) { - final Expr simplified = expression.simplify(); - return normalizeInternal(simplified).simplify(); - } - - private static Expr normalizeInternal(final Expr expression) { - return switch (expression.type()) { - case NOT -> normalizeNot(expression.operands().iterator().next()); - case AND -> normalizeAnd(expression.operands()); - case OR -> normalizeOr(expression.operands()); - case TRUE, FALSE, REF -> expression; - }; - } - - // pushes down negations further down the expression tree - private static Expr normalizeNot(final Expr operand) { - return switch (operand.type()) { - case AND -> - // !(a*b*...*x) -> !a+!b+...+!x - normalize( - or( - operand.operands().stream() - .map(Expr::not) - .map(ExprNormalizer::normalize) - .collect(toList()))); - case OR -> - // !(a+b+...+x) -> !a*!b*...*!x - normalize( - and( - operand.operands().stream() - .map(Expr::not) - .map(ExprNormalizer::normalize) - .collect(toList()))); - default -> not(operand); - }; - } - - // pulls up sums further up in the expression tree - private static Expr normalizeAnd(final Set and) { - final Expr normalized = and(and.stream().map(ExprNormalizer::normalize).collect(toList())); - - // (a+b+c...) (d+e+f...) ... - final List ors = - normalized.operands().stream().filter(operand -> operand.type() == Type.OR).toList(); - - // start with a single term of all factors: everything that is not an or - List> terms = - List.of( - normalized.operands().stream() - .filter(operand -> operand.type() != Type.OR) - .collect(toList())); - - // apply distributive law over all 'or' expressions - List> newSum = new ArrayList<>(); - for (Expr or : ors) { - for (List term : terms) { - for (Expr orTerm : or.operands()) { - final List product = new ArrayList<>(term); - product.add(orTerm); - newSum.add(product); - } - } - terms = newSum; - newSum = new ArrayList<>(); - } - - // single term does not need a sum - if (terms.size() == 1) { - final List expressions = terms.get(0); - if (expressions.size() == 1) { - return expressions.get(0); - } - return and(expressions); - } - // sum of products - return normalize(or(terms.stream().map(Expr::and).collect(toList()))); - } - - private static Expr normalizeOr(final Set or) { - return or(or.stream().map(ExprNormalizer::normalize).collect(toList())); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/False.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/False.java deleted file mode 100644 index bb14c202..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/False.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.spotify.confidence.expressions; - -import java.util.Set; - -record False() implements Expr { - - @Override - public Type type() { - return Type.FALSE; - } - - @Override - public String name() { - return "F"; - } - - @Override - public Set operands() { - return Set.of(); - } - - @Override - public String toString() { - return name(); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Not.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Not.java deleted file mode 100644 index 7d9ef130..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Not.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.spotify.confidence.expressions; - -import java.util.Set; - -record Not(Expr expr) implements Expr { - - @Override - public Type type() { - return Type.NOT; - } - - @Override - public Expr simplify() { - final Expr operand = operand().simplify(); - - if (operand.isNot()) { - // !!a -> a - return operand.operands().iterator().next(); - } else if (operand.isTrue() || operand.isFalse()) { - return operand.isTrue() ? F : T; - } - - return Expr.not(operand); - } - - @Override - public String name() { - if (operand().isAnd()) { - return "!(" + expr().name() + ")"; - } - return "!" + expr().name(); - } - - @Override - public Set operands() { - return Set.of(expr()); - } - - public Expr operand() { - return operands().iterator().next(); - } - - @Override - public String toString() { - return name(); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Or.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Or.java deleted file mode 100644 index ea8d6bfb..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Or.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.spotify.confidence.expressions; - -import static java.util.stream.Collectors.toSet; - -import java.util.Set; -import java.util.stream.Stream; - -record Or(Set operands) implements AndOr { - - @Override - public Type type() { - return Type.OR; - } - - @Override - public Expr simplify() { - final Set reduced = - reduceNegatedPairsTo(T) - .filter(o -> !o.isFalse()) - .flatMap(o -> o.isOr() ? o.operands().stream() : Stream.of(o)) - .collect(toSet()); - - if (reduced.contains(T)) { - return T; - } else if (reduced.size() == 1) { - return reduced.iterator().next(); - } else if (reduced.isEmpty()) { - return F; - } - return Expr.or(reduced); - } - - @Override - public String toString() { - return name(); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ord.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ord.java deleted file mode 100644 index 8ea85652..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ord.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.spotify.confidence.expressions; - -import static com.google.protobuf.util.Timestamps.toNanos; - -import com.spotify.confidence.SemanticVersion; -import com.spotify.confidence.shaded.flags.types.v1.Targeting; - -public interface Ord { - - boolean lt(Targeting.Value a, Targeting.Value b); - - boolean lte(Targeting.Value a, Targeting.Value b); - - static Ord getOrd(Targeting.Value value) { - switch (value.getValueCase()) { - case NUMBER_VALUE: - return new Ord() { - @Override - public boolean lt(Targeting.Value a, Targeting.Value b) { - return a.getNumberValue() < b.getNumberValue(); - } - - @Override - public boolean lte(Targeting.Value a, Targeting.Value b) { - return a.getNumberValue() <= b.getNumberValue(); - } - }; - case TIMESTAMP_VALUE: - return new Ord() { - @Override - public boolean lt(Targeting.Value a, Targeting.Value b) { - return toNanos(a.getTimestampValue()) < toNanos(b.getTimestampValue()); - } - - @Override - public boolean lte(Targeting.Value a, Targeting.Value b) { - return toNanos(a.getTimestampValue()) <= toNanos(b.getTimestampValue()); - } - }; - case VERSION_VALUE: - return new Ord() { - @Override - public boolean lt(Targeting.Value a, Targeting.Value b) { - final SemanticVersion versionA = - SemanticVersion.fromVersionString(a.getVersionValue().getVersion()); - final SemanticVersion versionB = - SemanticVersion.fromVersionString(b.getVersionValue().getVersion()); - return versionA.compareTo(versionB) < 0; - } - - @Override - public boolean lte(Targeting.Value a, Targeting.Value b) { - final SemanticVersion versionA = - SemanticVersion.fromVersionString(a.getVersionValue().getVersion()); - final SemanticVersion versionB = - SemanticVersion.fromVersionString(b.getVersionValue().getVersion()); - return versionA.compareTo(versionB) <= 0; - } - }; - - case BOOL_VALUE: - case STRING_VALUE: - default: - throw new UnsupportedOperationException(value.getValueCase() + " is not comparable"); - } - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ref.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ref.java deleted file mode 100644 index ceaee295..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/Ref.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.spotify.confidence.expressions; - -import java.util.Set; - -record Ref(String name) implements Expr { - - @Override - public Type type() { - return Type.REF; - } - - @Override - public Set operands() { - return Set.of(); - } - - @Override - public String toString() { - return name(); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/TargetingExpr.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/TargetingExpr.java deleted file mode 100644 index 83aa3bfc..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/TargetingExpr.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.spotify.confidence.expressions; - -import static com.spotify.confidence.Util.evalExpression; -import static com.spotify.confidence.expressions.Expr.T; -import static com.spotify.confidence.expressions.Expr.and; -import static com.spotify.confidence.expressions.Expr.not; -import static com.spotify.confidence.expressions.Expr.or; -import static com.spotify.confidence.expressions.Expr.ref; -import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toList; - -import com.spotify.confidence.shaded.flags.types.v1.Expression; -import com.spotify.confidence.shaded.flags.types.v1.Targeting; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.Criterion; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -public class TargetingExpr { - - private final Expr expression; - private final Map refs; - - TargetingExpr(final Expr expression, final Map refs) { - // todo: check invariants - // . all refs in expression exist as keys in the refs map - - this.expression = requireNonNull(expression); - this.refs = requireNonNull(refs); - } - - public Map refs() { - return refs; - } - - public boolean eval(Set trueRefs) { - return evalExpression(expression, trueRefs); - } - - @Override - public String toString() { - return "TargetingExpr{" + "expression=" + expression + '}'; - } - - public static TargetingExpr fromTargeting(final Targeting targeting) { - final Expr expr = ExprNormalizer.normalize(convert(targeting.getExpression())); - final Set includedRefs = new HashSet<>(); - collectRefs(expr, includedRefs); - final Map refs = new HashMap<>(targeting.getCriteriaMap()); - for (String key : Set.copyOf(refs.keySet())) { - if (!includedRefs.contains(key)) { - refs.remove(key); - } - } - - return new TargetingExpr(expr, refs); - } - - static Expr convert(final Expression expression) { - return switch (expression.getExpressionCase()) { - case REF -> ref(expression.getRef()); - case NOT -> not(convert(expression.getNot())); - case AND -> - and( - expression.getAnd().getOperandsList().stream() - .map(TargetingExpr::convert) - .collect(toList())); - case OR -> - or( - expression.getOr().getOperandsList().stream() - .map(TargetingExpr::convert) - .collect(toList())); - default -> T; - }; - } - - private static void collectRefs(final Expr expression, final Set refs) { - switch (expression.type()) { - case REF -> refs.add(expression.name()); - case NOT -> collectRefs(expression.operands().iterator().next(), refs); - case AND, OR -> { - expression.operands().forEach(e -> collectRefs(e, refs)); - expression.operands().forEach(e -> collectRefs(e, refs)); - } - } - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/True.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/True.java deleted file mode 100644 index 0d4d52cc..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/expressions/True.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.spotify.confidence.expressions; - -import java.util.Set; - -record True() implements Expr { - - @Override - public Type type() { - return Type.TRUE; - } - - @Override - public String name() { - return "T"; - } - - @Override - public Set operands() { - return Set.of(); - } - - @Override - public String toString() { - return name(); - } -} From 0ef3026f0fc0dc2e84c7a94a34cdd629b514f573 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 13 Oct 2025 15:41:47 +0200 Subject: [PATCH 6/8] refactor: More cleanups --- .../java/com/spotify/confidence/Account.java | 14 -- .../com/spotify/confidence/AssignLogger.java | 27 --- .../confidence/BadRequestException.java | 7 - .../DefaultDeadlineClientInterceptor.java | 18 -- .../java/com/spotify/confidence/EvalUtil.java | 158 ------------------ .../com/spotify/confidence/FlagLogger.java | 50 ++---- .../com/spotify/confidence/HealthStatus.java | 4 - .../confidence/InternalServerException.java | 7 - .../java/com/spotify/confidence/JwtUtils.java | 8 - .../java/com/spotify/confidence/Region.java | 6 - .../com/spotify/confidence/ResolvedValue.java | 85 ---------- .../SchemaFromEvaluationContext.java | 135 --------------- .../spotify/confidence/SemanticVersion.java | 119 ------------- .../com/spotify/confidence/Targetings.java | 7 - .../confidence/UnauthenticatedException.java | 7 - .../com/spotify/confidence/ResolveTest.java | 54 +++--- .../java/com/spotify/confidence/TestBase.java | 24 +-- .../spotify/confidence/WasmResolveTest.java | 13 +- 18 files changed, 52 insertions(+), 691 deletions(-) delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/Account.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/BadRequestException.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/EvalUtil.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/InternalServerException.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/Region.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/ResolvedValue.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/SchemaFromEvaluationContext.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/UnauthenticatedException.java diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Account.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Account.java deleted file mode 100644 index 8e366987..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Account.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.spotify.confidence; - -import java.util.Optional; - -record Account(String name, Optional region) { - - Account(String name, Region region) { - this(name, Optional.of(region)); - } - - Account(String name) { - this(name, Optional.empty()); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/AssignLogger.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/AssignLogger.java index 5f2c41b2..98ddb9ad 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/AssignLogger.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/AssignLogger.java @@ -19,7 +19,6 @@ import java.util.Collection; import java.util.List; import java.util.Timer; -import java.util.TimerTask; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; @@ -29,7 +28,6 @@ class AssignLogger implements Closeable { private static final Logger logger = LoggerFactory.getLogger(AssignLogger.class); - private static final Logger LOG = LoggerFactory.getLogger(AssignLogger.class); // Max size minus some wiggle room private static final int GRPC_MESSAGE_MAX_SIZE = 4194304 - 1048576; @@ -63,31 +61,6 @@ private Long timeSinceLastAssigned() { return Duration.between(lastFlagAssigned, Instant.now()).toMillis(); } - static AssignLogger createStarted( - InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceBlockingStub flagLoggerStub, - Duration checkpointInterval, - MetricRegistry metricRegistry, - long capacity) { - final Timer timer = new Timer("assign-logger-timer", true); - final AssignLogger assignLogger = - new AssignLogger(flagLoggerStub, timer, metricRegistry, capacity); - - timer.scheduleAtFixedRate( - new TimerTask() { - @Override - public void run() { - try { - assignLogger.checkpoint(); - } catch (Exception e) { - LOG.error("Failed to checkpoint assignments", e); - } - } - }, - checkpointInterval.toMillis(), - checkpointInterval.toMillis()); - return assignLogger; - } - @VisibleForTesting Collection queuedAssigns() { return ImmutableList.copyOf(queue); diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/BadRequestException.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/BadRequestException.java deleted file mode 100644 index 66d44a2f..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/BadRequestException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.spotify.confidence; - -class BadRequestException extends RuntimeException { - BadRequestException(String message) { - super(message); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/DefaultDeadlineClientInterceptor.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/DefaultDeadlineClientInterceptor.java index abd2e01a..b8be65b6 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/DefaultDeadlineClientInterceptor.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/DefaultDeadlineClientInterceptor.java @@ -32,24 +32,6 @@ class DefaultDeadlineClientInterceptor implements ClientInterceptor { this.duration = duration; } - /** - * Get the current default deadline duration. - * - * @return the current default deadline duration - */ - Duration getDuration() { - return duration; - } - - /** - * Set a new default deadline duration. - * - * @param duration the new default deadline duration - */ - void setDuration(Duration duration) { - this.duration = duration; - } - @Override public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/EvalUtil.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/EvalUtil.java deleted file mode 100644 index 634932c4..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/EvalUtil.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.spotify.confidence; - -import static com.spotify.confidence.Targetings.boolValue; -import static com.spotify.confidence.Targetings.numberValue; -import static com.spotify.confidence.Targetings.semverValue; -import static com.spotify.confidence.Targetings.stringValue; -import static com.spotify.confidence.shaded.flags.types.v1.Targeting.Value.ValueCase.BOOL_VALUE; -import static java.lang.Boolean.parseBoolean; -import static java.lang.Double.parseDouble; - -import com.google.protobuf.Struct; -import com.google.protobuf.Value; -import com.google.protobuf.util.Timestamps; -import com.google.protobuf.util.Values; -import com.spotify.confidence.shaded.flags.types.v1.FlagSchema; -import com.spotify.confidence.shaded.flags.types.v1.Targeting; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.ListValue; -import com.spotify.confidence.shaded.flags.types.v1.Targeting.Value.ValueCase; -import io.grpc.Status; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeParseException; -import java.util.List; -import java.util.Map; - -final class EvalUtil { - - private EvalUtil() {} - - /** - * Tries to parse the a {@link Value} to an expected targeting type value. If the expected type is - * not set, or there is no meaningful conversion to the expected type, this function will return - * null. The null value should be treated as not matching anything further down in the evaluation - * logic. - */ - static Targeting.Value convertToTargetingValue( - Value value, Targeting.Value.ValueCase expectedType) { - return switch (value.getKindCase()) { - case NULL_VALUE -> { - if (expectedType == ValueCase.VALUE_NOT_SET) { - yield Targeting.Value.getDefaultInstance(); - } - yield null; - } - case NUMBER_VALUE -> convertNumber(value.getNumberValue(), expectedType); - case STRING_VALUE -> convertString(value.getStringValue(), expectedType); - case BOOL_VALUE -> { - if (expectedType == BOOL_VALUE) { - yield boolValue(value.getBoolValue()); - } - yield null; - } - case LIST_VALUE -> convertList(value, expectedType); - case STRUCT_VALUE -> - throw Status.INVALID_ARGUMENT - .withDescription(value.getKindCase() + " values not supported for targeting") - .asRuntimeException(); - default -> throw new IllegalStateException("Unexpected value: " + value.getKindCase()); - }; - } - - private static Targeting.Value convertList(Value value, ValueCase expectedType) { - final List convertedValues = - value.getListValue().getValuesList().stream() - .map(v -> convertToTargetingValue(v, expectedType)) - .toList(); - return Targeting.Value.newBuilder() - .setListValue(ListValue.newBuilder().addAllValues(convertedValues).build()) - .build(); - } - - private static Targeting.Value convertNumber( - double value, Targeting.Value.ValueCase expectedType) { - return switch (expectedType) { - case NUMBER_VALUE -> numberValue(value); - case STRING_VALUE -> stringValue(Double.toString(value)); - default -> null; - }; - } - - private static Targeting.Value convertString( - String value, Targeting.Value.ValueCase expectedType) { - return switch (expectedType) { - case BOOL_VALUE -> boolValue(parseBoolean(value)); - case NUMBER_VALUE -> numberValue(parseDouble(value)); - case STRING_VALUE -> stringValue(value); - case TIMESTAMP_VALUE -> timestampValue(parseInstant(value)); - case VERSION_VALUE -> semverValue(value); - case LIST_VALUE, VALUE_NOT_SET -> null; - }; - } - - static Instant parseInstant(String value) { - if (value.isEmpty()) { - return null; - } - try { - if (value.contains("T")) { - final int tpos = value.indexOf('T'); - if (value.endsWith("Z") - || value.substring(tpos).contains("+") - || value.substring(tpos).contains("-")) { - return ZonedDateTime.parse(value).toInstant(); - } else { - return LocalDateTime.parse(value).toInstant(ZoneOffset.UTC); - } - } else { - return LocalDate.parse(value).atStartOfDay(ZoneOffset.UTC).toInstant(); - } - } catch (DateTimeParseException exception) { - return null; - } - } - - static Targeting.Value timestampValue(final Instant instant) { - if (instant == null) { - return null; - } - return Targeting.Value.newBuilder() - .setTimestampValue(Timestamps.fromMillis(instant.toEpochMilli())) - .build(); - } - - static Struct expandToSchema(Struct struct, FlagSchema.StructFlagSchema schema) { - final Struct.Builder builder = struct.toBuilder(); - for (Map.Entry schemaEntry : schema.getSchemaMap().entrySet()) { - final String fieldName = schemaEntry.getKey(); - final Value fieldValue = builder.getFieldsOrDefault(fieldName, null); - final FlagSchema fieldSchema = schemaEntry.getValue(); - builder.putFields(fieldName, expandToSchema(fieldValue, fieldSchema)); - } - return builder.build(); - } - - private static Value expandToSchema(Value value, FlagSchema schema) { - return switch (schema.getSchemaTypeCase()) { - case STRUCT_SCHEMA -> { - final Struct structValue = - value != null ? value.getStructValue() : Struct.getDefaultInstance(); - yield Values.of(expandToSchema(structValue, schema.getStructSchema())); - } - - case LIST_SCHEMA -> { - final List listValues = - value != null ? value.getListValue().getValuesList() : List.of(); - yield Values.of( - listValues.stream() - .map(v -> expandToSchema(v, schema.getListSchema().getElementSchema())) - .toList()); - } - - default -> value != null ? value : Values.ofNull(); - }; - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagLogger.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagLogger.java index e0fd3543..eb8c0d2d 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagLogger.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagLogger.java @@ -1,52 +1,15 @@ package com.spotify.confidence; -import static com.spotify.confidence.ResolvedValue.resolveToAssignmentReason; - -import com.google.protobuf.Struct; import com.google.protobuf.Timestamp; +import com.spotify.confidence.shaded.flags.resolver.v1.ResolveReason; import com.spotify.confidence.shaded.flags.resolver.v1.Sdk; import com.spotify.confidence.shaded.flags.resolver.v1.events.ClientInfo; -import com.spotify.confidence.shaded.flags.resolver.v1.events.FallthroughAssignment; import com.spotify.confidence.shaded.flags.resolver.v1.events.FlagAssigned; -import java.util.ArrayList; +import com.spotify.confidence.shaded.flags.resolver.v1.events.FlagAssigned.DefaultAssignment.DefaultAssignmentReason; import java.util.List; interface FlagLogger { - void logResolve( - String resolveId, - Struct evaluationContext, - Sdk sdk, - AccountClient accountClient, - List values); - - void logAssigns( - String resolveId, Sdk sdk, List flagsToApply, AccountClient accountClient); - - static List getResources(FlagAssigned flagAssigned) { - final List resources = new ArrayList<>(); - for (var flag : flagAssigned.getFlagsList()) { - if (flag.hasAssignmentInfo()) { - if (!flag.getAssignmentInfo().getSegment().isBlank()) { - resources.add(flag.getAssignmentInfo().getSegment()); - } - if (!flag.getAssignmentInfo().getVariant().isBlank()) { - resources.add(flag.getAssignmentInfo().getVariant()); - } - } - - for (FallthroughAssignment fallthroughAssignment : flag.getFallthroughAssignmentsList()) { - resources.add(fallthroughAssignment.getRule()); - } - - resources.add(flag.getFlag()); - resources.add(flag.getRule()); - } - resources.add(flagAssigned.getClientInfo().getClient()); - resources.add(flagAssigned.getClientInfo().getClientCredential()); - return resources; - } - static FlagAssigned createFlagAssigned( String resolveId, Sdk sdk, List flagsToApply, AccountClient accountClient) { final var clientInfo = @@ -90,4 +53,13 @@ static FlagAssigned createFlagAssigned( return builder.build(); } + + private static DefaultAssignmentReason resolveToAssignmentReason(ResolveReason reason) { + return switch (reason) { + case RESOLVE_REASON_NO_SEGMENT_MATCH -> DefaultAssignmentReason.NO_SEGMENT_MATCH; + case RESOLVE_REASON_NO_TREATMENT_MATCH -> DefaultAssignmentReason.NO_TREATMENT_MATCH; + case RESOLVE_REASON_FLAG_ARCHIVED -> DefaultAssignmentReason.FLAG_ARCHIVED; + default -> DefaultAssignmentReason.DEFAULT_ASSIGNMENT_REASON_UNSPECIFIED; + }; + } } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/HealthStatus.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/HealthStatus.java index 85aacaa7..1ddcfb9d 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/HealthStatus.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/HealthStatus.java @@ -21,8 +21,4 @@ synchronized void setStatus(HealthCheckResponse.ServingStatus status) { this.status.set(status); healthStatusManager.setStatus(SERVICE_NAME_ALL_SERVICES, status); } - - HealthCheckResponse.ServingStatus get() { - return status.get(); - } } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/InternalServerException.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/InternalServerException.java deleted file mode 100644 index 621c2ae5..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/InternalServerException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.spotify.confidence; - -class InternalServerException extends RuntimeException { - InternalServerException(String message) { - super(message); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/JwtUtils.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/JwtUtils.java index fa19e962..58df639a 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/JwtUtils.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/JwtUtils.java @@ -9,11 +9,7 @@ import java.util.Optional; class JwtUtils { - public static final String AUTH0_ISSUER = "https://auth.confidence.dev/"; - public static final String[] AUTH0_ISSUERS = - new String[] {"https://konfidens.eu.auth0.com/", AUTH0_ISSUER}; - public static final String BEARER_TYPE = "Bearer"; public static final Metadata.Key AUTHORIZATION_METADATA_KEY = Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER); @@ -38,10 +34,6 @@ public static Optional getClaim(final DecodedJWT jwt, final String claim) } } - public static boolean isValidToken(String token) { - return token.startsWith(BEARER_TYPE); - } - public static String getTokenAsHeader(String rawToken) { return String.format("Bearer %s", rawToken); } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Region.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Region.java deleted file mode 100644 index 835afd29..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Region.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.spotify.confidence; - -enum Region { - EU, - US -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolvedValue.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolvedValue.java deleted file mode 100644 index cfed5836..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolvedValue.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.spotify.confidence; - -import com.google.protobuf.Struct; -import com.spotify.confidence.shaded.flags.admin.v1.Flag; -import com.spotify.confidence.shaded.flags.admin.v1.Segment; -import com.spotify.confidence.shaded.flags.resolver.v1.ResolveReason; -import com.spotify.confidence.shaded.flags.resolver.v1.events.FallthroughAssignment; -import com.spotify.confidence.shaded.flags.resolver.v1.events.FlagAssigned.DefaultAssignment.DefaultAssignmentReason; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -record ResolvedValue( - Flag flag, - ResolveReason reason, - Optional matchedAssignment, - List fallthroughRules) { - ResolvedValue(Flag flag) { - this(flag, ResolveReason.RESOLVE_REASON_NO_SEGMENT_MATCH, Optional.empty(), List.of()); - } - - ResolvedValue withReason(ResolveReason reason) { - return new ResolvedValue(flag, reason, matchedAssignment, fallthroughRules); - } - - ResolvedValue withMatch( - String assignmentId, - String variant, - String unit, - Struct value, - Segment segment, - Flag.Rule matchedRule) { - return new ResolvedValue( - flag, - ResolveReason.RESOLVE_REASON_MATCH, - Optional.of( - new AssignmentMatch( - assignmentId, - unit, - Optional.of(variant), - Optional.of(value), - segment, - matchedRule)), - fallthroughRules); - } - - ResolvedValue withClientDefaultMatch( - String assignmentId, String unit, Segment segment, Flag.Rule matchedRule) { - return new ResolvedValue( - flag, - ResolveReason.RESOLVE_REASON_MATCH, - Optional.of( - new AssignmentMatch( - assignmentId, unit, Optional.empty(), Optional.empty(), segment, matchedRule)), - fallthroughRules); - } - - static DefaultAssignmentReason resolveToAssignmentReason(ResolveReason reason) { - return switch (reason) { - case RESOLVE_REASON_NO_SEGMENT_MATCH -> DefaultAssignmentReason.NO_SEGMENT_MATCH; - case RESOLVE_REASON_NO_TREATMENT_MATCH -> DefaultAssignmentReason.NO_TREATMENT_MATCH; - case RESOLVE_REASON_FLAG_ARCHIVED -> DefaultAssignmentReason.FLAG_ARCHIVED; - default -> DefaultAssignmentReason.DEFAULT_ASSIGNMENT_REASON_UNSPECIFIED; - }; - } - - ResolvedValue attributeFallthroughRule(Flag.Rule rule, String assignmentId, String unit) { - final List attributed = new ArrayList<>(fallthroughRules); - attributed.add(new FallthroughRule(rule, assignmentId, unit)); - return new ResolvedValue(flag, reason, matchedAssignment, attributed); - } - - List fallthroughAssignments() { - return fallthroughRules().stream() - .map( - assignment -> - FallthroughAssignment.newBuilder() - .setAssignmentId(assignment.assignmentId()) - .setRule(assignment.rule().getName()) - .setTargetingKey(assignment.targetingKey()) - .setTargetingKeySelector(assignment.rule().getTargetingKeySelector()) - .build()) - .toList(); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/SchemaFromEvaluationContext.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/SchemaFromEvaluationContext.java deleted file mode 100644 index e1a01367..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/SchemaFromEvaluationContext.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.spotify.confidence; - -import com.google.protobuf.Struct; -import com.google.protobuf.Value; -import com.spotify.confidence.shaded.flags.admin.v1.ContextFieldSemanticType; -import com.spotify.confidence.shaded.flags.admin.v1.EvaluationContextSchemaField; -import java.time.LocalDate; -import java.time.format.DateTimeParseException; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -class SchemaFromEvaluationContext { - private static final int MIN_DATE_LENGTH = "2025-04-01".length(); - private static final int MIN_TIMESTAMP_LENGTH = "2025-04-01T0000".length(); - private static final Set COUNTRY_CODES = - Locale.getISOCountries(Locale.IsoCountryCode.PART1_ALPHA2); - - record DerivedClientSchema( - Map fields, - Map semanticTypes) {} - - static DerivedClientSchema getSchema(Struct evaluationContext) { - final Map flatSchema = new HashMap<>(); - final Map semanticTypes = new HashMap<>(); - flattenedSchema(evaluationContext, "", flatSchema, semanticTypes); - return new DerivedClientSchema(flatSchema, semanticTypes); - } - - private static void flattenedSchema( - Struct struct, - String fieldPath, - Map flatSchema, - Map semanticTypes) { - struct - .getFieldsMap() - .forEach( - (field, value) -> { - if (value.getKindCase() == Value.KindCase.STRUCT_VALUE) { - flattenedSchema( - value.getStructValue(), fieldPath + field + ".", flatSchema, semanticTypes); - } else { - addFieldSchema(value, fieldPath + field, flatSchema, semanticTypes); - } - }); - } - - private static void addFieldSchema( - Value value, - String fieldPath, - Map flatSchema, - Map semanticTypes) { - final var kind = value.getKindCase(); - if (kind == Value.KindCase.STRING_VALUE) { - flatSchema.put(fieldPath, EvaluationContextSchemaField.Kind.STRING_KIND); - guessSemanticType(value.getStringValue(), fieldPath, semanticTypes); - } else if (kind == Value.KindCase.BOOL_VALUE) { - flatSchema.put(fieldPath, EvaluationContextSchemaField.Kind.BOOL_KIND); - } else if (kind == Value.KindCase.NUMBER_VALUE) { - flatSchema.put(fieldPath, EvaluationContextSchemaField.Kind.NUMBER_KIND); - } else if (kind == Value.KindCase.NULL_VALUE) { - flatSchema.put(fieldPath, EvaluationContextSchemaField.Kind.NULL_KIND); - } else if (kind == Value.KindCase.LIST_VALUE) { - final var subKinds = - value.getListValue().getValuesList().stream() - .map(Value::getKindCase) - .collect(Collectors.toSet()); - if (subKinds.size() == 1) { - addFieldSchema(value.getListValue().getValues(0), fieldPath, flatSchema, semanticTypes); - } - } - } - - private static void guessSemanticType( - String value, String fieldPath, Map semanticTypes) { - final String lowerCasePath = fieldPath.toLowerCase(Locale.ROOT); - if (lowerCasePath.contains("country")) { - if (COUNTRY_CODES.contains(value.toUpperCase())) { - semanticTypes.put( - fieldPath, - ContextFieldSemanticType.newBuilder() - .setCountry( - ContextFieldSemanticType.CountrySemanticType.newBuilder() - .setFormat( - ContextFieldSemanticType.CountrySemanticType.CountryFormat - .TWO_LETTER_ISO_CODE) - .build()) - .build()); - } - } else if (isDate(value)) { - semanticTypes.put( - fieldPath, - ContextFieldSemanticType.newBuilder() - .setDate(ContextFieldSemanticType.DateSemanticType.getDefaultInstance()) - .build()); - } else if (isTimestamp(value)) { - semanticTypes.put( - fieldPath, - ContextFieldSemanticType.newBuilder() - .setTimestamp(ContextFieldSemanticType.TimestampSemanticType.getDefaultInstance()) - .build()); - } else if (isSemanticVersion(value)) { - semanticTypes.put( - fieldPath, - ContextFieldSemanticType.newBuilder() - .setVersion(ContextFieldSemanticType.VersionSemanticType.getDefaultInstance()) - .build()); - } - } - - private static boolean isSemanticVersion(String value) { - return SemanticVersion.isValid(value); - } - - private static boolean isTimestamp(String value) { - if (value.length() < MIN_TIMESTAMP_LENGTH) { - return false; - } - return EvalUtil.parseInstant(value) != null; - } - - private static boolean isDate(String value) { - if (value.length() < MIN_DATE_LENGTH) { - return false; - } - try { - LocalDate.parse(value); - return true; - } catch (DateTimeParseException ex) { - return false; - } - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java deleted file mode 100644 index c10abb84..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.spotify.confidence; - -import java.util.Objects; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class SemanticVersion implements Comparable { - - private static final Pattern VERSION_PATTERN = - Pattern.compile("^(\\d{1,3})(\\.\\d{1,3})(\\.\\d{1,3})?(\\.\\d{1,10})?$"); - - private final int major; - private final int minor; - private final int patch; - private final int tag; - - private SemanticVersion(int major, int minor, int patch, int tag) { - this.major = major; - this.minor = minor; - this.patch = patch; - this.tag = tag; - } - - /** - * Builds a Semantic version from String - * - * @param version String representing semantic version - * @return instance of Semantic version - * @throws IllegalArgumentException in case an invalid Semver is provided - */ - public static SemanticVersion fromVersionString(final String version) { - if (version == null || version.isEmpty()) { - throw new IllegalArgumentException("Invalid version, version must be non-empty and not null"); - } - - final String[] split = version.split("-"); - - if (split.length == 0) { - throw new IllegalArgumentException("Invalid semantic version string: " + version); - } - - final String tokenBeforeHyphens = split[0]; - - final Matcher matcher = VERSION_PATTERN.matcher(tokenBeforeHyphens); - if (!matcher.find()) { - throw new IllegalArgumentException("Invalid semantic version string: " + version); - } - - final int major = parseVersionSegment(matcher.group(1), -1); - final int minor = parseVersionSegment(matcher.group(2), 0); - final int patch = parseVersionSegment(matcher.group(3), 0); - final int tag = parseVersionSegment(matcher.group(4), 0); - - if (major < 0 || minor < 0 || patch < 0 || tag < 0) { - throw new IllegalArgumentException("Invalid semantic version string: " + version); - } - - return new SemanticVersion(major, minor, patch, tag); - } - - static boolean isValid(final String version) { - try { - fromVersionString(version); - } catch (IllegalArgumentException ex) { - return false; - } - - return true; - } - - static int parseVersionSegment(final String segment, int defaultValue) { - return Optional.ofNullable(emptyToNull(segment)) - .map(s -> s.replace(".", "")) - .map(Integer::parseInt) - .orElse(defaultValue); - } - - @Override - public int compareTo(final SemanticVersion other) { - int result = major - other.major; - if (result == 0) { - result = minor - other.minor; - if (result == 0) { - result = patch - other.patch; - if (result == 0) { - result = tag - other.tag; - } - } - } - return result; - } - - @Override - public String toString() { - return String.format("%d.%d.%d.%d", major, minor, patch, tag); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (!(o instanceof SemanticVersion)) { - return false; - } - final SemanticVersion that = (SemanticVersion) o; - return major == that.major && minor == that.minor && patch == that.patch && tag == that.tag; - } - - @Override - public int hashCode() { - return Objects.hash(major, minor, patch, tag); - } - - private static String emptyToNull(final String string) { - return string == null || string.isEmpty() ? null : string; - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java index ac3a61be..425e9861 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java @@ -1,15 +1,8 @@ package com.spotify.confidence; -import com.spotify.confidence.shaded.flags.types.v1.Expression; import com.spotify.confidence.shaded.flags.types.v1.Targeting; -import java.util.Collection; class Targetings { - static Expression or(final Collection expressions) { - return Expression.newBuilder() - .setOr(Expression.Operands.newBuilder().addAllOperands(expressions).build()) - .build(); - } static Targeting.Value boolValue(final boolean value) { return Targeting.Value.newBuilder().setBoolValue(value).build(); diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/UnauthenticatedException.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/UnauthenticatedException.java deleted file mode 100644 index 23693126..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/UnauthenticatedException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.spotify.confidence; - -class UnauthenticatedException extends RuntimeException { - UnauthenticatedException(String message) { - super(message); - } -} diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java index 6b5cc9f1..1c4058be 100644 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java +++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java @@ -11,7 +11,6 @@ import com.spotify.confidence.shaded.flags.admin.v1.Segment; import com.spotify.confidence.shaded.flags.resolver.v1.ResolveReason; import com.spotify.confidence.shaded.flags.types.v1.FlagSchema; -import java.util.BitSet; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeAll; @@ -48,8 +47,8 @@ abstract class ResolveTest extends TestBase { .build()) .build(); private static final String segmentA = "segments/seg-a"; - static final ResolverState exampleState; - static final ResolverState exampleStateWithMaterialization; + static final byte[] exampleStateBytes; + static final byte[] exampleStateWithMaterializationBytes; private static final Map flags = Map.of( flag1, @@ -157,32 +156,14 @@ abstract class ResolveTest extends TestBase { .build()); protected static final Map segments = Map.of(segmentA, Segment.newBuilder().setName(segmentA).build()); - protected static final Map bitsets = Map.of(segmentA, getBitsetAllSet()); static { - exampleState = - new ResolverState( - Map.of( - account, - new AccountState( - new Account(account, Region.EU), flags, segments, bitsets, secrets, "abc")), - secrets); - exampleStateWithMaterialization = - new ResolverState( - Map.of( - account, - new AccountState( - new Account(account, Region.EU), - flagsWithMaterialization, - segments, - bitsets, - secrets, - "abc")), - secrets); + exampleStateBytes = buildResolverStateBytes(flags); + exampleStateWithMaterializationBytes = buildResolverStateBytes(flagsWithMaterialization); } protected ResolveTest(boolean isWasm) { - super(exampleState); + super(exampleStateBytes); } @BeforeAll @@ -298,4 +279,29 @@ public void testResolveDecimalUsername() { assertEquals( ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR, response.getResolvedFlags(0).getReason()); } + + private static byte[] buildResolverStateBytes(Map flagsMap) { + final var builder = com.spotify.confidence.shaded.flags.admin.v1.ResolverState.newBuilder(); + builder.addAllFlags(flagsMap.values()); + builder.addAllSegmentsNoBitsets(segments.values()); + // All-one bitset for each segment + segments + .keySet() + .forEach( + name -> + builder.addBitsets( + com.spotify.confidence.shaded.flags.admin.v1.ResolverState.PackedBitset + .newBuilder() + .setSegment(name) + .setFullBitset(true) + .build())); + builder.addClients( + com.spotify.confidence.shaded.iam.v1.Client.newBuilder().setName(clientName).build()); + builder.addClientCredentials( + com.spotify.confidence.shaded.iam.v1.ClientCredential.newBuilder() + .setName(credentialName) + .setClientSecret(TestBase.secret) + .build()); + return builder.build().toByteArray(); + } } diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java index 9396fc7e..4fb64751 100644 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java +++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java @@ -10,21 +10,16 @@ import com.spotify.confidence.shaded.flags.resolver.v1.WriteFlagLogsRequest; import com.spotify.confidence.shaded.iam.v1.Client; import com.spotify.confidence.shaded.iam.v1.ClientCredential; -import java.util.BitSet; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.BeforeEach; public class TestBase { - protected static final AtomicReference resolverState = - new AtomicReference<>(new ResolverState(Map.of(), Map.of())); - protected final ResolverFallback mockFallback = mock(ResolverFallback.class); protected static final ClientCredential.ClientSecret secret = ClientCredential.ClientSecret.newBuilder().setSecret("very-secret").build(); - protected final ResolverState desiredState; + protected final byte[] desiredStateBytes; protected static LocalResolverServiceFactory resolverServiceFactory; static final String account = "accounts/foo"; static final String clientName = "clients/client"; @@ -40,8 +35,8 @@ public class TestBase { .setClientSecret(secret) .build())); - protected TestBase(ResolverState state) { - this.desiredState = state; + protected TestBase(byte[] stateBytes) { + this.desiredStateBytes = stateBytes; final var wasmResolverApi = new SwapWasmResolverApi( new WasmFlagLogger() { @@ -51,7 +46,7 @@ public void write(WriteFlagLogsRequest request) {} @Override public void shutdown() {} }, - desiredState.toProto().toByteArray(), + desiredStateBytes, "", mockFallback); resolverServiceFactory = new LocalResolverServiceFactory(wasmResolverApi, mockFallback); @@ -60,9 +55,7 @@ public void shutdown() {} protected static void setup() {} @BeforeEach - protected void setUp() { - resolverState.set(desiredState); - } + protected void setUp() {} protected ResolveFlagsResponse resolveWithContext( List flags, @@ -128,11 +121,4 @@ protected ResolveFlagsResponse resolveWithContext( List flags, String username, String structFieldName, Struct struct, boolean apply) { return resolveWithContext(flags, username, structFieldName, struct, apply, secret.getSecret()); } - - protected static BitSet getBitsetAllSet() { - final BitSet bitset = new BitSet(1000000); - bitset.flip(0, bitset.size()); - - return bitset; - } } diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java index 78a6b622..557cd6ab 100644 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java +++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java @@ -30,8 +30,8 @@ import org.junit.jupiter.api.Test; public class WasmResolveTest extends ResolveTest { - // Override desiredState to use materialization-enabled state for StickyResolveStrategy tests - protected final ResolverState desiredState = ResolveTest.exampleStateWithMaterialization; + // Use materialization-enabled state bytes for StickyResolveStrategy tests + protected final byte[] desiredStateBytes = ResolveTest.exampleStateWithMaterializationBytes; public WasmResolveTest() { super(true); @@ -39,8 +39,7 @@ public WasmResolveTest() { @Test public void testAccountStateProviderInterface() { - final AccountStateProvider customProvider = - () -> ResolveTest.exampleState.toProto().toByteArray(); + final AccountStateProvider customProvider = () -> ResolveTest.exampleStateBytes; final OpenFeatureLocalResolveProvider localResolveProvider = new OpenFeatureLocalResolveProvider( customProvider, @@ -101,7 +100,7 @@ public void testResolverFallbackWhenMaterializationsMissing() { // Create the provider with mocked fallback strategy final OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider( - () -> desiredState.toProto().toByteArray(), "", secret.getSecret(), mockFallback); + () -> desiredStateBytes, "", secret.getSecret(), mockFallback); // Make the resolve request using OpenFeature API final ProviderEvaluation evaluation = @@ -135,7 +134,7 @@ public void testMaterializationRepositoryWhenMaterializationsMissing() { // Create the provider with mocked repository strategy final OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider( - () -> desiredState.toProto().toByteArray(), "", secret.getSecret(), mockRepository); + () -> desiredStateBytes, "", secret.getSecret(), mockRepository); // Make the resolve request using OpenFeature API final ProviderEvaluation evaluation = @@ -163,7 +162,7 @@ public void testConcurrentResolveLoadTest() throws InterruptedException { // Create the provider using normal exampleState (not with materialization) final OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider( - () -> ResolveTest.exampleState.toProto().toByteArray(), + () -> ResolveTest.exampleStateBytes, "", TestBase.secret.getSecret(), new ResolverFallback() { From d2ef998e665f14f8638c7a513f830e3bb773e850 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 13 Oct 2025 15:45:47 +0200 Subject: [PATCH 7/8] refactor: Even more cleanups --- .../spotify/confidence/AssignmentMatch.java | 14 ----------- .../DefaultDeadlineClientInterceptor.java | 2 +- .../spotify/confidence/FallthroughRule.java | 5 ---- .../com/spotify/confidence/FlagLogger.java | 1 + .../spotify/confidence/IsClosedException.java | 3 +++ .../confidence/MaterializationInfo.java | 8 ------- .../com/spotify/confidence/Targetings.java | 24 ------------------- .../ThreadLocalSwapWasmResolverApi.java | 8 ------- .../spotify/confidence/WasmFlagLogger.java | 9 +++++++ .../spotify/confidence/WasmResolveApi.java | 8 ------- 10 files changed, 14 insertions(+), 68 deletions(-) delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/AssignmentMatch.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/FallthroughRule.java create mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/IsClosedException.java delete mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java create mode 100644 openfeature-provider-local/src/main/java/com/spotify/confidence/WasmFlagLogger.java diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/AssignmentMatch.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/AssignmentMatch.java deleted file mode 100644 index 514f34e2..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/AssignmentMatch.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.spotify.confidence; - -import com.google.protobuf.Struct; -import com.spotify.confidence.shaded.flags.admin.v1.Flag; -import com.spotify.confidence.shaded.flags.admin.v1.Segment; -import java.util.Optional; - -record AssignmentMatch( - String assignmentId, - String targetingKey, - Optional variant, - Optional value, - Segment segment, - Flag.Rule matchedRule) {} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/DefaultDeadlineClientInterceptor.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/DefaultDeadlineClientInterceptor.java index b8be65b6..4781eec5 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/DefaultDeadlineClientInterceptor.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/DefaultDeadlineClientInterceptor.java @@ -23,7 +23,7 @@ class DefaultDeadlineClientInterceptor implements ClientInterceptor { - private Duration duration; + private final Duration duration; DefaultDeadlineClientInterceptor(Duration duration) { checkNotNull(duration, "duration"); diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/FallthroughRule.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/FallthroughRule.java deleted file mode 100644 index 7a64adc0..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/FallthroughRule.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.spotify.confidence; - -import com.spotify.confidence.shaded.flags.admin.v1.Flag; - -record FallthroughRule(Flag.Rule rule, String assignmentId, String targetingKey) {} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagLogger.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagLogger.java index eb8c0d2d..447feca2 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagLogger.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/FlagLogger.java @@ -54,6 +54,7 @@ static FlagAssigned createFlagAssigned( return builder.build(); } + @SuppressWarnings("deprecation") private static DefaultAssignmentReason resolveToAssignmentReason(ResolveReason reason) { return switch (reason) { case RESOLVE_REASON_NO_SEGMENT_MATCH -> DefaultAssignmentReason.NO_SEGMENT_MATCH; diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/IsClosedException.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/IsClosedException.java new file mode 100644 index 00000000..a432c930 --- /dev/null +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/IsClosedException.java @@ -0,0 +1,3 @@ +package com.spotify.confidence; + +class IsClosedException extends Exception {} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/MaterializationInfo.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/MaterializationInfo.java index aa8d4b10..09793cbb 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/MaterializationInfo.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/MaterializationInfo.java @@ -1,17 +1,9 @@ package com.spotify.confidence; import java.util.Map; -import java.util.Optional; public record MaterializationInfo( boolean isUnitInMaterialization, Map ruleToVariant) { - public Optional getVariantForRule(String rule) { - return Optional.ofNullable(ruleToVariant.get(rule)); - } - - public static MaterializationInfo empty() { - return new MaterializationInfo(false, Map.of()); - } public com.spotify.confidence.flags.resolver.v1.MaterializationInfo toProto() { return com.spotify.confidence.flags.resolver.v1.MaterializationInfo.newBuilder() diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java deleted file mode 100644 index 425e9861..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.spotify.confidence; - -import com.spotify.confidence.shaded.flags.types.v1.Targeting; - -class Targetings { - - static Targeting.Value boolValue(final boolean value) { - return Targeting.Value.newBuilder().setBoolValue(value).build(); - } - - static Targeting.Value numberValue(final double value) { - return Targeting.Value.newBuilder().setNumberValue(value).build(); - } - - static Targeting.Value stringValue(final String value) { - return Targeting.Value.newBuilder().setStringValue(value).build(); - } - - static Targeting.Value semverValue(final String value) { - return Targeting.Value.newBuilder() - .setVersionValue(Targeting.SemanticVersion.newBuilder().setVersion(value).build()) - .build(); - } -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ThreadLocalSwapWasmResolverApi.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ThreadLocalSwapWasmResolverApi.java index d707f1d0..862562b6 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ThreadLocalSwapWasmResolverApi.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/ThreadLocalSwapWasmResolverApi.java @@ -109,14 +109,6 @@ public ResolveFlagsResponse resolve(ResolveFlagsRequest request) { return getResolverForCurrentThread().resolve(request); } - /** - * Returns the number of pre-initialized resolver instances. This is primarily for debugging and - * monitoring purposes. - */ - public int getInstanceCount() { - return resolverInstances.size(); - } - /** Closes all pre-initialized resolver instances and clears the map. */ @Override public void close() { diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmFlagLogger.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmFlagLogger.java new file mode 100644 index 00000000..dcff465b --- /dev/null +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmFlagLogger.java @@ -0,0 +1,9 @@ +package com.spotify.confidence; + +import com.spotify.confidence.shaded.flags.resolver.v1.WriteFlagLogsRequest; + +interface WasmFlagLogger { + void write(WriteFlagLogsRequest request); + + void shutdown(); +} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmResolveApi.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmResolveApi.java index 30b3e47a..ae72889b 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmResolveApi.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmResolveApi.java @@ -29,14 +29,6 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; -class IsClosedException extends Exception {} - -interface WasmFlagLogger { - void write(WriteFlagLogsRequest request); - - void shutdown(); -} - class WasmResolveApi { private final FunctionType HOST_FN_TYPE = FunctionType.of(List.of(ValType.I32), List.of(ValType.I32)); From 284376138fcb28f5db256d5937efd5a662b998a0 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 13 Oct 2025 15:51:22 +0200 Subject: [PATCH 8/8] test: WIP Fixing tests --- openfeature-provider-local/pom.xml | 4 --- .../spotify/confidence/JavaResolveTest.java | 7 ---- .../com/spotify/confidence/ResolveTest.java | 23 +++++-------- .../java/com/spotify/confidence/TestBase.java | 32 +++++-------------- .../spotify/confidence/WasmResolveTest.java | 1 - pom.xml | 5 --- 6 files changed, 16 insertions(+), 56 deletions(-) delete mode 100644 openfeature-provider-local/src/test/java/com/spotify/confidence/JavaResolveTest.java diff --git a/openfeature-provider-local/pom.xml b/openfeature-provider-local/pom.xml index ee44a9fc..88ddba74 100644 --- a/openfeature-provider-local/pom.xml +++ b/openfeature-provider-local/pom.xml @@ -56,10 +56,6 @@ io.grpc grpc-api - - com.fasterxml.jackson.core - jackson-databind - com.google.protobuf protobuf-java diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/JavaResolveTest.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/JavaResolveTest.java deleted file mode 100644 index 1f99652d..00000000 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/JavaResolveTest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.spotify.confidence; - -public class JavaResolveTest extends ResolveTest { - public JavaResolveTest() { - super(false); - } -} diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java index 1c4058be..ce4a8de2 100644 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java +++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java @@ -179,7 +179,6 @@ public void testInvalidSecret() { resolveWithContext( List.of("flags/asd"), "foo", - "bar", Struct.newBuilder().build(), true, "invalid-secret")) @@ -189,7 +188,7 @@ public void testInvalidSecret() { @Test public void testInvalidFlag() { final var response = - resolveWithContext(List.of("flags/asd"), "foo", "bar", Struct.newBuilder().build(), false); + resolveWithContext(List.of("flags/asd"), "foo", Struct.newBuilder().build(), false); assertThat(response.getResolvedFlagsList()).isEmpty(); assertThat(response.getResolveId()).isNotEmpty(); } @@ -197,11 +196,9 @@ public void testInvalidFlag() { @Test public void testResolveFlag() { final var response = - resolveWithContext(List.of(flag1), "foo", "bar", Struct.newBuilder().build(), true); + resolveWithContext(List.of(flag1), "foo", Struct.newBuilder().build(), true); assertThat(response.getResolveId()).isNotEmpty(); - final Struct expectedValue = - // expanded with nulls to match schema - variantOn.getValue().toBuilder().putFields("extra", Values.ofNull()).build(); + final Struct expectedValue = variantOn.getValue(); assertEquals(variantOn.getName(), response.getResolvedFlags(0).getVariant()); assertEquals(expectedValue, response.getResolvedFlags(0).getValue()); @@ -211,11 +208,9 @@ public void testResolveFlag() { @Test public void testResolveFlagWithEncryptedResolveToken() { final var response = - resolveWithContext(List.of(flag1), "foo", "bar", Struct.newBuilder().build(), false); + resolveWithContext(List.of(flag1), "foo", Struct.newBuilder().build(), false); assertThat(response.getResolveId()).isNotEmpty(); - final Struct expectedValue = - // expanded with nulls to match schema - variantOn.getValue().toBuilder().putFields("extra", Values.ofNull()).build(); + final Struct expectedValue = variantOn.getValue(); assertEquals(variantOn.getName(), response.getResolvedFlags(0).getVariant()); assertEquals(expectedValue, response.getResolvedFlags(0).getValue()); @@ -255,15 +250,14 @@ public void testTooLongKey() { .isThrownBy( () -> resolveWithContext( - List.of(flag1), "a".repeat(101), "bar", Struct.newBuilder().build(), false)) + List.of(flag1), "a".repeat(101), Struct.newBuilder().build(), false)) .withMessageContaining("Targeting key is too larger, max 100 characters."); } @Test public void testResolveIntegerTargetingKeyTyped() { final var response = - resolveWithNumericTargetingKey( - List.of(flag1), 1234567890, "bar", Struct.newBuilder().build(), true); + resolveWithNumericTargetingKey(List.of(flag1), 1234567890, Struct.newBuilder().build()); assertThat(response.getResolvedFlagsList()).hasSize(1); assertEquals(ResolveReason.RESOLVE_REASON_MATCH, response.getResolvedFlags(0).getReason()); @@ -272,8 +266,7 @@ public void testResolveIntegerTargetingKeyTyped() { @Test public void testResolveDecimalUsername() { final var response = - resolveWithNumericTargetingKey( - List.of(flag1), 3.14159d, "bar", Struct.newBuilder().build(), true); + resolveWithNumericTargetingKey(List.of(flag1), 3.14159d, Struct.newBuilder().build()); assertThat(response.getResolvedFlagsList()).hasSize(1); assertEquals( diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java index 4fb64751..66f7a093 100644 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java +++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java @@ -58,12 +58,7 @@ protected static void setup() {} protected void setUp() {} protected ResolveFlagsResponse resolveWithContext( - List flags, - String username, - String structFieldName, - Struct struct, - boolean apply, - String secret) { + List flags, String username, Struct struct, boolean apply, String secret) { try { return resolverServiceFactory .create(secret) @@ -72,8 +67,7 @@ protected ResolveFlagsResponse resolveWithContext( .addAllFlags(flags) .setClientSecret(secret) .setEvaluationContext( - Structs.of( - "targeting_key", Values.of(username), structFieldName, Values.of(struct))) + Structs.of("targeting_key", Values.of(username), "bar", Values.of(struct))) .setApply(apply) .build()) .get(); @@ -83,32 +77,22 @@ protected ResolveFlagsResponse resolveWithContext( } protected ResolveFlagsResponse resolveWithNumericTargetingKey( - List flags, - Number targetingKey, - String structFieldName, - Struct struct, - boolean apply) { + List flags, Number targetingKey, Struct struct) { try { final var builder = ResolveFlagsRequest.newBuilder() .addAllFlags(flags) .setClientSecret(secret.getSecret()) - .setApply(apply); + .setApply(true); if (targetingKey instanceof Double || targetingKey instanceof Float) { builder.setEvaluationContext( Structs.of( - "targeting_key", - Values.of(targetingKey.doubleValue()), - structFieldName, - Values.of(struct))); + "targeting_key", Values.of(targetingKey.doubleValue()), "bar", Values.of(struct))); } else { builder.setEvaluationContext( Structs.of( - "targeting_key", - Values.of(targetingKey.longValue()), - structFieldName, - Values.of(struct))); + "targeting_key", Values.of(targetingKey.longValue()), "bar", Values.of(struct))); } return resolverServiceFactory.create(secret.getSecret()).resolveFlags(builder.build()).get(); @@ -118,7 +102,7 @@ protected ResolveFlagsResponse resolveWithNumericTargetingKey( } protected ResolveFlagsResponse resolveWithContext( - List flags, String username, String structFieldName, Struct struct, boolean apply) { - return resolveWithContext(flags, username, structFieldName, struct, apply, secret.getSecret()); + List flags, String username, Struct struct, boolean apply) { + return resolveWithContext(flags, username, struct, apply, secret.getSecret()); } } diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java index 557cd6ab..4783caca 100644 --- a/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java +++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java @@ -65,7 +65,6 @@ public void close() {} assertTrue(objectEvaluation.getValue().isStructure()); final var structure = objectEvaluation.getValue().asStructure(); assertEquals("on", structure.getValue("data").asString()); - assertTrue(structure.getValue("extra").isNull()); } @Test diff --git a/pom.xml b/pom.xml index ad39bea3..d6cba637 100644 --- a/pom.xml +++ b/pom.xml @@ -198,11 +198,6 @@ gson 2.11.0 - - com.fasterxml.jackson.core - jackson-databind - 2.15.4 -