|
45 | 45 | import org.junit.jupiter.api.Nested; |
46 | 46 | import org.junit.jupiter.api.Test; |
47 | 47 | import org.junit.jupiter.api.extension.ExtendWith; |
| 48 | +import org.mockito.ArgumentCaptor; |
48 | 49 | import org.mockito.Mock; |
49 | 50 | import org.mockito.junit.jupiter.MockitoExtension; |
50 | 51 |
|
@@ -163,6 +164,41 @@ void realHttpClientSendsAgentlessHeadersAndReadsResponse() throws Exception { |
163 | 164 | } |
164 | 165 | } |
165 | 166 |
|
| 167 | + @Test |
| 168 | + void downloadsAndAppliesLargeUfcWithoutPayloadLimit() throws Exception { |
| 169 | + final int flagCount = 5_000; |
| 170 | + final String largeConfig = largeConfig(flagCount); |
| 171 | + assertTrue(largeConfig.getBytes(UTF_8).length > 500_000); |
| 172 | + |
| 173 | + try (JavaTestHttpServer server = |
| 174 | + JavaTestHttpServer.httpServer( |
| 175 | + s -> s.handlers(h -> h.get(CONFIG_PATH, api -> api.getResponse().send(largeConfig))))) { |
| 176 | + final HttpUrl endpoint = HttpUrl.get(server.getAddress().resolve(CONFIG_PATH)); |
| 177 | + final OkHttpClient httpClient = new OkHttpClient.Builder().build(); |
| 178 | + final AgentlessConfigurationSource service = |
| 179 | + new AgentlessConfigurationSource( |
| 180 | + endpoint, |
| 181 | + config(), |
| 182 | + 30_000, |
| 183 | + new AgentlessConfigurationSource.OkHttpUfcHttpClient(httpClient), |
| 184 | + Executors.newSingleThreadScheduledExecutor()); |
| 185 | + final ArgumentCaptor<ServerConfiguration> configuration = |
| 186 | + ArgumentCaptor.forClass(ServerConfiguration.class); |
| 187 | + FeatureFlaggingGateway.addConfigListener(listener); |
| 188 | + |
| 189 | + try { |
| 190 | + assertTrue(service.pollOnce()); |
| 191 | + |
| 192 | + verify(listener).accept(configuration.capture()); |
| 193 | + assertEquals(flagCount, configuration.getValue().flags.size()); |
| 194 | + } finally { |
| 195 | + service.close(); |
| 196 | + httpClient.dispatcher().executorService().shutdownNow(); |
| 197 | + httpClient.connectionPool().evictAll(); |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + |
166 | 202 | @Test |
167 | 203 | void realHttpClientAllowsMissingEtagAndEmptyResponseBody() throws Exception { |
168 | 204 | try (JavaTestHttpServer server = |
@@ -524,33 +560,73 @@ void retriesServerErrorThenKeepsColdStateOnNotModified() throws Exception { |
524 | 560 | } |
525 | 561 |
|
526 | 562 | @Test |
527 | | - void givesUpAfterRetryableFailuresAreExhausted() throws Exception { |
| 563 | + void warnsRateLimitedAfterRetryableFailuresAreExhausted() throws Exception { |
| 564 | + final RatelimitedLogger ratelimitedLogger = mock(RatelimitedLogger.class); |
528 | 565 | final FakeClient client = |
529 | 566 | new FakeClient( |
530 | 567 | response(503, null, null), response(503, null, null), response(503, null, null)); |
531 | | - final AgentlessConfigurationSource service = service(client); |
| 568 | + final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); |
| 569 | + final AgentlessConfigurationSource service = |
| 570 | + new AgentlessConfigurationSource( |
| 571 | + HttpUrl.get("http://localhost" + CONFIG_PATH), |
| 572 | + config(), |
| 573 | + 30_000, |
| 574 | + client, |
| 575 | + executor, |
| 576 | + delay -> {}, |
| 577 | + () -> 1.0, |
| 578 | + ratelimitedLogger); |
532 | 579 | FeatureFlaggingGateway.addConfigListener(listener); |
533 | 580 |
|
534 | | - assertFalse(service.pollOnce()); |
| 581 | + try { |
| 582 | + assertFalse(service.pollOnce()); |
535 | 583 |
|
536 | | - assertEquals(3, client.calls.get()); |
537 | | - verifyNoInteractions(listener); |
| 584 | + assertEquals(3, client.calls.get()); |
| 585 | + verify(ratelimitedLogger) |
| 586 | + .warn( |
| 587 | + "Feature Flagging agentless endpoint failed after {} attempts with HTTP {}", 3, 503); |
| 588 | + verifyNoInteractions(listener); |
| 589 | + } finally { |
| 590 | + service.close(); |
| 591 | + } |
538 | 592 | } |
539 | 593 |
|
540 | 594 | @Test |
541 | | - void givesUpAfterIoFailuresAreExhausted() throws Exception { |
| 595 | + void warnsRateLimitedAfterIoFailuresAreExhausted() throws Exception { |
| 596 | + final RatelimitedLogger ratelimitedLogger = mock(RatelimitedLogger.class); |
| 597 | + final SocketTimeoutException finalFailure = |
| 598 | + new SocketTimeoutException("slow HTTP configuration source"); |
542 | 599 | final FakeClient client = |
543 | 600 | new FakeClient( |
544 | 601 | new SocketTimeoutException("slow HTTP configuration source"), |
545 | 602 | new SocketTimeoutException("slow HTTP configuration source"), |
546 | | - new SocketTimeoutException("slow HTTP configuration source")); |
547 | | - final AgentlessConfigurationSource service = service(client); |
| 603 | + finalFailure); |
| 604 | + final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); |
| 605 | + final AgentlessConfigurationSource service = |
| 606 | + new AgentlessConfigurationSource( |
| 607 | + HttpUrl.get("http://localhost" + CONFIG_PATH), |
| 608 | + config(), |
| 609 | + 30_000, |
| 610 | + client, |
| 611 | + executor, |
| 612 | + delay -> {}, |
| 613 | + () -> 1.0, |
| 614 | + ratelimitedLogger); |
548 | 615 | FeatureFlaggingGateway.addConfigListener(listener); |
549 | 616 |
|
550 | | - assertFalse(service.pollOnce()); |
| 617 | + try { |
| 618 | + assertFalse(service.pollOnce()); |
551 | 619 |
|
552 | | - assertEquals(3, client.calls.get()); |
553 | | - verifyNoInteractions(listener); |
| 620 | + assertEquals(3, client.calls.get()); |
| 621 | + verify(ratelimitedLogger) |
| 622 | + .warn( |
| 623 | + "Feature Flagging agentless endpoint request failed after {} attempts", |
| 624 | + 3, |
| 625 | + finalFailure); |
| 626 | + verifyNoInteractions(listener); |
| 627 | + } finally { |
| 628 | + service.close(); |
| 629 | + } |
554 | 630 | } |
555 | 631 |
|
556 | 632 | @Test |
@@ -638,6 +714,39 @@ void repeatedInitStartsOnlyOnePoller() throws Exception { |
638 | 714 | assertEquals(1, client.calls.get()); |
639 | 715 | } |
640 | 716 |
|
| 717 | + @Test |
| 718 | + void scheduledPollContinuesAfterListenerRuntimeException() throws Exception { |
| 719 | + final FakeClient client = |
| 720 | + new FakeClient( |
| 721 | + response(200, "etag-a", emptyConfig()), response(200, "etag-b", emptyConfig())); |
| 722 | + final AgentlessConfigurationSource service = |
| 723 | + new AgentlessConfigurationSource( |
| 724 | + HttpUrl.get("http://localhost" + CONFIG_PATH), |
| 725 | + config(), |
| 726 | + 10, |
| 727 | + client, |
| 728 | + Executors.newSingleThreadScheduledExecutor()); |
| 729 | + final AtomicInteger listenerCalls = new AtomicInteger(); |
| 730 | + final FeatureFlaggingGateway.ConfigListener flakyListener = |
| 731 | + configuration -> { |
| 732 | + if (listenerCalls.incrementAndGet() == 1) { |
| 733 | + throw new IllegalStateException("listener rejected first configuration"); |
| 734 | + } |
| 735 | + }; |
| 736 | + FeatureFlaggingGateway.addConfigListener(flakyListener); |
| 737 | + |
| 738 | + try { |
| 739 | + service.init(); |
| 740 | + awaitCalls(client, 2); |
| 741 | + |
| 742 | + assertEquals(2, listenerCalls.get()); |
| 743 | + assertNull(client.requests.get(1).etag); |
| 744 | + } finally { |
| 745 | + service.close(); |
| 746 | + FeatureFlaggingGateway.removeConfigListener(flakyListener); |
| 747 | + } |
| 748 | + } |
| 749 | + |
641 | 750 | @Test |
642 | 751 | void closeCancelsInFlightRequestAndIgnoresLateSuccess() throws Exception { |
643 | 752 | final CountDownLatch requestStarted = new CountDownLatch(1); |
@@ -823,6 +932,30 @@ private static String emptyConfig() { |
823 | 932 | + "}"; |
824 | 933 | } |
825 | 934 |
|
| 935 | + private static String largeConfig(final int flagCount) { |
| 936 | + final StringBuilder json = |
| 937 | + new StringBuilder( |
| 938 | + "{\"createdAt\":\"2024-04-17T19:40:53.716Z\"," |
| 939 | + + "\"format\":\"SERVER\"," |
| 940 | + + "\"environment\":{\"name\":\"Large Test\"}," |
| 941 | + + "\"flags\":{"); |
| 942 | + for (int index = 0; index < flagCount; index++) { |
| 943 | + if (index > 0) { |
| 944 | + json.append(','); |
| 945 | + } |
| 946 | + final String flagKey = "large-flag-" + index; |
| 947 | + json.append('"') |
| 948 | + .append(flagKey) |
| 949 | + .append("\":{\"key\":\"") |
| 950 | + .append(flagKey) |
| 951 | + .append( |
| 952 | + "\",\"enabled\":true,\"variationType\":\"STRING\"," |
| 953 | + + "\"variations\":{\"on\":{\"key\":\"on\",\"value\":\"on\"}}," |
| 954 | + + "\"allocations\":[]}"); |
| 955 | + } |
| 956 | + return json.append("}}").toString(); |
| 957 | + } |
| 958 | + |
826 | 959 | private static void awaitCalls(final FakeClient client, final int count) throws Exception { |
827 | 960 | for (int i = 0; i < 100; i++) { |
828 | 961 | if (client.calls.get() >= count) { |
|
0 commit comments