From b55056da3e12015d9be910cca1cb345600e43a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 22 Nov 2023 10:24:52 +0100 Subject: [PATCH 01/34] chore: allow to fail if unavailable (#225) --- .../repository/FeatureRepository.java | 23 +- .../io/getunleash/DefaultUnleashTest.java | 74 +++ .../repository/FeatureRepositoryTest.java | 420 +++++++----------- 3 files changed, 245 insertions(+), 272 deletions(-) diff --git a/src/main/java/io/getunleash/repository/FeatureRepository.java b/src/main/java/io/getunleash/repository/FeatureRepository.java index 611db85b3..36e54c33a 100644 --- a/src/main/java/io/getunleash/repository/FeatureRepository.java +++ b/src/main/java/io/getunleash/repository/FeatureRepository.java @@ -95,7 +95,9 @@ private void initCollections(UnleashScheduledExecutor executor) { } if (unleashConfig.isSynchronousFetchOnInitialisation()) { - updateFeatures(null).run(); + updateFeatures(e -> { + throw e; + }).run(); } if (!unleashConfig.isDisablePolling()) { @@ -108,11 +110,7 @@ private void initCollections(UnleashScheduledExecutor executor) { } } - private Integer calculateMaxSkips(int fetchTogglesInterval) { - return Integer.max(20, 300 / Integer.max(fetchTogglesInterval, 1)); - } - - private Runnable updateFeatures(@Nullable final Consumer handler) { + private Runnable updateFeatures(final Consumer handler) { return () -> { if (throttler.performAction()) { try { @@ -129,7 +127,12 @@ private Runnable updateFeatures(@Nullable final Consumer handl featureBackupHandler.write(featureCollection); } else if (response.getStatus() == ClientFeaturesResponse.Status.UNAVAILABLE) { - throttler.handleHttpErrorCodes(response.getHttpStatusCode()); + if (!ready && unleashConfig.isSynchronousFetchOnInitialisation()) { + throw new UnleashException(String.format("Could not initialize Unleash, got response code %d", response.getHttpStatusCode()), null); + } + if (ready) { + throttler.handleHttpErrorCodes(response.getHttpStatusCode()); + } return; } throttler.decrementFailureCountAndResetSkips(); @@ -138,11 +141,7 @@ private Runnable updateFeatures(@Nullable final Consumer handl ready = true; } } catch (UnleashException e) { - if (handler != null) { - handler.accept(e); - } else { - throw e; - } + handler.accept(e); } } else { throttler.skipped(); // We didn't do anything this iteration, just reduce the count diff --git a/src/test/java/io/getunleash/DefaultUnleashTest.java b/src/test/java/io/getunleash/DefaultUnleashTest.java index 44f329272..d60e1ce19 100644 --- a/src/test/java/io/getunleash/DefaultUnleashTest.java +++ b/src/test/java/io/getunleash/DefaultUnleashTest.java @@ -1,5 +1,7 @@ package io.getunleash; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -8,15 +10,23 @@ import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import io.getunleash.event.EventDispatcher; +import io.getunleash.event.UnleashReady; +import io.getunleash.event.UnleashSubscriber; +import io.getunleash.integration.TestDefinition; import io.getunleash.metric.UnleashMetricService; import io.getunleash.repository.*; import io.getunleash.strategy.DefaultStrategy; import io.getunleash.strategy.Strategy; import io.getunleash.util.UnleashConfig; + +import java.net.URI; +import java.net.URISyntaxException; import java.util.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.LoggerFactory; class DefaultUnleashTest { @@ -26,6 +36,13 @@ class DefaultUnleashTest { private EventDispatcher eventDispatcher; private UnleashMetricService metricService; + @RegisterExtension + static WireMockExtension serverMock = + WireMockExtension.newInstance() + .configureStaticDsl(true) + .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) + .build(); + @BeforeEach public void setup() { UnleashConfig unleashConfig = @@ -222,6 +239,7 @@ public void supports_failing_hard_on_multiple_instantiations() { @Test public void synchronous_fetch_on_initialisation_fails_on_initialization() { + IsReadyTestSubscriber readySubscriber = new IsReadyTestSubscriber(); UnleashConfig config = UnleashConfig.builder() .unleashAPI("http://wrong:4242") @@ -229,9 +247,58 @@ public void synchronous_fetch_on_initialisation_fails_on_initialization() { .apiKey("default:development:1234567890123456") .instanceId("multiple_connection_exception") .synchronousFetchOnInitialisation(true) + .subscriber(readySubscriber) .build(); assertThatThrownBy(() -> new DefaultUnleash(config)).isInstanceOf(UnleashException.class); + assertThat(readySubscriber.ready).isFalse(); + } + + @Test + public void synchronous_fetch_on_initialisation_fails_on_non_200_response() throws URISyntaxException { + mockUnleashAPI(401); + IsReadyTestSubscriber readySubscriber = new IsReadyTestSubscriber(); + UnleashConfig config = + UnleashConfig.builder() + .unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/")) + .appName("wrong_upstream") + .apiKey("default:development:1234567890123456") + .instanceId("non-200") + .synchronousFetchOnInitialisation(true) + .subscriber(readySubscriber) + .build(); + + assertThatThrownBy(() -> new DefaultUnleash(config)).isInstanceOf(UnleashException.class); + assertThat(readySubscriber.ready).isFalse(); + } + + @Test + public void synchronous_fetch_on_initialisation_switches_to_ready_on_200() throws URISyntaxException { + mockUnleashAPI(200); + IsReadyTestSubscriber readySubscriber = new IsReadyTestSubscriber(); + UnleashConfig config = + UnleashConfig.builder() + .unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/")) + .appName("wrong_upstream") + .apiKey("default:development:1234567890123456") + .instanceId("with-success-response") + .synchronousFetchOnInitialisation(true) + .subscriber(readySubscriber) + .build(); + new DefaultUnleash(config); + assertThat(readySubscriber.ready).isTrue(); + } + + private void mockUnleashAPI(int featuresStatusCode) { + stubFor( + get(urlEqualTo("/api/client/features")) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(featuresStatusCode) + .withHeader("Content-Type", "application/json") + .withBody("{\"features\": []}"))); + stubFor(post(urlEqualTo("/api/client/register")).willReturn(aResponse().withStatus(200))); } @Test @@ -271,4 +338,11 @@ public void client_identifier_handles_api_key_being_null() { assertThat(id) .isEqualTo("f83eb743f4c8dc41294aafb96f454763e5a90b96db8b7040ddc505d636bdb243"); } + + private static class IsReadyTestSubscriber implements UnleashSubscriber { + public boolean ready = false; + public void onReady(UnleashReady unleashReady) { + this.ready = true; + } + } } diff --git a/src/test/java/io/getunleash/repository/FeatureRepositoryTest.java b/src/test/java/io/getunleash/repository/FeatureRepositoryTest.java index ae3615133..c0641ef3e 100644 --- a/src/test/java/io/getunleash/repository/FeatureRepositoryTest.java +++ b/src/test/java/io/getunleash/repository/FeatureRepositoryTest.java @@ -1,14 +1,17 @@ package io.getunleash.repository; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - import io.getunleash.*; import io.getunleash.event.EventDispatcher; import io.getunleash.lang.Nullable; import io.getunleash.util.UnleashConfig; import io.getunleash.util.UnleashScheduledExecutor; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + import java.io.File; import java.io.IOException; import java.net.URISyntaxException; @@ -18,9 +21,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; + +import static io.getunleash.repository.FeatureToggleResponse.Status.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; public class FeatureRepositoryTest { FeatureBackupHandlerFile backupHandler; @@ -99,26 +104,8 @@ public void feature_toggles_should_be_updated() { .synchronousFetchOnInitialisation(false) .build(); - FeatureCollection featureCollection = - populatedFeatureCollection( - null, - new FeatureToggle( - "toggleFetcherCalled", - false, - Arrays.asList(new ActivationStrategy("custom", null)))); + when(backupHandler.read()).thenReturn(simpleFeatureCollection(false)); - when(backupHandler.read()).thenReturn(featureCollection); - - featureCollection = - populatedFeatureCollection( - null, - new FeatureToggle( - "toggleFetcherCalled", - true, - Arrays.asList(new ActivationStrategy("custom", null)))); - ClientFeaturesResponse response = - new ClientFeaturesResponse( - ClientFeaturesResponse.Status.CHANGED, featureCollection); FeatureRepository featureRepository = new FeatureRepository(config, backupHandler, executor, fetcher, bootstrapHandler); @@ -126,6 +113,7 @@ public void feature_toggles_should_be_updated() { verify(executor).setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong()); verify(fetcher, times(0)).fetchFeatures(); + ClientFeaturesResponse response = new ClientFeaturesResponse(CHANGED, simpleFeatureCollection(true)); when(fetcher.fetchFeatures()).thenReturn(response); runnableArgumentCaptor.getValue().run(); @@ -174,9 +162,7 @@ public void should_perform_synchronous_fetch_on_initialisation() { when(backupHandler.read()).thenReturn(new FeatureCollection()); FeatureCollection featureCollection = populatedFeatureCollection(null); - ClientFeaturesResponse response = - new ClientFeaturesResponse( - ClientFeaturesResponse.Status.CHANGED, featureCollection); + ClientFeaturesResponse response = new ClientFeaturesResponse(CHANGED, featureCollection); when(fetcher.fetchFeatures()).thenReturn(response); new FeatureRepository( @@ -200,7 +186,7 @@ public void should_not_perform_synchronous_fetch_on_initialisation() { FeatureCollection featureCollection = populatedFeatureCollection(null); ClientFeaturesResponse response = new ClientFeaturesResponse( - ClientFeaturesResponse.Status.CHANGED, featureCollection); + CHANGED, featureCollection); when(fetcher.fetchFeatures()).thenReturn(response); @@ -266,37 +252,18 @@ public void should_not_read_bootstrap_if_backup_was_found() when(backupHandler.read()) .thenReturn( - populatedFeatureCollection( - Arrays.asList( - new Segment( - 1, - "some-name", - Arrays.asList( - new Constraint( - "some-context", - Operator.IN, - "some-value")))), - new FeatureToggle( - "toggleFeatureName1", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))), - new FeatureToggle( - "toggleFeatureName2", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))))); + getFeatureCollection()); new FeatureRepository( config, backupHandler, new EventDispatcher(config), fetcher, bootstrapHandler); verify(toggleBootstrapProvider, times(0)).read(); } - @Test - public void should_increase_to_max_interval_when_denied() + @ParameterizedTest + @ValueSource(ints = {403, 404}) + public void should_increase_to_max_interval_when_code(int code) throws URISyntaxException, IOException { - UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); - ArgumentCaptor runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + TestRunner runner = new TestRunner(); File file = new File(getClass().getClassLoader().getResource("unleash-repo-v2.json").toURI()); ToggleBootstrapProvider toggleBootstrapProvider = mock(ToggleBootstrapProvider.class); @@ -305,36 +272,10 @@ public void should_increase_to_max_interval_when_denied() UnleashConfig.builder() .synchronousFetchOnInitialisation(false) .appName("test-sync-update") - .scheduledExecutor(executor) + .scheduledExecutor(runner.executor) .unleashAPI("http://localhost:8080") .build(); - when(backupHandler.read()) - .thenReturn( - populatedFeatureCollection( - Arrays.asList( - new Segment( - 1, - "some-name", - Arrays.asList( - new Constraint( - "some-context", - Operator.IN, - "some-value")))), - new FeatureToggle( - "toggleFeatureName1", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))), - new FeatureToggle( - "toggleFeatureName2", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))))); - when(fetcher.fetchFeatures()) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.UNAVAILABLE, 403)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)); + when(backupHandler.read()).thenReturn(getFeatureCollection()); FeatureRepository featureRepository = new FeatureRepository( @@ -343,25 +284,26 @@ public void should_increase_to_max_interval_when_denied() new EventDispatcher(config), fetcher, bootstrapHandler); - verify(executor).setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong()); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatFetchesAndReceives(CHANGED, 200); // set it ready + + runner.assertThatFetchesAndReceives(UNAVAILABLE, code); assertThat(featureRepository.getFailures()).isEqualTo(1); assertThat(featureRepository.getSkips()).isEqualTo(30); for (int i = 0; i < 30; i++) { - runnableArgumentCaptor.getValue().run(); + runner.assertThatSkipsNextRun(); } assertThat(featureRepository.getFailures()).isEqualTo(1); assertThat(featureRepository.getSkips()).isEqualTo(0); - runnableArgumentCaptor.getValue().run(); + runner.assertThatFetchesAndReceives(NOT_CHANGED, 304); assertThat(featureRepository.getFailures()).isEqualTo(0); assertThat(featureRepository.getSkips()).isEqualTo(0); } @Test - public void should_increase_to_max_interval_when_not_found() + public void should_incrementally_increase_interval_as_we_receive_too_many_requests() throws URISyntaxException, IOException { - UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); - ArgumentCaptor runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + TestRunner runner = new TestRunner(); File file = new File(getClass().getClassLoader().getResource("unleash-repo-v2.json").toURI()); ToggleBootstrapProvider toggleBootstrapProvider = mock(ToggleBootstrapProvider.class); @@ -370,37 +312,10 @@ public void should_increase_to_max_interval_when_not_found() UnleashConfig.builder() .synchronousFetchOnInitialisation(false) .appName("test-sync-update") - .scheduledExecutor(executor) + .scheduledExecutor(runner.executor) .unleashAPI("http://localhost:8080") .build(); - when(backupHandler.read()) - .thenReturn( - populatedFeatureCollection( - Arrays.asList( - new Segment( - 1, - "some-name", - Arrays.asList( - new Constraint( - "some-context", - Operator.IN, - "some-value")))), - new FeatureToggle( - "toggleFeatureName1", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))), - new FeatureToggle( - "toggleFeatureName2", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))))); - when(fetcher.fetchFeatures()) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.UNAVAILABLE, 404)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)); - + when(backupHandler.read()).thenReturn(getFeatureCollection()); FeatureRepository featureRepository = new FeatureRepository( config, @@ -408,116 +323,67 @@ public void should_increase_to_max_interval_when_not_found() new EventDispatcher(config), fetcher, bootstrapHandler); - verify(executor).setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong()); - runnableArgumentCaptor.getValue().run(); - assertThat(featureRepository.getFailures()).isEqualTo(1); - assertThat(featureRepository.getSkips()).isEqualTo(30); - for (int i = 0; i < 30; i++) { - runnableArgumentCaptor.getValue().run(); - } - assertThat(featureRepository.getFailures()).isEqualTo(1); + + runner.assertThatFetchesAndReceives(UNAVAILABLE, 429); + // client is not ready don't count errors or skips assertThat(featureRepository.getSkips()).isEqualTo(0); - runnableArgumentCaptor.getValue().run(); assertThat(featureRepository.getFailures()).isEqualTo(0); + + runner.assertThatFetchesAndReceives(UNAVAILABLE, 429); + // client is not ready don't count errors or skips assertThat(featureRepository.getSkips()).isEqualTo(0); - } + assertThat(featureRepository.getFailures()).isEqualTo(0); - @Test - public void should_incrementally_increase_interval_as_we_receive_too_many_requests() - throws URISyntaxException, IOException { - UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); - ArgumentCaptor runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); - File file = - new File(getClass().getClassLoader().getResource("unleash-repo-v2.json").toURI()); - ToggleBootstrapProvider toggleBootstrapProvider = mock(ToggleBootstrapProvider.class); - when(toggleBootstrapProvider.read()).thenReturn(fileToString(file)); - UnleashConfig config = - UnleashConfig.builder() - .synchronousFetchOnInitialisation(false) - .appName("test-sync-update") - .scheduledExecutor(executor) - .unleashAPI("http://localhost:8080") - .build(); - when(backupHandler.read()) - .thenReturn( - populatedFeatureCollection( - Arrays.asList( - new Segment( - 1, - "some-name", - Arrays.asList( - new Constraint( - "some-context", - Operator.IN, - "some-value")))), - new FeatureToggle( - "toggleFeatureName1", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))), - new FeatureToggle( - "toggleFeatureName2", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))))); - FeatureRepository featureRepository = - new FeatureRepository( - config, - backupHandler, - new EventDispatcher(config), - fetcher, - bootstrapHandler); - verify(executor).setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong()); - when(fetcher.fetchFeatures()) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.UNAVAILABLE, 429)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.UNAVAILABLE, 429)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.UNAVAILABLE, 429)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)); - runnableArgumentCaptor.getValue().run(); + // this changes the client to ready + runner.assertThatFetchesAndReceives(CHANGED, 200); + assertThat(featureRepository.getSkips()).isEqualTo(0); + + runner.assertThatFetchesAndReceives(UNAVAILABLE, 429); assertThat(featureRepository.getSkips()).isEqualTo(1); assertThat(featureRepository.getFailures()).isEqualTo(1); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(1); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatFetchesAndReceives(UNAVAILABLE, 429); assertThat(featureRepository.getSkips()).isEqualTo(2); assertThat(featureRepository.getFailures()).isEqualTo(2); - runnableArgumentCaptor.getValue().run(); // NO-OP because interval > 0 - runnableArgumentCaptor.getValue().run(); // NO-OP because interval > 0 + + runner.assertThatSkipsNextRun(); + assertThat(featureRepository.getSkips()).isEqualTo(1); + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(2); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatFetchesAndReceives(UNAVAILABLE, 429); assertThat(featureRepository.getSkips()).isEqualTo(3); assertThat(featureRepository.getFailures()).isEqualTo(3); - runnableArgumentCaptor.getValue().run(); - runnableArgumentCaptor.getValue().run(); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatSkipsNextRun(); + runner.assertThatSkipsNextRun(); + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(3); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatFetchesAndReceives(NOT_CHANGED, 304); assertThat(featureRepository.getSkips()).isEqualTo(2); assertThat(featureRepository.getFailures()).isEqualTo(2); - runnableArgumentCaptor.getValue().run(); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatSkipsNextRun(); + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(2); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatFetchesAndReceives(NOT_CHANGED, 304); assertThat(featureRepository.getSkips()).isEqualTo(1); assertThat(featureRepository.getFailures()).isEqualTo(1); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(1); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatFetchesAndReceives(NOT_CHANGED, 304); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(0); } @@ -525,8 +391,7 @@ public void should_incrementally_increase_interval_as_we_receive_too_many_reques @Test public void server_errors_should_incrementally_increase_interval() throws URISyntaxException, IOException { - UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); - ArgumentCaptor runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + TestRunner runner = new TestRunner(); File file = new File(getClass().getClassLoader().getResource("unleash-repo-v2.json").toURI()); ToggleBootstrapProvider toggleBootstrapProvider = mock(ToggleBootstrapProvider.class); @@ -535,31 +400,10 @@ public void server_errors_should_incrementally_increase_interval() UnleashConfig.builder() .synchronousFetchOnInitialisation(false) .appName("test-sync-update") - .scheduledExecutor(executor) + .scheduledExecutor(runner.executor) .unleashAPI("http://localhost:8080") .build(); - when(backupHandler.read()) - .thenReturn( - populatedFeatureCollection( - Arrays.asList( - new Segment( - 1, - "some-name", - Arrays.asList( - new Constraint( - "some-context", - Operator.IN, - "some-value")))), - new FeatureToggle( - "toggleFeatureName1", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))), - new FeatureToggle( - "toggleFeatureName2", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))))); + when(backupHandler.read()).thenReturn(getFeatureCollection()); FeatureRepository featureRepository = new FeatureRepository( config, @@ -567,57 +411,44 @@ public void server_errors_should_incrementally_increase_interval() new EventDispatcher(config), fetcher, bootstrapHandler); - verify(executor).setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong()); - when(fetcher.fetchFeatures()) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.UNAVAILABLE, 500)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.UNAVAILABLE, 502)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.UNAVAILABLE, 503)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)) - .thenReturn( - new ClientFeaturesResponse(FeatureToggleResponse.Status.NOT_CHANGED, 304)); - runnableArgumentCaptor.getValue().run(); + + runner.assertThatFetchesAndReceives(CHANGED, 200); // set it ready + + runner.assertThatFetchesAndReceives(UNAVAILABLE, 500); assertThat(featureRepository.getSkips()).isEqualTo(1); assertThat(featureRepository.getFailures()).isEqualTo(1); - runnableArgumentCaptor.getValue().run(); + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(1); - runnableArgumentCaptor.getValue().run(); + runner.assertThatFetchesAndReceives(UNAVAILABLE, 502); assertThat(featureRepository.getSkips()).isEqualTo(2); assertThat(featureRepository.getFailures()).isEqualTo(2); - runnableArgumentCaptor.getValue().run(); // NO-OP because interval > 0 - runnableArgumentCaptor.getValue().run(); // NO-OP because interval > 0 + runner.assertThatSkipsNextRun(); + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(2); - runnableArgumentCaptor.getValue().run(); + runner.assertThatFetchesAndReceives(UNAVAILABLE, 503); assertThat(featureRepository.getSkips()).isEqualTo(3); assertThat(featureRepository.getFailures()).isEqualTo(3); - runnableArgumentCaptor.getValue().run(); - runnableArgumentCaptor.getValue().run(); - runnableArgumentCaptor.getValue().run(); + runner.assertThatSkipsNextRun(); + runner.assertThatSkipsNextRun(); + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(3); - runnableArgumentCaptor.getValue().run(); + runner.assertThatFetchesAndReceives(NOT_CHANGED, 304); assertThat(featureRepository.getSkips()).isEqualTo(2); assertThat(featureRepository.getFailures()).isEqualTo(2); - runnableArgumentCaptor.getValue().run(); - runnableArgumentCaptor.getValue().run(); + runner.assertThatSkipsNextRun(); + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(2); - runnableArgumentCaptor.getValue().run(); + runner.assertThatFetchesAndReceives(NOT_CHANGED, 304); assertThat(featureRepository.getSkips()).isEqualTo(1); assertThat(featureRepository.getFailures()).isEqualTo(1); - runnableArgumentCaptor.getValue().run(); + runner.assertThatSkipsNextRun(); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(1); - runnableArgumentCaptor.getValue().run(); + runner.assertThatFetchesAndReceives(NOT_CHANGED, 304); assertThat(featureRepository.getSkips()).isEqualTo(0); assertThat(featureRepository.getFailures()).isEqualTo(0); } @@ -625,4 +456,73 @@ public void server_errors_should_incrementally_increase_interval() private String fileToString(File f) throws IOException { return new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8); } + + @NotNull + private FeatureCollection simpleFeatureCollection(boolean enabled) { + return populatedFeatureCollection( + null, + new FeatureToggle( + "toggleFetcherCalled", + enabled, + Arrays.asList(new ActivationStrategy("custom", null)))); + } + + @NotNull + private FeatureCollection getFeatureCollection() { + return populatedFeatureCollection( + Arrays.asList( + new Segment( + 1, + "some-name", + Arrays.asList( + new Constraint( + "some-context", + Operator.IN, + "some-value")))), + new FeatureToggle( + "toggleFeatureName1", + true, + Collections.singletonList( + new ActivationStrategy("custom", null))), + new FeatureToggle( + "toggleFeatureName2", + true, + Collections.singletonList( + new ActivationStrategy("custom", null)))); + } + + private class TestRunner { + + private final UnleashScheduledExecutor executor; + private final ArgumentCaptor runnableArgumentCaptor; + private int count = 0; + + private boolean initialized = false; + + public TestRunner() { + this.executor = mock(UnleashScheduledExecutor.class); + this.runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + } + + private void ensureInitialized() { + if (!initialized) { + verify(executor).setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong()); + initialized = true; + } + } + + public void assertThatFetchesAndReceives(FeatureToggleResponse.Status status, int statusCode) { + ensureInitialized(); + when(fetcher.fetchFeatures()) + .thenReturn(new ClientFeaturesResponse(status, statusCode)); + runnableArgumentCaptor.getValue().run(); + verify(fetcher, times(++count)).fetchFeatures(); + } + + public void assertThatSkipsNextRun() { + ensureInitialized(); + runnableArgumentCaptor.getValue().run(); + verify(fetcher, times(count)).fetchFeatures(); + } + } } From 675f9e85e5e95b4cd50fbb3c948bd9d0589854a4 Mon Sep 17 00:00:00 2001 From: Github Release Bot <> Date: Wed, 22 Nov 2023 10:30:00 +0000 Subject: [PATCH 02/34] [maven-release-plugin] prepare release unleash-client-java-9.1.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 20d9bfe35..ed062de20 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.1.1-SNAPSHOT + 9.1.1 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-8.4.0 + unleash-client-java-9.1.1 From 4e8f5ad728be95392ae258e37f6730fb0048dd6d Mon Sep 17 00:00:00 2001 From: Github Release Bot <> Date: Wed, 22 Nov 2023 10:30:01 +0000 Subject: [PATCH 03/34] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ed062de20..791f10a91 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.1.1 + 9.1.2-SNAPSHOT 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-9.1.1 + unleash-client-java-8.4.0 From db65acb979cb25cfb4de5efc8b8c0108d8e66eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Thu, 23 Nov 2023 13:37:52 +0100 Subject: [PATCH 04/34] chore: amend release generation (#226) --- .github/workflows/release_changelog.yml | 10 ++++------ src/test/java/io/getunleash/DefaultUnleashTest.java | 9 ++++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release_changelog.yml b/.github/workflows/release_changelog.yml index e32b7ae4d..1d8acdf00 100644 --- a/.github/workflows/release_changelog.yml +++ b/.github/workflows/release_changelog.yml @@ -10,17 +10,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build changelog id: github_release - uses: metcalfc/changelog-generator@v0.4.4 + uses: metcalfc/changelog-generator@v4.2.0 with: myToken: ${{ secrets.GITHUB_TOKEN }} - name: Create release - uses: actions/create-release@v1 + uses: ncipollo/release-action@v1 with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} body: ${{ steps.github_release.outputs.changelog }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN}} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN}} diff --git a/src/test/java/io/getunleash/DefaultUnleashTest.java b/src/test/java/io/getunleash/DefaultUnleashTest.java index d60e1ce19..d0bb1328c 100644 --- a/src/test/java/io/getunleash/DefaultUnleashTest.java +++ b/src/test/java/io/getunleash/DefaultUnleashTest.java @@ -27,6 +27,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.LoggerFactory; class DefaultUnleashTest { @@ -254,9 +256,10 @@ public void synchronous_fetch_on_initialisation_fails_on_initialization() { assertThat(readySubscriber.ready).isFalse(); } - @Test - public void synchronous_fetch_on_initialisation_fails_on_non_200_response() throws URISyntaxException { - mockUnleashAPI(401); + @ParameterizedTest + @ValueSource(ints = {401, 403, 404, 500}) + public void synchronous_fetch_on_initialisation_fails_on_non_200_response(int code) throws URISyntaxException { + mockUnleashAPI(code); IsReadyTestSubscriber readySubscriber = new IsReadyTestSubscriber(); UnleashConfig config = UnleashConfig.builder() From beff1b89793f1f9555c68c556d6a52d2ef92db6d Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 27 Nov 2023 09:10:22 +0100 Subject: [PATCH 05/34] feat: Added the possibility to add a custom startupExceptionHandler (#227) --- .../repository/FeatureRepository.java | 18 ++- .../io/getunleash/util/UnleashConfig.java | 43 +++++-- .../io/getunleash/DefaultUnleashTest.java | 63 +++++----- .../repository/FeatureRepositoryTest.java | 110 ++++++++++-------- 4 files changed, 141 insertions(+), 93 deletions(-) diff --git a/src/main/java/io/getunleash/repository/FeatureRepository.java b/src/main/java/io/getunleash/repository/FeatureRepository.java index 36e54c33a..dfeb8a61a 100644 --- a/src/main/java/io/getunleash/repository/FeatureRepository.java +++ b/src/main/java/io/getunleash/repository/FeatureRepository.java @@ -95,9 +95,15 @@ private void initCollections(UnleashScheduledExecutor executor) { } if (unleashConfig.isSynchronousFetchOnInitialisation()) { - updateFeatures(e -> { - throw e; - }).run(); + if (this.unleashConfig.getStartupExceptionHandler() != null) { + updateFeatures(this.unleashConfig.getStartupExceptionHandler()).run(); + } else { + updateFeatures( + e -> { + throw e; + }) + .run(); + } } if (!unleashConfig.isDisablePolling()) { @@ -128,7 +134,11 @@ private Runnable updateFeatures(final Consumer handler) { featureBackupHandler.write(featureCollection); } else if (response.getStatus() == ClientFeaturesResponse.Status.UNAVAILABLE) { if (!ready && unleashConfig.isSynchronousFetchOnInitialisation()) { - throw new UnleashException(String.format("Could not initialize Unleash, got response code %d", response.getHttpStatusCode()), null); + throw new UnleashException( + String.format( + "Could not initialize Unleash, got response code %d", + response.getHttpStatusCode()), + null); } if (ready) { throttler.handleHttpErrorCodes(response.getHttpStatusCode()); diff --git a/src/main/java/io/getunleash/util/UnleashConfig.java b/src/main/java/io/getunleash/util/UnleashConfig.java index 53428b1cc..601200b82 100644 --- a/src/main/java/io/getunleash/util/UnleashConfig.java +++ b/src/main/java/io/getunleash/util/UnleashConfig.java @@ -5,6 +5,7 @@ import io.getunleash.CustomHttpHeadersProvider; import io.getunleash.DefaultCustomHttpHeadersProviderImpl; import io.getunleash.UnleashContextProvider; +import io.getunleash.UnleashException; import io.getunleash.event.NoOpSubscriber; import io.getunleash.event.UnleashSubscriber; import io.getunleash.lang.Nullable; @@ -14,19 +15,15 @@ import io.getunleash.strategy.Strategy; import java.io.File; import java.math.BigInteger; -import java.net.Authenticator; -import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.PasswordAuthentication; -import java.net.Proxy; -import java.net.URI; -import java.net.UnknownHostException; +import java.net.*; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; public class UnleashConfig { @@ -71,6 +68,7 @@ public class UnleashConfig { @Nullable private final Strategy fallbackStrategy; @Nullable private final ToggleBootstrapProvider toggleBootstrapProvider; @Nullable private final Proxy proxy; + @Nullable private final Consumer startupExceptionHandler; private UnleashConfig( @Nullable URI unleashAPI, @@ -101,7 +99,8 @@ private UnleashConfig( @Nullable Strategy fallbackStrategy, @Nullable ToggleBootstrapProvider unleashBootstrapProvider, @Nullable Proxy proxy, - @Nullable Authenticator proxyAuthenticator) { + @Nullable Authenticator proxyAuthenticator, + @Nullable Consumer startupExceptionHandler) { if (appName == null) { throw new IllegalStateException("You are required to specify the unleash appName"); @@ -165,6 +164,7 @@ private UnleashConfig( this.metricSenderFactory = metricSenderFactory; this.clientSpecificationVersion = UnleashProperties.getProperty("client.specification.version"); + this.startupExceptionHandler = startupExceptionHandler; } public static Builder builder() { @@ -334,6 +334,11 @@ public UnleashFeatureFetcherFactory getUnleashFeatureFetcherFactory() { return this.unleashFeatureFetcherFactory; } + @Nullable + public Consumer getStartupExceptionHandler() { + return startupExceptionHandler; + } + static class SystemProxyAuthenticator extends Authenticator { @Override protected @Nullable PasswordAuthentication getPasswordAuthentication() { @@ -427,6 +432,8 @@ public static class Builder { private @Nullable Proxy proxy; private @Nullable Authenticator proxyAuthenticator; + private @Nullable Consumer startupExceptionHandler; + private static String getHostname() { String hostName = System.getProperty("hostname"); if (hostName == null || hostName.isEmpty()) { @@ -657,6 +664,19 @@ public Builder apiKey(String apiKey) { return this; } + /** + * Used to handle exceptions when starting up synchronously. Allows user the option to + * choose how errors should be handled. + * + * @param startupExceptionHandler - a lambda taking the Exception and doing what it wants to + * the system. + */ + public Builder startupExceptionHandler( + @Nullable Consumer startupExceptionHandler) { + this.startupExceptionHandler = startupExceptionHandler; + return this; + } + public UnleashConfig build() { return new UnleashConfig( unleashAPI, @@ -688,7 +708,8 @@ public UnleashConfig build() { fallbackStrategy, toggleBootstrapProvider, proxy, - proxyAuthenticator); + proxyAuthenticator, + startupExceptionHandler); } public String getDefaultSdkVersion() { diff --git a/src/test/java/io/getunleash/DefaultUnleashTest.java b/src/test/java/io/getunleash/DefaultUnleashTest.java index d0bb1328c..beebaa196 100644 --- a/src/test/java/io/getunleash/DefaultUnleashTest.java +++ b/src/test/java/io/getunleash/DefaultUnleashTest.java @@ -14,13 +14,11 @@ import io.getunleash.event.EventDispatcher; import io.getunleash.event.UnleashReady; import io.getunleash.event.UnleashSubscriber; -import io.getunleash.integration.TestDefinition; import io.getunleash.metric.UnleashMetricService; import io.getunleash.repository.*; import io.getunleash.strategy.DefaultStrategy; import io.getunleash.strategy.Strategy; import io.getunleash.util.UnleashConfig; - import java.net.URI; import java.net.URISyntaxException; import java.util.*; @@ -40,10 +38,10 @@ class DefaultUnleashTest { @RegisterExtension static WireMockExtension serverMock = - WireMockExtension.newInstance() - .configureStaticDsl(true) - .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) - .build(); + WireMockExtension.newInstance() + .configureStaticDsl(true) + .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) + .build(); @BeforeEach public void setup() { @@ -258,49 +256,51 @@ public void synchronous_fetch_on_initialisation_fails_on_initialization() { @ParameterizedTest @ValueSource(ints = {401, 403, 404, 500}) - public void synchronous_fetch_on_initialisation_fails_on_non_200_response(int code) throws URISyntaxException { + public void synchronous_fetch_on_initialisation_fails_on_non_200_response(int code) + throws URISyntaxException { mockUnleashAPI(code); IsReadyTestSubscriber readySubscriber = new IsReadyTestSubscriber(); UnleashConfig config = - UnleashConfig.builder() - .unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/")) - .appName("wrong_upstream") - .apiKey("default:development:1234567890123456") - .instanceId("non-200") - .synchronousFetchOnInitialisation(true) - .subscriber(readySubscriber) - .build(); + UnleashConfig.builder() + .unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/")) + .appName("wrong_upstream") + .apiKey("default:development:1234567890123456") + .instanceId("non-200") + .synchronousFetchOnInitialisation(true) + .subscriber(readySubscriber) + .build(); assertThatThrownBy(() -> new DefaultUnleash(config)).isInstanceOf(UnleashException.class); assertThat(readySubscriber.ready).isFalse(); } @Test - public void synchronous_fetch_on_initialisation_switches_to_ready_on_200() throws URISyntaxException { + public void synchronous_fetch_on_initialisation_switches_to_ready_on_200() + throws URISyntaxException { mockUnleashAPI(200); IsReadyTestSubscriber readySubscriber = new IsReadyTestSubscriber(); UnleashConfig config = - UnleashConfig.builder() - .unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/")) - .appName("wrong_upstream") - .apiKey("default:development:1234567890123456") - .instanceId("with-success-response") - .synchronousFetchOnInitialisation(true) - .subscriber(readySubscriber) - .build(); + UnleashConfig.builder() + .unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/")) + .appName("wrong_upstream") + .apiKey("default:development:1234567890123456") + .instanceId("with-success-response") + .synchronousFetchOnInitialisation(true) + .subscriber(readySubscriber) + .build(); new DefaultUnleash(config); assertThat(readySubscriber.ready).isTrue(); } private void mockUnleashAPI(int featuresStatusCode) { stubFor( - get(urlEqualTo("/api/client/features")) - .withHeader("Accept", equalTo("application/json")) - .willReturn( - aResponse() - .withStatus(featuresStatusCode) - .withHeader("Content-Type", "application/json") - .withBody("{\"features\": []}"))); + get(urlEqualTo("/api/client/features")) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(featuresStatusCode) + .withHeader("Content-Type", "application/json") + .withBody("{\"features\": []}"))); stubFor(post(urlEqualTo("/api/client/register")).willReturn(aResponse().withStatus(200))); } @@ -344,6 +344,7 @@ public void client_identifier_handles_api_key_being_null() { private static class IsReadyTestSubscriber implements UnleashSubscriber { public boolean ready = false; + public void onReady(UnleashReady unleashReady) { this.ready = true; } diff --git a/src/test/java/io/getunleash/repository/FeatureRepositoryTest.java b/src/test/java/io/getunleash/repository/FeatureRepositoryTest.java index c0641ef3e..2986eea51 100644 --- a/src/test/java/io/getunleash/repository/FeatureRepositoryTest.java +++ b/src/test/java/io/getunleash/repository/FeatureRepositoryTest.java @@ -1,17 +1,15 @@ package io.getunleash.repository; +import static io.getunleash.repository.FeatureToggleResponse.Status.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + import io.getunleash.*; import io.getunleash.event.EventDispatcher; import io.getunleash.lang.Nullable; import io.getunleash.util.UnleashConfig; import io.getunleash.util.UnleashScheduledExecutor; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - import java.io.File; import java.io.IOException; import java.net.URISyntaxException; @@ -21,11 +19,13 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; - -import static io.getunleash.repository.FeatureToggleResponse.Status.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import java.util.concurrent.atomic.AtomicBoolean; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; public class FeatureRepositoryTest { FeatureBackupHandlerFile backupHandler; @@ -106,14 +106,14 @@ public void feature_toggles_should_be_updated() { when(backupHandler.read()).thenReturn(simpleFeatureCollection(false)); - FeatureRepository featureRepository = new FeatureRepository(config, backupHandler, executor, fetcher, bootstrapHandler); // run the toggleName fetcher callback verify(executor).setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong()); verify(fetcher, times(0)).fetchFeatures(); - ClientFeaturesResponse response = new ClientFeaturesResponse(CHANGED, simpleFeatureCollection(true)); + ClientFeaturesResponse response = + new ClientFeaturesResponse(CHANGED, simpleFeatureCollection(true)); when(fetcher.fetchFeatures()).thenReturn(response); runnableArgumentCaptor.getValue().run(); @@ -184,9 +184,7 @@ public void should_not_perform_synchronous_fetch_on_initialisation() { when(backupHandler.read()).thenReturn(new FeatureCollection()); FeatureCollection featureCollection = populatedFeatureCollection(null); - ClientFeaturesResponse response = - new ClientFeaturesResponse( - CHANGED, featureCollection); + ClientFeaturesResponse response = new ClientFeaturesResponse(CHANGED, featureCollection); when(fetcher.fetchFeatures()).thenReturn(response); @@ -250,15 +248,35 @@ public void should_not_read_bootstrap_if_backup_was_found() when(toggleBootstrapProvider.read()).thenReturn(fileToString(file)); - when(backupHandler.read()) - .thenReturn( - getFeatureCollection()); + when(backupHandler.read()).thenReturn(getFeatureCollection()); new FeatureRepository( config, backupHandler, new EventDispatcher(config), fetcher, bootstrapHandler); verify(toggleBootstrapProvider, times(0)).read(); } + @Test + public void shouldCallStartupExceptionHandlerIfStartupFails() { + ToggleBootstrapProvider toggleBootstrapProvider = mock(ToggleBootstrapProvider.class); + UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); + AtomicBoolean failed = new AtomicBoolean(false); + UnleashConfig config = + UnleashConfig.builder() + .synchronousFetchOnInitialisation(true) + .startupExceptionHandler( + (e) -> { + failed.set(true); + }) + .appName("test-sync-update") + .scheduledExecutor(executor) + .unleashAPI("http://localhost:8080") + .toggleBootstrapProvider(toggleBootstrapProvider) + .build(); + + Unleash unleash = new DefaultUnleash(config); + assertThat(failed).isTrue(); + } + @ParameterizedTest @ValueSource(ints = {403, 404}) public void should_increase_to_max_interval_when_code(int code) @@ -460,35 +478,31 @@ private String fileToString(File f) throws IOException { @NotNull private FeatureCollection simpleFeatureCollection(boolean enabled) { return populatedFeatureCollection( - null, - new FeatureToggle( - "toggleFetcherCalled", - enabled, - Arrays.asList(new ActivationStrategy("custom", null)))); + null, + new FeatureToggle( + "toggleFetcherCalled", + enabled, + Arrays.asList(new ActivationStrategy("custom", null)))); } @NotNull private FeatureCollection getFeatureCollection() { return populatedFeatureCollection( - Arrays.asList( - new Segment( - 1, - "some-name", - Arrays.asList( - new Constraint( - "some-context", - Operator.IN, - "some-value")))), - new FeatureToggle( - "toggleFeatureName1", - true, - Collections.singletonList( - new ActivationStrategy("custom", null))), - new FeatureToggle( - "toggleFeatureName2", - true, - Collections.singletonList( - new ActivationStrategy("custom", null)))); + Arrays.asList( + new Segment( + 1, + "some-name", + Arrays.asList( + new Constraint( + "some-context", Operator.IN, "some-value")))), + new FeatureToggle( + "toggleFeatureName1", + true, + Collections.singletonList(new ActivationStrategy("custom", null))), + new FeatureToggle( + "toggleFeatureName2", + true, + Collections.singletonList(new ActivationStrategy("custom", null)))); } private class TestRunner { @@ -506,15 +520,17 @@ public TestRunner() { private void ensureInitialized() { if (!initialized) { - verify(executor).setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong()); + verify(executor) + .setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong()); initialized = true; } } - public void assertThatFetchesAndReceives(FeatureToggleResponse.Status status, int statusCode) { + public void assertThatFetchesAndReceives( + FeatureToggleResponse.Status status, int statusCode) { ensureInitialized(); when(fetcher.fetchFeatures()) - .thenReturn(new ClientFeaturesResponse(status, statusCode)); + .thenReturn(new ClientFeaturesResponse(status, statusCode)); runnableArgumentCaptor.getValue().run(); verify(fetcher, times(++count)).fetchFeatures(); } From 1f1b8c4cfa309dea9d5225a73fdd99b92d4cec96 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 27 Nov 2023 09:10:49 +0100 Subject: [PATCH 06/34] chore(update): Bring spring-boot example up to speed with Spring Boot 3.2 and unleash-client 9.1.1 (#228) --- examples/spring-boot-example/build.gradle.kts | 7 ++++--- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../io/getunleash/unleash/example/UnleashSpringConfig.java | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/spring-boot-example/build.gradle.kts b/examples/spring-boot-example/build.gradle.kts index 588d8220b..ac3e43b23 100644 --- a/examples/spring-boot-example/build.gradle.kts +++ b/examples/spring-boot-example/build.gradle.kts @@ -1,16 +1,17 @@ plugins { java - id("org.springframework.boot") version "3.0.0" - id("io.spring.dependency-management") version "1.1.0" + id("org.springframework.boot") version "3.2.0" + id("io.spring.dependency-management") version "1.1.4" } repositories { + mavenLocal() mavenCentral() } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") - implementation("io.getunleash:unleash-client-java:8.2.0") + implementation("io.getunleash:unleash-client-java:9.1.1") testImplementation("org.springframework.boot:spring-boot-starter-test") } diff --git a/examples/spring-boot-example/gradle/wrapper/gradle-wrapper.properties b/examples/spring-boot-example/gradle/wrapper/gradle-wrapper.properties index ae04661ee..e411586a5 100644 --- a/examples/spring-boot-example/gradle/wrapper/gradle-wrapper.properties +++ b/examples/spring-boot-example/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/spring-boot-example/src/main/java/io/getunleash/unleash/example/UnleashSpringConfig.java b/examples/spring-boot-example/src/main/java/io/getunleash/unleash/example/UnleashSpringConfig.java index cb0858761..74f1589f9 100644 --- a/examples/spring-boot-example/src/main/java/io/getunleash/unleash/example/UnleashSpringConfig.java +++ b/examples/spring-boot-example/src/main/java/io/getunleash/unleash/example/UnleashSpringConfig.java @@ -1,6 +1,5 @@ package io.getunleash.unleash.example; - import io.getunleash.DefaultUnleash; import io.getunleash.Unleash; import io.getunleash.util.UnleashConfig; @@ -12,9 +11,11 @@ public class UnleashSpringConfig { @Bean - public UnleashConfig unleashConfig(@Value("${unleash.url}") String url, @Value("${unleash.apikey}") String apiKey, @Value("${unleash.appname}") String appName) { + public UnleashConfig unleashConfig(@Value("${unleash.url}") String url, @Value("${unleash.apikey}") String apiKey, + @Value("${unleash.appname}") String appName) { UnleashConfig config = UnleashConfig.builder().unleashAPI(url).apiKey(apiKey).appName(appName) - .fetchTogglesInterval(15).build(); + .synchronousFetchOnInitialisation(true) + .fetchTogglesInterval(15).build(); return config; } From f64bc6b36ca4b9095bb1729e748429d582ea571e Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Thu, 7 Dec 2023 10:27:14 +0100 Subject: [PATCH 07/34] fix: add deprecated method for old hashing algorithm (#233) fix: add deprecated method for old hashing algorithm Co-authored-by: Christopher Kolstad --- .../java/io/getunleash/DefaultUnleash.java | 126 ++++++++++++++++++ src/main/java/io/getunleash/FakeUnleash.java | 10 ++ src/main/java/io/getunleash/Unleash.java | 13 ++ .../java/io/getunleash/strategy/Strategy.java | 21 +++ .../io/getunleash/variant/VariantUtil.java | 74 ++++++++++ src/test/java/io/getunleash/UnleashTest.java | 39 ++++++ .../getunleash/variant/VariantUtilTest.java | 28 ++++ 7 files changed, 311 insertions(+) diff --git a/src/main/java/io/getunleash/DefaultUnleash.java b/src/main/java/io/getunleash/DefaultUnleash.java index 7cd2ae992..58547125c 100644 --- a/src/main/java/io/getunleash/DefaultUnleash.java +++ b/src/main/java/io/getunleash/DefaultUnleash.java @@ -219,6 +219,69 @@ private FeatureEvaluationResult getFeatureEvaluationResult( } } + /** + * Uses the old, statistically broken Variant seed for finding the correct variant + * @deprecated + * @param toggleName Name of the toggle + * @param context The UnleashContext + * @param fallbackAction What to do if we fail to find the toggle + * @param defaultVariant If we can't resolve a variant, what are we returning + * @return A wrapper containing whether the feature was enabled as well which Variant was selected + */ + private FeatureEvaluationResult deprecatedGetFeatureEvaluationResult( + String toggleName, + UnleashContext context, + BiPredicate fallbackAction, + @Nullable Variant defaultVariant) { + checkIfToggleMatchesNamePrefix(toggleName); + FeatureToggle featureToggle = featureRepository.getToggle(toggleName); + + UnleashContext enhancedContext = context.applyStaticFields(config); + if (featureToggle == null) { + return new FeatureEvaluationResult( + fallbackAction.test(toggleName, enhancedContext), defaultVariant); + } else if (!featureToggle.isEnabled()) { + return new FeatureEvaluationResult(false, defaultVariant); + } else if (featureToggle.getStrategies().isEmpty()) { + return new FeatureEvaluationResult( + true, VariantUtil.selectDeprecatedVariantHashingAlgo(featureToggle, context, defaultVariant)); + } else { + // Dependent toggles, no point in evaluating child strategies if our dependencies are + // not satisfied + if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) { + for (ActivationStrategy strategy : featureToggle.getStrategies()) { + Strategy configuredStrategy = getStrategy(strategy.getName()); + if (configuredStrategy == UNKNOWN_STRATEGY) { + LOGGER.warn( + "Unable to find matching strategy for toggle:{} strategy:{}", + toggleName, + strategy.getName()); + } + + FeatureEvaluationResult result = + configuredStrategy.getDeprecatedHashingAlgoResult( + strategy.getParameters(), + enhancedContext, + ConstraintMerger.mergeConstraints(featureRepository, strategy), + strategy.getVariants()); + + if (result.isEnabled()) { + Variant variant = result.getVariant(); + // If strategy variant is null, look for a variant in the featureToggle + if (variant == null) { + variant = + VariantUtil.selectDeprecatedVariantHashingAlgo( + featureToggle, context, defaultVariant); + } + result.setVariant(variant); + return result; + } + } + } + return new FeatureEvaluationResult(false, defaultVariant); + } + } + private boolean isParentDependencySatisfied( @Nonnull FeatureToggle featureToggle, @Nonnull UnleashContext context, @@ -323,6 +386,69 @@ public Variant getVariant(String toggleName, Variant defaultValue) { return getVariant(toggleName, contextProvider.getContext(), defaultValue); } + /** + * Uses the old, statistically broken Variant seed for finding the correct variant + * @deprecated + * @param toggleName + * @param context + * @return + */ + @Override + public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { + return deprecatedGetVariant(toggleName, context, DISABLED_VARIANT); + } + + /** + * Uses the old, statistically broken Variant seed for finding the correct variant + * @deprecated + * @param toggleName + * @param context + * @param defaultValue + * @return + */ + @Override + public Variant deprecatedGetVariant(String toggleName, UnleashContext context, Variant defaultValue) { + return deprecatedGetVariant(toggleName, context, defaultValue, false); + } + + private Variant deprecatedGetVariant( + String toggleName, UnleashContext context, Variant defaultValue, boolean isParent) { + FeatureEvaluationResult result = + deprecatedGetFeatureEvaluationResult(toggleName, context, (n, c) -> false, defaultValue); + Variant variant = result.getVariant(); + if (!isParent) { + metricService.countVariant(toggleName, variant.getName()); + // Should count yes/no also when getting variant. + metricService.count(toggleName, result.isEnabled()); + } + dispatchVariantImpressionDataIfNeeded( + toggleName, variant.getName(), result.isEnabled(), context); + return variant; + } + + /** + * Uses the old, statistically broken Variant seed for finding the correct variant + * @deprecated + * @param toggleName + * @return + */ + @Override + public Variant deprecatedGetVariant(String toggleName) { + return deprecatedGetVariant(toggleName, contextProvider.getContext()); + } + + /** + * Uses the old, statistically broken Variant seed for finding the correct variant + * @deprecated + * @param toggleName + * @param defaultValue + * @return + */ + @Override + public Variant deprecatedGetVariant(String toggleName, Variant defaultValue) { + return deprecatedGetVariant(toggleName, contextProvider.getContext(), defaultValue); + } + /** * Use more().getFeatureToggleDefinition() instead * diff --git a/src/main/java/io/getunleash/FakeUnleash.java b/src/main/java/io/getunleash/FakeUnleash.java index 5eabb9e15..7c414b633 100644 --- a/src/main/java/io/getunleash/FakeUnleash.java +++ b/src/main/java/io/getunleash/FakeUnleash.java @@ -106,6 +106,16 @@ public void disableAllExcept(String... excludedFeatures) { } } + @Override + public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { + return null; + } + + @Override + public Variant deprecatedGetVariant(String toggleName, UnleashContext context, Variant defaultValue) { + return null; + } + public void resetAll() { disableAll = false; enableAll = false; diff --git a/src/main/java/io/getunleash/Unleash.java b/src/main/java/io/getunleash/Unleash.java index 6e5259799..79fe6964e 100644 --- a/src/main/java/io/getunleash/Unleash.java +++ b/src/main/java/io/getunleash/Unleash.java @@ -43,6 +43,19 @@ default Variant getVariant(final String toggleName, final Variant defaultValue) return getVariant(toggleName, UnleashContext.builder().build(), defaultValue); } + Variant deprecatedGetVariant(final String toggleName, final UnleashContext context); + + Variant deprecatedGetVariant( + final String toggleName, final UnleashContext context, final Variant defaultValue); + + default Variant deprecatedGetVariant(final String toggleName) { + return deprecatedGetVariant(toggleName, UnleashContext.builder().build()); + } + + default Variant deprecatedGetVariant(final String toggleName, final Variant defaultValue) { + return deprecatedGetVariant(toggleName, UnleashContext.builder().build(), defaultValue); + } + /** * Use more().getFeatureToggleNames() instead * diff --git a/src/main/java/io/getunleash/strategy/Strategy.java b/src/main/java/io/getunleash/strategy/Strategy.java index 241ea485f..7a1272b5b 100644 --- a/src/main/java/io/getunleash/strategy/Strategy.java +++ b/src/main/java/io/getunleash/strategy/Strategy.java @@ -26,6 +26,27 @@ default FeatureEvaluationResult getResult( enabled ? VariantUtil.selectVariant(parameters, variants, unleashContext) : null); } + + /** + * Uses the old pre 9.0.0 way of hashing for finding the Variant to return + * @deprecated + * @param parameters + * @param unleashContext + * @param constraints + * @param variants + * @return + */ + default FeatureEvaluationResult getDeprecatedHashingAlgoResult( + Map parameters, + UnleashContext unleashContext, + List constraints, + @Nullable List variants) { + boolean enabled = isEnabled(parameters, unleashContext, constraints); + return new FeatureEvaluationResult( + enabled, + enabled ? VariantUtil.selectDeprecatedVariantHashingAlgo(parameters, variants, unleashContext) : null); + } + default boolean isEnabled(Map parameters, UnleashContext unleashContext) { return isEnabled(parameters); } diff --git a/src/main/java/io/getunleash/variant/VariantUtil.java b/src/main/java/io/getunleash/variant/VariantUtil.java index 0a0fd5655..4bc22acc1 100644 --- a/src/main/java/io/getunleash/variant/VariantUtil.java +++ b/src/main/java/io/getunleash/variant/VariantUtil.java @@ -133,4 +133,78 @@ public static Variant selectVariant( } return null; } + + /** + * Uses the old pre 9.0.0 way of hashing for finding the Variant to return + * @deprecated + * @param parameters + * @param variants + * @param context + * @return + */ + public static @Nullable Variant selectDeprecatedVariantHashingAlgo( + Map parameters, + @Nullable List variants, + UnleashContext context) { + if (variants != null) { + int totalWeight = variants.stream().mapToInt(VariantDefinition::getWeight).sum(); + if (totalWeight <= 0) { + return null; + } + Optional variantOverride = getOverride(variants, context); + if (variantOverride.isPresent()) { + return variantOverride.get().toVariant(); + } + + Optional customStickiness = + variants.stream() + .filter( + f -> + f.getStickiness() != null + && !"default".equals(f.getStickiness())) + .map(VariantDefinition::getStickiness) + .findFirst(); + int target = + StrategyUtils.getNormalizedNumber( + getSeed(context, customStickiness), + parameters.get(GROUP_ID_KEY), + totalWeight, + 0); + + int counter = 0; + for (VariantDefinition variant : variants) { + if (variant.getWeight() != 0) { + counter += variant.getWeight(); + if (counter >= target) { + return variant.toVariant(); + } + } + } + } + return null; + } + + /** + * Uses the old pre 9.0.0 way of hashing for finding the Variant to return + * @deprecated + * @param featureToggle + * @param context + * @param defaultVariant + * @return + */ + public static @Nullable Variant selectDeprecatedVariantHashingAlgo( + FeatureToggle featureToggle, UnleashContext context, Variant defaultVariant + ) { + if (featureToggle == null) { + return defaultVariant; + } + + Variant variant = + selectDeprecatedVariantHashingAlgo( + Collections.singletonMap("groupId", featureToggle.getName()), + featureToggle.getVariants(), + context); + + return variant != null ? variant : defaultVariant; + } } diff --git a/src/test/java/io/getunleash/UnleashTest.java b/src/test/java/io/getunleash/UnleashTest.java index 0f08628e7..3db7636f8 100644 --- a/src/test/java/io/getunleash/UnleashTest.java +++ b/src/test/java/io/getunleash/UnleashTest.java @@ -431,6 +431,8 @@ public void get_default_variant_without_context() { assertThat(result.isEnabled()).isTrue(); } + + @Test public void get_first_variant_with_context_provider() { @@ -477,6 +479,36 @@ public void get_second_variant_with_context_provider() { assertThat(result.isEnabled()).isTrue(); } + @Test + public void get_second_variant_with_context_provider_and_deprecated_algorithm() { + + UnleashContext context = UnleashContext.builder().userId("5").build(); + when(contextProvider.getContext()).thenReturn(context); + + // Set up a toggleName using UserWithIdStrategy + Map params = new HashMap<>(); + params.put("userIds", "123, 5, 121"); + ActivationStrategy strategy = new ActivationStrategy("userWithId", params); + FeatureToggle featureToggle = + new FeatureToggle("test", true, asList(strategy), getTestVariantsForDeprecatedHash()); + + when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + + final Variant result = unleash.deprecatedGetVariant("test"); + + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("en"); + assertThat(result.getPayload().map(Payload::getValue).get()).isEqualTo("en"); + assertThat(result.isEnabled()).isTrue(); + + final Variant newHash = unleash.getVariant("test"); + assertThat(newHash).isNotNull(); + assertThat(newHash.getName()).isEqualTo("to"); + assertThat(newHash.getPayload().map(Payload::getValue).get()).isEqualTo("to"); + assertThat(newHash.isEnabled()).isTrue(); + + } + @Test public void should_be_enabled_with_strategy_constraints() { List constraints = new ArrayList<>(); @@ -605,4 +637,11 @@ private List getTestVariants() { new VariantDefinition( "to", 50, new Payload("string", "to"), Collections.emptyList())); } + + private List getTestVariantsForDeprecatedHash() { + return asList( + new VariantDefinition("en", 65, new Payload("string", "en"), Collections.emptyList()), + new VariantDefinition("to", 35, new Payload("string", "to"), Collections.emptyList()) + ); + } } diff --git a/src/test/java/io/getunleash/variant/VariantUtilTest.java b/src/test/java/io/getunleash/variant/VariantUtilTest.java index ee431edb6..a551984e3 100644 --- a/src/test/java/io/getunleash/variant/VariantUtilTest.java +++ b/src/test/java/io/getunleash/variant/VariantUtilTest.java @@ -387,6 +387,34 @@ public void feature_variants_variant_d_client_spec_tests() { assertThat(variantUser537.getName()).isEqualTo("variant3"); } + @Test + public void feature_variants_variant_d_client_spec_tests_with_deprecated_seed() { + List variants = new ArrayList<>(); + variants.add( + new VariantDefinition( + "variant1", 1, new Payload("string", "val1"), Collections.emptyList())); + variants.add( + new VariantDefinition( + "variant2", 49, new Payload("string", "val2"), Collections.emptyList())); + variants.add( + new VariantDefinition( + "variant3", 50, new Payload("string", "val3"), Collections.emptyList())); + FeatureToggle toggle = + new FeatureToggle("Feature.Variants.D", true, Collections.emptyList(), variants); + Variant variantUser712 = + VariantUtil.selectDeprecatedVariantHashingAlgo( + toggle, UnleashContext.builder().userId("712").build(), DISABLED_VARIANT); + assertThat(variantUser712.getName()).isEqualTo("variant3"); + Variant variantUser525 = + VariantUtil.selectDeprecatedVariantHashingAlgo( + toggle, UnleashContext.builder().userId("525").build(), DISABLED_VARIANT); + assertThat(variantUser525.getName()).isEqualTo("variant3"); + Variant variantUser537 = + VariantUtil.selectDeprecatedVariantHashingAlgo( + toggle, UnleashContext.builder().userId("537").build(), DISABLED_VARIANT); + assertThat(variantUser537.getName()).isEqualTo("variant2"); + } + @Test public void feature_variants_variant_d_with_override_client_spec_tests() { List variants = new ArrayList<>(); From dde0dde86b739f5e21c8d5abbc1997ed7944602d Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 7 Dec 2023 10:29:05 +0100 Subject: [PATCH 08/34] chore(format): spotless apply --- .../java/io/getunleash/DefaultUnleash.java | 20 ++++++++++---- src/main/java/io/getunleash/FakeUnleash.java | 3 ++- .../java/io/getunleash/strategy/Strategy.java | 7 +++-- .../io/getunleash/variant/VariantUtil.java | 13 +++++----- src/test/java/io/getunleash/UnleashTest.java | 13 +++++----- .../getunleash/variant/VariantUtilTest.java | 26 +++++++++---------- 6 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/main/java/io/getunleash/DefaultUnleash.java b/src/main/java/io/getunleash/DefaultUnleash.java index 58547125c..7480ec1e9 100644 --- a/src/main/java/io/getunleash/DefaultUnleash.java +++ b/src/main/java/io/getunleash/DefaultUnleash.java @@ -221,14 +221,16 @@ private FeatureEvaluationResult getFeatureEvaluationResult( /** * Uses the old, statistically broken Variant seed for finding the correct variant + * * @deprecated * @param toggleName Name of the toggle * @param context The UnleashContext * @param fallbackAction What to do if we fail to find the toggle * @param defaultVariant If we can't resolve a variant, what are we returning - * @return A wrapper containing whether the feature was enabled as well which Variant was selected + * @return A wrapper containing whether the feature was enabled as well which Variant was + * selected */ - private FeatureEvaluationResult deprecatedGetFeatureEvaluationResult( + private FeatureEvaluationResult deprecatedGetFeatureEvaluationResult( String toggleName, UnleashContext context, BiPredicate fallbackAction, @@ -244,7 +246,9 @@ private FeatureEvaluationResult deprecatedGetFeatureEvaluationResult( return new FeatureEvaluationResult(false, defaultVariant); } else if (featureToggle.getStrategies().isEmpty()) { return new FeatureEvaluationResult( - true, VariantUtil.selectDeprecatedVariantHashingAlgo(featureToggle, context, defaultVariant)); + true, + VariantUtil.selectDeprecatedVariantHashingAlgo( + featureToggle, context, defaultVariant)); } else { // Dependent toggles, no point in evaluating child strategies if our dependencies are // not satisfied @@ -388,6 +392,7 @@ public Variant getVariant(String toggleName, Variant defaultValue) { /** * Uses the old, statistically broken Variant seed for finding the correct variant + * * @deprecated * @param toggleName * @param context @@ -400,6 +405,7 @@ public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { /** * Uses the old, statistically broken Variant seed for finding the correct variant + * * @deprecated * @param toggleName * @param context @@ -407,14 +413,16 @@ public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { * @return */ @Override - public Variant deprecatedGetVariant(String toggleName, UnleashContext context, Variant defaultValue) { + public Variant deprecatedGetVariant( + String toggleName, UnleashContext context, Variant defaultValue) { return deprecatedGetVariant(toggleName, context, defaultValue, false); } private Variant deprecatedGetVariant( String toggleName, UnleashContext context, Variant defaultValue, boolean isParent) { FeatureEvaluationResult result = - deprecatedGetFeatureEvaluationResult(toggleName, context, (n, c) -> false, defaultValue); + deprecatedGetFeatureEvaluationResult( + toggleName, context, (n, c) -> false, defaultValue); Variant variant = result.getVariant(); if (!isParent) { metricService.countVariant(toggleName, variant.getName()); @@ -428,6 +436,7 @@ private Variant deprecatedGetVariant( /** * Uses the old, statistically broken Variant seed for finding the correct variant + * * @deprecated * @param toggleName * @return @@ -439,6 +448,7 @@ public Variant deprecatedGetVariant(String toggleName) { /** * Uses the old, statistically broken Variant seed for finding the correct variant + * * @deprecated * @param toggleName * @param defaultValue diff --git a/src/main/java/io/getunleash/FakeUnleash.java b/src/main/java/io/getunleash/FakeUnleash.java index 7c414b633..11b5bf425 100644 --- a/src/main/java/io/getunleash/FakeUnleash.java +++ b/src/main/java/io/getunleash/FakeUnleash.java @@ -112,7 +112,8 @@ public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { } @Override - public Variant deprecatedGetVariant(String toggleName, UnleashContext context, Variant defaultValue) { + public Variant deprecatedGetVariant( + String toggleName, UnleashContext context, Variant defaultValue) { return null; } diff --git a/src/main/java/io/getunleash/strategy/Strategy.java b/src/main/java/io/getunleash/strategy/Strategy.java index 7a1272b5b..fe940303b 100644 --- a/src/main/java/io/getunleash/strategy/Strategy.java +++ b/src/main/java/io/getunleash/strategy/Strategy.java @@ -26,9 +26,9 @@ default FeatureEvaluationResult getResult( enabled ? VariantUtil.selectVariant(parameters, variants, unleashContext) : null); } - /** * Uses the old pre 9.0.0 way of hashing for finding the Variant to return + * * @deprecated * @param parameters * @param unleashContext @@ -44,7 +44,10 @@ default FeatureEvaluationResult getDeprecatedHashingAlgoResult( boolean enabled = isEnabled(parameters, unleashContext, constraints); return new FeatureEvaluationResult( enabled, - enabled ? VariantUtil.selectDeprecatedVariantHashingAlgo(parameters, variants, unleashContext) : null); + enabled + ? VariantUtil.selectDeprecatedVariantHashingAlgo( + parameters, variants, unleashContext) + : null); } default boolean isEnabled(Map parameters, UnleashContext unleashContext) { diff --git a/src/main/java/io/getunleash/variant/VariantUtil.java b/src/main/java/io/getunleash/variant/VariantUtil.java index 4bc22acc1..4e3796bf7 100644 --- a/src/main/java/io/getunleash/variant/VariantUtil.java +++ b/src/main/java/io/getunleash/variant/VariantUtil.java @@ -136,6 +136,7 @@ public static Variant selectVariant( /** * Uses the old pre 9.0.0 way of hashing for finding the Variant to return + * * @deprecated * @param parameters * @param variants @@ -186,6 +187,7 @@ public static Variant selectVariant( /** * Uses the old pre 9.0.0 way of hashing for finding the Variant to return + * * @deprecated * @param featureToggle * @param context @@ -193,17 +195,16 @@ public static Variant selectVariant( * @return */ public static @Nullable Variant selectDeprecatedVariantHashingAlgo( - FeatureToggle featureToggle, UnleashContext context, Variant defaultVariant - ) { + FeatureToggle featureToggle, UnleashContext context, Variant defaultVariant) { if (featureToggle == null) { return defaultVariant; } Variant variant = - selectDeprecatedVariantHashingAlgo( - Collections.singletonMap("groupId", featureToggle.getName()), - featureToggle.getVariants(), - context); + selectDeprecatedVariantHashingAlgo( + Collections.singletonMap("groupId", featureToggle.getName()), + featureToggle.getVariants(), + context); return variant != null ? variant : defaultVariant; } diff --git a/src/test/java/io/getunleash/UnleashTest.java b/src/test/java/io/getunleash/UnleashTest.java index 3db7636f8..f80c976bc 100644 --- a/src/test/java/io/getunleash/UnleashTest.java +++ b/src/test/java/io/getunleash/UnleashTest.java @@ -431,8 +431,6 @@ public void get_default_variant_without_context() { assertThat(result.isEnabled()).isTrue(); } - - @Test public void get_first_variant_with_context_provider() { @@ -490,7 +488,8 @@ public void get_second_variant_with_context_provider_and_deprecated_algorithm() params.put("userIds", "123, 5, 121"); ActivationStrategy strategy = new ActivationStrategy("userWithId", params); FeatureToggle featureToggle = - new FeatureToggle("test", true, asList(strategy), getTestVariantsForDeprecatedHash()); + new FeatureToggle( + "test", true, asList(strategy), getTestVariantsForDeprecatedHash()); when(toggleRepository.getToggle("test")).thenReturn(featureToggle); @@ -506,7 +505,6 @@ public void get_second_variant_with_context_provider_and_deprecated_algorithm() assertThat(newHash.getName()).isEqualTo("to"); assertThat(newHash.getPayload().map(Payload::getValue).get()).isEqualTo("to"); assertThat(newHash.isEnabled()).isTrue(); - } @Test @@ -640,8 +638,9 @@ private List getTestVariants() { private List getTestVariantsForDeprecatedHash() { return asList( - new VariantDefinition("en", 65, new Payload("string", "en"), Collections.emptyList()), - new VariantDefinition("to", 35, new Payload("string", "to"), Collections.emptyList()) - ); + new VariantDefinition( + "en", 65, new Payload("string", "en"), Collections.emptyList()), + new VariantDefinition( + "to", 35, new Payload("string", "to"), Collections.emptyList())); } } diff --git a/src/test/java/io/getunleash/variant/VariantUtilTest.java b/src/test/java/io/getunleash/variant/VariantUtilTest.java index a551984e3..16325169f 100644 --- a/src/test/java/io/getunleash/variant/VariantUtilTest.java +++ b/src/test/java/io/getunleash/variant/VariantUtilTest.java @@ -391,27 +391,27 @@ public void feature_variants_variant_d_client_spec_tests() { public void feature_variants_variant_d_client_spec_tests_with_deprecated_seed() { List variants = new ArrayList<>(); variants.add( - new VariantDefinition( - "variant1", 1, new Payload("string", "val1"), Collections.emptyList())); + new VariantDefinition( + "variant1", 1, new Payload("string", "val1"), Collections.emptyList())); variants.add( - new VariantDefinition( - "variant2", 49, new Payload("string", "val2"), Collections.emptyList())); + new VariantDefinition( + "variant2", 49, new Payload("string", "val2"), Collections.emptyList())); variants.add( - new VariantDefinition( - "variant3", 50, new Payload("string", "val3"), Collections.emptyList())); + new VariantDefinition( + "variant3", 50, new Payload("string", "val3"), Collections.emptyList())); FeatureToggle toggle = - new FeatureToggle("Feature.Variants.D", true, Collections.emptyList(), variants); + new FeatureToggle("Feature.Variants.D", true, Collections.emptyList(), variants); Variant variantUser712 = - VariantUtil.selectDeprecatedVariantHashingAlgo( - toggle, UnleashContext.builder().userId("712").build(), DISABLED_VARIANT); + VariantUtil.selectDeprecatedVariantHashingAlgo( + toggle, UnleashContext.builder().userId("712").build(), DISABLED_VARIANT); assertThat(variantUser712.getName()).isEqualTo("variant3"); Variant variantUser525 = - VariantUtil.selectDeprecatedVariantHashingAlgo( - toggle, UnleashContext.builder().userId("525").build(), DISABLED_VARIANT); + VariantUtil.selectDeprecatedVariantHashingAlgo( + toggle, UnleashContext.builder().userId("525").build(), DISABLED_VARIANT); assertThat(variantUser525.getName()).isEqualTo("variant3"); Variant variantUser537 = - VariantUtil.selectDeprecatedVariantHashingAlgo( - toggle, UnleashContext.builder().userId("537").build(), DISABLED_VARIANT); + VariantUtil.selectDeprecatedVariantHashingAlgo( + toggle, UnleashContext.builder().userId("537").build(), DISABLED_VARIANT); assertThat(variantUser537.getName()).isEqualTo("variant2"); } From 9d762e0ed91ca3a358faee0310ae87ab715cfdee Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 7 Dec 2023 10:29:43 +0100 Subject: [PATCH 09/34] [maven-release-plugin] prepare release unleash-client-java-9.2.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 791f10a91..8164d8072 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.1.2-SNAPSHOT + 9.2.0 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-8.4.0 + unleash-client-java-9.2.0 From b2ac9aee6aecbd3e968ed5c0e51bb5fca1c60452 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 7 Dec 2023 10:29:46 +0100 Subject: [PATCH 10/34] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8164d8072..d71bae7cc 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.0 + 9.2.1-SNAPSHOT 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-9.2.0 + unleash-client-java-8.4.0 From 0e23677a3331cf3e548db6af7cdbb197678063e9 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 6 May 2024 14:25:51 +0200 Subject: [PATCH 11/34] fix: check that contextValue starts with (#240) * fix: check that contextValue starts with There had been an inversion of variable usage for one of our cases in the matcher. This PR makes sure to compare contextValue to see if it starts with the requested value in the constraint, instead of the other way around. fixes #238 --- .../constraints/StringConstraintOperator.java | 11 ++-- .../StringConstraintOperatorTest.java | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/getunleash/strategy/constraints/StringConstraintOperator.java b/src/main/java/io/getunleash/strategy/constraints/StringConstraintOperator.java index 5a300b420..3106d1c8b 100644 --- a/src/main/java/io/getunleash/strategy/constraints/StringConstraintOperator.java +++ b/src/main/java/io/getunleash/strategy/constraints/StringConstraintOperator.java @@ -58,17 +58,18 @@ private boolean startsWith( List values, Optional contextValue, boolean caseInsensitive) { return contextValue .map( - c -> + actualContextValue -> values.stream() .anyMatch( - v -> { + value -> { if (caseInsensitive) { - return v.toLowerCase(comparisonLocale) + return actualContextValue + .toLowerCase(comparisonLocale) .startsWith( - c.toLowerCase( + value.toLowerCase( comparisonLocale)); } else { - return c.startsWith(v); + return actualContextValue.startsWith(value); } })) .orElse(false); diff --git a/src/test/java/io/getunleash/strategy/constraints/StringConstraintOperatorTest.java b/src/test/java/io/getunleash/strategy/constraints/StringConstraintOperatorTest.java index 34234ee85..1ed3f76dd 100644 --- a/src/test/java/io/getunleash/strategy/constraints/StringConstraintOperatorTest.java +++ b/src/test/java/io/getunleash/strategy/constraints/StringConstraintOperatorTest.java @@ -211,4 +211,56 @@ public void shouldSupportInvertingStringContains() { .build(); assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isFalse(); } + + @Test + public void startsWithShouldMatchCorrectlyWhenCaseSensitive() { + Strategy strategy = new DefaultStrategy(); + List constraintList = + Collections.singletonList( + new Constraint( + "email", + Operator.STR_STARTS_WITH, + Collections.singletonList("testuser"), + false, + false)); + Map parameters = new HashMap<>(); + UnleashContext ctx = + UnleashContext.builder() + .environment("dev") + .addProperty("email", "TESTUSER@getunleash.io") + .build(); + assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isFalse(); + UnleashContext ctx2 = + UnleashContext.builder() + .environment("dev") + .addProperty("email", "testuser@getunleash.io") + .build(); + assertThat(strategy.isEnabled(parameters, ctx2, constraintList)).isTrue(); + } + + @Test + public void startsWithShouldMatchCorrectlyWhenCaseInsensitive() { + Strategy strategy = new DefaultStrategy(); + List constraintList = + Collections.singletonList( + new Constraint( + "email", + Operator.STR_STARTS_WITH, + Collections.singletonList("testuser"), + false, + true)); + Map parameters = new HashMap<>(); + UnleashContext ctx = + UnleashContext.builder() + .environment("dev") + .addProperty("email", "TESTUSER@getunleash.io") + .build(); + assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isTrue(); + UnleashContext ctx2 = + UnleashContext.builder() + .environment("dev") + .addProperty("email", "testuser@getunleash.io") + .build(); + assertThat(strategy.isEnabled(parameters, ctx2, constraintList)).isTrue(); + } } From bf6aa931745cc094cf4854829f5f29b3d68e8208 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 6 May 2024 14:28:21 +0200 Subject: [PATCH 12/34] [maven-release-plugin] prepare release unleash-client-java-9.2.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d71bae7cc..1700cb33a 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.1-SNAPSHOT + 9.2.1 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-8.4.0 + unleash-client-java-9.2.1 From cd3e6ffc03f0b5965dda22d8ca6149fe57bbda2f Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 6 May 2024 14:30:36 +0200 Subject: [PATCH 13/34] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1700cb33a..0d5b10517 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.1 + 9.2.2-SNAPSHOT 2.0.9 From a773b77c5c8c94fc1408498f9f655ba8b5474381 Mon Sep 17 00:00:00 2001 From: Sebastian Ullrich Date: Tue, 7 May 2024 23:15:58 +0200 Subject: [PATCH 14/34] docs: add explanation for startupExceptionHandler (#241) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a69eddf61..c280868d6 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,7 @@ The `UnleashConfig$Builder` class (created via `UnleashConfig.builder()`) expose | `unleashContextProvider` | An [Unleash context provider used to configure Unleash](#2-via-an-unleashcontextprovider). | No | `null` | | `unleashFeatureFetcherFactory` | A factory providing a FeatureFetcher implementation. | No | [`HttpFeatureFetcher::new`](src/main/java/io/getunleash/repository/HttpFeatureFetcher.java) | | `unleashMetricsSenderFactory` | A factory providing a MetricSender implementation. | No | [`DefaultHttpMetricsSender::new`](src/main/java/io/getunleash/metric/DefaultHttpMetricsSender.java) | +| `startupExceptionHandler` | Handler for the behavior in the event of an error when starting the client. | No | `null` | When you have set all the desired options, initialize the configuration with the `build` method. You can then pass the configuration to the Unleash client constructor. From 5fddde582ef0b43d8886ea451f5ae203eac7c9c5 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 8 May 2024 15:43:08 +0200 Subject: [PATCH 15/34] chore: Use client spec 5.1.5 instead of 5.0.2 (#242) * chore: Use client spec 5.1.5 instead of 5.0.2 * fix: inherit strategy stickiness setting for variants --- pom.xml | 2 +- .../strategy/FlexibleRolloutStrategy.java | 4 - .../java/io/getunleash/strategy/Strategy.java | 13 +- .../io/getunleash/variant/VariantUtil.java | 20 ++- .../strategy/StrategyVariantTest.java | 128 ++++++++++++++++++ 5 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 src/test/java/io/getunleash/strategy/StrategyVariantTest.java diff --git a/pom.xml b/pom.xml index 0d5b10517..b95cd793b 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ 5.10.0 4.10.0 UTF-8 - 5.0.2 + 5.1.5 2.14.3 diff --git a/src/main/java/io/getunleash/strategy/FlexibleRolloutStrategy.java b/src/main/java/io/getunleash/strategy/FlexibleRolloutStrategy.java index 40fbc44ab..5c951869c 100644 --- a/src/main/java/io/getunleash/strategy/FlexibleRolloutStrategy.java +++ b/src/main/java/io/getunleash/strategy/FlexibleRolloutStrategy.java @@ -59,8 +59,4 @@ public boolean isEnabled(Map parameters, UnleashContext unleashC .map(norm -> percentage > 0 && norm <= percentage) .orElse(false); } - - private String getStickiness(Map parameters) { - return parameters.getOrDefault("stickiness", "default"); - } } diff --git a/src/main/java/io/getunleash/strategy/Strategy.java b/src/main/java/io/getunleash/strategy/Strategy.java index fe940303b..e0e2fe7b7 100644 --- a/src/main/java/io/getunleash/strategy/Strategy.java +++ b/src/main/java/io/getunleash/strategy/Strategy.java @@ -21,9 +21,13 @@ default FeatureEvaluationResult getResult( List constraints, @Nullable List variants) { boolean enabled = isEnabled(parameters, unleashContext, constraints); + String strategyStickiness = getStickiness(parameters); return new FeatureEvaluationResult( enabled, - enabled ? VariantUtil.selectVariant(parameters, variants, unleashContext) : null); + enabled + ? VariantUtil.selectVariant( + parameters, variants, unleashContext, strategyStickiness) + : null); } /** @@ -61,4 +65,11 @@ default boolean isEnabled( return ConstraintUtil.validate(constraints, unleashContext) && isEnabled(parameters, unleashContext); } + + default String getStickiness(@Nullable Map parameters) { + if (parameters != null) { + return parameters.getOrDefault("stickiness", "default"); + } + return null; + } } diff --git a/src/main/java/io/getunleash/variant/VariantUtil.java b/src/main/java/io/getunleash/variant/VariantUtil.java index 4e3796bf7..dcd6ce925 100644 --- a/src/main/java/io/getunleash/variant/VariantUtil.java +++ b/src/main/java/io/getunleash/variant/VariantUtil.java @@ -95,7 +95,8 @@ public static Variant selectVariant( public static @Nullable Variant selectVariant( Map parameters, @Nullable List variants, - UnleashContext context) { + UnleashContext context, + @Nullable String strategyStickiness) { if (variants != null) { int totalWeight = variants.stream().mapToInt(VariantDefinition::getWeight).sum(); if (totalWeight <= 0) { @@ -106,7 +107,7 @@ public static Variant selectVariant( return variantOverride.get().toVariant(); } - Optional customStickiness = + Optional variantCustomStickiness = variants.stream() .filter( f -> @@ -114,6 +115,14 @@ public static Variant selectVariant( && !"default".equals(f.getStickiness())) .map(VariantDefinition::getStickiness) .findFirst(); + Optional customStickiness; + if (!variantCustomStickiness.isPresent()) { + customStickiness = + Optional.ofNullable(strategyStickiness) + .filter(stickiness -> !stickiness.equalsIgnoreCase("default")); + } else { + customStickiness = variantCustomStickiness; + } int target = StrategyUtils.getNormalizedNumber( getSeed(context, customStickiness), @@ -134,6 +143,13 @@ public static Variant selectVariant( return null; } + public static @Nullable Variant selectVariant( + Map parameters, + @Nullable List variants, + UnleashContext context) { + return selectVariant(parameters, variants, context, null); + } + /** * Uses the old pre 9.0.0 way of hashing for finding the Variant to return * diff --git a/src/test/java/io/getunleash/strategy/StrategyVariantTest.java b/src/test/java/io/getunleash/strategy/StrategyVariantTest.java new file mode 100644 index 000000000..6508c6238 --- /dev/null +++ b/src/test/java/io/getunleash/strategy/StrategyVariantTest.java @@ -0,0 +1,128 @@ +package io.getunleash.strategy; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.getunleash.FeatureEvaluationResult; +import io.getunleash.UnleashContext; +import io.getunleash.Variant; +import io.getunleash.variant.Payload; +import io.getunleash.variant.VariantDefinition; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import org.junit.jupiter.api.Test; + +public class StrategyVariantTest { + + @Test + public void should_inherit_stickiness_from_the_strategy_first_variant() { + HashMap params = new HashMap<>(); + params.put("rollout", "100"); + params.put("stickiness", "clientId"); + params.put("groupId", "a"); + FlexibleRolloutStrategy strategy = new FlexibleRolloutStrategy(); + VariantDefinition varA = + new VariantDefinition( + "variantNameA", + 1, + new Payload("string", "variantValueA"), + Collections.emptyList()); + VariantDefinition varB = + new VariantDefinition( + "variantNameB", + 1, + new Payload("string", "variantValueB"), + Collections.emptyList()); + + UnleashContext context = UnleashContext.builder().addProperty("clientId", "1").build(); + FeatureEvaluationResult result = + strategy.getResult( + params, context, Collections.emptyList(), Arrays.asList(varA, varB)); + Variant selectedVariant = result.getVariant(); + assert selectedVariant != null; + assertThat(selectedVariant.getName()).isEqualTo("variantNameA"); + } + + @Test + public void should_inherit_stickiness_from_the_strategy_second_variant() { + HashMap params = new HashMap<>(); + params.put("rollout", "100"); + params.put("stickiness", "clientId"); + params.put("groupId", "a"); + FlexibleRolloutStrategy strategy = new FlexibleRolloutStrategy(); + VariantDefinition varA = + new VariantDefinition( + "variantNameA", + 1, + new Payload("string", "variantValueA"), + Collections.emptyList()); + VariantDefinition varB = + new VariantDefinition( + "variantNameB", + 1, + new Payload("string", "variantValueB"), + Collections.emptyList()); + + UnleashContext context = UnleashContext.builder().addProperty("clientId", "2").build(); + FeatureEvaluationResult result = + strategy.getResult( + params, context, Collections.emptyList(), Arrays.asList(varA, varB)); + Variant selectedVariant = result.getVariant(); + assert selectedVariant != null; + assertThat(selectedVariant.getName()).isEqualTo("variantNameB"); + } + + @Test + public void multiple_variants_should_choose_first_variant() { + HashMap params = new HashMap<>(); + params.put("rollout", "100"); + params.put("groupId", "a"); + params.put("stickiness", "default"); + FlexibleRolloutStrategy strategy = new FlexibleRolloutStrategy(); + VariantDefinition varA = + new VariantDefinition( + "variantNameA", + 1, + new Payload("string", "variantValueA"), + Collections.emptyList()); + VariantDefinition varB = + new VariantDefinition( + "variantNameB", + 1, + new Payload("string", "variantValueB"), + Collections.emptyList()); + UnleashContext context = UnleashContext.builder().userId("5").build(); + FeatureEvaluationResult result = + strategy.getResult( + params, context, Collections.emptyList(), Arrays.asList(varA, varB)); + Variant selectedVariant = result.getVariant(); + assertThat(selectedVariant.getName()).isEqualTo("variantNameA"); + } + + @Test + public void multiple_variants_should_choose_second_variant() { + HashMap params = new HashMap<>(); + params.put("rollout", "100"); + params.put("groupId", "a"); + params.put("stickiness", "default"); + FlexibleRolloutStrategy strategy = new FlexibleRolloutStrategy(); + VariantDefinition varA = + new VariantDefinition( + "variantNameA", + 1, + new Payload("string", "variantValueA"), + Collections.emptyList()); + VariantDefinition varB = + new VariantDefinition( + "variantNameB", + 1, + new Payload("string", "variantValueB"), + Collections.emptyList()); + UnleashContext context = UnleashContext.builder().userId("0").build(); + FeatureEvaluationResult result = + strategy.getResult( + params, context, Collections.emptyList(), Arrays.asList(varA, varB)); + Variant selectedVariant = result.getVariant(); + assertThat(selectedVariant.getName()).isEqualTo("variantNameB"); + } +} From 132a086b306472f04d711201d175fd09750ad379 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 14 May 2024 14:18:48 +0200 Subject: [PATCH 16/34] fix: enableAll and disableAll overrides fallback (#243) As discussed in #239 - When all is enabled, we had a bit of a surprising behaviour where we'd fallback to fallback action for `isEnabled(featureName, fallback)` even if all was enabled and feature did not exist. This PR fixes that, and adds tests to confirm this behaviour is intentional. closes: #239 --- src/main/java/io/getunleash/FakeUnleash.java | 3 +- .../java/io/getunleash/FakeUnleashTest.java | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/getunleash/FakeUnleash.java b/src/main/java/io/getunleash/FakeUnleash.java index 11b5bf425..0e88d135a 100644 --- a/src/main/java/io/getunleash/FakeUnleash.java +++ b/src/main/java/io/getunleash/FakeUnleash.java @@ -38,7 +38,8 @@ public boolean isEnabled( @Override public boolean isEnabled( String toggleName, BiPredicate fallbackAction) { - if (!features.containsKey(toggleName)) { + if ((!enableAll && !disableAll || excludedFeatures.containsKey(toggleName)) + && !features.containsKey(toggleName)) { return fallbackAction.test(toggleName, UnleashContext.builder().build()); } return isEnabled(toggleName); diff --git a/src/test/java/io/getunleash/FakeUnleashTest.java b/src/test/java/io/getunleash/FakeUnleashTest.java index a4c00977d..daaaf09c2 100644 --- a/src/test/java/io/getunleash/FakeUnleashTest.java +++ b/src/test/java/io/getunleash/FakeUnleashTest.java @@ -179,4 +179,32 @@ public void should_countVariant_and_not_throw_an_error() { FakeUnleash fakeUnleash = new FakeUnleash(); fakeUnleash.more().countVariant("toggleName", "variantName"); } + + @Test + public void + if_all_is_enabled_should_return_true_even_if_feature_does_not_exist_and_fallback_returns_false() { + FakeUnleash fakeUnleash = new FakeUnleash(); + fakeUnleash.enableAll(); + assertThat(fakeUnleash.isEnabled("my.non.existing.feature", (name, context) -> false)) + .isTrue(); + } + + @Test + public void + if_all_is_disabled_should_return_false_even_if_feature_does_not_exist_and_fallback_returns_true() { + FakeUnleash fakeUnleash = new FakeUnleash(); + fakeUnleash.disableAll(); + assertThat(fakeUnleash.isEnabled("my.non.existing.feature", (name, context) -> true)) + .isFalse(); + } + + @Test + public void all_enabled_and_exclusion_toggle_returns_expected_result() { + FakeUnleash fakeUnleash = new FakeUnleash(); + fakeUnleash.enableAllExcept("my.feature.that.should.be.disabled"); + assertThat( + fakeUnleash.isEnabled( + "my.feature.that.should.be.disabled", (name, context) -> false)) + .isFalse(); + } } From d683177687138fcc643cf58cc8cc2882b5c7ccc0 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 14 May 2024 14:42:49 +0200 Subject: [PATCH 17/34] [maven-release-plugin] prepare release unleash-client-java-9.2.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b95cd793b..cd9105d9c 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.2-SNAPSHOT + 9.2.2 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-9.2.1 + unleash-client-java-9.2.2 From 90a872a49bdd9c97519351f9c3f8c0fa9736c4ad Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 14 May 2024 14:44:22 +0200 Subject: [PATCH 18/34] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cd9105d9c..2d825882c 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.2 + 9.2.3-SNAPSHOT 2.0.9 From fcbc319b7b5753b3e701839da36d27ba11135a1a Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 25 Jul 2024 13:08:54 +0200 Subject: [PATCH 19/34] feat: Added new metadata fields to Registration and Metrics (#244) --- .../io/getunleash/metric/ClientMetrics.java | 28 +++++++++++ .../getunleash/metric/ClientRegistration.java | 27 +++++++++++ .../metric/UnleashMetricServiceImpl.java | 2 +- .../metric/UnleashMetricServiceImplTest.java | 46 +++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/getunleash/metric/ClientMetrics.java b/src/main/java/io/getunleash/metric/ClientMetrics.java index f59a8ecce..82eac44e9 100644 --- a/src/main/java/io/getunleash/metric/ClientMetrics.java +++ b/src/main/java/io/getunleash/metric/ClientMetrics.java @@ -2,6 +2,7 @@ import io.getunleash.event.UnleashEvent; import io.getunleash.event.UnleashSubscriber; +import io.getunleash.lang.Nullable; import io.getunleash.util.UnleashConfig; public class ClientMetrics implements UnleashEvent { @@ -10,12 +11,20 @@ public class ClientMetrics implements UnleashEvent { private final String instanceId; private final MetricsBucket bucket; private final String environment; + private final String specVersion; + @Nullable private final String platformName; + @Nullable private final String platformVersion; + @Nullable private final String yggdrasilVersion; ClientMetrics(UnleashConfig config, MetricsBucket bucket) { this.environment = config.getEnvironment(); this.appName = config.getAppName(); this.instanceId = config.getInstanceId(); this.bucket = bucket; + this.specVersion = config.getClientSpecificationVersion(); + this.platformName = System.getProperty("java.vm.name"); + this.platformVersion = System.getProperty("java.version"); + this.yggdrasilVersion = null; } public String getAppName() { @@ -34,6 +43,25 @@ public String getEnvironment() { return environment; } + public String getSpecVersion() { + return specVersion; + } + + @Nullable + public String getPlatformName() { + return platformName; + } + + @Nullable + public String getPlatformVersion() { + return platformVersion; + } + + @Nullable + public String getYggdrasilVersion() { + return yggdrasilVersion; + } + @Override public void publishTo(UnleashSubscriber unleashSubscriber) { unleashSubscriber.clientMetrics(this); diff --git a/src/main/java/io/getunleash/metric/ClientRegistration.java b/src/main/java/io/getunleash/metric/ClientRegistration.java index b3019de7b..a820ccf92 100644 --- a/src/main/java/io/getunleash/metric/ClientRegistration.java +++ b/src/main/java/io/getunleash/metric/ClientRegistration.java @@ -2,6 +2,7 @@ import io.getunleash.event.UnleashEvent; import io.getunleash.event.UnleashSubscriber; +import io.getunleash.lang.Nullable; import io.getunleash.util.UnleashConfig; import java.time.LocalDateTime; import java.util.Set; @@ -14,6 +15,10 @@ public class ClientRegistration implements UnleashEvent { private final LocalDateTime started; private final long interval; private final String environment; + @Nullable private final String platformName; + @Nullable private final String platformVersion; + @Nullable private final String yggdrasilVersion; + private final String specVersion; ClientRegistration(UnleashConfig config, LocalDateTime started, Set strategies) { this.environment = config.getEnvironment(); @@ -23,6 +28,10 @@ public class ClientRegistration implements UnleashEvent { this.started = started; this.strategies = strategies; this.interval = config.getSendMetricsInterval(); + this.specVersion = config.getClientSpecificationVersion(); + this.platformName = System.getProperty("java.vm.name"); + this.platformVersion = System.getProperty("java.version"); + this.yggdrasilVersion = null; } public String getAppName() { @@ -53,6 +62,24 @@ public String getEnvironment() { return environment; } + @Nullable + public String getPlatformName() { + return platformName; + } + + @Nullable + public String getPlatformVersion() { + return platformVersion; + } + + public @Nullable String getYggdrasilVersion() { + return yggdrasilVersion; + } + + public String getSpecVersion() { + return specVersion; + } + @Override public void publishTo(UnleashSubscriber unleashSubscriber) { unleashSubscriber.clientRegistered(this); diff --git a/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java b/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java index fa200cd1b..caf2c3b0e 100644 --- a/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java +++ b/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java @@ -13,7 +13,6 @@ public class UnleashMetricServiceImpl implements UnleashMetricService { private static final Logger LOGGER = LoggerFactory.getLogger(UnleashMetricServiceImpl.class); private final LocalDateTime started; private final UnleashConfig unleashConfig; - private final MetricSender metricSender; // mutable @@ -40,6 +39,7 @@ public UnleashMetricServiceImpl( 300, unleashConfig.getUnleashURLs().getClientMetricsURL()); long metricsInterval = unleashConfig.getSendMetricsInterval(); + executor.setInterval(sendMetrics(), metricsInterval, metricsInterval); } diff --git a/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java b/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java index 1640ce03b..ed5b631ad 100644 --- a/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java +++ b/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java @@ -5,6 +5,7 @@ import io.getunleash.util.UnleashConfig; import io.getunleash.util.UnleashScheduledExecutor; +import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.Test; @@ -412,4 +413,49 @@ public void url_not_found_immediately_increases_interval_to_max() { assertThat(unleashMetricService.getFailures()).isEqualTo(0); assertThat(unleashMetricService.getSkips()).isEqualTo(0); } + + @Test + public void should_add_new_metrics_data_to_bucket() { + UnleashConfig config = + UnleashConfig.builder() + .appName("test") + .sendMetricsInterval(10) + .unleashAPI("http://unleash.com") + .build(); + + UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); + DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); + + UnleashMetricService unleashMetricService = + new UnleashMetricServiceImpl(config, sender, executor); + + ArgumentCaptor sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class); + verify(executor).setInterval(sendMetricsCallback.capture(), anyLong(), anyLong()); + + sendMetricsCallback.getValue().run(); + ArgumentCaptor metricsSent = ArgumentCaptor.forClass(ClientMetrics.class); + verify(sender, times(1)).sendMetrics(metricsSent.capture()); + ClientMetrics metrics = metricsSent.getValue(); + assertThat(metrics.getSpecVersion()).isNotEmpty(); + assertThat(metrics.getYggdrasilVersion()).isNull(); + assertThat(metrics.getPlatformName()).isNotEmpty(); + assertThat(metrics.getPlatformVersion()).isNotEmpty(); + } + + @Test + public void client_registration_also_includes_new_metrics_metadata() { + UnleashConfig config = + UnleashConfig.builder() + .appName("test") + .sendMetricsInterval(10) + .unleashAPI("http://unleash.com") + .build(); + Set strategies = new HashSet<>(); + strategies.add("default"); + ClientRegistration reg = new ClientRegistration(config, LocalDateTime.now(), strategies); + assertThat(reg.getPlatformName()).isNotEmpty(); + assertThat(reg.getPlatformVersion()).isNotEmpty(); + assertThat(reg.getSpecVersion()).isEqualTo(config.getClientSpecificationVersion()); + assertThat(reg.getYggdrasilVersion()).isNull(); + } } From 1b52d67ad1d208f59a68b688c44c0dca27259b14 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 25 Jul 2024 13:19:32 +0200 Subject: [PATCH 20/34] chore: bumped action versions to avoid deprecation warnings about node versions (#245) --- .github/workflows/main.yml | 8 ++++---- .github/workflows/publish_javadoc.yml | 4 ++-- .github/workflows/pull_requests.yml | 2 +- .github/workflows/release_to_central.yml | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 704873b7d..66a52c778 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,9 +15,9 @@ jobs: version: [8,11] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: ${{ matrix.version }} cache: 'maven' @@ -38,9 +38,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'temurin' diff --git a/.github/workflows/publish_javadoc.yml b/.github/workflows/publish_javadoc.yml index 18f23e45b..a5c9c5203 100644 --- a/.github/workflows/publish_javadoc.yml +++ b/.github/workflows/publish_javadoc.yml @@ -11,9 +11,9 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: java-version: 17 - name: Build diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index 8713d09b8..42896f0b5 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: ${{ matrix.version }} cache: 'maven' diff --git a/.github/workflows/release_to_central.yml b/.github/workflows/release_to_central.yml index acb266ae3..73371963c 100644 --- a/.github/workflows/release_to_central.yml +++ b/.github/workflows/release_to_central.yml @@ -13,10 +13,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 name: Checkout code - name: Setup Java and Maven Central Repo - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '8' distribution: 'temurin' From 01bb5e7e61498ee3dab5df7de84b4bc03b035f73 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 25 Jul 2024 13:21:36 +0200 Subject: [PATCH 21/34] fix: setup-java requires distribution argument now --- .github/workflows/publish_javadoc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish_javadoc.yml b/.github/workflows/publish_javadoc.yml index a5c9c5203..c99f4aac3 100644 --- a/.github/workflows/publish_javadoc.yml +++ b/.github/workflows/publish_javadoc.yml @@ -16,6 +16,7 @@ jobs: uses: actions/setup-java@v4 with: java-version: 17 + distribution: 'temurin' - name: Build run: ./mvnw javadoc:javadoc - name: Deploy docs to pages From c82a2473523860619dd4984b2c597ff20aeaf97c Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 25 Jul 2024 13:58:21 +0200 Subject: [PATCH 22/34] [maven-release-plugin] prepare release unleash-client-java-9.2.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 2d825882c..473ea0edc 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.3-SNAPSHOT + 9.2.3 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-9.2.2 + unleash-client-java-9.2.3 From 65fa0693ce666cfbe3a32b94c88173ed9af3a726 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 25 Jul 2024 14:00:01 +0200 Subject: [PATCH 23/34] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 473ea0edc..50c7c4a34 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.3 + 9.2.4-SNAPSHOT 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-9.2.3 + unleash-client-java-9.2.2 From efef58804421ebcd1eaeb8d5334222f580130dd2 Mon Sep 17 00:00:00 2001 From: HyeonWoo Park <49335446+gogoadl@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:05:48 +0900 Subject: [PATCH 24/34] docs : Add explanation for gradle project (#247) Co-authored-by: hyunwooP --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c280868d6..8e4429c23 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ This section shows you how to get started quickly and explains some common confi ### Step 1: Install the Unleash Java SDK -You need to add the Unleash SDK as a dependency for your project. Here's how you would add it to your `pom.xml` file: +You need to add the Unleash SDK as a dependency for your project. Here's how you would add it to your `pom.xml` and `build.gradle` file: +**pom.xml** ```xml io.getunleash @@ -22,7 +23,10 @@ You need to add the Unleash SDK as a dependency for your project. Here's how you Latest version here ``` - +**build.gradle** +```gradle + implementation("io.getunleash:unleash-client-java:$unleashedVersion") +``` ### Step 2: Create a new Unleash instance From 60a135c09ac1ec42c0d52d3c8c8bd75299b8be4e Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 29 Jul 2024 15:55:36 +0200 Subject: [PATCH 25/34] fix: Always check parent dependency if present (#248) Fixes: #246 --- .../java/io/getunleash/DefaultUnleash.java | 136 +++++++++--------- .../DependentFeatureToggleTest.java | 26 ++++ 2 files changed, 91 insertions(+), 71 deletions(-) diff --git a/src/main/java/io/getunleash/DefaultUnleash.java b/src/main/java/io/getunleash/DefaultUnleash.java index 7480ec1e9..9abcdb9ba 100644 --- a/src/main/java/io/getunleash/DefaultUnleash.java +++ b/src/main/java/io/getunleash/DefaultUnleash.java @@ -179,56 +179,53 @@ private FeatureEvaluationResult getFeatureEvaluationResult( fallbackAction.test(toggleName, enhancedContext), defaultVariant); } else if (!featureToggle.isEnabled()) { return new FeatureEvaluationResult(false, defaultVariant); - } else if (featureToggle.getStrategies().isEmpty()) { - return new FeatureEvaluationResult( - true, VariantUtil.selectVariant(featureToggle, context, defaultVariant)); - } else { + } else if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) { // Dependent toggles, no point in evaluating child strategies if our dependencies are // not satisfied - if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) { - for (ActivationStrategy strategy : featureToggle.getStrategies()) { - Strategy configuredStrategy = getStrategy(strategy.getName()); - if (configuredStrategy == UNKNOWN_STRATEGY) { - LOGGER.warn( - "Unable to find matching strategy for toggle:{} strategy:{}", - toggleName, - strategy.getName()); - } + if (featureToggle.getStrategies().isEmpty()) { + return new FeatureEvaluationResult( + true, VariantUtil.selectVariant(featureToggle, context, defaultVariant)); + } + for (ActivationStrategy strategy : featureToggle.getStrategies()) { + Strategy configuredStrategy = getStrategy(strategy.getName()); + if (configuredStrategy == UNKNOWN_STRATEGY) { + LOGGER.warn( + "Unable to find matching strategy for toggle:{} strategy:{}", + toggleName, + strategy.getName()); + } - FeatureEvaluationResult result = - configuredStrategy.getResult( - strategy.getParameters(), - enhancedContext, - ConstraintMerger.mergeConstraints(featureRepository, strategy), - strategy.getVariants()); - - if (result.isEnabled()) { - Variant variant = result.getVariant(); - // If strategy variant is null, look for a variant in the featureToggle - if (variant == null) { - variant = - VariantUtil.selectVariant( - featureToggle, context, defaultVariant); - } - result.setVariant(variant); - return result; + FeatureEvaluationResult result = + configuredStrategy.getResult( + strategy.getParameters(), + enhancedContext, + ConstraintMerger.mergeConstraints(featureRepository, strategy), + strategy.getVariants()); + + if (result.isEnabled()) { + Variant variant = result.getVariant(); + // If strategy variant is null, look for a variant in the featureToggle + if (variant == null) { + variant = VariantUtil.selectVariant(featureToggle, context, defaultVariant); } + result.setVariant(variant); + return result; } } - return new FeatureEvaluationResult(false, defaultVariant); } + return new FeatureEvaluationResult(false, defaultVariant); } /** * Uses the old, statistically broken Variant seed for finding the correct variant * - * @deprecated * @param toggleName Name of the toggle * @param context The UnleashContext * @param fallbackAction What to do if we fail to find the toggle * @param defaultVariant If we can't resolve a variant, what are we returning * @return A wrapper containing whether the feature was enabled as well which Variant was * selected + * @deprecated */ private FeatureEvaluationResult deprecatedGetFeatureEvaluationResult( String toggleName, @@ -244,46 +241,43 @@ private FeatureEvaluationResult deprecatedGetFeatureEvaluationResult( fallbackAction.test(toggleName, enhancedContext), defaultVariant); } else if (!featureToggle.isEnabled()) { return new FeatureEvaluationResult(false, defaultVariant); - } else if (featureToggle.getStrategies().isEmpty()) { - return new FeatureEvaluationResult( - true, - VariantUtil.selectDeprecatedVariantHashingAlgo( - featureToggle, context, defaultVariant)); - } else { - // Dependent toggles, no point in evaluating child strategies if our dependencies are - // not satisfied - if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) { - for (ActivationStrategy strategy : featureToggle.getStrategies()) { - Strategy configuredStrategy = getStrategy(strategy.getName()); - if (configuredStrategy == UNKNOWN_STRATEGY) { - LOGGER.warn( - "Unable to find matching strategy for toggle:{} strategy:{}", - toggleName, - strategy.getName()); - } + } else if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) { + if (featureToggle.getStrategies().isEmpty()) { + return new FeatureEvaluationResult( + true, + VariantUtil.selectDeprecatedVariantHashingAlgo( + featureToggle, context, defaultVariant)); + } + for (ActivationStrategy strategy : featureToggle.getStrategies()) { + Strategy configuredStrategy = getStrategy(strategy.getName()); + if (configuredStrategy == UNKNOWN_STRATEGY) { + LOGGER.warn( + "Unable to find matching strategy for toggle:{} strategy:{}", + toggleName, + strategy.getName()); + } - FeatureEvaluationResult result = - configuredStrategy.getDeprecatedHashingAlgoResult( - strategy.getParameters(), - enhancedContext, - ConstraintMerger.mergeConstraints(featureRepository, strategy), - strategy.getVariants()); - - if (result.isEnabled()) { - Variant variant = result.getVariant(); - // If strategy variant is null, look for a variant in the featureToggle - if (variant == null) { - variant = - VariantUtil.selectDeprecatedVariantHashingAlgo( - featureToggle, context, defaultVariant); - } - result.setVariant(variant); - return result; + FeatureEvaluationResult result = + configuredStrategy.getDeprecatedHashingAlgoResult( + strategy.getParameters(), + enhancedContext, + ConstraintMerger.mergeConstraints(featureRepository, strategy), + strategy.getVariants()); + + if (result.isEnabled()) { + Variant variant = result.getVariant(); + // If strategy variant is null, look for a variant in the featureToggle + if (variant == null) { + variant = + VariantUtil.selectDeprecatedVariantHashingAlgo( + featureToggle, context, defaultVariant); } + result.setVariant(variant); + return result; } } - return new FeatureEvaluationResult(false, defaultVariant); } + return new FeatureEvaluationResult(false, defaultVariant); } private boolean isParentDependencySatisfied( @@ -393,10 +387,10 @@ public Variant getVariant(String toggleName, Variant defaultValue) { /** * Uses the old, statistically broken Variant seed for finding the correct variant * - * @deprecated * @param toggleName * @param context * @return + * @deprecated */ @Override public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { @@ -406,11 +400,11 @@ public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { /** * Uses the old, statistically broken Variant seed for finding the correct variant * - * @deprecated * @param toggleName * @param context * @param defaultValue * @return + * @deprecated */ @Override public Variant deprecatedGetVariant( @@ -437,9 +431,9 @@ private Variant deprecatedGetVariant( /** * Uses the old, statistically broken Variant seed for finding the correct variant * - * @deprecated * @param toggleName * @return + * @deprecated */ @Override public Variant deprecatedGetVariant(String toggleName) { @@ -449,10 +443,10 @@ public Variant deprecatedGetVariant(String toggleName) { /** * Uses the old, statistically broken Variant seed for finding the correct variant * - * @deprecated * @param toggleName * @param defaultValue * @return + * @deprecated */ @Override public Variant deprecatedGetVariant(String toggleName, Variant defaultValue) { diff --git a/src/test/java/io/getunleash/DependentFeatureToggleTest.java b/src/test/java/io/getunleash/DependentFeatureToggleTest.java index 84761aa91..194bf671f 100644 --- a/src/test/java/io/getunleash/DependentFeatureToggleTest.java +++ b/src/test/java/io/getunleash/DependentFeatureToggleTest.java @@ -176,4 +176,30 @@ public void should_trigger_impression_event_for_parent_variant_when_checking_chi when(featureRepository.getToggle(parentName)).thenReturn(parent); assertThat(sut.isEnabled(childName, UnleashContext.builder().build())).isFalse(); } + + @Test + public void childIsDisabledWhenChildDoesNotHaveStrategiesAndParentIsDisabled() { + FeatureToggle parent = + new FeatureToggle( + "parent", false, singletonList(new ActivationStrategy("default", null))); + FeatureDependency childDependsOnParent = new FeatureDependency("parant", true, emptyList()); + FeatureToggle child = + new FeatureToggle( + "child", + true, + emptyList(), + emptyList(), + true, + singletonList(childDependsOnParent)); + when(featureRepository.getToggle("child")).thenReturn(child); + when(featureRepository.getToggle("parent")).thenReturn(parent); + assertThat(sut.isEnabled("child", UnleashContext.builder().build())).isFalse(); + } + + @Test + public void shouldBeEnabledWhenMissingStrategies() { + FeatureToggle c = new FeatureToggle("c", true, emptyList()); + when(featureRepository.getToggle("c")).thenReturn(c); + assertThat(sut.isEnabled("c", UnleashContext.builder().build())).isTrue(); + } } From df165d54a35820283a66757e88620ae556f4027e Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 29 Jul 2024 15:59:46 +0200 Subject: [PATCH 26/34] chore: spotless format --- src/main/java/io/getunleash/util/ConstraintMerger.java | 3 +-- src/test/java/io/getunleash/util/IpAddressMatcherTest.java | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/getunleash/util/ConstraintMerger.java b/src/main/java/io/getunleash/util/ConstraintMerger.java index 7440decb7..28db759e2 100644 --- a/src/main/java/io/getunleash/util/ConstraintMerger.java +++ b/src/main/java/io/getunleash/util/ConstraintMerger.java @@ -17,8 +17,7 @@ public static List mergeConstraints( Optional.ofNullable(strategy.getConstraints()) .orElseGet(Collections::emptyList), Optional.ofNullable(strategy.getSegments()) - .orElseGet(Collections::emptyList) - .stream() + .orElseGet(Collections::emptyList).stream() .map(repository::getSegment) .map(s -> s == null ? DENY_SEGMENT : s) .map(Segment::getConstraints) diff --git a/src/test/java/io/getunleash/util/IpAddressMatcherTest.java b/src/test/java/io/getunleash/util/IpAddressMatcherTest.java index 8b2bf5cba..f9d48f37c 100644 --- a/src/test/java/io/getunleash/util/IpAddressMatcherTest.java +++ b/src/test/java/io/getunleash/util/IpAddressMatcherTest.java @@ -19,9 +19,7 @@ import org.junit.jupiter.api.Test; -/** - * @author Luke Taylor - */ +/** @author Luke Taylor */ class IpAddressMatcherTest { private final IpAddressMatcher v6matcher = new IpAddressMatcher("fe80::21f:5bff:fe33:bd68"); private final IpAddressMatcher v4matcher = new IpAddressMatcher("192.168.1.104"); From 97f63b50b98dcab43c9c28a6d685673c7d1c671a Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 29 Jul 2024 16:00:19 +0200 Subject: [PATCH 27/34] [maven-release-plugin] prepare release unleash-client-java-9.2.4 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 50c7c4a34..76f894dd7 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.4-SNAPSHOT + 9.2.4 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-9.2.2 + unleash-client-java-9.2.4 From 414734e91ca269a4d5c52312796a4d318f3bb7c2 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 29 Jul 2024 16:01:13 +0200 Subject: [PATCH 28/34] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 76f894dd7..b6b7c1ae1 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.4 + 9.2.5-SNAPSHOT 2.0.9 @@ -51,7 +51,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-9.2.4 + unleash-client-java-9.2.2 From 536f407e005828740af1f93c2c22b585d026ee12 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 22 Aug 2024 09:42:53 +0200 Subject: [PATCH 29/34] Setup coveralls action instead of ancient coveralls maven plugin (#249) * Setup coveralls action instead of ancient coveralls maven plugin * fix: rollback to versions that supports java 8 * Include finish step in main workflow as well. Thanks Gaston * Add checkout so git command from coverallsapp addon works --- .github/workflows/main.yml | 46 +++++++++++-------------- .github/workflows/pull_requests.yml | 32 ++++++++++++++--- .github/workflows/release_changelog.yml | 14 ++++++-- pom.xml | 42 ++++++++-------------- 4 files changed, 74 insertions(+), 60 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 66a52c778..bc82f8d02 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,14 +5,12 @@ on: branches: - main - - jobs: build: runs-on: ubuntu-latest strategy: matrix: - version: [8,11] + version: [8, 11, 17] steps: - name: Checkout uses: actions/checkout@v4 @@ -20,10 +18,18 @@ jobs: uses: actions/setup-java@v4 with: java-version: ${{ matrix.version }} - cache: 'maven' - distribution: 'temurin' + cache: "maven" + distribution: "temurin" - name: Build, test, coverage - run: ./mvnw clean test jacoco:report coveralls:report + run: ./mvnw clean test jacoco:report + - name: Coveralls + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + allow-empty: true + base-path: src/main/java + parallel: true + flag-name: run-jvm-${{ join(matrix.*, '-') }} - name: Notify Slack of pipeline completion uses: 8398a7/action-slack@v2 with: @@ -34,28 +40,18 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SLACK_WEBHOOK_URL: ${{ secrets.slack_webhook }} if: always() - java17: + finish: + needs: build + if: ${{ always() }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup Java - uses: actions/setup-java@v4 with: - java-version: 17 - distribution: 'temurin' - cache: 'maven' - - name: Build, test, coverage - run: ./mvnw clean test jacoco:report coveralls:report - env: - MAVEN_OPTS: "--add-opens java.base/java.net=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" - - name: Notify Slack of pipeline completion - uses: 8398a7/action-slack@v2 + fetch-depth: 0 + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 with: - status: ${{ job.status }} - author_name: Github Action - text: Build on Java 17 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SLACK_WEBHOOK_URL: ${{ secrets.slack_webhook }} - if: always() + parallel-finished: true + carryforward: run-jvm-8,run-jvm-11,run-jvm-17 + diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index 42896f0b5..00dedd4e6 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -1,13 +1,12 @@ on: pull_request: - jobs: build: runs-on: ubuntu-latest strategy: matrix: - version: [8,11,17] + version: [ 8, 11, 17 ] steps: - name: Checkout uses: actions/checkout@v4 @@ -15,7 +14,30 @@ jobs: uses: actions/setup-java@v4 with: java-version: ${{ matrix.version }} - cache: 'maven' - distribution: 'temurin' + cache: "maven" + distribution: "temurin" - name: Build, test, coverage - run: ./mvnw clean test jacoco:report coveralls:report + run: ./mvnw clean test jacoco:report + - name: Coveralls parallel + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + allow-empty: true + flag-name: run-jvm-${{ join(matrix.*, '-') }} + parallel: true + base-path: src/main/java + finish: + needs: build + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: run-jvm-8,run-jvm-11,run-jvm-17 diff --git a/.github/workflows/release_changelog.yml b/.github/workflows/release_changelog.yml index 1d8acdf00..01345cacc 100644 --- a/.github/workflows/release_changelog.yml +++ b/.github/workflows/release_changelog.yml @@ -1,8 +1,8 @@ -name: 'Releases' +name: "Releases" on: push: tags: - - 'unleash-client-java-*' + - "unleash-client-java-*" jobs: release: @@ -22,3 +22,13 @@ jobs: body: ${{ steps.github_release.outputs.changelog }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN}} + finish: + needs: build + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: run-jvm-8,run-jvm-11,run-jvm-17 diff --git a/pom.xml b/pom.xml index b6b7c1ae1..03a89916a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,14 +6,15 @@ 9.2.5-SNAPSHOT - 2.0.9 - 2.19.0 - 5.10.0 - 4.10.0 + 2.0.13 + 5.10.3 + 4.12.0 UTF-8 5.1.5 - 2.14.3 + 2.17.2 + 1.3.14 + 2.11.0 io.getunleash:unleash-client-java @@ -58,7 +59,7 @@ com.google.code.gson gson - 2.10.1 + ${version.gson} com.squareup.okhttp3 @@ -104,20 +105,20 @@ org.assertj assertj-core - 3.24.2 + 3.26.3 test org.mockito mockito-core - 4.8.0 + 4.8.1 test com.github.tomakehurst wiremock-jre8 - 2.35.1 + 2.35.2 test @@ -142,13 +143,13 @@ ch.qos.logback logback-core - 1.3.5 + ${version.logback} test ch.qos.logback logback-classic - 1.3.5 + ${version.logback} test @@ -218,7 +219,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.12 prepare-agent @@ -228,21 +229,6 @@ - - org.eluder.coveralls - coveralls-maven-plugin - 4.3.0 - - Z9wVezAubEVqnGPYkOUW031cqHPve2jBz - - - - javax.xml.bind - jaxb-api - 2.3.1 - - - @@ -334,7 +320,7 @@ com.diffplug.spotless spotless-maven-plugin - 2.28.0 + 2.30.0 From 71efb3cae9d4c27fb6b41a0ba9a0a9d900a081e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 4 Sep 2024 10:18:31 +0200 Subject: [PATCH 30/34] chore: add note about the missing versions in changelog (#251) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a5618f9..778c24eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +**Note:** Since 6.0.1 onwards, we moved the changelog into the release notes of each version. You will find hte details here https://github.com/Unleash/unleash-client-java/releases + ## 6.0.1 - Make connect and read timeouts configurable for both toggle fetching and metrics posting ## 6.0.0 From e4628ced601514ffa39676e1276021181bd60504 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 20 Nov 2024 12:56:29 +0100 Subject: [PATCH 31/34] chore: java cli example now accepts env variables for URL and Key --- .../example/AdvancedConstraints.java | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/examples/cli-example/src/main/java/io/getunleash/example/AdvancedConstraints.java b/examples/cli-example/src/main/java/io/getunleash/example/AdvancedConstraints.java index 912bdbe93..0faa50f83 100644 --- a/examples/cli-example/src/main/java/io/getunleash/example/AdvancedConstraints.java +++ b/examples/cli-example/src/main/java/io/getunleash/example/AdvancedConstraints.java @@ -8,27 +8,53 @@ import io.getunleash.util.UnleashConfig; public class AdvancedConstraints { + public static void main(String[] args) throws InterruptedException { - UnleashConfig config = UnleashConfig.builder().appName("client-example.advanced") - .customHttpHeader("Authorization", - "*:production.ZvzGdauVXYPyevrQVqnt8LSRHKuW") - .unleashAPI("http://localhost:1500/api").instanceId("example") + UnleashConfig config = UnleashConfig.builder() + .appName("client-example.advanced.java") + .customHttpHeader( + "Authorization", + getOrElse("UNLEASH_API_TOKEN", + "default:default.a45fede67f99b17f67312c93e00f448340e7af4ace2b0de2650f5a99")) + .unleashAPI(getOrElse("UNLEASH_API_URL", "http://localhost:3063/api")) + .instanceId("java-example") .synchronousFetchOnInitialisation(true) - .subscriber(new UnleashSubscriber() { - @Override - public void togglesFetched(FeatureToggleResponse toggleResponse) { - System.out.println(toggleResponse); - System.out.println(toggleResponse.getToggleCollection().getFeatures().size()); - } - }) + .sendMetricsInterval(30) + .subscriber( + new UnleashSubscriber() { + @Override + public void togglesFetched( + FeatureToggleResponse toggleResponse) { + System.out.println(toggleResponse); + System.out.println( + toggleResponse + .getToggleCollection() + .getFeatures() + .size()); + } + }) .build(); Unleash unleash = new DefaultUnleash(config); while (true) { Thread.sleep(2000); - UnleashContext context = UnleashContext.builder().addProperty("semver", "1.5.2").build(); - System.out.println(unleash.isEnabled("advanced.constraints", context)); // expect this to be true - UnleashContext smallerSemver = UnleashContext.builder().addProperty("semver", "1.1.0").build(); - System.out.println(unleash.isEnabled("advanced.constraints", smallerSemver)); // expect this to be false + UnleashContext context = UnleashContext.builder() + .addProperty("semver", "1.5.2") + .build(); + System.out.println( + unleash.isEnabled("advanced.constraints", context)); // expect this to be true + UnleashContext smallerSemver = UnleashContext.builder() + .addProperty("semver", "1.1.0") + .build(); + System.out.println( + unleash.isEnabled("advanced.constraints", smallerSemver)); // expect this to be false + } + } + + public static String getOrElse(String key, String defaultValue) { + String value = System.getenv(key); + if (value == null) { + return defaultValue; } + return value; } } From 26d7016c42f0a20f40c4aa2cb9930b5206c04832 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 20 Nov 2024 13:38:58 +0100 Subject: [PATCH 32/34] chore: spotless --- src/main/java/io/getunleash/util/ConstraintMerger.java | 3 ++- src/test/java/io/getunleash/util/IpAddressMatcherTest.java | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/getunleash/util/ConstraintMerger.java b/src/main/java/io/getunleash/util/ConstraintMerger.java index 28db759e2..7440decb7 100644 --- a/src/main/java/io/getunleash/util/ConstraintMerger.java +++ b/src/main/java/io/getunleash/util/ConstraintMerger.java @@ -17,7 +17,8 @@ public static List mergeConstraints( Optional.ofNullable(strategy.getConstraints()) .orElseGet(Collections::emptyList), Optional.ofNullable(strategy.getSegments()) - .orElseGet(Collections::emptyList).stream() + .orElseGet(Collections::emptyList) + .stream() .map(repository::getSegment) .map(s -> s == null ? DENY_SEGMENT : s) .map(Segment::getConstraints) diff --git a/src/test/java/io/getunleash/util/IpAddressMatcherTest.java b/src/test/java/io/getunleash/util/IpAddressMatcherTest.java index f9d48f37c..8b2bf5cba 100644 --- a/src/test/java/io/getunleash/util/IpAddressMatcherTest.java +++ b/src/test/java/io/getunleash/util/IpAddressMatcherTest.java @@ -19,7 +19,9 @@ import org.junit.jupiter.api.Test; -/** @author Luke Taylor */ +/** + * @author Luke Taylor + */ class IpAddressMatcherTest { private final IpAddressMatcher v6matcher = new IpAddressMatcher("fe80::21f:5bff:fe33:bd68"); private final IpAddressMatcher v4matcher = new IpAddressMatcher("192.168.1.104"); From 06b8f519694b76bf8736a6bbac5c7ce4b0d16de8 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 20 Nov 2024 13:39:33 +0100 Subject: [PATCH 33/34] [maven-release-plugin] prepare release unleash-client-java-9.2.5 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 03a89916a..b046135c2 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.5-SNAPSHOT + 9.2.5 2.0.13 @@ -52,7 +52,7 @@ https://github.com/Unleash/unleash-client-java scm:git:https://github.com/Unleash/unleash-client-java.git scm:git:https://github.com/Unleash/unleash-client-java.git - unleash-client-java-9.2.2 + unleash-client-java-9.2.5 From 19c1a3c67dad53fa3a1449c32fc1793b13b065bb Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 20 Nov 2024 13:41:41 +0100 Subject: [PATCH 34/34] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b046135c2..e3755ef01 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.getunleash unleash-client-java - 9.2.5 + 9.2.6-SNAPSHOT 2.0.13