From 7e778290088f09b19288c7a54718a1b103ffcda7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:25:49 +0000 Subject: [PATCH 01/10] Initial plan From d8b12ee74193957a1cfd75bcee683831d7a5b668 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:44:42 +0000 Subject: [PATCH 02/10] Document selector migration guidance for in-process mode Co-authored-by: aepfli <9987394+aepfli@users.noreply.github.com> --- providers/flagd/README.md | 130 ++++++++++++++++++++++++++++++++------ 1 file changed, 109 insertions(+), 21 deletions(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index f2d680c2c..3c7967b65 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -47,6 +47,88 @@ FlagdProvider flagdProvider = new FlagdProvider( In the above example, in-process handlers attempt to connect to a sync service on address `localhost:8013` to obtain [flag definitions](https://github.com/open-feature/schemas/blob/main/json/flags.json). +#### Selector filtering (In-process mode only) + +The `selector` option allows filtering flag configurations from flagd based on source identifiers when using the in-process resolver. This is useful when flagd is configured with multiple flag sources and you want to sync only a specific subset. + +##### Current implementation (Request body) + +The current implementation passes the selector in the gRPC request body via the `SyncFlagsRequest`: + +```java +FlagdProvider flagdProvider = new FlagdProvider( + FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .selector("source=my-app") + .build()); +``` + +Or via environment variable: +```bash +export FLAGD_SOURCE_SELECTOR="source=my-app" +``` + +##### Migration to header-based selector + +> [!IMPORTANT] +> **Selector normalization and deprecation notice** +> +> As part of [flagd issue #1814](https://github.com/open-feature/flagd/issues/1814), the flagd project is normalizing selector handling across all services. The preferred approach is to use the `flagd-selector` gRPC metadata header instead of the request body field. +> +> **Current status:** +> - The Java SDK currently uses the **request body** approach (via `SyncFlagsRequest.setSelector()`) +> - flagd services support both request body and header for backward compatibility +> - In a future major version, the request body selector field may be removed from flagd +> +> **Recommended migration path:** +> +> To prepare for future changes and align with the preferred approach, you can pass the selector via gRPC headers using a custom `ClientInterceptor`: + +```java +import io.grpc.*; + +private static ClientInterceptor createSelectorHeaderInterceptor(String selector) { + return new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + headers.put( + Metadata.Key.of("flagd-selector", Metadata.ASCII_STRING_MARSHALLER), + selector + ); + super.start(responseListener, headers); + } + }; + } + }; +} + +// Use the interceptor when creating the provider +List interceptors = new ArrayList<>(); +interceptors.add(createSelectorHeaderInterceptor("source=my-app")); + +FlagdProvider flagdProvider = new FlagdProvider( + FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .clientInterceptors(interceptors) + // Note: You can still use .selector() for backward compatibility + // but the header approach is preferred for future-proofing + .build()); +``` + +**Backward compatibility:** +- The current request body approach will continue to work with flagd services that support it +- Both approaches can be used simultaneously during the migration period +- The Java SDK will be updated in a future major version to use headers by default + +For more details on selector normalization, see the [flagd selector normalization issue](https://github.com/open-feature/flagd/issues/1814). + #### Sync-metadata To support the injection of contextual data configured in flagd for in-process evaluation, the provider exposes a `getSyncMetadata` accessor which provides the most recent value returned by the [GetMetadata RPC](https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata). @@ -106,30 +188,33 @@ variables. Given below are the supported configurations: -| Option name | Environment variable name | Type & Values | Default | Compatible resolver | -|-----------------------|------------------------------------------------------------------------|--------------------------|-------------------------------|-------------------------| -| resolver | FLAGD_RESOLVER | String - rpc, in-process | rpc | | -| host | FLAGD_HOST | String | localhost | rpc & in-process | -| port | FLAGD_PORT (rpc), FLAGD_SYNC_PORT (in-process, FLAGD_PORT as fallback) | int | 8013 (rpc), 8015 (in-process) | rpc & in-process | -| targetUri | FLAGD_TARGET_URI | string | null | rpc & in-process | -| tls | FLAGD_TLS | boolean | false | rpc & in-process | -| defaultAuthority | FLAGD_DEFAULT_AUTHORITY | String | null | rpc & in-process | -| socketPath | FLAGD_SOCKET_PATH | String | null | rpc & in-process | -| certPath | FLAGD_SERVER_CERT_PATH | String | null | rpc & in-process | -| deadline | FLAGD_DEADLINE_MS | int | 500 | rpc & in-process & file | -| streamDeadlineMs | FLAGD_STREAM_DEADLINE_MS | int | 600000 | rpc & in-process | -| keepAliveTime | FLAGD_KEEP_ALIVE_TIME_MS | long | 0 | rpc & in-process | -| selector | FLAGD_SOURCE_SELECTOR | String | null | in-process | -| providerId | FLAGD_SOURCE_PROVIDER_ID | String | null | in-process | -| cache | FLAGD_CACHE | String - lru, disabled | lru | rpc | -| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | rpc | -| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | rpc | -| retryBackoffMs | FLAGD_RETRY_BACKOFF_MS | int | 1000 | rpc | -| offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | String | null | file | -| offlinePollIntervalMs | FLAGD_OFFLINE_POLL_MS | int | 5000 | file | +| Option name | Environment variable name | Type & Values | Default | Compatible resolver | +| --------------------- | ---------------------------------------------------------------------- | ------------------------ | ----------------------------- | ------------------------------------------------------------------------------- | +| resolver | FLAGD_RESOLVER | String - rpc, in-process | rpc | | +| host | FLAGD_HOST | String | localhost | rpc & in-process | +| port | FLAGD_PORT (rpc), FLAGD_SYNC_PORT (in-process, FLAGD_PORT as fallback) | int | 8013 (rpc), 8015 (in-process) | rpc & in-process | +| targetUri | FLAGD_TARGET_URI | string | null | rpc & in-process | +| tls | FLAGD_TLS | boolean | false | rpc & in-process | +| defaultAuthority | FLAGD_DEFAULT_AUTHORITY | String | null | rpc & in-process | +| socketPath | FLAGD_SOCKET_PATH | String | null | rpc & in-process | +| certPath | FLAGD_SERVER_CERT_PATH | String | null | rpc & in-process | +| deadline | FLAGD_DEADLINE_MS | int | 500 | rpc & in-process & file | +| streamDeadlineMs | FLAGD_STREAM_DEADLINE_MS | int | 600000 | rpc & in-process | +| keepAliveTime | FLAGD_KEEP_ALIVE_TIME_MS | long | 0 | rpc & in-process | +| selector | FLAGD_SOURCE_SELECTOR | String | null | in-process (see [migration guidance](#selector-filtering-in-process-mode-only)) | +| providerId | FLAGD_SOURCE_PROVIDER_ID | String | null | in-process | +| cache | FLAGD_CACHE | String - lru, disabled | lru | rpc | +| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | rpc | +| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | rpc | +| retryBackoffMs | FLAGD_RETRY_BACKOFF_MS | int | 1000 | rpc | +| offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | String | null | file | +| offlinePollIntervalMs | FLAGD_OFFLINE_POLL_MS | int | 5000 | file | > [!NOTE] > Some configurations are only applicable for RPC resolver. + +> [!WARNING] +> The `selector` option currently uses the gRPC request body approach, which may be deprecated in future flagd versions. See [Selector filtering](#selector-filtering-in-process-mode-only) for migration guidance to the header-based approach. > ### Unix socket support @@ -189,6 +274,9 @@ FlagdProvider flagdProvider = new FlagdProvider( The `clientInterceptors` and `defaultAuthority` are meant for connection of the in-process resolver to a Sync API implementation on a host/port, that might require special credentials or headers. +> [!TIP] +> `ClientInterceptor` can also be used to pass the `flagd-selector` header for selector-based filtering. See [Selector filtering](#selector-filtering-in-process-mode-only) for details on the preferred header-based approach. + ```java private static ClientInterceptor createHeaderInterceptor() { return new ClientInterceptor() { From 6988d889a8ad03d096654fd816d2f9aabe5c52cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:47:37 +0000 Subject: [PATCH 03/10] Add JavaDoc deprecation notice for selector option Co-authored-by: aepfli <9987394+aepfli@users.noreply.github.com> --- .../contrib/providers/flagd/FlagdOptions.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 4cda34df4..2b87d30cc 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 @@ -124,6 +124,15 @@ public class FlagdOptions { fallBackToEnvOrDefault(Config.STREAM_RETRY_GRACE_PERIOD, Config.DEFAULT_STREAM_RETRY_GRACE_PERIOD); /** * Selector to be used with flag sync gRPC contract. + *

+ * Note: This currently uses the gRPC request body approach. The preferred approach is to use + * the {@code flagd-selector} header via a {@link ClientInterceptor}. + * See the selector migration guidance + * for details on the header-based approach. + *

+ * Only applicable for in-process resolver mode. + * + * @see flagd selector normalization issue **/ @Builder.Default private String selector = fallBackToEnvOrDefault(Config.SOURCE_SELECTOR_ENV_VAR_NAME, null); From 42834a1edd59a24e44594d9f6015917a37b541ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:58:13 +0000 Subject: [PATCH 04/10] Implement header-based selector for sync stream connection Co-authored-by: aepfli <9987394+aepfli@users.noreply.github.com> --- .../flagd/resolver/common/ChannelBuilder.java | 42 +++++++++++++++++-- .../connector/sync/SyncStreamQueueSource.java | 33 +++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java index 39f33af27..860c9a128 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java @@ -3,7 +3,14 @@ import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.common.nameresolvers.EnvoyResolverProvider; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; import io.grpc.NameResolverRegistry; import io.grpc.Status.Code; import io.grpc.netty.GrpcSslContexts; @@ -94,14 +101,19 @@ public static ManagedChannel nettyChannel(final FlagdOptions options) { if (!Epoll.isAvailable()) { throw new IllegalStateException("unix socket cannot be used", Epoll.unavailabilityCause()); } - return NettyChannelBuilder.forAddress(new DomainSocketAddress(options.getSocketPath())) + var channelBuilder = NettyChannelBuilder.forAddress(new DomainSocketAddress(options.getSocketPath())) .keepAliveTime(keepAliveMs, TimeUnit.MILLISECONDS) .eventLoopGroup(new MultiThreadIoEventLoopGroup(EpollIoHandler.newFactory())) .channelType(EpollDomainSocketChannel.class) .usePlaintext() .defaultServiceConfig(buildRetryPolicy(options)) - .enableRetry() - .build(); + .enableRetry(); + + // add header-based selector interceptor if selector is provided + if (options.getSelector() != null) { + channelBuilder.intercept(createSelectorInterceptor(options.getSelector())); + } + return channelBuilder.build(); } // build a TCP socket @@ -160,6 +172,30 @@ public static ManagedChannel nettyChannel(final FlagdOptions options) { } } + /** + * Creates a ClientInterceptor that adds the flagd-selector header to gRPC requests. + * This is the preferred approach for passing selectors as per flagd issue #1814. + * + * @param selector the selector value to pass in the header + * @return a ClientInterceptor that adds the flagd-selector header + */ + private static ClientInterceptor createSelectorInterceptor(String selector) { + return new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + headers.put(Metadata.Key.of("flagd-selector", Metadata.ASCII_STRING_MARSHALLER), selector); + super.start(responseListener, headers); + } + }; + } + }; + } + private static boolean isValidTargetUri(String targetUri) { if (targetUri == null) { return false; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/SyncStreamQueueSource.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/SyncStreamQueueSource.java index 3c1058566..f6d72e624 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/SyncStreamQueueSource.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/SyncStreamQueueSource.java @@ -16,6 +16,13 @@ import dev.openfeature.flagd.grpc.sync.Sync.SyncFlagsResponse; import dev.openfeature.sdk.Awaitable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; @@ -253,6 +260,8 @@ private void syncFlags(SyncStreamObserver streamObserver) { } final SyncFlagsRequest.Builder syncRequest = SyncFlagsRequest.newBuilder(); + // Selector is now passed via header using ClientInterceptor (see constructor) + // Keeping this for backward compatibility with older flagd versions if (this.selector != null) { syncRequest.setSelector(this.selector); } @@ -266,6 +275,30 @@ private void syncFlags(SyncStreamObserver streamObserver) { streamObserver.done.await(); } + /** + * Creates a ClientInterceptor that adds the flagd-selector header to gRPC requests. + * This is the preferred approach for passing selectors as per flagd issue #1814. + * + * @param selector the selector value to pass in the header + * @return a ClientInterceptor that adds the flagd-selector header + */ + private static ClientInterceptor createSelectorInterceptor(String selector) { + return new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + headers.put(Metadata.Key.of("flagd-selector", Metadata.ASCII_STRING_MARSHALLER), selector); + super.start(responseListener, headers); + } + }; + } + }; + } + private void enqueueError(String message) { enqueueError(outgoingQueue, message); } From c43a7387cc5566291f3327e40deb5e0db1331527 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:02:17 +0000 Subject: [PATCH 05/10] Update documentation to reflect header-based implementation Co-authored-by: aepfli <9987394+aepfli@users.noreply.github.com> --- providers/flagd/README.md | 75 +++++-------------- .../contrib/providers/flagd/FlagdOptions.java | 9 +-- 2 files changed, 23 insertions(+), 61 deletions(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index 3c7967b65..483e8ca05 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -51,9 +51,9 @@ In the above example, in-process handlers attempt to connect to a sync service o The `selector` option allows filtering flag configurations from flagd based on source identifiers when using the in-process resolver. This is useful when flagd is configured with multiple flag sources and you want to sync only a specific subset. -##### Current implementation (Request body) +##### Usage -The current implementation passes the selector in the gRPC request body via the `SyncFlagsRequest`: +To use selector filtering, simply configure the `selector` option when creating the provider: ```java FlagdProvider flagdProvider = new FlagdProvider( @@ -68,64 +68,27 @@ Or via environment variable: export FLAGD_SOURCE_SELECTOR="source=my-app" ``` -##### Migration to header-based selector +##### Implementation details > [!IMPORTANT] -> **Selector normalization and deprecation notice** +> **Selector normalization (flagd issue #1814)** > -> As part of [flagd issue #1814](https://github.com/open-feature/flagd/issues/1814), the flagd project is normalizing selector handling across all services. The preferred approach is to use the `flagd-selector` gRPC metadata header instead of the request body field. +> As part of [flagd issue #1814](https://github.com/open-feature/flagd/issues/1814), the flagd project is normalizing selector handling across all services to use the `flagd-selector` gRPC metadata header. > -> **Current status:** -> - The Java SDK currently uses the **request body** approach (via `SyncFlagsRequest.setSelector()`) -> - flagd services support both request body and header for backward compatibility -> - In a future major version, the request body selector field may be removed from flagd +> **Current implementation:** +> - The Java SDK **automatically passes the selector via the `flagd-selector` header** (preferred approach) +> - For backward compatibility with older flagd versions, the selector is **also sent in the request body** +> - Both methods work with current flagd versions +> - In a future major version of flagd, the request body selector field may be removed > -> **Recommended migration path:** +> **No migration needed:** > -> To prepare for future changes and align with the preferred approach, you can pass the selector via gRPC headers using a custom `ClientInterceptor`: - -```java -import io.grpc.*; - -private static ClientInterceptor createSelectorHeaderInterceptor(String selector) { - return new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, - CallOptions callOptions, - Channel next) { - return new ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void start(Listener responseListener, Metadata headers) { - headers.put( - Metadata.Key.of("flagd-selector", Metadata.ASCII_STRING_MARSHALLER), - selector - ); - super.start(responseListener, headers); - } - }; - } - }; -} - -// Use the interceptor when creating the provider -List interceptors = new ArrayList<>(); -interceptors.add(createSelectorHeaderInterceptor("source=my-app")); - -FlagdProvider flagdProvider = new FlagdProvider( - FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .clientInterceptors(interceptors) - // Note: You can still use .selector() for backward compatibility - // but the header approach is preferred for future-proofing - .build()); -``` +> Users do not need to make any code changes. The SDK handles selector normalization automatically. **Backward compatibility:** -- The current request body approach will continue to work with flagd services that support it -- Both approaches can be used simultaneously during the migration period -- The Java SDK will be updated in a future major version to use headers by default +- Both header and request body approaches work with current flagd versions +- Older flagd versions that only support request body selectors are still supported +- Future flagd versions may remove request body selector support, but the SDK will continue to work using headers For more details on selector normalization, see the [flagd selector normalization issue](https://github.com/open-feature/flagd/issues/1814). @@ -213,8 +176,8 @@ Given below are the supported configurations: > [!NOTE] > Some configurations are only applicable for RPC resolver. -> [!WARNING] -> The `selector` option currently uses the gRPC request body approach, which may be deprecated in future flagd versions. See [Selector filtering](#selector-filtering-in-process-mode-only) for migration guidance to the header-based approach. +> [!NOTE] +> The `selector` option automatically uses the `flagd-selector` header (the preferred approach per [flagd issue #1814](https://github.com/open-feature/flagd/issues/1814)) while maintaining backward compatibility with older flagd versions. See [Selector filtering](#selector-filtering-in-process-mode-only) for details. > ### Unix socket support @@ -274,8 +237,8 @@ FlagdProvider flagdProvider = new FlagdProvider( The `clientInterceptors` and `defaultAuthority` are meant for connection of the in-process resolver to a Sync API implementation on a host/port, that might require special credentials or headers. -> [!TIP] -> `ClientInterceptor` can also be used to pass the `flagd-selector` header for selector-based filtering. See [Selector filtering](#selector-filtering-in-process-mode-only) for details on the preferred header-based approach. +> [!NOTE] +> The SDK automatically handles the `flagd-selector` header when the `selector` option is configured. Custom interceptors are not needed for selector filtering. See [Selector filtering](#selector-filtering-in-process-mode-only) for details. ```java private static ClientInterceptor createHeaderInterceptor() { 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 2b87d30cc..2151afb3b 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 @@ -125,14 +125,13 @@ public class FlagdOptions { /** * Selector to be used with flag sync gRPC contract. *

- * Note: This currently uses the gRPC request body approach. The preferred approach is to use - * the {@code flagd-selector} header via a {@link ClientInterceptor}. - * See the selector migration guidance - * for details on the header-based approach. + * The SDK automatically passes the selector via the {@code flagd-selector} gRPC metadata header + * (the preferred approach per flagd issue #1814). + * For backward compatibility with older flagd versions, the selector is also sent in the request body. *

* Only applicable for in-process resolver mode. * - * @see flagd selector normalization issue + * @see Selector filtering documentation **/ @Builder.Default private String selector = fallBackToEnvOrDefault(Config.SOURCE_SELECTOR_ENV_VAR_NAME, null); From 5a22bf806033a2437ffc0ba403c21e2e44540238 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 08:10:39 +0000 Subject: [PATCH 06/10] Fix JavaDoc formatting errors in FlagdOptions Co-authored-by: aepfli <9987394+aepfli@users.noreply.github.com> --- .../contrib/providers/flagd/FlagdOptions.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 2151afb3b..0eebe16c9 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 @@ -124,13 +124,13 @@ public class FlagdOptions { fallBackToEnvOrDefault(Config.STREAM_RETRY_GRACE_PERIOD, Config.DEFAULT_STREAM_RETRY_GRACE_PERIOD); /** * Selector to be used with flag sync gRPC contract. - *

- * The SDK automatically passes the selector via the {@code flagd-selector} gRPC metadata header + * + *

The SDK automatically passes the selector via the {@code flagd-selector} gRPC metadata header * (the preferred approach per flagd issue #1814). * For backward compatibility with older flagd versions, the selector is also sent in the request body. - *

- * Only applicable for in-process resolver mode. - * + * + *

Only applicable for in-process resolver mode. + * * @see Selector filtering documentation **/ @Builder.Default From 1df95d0e4809d68ee656050d03c2ec5675ac1d89 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Thu, 6 Nov 2025 13:01:23 +0100 Subject: [PATCH 07/10] fixup: spotless and gemini feedback Signed-off-by: Simon Schrottner --- providers/flagd/README.md | 5 --- .../flagd/resolver/common/ChannelBuilder.java | 24 +++++++++----- .../connector/sync/SyncStreamQueueSource.java | 32 ------------------- 3 files changed, 16 insertions(+), 45 deletions(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index 483e8ca05..5194e3a87 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -85,11 +85,6 @@ export FLAGD_SOURCE_SELECTOR="source=my-app" > > Users do not need to make any code changes. The SDK handles selector normalization automatically. -**Backward compatibility:** -- Both header and request body approaches work with current flagd versions -- Older flagd versions that only support request body selectors are still supported -- Future flagd versions may remove request body selector support, but the SDK will continue to work using headers - For more details on selector normalization, see the [flagd selector normalization issue](https://github.com/open-feature/flagd/issues/1814). #### Sync-metadata diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java index 860c9a128..8d1644766 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java @@ -33,6 +33,10 @@ /** gRPC channel builder helper. */ @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "we don't care to serialize this") public class ChannelBuilder { + + private static final Metadata.Key FLAGD_SELECTOR_KEY = + Metadata.Key.of("flagd-selector", Metadata.ASCII_STRING_MARSHALLER); + /** * Controls retry (not-reconnection) policy for failed RPCs. */ @@ -128,14 +132,14 @@ public static ManagedChannel nettyChannel(final FlagdOptions options) { final String defaultTarget = String.format("%s:%s", options.getHost(), options.getPort()); final String targetUri = isValidTargetUri(options.getTargetUri()) ? options.getTargetUri() : defaultTarget; - final NettyChannelBuilder builder = + final NettyChannelBuilder channelBuilder = NettyChannelBuilder.forTarget(targetUri).keepAliveTime(keepAliveMs, TimeUnit.MILLISECONDS); if (options.getDefaultAuthority() != null) { - builder.overrideAuthority(options.getDefaultAuthority()); + channelBuilder.overrideAuthority(options.getDefaultAuthority()); } if (options.getClientInterceptors() != null) { - builder.intercept(options.getClientInterceptors()); + channelBuilder.intercept(options.getClientInterceptors()); } if (options.isTls()) { SslContextBuilder sslContext = GrpcSslContexts.forClient(); @@ -147,17 +151,21 @@ public static ManagedChannel nettyChannel(final FlagdOptions options) { } } - builder.sslContext(sslContext.build()); + channelBuilder.sslContext(sslContext.build()); } else { - builder.usePlaintext(); + channelBuilder.usePlaintext(); } // telemetry interceptor if option is provided if (options.getOpenTelemetry() != null) { - builder.intercept(new FlagdGrpcInterceptor(options.getOpenTelemetry())); + channelBuilder.intercept(new FlagdGrpcInterceptor(options.getOpenTelemetry())); + } + // add header-based selector interceptor if selector is provided + if (options.getSelector() != null) { + channelBuilder.intercept(createSelectorInterceptor(options.getSelector())); } - return builder.defaultServiceConfig(buildRetryPolicy(options)) + return channelBuilder.defaultServiceConfig(buildRetryPolicy(options)) .enableRetry() .build(); } catch (SSLException ssle) { @@ -188,7 +196,7 @@ public ClientCall interceptCall( next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - headers.put(Metadata.Key.of("flagd-selector", Metadata.ASCII_STRING_MARSHALLER), selector); + headers.put(FLAGD_SELECTOR_KEY, selector); super.start(responseListener, headers); } }; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/SyncStreamQueueSource.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/SyncStreamQueueSource.java index f6d72e624..1e2e043d7 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/SyncStreamQueueSource.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/SyncStreamQueueSource.java @@ -16,13 +16,6 @@ import dev.openfeature.flagd.grpc.sync.Sync.SyncFlagsResponse; import dev.openfeature.sdk.Awaitable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.ForwardingClientCall; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; @@ -42,7 +35,6 @@ 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; - private final AtomicBoolean shutdown = new AtomicBoolean(false); private final AtomicBoolean shouldThrottle = new AtomicBoolean(false); private final int streamDeadline; @@ -275,30 +267,6 @@ private void syncFlags(SyncStreamObserver streamObserver) { streamObserver.done.await(); } - /** - * Creates a ClientInterceptor that adds the flagd-selector header to gRPC requests. - * This is the preferred approach for passing selectors as per flagd issue #1814. - * - * @param selector the selector value to pass in the header - * @return a ClientInterceptor that adds the flagd-selector header - */ - private static ClientInterceptor createSelectorInterceptor(String selector) { - return new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, CallOptions callOptions, Channel next) { - return new ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void start(Listener responseListener, Metadata headers) { - headers.put(Metadata.Key.of("flagd-selector", Metadata.ASCII_STRING_MARSHALLER), selector); - super.start(responseListener, headers); - } - }; - } - }; - } - private void enqueueError(String message) { enqueueError(outgoingQueue, message); } From 6ceaa82d072798cefe8de939a369740379303bbc Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 24 Dec 2025 09:29:31 -0500 Subject: [PATCH 08/10] fixup: more spotless Signed-off-by: Todd Baert --- .../providers/flagd/resolver/common/ChannelBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java index 8d1644766..72cda613b 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelBuilder.java @@ -165,7 +165,8 @@ public static ManagedChannel nettyChannel(final FlagdOptions options) { channelBuilder.intercept(createSelectorInterceptor(options.getSelector())); } - return channelBuilder.defaultServiceConfig(buildRetryPolicy(options)) + return channelBuilder + .defaultServiceConfig(buildRetryPolicy(options)) .enableRetry() .build(); } catch (SSLException ssle) { From bde150b7a9bd30a8ffe2b68112e49900dff200e0 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 24 Dec 2025 09:40:45 -0500 Subject: [PATCH 09/10] fixup: flaky test Signed-off-by: Todd Baert --- .../providers/flagd/FlagdProviderSyncResourcesTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java index c7d9672ef..eabf445e0 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java @@ -100,11 +100,13 @@ void callingInitialize_wakesUpWaitingThread() throws InterruptedException { waitingThread.join(); + var wait = MAX_TIME_TOLERANCE * 3; + Assertions.assertTrue( - waitTime.get() < MAX_TIME_TOLERANCE * 2, + waitTime.get() "Wakeup should be almost instant, but took " + waitTime.get() + " ms, which is more than the max of" - + (MAX_TIME_TOLERANCE * 2) + " ms"); + + wait + " ms"); } @Timeout(2) From 1b0603af75ccf7239a188feb3a6e7d078a18a3da Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 24 Dec 2025 09:48:14 -0500 Subject: [PATCH 10/10] fixup: SPOTLESS AGAIN Signed-off-by: Todd Baert --- .../contrib/providers/flagd/FlagdProviderSyncResourcesTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java index eabf445e0..fd7f55111 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java @@ -103,7 +103,7 @@ void callingInitialize_wakesUpWaitingThread() throws InterruptedException { var wait = MAX_TIME_TOLERANCE * 3; Assertions.assertTrue( - waitTime.get() "Wakeup should be almost instant, but took " + waitTime.get() + " ms, which is more than the max of" + wait + " ms");