-
Notifications
You must be signed in to change notification settings - Fork 0
fix: Simplify refresh logic on a failed fetch #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3502432
8a7d553
4ea2f9b
111a4be
626dd21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,7 +43,7 @@ private static String loadProviderVersion() { | |
| this.config = config; | ||
| } | ||
|
|
||
| Boolean haveFeatureTogglesChanged(byte[] contentHash) { | ||
| Boolean haveFeatureTogglesChanged(byte[] contentHash) throws IOException, InterruptedException { | ||
| if (contentHash.length == 0) { | ||
| return true; | ||
| } | ||
|
|
@@ -55,18 +55,12 @@ Boolean haveFeatureTogglesChanged(byte[] contentHash) { | |
| .header("Authorization", String.format("Bearer %s", config.getClientIdentifier())) | ||
| .header("X-Octopus-Client", buildOctopusClientHeaderValue()) | ||
| .build(); | ||
| try { | ||
| HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString()); | ||
| FeatureToggleCheckResponse checkResponse = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), FeatureToggleCheckResponse.class); | ||
| return !Arrays.equals(checkResponse.contentHash, contentHash); | ||
| } catch (Exception e) { | ||
| logger.log(System.Logger.Level.WARNING, String.format("Unable to query Octopus Feature Toggle service. URI: %s", checkURI.toString()), e); | ||
| // Use cached toggles | ||
| return false; | ||
| } | ||
| HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString()); | ||
| FeatureToggleCheckResponse checkResponse = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), FeatureToggleCheckResponse.class); | ||
| return !Arrays.equals(checkResponse.contentHash, contentHash); | ||
| } | ||
|
|
||
| FeatureToggles getFeatureToggleEvaluationManifest() { | ||
| FeatureToggles getFeatureToggleEvaluationManifest() throws IOException, InterruptedException { | ||
| URI manifestURI = getManifestURI(); | ||
| HttpClient client = HttpClient.newHttpClient(); | ||
| HttpRequest request = HttpRequest.newBuilder() | ||
|
|
@@ -75,23 +69,18 @@ FeatureToggles getFeatureToggleEvaluationManifest() { | |
| .header("Authorization", String.format("Bearer %s", config.getClientIdentifier())) | ||
| .header("X-Octopus-Client", buildOctopusClientHeaderValue()) | ||
| .build(); | ||
| try { | ||
| HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString()); | ||
| if (httpResponse.statusCode() == StatusCodeNotFound) { | ||
| logger.log(System.Logger.Level.WARNING, String.format("Failed to retrieve feature toggles for client identifier %s from %s", config.getClientIdentifier(), manifestURI.toString())); | ||
| return null; | ||
| } | ||
| Optional<String> contentHashHeader = httpResponse.headers().firstValue("ContentHash"); | ||
| if (contentHashHeader.isEmpty()) { | ||
| logger.log(System.Logger.Level.WARNING, String.format("Feature toggle response from %s did not contain expected ContentHash header", manifestURI.toString())); | ||
| return null; | ||
| } | ||
| var evaluations = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), new TypeReference<List<FeatureToggleEvaluation>>() {}); | ||
| return new FeatureToggles(evaluations, Base64.getDecoder().decode(contentHashHeader.get())); | ||
| } catch (Exception e) { | ||
| logger.log(System.Logger.Level.WARNING, "Unable to query Octopus Feature Toggle service", e); | ||
| HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString()); | ||
| if (httpResponse.statusCode() == StatusCodeNotFound) { | ||
| logger.log(System.Logger.Level.WARNING, String.format("Failed to retrieve feature toggles for client identifier %s from %s", config.getClientIdentifier(), manifestURI.toString())); | ||
| return null; | ||
| } | ||
| Optional<String> contentHashHeader = httpResponse.headers().firstValue("ContentHash"); | ||
| if (contentHashHeader.isEmpty()) { | ||
| logger.log(System.Logger.Level.WARNING, String.format("Feature toggle response from %s did not contain expected ContentHash header", manifestURI.toString())); | ||
| return null; | ||
| } | ||
| var evaluations = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), new TypeReference<List<FeatureToggleEvaluation>>() {}); | ||
| return new FeatureToggles(evaluations, Base64.getDecoder().decode(contentHashHeader.get())); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change removes error handling for consistency with the |
||
| } | ||
|
|
||
| String buildOctopusClientHeaderValue() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.concurrent.CopyOnWriteArrayList; | ||
| import java.util.logging.Handler; | ||
| import java.util.logging.LogRecord; | ||
| import java.util.logging.Logger; | ||
|
|
@@ -105,6 +106,9 @@ void whenInitialized_AndRefreshFails_RetainsExistingContextAndLogsError() throws | |
| try { | ||
| provider.initialize(); | ||
|
|
||
| // Stop warnings about refresh failures from reaching the test output | ||
| julLogger.setUseParentHandlers(false); | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. QUESTION: The tests involving refresh failures were outputting a lot of scary looking logs - which is expected as we're throwing an error every time a refresh fails. I've added these lines to keep the test output clean, however this could potentially hide other more useful logs. What is the better option here between hiding logs and showing too many? Here is the combined test output before adding these limitations to the test loggers.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't feel strongly either way, but given we are adding the logger to assert against, this test output is a side effect and it is reasonable to suppress it. |
||
| // Simulate a failed fetch | ||
| client.changeToggles(null); | ||
|
|
||
|
|
@@ -118,6 +122,153 @@ void whenInitialized_AndRefreshFails_RetainsExistingContextAndLogsError() throws | |
|
|
||
| } finally { | ||
| julLogger.removeHandler(handler); | ||
| julLogger.setUseParentHandlers(true); | ||
| provider.shutdown(); | ||
| } | ||
| } | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tests below have been translated straight across from my .NET PR for the same ticket. |
||
| @Test | ||
| void whenInitialFetchReturnsNothing_AndRefreshSucceeds_ContextIsPopulated() throws InterruptedException { | ||
|
|
||
| byte[] contentHash = {0x01, 0x02, 0x03, 0x04}; | ||
|
|
||
| var julLogger = Logger.getLogger(OctopusClient.class.getName()); | ||
| julLogger.setUseParentHandlers(false); | ||
|
|
||
| // Initialize with null so first fetch fails | ||
| var client = new MockOctopusFeatureClient(null); | ||
| var provider = new OctopusContextProvider(configuration, client); | ||
| provider.initialize(); | ||
|
|
||
| try { | ||
| // Check that the context is empty | ||
| assertThat(provider.getOctopusContext().getContentHash()).isEmpty(); | ||
|
|
||
| // Update client to return valid toggles and wait for refresh | ||
| client.changeToggles(new FeatureToggles( | ||
| List.of(new FeatureToggleEvaluation("test-feature", false, "evaluation-key", Collections.emptyList(), 100)), | ||
| contentHash | ||
| )); | ||
| Thread.sleep(5000); | ||
|
|
||
| // Assert that the context is now correctly populated | ||
| assertThat(provider.getOctopusContext().getContentHash()).isEqualTo(contentHash); | ||
|
|
||
| } finally { | ||
| julLogger.setUseParentHandlers(true); | ||
| provider.shutdown(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void whenRefreshReturnsNothing_AndSubsequentRefreshSucceeds_ContextIsUpdated() throws InterruptedException { | ||
|
|
||
| byte[] initialHash = {0x01, 0x02, 0x03, 0x04}; | ||
| byte[] updatedHash = {0x01, 0x02, 0x03, 0x05}; | ||
|
|
||
| var logMessages = new CopyOnWriteArrayList<String>(); | ||
| var julLogger = Logger.getLogger(OctopusClient.class.getName()); | ||
| var handler = new Handler() { | ||
| @Override public void publish(LogRecord record) { logMessages.add(record.getMessage()); } | ||
| @Override public void flush() {} | ||
| @Override public void close() {} | ||
| }; | ||
| julLogger.addHandler(handler); | ||
|
|
||
| // initialize with a client that returns valid toggles | ||
| var client = new MockOctopusFeatureClient(new FeatureToggles( | ||
| List.of(new FeatureToggleEvaluation("test-feature", true, "evaluation-key", Collections.emptyList(), 100)), | ||
| initialHash | ||
| )); | ||
| var provider = new OctopusContextProvider(configuration, client); | ||
| provider.initialize(); | ||
|
|
||
| // Stop warnings about refresh failures from reaching the test output | ||
| julLogger.setUseParentHandlers(false); | ||
|
|
||
| try { | ||
| // Switch to a null client and wait for refresh to fail | ||
| client.changeToggles(null); | ||
| Thread.sleep(5000); | ||
|
|
||
| // Assert that failed refresh is logged and old context is retained | ||
| assertThat(logMessages).anyMatch(m -> m.startsWith("Failed to retrieve updated feature manifest")); | ||
| assertThat(provider.getOctopusContext().getContentHash()).isEqualTo(initialHash); | ||
|
|
||
| // Update client to return valid toggles again and wait for refresh | ||
| client.changeToggles(new FeatureToggles( | ||
| List.of(new FeatureToggleEvaluation("test-feature", false, "evaluation-key", Collections.emptyList(), 100)), | ||
| updatedHash | ||
| )); | ||
| Thread.sleep(5000); | ||
|
|
||
| assertThat(provider.getOctopusContext().getContentHash()).isEqualTo(updatedHash); | ||
|
|
||
| } finally { | ||
| julLogger.removeHandler(handler); | ||
| julLogger.setUseParentHandlers(true); | ||
| provider.shutdown(); | ||
| } | ||
| } | ||
|
|
||
| static class ThrowsOnRefreshClient extends OctopusClient { | ||
|
|
||
| static final String ERROR_MESSAGE = "Oops! Simulated refresh error"; | ||
| private final FeatureToggles initial; | ||
|
|
||
| ThrowsOnRefreshClient(FeatureToggles initial) { | ||
| super(null); | ||
| this.initial = initial; | ||
| } | ||
|
|
||
| @Override | ||
| Boolean haveFeatureTogglesChanged(byte[] contentHash) { | ||
| throw new RuntimeException(ERROR_MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| FeatureToggles getFeatureToggleEvaluationManifest() { | ||
| return initial; | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void whenAnExceptionIsThrownDuringRefresh_LogsErrorDetails() throws InterruptedException { | ||
|
|
||
| byte[] contentHash = {0x01, 0x02, 0x03, 0x04}; | ||
|
|
||
| var logRecords = new CopyOnWriteArrayList<LogRecord>(); | ||
| var julLogger = Logger.getLogger(OctopusClient.class.getName()); | ||
| var handler = new Handler() { | ||
| @Override public void publish(LogRecord record) { logRecords.add(record); } | ||
| @Override public void flush() {} | ||
| @Override public void close() {} | ||
| }; | ||
| julLogger.addHandler(handler); | ||
| // Stop warnings about refresh failures from reaching the test output | ||
| julLogger.setUseParentHandlers(false); | ||
|
|
||
| // Initialize with a client that will throw on refresh | ||
| var client = new ThrowsOnRefreshClient(new FeatureToggles( | ||
| List.of(new FeatureToggleEvaluation("test-feature", true, "evaluation-key", Collections.emptyList(), 100)), | ||
| contentHash | ||
| )); | ||
| var provider = new OctopusContextProvider(configuration, client); | ||
| provider.initialize(); | ||
|
|
||
| try { | ||
| // Wait for cache to expire and refresh attempt to throw | ||
| Thread.sleep(500); | ||
|
|
||
| assertThat(logRecords).anyMatch(r -> | ||
| r.getMessage().startsWith("Failed to retrieve updated feature manifest") | ||
| && r.getThrown() != null | ||
| && r.getThrown().getMessage().contains(ThrowsOnRefreshClient.ERROR_MESSAGE) | ||
| ); | ||
|
|
||
| } finally { | ||
| julLogger.removeHandler(handler); | ||
| julLogger.setUseParentHandlers(true); | ||
| provider.shutdown(); | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change removes error handling for consistency with the
HaveFeaturesChangedfunction in the .NET provider. Errors here will now bubble up to be logged in the catch blocks of the OctopusContextProvider'srefresh()function.