+ */
+@Builder
+@Getter
+public class S3InputStreamInfos {
+ InputStream inputStream;
+ String fileName;
+ Long fileLength;
+}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java
index c5424d6..2dc37d0 100644
--- a/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java
+++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java
@@ -22,4 +22,14 @@ public abstract class AbstractComputationResultService {
public abstract void deleteAll();
public abstract S findStatus(UUID resultUuid);
+
+ // --- Must implement these following methods if a computation server supports s3 storage --- //
+ public void saveDebugFileLocation(UUID resultUuid, String debugFilePath) {
+ // to override by subclasses
+ }
+
+ public String findDebugFileLocation(UUID resultUuid) {
+ // to override by subclasses
+ return null;
+ }
}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java
index 8cb4ca9..53e7afc 100644
--- a/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java
+++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java
@@ -12,6 +12,7 @@
import lombok.Setter;
import org.gridsuite.computation.dto.ReportInfos;
+import java.nio.file.Path;
import java.util.UUID;
/**
@@ -30,9 +31,16 @@ public abstract class AbstractComputationRunContext {
private P parameters;
private ReportNode reportNode;
private Network network;
+ private Boolean debug;
+ private Path debugDir;
protected AbstractComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos,
String userId, String provider, P parameters) {
+ this(networkUuid, variantId, receiver, reportInfos, userId, provider, parameters, null);
+ }
+
+ protected AbstractComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos,
+ String userId, String provider, P parameters, Boolean debug) {
this.networkUuid = networkUuid;
this.variantId = variantId;
this.receiver = receiver;
@@ -42,5 +50,6 @@ protected AbstractComputationRunContext(UUID networkUuid, String variantId, Stri
this.parameters = parameters;
this.reportNode = ReportNode.NO_OP;
this.network = null;
+ this.debug = debug;
}
}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java
index 824f1ee..4684339 100644
--- a/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java
+++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java
@@ -7,13 +7,26 @@
package org.gridsuite.computation.service;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.powsybl.commons.PowsyblException;
import lombok.Getter;
+import org.gridsuite.computation.s3.ComputationS3Service;
+import org.gridsuite.computation.s3.S3InputStreamInfos;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.ContentDisposition;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
+import static org.gridsuite.computation.s3.ComputationS3Service.S3_SERVICE_NOT_AVAILABLE_MESSAGE;
+
/**
* @author Mathieu Deharbe
* @param run context specific to a computation, including parameters
@@ -26,6 +39,7 @@ public abstract class AbstractComputationService resultUuids, S status) {
public S getStatus(UUID resultUuid) {
return resultService.findStatus(resultUuid);
}
+
+ public ResponseEntity downloadDebugFile(UUID resultUuid) {
+ if (computationS3Service == null) {
+ throw new PowsyblException(S3_SERVICE_NOT_AVAILABLE_MESSAGE);
+ }
+
+ String s3Key = resultService.findDebugFileLocation(resultUuid);
+ if (s3Key == null) {
+ return ResponseEntity.notFound().build();
+ }
+
+ try {
+ S3InputStreamInfos s3InputStreamInfos = computationS3Service.downloadFile(s3Key);
+ InputStream inputStream = s3InputStreamInfos.getInputStream();
+ String fileName = s3InputStreamInfos.getFileName();
+ Long fileLength = s3InputStreamInfos.getFileLength();
+
+ // build header
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentDisposition(ContentDisposition.builder("attachment").filename(fileName).build());
+ headers.setContentLength(fileLength);
+
+ // wrap s3 input stream
+ InputStreamResource resource = new InputStreamResource(inputStream);
+ return ResponseEntity.ok()
+ .headers(headers)
+ .contentType(MediaType.APPLICATION_OCTET_STREAM)
+ .body(resource);
+ } catch (IOException e) {
+ return ResponseEntity.notFound().build();
+ }
+ }
+
}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java b/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java
index b80728d..3201a59 100644
--- a/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java
+++ b/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java
@@ -67,6 +67,7 @@ public Message toMessage(ObjectMapper objectMapper) {
.setHeader(REPORT_UUID_HEADER, runContext.getReportInfos().reportUuid() != null ? runContext.getReportInfos().reportUuid().toString() : null)
.setHeader(REPORTER_ID_HEADER, runContext.getReportInfos().reporterId())
.setHeader(REPORT_TYPE_HEADER, runContext.getReportInfos().computationType())
+ .setHeader(HEADER_DEBUG, runContext.getDebug())
.copyHeaders(getSpecificMsgHeaders(objectMapper))
.build();
}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java b/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java
index 45d5c99..2becdc9 100644
--- a/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java
+++ b/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java
@@ -8,27 +8,44 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.powsybl.commons.PowsyblException;
+import com.powsybl.commons.io.FileUtil;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.VariantManagerConstants;
import com.powsybl.network.store.client.NetworkStoreService;
import com.powsybl.network.store.client.PreloadingStrategy;
+import com.powsybl.ws.commons.ZipUtils;
import org.apache.commons.lang3.StringUtils;
import org.gridsuite.computation.ComputationException;
+import org.gridsuite.computation.s3.ComputationS3Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
+import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.web.server.ResponseStatusException;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
-import java.util.concurrent.*;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
+import static org.gridsuite.computation.s3.ComputationS3Service.S3_DELIMITER;
+import static org.gridsuite.computation.s3.ComputationS3Service.S3_SERVICE_NOT_AVAILABLE_MESSAGE;
+import static org.gridsuite.computation.service.NotificationService.HEADER_ERROR_MESSAGE;
+
/**
* @author Mathieu Deharbe
* @param powsybl Result class specific to the computation
@@ -39,6 +56,9 @@
public abstract class AbstractWorkerService, P, S extends AbstractComputationResultService>> {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractWorkerService.class);
+ @Value("${powsybl-ws.s3.subpath.prefix:}${debug-subpath:debug}")
+ private String debugRootPath;
+
protected final Lock lockRunAndCancel = new ReentrantLock();
protected final ObjectMapper objectMapper;
protected final NetworkStoreService networkStoreService;
@@ -50,10 +70,23 @@ public abstract class AbstractWorkerService cancelComputationRequests = new ConcurrentHashMap<>();
protected final S resultService;
+ protected final ComputationS3Service computationS3Service;
+
+ protected AbstractWorkerService(NetworkStoreService networkStoreService,
+ NotificationService notificationService,
+ ReportService reportService,
+ S resultService,
+ ExecutionService executionService,
+ AbstractComputationObserver observer,
+ ObjectMapper objectMapper) {
+ this(networkStoreService, notificationService, reportService, resultService, null, executionService, observer, objectMapper);
+ }
+
protected AbstractWorkerService(NetworkStoreService networkStoreService,
NotificationService notificationService,
ReportService reportService,
S resultService,
+ ComputationS3Service computationS3Service,
ExecutionService executionService,
AbstractComputationObserver observer,
ObjectMapper objectMapper) {
@@ -61,6 +94,7 @@ protected AbstractWorkerService(NetworkStoreService networkStoreService,
this.notificationService = notificationService;
this.reportService = reportService;
this.resultService = resultService;
+ this.computationS3Service = computationS3Service;
this.executionService = executionService;
this.observer = observer;
this.objectMapper = objectMapper;
@@ -148,6 +182,9 @@ public Consumer> consumeRun() {
this.handleNonCancellationException(resultContext, e, rootReporter);
throw new ComputationException(String.format("%s: %s", NotificationService.getFailedMessage(getComputationType()), e.getMessage()), e.getCause());
} finally {
+ if (Boolean.TRUE.equals(resultContext.getRunContext().getDebug())) {
+ processDebug(resultContext);
+ }
clean(resultContext);
}
};
@@ -160,6 +197,54 @@ public Consumer> consumeRun() {
protected void clean(AbstractResultContext resultContext) {
futures.remove(resultContext.getResultUuid());
cancelComputationRequests.remove(resultContext.getResultUuid());
+
+ // run in debug mode, clean debug dir
+ C runContext = resultContext.getRunContext();
+ if (Boolean.TRUE.equals(runContext.getDebug()) && computationS3Service != null) {
+ removeDirectory(runContext.getDebugDir());
+ }
+ }
+
+ /**
+ * Process debug option
+ * @param resultContext The context of the computation
+ */
+ protected void processDebug(AbstractResultContext resultContext) {
+ if (computationS3Service == null) {
+ sendDebugMessage(resultContext, S3_SERVICE_NOT_AVAILABLE_MESSAGE);
+ return;
+ }
+
+ C runContext = resultContext.getRunContext();
+ Path debugDir = runContext.getDebugDir();
+ Path parentDir = debugDir.getParent();
+ Path debugFilePath = parentDir.resolve(debugDir.getFileName().toString() + ".zip");
+ String fileName = debugFilePath.getFileName().toString();
+
+ try {
+ // zip the working directory
+ ZipUtils.zip(debugDir, debugFilePath);
+ String s3Key = debugRootPath + S3_DELIMITER + fileName;
+
+ // insert debug file path into db
+ resultService.saveDebugFileLocation(resultContext.getResultUuid(), s3Key);
+
+ // upload zip file to s3 storage
+ computationS3Service.uploadFile(debugFilePath, s3Key, fileName, 30);
+
+ // notify to study-server
+ sendDebugMessage(resultContext, null);
+ } catch (IOException | UncheckedIOException e) {
+ LOGGER.info("Error occurred while uploading debug file {}: {}", fileName, e.getMessage());
+ sendDebugMessage(resultContext, e.getMessage());
+ } finally {
+ // delete debug file
+ try {
+ Files.delete(debugFilePath);
+ } catch (IOException e) {
+ LOGGER.info("Error occurred while deleting debug file {}: {}", fileName, e.getMessage());
+ }
+ }
}
/**
@@ -187,12 +272,27 @@ protected void sendResultMessage(AbstractResultContext resultContext, R ignor
resultContext.getRunContext().getUserId(), null);
}
+ private void sendDebugMessage(AbstractResultContext resultContext, @Nullable String messageError) {
+ Map resultHeaders = new HashMap<>();
+
+ // --- attach debug to result headers --- //
+ resultHeaders.put(HEADER_ERROR_MESSAGE, messageError);
+
+ notificationService.sendDebugMessage(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(),
+ resultContext.getRunContext().getUserId(), resultHeaders);
+ }
+
/**
* Do some extra task before running the computation, e.g. print log or init extra data for the run context
- * @param ignoredRunContext This context may be used for further computation in overriding classes
+ * @param runContext The run context of the computation
*/
- protected void preRun(C ignoredRunContext) {
+ protected void preRun(C runContext) {
LOGGER.info("Run {} computation...", getComputationType());
+
+ // run in debug mode, create debug dir
+ if (Boolean.TRUE.equals(runContext.getDebug()) && computationS3Service != null) {
+ runContext.setDebugDir(createDebugDir());
+ }
}
protected R run(C runContext, UUID resultUuid, AtomicReference rootReporter) {
@@ -257,4 +357,31 @@ protected CompletableFuture runAsync(
protected abstract String getComputationType();
protected abstract CompletableFuture getCompletableFuture(C runContext, String provider, UUID resultUuid);
+
+ private Path createDebugDir() {
+ Path localDir = executionService.getComputationManager().getLocalDir();
+ try {
+ String debugDirPrefix = buildComputationDirPrefix() + "debug_";
+ return Files.createTempDirectory(localDir, debugDirPrefix);
+ } catch (IOException e) {
+ throw new UncheckedIOException(String.format("Error occurred while creating a debug directory inside the local directory %s",
+ localDir.toAbsolutePath()), e);
+ }
+ }
+
+ protected String buildComputationDirPrefix() {
+ return getComputationType().replaceAll("\\s+", "_").toLowerCase() + "_";
+ }
+
+ protected void removeDirectory(Path dir) {
+ if (dir != null) {
+ try {
+ FileUtil.removeDir(dir);
+ } catch (IOException e) {
+ LOGGER.error(String.format("%s: Error occurred while removing directory %s", getComputationType(), dir.toAbsolutePath()), e);
+ }
+ } else {
+ LOGGER.info("{}: No directory to clean", getComputationType());
+ }
+ }
}
diff --git a/src/main/java/org/gridsuite/computation/service/NotificationService.java b/src/main/java/org/gridsuite/computation/service/NotificationService.java
index d3aa71d..fa07fe4 100644
--- a/src/main/java/org/gridsuite/computation/service/NotificationService.java
+++ b/src/main/java/org/gridsuite/computation/service/NotificationService.java
@@ -46,6 +46,8 @@ public class NotificationService {
public static final String HEADER_PROVIDER = "provider";
public static final String HEADER_MESSAGE = "message";
public static final String HEADER_USER_ID = "userId";
+ public static final String HEADER_DEBUG = "debug";
+ public static final String HEADER_ERROR_MESSAGE = "errorMessage";
public static final String SENDING_MESSAGE = "Sending message : {}";
@@ -67,6 +69,19 @@ public void sendCancelMessage(Message message) {
publisher.send(publishPrefix + "Cancel-out-0", message);
}
+ @PostCompletion
+ public void sendDebugMessage(UUID resultUuid, String receiver, String userId, @Nullable Map additionalHeaders) {
+ MessageBuilder builder = MessageBuilder
+ .withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .setHeader(HEADER_USER_ID, userId)
+ .copyHeaders(additionalHeaders);
+ Message message = builder.build();
+ RESULT_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "Debug-out-0", message);
+ }
+
@PostCompletion
public void sendResultMessage(UUID resultUuid, String receiver, String userId, @Nullable Map additionalHeaders) {
MessageBuilder builder = MessageBuilder
diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..00f723d
--- /dev/null
+++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+# AutoConfigureCache auto-configuration imports
+org.gridsuite.computation.s3.S3AutoConfiguration
\ No newline at end of file
diff --git a/src/test/java/org/gridsuite/computation/ComputationTest.java b/src/test/java/org/gridsuite/computation/ComputationTest.java
index 5572102..8eff721 100644
--- a/src/test/java/org/gridsuite/computation/ComputationTest.java
+++ b/src/test/java/org/gridsuite/computation/ComputationTest.java
@@ -7,10 +7,16 @@
package org.gridsuite.computation;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.computation.local.LocalComputationConfig;
+import com.powsybl.computation.local.LocalComputationManager;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.VariantManager;
import com.powsybl.network.store.client.NetworkStoreService;
import com.powsybl.network.store.client.PreloadingStrategy;
+import com.powsybl.ws.commons.ZipUtils;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.observation.ObservationRegistry;
@@ -20,25 +26,42 @@
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.WithAssertions;
import org.gridsuite.computation.dto.ReportInfos;
+import org.gridsuite.computation.s3.ComputationS3Service;
+import org.gridsuite.computation.s3.S3InputStreamInfos;
import org.gridsuite.computation.service.*;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
+import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.stream.function.StreamBridge;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
+import java.util.concurrent.ForkJoinPool;
+import static org.gridsuite.computation.s3.ComputationS3Service.S3_DELIMITER;
+import static org.gridsuite.computation.s3.ComputationS3Service.S3_SERVICE_NOT_AVAILABLE_MESSAGE;
import static org.gridsuite.computation.service.NotificationService.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
@@ -48,13 +71,23 @@
@Slf4j
class ComputationTest implements WithAssertions {
private static final String COMPUTATION_TYPE = "mockComputation";
+ public static final String S3_DEBUG_SUBPATH = "debug";
+
+ public static final String WORKING_DIR = "test";
+ public static final String S3_DEBUG_FILE_ZIP = WORKING_DIR + ".zip";
+ public static final String S3_KEY = S3_DEBUG_SUBPATH + S3_DELIMITER + S3_DEBUG_FILE_ZIP;
+
+ protected FileSystem fileSystem;
+ protected Path tmpDir;
+
@Mock
private VariantManager variantManager;
@Mock
private NetworkStoreService networkStoreService;
@Mock
private ReportService reportService;
- private final ExecutionService executionService = new ExecutionService();
+ @Mock
+ private ExecutionService executionService;
private final UuidGeneratorService uuidGeneratorService = new UuidGeneratorService();
@Mock
private StreamBridge publisher;
@@ -63,6 +96,8 @@ class ComputationTest implements WithAssertions {
private ObjectMapper objectMapper;
@Mock
private Network network;
+ @Mock
+ private ComputationS3Service computationS3Service;
private enum MockComputationStatus {
NOT_DONE,
@@ -129,8 +164,8 @@ protected MockComputationResultContext(UUID resultUuid, MockComputationRunContex
}
private static class MockComputationService extends AbstractComputationService {
- protected MockComputationService(NotificationService notificationService, MockComputationResultService resultService, ObjectMapper objectMapper, UuidGeneratorService uuidGeneratorService, String defaultProvider) {
- super(notificationService, resultService, objectMapper, uuidGeneratorService, defaultProvider);
+ protected MockComputationService(NotificationService notificationService, MockComputationResultService resultService, ComputationS3Service computationS3Service, ObjectMapper objectMapper, UuidGeneratorService uuidGeneratorService, String defaultProvider) {
+ super(notificationService, resultService, computationS3Service, objectMapper, uuidGeneratorService, defaultProvider);
}
@Override
@@ -152,8 +187,8 @@ private enum ComputationResultWanted {
}
private static class MockComputationWorkerService extends AbstractWorkerService