diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ExponentialRetryStrategy.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ExponentialRetryStrategy.java deleted file mode 100644 index e7fa75d4..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ExponentialRetryStrategy.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.spotify.confidence; - -import com.dylibso.chicory.wasm.ChicoryException; -import java.util.function.Supplier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A retry strategy that retries operations with exponential backoff for Chicory WASM runtime - * exceptions. - */ -class ExponentialRetryStrategy implements RetryStrategy { - private static final Logger logger = LoggerFactory.getLogger(ExponentialRetryStrategy.class); - - private final int maxRetries; - private final long initialBackoffMs; - private final double backoffMultiplier; - - /** - * Creates an exponential retry strategy with the specified configuration. - * - * @param maxRetries Maximum number of retries (not including the initial attempt) - * @param initialBackoffMs Initial backoff delay in milliseconds - * @param backoffMultiplier Multiplier for exponential backoff - */ - public ExponentialRetryStrategy(int maxRetries, long initialBackoffMs, double backoffMultiplier) { - this.maxRetries = maxRetries; - this.initialBackoffMs = initialBackoffMs; - this.backoffMultiplier = backoffMultiplier; - } - - /** - * Creates an exponential retry strategy with default configuration (3 retries, 100ms initial). - */ - public ExponentialRetryStrategy() { - this(3, 100, 2.0); - } - - @Override - public T execute(Supplier operation, String operationName) { - RuntimeException lastException = null; - long backoffMs = initialBackoffMs; - - for (int attempt = 0; attempt <= maxRetries; attempt++) { - try { - return operation.get(); - } catch (ChicoryException e) { - logger.warn("{} attempt {} failed: {}", operationName, attempt + 1, e.getMessage()); - lastException = e; - if (attempt < maxRetries) { - try { - Thread.sleep(backoffMs); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new RuntimeException(operationName + " interrupted during retry backoff", ie); - } - backoffMs = (long) (backoffMs * backoffMultiplier); - } - } - } - - throw new RuntimeException( - operationName + " failed after " + (maxRetries + 1) + " attempts", lastException); - } -} 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 586fe99a..b0315710 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 @@ -2,7 +2,6 @@ import com.spotify.confidence.shaded.flags.resolver.v1.InternalFlagLoggerServiceGrpc; import com.spotify.confidence.shaded.flags.resolver.v1.WriteFlagLogsRequest; -import com.spotify.confidence.shaded.flags.resolver.v1.WriteFlagLogsResponse; import com.spotify.confidence.shaded.iam.v1.AuthServiceGrpc; import io.grpc.Channel; import io.grpc.ClientInterceptors; @@ -16,7 +15,7 @@ public class GrpcWasmFlagLogger implements WasmFlagLogger { private static final String CONFIDENCE_DOMAIN = "edge-grpc.spotify.com"; private static final Logger logger = LoggerFactory.getLogger(GrpcWasmFlagLogger.class); - private final InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceBlockingStub stub; + private final InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceFutureStub stub; public GrpcWasmFlagLogger(ApiSecret apiSecret) { final var channel = createConfidenceChannel(); @@ -27,12 +26,12 @@ public GrpcWasmFlagLogger(ApiSecret apiSecret) { final TokenHolder.Token token = tokenHolder.getToken(); final Channel authenticatedChannel = ClientInterceptors.intercept(channel, new JwtAuthClientInterceptor(tokenHolder)); - this.stub = InternalFlagLoggerServiceGrpc.newBlockingStub(authenticatedChannel); + this.stub = InternalFlagLoggerServiceGrpc.newFutureStub(authenticatedChannel); } @Override - public WriteFlagLogsResponse write(WriteFlagLogsRequest request) { - return stub.writeFlagLogs(request); + public void write(WriteFlagLogsRequest request) { + final var ignore = stub.writeFlagLogs(request); } private static ManagedChannel createConfidenceChannel() { 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 8db8eda7..802507ce 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,6 +42,7 @@ class LocalResolverServiceFactory implements ResolverServiceFactory { 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); @@ -67,27 +68,22 @@ static FlagResolverService from( ApiSecret apiSecret, String clientSecret, boolean isWasm, - StickyResolveStrategy stickyResolveStrategy, - RetryStrategy retryStrategy) { - return createFlagResolverService( - apiSecret, clientSecret, isWasm, stickyResolveStrategy, retryStrategy); + StickyResolveStrategy stickyResolveStrategy) { + return createFlagResolverService(apiSecret, clientSecret, isWasm, stickyResolveStrategy); } static FlagResolverService from( AccountStateProvider accountStateProvider, String accountId, - StickyResolveStrategy stickyResolveStrategy, - RetryStrategy retryStrategy) { - return createFlagResolverService( - accountStateProvider, accountId, stickyResolveStrategy, retryStrategy); + StickyResolveStrategy stickyResolveStrategy) { + return createFlagResolverService(accountStateProvider, accountId, stickyResolveStrategy); } private static FlagResolverService createFlagResolverService( ApiSecret apiSecret, String clientSecret, boolean isWasm, - StickyResolveStrategy stickyResolveStrategy, - RetryStrategy retryStrategy) { + StickyResolveStrategy stickyResolveStrategy) { final var channel = createConfidenceChannel(); final AuthServiceBlockingStub authService = AuthServiceGrpc.newBlockingStub(channel); final TokenHolder tokenHolder = @@ -116,8 +112,7 @@ private static FlagResolverService createFlagResolverService( wasmFlagLogger, sidecarFlagsAdminFetcher.rawStateHolder().get().toByteArray(), sidecarFlagsAdminFetcher.accountId, - stickyResolveStrategy, - retryStrategy); + stickyResolveStrategy); flagsFetcherExecutor.scheduleAtFixedRate( sidecarFlagsAdminFetcher::reload, pollIntervalSeconds, @@ -130,8 +125,8 @@ private static FlagResolverService createFlagResolverService( sidecarFlagsAdminFetcher.rawStateHolder().get().toByteArray(), sidecarFlagsAdminFetcher.accountId); }, - 10, - 10, + POLL_LOG_INTERVAL.getSeconds(), + POLL_LOG_INTERVAL.getSeconds(), TimeUnit.SECONDS); return new WasmFlagResolverService(wasmResolverApi, stickyResolveStrategy); @@ -173,8 +168,7 @@ private static boolean getFailFast(StickyResolveStrategy stickyResolveStrategy) private static FlagResolverService createFlagResolverService( AccountStateProvider accountStateProvider, String accountId, - StickyResolveStrategy stickyResolveStrategy, - RetryStrategy retryStrategy) { + StickyResolveStrategy stickyResolveStrategy) { final var mode = System.getenv("LOCAL_RESOLVE_MODE"); if (!(mode == null || mode.equals("WASM"))) { throw new RuntimeException("Only WASM mode supported with AccountStateProvider"); @@ -183,19 +177,22 @@ private static FlagResolverService createFlagResolverService( Optional.ofNullable(System.getenv("CONFIDENCE_RESOLVER_POLL_INTERVAL_SECONDS")) .map(Long::parseLong) .orElse(Duration.ofMinutes(5).toSeconds()); - final byte[] resolverStateProtobuf = accountStateProvider.provide(); + final AtomicReference resolverStateProtobuf = + new AtomicReference<>(accountStateProvider.provide()); final WasmFlagLogger flagLogger = request -> WriteFlagLogsResponse.getDefaultInstance(); final ResolverApi wasmResolverApi = new ThreadLocalSwapWasmResolverApi( - flagLogger, resolverStateProtobuf, accountId, stickyResolveStrategy, retryStrategy); + flagLogger, resolverStateProtobuf.get(), accountId, stickyResolveStrategy); flagsFetcherExecutor.scheduleAtFixedRate( - () -> { - wasmResolverApi.updateStateAndFlushLogs(accountStateProvider.provide(), accountId); - }, + () -> resolverStateProtobuf.set(accountStateProvider.provide()), pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS); - + logPollExecutor.scheduleAtFixedRate( + () -> wasmResolverApi.updateStateAndFlushLogs(resolverStateProtobuf.get(), accountId), + POLL_LOG_INTERVAL.getSeconds(), + POLL_LOG_INTERVAL.getSeconds(), + TimeUnit.SECONDS); return new WasmFlagResolverService(wasmResolverApi, stickyResolveStrategy); } diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/NoRetryStrategy.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/NoRetryStrategy.java deleted file mode 100644 index de893cb4..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/NoRetryStrategy.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.spotify.confidence; - -import java.util.function.Supplier; - -/** A retry strategy that doesn't retry - executes the operation once and returns or throws. */ -class NoRetryStrategy implements RetryStrategy { - @Override - public T execute(Supplier operation, String operationName) { - return operation.get(); - } -} 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 e5318123..098345c9 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 @@ -90,28 +90,7 @@ public class OpenFeatureLocalResolveProvider implements FeatureProvider { * @since 0.2.4 */ public OpenFeatureLocalResolveProvider(ApiSecret apiSecret, String clientSecret) { - this(apiSecret, clientSecret, new RemoteResolverFallback(), new NoRetryStrategy()); - } - - /** - * Creates a new OpenFeature provider for local flag resolution with configurable sticky resolve - * strategy and no retry. - * - *

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")} - * @param clientSecret the client secret for your application, used for flag resolution - * authentication. This is different from the API secret and is specific to your application - * configuration - * @param stickyResolveStrategy the strategy to use for handling sticky flag resolution - * @since 0.2.4 - */ - public OpenFeatureLocalResolveProvider( - ApiSecret apiSecret, String clientSecret, StickyResolveStrategy stickyResolveStrategy) { - this(apiSecret, clientSecret, stickyResolveStrategy, new NoRetryStrategy()); + this(apiSecret, clientSecret, new RemoteResolverFallback()); } /** @@ -128,56 +107,25 @@ public OpenFeatureLocalResolveProvider( * authentication. This is different from the API secret and is specific to your application * configuration * @param stickyResolveStrategy the strategy to use for handling sticky flag resolution - * @param retryStrategy the retry strategy for WASM operations (use {@link NoRetryStrategy} to - * disable retries or {@link ExponentialRetryStrategy} for exponential backoff) * @since 0.2.4 */ public OpenFeatureLocalResolveProvider( - ApiSecret apiSecret, - String clientSecret, - StickyResolveStrategy stickyResolveStrategy, - RetryStrategy retryStrategy) { + 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, retryStrategy); + LocalResolverServiceFactory.from(apiSecret, clientSecret, true, stickyResolveStrategy); } else if (env != null && env.equals("JAVA")) { this.flagResolverService = - LocalResolverServiceFactory.from( - apiSecret, clientSecret, false, stickyResolveStrategy, retryStrategy); + LocalResolverServiceFactory.from(apiSecret, clientSecret, false, stickyResolveStrategy); } else { this.flagResolverService = - LocalResolverServiceFactory.from( - apiSecret, clientSecret, true, stickyResolveStrategy, retryStrategy); + LocalResolverServiceFactory.from(apiSecret, clientSecret, true, stickyResolveStrategy); } this.stickyResolveStrategy = stickyResolveStrategy; this.clientSecret = clientSecret; } - /** - * To be used for testing purposes only! This constructor allows to inject flags state for testing - * the WASM resolver (no Java supported) No resolve/assign logging is forwarded to production No - * need to supply ApiSecret - * - * @param accountStateProvider a functional interface that provides AccountState instances - * @param clientSecret the flag client key used to filter the flags - * @since 0.2.4 - */ - @VisibleForTesting - public OpenFeatureLocalResolveProvider( - AccountStateProvider accountStateProvider, - String accountId, - String clientSecret, - StickyResolveStrategy stickyResolveStrategy) { - this( - accountStateProvider, - accountId, - clientSecret, - stickyResolveStrategy, - new NoRetryStrategy()); - } - /** * To be used for testing purposes only! This constructor allows to inject flags state for testing * the WASM resolver with full control over retry strategy. @@ -186,7 +134,6 @@ public OpenFeatureLocalResolveProvider( * @param accountId the account ID * @param clientSecret the flag client key used to filter the flags * @param stickyResolveStrategy the strategy to use for handling sticky flag resolution - * @param retryStrategy the retry strategy for WASM operations * @since 0.2.4 */ @VisibleForTesting @@ -194,13 +141,11 @@ public OpenFeatureLocalResolveProvider( AccountStateProvider accountStateProvider, String accountId, String clientSecret, - StickyResolveStrategy stickyResolveStrategy, - RetryStrategy retryStrategy) { + StickyResolveStrategy stickyResolveStrategy) { this.stickyResolveStrategy = stickyResolveStrategy; this.clientSecret = clientSecret; this.flagResolverService = - LocalResolverServiceFactory.from( - accountStateProvider, accountId, stickyResolveStrategy, retryStrategy); + LocalResolverServiceFactory.from(accountStateProvider, accountId, stickyResolveStrategy); } @Override diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/RetryStrategy.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/RetryStrategy.java deleted file mode 100644 index 7cca80db..00000000 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/RetryStrategy.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.spotify.confidence; - -import java.util.function.Supplier; - -/** Strategy for retrying operations that fail with transient errors. */ -interface RetryStrategy { - /** - * Executes an operation with the configured retry strategy. - * - * @param operation The operation to execute - * @param operationName Name of the operation for error messages - * @param Return type of the operation - * @return The result of the operation - * @throws RuntimeException if the operation fails - */ - T execute(Supplier operation, String operationName); -} diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/SwapWasmResolverApi.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/SwapWasmResolverApi.java index cb2b4ed0..51ae0876 100644 --- a/openfeature-provider-local/src/main/java/com/spotify/confidence/SwapWasmResolverApi.java +++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/SwapWasmResolverApi.java @@ -11,71 +11,53 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; class SwapWasmResolverApi implements ResolverApi { private final AtomicReference wasmResolverApiRef = new AtomicReference<>(); private final StickyResolveStrategy stickyResolveStrategy; private final WasmFlagLogger flagLogger; - private final RetryStrategy retryStrategy; public SwapWasmResolverApi( WasmFlagLogger flagLogger, byte[] initialState, String accountId, - StickyResolveStrategy stickyResolveStrategy, - RetryStrategy retryStrategy) { + StickyResolveStrategy stickyResolveStrategy) { this.stickyResolveStrategy = stickyResolveStrategy; this.flagLogger = flagLogger; - this.retryStrategy = retryStrategy; // Create initial instance - final WasmResolveApi initialInstance = new WasmResolveApi(flagLogger, retryStrategy); + final WasmResolveApi initialInstance = new WasmResolveApi(flagLogger); initialInstance.setResolverState(initialState, accountId); this.wasmResolverApiRef.set(initialInstance); } @Override public void updateStateAndFlushLogs(byte[] state, String accountId) { - logResolveLock.lock(); // Create new instance with updated state - final WasmResolveApi newInstance = new WasmResolveApi(flagLogger, retryStrategy); + final WasmResolveApi newInstance = new WasmResolveApi(flagLogger); newInstance.setResolverState(state, accountId); // Get current instance before switching final WasmResolveApi oldInstance = wasmResolverApiRef.getAndSet(newInstance); - - // Flush logs from the old instance (this allows it to be GC'd after method completion) if (oldInstance != null) { - oldInstance.flushLogs(); + oldInstance.close(); } - logResolveLock.unlock(); } @Override - public void close() { - final WasmResolveApi currentInstance = wasmResolverApiRef.get(); - if (currentInstance != null) { - // Note: WasmResolveApi doesn't have a close method, so this is a no-op for now - // but the instance will be GC'd naturally - } - } - - private final ReentrantLock logResolveLock = new ReentrantLock(); + public void close() {} @Override public CompletableFuture resolveWithSticky( ResolveWithStickyRequest request) { - logResolveLock.lock(); - final var response = resolveWithStickyInternal(request); - logResolveLock.unlock(); - return response; - } - - private CompletableFuture resolveWithStickyInternal( - ResolveWithStickyRequest request) { - final var response = wasmResolverApiRef.get().resolveWithSticky(request); + final var instance = wasmResolverApiRef.get(); + final ResolveWithStickyResponse response; + try { + response = instance.resolveWithSticky(request); + } catch (IsClosedException e) { + return resolveWithSticky(request); + } switch (response.getResolveResultCase()) { case SUCCESS -> { @@ -99,7 +81,7 @@ private CompletableFuture resolveWithStickyInternal( final var currentRequest = handleMissingMaterializations( request, missingMaterializations.getItemsList(), repository); - return resolveWithStickyInternal(currentRequest); + return resolveWithSticky(currentRequest); } throw new RuntimeException( @@ -204,9 +186,11 @@ private ResolveWithStickyRequest handleMissingMaterializations( @Override public ResolveFlagsResponse resolve(ResolveFlagsRequest request) { - logResolveLock.lock(); - final var response = wasmResolverApiRef.get().resolve(request); - logResolveLock.unlock(); - return response; + final var instance = wasmResolverApiRef.get(); + try { + return instance.resolve(request); + } catch (IsClosedException e) { + return resolve(request); + } } } 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 dd00a3ea..dd10bbf7 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 @@ -3,6 +3,8 @@ import com.spotify.confidence.flags.resolver.v1.ResolveWithStickyRequest; import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsRequest; import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse; +import com.spotify.futures.CompletableFutures; +import java.util.ArrayList; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -20,7 +22,6 @@ class ThreadLocalSwapWasmResolverApi implements ResolverApi { LoggerFactory.getLogger(ThreadLocalSwapWasmResolverApi.class); private final WasmFlagLogger flagLogger; private final StickyResolveStrategy stickyResolveStrategy; - private final RetryStrategy retryStrategy; private volatile byte[] currentState; private volatile String currentAccountId; @@ -40,11 +41,9 @@ public ThreadLocalSwapWasmResolverApi( WasmFlagLogger flagLogger, byte[] initialState, String accountId, - StickyResolveStrategy stickyResolveStrategy, - RetryStrategy retryStrategy) { + StickyResolveStrategy stickyResolveStrategy) { this.flagLogger = flagLogger; this.stickyResolveStrategy = stickyResolveStrategy; - this.retryStrategy = retryStrategy; this.currentState = initialState; this.currentAccountId = accountId; @@ -52,18 +51,23 @@ public ThreadLocalSwapWasmResolverApi( this.numInstances = Runtime.getRuntime().availableProcessors(); logger.info( "Initialized ThreadLocalSwapWasmResolverApi with {} available processors", numInstances); + final var futures = new ArrayList>(numInstances); + IntStream.range(0, numInstances) .forEach( - i -> { - final var instance = - new SwapWasmResolverApi( - this.flagLogger, - this.currentState, - this.currentAccountId, - this.stickyResolveStrategy, - this.retryStrategy); - resolverInstances.put(i, instance); - }); + i -> + futures.add( + CompletableFuture.runAsync( + () -> { + final var instance = + new SwapWasmResolverApi( + this.flagLogger, + this.currentState, + this.currentAccountId, + this.stickyResolveStrategy); + resolverInstances.put(i, instance); + }))); + CompletableFutures.allAsList(futures).join(); } /** @@ -75,10 +79,11 @@ public void updateStateAndFlushLogs(byte[] state, String accountId) { this.currentState = state; this.currentAccountId = accountId; - // Update all pre-initialized resolver instances - resolverInstances - .values() - .forEach(resolver -> resolver.updateStateAndFlushLogs(state, accountId)); + final var futures = + resolverInstances.values().stream() + .map(v -> CompletableFuture.runAsync(() -> v.updateStateAndFlushLogs(state, accountId))) + .toList(); + CompletableFutures.allAsList(futures).join(); } /** 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 de262764..29686bac 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 @@ -20,17 +20,20 @@ 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.WriteFlagLogsRequest; -import com.spotify.confidence.shaded.flags.resolver.v1.WriteFlagLogsResponse; import com.spotify.confidence.wasm.Messages; import java.io.IOException; import java.io.InputStream; import java.time.Instant; import java.util.List; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; +class IsClosedException extends Exception {} + @FunctionalInterface interface WasmFlagLogger { - WriteFlagLogsResponse write(WriteFlagLogsRequest request); + void write(WriteFlagLogsRequest request); } class WasmResolveApi { @@ -48,12 +51,9 @@ class WasmResolveApi { private final ExportFunction wasmMsgFlushLogs; private final ExportFunction wasmMsgGuestResolve; private final ExportFunction wasmMsgGuestResolveWithSticky; + private final ReadWriteLock wasmLock = new ReentrantReadWriteLock(); - // Retry strategy - private final RetryStrategy retryStrategy; - - public WasmResolveApi(WasmFlagLogger flagLogger, RetryStrategy retryStrategy) { - this.retryStrategy = retryStrategy; + public WasmResolveApi(WasmFlagLogger flagLogger) { this.writeFlagLogs = flagLogger; try (InputStream wasmStream = getClass().getClassLoader().getResourceAsStream("wasm/confidence_resolver.wasm")) { @@ -119,32 +119,44 @@ public void setResolverState(byte[] state, String accountId) { consumeResponse(respPtr, Messages.Void::parseFrom); } - public void flushLogs() { - final var voidRequest = Messages.Void.getDefaultInstance(); - final var reqPtr = transferRequest(voidRequest); - final var respPtr = (int) wasmMsgFlushLogs.apply(reqPtr)[0]; - final var request = consumeResponse(respPtr, WriteFlagLogsRequest::parseFrom); - final var ignore = writeFlagLogs.write(request); - } - - public ResolveWithStickyResponse resolveWithSticky(ResolveWithStickyRequest request) { - return retryStrategy.execute( - () -> { - final int reqPtr = transferRequest(request); - final int respPtr = (int) wasmMsgGuestResolveWithSticky.apply(reqPtr)[0]; - return consumeResponse(respPtr, ResolveWithStickyResponse::parseFrom); - }, - "resolveWithSticky"); - } - - public ResolveFlagsResponse resolve(ResolveFlagsRequest request) { - return retryStrategy.execute( - () -> { - final int reqPtr = transferRequest(request); - final int respPtr = (int) wasmMsgGuestResolve.apply(reqPtr)[0]; - return consumeResponse(respPtr, ResolveFlagsResponse::parseFrom); - }, - "resolve"); + public void close() { + wasmLock.readLock().lock(); + try { + final var voidRequest = Messages.Void.getDefaultInstance(); + final var reqPtr = transferRequest(voidRequest); + final var respPtr = (int) wasmMsgFlushLogs.apply(reqPtr)[0]; + final var request = consumeResponse(respPtr, WriteFlagLogsRequest::parseFrom); + writeFlagLogs.write(request); + } finally { + wasmLock.readLock().unlock(); + } + } + + public ResolveWithStickyResponse resolveWithSticky(ResolveWithStickyRequest request) + throws IsClosedException { + if (!wasmLock.writeLock().tryLock()) { + throw new IsClosedException(); + } + try { + final int reqPtr = transferRequest(request); + final int respPtr = (int) wasmMsgGuestResolveWithSticky.apply(reqPtr)[0]; + return consumeResponse(respPtr, ResolveWithStickyResponse::parseFrom); + } finally { + wasmLock.writeLock().unlock(); + } + } + + public ResolveFlagsResponse resolve(ResolveFlagsRequest request) throws IsClosedException { + if (!wasmLock.writeLock().tryLock()) { + throw new IsClosedException(); + } + try { + final int reqPtr = transferRequest(request); + final int respPtr = (int) wasmMsgGuestResolve.apply(reqPtr)[0]; + return consumeResponse(respPtr, ResolveFlagsResponse::parseFrom); + } finally { + wasmLock.writeLock().unlock(); + } } private T consumeResponse(int addr, ParserFn codec) { 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 65c9e110..9687f72c 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 @@ -45,11 +45,7 @@ protected TestBase(ResolverState state, boolean isWasm) { if (isWasm) { final var wasmResolverApi = new SwapWasmResolverApi( - request -> null, - desiredState.toProto().toByteArray(), - "", - mockFallback, - new NoRetryStrategy()); + request -> {}, desiredState.toProto().toByteArray(), "", mockFallback); resolverServiceFactory = new LocalResolverServiceFactory( wasmResolverApi, resolverState, resolveTokenConverter, mock(), mockFallback); 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 41b4fc2e..78a6b622 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 @@ -153,11 +153,11 @@ public void testMaterializationRepositoryWhenMaterializationsMissing() { verify(mockRepository).loadMaterializedAssignmentsForUnit("test-user", "read-mat"); } - // add @Test and run it locally for load test + // add @Test and run it locally for load testing public void testConcurrentResolveLoadTest() throws InterruptedException { // Test configuration final int totalResolves = 1000_000; - final int numThreads = 8; + final int numThreads = 10; final int resolvesPerThread = totalResolves / numThreads; // Create the provider using normal exampleState (not with materialization)