From 0f0654fac2bd7d3925d6e39d42e5863dfb448c2a 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 1/7] Initial plan From 0e8b0e63be3a7ecc3ad42265cfe9d9aa923cadf3 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 2/7] Document selector migration guidance for in-process mode Co-authored-by: aepfli <9987394+aepfli@users.noreply.github.com> --- providers/flagd/README.md | 90 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index a3eba9bb8..8f3631b19 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). @@ -119,7 +201,7 @@ Given below are the supported configurations: | 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 | +| 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 | @@ -130,6 +212,9 @@ 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. > ### 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 2287985407de02676c2d48f4068eca139913f8df 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 3/7] 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 a2463a946..4c972ff2c 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 48cb95b48a352e92d10c58bf000c63b7ef5fbe3d 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 4/7] Implement header-based selector for sync stream connection Co-authored-by: aepfli <9987394+aepfli@users.noreply.github.com> --- .../connector/sync/SyncStreamQueueSource.java | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) 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 d68adaeae..251fe4bb5 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 @@ -17,6 +17,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; @@ -60,8 +67,14 @@ public SyncStreamQueueSource(final FlagdOptions options, Consumer 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 388f82fea06146399bb18a0d7afd947378df9743 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 5/7] Update documentation to reflect header-based implementation Co-authored-by: aepfli <9987394+aepfli@users.noreply.github.com> --- providers/flagd/README.md | 77 +++++-------------- .../contrib/providers/flagd/FlagdOptions.java | 9 +-- 2 files changed, 24 insertions(+), 62 deletions(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index 8f3631b19..78fcf207b 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). @@ -201,7 +164,7 @@ Given below are the supported configurations: | 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)) | +| selector | FLAGD_SOURCE_SELECTOR | String | null | in-process (see [details](#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 | @@ -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 4c972ff2c..0dc92db78 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 d9f840f2b491f15e48df47d77eea927da469a749 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 6/7] 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 0dc92db78..afe4fee97 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 2f7a4314eb5f12b593b48227e4a87fc28ce05ae5 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Thu, 6 Nov 2025 13:01:23 +0100 Subject: [PATCH 7/7] fixup: spotless and gemini feedback Signed-off-by: Simon Schrottner --- providers/flagd/README.md | 5 ----- .../connector/sync/SyncStreamQueueSource.java | 19 ++++++++----------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index 78fcf207b..2bbde68e9 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/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 251fe4bb5..802c03547 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 @@ -43,6 +43,8 @@ justification = "Random is used to generate a variation & flag configurations require exposing") public class SyncStreamQueueSource implements QueueSource { private static final int QUEUE_SIZE = 5; + private static final Metadata.Key FLAGD_SELECTOR_KEY = + Metadata.Key.of("flagd-selector", Metadata.ASCII_STRING_MARSHALLER); private final AtomicBoolean shutdown = new AtomicBoolean(false); private final int streamDeadline; @@ -67,16 +69,15 @@ public SyncStreamQueueSource(final FlagdOptions options, Consumer ClientCall interceptCall( - MethodDescriptor method, - CallOptions callOptions, - Channel next) { + 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); + headers.put(FLAGD_SELECTOR_KEY, selector); super.start(responseListener, headers); } };