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] 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(); + } + } }