Skip to content

Commit 6c2e058

Browse files
author
Bernd Warmuth
committed
feat: refactor GrpcConnector to use grpc builtin reconnection
Signed-off-by: Bernd Warmuth <[email protected]>
1 parent bc06454 commit 6c2e058

File tree

19 files changed

+959
-1145
lines changed

19 files changed

+959
-1145
lines changed

providers/flagd/pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,5 +399,4 @@
399399
</build>
400400
</profile>
401401
</profiles>
402-
403402
</project>

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public final class Config {
5757
public static final String LRU_CACHE = CacheType.LRU.getValue();
5858
static final String DEFAULT_CACHE = LRU_CACHE;
5959

60-
static final int DEFAULT_MAX_EVENT_STREAM_RETRIES = 7;
6160
static final int BASE_EVENT_STREAM_RETRY_BACKOFF_MS = 1000;
6261

6362
static String fallBackToEnvOrDefault(String key, String defaultValue) {

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,6 @@ public class FlagdOptions {
6969
private int maxCacheSize = fallBackToEnvOrDefault(Config.MAX_CACHE_SIZE_ENV_VAR_NAME,
7070
Config.DEFAULT_MAX_CACHE_SIZE);
7171

72-
/**
73-
* Max event stream connection retries.
74-
*/
75-
@Builder.Default
76-
private int maxEventStreamRetries = fallBackToEnvOrDefault(Config.MAX_EVENT_STREAM_RETRIES_ENV_VAR_NAME,
77-
Config.DEFAULT_MAX_EVENT_STREAM_RETRIES);
78-
7972
/**
8073
* Backoff interval in milliseconds.
8174
*/
@@ -102,11 +95,12 @@ public class FlagdOptions {
10295
Config.DEFAULT_STREAM_DEADLINE_MS);
10396

10497
/**
105-
* Amount of stream retry attempts before provider moves from STALE to ERROR
106-
* Defaults to 5
98+
* Grace time period in milliseconds before provider moves from STALE to ERROR.
99+
* Defaults to 50_000
107100
*/
108101
@Builder.Default
109-
private int streamRetryGracePeriod = fallBackToEnvOrDefault(Config.STREAM_RETRY_GRACE_PERIOD, Config.DEFAULT_STREAM_RETRY_GRACE_PERIOD);
102+
private int streamRetryGracePeriod = fallBackToEnvOrDefault(Config.STREAM_RETRY_GRACE_PERIOD,
103+
Config.DEFAULT_STREAM_RETRY_GRACE_PERIOD);
110104
/**
111105
* Selector to be used with flag sync gRPC contract.
112106
**/
@@ -116,7 +110,6 @@ public class FlagdOptions {
116110
/**
117111
* gRPC client KeepAlive in milliseconds. Disabled with 0.
118112
* Defaults to 0 (disabled).
119-
*
120113
**/
121114
@Builder.Default
122115
private long keepAlive = fallBackToEnvOrDefault(Config.KEEP_ALIVE_MS_ENV_VAR_NAME,

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
package dev.openfeature.contrib.providers.flagd;
22

3-
import java.util.ArrayList;
4-
import java.util.Collections;
5-
import java.util.List;
6-
import java.util.function.Function;
7-
83
import dev.openfeature.contrib.providers.flagd.resolver.Resolver;
94
import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
105
import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver;
@@ -22,11 +17,16 @@
2217
import dev.openfeature.sdk.Value;
2318
import lombok.extern.slf4j.Slf4j;
2419

20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.function.Function;
24+
2525
/**
2626
* OpenFeature provider for flagd.
2727
*/
2828
@Slf4j
29-
@SuppressWarnings({ "PMD.TooManyStaticImports", "checkstyle:NoFinalizer" })
29+
@SuppressWarnings({"PMD.TooManyStaticImports", "checkstyle:NoFinalizer"})
3030
public class FlagdProvider extends EventProvider {
3131
private Function<Structure, EvaluationContext> contextEnricher;
3232
private static final String FLAGD_PROVIDER = "flagd";
@@ -62,7 +62,6 @@ public FlagdProvider(final FlagdOptions options) {
6262
case Config.RESOLVER_RPC:
6363
this.flagResolver = new GrpcResolver(options,
6464
new Cache(options.getCacheType(), options.getMaxCacheSize()),
65-
this::isConnected,
6665
this::onConnectionEvent);
6766
break;
6867
default:
@@ -85,7 +84,7 @@ public synchronized void initialize(EvaluationContext evaluationContext) throws
8584
}
8685

8786
this.flagResolver.init();
88-
this.initialized = true;
87+
this.initialized = this.connected = true;
8988
}
9089

9190
@Override
@@ -139,7 +138,7 @@ public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultVa
139138
* Set on initial connection and updated with every reconnection.
140139
* see:
141140
* https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata
142-
*
141+
*
143142
* @return Object map representing sync metadata
144143
*/
145144
protected Structure getSyncMetadata() {
@@ -148,6 +147,7 @@ protected Structure getSyncMetadata() {
148147

149148
/**
150149
* The updated context mixed into all evaluations based on the sync-metadata.
150+
*
151151
* @return context
152152
*/
153153
EvaluationContext getEnrichedContext() {
@@ -159,33 +159,42 @@ private boolean isConnected() {
159159
}
160160

161161
private void onConnectionEvent(ConnectionEvent connectionEvent) {
162-
boolean previous = connected;
163-
boolean current = connected = connectionEvent.isConnected();
162+
final boolean wasConnected = connected;// WHY the F*** is this false? wasconnected is false ,hence no change
163+
// event will be sent. why is was connected false? not updated via event?
164+
final boolean isConnected = connected = connectionEvent.isConnected();
165+
164166
syncMetadata = connectionEvent.getSyncMetadata();
165167
enrichedContext = contextEnricher.apply(connectionEvent.getSyncMetadata());
166168

167-
// configuration changed
168-
if (initialized && previous && current) {
169-
log.debug("Configuration changed");
169+
if (!initialized) {
170+
return;
171+
}
172+
173+
if (!wasConnected && isConnected) {
170174
ProviderEventDetails details = ProviderEventDetails.builder()
171175
.flagsChanged(connectionEvent.getFlagsChanged())
172-
.message("configuration changed").build();
173-
this.emitProviderConfigurationChanged(details);
176+
.message("connected to flagd")
177+
.build();
178+
this.emitProviderReady(details);
174179
return;
175180
}
176-
// there was an error
177-
if (initialized && previous && !current) {
178-
log.debug("There has been an error");
179-
ProviderEventDetails details = ProviderEventDetails.builder().message("there has been an error").build();
180-
this.emitProviderError(details);
181+
182+
if (wasConnected && isConnected) {
183+
ProviderEventDetails details = ProviderEventDetails.builder()
184+
.flagsChanged(connectionEvent.getFlagsChanged())
185+
.message("configuration changed")
186+
.build();
187+
this.emitProviderConfigurationChanged(details);
181188
return;
182189
}
183-
// we recovered from an error
184-
if (initialized && !previous && current) {
185-
log.debug("Recovered from error");
186-
ProviderEventDetails details = ProviderEventDetails.builder().message("recovered from error").build();
187-
this.emitProviderReady(details);
188-
this.emitProviderConfigurationChanged(details);
190+
191+
if (connectionEvent.isStale()) {
192+
this.emitProviderStale(ProviderEventDetails.builder().message("there has been an error").build());
193+
} else {
194+
this.emitProviderError(ProviderEventDetails.builder().message("there has been an error").build());
189195
}
190196
}
191197
}
198+
199+
200+
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common;
2+
3+
import dev.openfeature.sdk.exceptions.GeneralError;
4+
import io.grpc.ConnectivityState;
5+
import io.grpc.ManagedChannel;
6+
import lombok.extern.slf4j.Slf4j;
7+
8+
import java.util.concurrent.CountDownLatch;
9+
import java.util.concurrent.ScheduledExecutorService;
10+
import java.util.concurrent.ScheduledFuture;
11+
import java.util.concurrent.TimeUnit;
12+
import java.util.concurrent.atomic.AtomicReference;
13+
14+
15+
/**
16+
* A utility class to monitor and manage the connectivity state of a gRPC ManagedChannel.
17+
*/
18+
@Slf4j
19+
public class ChannelMonitor {
20+
21+
22+
private ChannelMonitor() {
23+
24+
}
25+
26+
/**
27+
* Monitors the state of a gRPC channel and triggers the specified callbacks based on state changes.
28+
*
29+
* @param expectedState the initial state to monitor.
30+
* @param channel the ManagedChannel to monitor.
31+
* @param onConnectionReady callback invoked when the channel transitions to a READY state.
32+
* @param onConnectionLost callback invoked when the channel transitions to a FAILURE or SHUTDOWN state.
33+
*/
34+
public static void monitorChannelState(ConnectivityState expectedState, ManagedChannel channel,
35+
Runnable onConnectionReady, Runnable onConnectionLost) {
36+
channel.notifyWhenStateChanged(expectedState, () -> {
37+
ConnectivityState currentState = channel.getState(true);
38+
log.info("Channel state changed to: {}", currentState);
39+
if (currentState == ConnectivityState.READY) {
40+
onConnectionReady.run();
41+
} else if (currentState == ConnectivityState.TRANSIENT_FAILURE
42+
|| currentState == ConnectivityState.SHUTDOWN) {
43+
onConnectionLost.run();
44+
}
45+
// Re-register the state monitor to watch for the next state transition.
46+
monitorChannelState(currentState, channel, onConnectionReady, onConnectionLost);
47+
});
48+
}
49+
50+
51+
/**
52+
* Waits for the channel to reach a desired state within a specified timeout period.
53+
*
54+
* @param channel the ManagedChannel to monitor.
55+
* @param desiredState the ConnectivityState to wait for.
56+
* @param connectCallback callback invoked when the desired state is reached.
57+
* @param timeout the maximum amount of time to wait.
58+
* @param unit the time unit of the timeout.
59+
* @throws InterruptedException if the current thread is interrupted while waiting.
60+
*/
61+
public static void waitForDesiredState(ManagedChannel channel,
62+
ConnectivityState desiredState,
63+
Runnable connectCallback,
64+
long timeout,
65+
TimeUnit unit) throws InterruptedException {
66+
waitForDesiredState(channel, desiredState, connectCallback, new CountDownLatch(1), timeout, unit);
67+
}
68+
69+
70+
private static void waitForDesiredState(ManagedChannel channel,
71+
ConnectivityState desiredState,
72+
Runnable connectCallback,
73+
CountDownLatch latch,
74+
long timeout,
75+
TimeUnit unit) throws InterruptedException {
76+
channel.notifyWhenStateChanged(ConnectivityState.SHUTDOWN, () -> {
77+
try {
78+
ConnectivityState state = channel.getState(true);
79+
log.debug("Channel state changed to: {}", state);
80+
81+
if (state == desiredState) {
82+
connectCallback.run();
83+
latch.countDown();
84+
return;
85+
}
86+
waitForDesiredState(channel, desiredState, connectCallback, latch, timeout, unit);
87+
} catch (InterruptedException e) {
88+
Thread.currentThread().interrupt();
89+
log.error("Thread interrupted while waiting for desired state", e);
90+
} catch (Exception e) {
91+
log.error("Error occurred while waiting for desired state", e);
92+
}
93+
});
94+
95+
// Await the latch or timeout for the state change
96+
if (!latch.await(timeout, unit)) {
97+
throw new GeneralError(String.format("Deadline exceeded. Condition did not complete within the %d "
98+
+ "deadline", timeout));
99+
}
100+
}
101+
102+
103+
/**
104+
* Polls the state of a gRPC channel at regular intervals and triggers callbacks upon state changes.
105+
*
106+
* @param executor the ScheduledExecutorService used for polling.
107+
* @param channel the ManagedChannel to monitor.
108+
* @param onConnectionReady callback invoked when the channel transitions to a READY state.
109+
* @param onConnectionLost callback invoked when the channel transitions to a FAILURE or SHUTDOWN state.
110+
* @param pollIntervalMs the polling interval in milliseconds.
111+
*/
112+
public static void pollChannelState(ScheduledExecutorService executor, ManagedChannel channel,
113+
Runnable onConnectionReady,
114+
Runnable onConnectionLost, long pollIntervalMs) {
115+
116+
AtomicReference<ConnectivityState> lastState = new AtomicReference<>(ConnectivityState.READY);
117+
118+
Runnable pollTask = () -> {
119+
ConnectivityState currentState = channel.getState(true);
120+
if (currentState != lastState.get()) {
121+
if (currentState == ConnectivityState.READY) {
122+
log.debug("gRPC connection became READY");
123+
onConnectionReady.run();
124+
} else if (currentState == ConnectivityState.TRANSIENT_FAILURE
125+
|| currentState == ConnectivityState.SHUTDOWN) {
126+
log.debug("gRPC connection became TRANSIENT_FAILURE");
127+
onConnectionLost.run();
128+
}
129+
lastState.set(currentState);
130+
}
131+
};
132+
executor.scheduleAtFixedRate(pollTask, 0, pollIntervalMs, TimeUnit.MILLISECONDS);
133+
}
134+
135+
136+
/**
137+
* Polls the channel state at fixed intervals and waits for the channel to reach a desired state within a timeout
138+
* period.
139+
*
140+
* @param executor the ScheduledExecutorService used for polling.
141+
* @param channel the ManagedChannel to monitor.
142+
* @param desiredState the ConnectivityState to wait for.
143+
* @param connectCallback callback invoked when the desired state is reached.
144+
* @param timeout the maximum amount of time to wait.
145+
* @param unit the time unit of the timeout.
146+
* @return {@code true} if the desired state was reached within the timeout period, {@code false} otherwise.
147+
* @throws InterruptedException if the current thread is interrupted while waiting.
148+
*/
149+
public static boolean pollForDesiredState(ScheduledExecutorService executor, ManagedChannel channel,
150+
ConnectivityState desiredState, Runnable connectCallback, long timeout,
151+
TimeUnit unit) throws InterruptedException {
152+
CountDownLatch latch = new CountDownLatch(1);
153+
154+
Runnable waitForStateTask = () -> {
155+
ConnectivityState currentState = channel.getState(true);
156+
if (currentState == desiredState) {
157+
connectCallback.run();
158+
latch.countDown();
159+
}
160+
};
161+
162+
ScheduledFuture<?> scheduledFuture = executor.scheduleWithFixedDelay(waitForStateTask, 0, 100,
163+
TimeUnit.MILLISECONDS);
164+
165+
boolean success = latch.await(timeout, unit);
166+
scheduledFuture.cancel(true);
167+
return success;
168+
}
169+
}

0 commit comments

Comments
 (0)