diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactory.java index b6b2cccc466..04c3aaf097e 100644 --- a/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactory.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactory.java @@ -17,6 +17,7 @@ */ package com.datastax.oss.driver.internal.core.config.cloud; +import com.datastax.oss.driver.api.core.DriverTimeoutException; import com.datastax.oss.driver.api.core.metadata.EndPoint; import com.datastax.oss.driver.internal.core.metadata.SniEndPoint; import com.datastax.oss.driver.internal.core.ssl.SniSslEngineFactory; @@ -58,6 +59,8 @@ @ThreadSafe public class CloudConfigFactory { private static final Logger LOG = LoggerFactory.getLogger(CloudConfigFactory.class); + private static final int METADATA_RETRY_MAX_ATTEMPTS = 4; + private static final int METADATA_RETRY_INITIAL_DELAY_MS = 250; /** * Creates a {@link CloudConfig} with information fetched from the specified Cloud configuration * URL. @@ -225,22 +228,51 @@ protected TrustManagerFactory createTrustManagerFactory( @NonNull protected BufferedReader fetchProxyMetadata( @NonNull URL metadataServiceUrl, @NonNull SSLContext sslContext) throws IOException { - try { - HttpsURLConnection connection = (HttpsURLConnection) metadataServiceUrl.openConnection(); + HttpsURLConnection connection = null; + int attempt = 0; + while (attempt < METADATA_RETRY_MAX_ATTEMPTS) { + attempt++; + connection = (HttpsURLConnection) metadataServiceUrl.openConnection(); connection.setSSLSocketFactory(sslContext.getSocketFactory()); connection.setRequestMethod("GET"); connection.setRequestProperty("host", "localhost"); - return new BufferedReader( - new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); - } catch (ConnectException e) { - throw new IllegalStateException( - "Unable to connect to cloud metadata service. Please make sure your cluster is not parked or terminated", - e); - } catch (UnknownHostException e) { - throw new IllegalStateException( - "Unable to resolve host for cloud metadata service. Please make sure your cluster is not terminated", - e); + // if this is the last attempt, throw + // else if the response code is 401, 421, or 5xx, retry + // else, throw + if (attempt < METADATA_RETRY_MAX_ATTEMPTS + && (connection.getResponseCode() == 401 + || connection.getResponseCode() == 421 + || connection.getResponseCode() >= 500)) { + try { + Thread.sleep(METADATA_RETRY_INITIAL_DELAY_MS); + continue; + } catch (InterruptedException interruptedException) { + throw new IOException( + "Interrupted while waiting to retry metadata fetch", interruptedException); + } + } + + // last attempt, still 421 or 401 + if (connection.getResponseCode() == 421 || connection.getResponseCode() == 401) { + throw new IllegalStateException( + "Unable to fetch metadata from cloud metadata service. Please make sure your cluster is not parked or terminated"); + } + + try { + return new BufferedReader( + new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); + } catch (ConnectException e) { + throw new IllegalStateException( + "Unable to connect to cloud metadata service. Please make sure your cluster is not parked or terminated", + e); + } catch (UnknownHostException e) { + throw new IllegalStateException( + "Unable to resolve host for cloud metadata service. Please make sure your cluster is not terminated", + e); + } } + throw new DriverTimeoutException( + "Unable to fetch metadata from cloud metadata service"); // dead code } @NonNull diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactoryTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactoryTest.java index a0db82d298e..65495d6c726 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactoryTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/config/cloud/CloudConfigFactoryTest.java @@ -24,6 +24,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static org.assertj.core.api.Assertions.catchThrowable; import com.datastax.oss.driver.internal.core.ssl.SniSslEngineFactory; @@ -139,6 +140,57 @@ public void should_throw_when_metadata_not_found() throws Exception { assertThat(t).isInstanceOf(FileNotFoundException.class).hasMessageContaining("metadata"); } + @Test + public void should_retry_when_metadata_return_non_200() throws Exception { + // given, first attempt 421, subsequent attempts 200 + mockHttpSecureBundle(secureBundle()); + URL configFile = new URL("http", "localhost", wireMockRule.port(), BUNDLE_PATH); + stubFor( + any(urlPathEqualTo("/metadata")) + .inScenario("retry") + .whenScenarioStateIs(STARTED) + .willReturn(aResponse().withStatus(421)) + .willSetStateTo("second-200")); + stubFor( + any(urlPathEqualTo("/metadata")) + .inScenario("retry") + .whenScenarioStateIs("second-200") + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(jsonMetadata()))); + // when + CloudConfigFactory cloudConfigFactory = new CloudConfigFactory(); + CloudConfig cloudConfig = cloudConfigFactory.createCloudConfig(configFile); + // then + assertCloudConfig(cloudConfig); + } + + @Test + public void should_throw_illegal_state_when_421() throws Exception { + // given status code 421 + mockHttpSecureBundle(secureBundle()); + URL configFile = new URL("http", "localhost", wireMockRule.port(), BUNDLE_PATH); + stubFor(any(urlPathEqualTo("/metadata")).willReturn(aResponse().withStatus(421))); + // when + CloudConfigFactory cloudConfigFactory = new CloudConfigFactory(); + Throwable t = catchThrowable(() -> cloudConfigFactory.createCloudConfig(configFile)); + assertThat(t).isInstanceOf(IllegalStateException.class).hasMessageContaining("metadata"); + } + + @Test + public void should_throw_IOException_when_500() throws Exception { + // given status code 500 + mockHttpSecureBundle(secureBundle()); + URL configFile = new URL("http", "localhost", wireMockRule.port(), BUNDLE_PATH); + stubFor(any(urlPathEqualTo("/metadata")).willReturn(aResponse().withStatus(500))); + // when + CloudConfigFactory cloudConfigFactory = new CloudConfigFactory(); + Throwable t = catchThrowable(() -> cloudConfigFactory.createCloudConfig(configFile)); + assertThat(t).isInstanceOf(IOException.class).hasMessageContaining("500"); + } + @Test public void should_throw_when_metadata_not_readable() throws Exception { // given