-
Notifications
You must be signed in to change notification settings - Fork 64
feat: add option to rebuild gRPC connection on error #1668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b429a68
95a0b5e
4501e45
f4f9d62
f1b57c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,12 +28,13 @@ | |
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| /** | ||
| * Implements the {@link QueueSource} contract and emit flags obtained from flagd sync gRPC contract. | ||
| * Implements the {@link QueueSource} contract and emit flags obtained from | ||
| * flagd sync gRPC contract. | ||
| */ | ||
| @Slf4j | ||
| @SuppressFBWarnings( | ||
| value = {"EI_EXPOSE_REP"}, | ||
| justification = "Random is used to generate a variation & flag configurations require exposing") | ||
| justification = "We need to expose the BlockingQueue to allow consumers to read from it") | ||
| public class SyncStreamQueueSource implements QueueSource { | ||
| private static final int QUEUE_SIZE = 5; | ||
|
|
||
|
|
@@ -45,13 +46,32 @@ public class SyncStreamQueueSource implements QueueSource { | |
| private final String selector; | ||
| private final String providerId; | ||
| private final boolean syncMetadataDisabled; | ||
| private final ChannelConnector channelConnector; | ||
| private final boolean reinitializeOnError; | ||
| private final FlagdOptions options; | ||
| private final Consumer<FlagdProviderEvent> onConnectionEvent; | ||
| private final BlockingQueue<QueuePayload> outgoingQueue = new LinkedBlockingQueue<>(QUEUE_SIZE); | ||
| private final FlagSyncServiceStub flagSyncStub; | ||
| private final FlagSyncServiceBlockingStub metadataStub; | ||
| private volatile GrpcComponents grpcComponents; | ||
|
|
||
| /** | ||
| * Creates a new SyncStreamQueueSource responsible for observing the event stream. | ||
| * Container for gRPC components to ensure atomicity during reinitialization. | ||
| * All three components are updated together to prevent consumers from seeing | ||
| * an inconsistent state where components are from different channel instances. | ||
| */ | ||
| private static class GrpcComponents { | ||
| final ChannelConnector channelConnector; | ||
| final FlagSyncServiceStub flagSyncStub; | ||
| final FlagSyncServiceBlockingStub metadataStub; | ||
|
|
||
| GrpcComponents(ChannelConnector connector, FlagSyncServiceStub stub, FlagSyncServiceBlockingStub blockingStub) { | ||
| this.channelConnector = connector; | ||
| this.flagSyncStub = stub; | ||
| this.metadataStub = blockingStub; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new SyncStreamQueueSource responsible for observing the event | ||
| * stream. | ||
| */ | ||
| public SyncStreamQueueSource(final FlagdOptions options, Consumer<FlagdProviderEvent> onConnectionEvent) { | ||
| streamDeadline = options.getStreamDeadlineMs(); | ||
|
|
@@ -60,11 +80,10 @@ public SyncStreamQueueSource(final FlagdOptions options, Consumer<FlagdProviderE | |
| providerId = options.getProviderId(); | ||
| maxBackoffMs = options.getRetryBackoffMaxMs(); | ||
| syncMetadataDisabled = options.isSyncMetadataDisabled(); | ||
| channelConnector = new ChannelConnector(options, onConnectionEvent, ChannelBuilder.nettyChannel(options)); | ||
| flagSyncStub = | ||
| FlagSyncServiceGrpc.newStub(channelConnector.getChannel()).withWaitForReady(); | ||
| metadataStub = FlagSyncServiceGrpc.newBlockingStub(channelConnector.getChannel()) | ||
| .withWaitForReady(); | ||
| reinitializeOnError = options.isReinitializeOnError(); | ||
| this.options = options; | ||
| this.onConnectionEvent = onConnectionEvent; | ||
| initializeChannelComponents(); | ||
| } | ||
|
|
||
| // internal use only | ||
|
|
@@ -77,16 +96,57 @@ protected SyncStreamQueueSource( | |
| deadline = options.getDeadline(); | ||
| selector = options.getSelector(); | ||
| providerId = options.getProviderId(); | ||
| channelConnector = connectorMock; | ||
| maxBackoffMs = options.getRetryBackoffMaxMs(); | ||
| flagSyncStub = stubMock; | ||
| syncMetadataDisabled = options.isSyncMetadataDisabled(); | ||
| metadataStub = blockingStubMock; | ||
| reinitializeOnError = options.isReinitializeOnError(); | ||
toddbaert marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.options = options; | ||
| this.onConnectionEvent = e -> {}; | ||
| this.grpcComponents = new GrpcComponents(connectorMock, stubMock, blockingStubMock); | ||
| } | ||
|
|
||
| /** Initialize channel connector and stubs. */ | ||
| private synchronized void initializeChannelComponents() { | ||
| ChannelConnector newConnector = | ||
| new ChannelConnector(options, onConnectionEvent, ChannelBuilder.nettyChannel(options)); | ||
| FlagSyncServiceStub newFlagSyncStub = | ||
| FlagSyncServiceGrpc.newStub(newConnector.getChannel()).withWaitForReady(); | ||
| FlagSyncServiceBlockingStub newMetadataStub = | ||
| FlagSyncServiceGrpc.newBlockingStub(newConnector.getChannel()).withWaitForReady(); | ||
|
|
||
| // atomic assignment of all components as a single unit | ||
| grpcComponents = new GrpcComponents(newConnector, newFlagSyncStub, newMetadataStub); | ||
| } | ||
|
|
||
| /** Reinitialize channel connector and stubs on error. */ | ||
| public synchronized void reinitializeChannelComponents() { | ||
| if (!reinitializeOnError || shutdown.get()) { | ||
| return; | ||
| } | ||
|
|
||
| log.info("Reinitializing channel gRPC components in attempt to restore stream."); | ||
| GrpcComponents oldComponents = grpcComponents; | ||
|
|
||
| try { | ||
| // create new channel components first | ||
| initializeChannelComponents(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to call
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very good eye... but... I think I discovered something earlier about this that I lost track of and forgot to mention: I think the I think this is why my manual testing had no issue not including the call you suggest above, and same with our e2e test suite. In fact, if I completely comment out all the functionality of the Unless I'm missing something which we have no tests for, I think this method can be deleted. cc @aepfli do you know anything about this? |
||
| } catch (Exception e) { | ||
| log.error("Failed to reinitialize channel components", e); | ||
| return; | ||
| } | ||
|
|
||
| // shutdown old connector after successful reinitialization | ||
| if (oldComponents != null && oldComponents.channelConnector != null) { | ||
| try { | ||
| oldComponents.channelConnector.shutdown(); | ||
| } catch (Exception e) { | ||
| log.debug("Error shutting down old channel connector during reinitialization", e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Initialize sync stream connector. */ | ||
| public void init() throws Exception { | ||
| channelConnector.initialize(); | ||
| grpcComponents.channelConnector.initialize(); | ||
| Thread listener = new Thread(this::observeSyncStream); | ||
| listener.setDaemon(true); | ||
| listener.start(); | ||
|
|
@@ -109,7 +169,7 @@ public void shutdown() throws InterruptedException { | |
| log.debug("Shutdown already in progress or completed"); | ||
| return; | ||
| } | ||
| this.channelConnector.shutdown(); | ||
| grpcComponents.channelConnector.shutdown(); | ||
| } | ||
|
|
||
| /** Contains blocking calls, to be used concurrently. */ | ||
|
|
@@ -159,13 +219,14 @@ private void observeSyncStream() { | |
| log.info("Shutdown invoked, exiting event stream listener"); | ||
| } | ||
|
|
||
| // TODO: remove the metadata call entirely after https://github.com/open-feature/flagd/issues/1584 | ||
| // TODO: remove the metadata call entirely after | ||
| // https://github.com/open-feature/flagd/issues/1584 | ||
| private Struct getMetadata() { | ||
| if (syncMetadataDisabled) { | ||
| return null; | ||
| } | ||
|
|
||
| FlagSyncServiceBlockingStub localStub = metadataStub; | ||
| FlagSyncServiceBlockingStub localStub = grpcComponents.metadataStub; | ||
|
|
||
| if (deadline > 0) { | ||
| localStub = localStub.withDeadlineAfter(deadline, TimeUnit.MILLISECONDS); | ||
|
|
@@ -180,7 +241,8 @@ private Struct getMetadata() { | |
|
|
||
| return null; | ||
| } catch (StatusRuntimeException e) { | ||
| // In newer versions of flagd, metadata is part of the sync stream. If the method is unimplemented, we | ||
| // In newer versions of flagd, metadata is part of the sync stream. If the | ||
| // method is unimplemented, we | ||
| // can ignore the error | ||
| if (e.getStatus() != null | ||
| && Status.Code.UNIMPLEMENTED.equals(e.getStatus().getCode())) { | ||
|
|
@@ -192,7 +254,7 @@ private Struct getMetadata() { | |
| } | ||
|
|
||
| private void syncFlags(SyncStreamObserver streamObserver) { | ||
| FlagSyncServiceStub localStub = flagSyncStub; // don't mutate the stub | ||
| FlagSyncServiceStub localStub = grpcComponents.flagSyncStub; // don't mutate the stub | ||
| if (streamDeadline > 0) { | ||
| localStub = localStub.withDeadlineAfter(streamDeadline, TimeUnit.MILLISECONDS); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.