Commit d60374fa authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Handled polling of moveNode operations

parent a5974e7f
Loading
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.xml.bind.JAXB;
import net.ivoa.xml.uws.v1.ExecutionPhase;
import net.ivoa.xml.uws.v1.JobSummary;
import net.ivoa.xml.uws.v1.Jobs;
import net.ivoa.xml.vospace.v2.Node;
@@ -189,6 +190,21 @@ public class VOSpaceClient {
        });
    }

    public ExecutionPhase getJobPhase(String jobId) {

        HttpRequest request = getRequest("/transfers/" + jobId + "/phase")
                .GET()
                .build();

        return call(request, BodyHandlers.ofInputStream(), 200, res -> {
            try {
                return ExecutionPhase.valueOf(new String(res.readAllBytes()));
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        });
    }

    public String getErrorDetail(String jobId) {

        HttpRequest request = getRequest("/transfers/" + jobId + "/error")
+29 −2
Original line number Diff line number Diff line
@@ -7,7 +7,9 @@ package it.inaf.ia2.vospace.ui.controller;

import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.Job;
import it.inaf.ia2.vospace.ui.data.ListNodeData;
import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator;
import it.inaf.ia2.vospace.ui.service.MoveNodeModalHtmlGenerator;
import it.inaf.oats.vospace.datamodel.NodeUtils;
@@ -18,6 +20,7 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import net.ivoa.xml.uws.v1.ExecutionPhase;
import net.ivoa.xml.uws.v1.JobSummary;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Node;
@@ -46,6 +49,9 @@ public class NodesController extends BaseController {
    @Value("${vospace-authority}")
    private String authority;

    @Value("${maxPollingAttempts:10}")
    private int maxPollingAttempts;
    
    @Autowired
    private VOSpaceClient client;

@@ -154,7 +160,7 @@ public class NodesController extends BaseController {
    }

    @PostMapping(value = "/move")
    public void moveNode(@RequestBody Map<String, Object> params) {
    public ResponseEntity<Job> moveNode(@RequestBody Map<String, Object> params) {

        String target = getRequiredParam(params, "target");
        String direction = getRequiredParam(params, "direction");
@@ -165,7 +171,28 @@ public class NodesController extends BaseController {

        JobSummary job = client.startTransferJob(transfer);

        // TODO: polling
        // Try to perform polling until completion. If it takes too much time 
        // sends the execution phase to the UI and let it handles the polling.
        ExecutionPhase phase;
        int i = 0;
        do {
            phase = client.getJobPhase(job.getJobId());
            if (phase == ExecutionPhase.COMPLETED) {
                break;
            } else if (phase == ExecutionPhase.ERROR) {
                String errorDetail = client.getErrorDetail(job.getJobId());
                throw new VOSpaceException("MoveNode operation failed: " + errorDetail);
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
            }
            i++;
        } while (i < maxPollingAttempts);

        job.setPhase(phase);

        return ResponseEntity.ok(new Job(job));
    }

    protected String getPath(String prefix) {
+10 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import javax.servlet.http.HttpServletRequest;
import net.ivoa.xml.uws.v1.ExecutionPhase;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Protocol;
import net.ivoa.xml.vospace.v2.Transfer;
@@ -186,6 +187,15 @@ public class VOSpaceClientTest {
        assertEquals("Protocol negotiation failed", ex.getMessage());
    }

    @Test
    public void testGetJobPhase() {

        CompletableFuture response = getMockedStreamResponseFuture(200, "COMPLETED");
        when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response);

        assertEquals(ExecutionPhase.COMPLETED, voSpaceClient.getJobPhase("job_id"));
    }

    protected static String getResourceFileContent(String fileName) {
        try ( InputStream in = VOSpaceClientTest.class.getClassLoader().getResourceAsStream(fileName)) {
            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+72 −0
Original line number Diff line number Diff line
@@ -7,12 +7,19 @@ package it.inaf.ia2.vospace.ui.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.Job;
import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import net.ivoa.xml.uws.v1.ExecutionPhase;
import net.ivoa.xml.uws.v1.JobSummary;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.DataNode;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
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.eq;
@@ -25,7 +32,9 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -33,6 +42,7 @@ import org.springframework.web.util.NestedServletException;

@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {"vospace-authority=example.com!vospace", "maxPollingAttempts=2"})
public class NodesControllerTest {

    private static final ObjectMapper MAPPER = new ObjectMapper();
@@ -139,4 +149,66 @@ public class NodesControllerTest {

        verify(client, times(1)).getNode(eq("/a/b/c"));
    }

    @Test
    public void testMoveNodeSuccess() throws Exception {

        when(client.getJobPhase("job_id"))
                .thenReturn(ExecutionPhase.EXECUTING)
                .thenReturn(ExecutionPhase.COMPLETED);

        String response = testMoveNode()
                .andExpect(status().isOk())
                .andReturn().getResponse()
                .getContentAsString();

        Job job = MAPPER.readValue(response, Job.class);
        assertEquals(ExecutionPhase.COMPLETED, job.getPhase());
    }

    @Test
    public void testMoveNodeExecuting() throws Exception {

        when(client.getJobPhase("job_id"))
                .thenReturn(ExecutionPhase.EXECUTING);

        String response = testMoveNode()
                .andExpect(status().isOk())
                .andReturn().getResponse()
                .getContentAsString();

        Job job = MAPPER.readValue(response, Job.class);
        assertEquals(ExecutionPhase.EXECUTING, job.getPhase());
    }

    @Test
    public void testMoveNodeError() throws Exception {

        when(client.getJobPhase("job_id"))
                .thenReturn(ExecutionPhase.ERROR);

        when(client.getErrorDetail("job_id")).thenReturn("move_error");

        try {
            testMoveNode();
            fail("Exception was expected");
        } catch (Exception ex) {
            assertTrue(ex.getCause() instanceof VOSpaceException);
            assertTrue(ex.getCause().getMessage().contains("move_error"));
        }
    }

    private ResultActions testMoveNode() throws Exception {

        JobSummary job = new JobSummary();
        job.setJobId("job_id");

        when(client.startTransferJob(any())).thenReturn(job);

        Map<String, String> params = Map.of("target", "/path/to/target", "direction", "/path/to/direction");

        return mockMvc.perform(post("/move")
                .contentType(MediaType.APPLICATION_JSON)
                .content(MAPPER.writeValueAsString(params)));
    }
}
+9 −4
Original line number Diff line number Diff line
@@ -207,11 +207,16 @@ export default new Vuex.Store({
          dispatch('setPath', state.path);
        });
    },
    moveNode({ state, dispatch }, data) {
    moveNode({ state, commit, dispatch }, data) {
      client.moveNode(data)
        .then(() => {
        .then(job => {
          if (job.phase === 'COMPLETED') {
            // Reload current node
            dispatch('setPath', state.path);
          } else {
            main.showInfo('Move operation is taking some time and will be handled in background');
            commit('addJob', job);
          }
        });
    },
    openNodeInMoveModal({ state, commit }, path) {