diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java index a5249671c586051b51343da8076dc7a66801bcbb..dc16f0e40075fa6614dd0271be3889f78279fd80 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java @@ -154,6 +154,16 @@ public class VOSpaceClient { }); } + public String getErrorDetail(String jobId) { + + HttpRequest request = getRequest("/transfers/" + jobId + "/error") + .header("Accept", "text/plain") + .GET() + .build(); + + return call(request, BodyHandlers.ofString(), 200, res -> res); + } + private <T, U> U call(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, int expectedStatusCode, Function<T, U> responseHandler) { try { return httpClient.sendAsync(request, responseBodyHandler) diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java index 832de819af8430bd177a0d986b5adca93f7e3e4a..fa7309492a7a36147aabbf4da7a17f58b626e3d7 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java @@ -6,6 +6,7 @@ import it.inaf.ia2.vospace.ui.exception.BadRequestException; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import net.ivoa.xml.uws.v1.ErrorType; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.Param; @@ -13,6 +14,8 @@ import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.StructuredDataNode; import net.ivoa.xml.vospace.v2.Transfer; import net.ivoa.xml.vospace.v2.View; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; @@ -32,6 +35,8 @@ import org.springframework.web.client.RestTemplate; @RestController public class JobController extends BaseController { + private static final Logger LOG = LoggerFactory.getLogger(JobController.class); + @Value("${vospace-authority}") private String authority; @@ -69,13 +74,35 @@ public class JobController extends BaseController { transfer.getProtocols().add(protocol); JobSummary job = client.startTransferJob(transfer); - - if (job.getPhase() == ExecutionPhase.QUEUED || job.getPhase() == ExecutionPhase.PENDING) { + + if (job.getPhase() == ExecutionPhase.QUEUED + || job.getPhase() == ExecutionPhase.PENDING + || job.getPhase() == ExecutionPhase.EXECUTING) { return ResponseEntity.ok(new Job(job)); } - // TODO: proper handling - throw new RuntimeException("Error while executing job " + job.getJobId() + ". Job phase is " - + job.getPhase() + ". QUEUED or PENDING expected"); + String errorMessage; + if (job.getPhase() == ExecutionPhase.ERROR) { + if (job.getErrorSummary() != null) { + if (job.getErrorSummary().isHasDetail()) { + errorMessage = client.getErrorDetail(job.getJobId()); + } else { + errorMessage = job.getErrorSummary().getMessage(); + } + if (job.getErrorSummary().getType() == ErrorType.TRANSIENT) { + if (!errorMessage.endsWith(".")) { + errorMessage += "."; + } + errorMessage += " Please retry later."; + } + } else { + errorMessage = "Unexpected error"; + } + } else { + LOG.error("Error while executing job {}", job.getJobId()); + errorMessage = "Error while executing job. Job phase is " + + job.getPhase() + ". QUEUED, PENDING or EXECUTING expected"; + } + throw new RuntimeException(errorMessage); } private String createTempListOfFilesNode(List<String> paths) { diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java index e8a1eabc6463733408e972734c2c2e19ab8f4e14..91b55bfdeeff105424d35ae0df52e81b8f1a7a7c 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java @@ -89,6 +89,15 @@ public class VOSpaceClientTest { voSpaceClient.createNode(newNode); } + + @Test + public void testGetErrorDetail() { + + CompletableFuture response = getMockedStringResponseFuture(200, "error message"); + when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); + + assertEquals("error message", voSpaceClient.getErrorDetail("123")); + } protected static String getResourceFileContent(String fileName) { try ( InputStream in = VOSpaceClientTest.class.getClassLoader().getResourceAsStream(fileName)) { @@ -102,6 +111,10 @@ public class VOSpaceClientTest { return CompletableFuture.completedFuture(getMockedStreamResponse(200, body)); } + protected static CompletableFuture<HttpResponse<String>> getMockedStringResponseFuture(int statusCode, String body) { + return CompletableFuture.completedFuture(getMockedStringResponse(200, body)); + } + protected static HttpResponse<InputStream> getMockedStreamResponse(int statusCode, String body) { HttpResponse response = getMockedResponse(statusCode); InputStream in = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); @@ -109,6 +122,12 @@ public class VOSpaceClientTest { return response; } + protected static HttpResponse<String> getMockedStringResponse(int statusCode, String body) { + HttpResponse response = getMockedResponse(statusCode); + when(response.body()).thenReturn(body); + return response; + } + protected static HttpResponse getMockedResponse(int statusCode) { HttpResponse response = mock(HttpResponse.class); when(response.statusCode()).thenReturn(statusCode); diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java index 53128e3c8929d9e380d64d4e84a0da619b6f456f..683667ce8a3e3976ca602389a183127ecb903232 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java @@ -2,9 +2,14 @@ package it.inaf.ia2.vospace.ui.controller; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import java.util.Collections; +import java.util.function.Consumer; +import net.ivoa.xml.uws.v1.ErrorSummary; +import net.ivoa.xml.uws.v1.ErrorType; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.Protocol; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; @@ -30,7 +35,7 @@ public class JobControllerTest { @MockBean private RestTemplate restTemplate; - + @Autowired private MockMvc mockMvc; @@ -52,7 +57,7 @@ public class JobControllerTest { public void testMultipleFilesAsyncRecall() throws Exception { JobSummary job = new JobSummary(); - job.setPhase(ExecutionPhase.QUEUED); + job.setPhase(ExecutionPhase.PENDING); when(client.startTransferJob(argThat(transfer -> { return transfer.getTarget().startsWith("vos://example.com!vospace/path/to/.tmp-"); @@ -68,4 +73,64 @@ public class JobControllerTest { .content("[\"/path/to/file1\", \"/path/to/file2\"]")) .andExpect(status().isOk()); } + + @Test + public void testAsyncRecallErrorWithDetails() throws Exception { + + JobSummary job = new JobSummary(); + job.setPhase(ExecutionPhase.ERROR); + + when(client.getErrorDetail(any())).thenReturn("Error was xxx"); + + ErrorSummary errorSummary = new ErrorSummary(); + errorSummary.setHasDetail(true); + errorSummary.setType(ErrorType.FATAL); + + job.setErrorSummary(errorSummary); + + when(client.startTransferJob(any())).thenReturn(job); + + testErrorCall(ex -> assertTrue(ex.getMessage().contains("Error was xxx"))); + } + + @Test + public void testAsyncRecallTransientErrorWithoutDetails() throws Exception { + + JobSummary job = new JobSummary(); + job.setPhase(ExecutionPhase.ERROR); + + ErrorSummary errorSummary = new ErrorSummary(); + errorSummary.setMessage("simple message."); + errorSummary.setType(ErrorType.TRANSIENT); + + job.setErrorSummary(errorSummary); + + when(client.startTransferJob(any())).thenReturn(job); + + testErrorCall(ex -> assertTrue(ex.getMessage().contains("simple message") && ex.getMessage().contains("retry"))); + } + + @Test + public void testAsyncRecallErrorWithoutSummary() throws Exception { + + JobSummary job = new JobSummary(); + job.setPhase(ExecutionPhase.ERROR); + + when(client.startTransferJob(any())).thenReturn(job); + + testErrorCall(ex -> assertTrue(ex.getMessage().contains("error"))); + } + + private void testErrorCall(Consumer<Exception> exceptionChecker) throws Exception { + try { + mockMvc.perform(post("/recall") + .contentType(MediaType.APPLICATION_JSON) + .content("[\"/path/to/file\"]")) + .andExpect(status().is5xxServerError()); + } catch (Exception ex) { + exceptionChecker.accept(ex); + return; + } + fail("An error was expected"); + } }