diff --git a/NOTICE b/NOTICE index 4c408c6e65feaa0b7ffa2b5a434e33d58a3b25d1..f26b38d79a26e4ed4242828fb75b1e2c06f17ff0 100644 --- a/NOTICE +++ b/NOTICE @@ -382,10 +382,10 @@ The following software have components provided under the terms of this license: - Microsoft Azure Java Core Library (from https://github.com/Azure/azure-sdk-for-java) - Microsoft Azure Netty HTTP Client Library (from https://github.com/Azure/azure-sdk-for-java) - Microsoft Azure SDK for SQL API of Azure Cosmos DB Service (from https://github.com/Azure/azure-sdk-for-java) +- Mockito (from http://www.mockito.org) - Mockito (from http://mockito.org) - Mockito (from http://mockito.org) - Mockito (from http://mockito.org) -- Mockito (from http://www.mockito.org) - Mojo's Maven plugin for Cobertura (from http://mojo.codehaus.org/cobertura-maven-plugin/) - MongoDB Driver (from http://www.mongodb.org) - MongoDB Java Driver Core (from http://www.mongodb.org) @@ -487,6 +487,8 @@ The following software have components provided under the terms of this license: - StAX (from http://stax.codehaus.org/) - StAX API (from http://stax.codehaus.org/) - T-Digest (from https://github.com/tdunning/t-digest) +- Vavr (from http://vavr.io) +- Vavr Match (from http://vavr.io) - Woodstox (from https://github.com/FasterXML/woodstox) - Xerces2-j (from https://xerces.apache.org/xerces2-j/) - aalto-xml (from ) @@ -561,6 +563,8 @@ The following software have components provided under the terms of this license: - proton-j (from ) - rank-eval (from https://github.com/elastic/elasticsearch) - rank-eval (from https://github.com/elastic/elasticsearch) +- resilience4j (from https://github.com/resilience4j/resilience4j) +- resilience4j (from https://github.com/resilience4j/resilience4j) - rest (from https://github.com/elastic/elasticsearch) - rest (from https://github.com/elastic/elasticsearch) - rest-high-level (from https://github.com/elastic/elasticsearch) diff --git a/provider/indexer-azure/pom.xml b/provider/indexer-azure/pom.xml index 764fabd09e41e39c7aa40402da08f2e7102b1d97..ab260aa375a132903c8f3656bb9be98976e91219 100644 --- a/provider/indexer-azure/pom.xml +++ b/provider/indexer-azure/pom.xml @@ -203,6 +203,18 @@ <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency> + <!-- Resilience4j Dependencies--> + <dependency> + <groupId>io.github.resilience4j</groupId> + <artifactId>resilience4j-retry</artifactId> + <version>1.7.0</version> + </dependency> + <dependency> + <groupId>io.github.resilience4j</groupId> + <artifactId>resilience4j-core</artifactId> + <version>1.7.0</version> + </dependency> + <!-- Test Dependencies --> <dependency> <groupId>org.springframework.boot</groupId> diff --git a/provider/indexer-azure/src/main/java/org/opengroup/osdu/indexer/azure/service/RetryPolicy.java b/provider/indexer-azure/src/main/java/org/opengroup/osdu/indexer/azure/service/RetryPolicy.java new file mode 100644 index 0000000000000000000000000000000000000000..47d906c9ad7e83e29b112ea8339117877503a430 --- /dev/null +++ b/provider/indexer-azure/src/main/java/org/opengroup/osdu/indexer/azure/service/RetryPolicy.java @@ -0,0 +1,81 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.opengroup.osdu.indexer.azure.service; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.github.resilience4j.retry.RetryConfig; +import lombok.Data; +import lombok.extern.java.Log; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.HttpResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +/** + * This class handles retry configuration logic for calls made to <prefix>/storage/v2/query/records:batch + * to resolve intermittent CosmosDb Not found issue + */ + +@Log +@Component +@Data +@ConfigurationProperties(prefix = "azure.storage.client.retry") +public class RetryPolicy { + + @Autowired + private JaxRsDpsLog logger; + + private int attempts = 3; + private int waitDuration = 1000; + private final String RECORD_NOT_FOUND = "notFound"; + + /** + * @return RetryConfig with 3 attempts and 1 sec wait time + */ + public RetryConfig retryConfig() { + return RetryConfig.<HttpResponse>custom() + .maxAttempts(attempts) + .waitDuration(Duration.ofMillis(waitDuration)) + .retryOnResult(response -> isRetryRequired(response)) + .build(); + } + + /** + * Unfound records get listed under a JsonArray "notFound" in the http json response + * @param response + * @return if there are elements in "notFound" returns true, else false + */ + private boolean isRetryRequired(HttpResponse response) { + if (response == null || response.getBody().isEmpty()) { + return false; + } + JsonObject jsonObject = new JsonParser().parse(response.getBody()).getAsJsonObject(); + JsonElement notFoundElement = (JsonArray) jsonObject.get(RECORD_NOT_FOUND); + if (notFoundElement == null || + !notFoundElement.isJsonArray() || + notFoundElement.getAsJsonArray().size() == 0 || + notFoundElement.getAsJsonArray().isJsonNull()) { + return false; + } + log.info("Retry is set true"); + return true; + } +} diff --git a/provider/indexer-azure/src/main/java/org/opengroup/osdu/indexer/azure/service/UrlFetchServiceAzureImpl.java b/provider/indexer-azure/src/main/java/org/opengroup/osdu/indexer/azure/service/UrlFetchServiceAzureImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d56b888fafaf747bd51f97658e7bfd255a0f31e9 --- /dev/null +++ b/provider/indexer-azure/src/main/java/org/opengroup/osdu/indexer/azure/service/UrlFetchServiceAzureImpl.java @@ -0,0 +1,95 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.opengroup.osdu.indexer.azure.service; + +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import io.github.resilience4j.retry.RetryRegistry; +import org.opengroup.osdu.core.common.http.FetchServiceHttpRequest; +import org.opengroup.osdu.core.common.http.IUrlFetchService; +import org.opengroup.osdu.core.common.http.UrlFetchServiceImpl; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.HttpResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.springframework.web.context.annotation.RequestScope; + +import java.net.URISyntaxException; +import java.util.function.Supplier; + +/** + * This class has same function as that of UrlFetchService except in the case of + * <prefix>/storage/v2/query/records:batch call for which it enables retry + */ + +@Service +@RequestScope +@Primary +public class UrlFetchServiceAzureImpl implements IUrlFetchService { + + public static final String STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST_URL = "storage/v2/query/records:batch"; + + @Autowired + private RetryPolicy policy; + + @Autowired + private UrlFetchServiceImpl urlFetchService; + + @Autowired + private JaxRsDpsLog logger; + /** + * this method invokes retryFunction only for <prefix>/storage/v2/query/records:batch + * calls otherwise invokes UrlFetchService.sendRequest(FetchServiceHttpRequest request) + * + * @param httpRequest + * @return + * @throws URISyntaxException + */ + @Override + public HttpResponse sendRequest(FetchServiceHttpRequest httpRequest) throws URISyntaxException { + HttpResponse output; + if (httpRequest.getUrl().contains(STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST_URL)) { + output = this.retryFunction(httpRequest); + if (output != null) { + return output; + } + } + return this.urlFetchService.sendRequest(httpRequest); + } + + /** + * decorates UrlFetchService.sendRequest(FetchServiceHttpRequest request) + * with retry configurations in RetryPolicy + * + * @param request + * @return null if URISyntaxException is caught else returns HttpResponse + */ + private HttpResponse retryFunction(FetchServiceHttpRequest request) { + RetryConfig config = this.policy.retryConfig(); + RetryRegistry registry = RetryRegistry.of(config); + Retry retry = registry.retry("retryPolicy", config); + + Supplier<HttpResponse> urlFetchServiceSupplier = () -> { + try { + return this.urlFetchService.sendRequest(request); + } catch (URISyntaxException e) { + logger.error("HttpResponse is null due to URISyntaxException. " + e.getReason()); + return null; + } + }; + return (urlFetchServiceSupplier == null) ? null : Retry.decorateSupplier(retry, urlFetchServiceSupplier).get(); + } +} diff --git a/provider/indexer-azure/src/test/java/org/opengroup/osdu/indexer/azure/service/RetryPolicyTest.java b/provider/indexer-azure/src/test/java/org/opengroup/osdu/indexer/azure/service/RetryPolicyTest.java new file mode 100644 index 0000000000000000000000000000000000000000..70a7fcb9e0e7bdbb2da003a5953645e8cf91cfc3 --- /dev/null +++ b/provider/indexer-azure/src/test/java/org/opengroup/osdu/indexer/azure/service/RetryPolicyTest.java @@ -0,0 +1,163 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.opengroup.osdu.indexer.azure.service; + +import io.github.resilience4j.retry.RetryConfig; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.opengroup.osdu.core.common.http.FetchServiceHttpRequest; +import org.opengroup.osdu.core.common.http.UrlFetchServiceImpl; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.HttpResponse; + +import java.util.function.Predicate; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class RetryPolicyTest { + + private static final String JSON_RESPONSE_WITH_NOT_FOUND = "{\n" + + " \"records\": [\n" + + " {\n" + + " \"data\": {\n" + + " \"Spuddate\": \"atspud\",\n" + + " \"UWI\": \"atuwi\",\n" + + " \"latitude\": \"latitude\",\n" + + " \"longitude\": \"longitude\"\n" + + " },\n" + + " \"meta\": null,\n" + + " \"id\": \"demo\",\n" + + " \"version\": demo,\n" + + " \"kind\": \"demo\",\n" + + " \"acl\": {\n" + + " \"viewers\": [\n" + + " \"demo\"\n" + + " ],\n" + + " \"owners\": [\n" + + " \"demo\"\n" + + " ]\n" + + " },\n" + + " \"legal\": {\n" + + " \"legaltags\": [\n" + + " \"opendes-test-tag\"\n" + + " ],\n" + + " \"otherRelevantDataCountries\": [\n" + + " \"BR\"\n" + + " ],\n" + + " \"status\": \"compliant\"\n" + + " },\n" + + " \"createUser\": \"demo\",\n" + + " \"createTime\": \"demo\"\n" + + " }\n" + + " ],\n" + + " \"notFound\": [\n" + + " \"demo\"\n" + + " ],\n" + + " \"conversionStatuses\": []\n" + + "}"; + + private static final String JSON_RESPONSE1_WITHOUT_NOT_FOUND = "{\n" + + " \"records\": [\n" + + " {\n" + + " \"data\": {\n" + + " \"Spuddate\": \"atspud\",\n" + + " \"UWI\": \"atuwi\",\n" + + " \"latitude\": \"latitude\",\n" + + " \"longitude\": \"longitude\"\n" + + " },\n" + + " \"meta\": null,\n" + + " \"id\": \"demo\",\n" + + " \"version\": demo,\n" + + " \"kind\": \"demo\",\n" + + " \"acl\": {\n" + + " \"viewers\": [\n" + + " \"demo\"\n" + + " ],\n" + + " \"owners\": [\n" + + " \"demo\"\n" + + " ]\n" + + " },\n" + + " \"legal\": {\n" + + " \"legaltags\": [\n" + + " \"opendes-test-tag\"\n" + + " ],\n" + + " \"otherRelevantDataCountries\": [\n" + + " \"BR\"\n" + + " ],\n" + + " \"status\": \"compliant\"\n" + + " },\n" + + " \"createUser\": \"demo\",\n" + + " \"createTime\": \"demo\"\n" + + " }\n" + + " ],\n" + + " \"notFound\": [],\n" + + " \"conversionStatuses\": []\n" + + "}"; + + private static final String JSON_RESPONSE2_WITHOUT_NOT_FOUND = "{\n" + + " \"records\" :[],\n" + + " \"conversionStatuses\":[]\n" + + "}"; + + @Mock + private UrlFetchServiceImpl urlFetchService; + @Mock + private FetchServiceHttpRequest httpRequest; + @InjectMocks + private HttpResponse response; + @InjectMocks + private RetryPolicy retryPolicy; + @Mock + private JaxRsDpsLog logger; + + + @Test + public void retry_should_be_true_for_jsonResponseWithNotFound() { + RetryConfig config = this.retryPolicy.retryConfig(); + Predicate<HttpResponse> retry = config.getResultPredicate(); + response.setBody(JSON_RESPONSE_WITH_NOT_FOUND); + assert retry != null; + boolean value = retry.test(response); + + assertTrue(value); + } + + @Test + public void retry_should_be_false_for_jsonResponse1WithOut_NotFound() { + RetryConfig config = this.retryPolicy.retryConfig(); + Predicate<HttpResponse> retry = config.getResultPredicate(); + response.setBody(JSON_RESPONSE1_WITHOUT_NOT_FOUND); + boolean value = retry.test(response); + + assertFalse(value); + } + + @Test + public void retry_should_be_false_for_jsonResponse2WithOut_NotFound() { + RetryConfig config = this.retryPolicy.retryConfig(); + Predicate<HttpResponse> retry = config.getResultPredicate(); + response.setBody(JSON_RESPONSE2_WITHOUT_NOT_FOUND); + boolean value = retry.test(response); + + assertFalse(value); + } + +} diff --git a/provider/indexer-azure/src/test/java/org/opengroup/osdu/indexer/azure/service/UrlFetchServiceAzureImplTest.java b/provider/indexer-azure/src/test/java/org/opengroup/osdu/indexer/azure/service/UrlFetchServiceAzureImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..916c5b8bc987621937eb80d955fb80c4e7dab5f2 --- /dev/null +++ b/provider/indexer-azure/src/test/java/org/opengroup/osdu/indexer/azure/service/UrlFetchServiceAzureImplTest.java @@ -0,0 +1,139 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.opengroup.osdu.indexer.azure.service; + +import io.github.resilience4j.retry.RetryConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.opengroup.osdu.core.common.http.FetchServiceHttpRequest; +import org.opengroup.osdu.core.common.http.UrlFetchServiceImpl; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.HttpResponse; + +import java.time.Duration; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + + +@RunWith(MockitoJUnitRunner.class) +public class UrlFetchServiceAzureImplTest { + + @Mock + private JaxRsDpsLog logger; + @Mock + private UrlFetchServiceImpl urlFetchService; + @InjectMocks + private FetchServiceHttpRequest httpRequest; + @InjectMocks + private HttpResponse response; + @Mock + private RetryPolicy retryPolicy; + @InjectMocks + private UrlFetchServiceAzureImpl urlFetchServiceAzure; + + private static final String JSON1 = "{\n" + + " \"records\": [\n" + + " {\n" + + " \"data\": {\n" + + " \"Spuddate\": \"atspud\",\n" + + " \"UWI\": \"atuwi\",\n" + + " \"latitude\": \"latitude\",\n" + + " \"longitude\": \"longitude\"\n" + + " },\n" + + " \"meta\": null,\n" + + " \"id\": \"demo\",\n" + + " \"version\": demo,\n" + + " \"kind\": \"demo\",\n" + + " \"acl\": {\n" + + " \"viewers\": [\n" + + " \"demo\"\n" + + " ],\n" + + " \"owners\": [\n" + + " \"demo\"\n" + + " ]\n" + + " },\n" + + " \"legal\": {\n" + + " \"legaltags\": [\n" + + " \"opendes-test-tag\"\n" + + " ],\n" + + " \"otherRelevantDataCountries\": [\n" + + " \"BR\"\n" + + " ],\n" + + " \"status\": \"compliant\"\n" + + " },\n" + + " \"createUser\": \"demo\",\n" + + " \"createTime\": \"demo\"\n" + + " }\n" + + " ],\n" + + " \"notFound\": [\n" + + " \"demo\"\n" + + " ],\n" + + " \"conversionStatuses\": []\n" + + "}"; + + private static final String JSON2 = "{\n" + + " \"records\" :[],\n" + + " \"conversionStatuses\":[]\n" + + "}"; + + private static final String url = "https://demo/api/storage/v2/query/records:batch"; + private static final String url2 = "https://demo/api/storage/v2/schemas"; + + @Before + public void setUp() { + when(this.retryPolicy.retryConfig()).thenReturn(new RetryPolicy().retryConfig()); + } + + @Test + public void shouldRetry_ForJSON1_when_storageQueryRecordCallIsMade() throws Exception { + response.setBody(JSON1); + httpRequest.setUrl(url); + + when(urlFetchServiceAzure.sendRequest(httpRequest)).thenReturn(response); + + urlFetchServiceAzure.sendRequest(httpRequest); + verify(urlFetchService, atMost(4)).sendRequest(httpRequest); + } + + @Test + public void shouldNotRetry_ForJSON2_when_storageQueryRecordCallIsMade() throws Exception { + response.setBody(JSON2); + httpRequest.setUrl(url); + + when(urlFetchServiceAzure.sendRequest(httpRequest)).thenReturn(response); + + urlFetchServiceAzure.sendRequest(httpRequest); + verify(urlFetchService, atMost(2)).sendRequest(httpRequest); + } + + + @Test + public void retryFunction_shouldNotBeCalled() throws Exception { + httpRequest.setUrl(url2); + + when(urlFetchService.sendRequest(httpRequest)).thenReturn(response); + + urlFetchServiceAzure.sendRequest(httpRequest); + verify(urlFetchService, times(1)).sendRequest(httpRequest); + } + +}