Skip to content

Commit 9444297

Browse files
feat: add option to rebuild gRPC connection on error (#1668)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 626a933 commit 9444297

File tree

6 files changed

+219
-34
lines changed

6 files changed

+219
-34
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public final class Config {
2020
static final int DEFAULT_MAX_CACHE_SIZE = 1000;
2121
static final int DEFAULT_OFFLINE_POLL_MS = 5000;
2222
static final long DEFAULT_KEEP_ALIVE = 0;
23+
static final String DEFAULT_REINITIALIZE_ON_ERROR = "false";
2324

2425
static final String RESOLVER_ENV_VAR = "FLAGD_RESOLVER";
2526
static final String HOST_ENV_VAR_NAME = "FLAGD_HOST";
@@ -51,6 +52,7 @@ public final class Config {
5152
static final String KEEP_ALIVE_MS_ENV_VAR_NAME = "FLAGD_KEEP_ALIVE_TIME_MS";
5253
static final String TARGET_URI_ENV_VAR_NAME = "FLAGD_TARGET_URI";
5354
static final String STREAM_RETRY_GRACE_PERIOD = "FLAGD_RETRY_GRACE_PERIOD";
55+
static final String REINITIALIZE_ON_ERROR_ENV_VAR_NAME = "FLAGD_REINITIALIZE_ON_ERROR";
5456

5557
static final String RESOLVER_RPC = "rpc";
5658
static final String RESOLVER_IN_PROCESS = "in-process";

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,16 @@ public class FlagdOptions {
204204
@Builder.Default
205205
private String defaultAuthority = fallBackToEnvOrDefault(Config.DEFAULT_AUTHORITY_ENV_VAR_NAME, null);
206206

207+
/**
208+
* !EXPERIMENTAL!
209+
* Whether to reinitialize the channel (TCP connection) after the grace period is exceeded.
210+
* This can help recover from connection issues by creating fresh connections.
211+
* Particularly useful for troubleshooting network issues related to proxies or service meshes.
212+
*/
213+
@Builder.Default
214+
private boolean reinitializeOnError = Boolean.parseBoolean(
215+
fallBackToEnvOrDefault(Config.REINITIALIZE_ON_ERROR_ENV_VAR_NAME, Config.DEFAULT_REINITIALIZE_ON_ERROR));
216+
207217
/**
208218
* Builder overwrite in order to customize the "build" method.
209219
*

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class InProcessResolver implements Resolver {
4141
private final Consumer<FlagdProviderEvent> onConnectionEvent;
4242
private final Operator operator;
4343
private final String scope;
44+
private final QueueSource queueSource;
4445

4546
/**
4647
* Resolves flag values using
@@ -52,7 +53,8 @@ public class InProcessResolver implements Resolver {
5253
* connection/stream
5354
*/
5455
public InProcessResolver(FlagdOptions options, Consumer<FlagdProviderEvent> onConnectionEvent) {
55-
this.flagStore = new FlagStore(getConnector(options));
56+
this.queueSource = getQueueSource(options);
57+
this.flagStore = new FlagStore(queueSource);
5658
this.onConnectionEvent = onConnectionEvent;
5759
this.operator = new Operator();
5860
this.scope = options.getSelector();
@@ -94,6 +96,19 @@ public void init() throws Exception {
9496
stateWatcher.start();
9597
}
9698

99+
/**
100+
* Called when the provider enters error state after grace period.
101+
* Attempts to reinitialize the sync connector if enabled.
102+
*/
103+
@Override
104+
public void onError() {
105+
if (queueSource instanceof SyncStreamQueueSource) {
106+
SyncStreamQueueSource syncConnector = (SyncStreamQueueSource) queueSource;
107+
// only reinitialize if option is enabled
108+
syncConnector.reinitializeChannelComponents();
109+
}
110+
}
111+
97112
/**
98113
* Shutdown in-process resolver.
99114
*
@@ -147,7 +162,7 @@ public ProviderEvaluation<Value> objectEvaluation(String key, Value defaultValue
147162
.build();
148163
}
149164

150-
static QueueSource getConnector(final FlagdOptions options) {
165+
static QueueSource getQueueSource(final FlagdOptions options) {
151166
if (options.getCustomConnector() != null) {
152167
return options.getCustomConnector();
153168
}

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/SyncStreamQueueSource.java

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@
2626
import lombok.extern.slf4j.Slf4j;
2727

2828
/**
29-
* Implements the {@link QueueSource} contract and emit flags obtained from flagd sync gRPC contract.
29+
* Implements the {@link QueueSource} contract and emit flags obtained from
30+
* flagd sync gRPC contract.
3031
*/
3132
@Slf4j
3233
@SuppressFBWarnings(
3334
value = {"EI_EXPOSE_REP"},
34-
justification = "Random is used to generate a variation & flag configurations require exposing")
35+
justification = "We need to expose the BlockingQueue to allow consumers to read from it")
3536
public class SyncStreamQueueSource implements QueueSource {
3637
private static final int QUEUE_SIZE = 5;
3738

@@ -43,13 +44,31 @@ public class SyncStreamQueueSource implements QueueSource {
4344
private final String selector;
4445
private final String providerId;
4546
private final boolean syncMetadataDisabled;
46-
private final ChannelConnector channelConnector;
47+
private final boolean reinitializeOnError;
48+
private final FlagdOptions options;
4749
private final BlockingQueue<QueuePayload> outgoingQueue = new LinkedBlockingQueue<>(QUEUE_SIZE);
48-
private final FlagSyncServiceStub flagSyncStub;
49-
private final FlagSyncServiceBlockingStub metadataStub;
50+
private volatile GrpcComponents grpcComponents;
5051

5152
/**
52-
* Creates a new SyncStreamQueueSource responsible for observing the event stream.
53+
* Container for gRPC components to ensure atomicity during reinitialization.
54+
* All three components are updated together to prevent consumers from seeing
55+
* an inconsistent state where components are from different channel instances.
56+
*/
57+
private static class GrpcComponents {
58+
final ChannelConnector channelConnector;
59+
final FlagSyncServiceStub flagSyncStub;
60+
final FlagSyncServiceBlockingStub metadataStub;
61+
62+
GrpcComponents(ChannelConnector connector, FlagSyncServiceStub stub, FlagSyncServiceBlockingStub blockingStub) {
63+
this.channelConnector = connector;
64+
this.flagSyncStub = stub;
65+
this.metadataStub = blockingStub;
66+
}
67+
}
68+
69+
/**
70+
* Creates a new SyncStreamQueueSource responsible for observing the event
71+
* stream.
5372
*/
5473
public SyncStreamQueueSource(final FlagdOptions options) {
5574
streamDeadline = options.getStreamDeadlineMs();
@@ -58,11 +77,9 @@ public SyncStreamQueueSource(final FlagdOptions options) {
5877
providerId = options.getProviderId();
5978
maxBackoffMs = options.getRetryBackoffMaxMs();
6079
syncMetadataDisabled = options.isSyncMetadataDisabled();
61-
channelConnector = new ChannelConnector(options, ChannelBuilder.nettyChannel(options));
62-
flagSyncStub =
63-
FlagSyncServiceGrpc.newStub(channelConnector.getChannel()).withWaitForReady();
64-
metadataStub = FlagSyncServiceGrpc.newBlockingStub(channelConnector.getChannel())
65-
.withWaitForReady();
80+
reinitializeOnError = options.isReinitializeOnError();
81+
this.options = options;
82+
initializeChannelComponents();
6683
}
6784

6885
// internal use only
@@ -75,11 +92,50 @@ protected SyncStreamQueueSource(
7592
deadline = options.getDeadline();
7693
selector = options.getSelector();
7794
providerId = options.getProviderId();
78-
channelConnector = connectorMock;
7995
maxBackoffMs = options.getRetryBackoffMaxMs();
80-
flagSyncStub = stubMock;
8196
syncMetadataDisabled = options.isSyncMetadataDisabled();
82-
metadataStub = blockingStubMock;
97+
reinitializeOnError = options.isReinitializeOnError();
98+
this.options = options;
99+
this.grpcComponents = new GrpcComponents(connectorMock, stubMock, blockingStubMock);
100+
}
101+
102+
/** Initialize channel connector and stubs. */
103+
private synchronized void initializeChannelComponents() {
104+
ChannelConnector newConnector = new ChannelConnector(options, ChannelBuilder.nettyChannel(options));
105+
FlagSyncServiceStub newFlagSyncStub =
106+
FlagSyncServiceGrpc.newStub(newConnector.getChannel()).withWaitForReady();
107+
FlagSyncServiceBlockingStub newMetadataStub =
108+
FlagSyncServiceGrpc.newBlockingStub(newConnector.getChannel()).withWaitForReady();
109+
110+
// atomic assignment of all components as a single unit
111+
grpcComponents = new GrpcComponents(newConnector, newFlagSyncStub, newMetadataStub);
112+
}
113+
114+
/** Reinitialize channel connector and stubs on error. */
115+
public synchronized void reinitializeChannelComponents() {
116+
if (!reinitializeOnError || shutdown.get()) {
117+
return;
118+
}
119+
120+
log.info("Reinitializing channel gRPC components in attempt to restore stream.");
121+
GrpcComponents oldComponents = grpcComponents;
122+
123+
try {
124+
// create new channel components first
125+
initializeChannelComponents();
126+
} catch (Exception e) {
127+
log.error("Failed to reinitialize channel components", e);
128+
return;
129+
}
130+
131+
// shutdown old connector after successful reinitialization
132+
if (oldComponents != null && oldComponents.channelConnector != null) {
133+
try {
134+
oldComponents.channelConnector.shutdown();
135+
} catch (Exception e) {
136+
log.debug("Error shutting down old channel connector during reinitialization", e);
137+
}
138+
}
83139
}
84140

85141
/** Initialize sync stream connector. */
@@ -106,7 +162,7 @@ public void shutdown() throws InterruptedException {
106162
log.debug("Shutdown already in progress or completed");
107163
return;
108164
}
109-
this.channelConnector.shutdown();
165+
grpcComponents.channelConnector.shutdown();
110166
}
111167

112168
/** Contains blocking calls, to be used concurrently. */
@@ -156,13 +212,14 @@ private void observeSyncStream() {
156212
log.info("Shutdown invoked, exiting event stream listener");
157213
}
158214

159-
// TODO: remove the metadata call entirely after https://github.com/open-feature/flagd/issues/1584
215+
// TODO: remove the metadata call entirely after
216+
// https://github.com/open-feature/flagd/issues/1584
160217
private Struct getMetadata() {
161218
if (syncMetadataDisabled) {
162219
return null;
163220
}
164221

165-
FlagSyncServiceBlockingStub localStub = metadataStub;
222+
FlagSyncServiceBlockingStub localStub = grpcComponents.metadataStub;
166223

167224
if (deadline > 0) {
168225
localStub = localStub.withDeadlineAfter(deadline, TimeUnit.MILLISECONDS);
@@ -177,7 +234,8 @@ private Struct getMetadata() {
177234

178235
return null;
179236
} catch (StatusRuntimeException e) {
180-
// In newer versions of flagd, metadata is part of the sync stream. If the method is unimplemented, we
237+
// In newer versions of flagd, metadata is part of the sync stream. If the
238+
// method is unimplemented, we
181239
// can ignore the error
182240
if (e.getStatus() != null
183241
&& Status.Code.UNIMPLEMENTED.equals(e.getStatus().getCode())) {
@@ -189,7 +247,7 @@ private Struct getMetadata() {
189247
}
190248

191249
private void syncFlags(SyncStreamObserver streamObserver) {
192-
FlagSyncServiceStub localStub = flagSyncStub; // don't mutate the stub
250+
FlagSyncServiceStub localStub = grpcComponents.flagSyncStub; // don't mutate the stub
193251
if (streamDeadline > 0) {
194252
localStub = localStub.withDeadlineAfter(streamDeadline, TimeUnit.MILLISECONDS);
195253
}

providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
import static org.junit.jupiter.api.Assertions.assertNotNull;
1818
import static org.junit.jupiter.api.Assertions.assertThrows;
1919
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.times;
22+
import static org.mockito.Mockito.verify;
2023

2124
import dev.openfeature.contrib.providers.flagd.Config;
2225
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
@@ -51,6 +54,26 @@
5154
import org.junit.jupiter.api.Test;
5255

5356
class InProcessResolverTest {
57+
@Test
58+
void onError_delegatesToQueueSource() throws Exception {
59+
// given
60+
FlagdOptions options = FlagdOptions.builder().build(); // option value doesn't matter here
61+
SyncStreamQueueSource mockConnector = mock(SyncStreamQueueSource.class);
62+
InProcessResolver resolver = new InProcessResolver(options, e -> {});
63+
64+
// Inject mock connector
65+
java.lang.reflect.Field queueSourceField = InProcessResolver.class.getDeclaredField("queueSource");
66+
queueSourceField.setAccessible(true);
67+
queueSourceField.set(resolver, mockConnector);
68+
69+
// when
70+
resolver.onError();
71+
72+
// then
73+
// InProcessResolver should always delegate to the queue source.
74+
// The decision to re-initialize or not is handled within SyncStreamQueueSource.
75+
verify(mockConnector, times(1)).reinitializeChannelComponents();
76+
}
5477

5578
@Test
5679
public void connectorSetup() {
@@ -70,9 +93,9 @@ public void connectorSetup() {
7093
.build();
7194

7295
// then
73-
assertInstanceOf(SyncStreamQueueSource.class, InProcessResolver.getConnector(forGrpcOptions));
74-
assertInstanceOf(FileQueueSource.class, InProcessResolver.getConnector(forOfflineOptions));
75-
assertInstanceOf(MockConnector.class, InProcessResolver.getConnector(forCustomConnectorOptions));
96+
assertInstanceOf(SyncStreamQueueSource.class, InProcessResolver.getQueueSource(forGrpcOptions));
97+
assertInstanceOf(FileQueueSource.class, InProcessResolver.getQueueSource(forOfflineOptions));
98+
assertInstanceOf(MockConnector.class, InProcessResolver.getQueueSource(forCustomConnectorOptions));
7699
}
77100

78101
@Test

0 commit comments

Comments
 (0)