From 353fdb16748f49f7589246f34e16976288d96725 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 19 Aug 2026 13:41:19 -0400 Subject: [PATCH 1/4] fix: default grace period causes log spam Signed-off-by: Todd Baert --- .../contrib/providers/flagd/Config.java | 4 ++-- .../flagd/resolver/common/ChannelBuilder.java | 4 ++-- .../contrib/providers/flagd/e2e/State.java | 5 ++++- .../providers/flagd/e2e/steps/EventSteps.java | 20 +++++++++++++++---- .../flagd/e2e/steps/ProviderSteps.java | 4 ++-- providers/flagd/test-harness | 2 +- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java index e90699405..1e152ff43 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java @@ -17,9 +17,9 @@ public final class Config { static final String DEFAULT_HOST = "localhost"; static final int DEFAULT_DEADLINE = 500; - static final int DEFAULT_MAX_RETRY_BACKOFF_MS = 12000; + static final int DEFAULT_MAX_RETRY_BACKOFF_MS = 5000; static final int DEFAULT_STREAM_DEADLINE_MS = 10 * 60 * 1000; - static final int DEFAULT_STREAM_RETRY_GRACE_PERIOD = 5; + static final int DEFAULT_STREAM_RETRY_GRACE_PERIOD = 10; static final int DEFAULT_MAX_CACHE_SIZE = 1000; static final int DEFAULT_OFFLINE_POLL_MS = 5000; static final long DEFAULT_KEEP_ALIVE = 0; 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 826ca0b7a..cb202050f 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 @@ -53,8 +53,8 @@ public class ChannelBuilder { Collections.singletonMap("service", "flagd.evaluation.v2.Service"))); put("retryPolicy", new HashMap() { { - // 1 + 2 + 4 - put("maxAttempts", 3.0); // types used here are important, need to be doubles + // total attempts = initial + 3 retries (backoff 1s, 2s, 4s) + put("maxAttempts", 4.0); // types used here are important, need to be doubles put("initialBackoff", "1s"); put( "maxBackoff", diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java index 15f555e46..2c671d542 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java @@ -21,7 +21,10 @@ public class State { /** The container borrowed from {@link ContainerPool} for this scenario. */ public ContainerEntry containerEntry; - public ConcurrentLinkedQueue events = new ConcurrentLinkedQueue<>(); + // events not yet consumed by a positive assertion; drained as they are matched + public ConcurrentLinkedQueue assertedEvents = new ConcurrentLinkedQueue<>(); + // complete log of every event emitted, used for "never fired" assertions + public ConcurrentLinkedQueue allEvents = new ConcurrentLinkedQueue<>(); public Optional lastEvent; public FlagSteps.Flag flag; public MutableContext context = new MutableContext(); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java index 6e8222b19..7bf1502f5 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java @@ -1,6 +1,7 @@ package dev.openfeature.contrib.providers.flagd.e2e.steps; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import dev.openfeature.contrib.providers.flagd.e2e.State; @@ -24,7 +25,9 @@ public EventSteps(State state) { public void a_stale_event_handler(String eventType) { state.client.on(mapEventType(eventType), eventDetails -> { log.info("{} event tracked", eventType); - state.events.add(new Event(eventType, eventDetails)); + Event event = new Event(eventType, eventDetails); + state.assertedEvents.add(event); + state.allEvents.add(event); }); } @@ -53,20 +56,29 @@ public void eventHandlerShouldBeExecuted(String eventType) { eventHandlerShouldBeExecutedWithin(eventType, EVENT_TIMEOUT_MS); } + @Then("the {} event handler should not have been executed") + public void eventHandlerShouldNotHaveBeenExecuted(String eventType) { + // checks the full log, not assertedEvents: preceding positive + // assertions may have drained an intervening event of this type + assertThat(state.allEvents.stream().anyMatch(event -> event.type.equals(eventType))) + .as("no %s event should have fired", eventType) + .isFalse(); + } + @Then("the {} event handler should have been executed within {int}ms") public void eventHandlerShouldBeExecutedWithin(String eventType, int ms) { log.info("waiting for eventtype: {}", eventType); await().alias("waiting for eventtype " + eventType) .atMost(ms, MILLISECONDS) .pollInterval(10, MILLISECONDS) - .until(() -> state.events.stream().anyMatch(event -> event.type.equals(eventType))); + .until(() -> state.assertedEvents.stream().anyMatch(event -> event.type.equals(eventType))); // Drain all events up to and including the first match. This ensures that // older events (e.g. a READY from before a disconnect) cannot satisfy a // later assertion that expects a *new* event of the same type, while still // preserving events that arrived *after* the match for subsequent steps. Event matched = null; - while (!state.events.isEmpty()) { - Event head = state.events.poll(); + while (!state.assertedEvents.isEmpty()) { + Event head = state.assertedEvents.poll(); if (head != null && head.type.equals(eventType)) { matched = head; break; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java index b747672cc..f9e1b846f 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java @@ -72,9 +72,9 @@ public void setupProvider(String providerType) throws InterruptedException { state.builder .deadline(1000) .keepAlive(0) - .retryGracePeriod(2) + .retryGracePeriod(5) .retryBackoffMs(500) - .retryBackoffMaxMs(2000); + .retryBackoffMaxMs(500); boolean wait = true; switch (providerType) { diff --git a/providers/flagd/test-harness b/providers/flagd/test-harness index 7575a1dc4..6dea1da12 160000 --- a/providers/flagd/test-harness +++ b/providers/flagd/test-harness @@ -1 +1 @@ -Subproject commit 7575a1dc45f176e57e809748a712a555e9aa5d11 +Subproject commit 6dea1da12636b9332d9018ae6ff8ed2894742616 From a17c872402e3db4aa28c5aaaeba3869ad8aef6c1 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 19 Aug 2026 15:12:45 -0400 Subject: [PATCH 2/4] fixup: record event history before exposing to consumable queue Signed-off-by: Todd Baert --- .../contrib/providers/flagd/e2e/steps/EventSteps.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java index 7bf1502f5..9c3fd9116 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java @@ -26,8 +26,8 @@ public void a_stale_event_handler(String eventType) { state.client.on(mapEventType(eventType), eventDetails -> { log.info("{} event tracked", eventType); Event event = new Event(eventType, eventDetails); - state.assertedEvents.add(event); state.allEvents.add(event); + state.assertedEvents.add(event); }); } From 64b992eac9b9cb7c7cfdb160ead5a549dfad3c34 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 20 Aug 2026 08:26:15 -0400 Subject: [PATCH 3/4] fixup: bump testbed to v3.10.1, exclude fractional-v3, isolate env-var config scenarios Signed-off-by: Todd Baert --- .../contrib/providers/flagd/e2e/RunFileTest.java | 1 + .../contrib/providers/flagd/e2e/RunInProcessTest.java | 2 +- .../contrib/providers/flagd/e2e/RunRpcTest.java | 2 +- .../providers/flagd/e2e/steps/config/ConfigSteps.java | 1 - .../flagd/src/test/resources/junit-platform.properties | 10 ++++++---- providers/flagd/test-harness | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java index b5a6ffcf4..5901fe08e 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java @@ -36,6 +36,7 @@ "events", "contextEnrichment", "fractional-v1", + "fractional-v3", "deprecated" }) @Testcontainers diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java index 69b0e576e..797756211 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java @@ -28,7 +28,7 @@ @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps") @ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") @IncludeTags("in-process") -@ExcludeTags({"unixsocket", "fractional-v1", "deprecated"}) +@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v3", "deprecated"}) @Testcontainers public class RunInProcessTest { diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java index 16b9ce60a..05850c6e5 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java @@ -28,7 +28,7 @@ @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps") @ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") @IncludeTags({"rpc"}) -@ExcludeTags({"unixsocket", "fractional-v1", "deprecated"}) +@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v3", "deprecated"}) @Testcontainers public class RunRpcTest { diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java index 25a6cdc7d..db5ee14f1 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java @@ -119,7 +119,6 @@ private static String mapOptionNames(String option) { propertyMapper.put("resolver", "resolverType"); propertyMapper.put("deadlineMs", "deadline"); propertyMapper.put("keepAliveTime", "keepAlive"); - propertyMapper.put("retryBackoffMaxMs", "keepAlive"); propertyMapper.put("cache", "cacheType"); if (propertyMapper.get(option) != null) { diff --git a/providers/flagd/src/test/resources/junit-platform.properties b/providers/flagd/src/test/resources/junit-platform.properties index 0d0be24ee..1bc7bda28 100644 --- a/providers/flagd/src/test/resources/junit-platform.properties +++ b/providers/flagd/src/test/resources/junit-platform.properties @@ -8,10 +8,12 @@ cucumber.execution.parallel.enabled=true # Override pool size via -Dflagd.e2e.pool.size=N if needed. cucumber.execution.parallel.config.strategy=dynamic cucumber.execution.parallel.config.dynamic.factor=1 -# Scenarios tagged @env-var mutate System env vars globally. -# Serialise them behind an exclusive resource lock so concurrent scenarios -# don't clobber each other's environment variable state. -cucumber.execution.exclusive-resources.env-var.read-write=ENV_VARS +# Scenarios tagged @env-var mutate System env vars globally, which every config +# scenario reads when building FlagdOptions defaults. A plain ENV_VARS lock only +# serialises @env-var scenarios against each other, so a concurrent default-reading +# scenario could still observe a mutated var. Acquire the JUnit global lock so +# @env-var scenarios run in full isolation (no other scenario runs concurrently). +cucumber.execution.exclusive-resources.env-var.read-write=org.junit.platform.engine.support.hierarchical.ExclusiveResource # Scenarios tagged @grace involve container restart + reconnection timing. # Running two concurrent restarts under parallel load can push the # reconnection past the 12-second EVENT_TIMEOUT_MS threshold. Serialise diff --git a/providers/flagd/test-harness b/providers/flagd/test-harness index 6dea1da12..b308c1bbb 160000 --- a/providers/flagd/test-harness +++ b/providers/flagd/test-harness @@ -1 +1 @@ -Subproject commit 6dea1da12636b9332d9018ae6ff8ed2894742616 +Subproject commit b308c1bbb12fc4511af6220f1b072a20a1a8831f From 34c58ad7ddbe26c45d3350cf729f59d354c0a6b0 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 20 Aug 2026 10:45:36 -0400 Subject: [PATCH 4/4] fixup: use ExclusiveResource.GLOBAL_KEY value for env-var isolation Signed-off-by: Todd Baert --- providers/flagd/src/test/resources/junit-platform.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/flagd/src/test/resources/junit-platform.properties b/providers/flagd/src/test/resources/junit-platform.properties index 1bc7bda28..cc6bbf8f6 100644 --- a/providers/flagd/src/test/resources/junit-platform.properties +++ b/providers/flagd/src/test/resources/junit-platform.properties @@ -13,7 +13,7 @@ cucumber.execution.parallel.config.dynamic.factor=1 # serialises @env-var scenarios against each other, so a concurrent default-reading # scenario could still observe a mutated var. Acquire the JUnit global lock so # @env-var scenarios run in full isolation (no other scenario runs concurrently). -cucumber.execution.exclusive-resources.env-var.read-write=org.junit.platform.engine.support.hierarchical.ExclusiveResource +cucumber.execution.exclusive-resources.env-var.read-write=org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY # Scenarios tagged @grace involve container restart + reconnection timing. # Running two concurrent restarts under parallel load can push the # reconnection past the 12-second EVENT_TIMEOUT_MS threshold. Serialise