diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java
index 4c3fba4c1..027d1d75d 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java
@@ -15,6 +15,7 @@ public final class Config {
static final int DEFAULT_DEADLINE = 500;
static final int DEFAULT_STREAM_DEADLINE_MS = 10 * 60 * 1000;
+ static final int DEFAULT_STREAM_RETRY_GRACE_PERIOD = 5;
static final int DEFAULT_MAX_CACHE_SIZE = 1000;
static final long DEFAULT_KEEP_ALIVE = 0;
@@ -35,6 +36,7 @@ public final class Config {
static final String KEEP_ALIVE_MS_ENV_VAR_NAME_OLD = "FLAGD_KEEP_ALIVE_TIME";
static final String KEEP_ALIVE_MS_ENV_VAR_NAME = "FLAGD_KEEP_ALIVE_TIME_MS";
static final String TARGET_URI_ENV_VAR_NAME = "FLAGD_TARGET_URI";
+ static final String STREAM_RETRY_GRACE_PERIOD = "FLAGD_RETRY_GRACE_PERIOD";
static final String RESOLVER_RPC = "rpc";
static final String RESOLVER_IN_PROCESS = "in-process";
@@ -52,7 +54,6 @@ public final class Config {
public static final String LRU_CACHE = CacheType.LRU.getValue();
static final String DEFAULT_CACHE = LRU_CACHE;
- static final int DEFAULT_MAX_EVENT_STREAM_RETRIES = 5;
static final int BASE_EVENT_STREAM_RETRY_BACKOFF_MS = 1000;
static String fallBackToEnvOrDefault(String key, String defaultValue) {
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java
index 4ba459e4d..c98effac2 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java
@@ -13,82 +13,111 @@
import lombok.Builder;
import lombok.Getter;
-/** FlagdOptions is a builder to build flagd provider options. */
+/**
+ * FlagdOptions is a builder to build flagd provider options.
+ */
@Builder
@Getter
@SuppressWarnings("PMD.TooManyStaticImports")
public class FlagdOptions {
- /** flagd resolving type. */
+ /**
+ * flagd resolving type.
+ */
private Config.EvaluatorType resolverType;
- /** flagd connection host. */
+ /**
+ * flagd connection host.
+ */
@Builder.Default
private String host = fallBackToEnvOrDefault(Config.HOST_ENV_VAR_NAME, Config.DEFAULT_HOST);
- /** flagd connection port. */
+ /**
+ * flagd connection port.
+ */
private int port;
- /** Use TLS connectivity. */
+ /**
+ * Use TLS connectivity.
+ */
@Builder.Default
private boolean tls = Boolean.parseBoolean(fallBackToEnvOrDefault(Config.TLS_ENV_VAR_NAME, Config.DEFAULT_TLS));
- /** TLS certificate overriding if TLS connectivity is used. */
+ /**
+ * TLS certificate overriding if TLS connectivity is used.
+ */
@Builder.Default
private String certPath = fallBackToEnvOrDefault(Config.SERVER_CERT_PATH_ENV_VAR_NAME, null);
- /** Unix socket path to flagd. */
+ /**
+ * Unix socket path to flagd.
+ */
@Builder.Default
private String socketPath = fallBackToEnvOrDefault(Config.SOCKET_PATH_ENV_VAR_NAME, null);
- /** Cache type to use. Supports - lru, disabled. */
+ /**
+ * Cache type to use. Supports - lru, disabled.
+ */
@Builder.Default
private String cacheType = fallBackToEnvOrDefault(Config.CACHE_ENV_VAR_NAME, Config.DEFAULT_CACHE);
- /** Max cache size. */
+ /**
+ * Max cache size.
+ */
@Builder.Default
private int maxCacheSize =
fallBackToEnvOrDefault(Config.MAX_CACHE_SIZE_ENV_VAR_NAME, Config.DEFAULT_MAX_CACHE_SIZE);
- /** Max event stream connection retries. */
- @Builder.Default
- private int maxEventStreamRetries = fallBackToEnvOrDefault(
- Config.MAX_EVENT_STREAM_RETRIES_ENV_VAR_NAME, Config.DEFAULT_MAX_EVENT_STREAM_RETRIES);
-
- /** Backoff interval in milliseconds. */
+ /**
+ * Backoff interval in milliseconds.
+ */
@Builder.Default
private int retryBackoffMs = fallBackToEnvOrDefault(
Config.BASE_EVENT_STREAM_RETRY_BACKOFF_MS_ENV_VAR_NAME, Config.BASE_EVENT_STREAM_RETRY_BACKOFF_MS);
/**
- * Connection deadline in milliseconds. For RPC resolving, this is the deadline to connect to
- * flagd for flag evaluation. For in-process resolving, this is the deadline for sync stream
- * termination.
+ * Connection deadline in milliseconds.
+ * For RPC resolving, this is the deadline to connect to flagd for flag
+ * evaluation.
+ * For in-process resolving, this is the deadline for sync stream termination.
*/
@Builder.Default
private int deadline = fallBackToEnvOrDefault(Config.DEADLINE_MS_ENV_VAR_NAME, Config.DEFAULT_DEADLINE);
/**
- * Streaming connection deadline in milliseconds. Set to 0 to disable the deadline. Defaults to
- * 600000 (10 minutes); recommended to prevent infrastructure from killing idle connections.
+ * Streaming connection deadline in milliseconds.
+ * Set to 0 to disable the deadline.
+ * Defaults to 600000 (10 minutes); recommended to prevent infrastructure from killing idle connections.
*/
@Builder.Default
private int streamDeadlineMs =
fallBackToEnvOrDefault(Config.STREAM_DEADLINE_MS_ENV_VAR_NAME, Config.DEFAULT_STREAM_DEADLINE_MS);
- /** Selector to be used with flag sync gRPC contract. */
+ /**
+ * Grace time period in seconds before provider moves from STALE to ERROR.
+ * Defaults to 5
+ */
+ @Builder.Default
+ private int retryGracePeriod =
+ fallBackToEnvOrDefault(Config.STREAM_RETRY_GRACE_PERIOD, Config.DEFAULT_STREAM_RETRY_GRACE_PERIOD);
+ /**
+ * Selector to be used with flag sync gRPC contract.
+ **/
@Builder.Default
private String selector = fallBackToEnvOrDefault(Config.SOURCE_SELECTOR_ENV_VAR_NAME, null);
- /** gRPC client KeepAlive in milliseconds. Disabled with 0. Defaults to 0 (disabled). */
+ /**
+ * gRPC client KeepAlive in milliseconds. Disabled with 0.
+ * Defaults to 0 (disabled).
+ **/
@Builder.Default
private long keepAlive = fallBackToEnvOrDefault(
Config.KEEP_ALIVE_MS_ENV_VAR_NAME,
fallBackToEnvOrDefault(Config.KEEP_ALIVE_MS_ENV_VAR_NAME_OLD, Config.DEFAULT_KEEP_ALIVE));
/**
- * File source of flags to be used by offline mode. Setting this enables the offline mode of the
- * in-process provider.
+ * File source of flags to be used by offline mode.
+ * Setting this enables the offline mode of the in-process provider.
*/
@Builder.Default
private String offlineFlagSourcePath = fallBackToEnvOrDefault(Config.OFFLINE_SOURCE_PATH, null);
@@ -96,31 +125,35 @@ public class FlagdOptions {
/**
* gRPC custom target string.
*
- *
Setting this will allow user to use custom gRPC name resolver at present we are supporting
- * all core resolver along with a custom resolver for envoy proxy resolution. For more visit
- * (https://grpc.io/docs/guides/custom-name-resolution/)
+ *
Setting this will allow user to use custom gRPC name resolver at present
+ * we are supporting all core resolver along with a custom resolver for envoy proxy
+ * resolution. For more visit (https://grpc.io/docs/guides/custom-name-resolution/)
*/
@Builder.Default
private String targetUri = fallBackToEnvOrDefault(Config.TARGET_URI_ENV_VAR_NAME, null);
/**
- * Function providing an EvaluationContext to mix into every evaluations. The sync-metadata
- * response
+ * Function providing an EvaluationContext to mix into every evaluations.
+ * The sync-metadata response
* (https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.GetMetadataResponse),
- * represented as a {@link dev.openfeature.sdk.Structure}, is passed as an argument. This function
- * runs every time the provider (re)connects, and its result is cached and used in every
- * evaluation. By default, the entire sync response (converted to a Structure) is used.
+ * represented as a {@link dev.openfeature.sdk.Structure}, is passed as an
+ * argument.
+ * This function runs every time the provider (re)connects, and its result is cached and used in every evaluation.
+ * By default, the entire sync response (converted to a Structure) is used.
*/
@Builder.Default
private Function contextEnricher =
(syncMetadata) -> new ImmutableContext(syncMetadata.asMap());
- /** Inject a Custom Connector for fetching flags. */
+ /**
+ * Inject a Custom Connector for fetching flags.
+ */
private Connector customConnector;
/**
- * Inject OpenTelemetry for the library runtime. Providing sdk will initiate distributed tracing
- * for flagd grpc connectivity.
+ * Inject OpenTelemetry for the library runtime. Providing sdk will initiate
+ * distributed tracing for flagd grpc
+ * connectivity.
*/
private OpenTelemetry openTelemetry;
@@ -139,11 +172,14 @@ public FlagdOptions build() {
};
}
- /** Overload default lombok builder. */
+ /**
+ * Overload default lombok builder.
+ */
public static class FlagdOptionsBuilder {
/**
- * Enable OpenTelemetry instance extraction from GlobalOpenTelemetry. Note that, this is only
- * useful if global configurations are registered.
+ * Enable OpenTelemetry instance extraction from GlobalOpenTelemetry. Note that,
+ * this is only useful if global
+ * configurations are registered.
*/
public FlagdOptionsBuilder withGlobalTelemetry(final boolean b) {
if (b) {
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
index 5f3d8a361..1e9c30882 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
@@ -21,7 +21,9 @@
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
-/** OpenFeature provider for flagd. */
+/**
+ * OpenFeature provider for flagd.
+ */
@Slf4j
@SuppressWarnings({"PMD.TooManyStaticImports", "checkstyle:NoFinalizer"})
public class FlagdProvider extends EventProvider {
@@ -38,7 +40,9 @@ protected final void finalize() {
// DO NOT REMOVE, spotbugs: CT_CONSTRUCTOR_THROW
}
- /** Create a new FlagdProvider instance with default options. */
+ /**
+ * Create a new FlagdProvider instance with default options.
+ */
public FlagdProvider() {
this(FlagdOptions.builder().build());
}
@@ -55,10 +59,7 @@ public FlagdProvider(final FlagdOptions options) {
break;
case Config.RESOLVER_RPC:
this.flagResolver = new GrpcResolver(
- options,
- new Cache(options.getCacheType(), options.getMaxCacheSize()),
- this::isConnected,
- this::onConnectionEvent);
+ options, new Cache(options.getCacheType(), options.getMaxCacheSize()), this::onConnectionEvent);
break;
default:
throw new IllegalStateException(
@@ -80,7 +81,7 @@ public synchronized void initialize(EvaluationContext evaluationContext) throws
}
this.flagResolver.init();
- this.initialized = true;
+ this.initialized = this.connected = true;
}
@Override
@@ -129,8 +130,10 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa
}
/**
- * An unmodifiable view of a Structure representing the latest result of the SyncMetadata. Set on
- * initial connection and updated with every reconnection. see:
+ * An unmodifiable view of a Structure representing the latest result of the
+ * SyncMetadata.
+ * Set on initial connection and updated with every reconnection.
+ * see:
* https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata
*
* @return Object map representing sync metadata
@@ -153,38 +156,42 @@ private boolean isConnected() {
}
private void onConnectionEvent(ConnectionEvent connectionEvent) {
- boolean previous = connected;
- boolean current = connected = connectionEvent.isConnected();
+ final boolean wasConnected = connected;
+ final boolean isConnected = connected = connectionEvent.isConnected();
+
syncMetadata = connectionEvent.getSyncMetadata();
enrichedContext = contextEnricher.apply(connectionEvent.getSyncMetadata());
- // configuration changed
- if (initialized && previous && current) {
- log.debug("Configuration changed");
- ProviderEventDetails details = ProviderEventDetails.builder()
- .flagsChanged(connectionEvent.getFlagsChanged())
- .message("configuration changed")
- .build();
- this.emitProviderConfigurationChanged(details);
+ if (!initialized) {
return;
}
- // there was an error
- if (initialized && previous && !current) {
- log.debug("There has been an error");
+
+ if (!wasConnected && isConnected) {
ProviderEventDetails details = ProviderEventDetails.builder()
- .message("there has been an error")
+ .flagsChanged(connectionEvent.getFlagsChanged())
+ .message("connected to flagd")
.build();
- this.emitProviderError(details);
+ this.emitProviderReady(details);
return;
}
- // we recovered from an error
- if (initialized && !previous && current) {
- log.debug("Recovered from error");
+
+ if (wasConnected && isConnected) {
ProviderEventDetails details = ProviderEventDetails.builder()
- .message("recovered from error")
+ .flagsChanged(connectionEvent.getFlagsChanged())
+ .message("configuration changed")
.build();
- this.emitProviderReady(details);
this.emitProviderConfigurationChanged(details);
+ return;
+ }
+
+ if (connectionEvent.isStale()) {
+ this.emitProviderStale(ProviderEventDetails.builder()
+ .message("there has been an error")
+ .build());
+ } else {
+ this.emitProviderError(ProviderEventDetails.builder()
+ .message("there has been an error")
+ .build());
}
}
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelMonitor.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelMonitor.java
new file mode 100644
index 000000000..0878ce910
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelMonitor.java
@@ -0,0 +1,98 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common;
+
+import dev.openfeature.sdk.exceptions.GeneralError;
+import io.grpc.ConnectivityState;
+import io.grpc.ManagedChannel;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * A utility class to monitor and manage the connectivity state of a gRPC ManagedChannel.
+ */
+@Slf4j
+public class ChannelMonitor {
+
+ private ChannelMonitor() {}
+
+ /**
+ * Monitors the state of a gRPC channel and triggers the specified callbacks based on state changes.
+ *
+ * @param expectedState the initial state to monitor.
+ * @param channel the ManagedChannel to monitor.
+ * @param onConnectionReady callback invoked when the channel transitions to a READY state.
+ * @param onConnectionLost callback invoked when the channel transitions to a FAILURE or SHUTDOWN state.
+ */
+ public static void monitorChannelState(
+ ConnectivityState expectedState,
+ ManagedChannel channel,
+ Runnable onConnectionReady,
+ Runnable onConnectionLost) {
+ channel.notifyWhenStateChanged(expectedState, () -> {
+ ConnectivityState currentState = channel.getState(true);
+ log.info("Channel state changed to: {}", currentState);
+ if (currentState == ConnectivityState.READY) {
+ onConnectionReady.run();
+ } else if (currentState == ConnectivityState.TRANSIENT_FAILURE
+ || currentState == ConnectivityState.SHUTDOWN) {
+ onConnectionLost.run();
+ }
+ // Re-register the state monitor to watch for the next state transition.
+ monitorChannelState(currentState, channel, onConnectionReady, onConnectionLost);
+ });
+ }
+
+ /**
+ * Waits for the channel to reach a desired state within a specified timeout period.
+ *
+ * @param channel the ManagedChannel to monitor.
+ * @param desiredState the ConnectivityState to wait for.
+ * @param connectCallback callback invoked when the desired state is reached.
+ * @param timeout the maximum amount of time to wait.
+ * @param unit the time unit of the timeout.
+ * @throws InterruptedException if the current thread is interrupted while waiting.
+ */
+ public static void waitForDesiredState(
+ ManagedChannel channel,
+ ConnectivityState desiredState,
+ Runnable connectCallback,
+ long timeout,
+ TimeUnit unit)
+ throws InterruptedException {
+ waitForDesiredState(channel, desiredState, connectCallback, new CountDownLatch(1), timeout, unit);
+ }
+
+ private static void waitForDesiredState(
+ ManagedChannel channel,
+ ConnectivityState desiredState,
+ Runnable connectCallback,
+ CountDownLatch latch,
+ long timeout,
+ TimeUnit unit)
+ throws InterruptedException {
+ channel.notifyWhenStateChanged(ConnectivityState.SHUTDOWN, () -> {
+ try {
+ ConnectivityState state = channel.getState(true);
+ log.debug("Channel state changed to: {}", state);
+
+ if (state == desiredState) {
+ connectCallback.run();
+ latch.countDown();
+ return;
+ }
+ waitForDesiredState(channel, desiredState, connectCallback, latch, timeout, unit);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ log.error("Thread interrupted while waiting for desired state", e);
+ } catch (Exception e) {
+ log.error("Error occurred while waiting for desired state", e);
+ }
+ });
+
+ // Await the latch or timeout for the state change
+ if (!latch.await(timeout, unit)) {
+ throw new GeneralError(String.format(
+ "Deadline exceeded. Condition did not complete within the %d " + "deadline", timeout));
+ }
+ }
+}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java
index 994ccdc9c..0e8ff4c6b 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java
@@ -4,65 +4,121 @@
import dev.openfeature.sdk.Structure;
import java.util.Collections;
import java.util.List;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
/**
- * Event payload for a {@link dev.openfeature.contrib.providers.flagd.resolver.Resolver} connection
- * state change event.
+ * Represents an event payload for a connection state change in a
+ * {@link dev.openfeature.contrib.providers.flagd.resolver.Resolver}.
+ * The event includes information about the connection status, any flags that have changed,
+ * and metadata associated with the synchronization process.
*/
-@AllArgsConstructor
public class ConnectionEvent {
- @Getter
- private final boolean connected;
+ /**
+ * The current state of the connection.
+ */
+ private final ConnectionState connected;
+
+ /**
+ * A list of flags that have changed due to this connection event.
+ */
private final List flagsChanged;
+
+ /**
+ * Metadata associated with synchronization in this connection event.
+ */
private final Structure syncMetadata;
/**
- * Construct a new ConnectionEvent.
+ * Constructs a new {@code ConnectionEvent} with the connection status only.
*
- * @param connected status of the connection
+ * @param connected {@code true} if the connection is established, otherwise {@code false}.
*/
public ConnectionEvent(boolean connected) {
+ this(
+ connected ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED,
+ Collections.emptyList(),
+ new ImmutableStructure());
+ }
+
+ /**
+ * Constructs a new {@code ConnectionEvent} with the specified connection state.
+ *
+ * @param connected the connection state indicating if the connection is established or not.
+ */
+ public ConnectionEvent(ConnectionState connected) {
this(connected, Collections.emptyList(), new ImmutableStructure());
}
/**
- * Construct a new ConnectionEvent.
+ * Constructs a new {@code ConnectionEvent} with the specified connection state and changed flags.
*
- * @param connected status of the connection
- * @param flagsChanged list of flags changed
+ * @param connected the connection state indicating if the connection is established or not.
+ * @param flagsChanged a list of flags that have changed due to this connection event.
*/
- public ConnectionEvent(boolean connected, List flagsChanged) {
+ public ConnectionEvent(ConnectionState connected, List flagsChanged) {
this(connected, flagsChanged, new ImmutableStructure());
}
/**
- * Construct a new ConnectionEvent.
+ * Constructs a new {@code ConnectionEvent} with the specified connection state and synchronization metadata.
*
- * @param connected status of the connection
- * @param syncMetadata sync.getMetadata
+ * @param connected the connection state indicating if the connection is established or not.
+ * @param syncMetadata metadata related to the synchronization process of this event.
*/
- public ConnectionEvent(boolean connected, Structure syncMetadata) {
+ public ConnectionEvent(ConnectionState connected, Structure syncMetadata) {
this(connected, Collections.emptyList(), new ImmutableStructure(syncMetadata.asMap()));
}
/**
- * Get changed flags.
+ * Constructs a new {@code ConnectionEvent} with the specified connection state, changed flags, and
+ * synchronization metadata.
+ *
+ * @param connectionState the state of the connection.
+ * @param flagsChanged a list of flags that have changed due to this connection event.
+ * @param syncMetadata metadata related to the synchronization process of this event.
+ */
+ public ConnectionEvent(ConnectionState connectionState, List flagsChanged, Structure syncMetadata) {
+ this.connected = connectionState;
+ this.flagsChanged = flagsChanged != null ? flagsChanged : Collections.emptyList(); // Ensure non-null list
+ this.syncMetadata = syncMetadata != null
+ ? new ImmutableStructure(syncMetadata.asMap())
+ : new ImmutableStructure(); // Ensure valid syncMetadata
+ }
+
+ /**
+ * Retrieves an unmodifiable view of the list of changed flags.
*
- * @return an unmodifiable view of the changed flags
+ * @return an unmodifiable list of changed flags.
*/
public List getFlagsChanged() {
return Collections.unmodifiableList(flagsChanged);
}
/**
- * Get changed sync metadata represented as SDK structure type.
+ * Retrieves the synchronization metadata represented as an immutable SDK structure type.
*
- * @return an unmodifiable view of the sync metadata
+ * @return an immutable structure containing the synchronization metadata.
*/
public Structure getSyncMetadata() {
return new ImmutableStructure(syncMetadata.asMap());
}
+
+ /**
+ * Indicates whether the current connection state is connected.
+ *
+ * @return {@code true} if connected, otherwise {@code false}.
+ */
+ public boolean isConnected() {
+ return this.connected == ConnectionState.CONNECTED;
+ }
+
+ /**
+ * Indicates
+ * whether the current connection state is stale.
+ *
+ * @return {@code true} if stale, otherwise {@code false}.
+ */
+ public boolean isStale() {
+ return this.connected == ConnectionState.STALE;
+ }
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionState.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionState.java
new file mode 100644
index 000000000..6dbd388a0
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionState.java
@@ -0,0 +1,27 @@
+package dev.openfeature.contrib.providers.flagd.resolver.common;
+
+/**
+ * Represents the possible states of a connection.
+ */
+public enum ConnectionState {
+
+ /**
+ * The connection is active and functioning as expected.
+ */
+ CONNECTED,
+
+ /**
+ * The connection is not active and has been fully disconnected.
+ */
+ DISCONNECTED,
+
+ /**
+ * The connection is inactive or degraded but may still recover.
+ */
+ STALE,
+
+ /**
+ * The connection has encountered an error and cannot function correctly.
+ */
+ ERROR,
+}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Util.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Util.java
index d634f0745..394716415 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Util.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Util.java
@@ -2,18 +2,26 @@
import dev.openfeature.sdk.exceptions.GeneralError;
import java.util.function.Supplier;
+import lombok.extern.slf4j.Slf4j;
-/** Utils for flagd resolvers. */
+/**
+ * Utility class for managing gRPC connection states and handling synchronization operations.
+ */
+@Slf4j
public class Util {
+ /**
+ * Private constructor to prevent instantiation of utility class.
+ */
private Util() {}
/**
- * A helper to block the caller for given conditions.
+ * A helper method to block the caller until a condition is met or a timeout occurs.
*
- * @param deadline number of milliseconds to block
- * @param connectedSupplier func to check for status true
- * @throws InterruptedException if interrupted
+ * @param deadline the maximum number of milliseconds to block
+ * @param connectedSupplier a function that evaluates to {@code true} when the desired condition is met
+ * @throws InterruptedException if the thread is interrupted during the waiting process
+ * @throws GeneralError if the deadline is exceeded before the condition is met
*/
public static void busyWaitAndCheck(final Long deadline, final Supplier connectedSupplier)
throws InterruptedException {
@@ -22,7 +30,7 @@ public static void busyWaitAndCheck(final Long deadline, final Supplier
do {
if (deadline <= System.currentTimeMillis() - start) {
throw new GeneralError(String.format(
- "Deadline exceeded. Condition did not complete within the %d deadline", deadline));
+ "Deadline exceeded. Condition did not complete within the %d " + "deadline", deadline));
}
Thread.sleep(50L);
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java
index 7d60a704a..db89931e5 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java
@@ -10,38 +10,42 @@
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
-import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
-/** EventStreamObserver handles events emitted by flagd. */
+/**
+ * Observer for a gRPC event stream that handles notifications about flag changes and provider readiness events.
+ * This class updates a cache and notifies listeners via a lambda callback when events occur.
+ */
@Slf4j
@SuppressFBWarnings(justification = "cache needs to be read and write by multiple objects")
class EventStreamObserver implements StreamObserver {
+
+ /**
+ * A consumer to handle connection events with a flag indicating success and a list of changed flags.
+ */
private final BiConsumer> onConnectionEvent;
- private final Supplier shouldRetrySilently;
- private final Object sync;
+
+ /**
+ * The cache to update based on received events.
+ */
private final Cache cache;
/**
- * Create a gRPC stream that get notified about flag changes.
+ * Constructs a new {@code EventStreamObserver} instance.
*
- * @param sync synchronization object from caller
- * @param cache cache to update
- * @param onConnectionEvent lambda to call to handle the response
- * @param shouldRetrySilently Boolean supplier indicating if the GRPC connector will try to
- * recover silently
+ * @param cache the cache to update based on received events
+ * @param onConnectionEvent a consumer to handle connection events with a boolean and a list of changed flags
*/
- EventStreamObserver(
- Object sync,
- Cache cache,
- BiConsumer> onConnectionEvent,
- Supplier shouldRetrySilently) {
- this.sync = sync;
+ EventStreamObserver(Cache cache, BiConsumer> onConnectionEvent) {
this.cache = cache;
this.onConnectionEvent = onConnectionEvent;
- this.shouldRetrySilently = shouldRetrySilently;
}
+ /**
+ * Called when a new event is received from the stream.
+ *
+ * @param value the event stream response containing event data
+ */
@Override
public void onNext(EventStreamResponse value) {
switch (value.getType()) {
@@ -52,37 +56,38 @@ public void onNext(EventStreamResponse value) {
this.handleProviderReadyEvent();
break;
default:
- log.debug("unhandled event type {}", value.getType());
+ log.debug("Unhandled event type {}", value.getType());
}
}
+ /**
+ * Called when an error occurs in the stream.
+ *
+ * @param throwable the error that occurred
+ */
@Override
public void onError(Throwable throwable) {
- if (Boolean.TRUE.equals(shouldRetrySilently.get())) {
- log.debug("Event stream error, trying to recover", throwable);
- } else {
- log.error("Event stream error", throwable);
- if (this.cache.getEnabled()) {
- this.cache.clear();
- }
- this.onConnectionEvent.accept(false, Collections.emptyList());
+ if (this.cache.getEnabled().equals(Boolean.TRUE)) {
+ this.cache.clear();
}
-
- // handle last call of this stream
- handleEndOfStream();
}
+ /**
+ * Called when the stream is completed.
+ */
@Override
public void onCompleted() {
- if (this.cache.getEnabled()) {
+ if (this.cache.getEnabled().equals(Boolean.TRUE)) {
this.cache.clear();
}
this.onConnectionEvent.accept(false, Collections.emptyList());
-
- // handle last call of this stream
- handleEndOfStream();
}
+ /**
+ * Handles configuration change events by updating the cache and notifying listeners about changed flags.
+ *
+ * @param value the event stream response containing configuration change data
+ */
private void handleConfigurationChangeEvent(EventStreamResponse value) {
List changedFlags = new ArrayList<>();
boolean cachingEnabled = this.cache.getEnabled();
@@ -95,7 +100,6 @@ private void handleConfigurationChangeEvent(EventStreamResponse value) {
}
} else {
Map flags = flagsValue.getStructValue().getFieldsMap();
- this.cache.getEnabled();
for (String flagKey : flags.keySet()) {
changedFlags.add(flagKey);
if (cachingEnabled) {
@@ -107,16 +111,12 @@ private void handleConfigurationChangeEvent(EventStreamResponse value) {
this.onConnectionEvent.accept(true, changedFlags);
}
+ /**
+ * Handles provider readiness events by clearing the cache (if enabled) and notifying listeners of readiness.
+ */
private void handleProviderReadyEvent() {
- this.onConnectionEvent.accept(true, Collections.emptyList());
- if (this.cache.getEnabled()) {
+ if (this.cache.getEnabled().equals(Boolean.TRUE)) {
this.cache.clear();
}
}
-
- private void handleEndOfStream() {
- synchronized (this.sync) {
- this.sync.notifyAll();
- }
- }
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java
index 8fabb5d8b..9508c521b 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java
@@ -1,169 +1,246 @@
package dev.openfeature.contrib.providers.flagd.resolver.grpc;
-import static dev.openfeature.contrib.providers.flagd.resolver.common.backoff.BackoffStrategies.maxRetriesWithExponentialTimeBackoffStrategy;
-
+import com.google.common.annotations.VisibleForTesting;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelBuilder;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelMonitor;
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
-import dev.openfeature.contrib.providers.flagd.resolver.common.Util;
-import dev.openfeature.contrib.providers.flagd.resolver.common.backoff.GrpcStreamConnectorBackoffService;
-import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
-import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamRequest;
-import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamResponse;
-import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionState;
+import dev.openfeature.sdk.ImmutableStructure;
+import io.grpc.ConnectivityState;
import io.grpc.ManagedChannel;
-import io.grpc.stub.StreamObserver;
+import io.grpc.stub.AbstractBlockingStub;
+import io.grpc.stub.AbstractStub;
import java.util.Collections;
-import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
-import java.util.function.Supplier;
+import java.util.function.Function;
+import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
-/** Class that abstracts the gRPC communication with flagd. */
+/**
+ * A generic GRPC connector that manages connection states, reconnection logic, and event streaming for
+ * GRPC services.
+ *
+ * @param the type of the asynchronous stub for the GRPC service
+ * @param the type of the blocking stub for the GRPC service
+ */
@Slf4j
-@SuppressFBWarnings(justification = "cache needs to be read and write by multiple objects")
-public class GrpcConnector {
- private final Object sync = new Object();
+public class GrpcConnector, K extends AbstractBlockingStub> {
- private final ServiceGrpc.ServiceBlockingStub serviceBlockingStub;
- private final ServiceGrpc.ServiceStub serviceStub;
+ /**
+ * The asynchronous service stub for making non-blocking GRPC calls.
+ */
+ private final T serviceStub;
+
+ /**
+ * The blocking service stub for making blocking GRPC calls.
+ */
+ private final K blockingStub;
+
+ /**
+ * The GRPC managed channel for managing the underlying GRPC connection.
+ */
private final ManagedChannel channel;
+ /**
+ * The deadline in milliseconds for GRPC operations.
+ */
private final long deadline;
+
+ /**
+ * The deadline in milliseconds for event streaming operations.
+ */
private final long streamDeadlineMs;
- private final Cache cache;
+ /**
+ * A consumer that handles connection events such as connection loss or reconnection.
+ */
private final Consumer onConnectionEvent;
- private final Supplier connectedSupplier;
- private final GrpcStreamConnectorBackoffService backoff;
- // Thread responsible for event observation
- private Thread eventObserverThread;
+ /**
+ * A consumer that handles GRPC service stubs for event stream handling.
+ */
+ private final Consumer streamObserver;
+
+ /**
+ * An executor service responsible for scheduling reconnection attempts.
+ */
+ private final ScheduledExecutorService reconnectExecutor;
+
+ /**
+ * The grace period in milliseconds to wait for reconnection before emitting an error event.
+ */
+ private final long gracePeriod;
+
+ /**
+ * Indicates whether the connector is currently connected to the GRPC service.
+ */
+ @Getter
+ private boolean connected = false;
+
+ /**
+ * A scheduled task for managing reconnection attempts.
+ */
+ private ScheduledFuture> reconnectTask;
/**
- * GrpcConnector creates an abstraction over gRPC communication.
+ * Constructs a new {@code GrpcConnector} instance with the specified options and parameters.
*
- * @param options flagd options
- * @param cache cache to use
- * @param connectedSupplier lambda providing current connection status from caller
- * @param onConnectionEvent lambda which handles changes in the connection/stream
+ * @param options the configuration options for the GRPC connection
+ * @param stub a function to create the asynchronous service stub from a {@link ManagedChannel}
+ * @param blockingStub a function to create the blocking service stub from a {@link ManagedChannel}
+ * @param onConnectionEvent a consumer to handle connection events
+ * @param eventStreamObserver a consumer to handle the event stream
+ * @param channel the managed channel for the GRPC connection
*/
public GrpcConnector(
final FlagdOptions options,
- final Cache cache,
- final Supplier connectedSupplier,
- Consumer onConnectionEvent) {
- this.channel = ChannelBuilder.nettyChannel(options);
- this.serviceStub = ServiceGrpc.newStub(channel);
- this.serviceBlockingStub = ServiceGrpc.newBlockingStub(channel);
+ final Function stub,
+ final Function blockingStub,
+ final Consumer onConnectionEvent,
+ final Consumer eventStreamObserver,
+ ManagedChannel channel) {
+
+ this.channel = channel;
+ this.serviceStub = stub.apply(channel);
+ this.blockingStub = blockingStub.apply(channel);
this.deadline = options.getDeadline();
this.streamDeadlineMs = options.getStreamDeadlineMs();
- this.cache = cache;
this.onConnectionEvent = onConnectionEvent;
- this.connectedSupplier = connectedSupplier;
- this.backoff = new GrpcStreamConnectorBackoffService(maxRetriesWithExponentialTimeBackoffStrategy(
- options.getMaxEventStreamRetries(), options.getRetryBackoffMs()));
+ this.streamObserver = eventStreamObserver;
+ this.gracePeriod = options.getRetryGracePeriod();
+ this.reconnectExecutor = Executors.newSingleThreadScheduledExecutor();
}
- /** Initialize the gRPC stream. */
+ /**
+ * Constructs a {@code GrpcConnector} instance for testing purposes.
+ *
+ * @param options the configuration options for the GRPC connection
+ * @param stub a function to create the asynchronous service stub from a {@link ManagedChannel}
+ * @param blockingStub a function to create the blocking service stub from a {@link ManagedChannel}
+ * @param onConnectionEvent a consumer to handle connection events
+ * @param eventStreamObserver a consumer to handle the event stream
+ */
+ @VisibleForTesting
+ GrpcConnector(
+ final FlagdOptions options,
+ final Function stub,
+ final Function blockingStub,
+ final Consumer onConnectionEvent,
+ final Consumer eventStreamObserver) {
+ this(options, stub, blockingStub, onConnectionEvent, eventStreamObserver, ChannelBuilder.nettyChannel(options));
+ }
+
+ /**
+ * Initializes the GRPC connection by waiting for the channel to be ready and monitoring its state.
+ *
+ * @throws Exception if the channel does not reach the desired state within the deadline
+ */
public void initialize() throws Exception {
- eventObserverThread = new Thread(this::observeEventStream);
- eventObserverThread.setDaemon(true);
- eventObserverThread.start();
+ log.info("Initializing GRPC connection...");
+ ChannelMonitor.waitForDesiredState(
+ channel, ConnectivityState.READY, this::onInitialConnect, deadline, TimeUnit.MILLISECONDS);
+ ChannelMonitor.monitorChannelState(ConnectivityState.READY, channel, this::onReady, this::onConnectionLost);
+ }
- // block till ready
- Util.busyWaitAndCheck(this.deadline, this.connectedSupplier);
+ /**
+ * Returns the blocking service stub for making blocking GRPC calls.
+ *
+ * @return the blocking service stub
+ */
+ public K getResolver() {
+ return blockingStub;
}
/**
- * Shuts down all gRPC resources.
+ * Shuts down the GRPC connection and cleans up associated resources.
*
- * @throws Exception is something goes wrong while terminating the communication.
+ * @throws InterruptedException if interrupted while waiting for termination
*/
- public void shutdown() throws Exception {
- // first shutdown the event listener
- if (this.eventObserverThread != null) {
- this.eventObserverThread.interrupt();
+ public void shutdown() throws InterruptedException {
+ log.info("Shutting down GRPC connection...");
+ if (reconnectExecutor != null) {
+ reconnectExecutor.shutdownNow();
+ reconnectExecutor.awaitTermination(deadline, TimeUnit.MILLISECONDS);
}
- try {
- if (this.channel != null && !this.channel.isShutdown()) {
- this.channel.shutdown();
- this.channel.awaitTermination(this.deadline, TimeUnit.MILLISECONDS);
- }
- } finally {
- this.cache.clear();
- if (this.channel != null && !this.channel.isShutdown()) {
- this.channel.shutdownNow();
- this.channel.awaitTermination(this.deadline, TimeUnit.MILLISECONDS);
- log.warn(String.format("Unable to shut down channel by %d deadline", this.deadline));
- }
+ if (!channel.isShutdown()) {
+ channel.shutdownNow();
+ channel.awaitTermination(deadline, TimeUnit.MILLISECONDS);
+ }
+
+ if (connected) {
this.onConnectionEvent.accept(new ConnectionEvent(false));
+ connected = false;
}
}
- /**
- * Provide the object that can be used to resolve Feature Flag values.
- *
- * @return a {@link ServiceGrpc.ServiceBlockingStub} for running FF resolution.
- */
- public ServiceGrpc.ServiceBlockingStub getResolver() {
- return serviceBlockingStub.withDeadlineAfter(this.deadline, TimeUnit.MILLISECONDS);
+ private synchronized void onInitialConnect() {
+ connected = true;
+ restartStream();
}
/**
- * Event stream observer logic. This contains blocking mechanisms, hence must be run in a
- * dedicated thread.
+ * Handles the event when the GRPC channel becomes ready, marking the connection as established.
+ * Cancels any pending reconnection task and restarts the event stream.
*/
- private void observeEventStream() {
- while (backoff.shouldRetry()) {
- final StreamObserver responseObserver =
- new EventStreamObserver(sync, this.cache, this::onConnectionEvent, backoff::shouldRetrySilently);
+ private synchronized void onReady() {
+ connected = true;
- ServiceGrpc.ServiceStub localServiceStub = this.serviceStub;
+ if (reconnectTask != null && !reconnectTask.isCancelled()) {
+ reconnectTask.cancel(false);
+ log.debug("Reconnection task cancelled as connection became READY.");
+ }
+ restartStream();
+ this.onConnectionEvent.accept(new ConnectionEvent(true));
+ }
- if (this.streamDeadlineMs > 0) {
- localServiceStub = localServiceStub.withDeadlineAfter(this.streamDeadlineMs, TimeUnit.MILLISECONDS);
- }
+ /**
+ * Handles the event when the GRPC channel loses its connection, marking the connection as lost.
+ * Schedules a reconnection task after a grace period and emits a stale connection event.
+ */
+ private synchronized void onConnectionLost() {
+ log.debug("Connection lost. Emit STALE event...");
+ log.debug("Waiting {}s for connection to become available...", gracePeriod);
+ connected = false;
- localServiceStub.eventStream(EventStreamRequest.getDefaultInstance(), responseObserver);
-
- try {
- synchronized (sync) {
- sync.wait();
- }
- } catch (InterruptedException e) {
- // Interruptions are considered end calls for this observer, hence log and
- // return
- // Note - this is the most common interruption when shutdown, hence the log
- // level debug
- log.debug("interruption while waiting for condition", e);
- Thread.currentThread().interrupt();
- }
+ this.onConnectionEvent.accept(
+ new ConnectionEvent(ConnectionState.STALE, Collections.emptyList(), new ImmutableStructure()));
- try {
- backoff.waitUntilNextAttempt();
- } catch (InterruptedException e) {
- // Interruptions are considered end calls for this observer, hence log and
- // return
- log.warn("interrupted while restarting gRPC Event Stream");
- Thread.currentThread().interrupt();
- }
+ if (reconnectTask != null && !reconnectTask.isCancelled()) {
+ reconnectTask.cancel(false);
}
- log.error("failed to connect to event stream, exhausted retries");
- this.onConnectionEvent(false, Collections.emptyList());
+ if (!reconnectExecutor.isShutdown()) {
+ reconnectTask = reconnectExecutor.schedule(
+ () -> {
+ log.debug(
+ "Provider did not reconnect successfully within {}s. Emit ERROR event...", gracePeriod);
+ this.onConnectionEvent.accept(new ConnectionEvent(false));
+ },
+ gracePeriod,
+ TimeUnit.SECONDS);
+ }
}
- private void onConnectionEvent(final boolean connected, final List changedFlags) {
- // reset reconnection states
+ /**
+ * Restarts the event stream using the asynchronous service stub, applying a deadline if configured.
+ * Emits a connection event if the restart is successful.
+ */
+ private synchronized void restartStream() {
if (connected) {
- backoff.reset();
+ log.debug("(Re)initializing event stream.");
+ T localServiceStub = this.serviceStub;
+ if (streamDeadlineMs > 0) {
+ localServiceStub = localServiceStub.withDeadlineAfter(this.streamDeadlineMs, TimeUnit.MILLISECONDS);
+ }
+ streamObserver.accept(localServiceStub);
+ return;
}
-
- // chain to initiator
- this.onConnectionEvent.accept(new ConnectionEvent(connected, changedFlags));
+ log.debug("Stream restart skipped. Not connected.");
}
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java
index 9d8c3a9f2..a64275c2b 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java
@@ -11,14 +11,17 @@
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionState;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.strategy.ResolveFactory;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.strategy.ResolveStrategy;
+import dev.openfeature.flagd.grpc.evaluation.Evaluation;
import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveBooleanRequest;
import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveFloatRequest;
import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveIntRequest;
import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveObjectRequest;
import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveStringRequest;
+import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.ProviderEvaluation;
@@ -34,7 +37,6 @@
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
-import java.util.function.Supplier;
/**
* Resolves flag values using https://buf.build/open-feature/flagd/docs/main:flagd.evaluation.v1.
@@ -44,72 +46,89 @@
@SuppressFBWarnings(justification = "cache needs to be read and write by multiple objects")
public final class GrpcResolver implements Resolver {
- private final GrpcConnector connector;
+ private final GrpcConnector connector;
private final Cache cache;
private final ResolveStrategy strategy;
- private final Supplier connectedSupplier;
/**
* Resolves flag values using https://buf.build/open-feature/flagd/docs/main:flagd.evaluation.v1.
* Flags are evaluated remotely.
*
- * @param options flagd options
- * @param cache cache to use
- * @param connectedSupplier lambda providing current connection status from caller
+ * @param options flagd options
+ * @param cache cache to use
* @param onConnectionEvent lambda which handles changes in the connection/stream
*/
public GrpcResolver(
- final FlagdOptions options,
- final Cache cache,
- final Supplier connectedSupplier,
- final Consumer onConnectionEvent) {
+ final FlagdOptions options, final Cache cache, final Consumer onConnectionEvent) {
this.cache = cache;
- this.connectedSupplier = connectedSupplier;
this.strategy = ResolveFactory.getStrategy(options);
- this.connector = new GrpcConnector(options, cache, connectedSupplier, onConnectionEvent);
+ this.connector = new GrpcConnector<>(
+ options,
+ ServiceGrpc::newStub,
+ ServiceGrpc::newBlockingStub,
+ onConnectionEvent,
+ stub -> stub.eventStream(
+ Evaluation.EventStreamRequest.getDefaultInstance(),
+ new EventStreamObserver(
+ cache,
+ (k, e) ->
+ onConnectionEvent.accept(new ConnectionEvent(ConnectionState.CONNECTED, e)))));
}
- /** Initialize Grpc resolver. */
+ /**
+ * Initialize Grpc resolver.
+ */
public void init() throws Exception {
this.connector.initialize();
}
- /** Shutdown Grpc resolver. */
+ /**
+ * Shutdown Grpc resolver.
+ */
public void shutdown() throws Exception {
this.connector.shutdown();
}
- /** Boolean evaluation from grpc resolver. */
+ /**
+ * Boolean evaluation from grpc resolver.
+ */
public ProviderEvaluation booleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
ResolveBooleanRequest request = ResolveBooleanRequest.newBuilder().buildPartial();
- return resolve(key, ctx, request, this.connector.getResolver()::resolveBoolean, null);
+ return resolve(key, ctx, request, connector.getResolver()::resolveBoolean, null);
}
- /** String evaluation from grpc resolver. */
+ /**
+ * String evaluation from grpc resolver.
+ */
public ProviderEvaluation stringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
ResolveStringRequest request = ResolveStringRequest.newBuilder().buildPartial();
-
- return resolve(key, ctx, request, this.connector.getResolver()::resolveString, null);
+ return resolve(key, ctx, request, connector.getResolver()::resolveString, null);
}
- /** Double evaluation from grpc resolver. */
+ /**
+ * Double evaluation from grpc resolver.
+ */
public ProviderEvaluation doubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
ResolveFloatRequest request = ResolveFloatRequest.newBuilder().buildPartial();
- return resolve(key, ctx, request, this.connector.getResolver()::resolveFloat, null);
+ return resolve(key, ctx, request, connector.getResolver()::resolveFloat, null);
}
- /** Integer evaluation from grpc resolver. */
+ /**
+ * Integer evaluation from grpc resolver.
+ */
public ProviderEvaluation integerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
ResolveIntRequest request = ResolveIntRequest.newBuilder().buildPartial();
- return resolve(key, ctx, request, this.connector.getResolver()::resolveInt, (Object value) -> ((Long) value)
- .intValue());
+ return resolve(
+ key, ctx, request, connector.getResolver()::resolveInt, (Object value) -> ((Long) value).intValue());
}
- /** Object evaluation from grpc resolver. */
+ /**
+ * Object evaluation from grpc resolver.
+ */
public ProviderEvaluation objectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
ResolveObjectRequest request = ResolveObjectRequest.newBuilder().buildPartial();
@@ -118,13 +137,13 @@ public ProviderEvaluation objectEvaluation(String key, Value defaultValue
key,
ctx,
request,
- this.connector.getResolver()::resolveObject,
+ connector.getResolver()::resolveObject,
(Object value) -> convertObjectResponse((Struct) value));
}
/**
- * A generic resolve method that takes a resolverRef and an optional converter lambda to transform
- * the result.
+ * A generic resolve method that takes a resolverRef and an optional converter
+ * lambda to transform the result.
*/
private ProviderEvaluation resolve(
String key,
@@ -187,7 +206,7 @@ private Boolean isEvaluationCacheable(ProviderEvaluation evaluation) {
}
private Boolean cacheAvailable() {
- return this.cache.getEnabled() && this.connectedSupplier.get();
+ return this.cache.getEnabled() && this.connector.isConnected();
}
private static ImmutableMetadata metadataFromResponse(Message response) {
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java
index 663d4bb0d..fd617af1f 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java
@@ -5,6 +5,7 @@
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
+import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionState;
import dev.openfeature.contrib.providers.flagd.resolver.common.Util;
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore;
@@ -28,8 +29,9 @@
import lombok.extern.slf4j.Slf4j;
/**
- * Resolves flag values using https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1. Flags
- * are evaluated locally.
+ * Resolves flag values using
+ * https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1.
+ * Flags are evaluated locally.
*/
@Slf4j
public class InProcessResolver implements Resolver {
@@ -41,12 +43,15 @@ public class InProcessResolver implements Resolver {
private final Supplier connectedSupplier;
/**
- * Resolves flag values using https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1. Flags
- * are evaluated locally.
+ * Resolves flag values using
+ * https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1.
+ * Flags are evaluated locally.
*
- * @param options flagd options
- * @param connectedSupplier lambda providing current connection status from caller
- * @param onConnectionEvent lambda which handles changes in the connection/stream
+ * @param options flagd options
+ * @param connectedSupplier lambda providing current connection status from
+ * caller
+ * @param onConnectionEvent lambda which handles changes in the
+ * connection/stream
*/
public InProcessResolver(
FlagdOptions options,
@@ -64,7 +69,9 @@ public InProcessResolver(
.build();
}
- /** Initialize in-process resolver. */
+ /**
+ * Initialize in-process resolver.
+ */
public void init() throws Exception {
flagStore.init();
final Thread stateWatcher = new Thread(() -> {
@@ -75,7 +82,7 @@ public void init() throws Exception {
switch (storageStateChange.getStorageState()) {
case OK:
onConnectionEvent.accept(new ConnectionEvent(
- true,
+ ConnectionState.CONNECTED,
storageStateChange.getChangedFlagsKeys(),
storageStateChange.getSyncMetadata()));
break;
@@ -109,27 +116,37 @@ public void shutdown() throws InterruptedException {
onConnectionEvent.accept(new ConnectionEvent(false));
}
- /** Resolve a boolean flag. */
+ /**
+ * Resolve a boolean flag.
+ */
public ProviderEvaluation booleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
return resolve(Boolean.class, key, ctx);
}
- /** Resolve a string flag. */
+ /**
+ * Resolve a string flag.
+ */
public ProviderEvaluation stringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
return resolve(String.class, key, ctx);
}
- /** Resolve a double flag. */
+ /**
+ * Resolve a double flag.
+ */
public ProviderEvaluation doubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
return resolve(Double.class, key, ctx);
}
- /** Resolve an integer flag. */
+ /**
+ * Resolve an integer flag.
+ */
public ProviderEvaluation integerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
return resolve(Integer.class, key, ctx);
}
- /** Resolve an object flag. */
+ /**
+ * Resolve an object flag.
+ */
public ProviderEvaluation objectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
final ProviderEvaluation