powsybl Result class specific to the computation
+ * @param powsybl and gridsuite parameters specifics to the computation
+ */
+@Getter(AccessLevel.PROTECTED)
+public abstract class AbstractComputationObserver {
+ protected static final String OBSERVATION_PREFIX = "app.computation.";
+ protected static final String PROVIDER_TAG_NAME = "provider";
+ protected static final String TYPE_TAG_NAME = "type";
+ protected static final String STATUS_TAG_NAME = "status";
+ protected static final String COMPUTATION_COUNTER_NAME = OBSERVATION_PREFIX + "count";
+
+ private final ObservationRegistry observationRegistry;
+ private final MeterRegistry meterRegistry;
+
+ protected AbstractComputationObserver(@NonNull ObservationRegistry observationRegistry, @NonNull MeterRegistry meterRegistry) {
+ this.observationRegistry = observationRegistry;
+ this.meterRegistry = meterRegistry;
+ }
+
+ protected abstract String getComputationType();
+
+ protected Observation createObservation(String name, AbstractComputationRunContext runContext) {
+ Observation observation = Observation.createNotStarted(OBSERVATION_PREFIX + name, observationRegistry)
+ .lowCardinalityKeyValue(TYPE_TAG_NAME, getComputationType());
+ if (runContext.getProvider() != null) {
+ observation.lowCardinalityKeyValue(PROVIDER_TAG_NAME, runContext.getProvider());
+ }
+ return observation;
+ }
+
+ public void observe(String name, AbstractComputationRunContext runContext, Observation.CheckedRunnable callable) throws E {
+ createObservation(name, runContext).observeChecked(callable);
+ }
+
+ public T observe(String name, AbstractComputationRunContext runContext, Observation.CheckedCallable callable) throws E {
+ return createObservation(name, runContext).observeChecked(callable);
+ }
+
+ public T observeRun(
+ String name, AbstractComputationRunContext runContext, Observation.CheckedCallable callable) throws E {
+ T result = createObservation(name, runContext).observeChecked(callable);
+ incrementCount(runContext, result);
+ return result;
+ }
+
+ private void incrementCount(AbstractComputationRunContext runContext, R result) {
+ Counter.Builder builder =
+ Counter.builder(COMPUTATION_COUNTER_NAME);
+ if (runContext.getProvider() != null) {
+ builder.tag(PROVIDER_TAG_NAME, runContext.getProvider());
+ }
+ builder.tag(TYPE_TAG_NAME, getComputationType())
+ .tag(STATUS_TAG_NAME, getResultStatus(result))
+ .register(meterRegistry)
+ .increment();
+ }
+
+ protected abstract String getResultStatus(R res);
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationResultService.java b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationResultService.java
new file mode 100644
index 0000000..5f03978
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationResultService.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.service;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @author Mathieu Deharbe
+ * @param status specific to the computation
+ */
+public abstract class AbstractComputationResultService {
+
+ public abstract void insertStatus(List resultUuids, S status);
+
+ public abstract void delete(UUID resultUuid);
+
+ public abstract void deleteAll();
+
+ public abstract S findStatus(UUID resultUuid);
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationRunContext.java b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationRunContext.java
new file mode 100644
index 0000000..6962d4f
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationRunContext.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.service;
+
+import com.powsybl.commons.report.ReportNode;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.ws.commons.computation.dto.ReportInfos;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.UUID;
+
+/**
+ * @author Mathieu Deharbe
+ * @param parameters structure specific to the computation
+ */
+@Getter
+@Setter
+public abstract class AbstractComputationRunContext
{
+ private final UUID networkUuid;
+ private final String variantId;
+ private final String receiver;
+ private final ReportInfos reportInfos;
+ private final String userId;
+ private String provider;
+ private P parameters;
+ private ReportNode reportNode;
+ private Network network;
+
+ protected AbstractComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos,
+ String userId, String provider, P parameters) {
+ this.networkUuid = networkUuid;
+ this.variantId = variantId;
+ this.receiver = receiver;
+ this.reportInfos = reportInfos;
+ this.userId = userId;
+ this.provider = provider;
+ this.parameters = parameters;
+ this.reportNode = ReportNode.NO_OP;
+ this.network = null;
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationService.java b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationService.java
new file mode 100644
index 0000000..95575c0
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationService.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Getter;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * @author Mathieu Deharbe
+ * @param run context specific to a computation, including parameters
+ * @param run service specific to a computation
+ * @param enum status specific to a computation
+ */
+public abstract class AbstractComputationService, T extends AbstractComputationResultService, S> {
+
+ protected ObjectMapper objectMapper;
+ protected NotificationService notificationService;
+ protected UuidGeneratorService uuidGeneratorService;
+ protected T resultService;
+ @Getter
+ private final String defaultProvider;
+
+ protected AbstractComputationService(NotificationService notificationService,
+ T resultService,
+ ObjectMapper objectMapper,
+ UuidGeneratorService uuidGeneratorService,
+ String defaultProvider) {
+ this.notificationService = Objects.requireNonNull(notificationService);
+ this.objectMapper = objectMapper;
+ this.uuidGeneratorService = Objects.requireNonNull(uuidGeneratorService);
+ this.defaultProvider = defaultProvider;
+ this.resultService = Objects.requireNonNull(resultService);
+ }
+
+ public void stop(UUID resultUuid, String receiver) {
+ notificationService.sendCancelMessage(new CancelContext(resultUuid, receiver).toMessage());
+ }
+
+ public abstract List getProviders();
+
+ public abstract UUID runAndSaveResult(R runContext);
+
+ public void deleteResult(UUID resultUuid) {
+ resultService.delete(resultUuid);
+ }
+
+ public void deleteResults(List resultUuids) {
+ if (!CollectionUtils.isEmpty(resultUuids)) {
+ resultUuids.forEach(resultService::delete);
+ } else {
+ deleteResults();
+ }
+ }
+
+ public void deleteResults() {
+ resultService.deleteAll();
+ }
+
+ public void setStatus(List resultUuids, S status) {
+ resultService.insertStatus(resultUuids, status);
+ }
+
+ public S getStatus(UUID resultUuid) {
+ return resultService.findStatus(resultUuid);
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractResultContext.java b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractResultContext.java
new file mode 100644
index 0000000..dfed304
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractResultContext.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Data;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.MessageBuilder;
+
+import javax.annotation.Nullable;
+import java.io.UncheckedIOException;
+import java.time.Clock;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+
+import static com.powsybl.ws.commons.computation.service.NotificationService.HEADER_PROVIDER;
+import static com.powsybl.ws.commons.computation.service.NotificationService.HEADER_RECEIVER;
+import static com.powsybl.ws.commons.computation.service.NotificationService.HEADER_USER_ID;
+
+/**
+ * @author Mathieu Deharbe
+ * @param run context specific to a computation, including parameters
+ */
+@Data
+public abstract class AbstractResultContext> {
+
+ protected static final String RESULT_UUID_HEADER = "resultUuid";
+
+ protected static final String NETWORK_UUID_HEADER = "networkUuid";
+
+ protected static final String REPORT_UUID_HEADER = "reportUuid";
+
+ public static final String VARIANT_ID_HEADER = "variantId";
+
+ public static final String REPORTER_ID_HEADER = "reporterId";
+
+ public static final String REPORT_TYPE_HEADER = "reportType";
+
+ protected static final String MESSAGE_ROOT_NAME = "parameters";
+
+ private final UUID resultUuid;
+ private final R runContext;
+
+ protected AbstractResultContext(UUID resultUuid, R runContext) {
+ this.resultUuid = Objects.requireNonNull(resultUuid);
+ this.runContext = Objects.requireNonNull(runContext);
+ }
+
+ public Message toMessage(@Nullable final Clock clock, @Nullable final ObjectMapper objectMapper) {
+ String parametersJson = "";
+ if (objectMapper != null) {
+ try {
+ parametersJson = objectMapper.writeValueAsString(runContext.getParameters());
+ } catch (JsonProcessingException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ MessageBuilder messageBuilder = MessageBuilder.withPayload(parametersJson);
+ if (clock != null) {
+ messageBuilder.setHeader(MessageHeaders.TIMESTAMP, clock.millis());
+ }
+ return messageBuilder
+ .setHeader(RESULT_UUID_HEADER, resultUuid.toString())
+ .setHeader(NETWORK_UUID_HEADER, runContext.getNetworkUuid().toString())
+ .setHeader(VARIANT_ID_HEADER, runContext.getVariantId())
+ .setHeader(HEADER_RECEIVER, runContext.getReceiver())
+ .setHeader(HEADER_PROVIDER, runContext.getProvider())
+ .setHeader(HEADER_USER_ID, runContext.getUserId())
+ .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())
+ .copyHeaders(getSpecificMsgHeaders())
+ .build();
+ }
+
+ protected Map getSpecificMsgHeaders() {
+ return Map.of();
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java
new file mode 100644
index 0000000..e40baff
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java
@@ -0,0 +1,247 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.powsybl.commons.PowsyblException;
+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 org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.messaging.Message;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+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;
+
+/**
+ * @author Mathieu Deharbe
+ * @param powsybl Result class specific to the computation
+ * @param Run context specific to a computation, including parameters
+ * @param powsybl and gridsuite Parameters specifics to the computation
+ * @param result service specific to the computation
+ */
+public abstract class AbstractWorkerService, P, S extends AbstractComputationResultService>> {
+ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractWorkerService.class);
+
+ protected final Lock lockRunAndCancel = new ReentrantLock();
+ protected final ObjectMapper objectMapper;
+ protected final NetworkStoreService networkStoreService;
+ protected final ReportService reportService;
+ protected final ExecutionService executionService;
+ protected final NotificationService notificationService;
+ protected final AbstractComputationObserver observer;
+ protected final Map> futures = new ConcurrentHashMap<>();
+ protected final Map cancelComputationRequests = new ConcurrentHashMap<>();
+ protected final S resultService;
+
+ protected AbstractWorkerService(NetworkStoreService networkStoreService,
+ NotificationService notificationService,
+ ReportService reportService,
+ S resultService,
+ ExecutionService executionService,
+ AbstractComputationObserver observer,
+ ObjectMapper objectMapper) {
+ this.networkStoreService = networkStoreService;
+ this.notificationService = notificationService;
+ this.reportService = reportService;
+ this.resultService = resultService;
+ this.executionService = executionService;
+ this.observer = observer;
+ this.objectMapper = objectMapper;
+ }
+
+ protected PreloadingStrategy getNetworkPreloadingStrategy() {
+ return PreloadingStrategy.COLLECTION;
+ }
+
+ protected Network getNetwork(UUID networkUuid, String variantId) {
+ try {
+ Network network = networkStoreService.getNetwork(networkUuid, getNetworkPreloadingStrategy());
+ String variant = StringUtils.isBlank(variantId) ? VariantManagerConstants.INITIAL_VARIANT_ID : variantId;
+ network.getVariantManager().setWorkingVariant(variant);
+ return network;
+ } catch (PowsyblException e) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
+ }
+ }
+
+ protected void cleanResultsAndPublishCancel(UUID resultUuid, String receiver) {
+ resultService.delete(resultUuid);
+ notificationService.publishStop(resultUuid, receiver, getComputationType());
+ if (LOGGER.isInfoEnabled()) {
+ LOGGER.info("{} (resultUuid='{}')",
+ NotificationService.getCancelMessage(getComputationType()),
+ resultUuid);
+ }
+ }
+
+ private void cancelAsync(CancelContext cancelContext) {
+ lockRunAndCancel.lock();
+ try {
+ cancelComputationRequests.put(cancelContext.resultUuid(), cancelContext);
+
+ // find the completableFuture associated with result uuid
+ CompletableFuture future = futures.get(cancelContext.resultUuid());
+ if (future != null) {
+ future.cancel(true); // cancel computation in progress
+ }
+ cleanResultsAndPublishCancel(cancelContext.resultUuid(), cancelContext.receiver());
+ } finally {
+ lockRunAndCancel.unlock();
+ }
+ }
+
+ protected abstract AbstractResultContext fromMessage(Message message);
+
+ protected boolean resultCanBeSaved(R result) {
+ return result != null;
+ }
+
+ public Consumer> consumeRun() {
+ return message -> {
+ AbstractResultContext resultContext = fromMessage(message);
+ try {
+ long startTime = System.nanoTime();
+
+ Network network = getNetwork(resultContext.getRunContext().getNetworkUuid(),
+ resultContext.getRunContext().getVariantId());
+ resultContext.getRunContext().setNetwork(network);
+ R result = run(resultContext.getRunContext(), resultContext.getResultUuid());
+
+ LOGGER.info("Just run in {}s", TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime));
+
+ if (resultCanBeSaved(result)) {
+ startTime = System.nanoTime();
+ observer.observe("results.save", resultContext.getRunContext(), () -> saveResult(network, resultContext, result));
+
+ LOGGER.info("Stored in {}s", TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime));
+
+ sendResultMessage(resultContext, result);
+ LOGGER.info("{} complete (resultUuid='{}')", getComputationType(), resultContext.getResultUuid());
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (Exception e) {
+ if (!(e instanceof CancellationException)) {
+ LOGGER.error(NotificationService.getFailedMessage(getComputationType()), e);
+ publishFail(resultContext, e.getMessage());
+ resultService.delete(resultContext.getResultUuid());
+ this.handleNonCancellationException(resultContext, e);
+ }
+ } finally {
+ futures.remove(resultContext.getResultUuid());
+ cancelComputationRequests.remove(resultContext.getResultUuid());
+ }
+ };
+ }
+
+ /**
+ * Handle exception in consumeRun that is not a CancellationException
+ * @param resultContext The context of the computation
+ * @param exception The exception to handle
+ */
+ protected void handleNonCancellationException(AbstractResultContext resultContext, Exception exception) {
+ }
+
+ public Consumer> consumeCancel() {
+ return message -> cancelAsync(CancelContext.fromMessage(message));
+ }
+
+ protected abstract void saveResult(Network network, AbstractResultContext resultContext, R result);
+
+ protected void sendResultMessage(AbstractResultContext resultContext, R ignoredResult) {
+ notificationService.sendResultMessage(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(),
+ resultContext.getRunContext().getUserId(), null);
+ }
+
+ protected void publishFail(AbstractResultContext resultContext, String message) {
+ notificationService.publishFail(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(),
+ message, resultContext.getRunContext().getUserId(), getComputationType(), null);
+ }
+
+ /**
+ * 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
+ */
+ protected void preRun(C ignoredRunContext) {
+ LOGGER.info("Run {} computation...", getComputationType());
+ }
+
+ protected R run(C runContext, UUID resultUuid) throws Exception {
+ String provider = runContext.getProvider();
+ AtomicReference rootReporter = new AtomicReference<>(ReportNode.NO_OP);
+ ReportNode reportNode = ReportNode.NO_OP;
+
+ if (runContext.getReportInfos() != null && runContext.getReportInfos().reportUuid() != null) {
+ final String reportType = runContext.getReportInfos().computationType();
+ String rootReporterId = runContext.getReportInfos().reporterId() == null ? reportType : runContext.getReportInfos().reporterId() + "@" + reportType;
+ rootReporter.set(ReportNode.newRootReportNode().withMessageTemplate(rootReporterId, rootReporterId).build());
+ reportNode = rootReporter.get().newReportNode().withMessageTemplate(reportType, reportType + (provider != null ? " (" + provider + ")" : ""))
+ .withUntypedValue("providerToUse", Objects.requireNonNullElse(provider, "")).add();
+ // Delete any previous computation logs
+ observer.observe("report.delete",
+ runContext, () -> reportService.deleteReport(runContext.getReportInfos().reportUuid(), reportType));
+ }
+ runContext.setReportNode(reportNode);
+
+ preRun(runContext);
+ CompletableFuture future = runAsync(runContext, provider, resultUuid);
+ R result = future == null ? null : observer.observeRun("run", runContext, future::get);
+ postRun(runContext, rootReporter, result);
+ return result;
+ }
+
+ /**
+ * Do some extra task after running the computation
+ * @param runContext This context may be used for extra task in overriding classes
+ * @param rootReportNode root of the reporter tree
+ * @param ignoredResult The result of the computation
+ */
+ protected void postRun(C runContext, AtomicReference rootReportNode, R ignoredResult) {
+ if (runContext.getReportInfos().reportUuid() != null) {
+ observer.observe("report.send", runContext, () -> reportService.sendReport(runContext.getReportInfos().reportUuid(), rootReportNode.get()));
+ }
+ }
+
+ protected CompletableFuture runAsync(
+ C runContext,
+ String provider,
+ UUID resultUuid) {
+ lockRunAndCancel.lock();
+ try {
+ if (resultUuid != null && cancelComputationRequests.get(resultUuid) != null) {
+ return null;
+ }
+ CompletableFuture future = getCompletableFuture(runContext, provider, resultUuid);
+ if (resultUuid != null) {
+ futures.put(resultUuid, future);
+ }
+ return future;
+ } finally {
+ lockRunAndCancel.unlock();
+ }
+ }
+
+ protected abstract String getComputationType();
+
+ protected abstract CompletableFuture getCompletableFuture(C runContext, String provider, UUID resultUuid);
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/CancelContext.java b/src/main/java/com/powsybl/ws/commons/computation/service/CancelContext.java
new file mode 100644
index 0000000..2a7fe1d
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/CancelContext.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.service;
+
+import com.powsybl.ws.commons.computation.utils.MessageUtils;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.MessageBuilder;
+
+import java.util.Objects;
+import java.util.UUID;
+
+import static com.powsybl.ws.commons.computation.service.NotificationService.HEADER_RECEIVER;
+import static com.powsybl.ws.commons.computation.service.NotificationService.HEADER_RESULT_UUID;
+
+
+/**
+ * @author Anis Touri
+ */
+public record CancelContext(UUID resultUuid, String receiver) {
+
+ public CancelContext(UUID resultUuid, String receiver) {
+ this.resultUuid = Objects.requireNonNull(resultUuid);
+ this.receiver = Objects.requireNonNull(receiver);
+ }
+
+ public static CancelContext fromMessage(Message message) {
+ Objects.requireNonNull(message);
+ MessageHeaders headers = message.getHeaders();
+ UUID resultUuid = UUID.fromString(MessageUtils.getNonNullHeader(headers, HEADER_RESULT_UUID));
+ String receiver = headers.get(HEADER_RECEIVER, String.class);
+ return new CancelContext(resultUuid, receiver);
+ }
+
+ public Message toMessage() {
+ return MessageBuilder.withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .build();
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/ExecutionService.java b/src/main/java/com/powsybl/ws/commons/computation/service/ExecutionService.java
new file mode 100644
index 0000000..f6a124a
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/ExecutionService.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package com.powsybl.ws.commons.computation.service;
+
+import com.powsybl.computation.ComputationManager;
+import com.powsybl.computation.local.LocalComputationManager;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+
+/**
+ * @author David Braquart
+ */
+@Service
+@Getter
+public class ExecutionService {
+
+ private ExecutorService executorService;
+
+ private ComputationManager computationManager;
+
+ @SneakyThrows
+ @PostConstruct
+ private void postConstruct() {
+ executorService = Executors.newCachedThreadPool();
+ computationManager = new LocalComputationManager(getExecutorService());
+ }
+
+ @PreDestroy
+ private void preDestroy() {
+ executorService.shutdown();
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/NotificationService.java b/src/main/java/com/powsybl/ws/commons/computation/service/NotificationService.java
new file mode 100644
index 0000000..a7bf5c3
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/NotificationService.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.service;
+
+import com.powsybl.ws.commons.computation.utils.annotations.PostCompletion;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.stream.function.StreamBridge;
+import org.springframework.lang.Nullable;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.support.MessageBuilder;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+import java.util.UUID;
+
+import static com.powsybl.ws.commons.computation.utils.MessageUtils.shortenMessage;
+
+/**
+ * @author Etienne Homer message) {
+ RUN_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "Run-out-0", message);
+ }
+
+ public void sendCancelMessage(Message message) {
+ CANCEL_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "Cancel-out-0", message);
+ }
+
+ @PostCompletion
+ public void sendResultMessage(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 + "Result-out-0", message);
+ }
+
+ @PostCompletion
+ public void publishStop(UUID resultUuid, String receiver, String computationLabel) {
+ Message message = MessageBuilder
+ .withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .setHeader(HEADER_MESSAGE, getCancelMessage(computationLabel))
+ .build();
+ STOP_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "Stopped-out-0", message);
+ }
+
+ @PostCompletion
+ public void publishFail(UUID resultUuid, String receiver, String causeMessage, String userId, String computationLabel, Map additionalHeaders) {
+ MessageBuilder builder = MessageBuilder
+ .withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .setHeader(HEADER_MESSAGE, shortenMessage(
+ getFailedMessage(computationLabel) + " : " + causeMessage))
+ .setHeader(HEADER_USER_ID, userId)
+ .copyHeaders(additionalHeaders);
+ Message message = builder.build();
+ FAILED_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "Failed-out-0", message);
+ }
+
+ public static String getCancelMessage(String computationLabel) {
+ return computationLabel + " was canceled";
+ }
+
+ public static String getFailedMessage(String computationLabel) {
+ return computationLabel + " has failed";
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/ReportService.java b/src/main/java/com/powsybl/ws/commons/computation/service/ReportService.java
new file mode 100644
index 0000000..1fc1a0f
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/ReportService.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.commons.report.ReportNode;
+import lombok.Setter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * @author Anis Touri
+ */
+@Service
+public class ReportService {
+
+ static final String REPORT_API_VERSION = "v1";
+ private static final String DELIMITER = "/";
+ private static final String QUERY_PARAM_REPORT_TYPE_FILTER = "reportTypeFilter";
+ private static final String QUERY_PARAM_REPORT_THROW_ERROR = "errorOnReportNotFound";
+ @Setter
+ private String reportServerBaseUri;
+
+ private final RestTemplate restTemplate;
+
+ private final ObjectMapper objectMapper;
+
+ public ReportService(ObjectMapper objectMapper,
+ @Value("${gridsuite.services.report-server.base-uri:http://report-server/}") String reportServerBaseUri,
+ RestTemplate restTemplate) {
+ this.reportServerBaseUri = reportServerBaseUri;
+ this.objectMapper = objectMapper;
+ this.restTemplate = restTemplate;
+ }
+
+ private String getReportServerURI() {
+ return this.reportServerBaseUri + DELIMITER + REPORT_API_VERSION + DELIMITER + "reports" + DELIMITER;
+ }
+
+ public void sendReport(UUID reportUuid, ReportNode reportNode) {
+ Objects.requireNonNull(reportUuid);
+
+ var path = UriComponentsBuilder.fromPath("{reportUuid}")
+ .buildAndExpand(reportUuid)
+ .toUriString();
+ var headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+
+ try {
+ restTemplate.exchange(getReportServerURI() + path, HttpMethod.PUT, new HttpEntity<>(objectMapper.writeValueAsString(reportNode), headers), ReportNode.class);
+ } catch (JsonProcessingException error) {
+ throw new PowsyblException("Error sending report", error);
+ }
+ }
+
+ public void deleteReport(UUID reportUuid, String reportType) {
+ Objects.requireNonNull(reportUuid);
+
+ var path = UriComponentsBuilder.fromPath("{reportUuid}")
+ .queryParam(QUERY_PARAM_REPORT_TYPE_FILTER, reportType)
+ .queryParam(QUERY_PARAM_REPORT_THROW_ERROR, false)
+ .buildAndExpand(reportUuid)
+ .toUriString();
+ var headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ restTemplate.exchange(getReportServerURI() + path, HttpMethod.DELETE, new HttpEntity<>(headers), Void.class);
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/UuidGeneratorService.java b/src/main/java/com/powsybl/ws/commons/computation/service/UuidGeneratorService.java
new file mode 100644
index 0000000..faca3b7
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/service/UuidGeneratorService.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.service;
+
+import org.springframework.stereotype.Service;
+
+import java.util.UUID;
+
+/**
+ * @author Geoffroy Jamgotchian
+ */
+@Service
+public class UuidGeneratorService {
+
+ public UUID generate() {
+ return UUID.randomUUID();
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/utils/MessageUtils.java b/src/main/java/com/powsybl/ws/commons/computation/utils/MessageUtils.java
new file mode 100644
index 0000000..a18903d
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/utils/MessageUtils.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.utils;
+
+import com.powsybl.commons.PowsyblException;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.lang.Nullable;
+import org.springframework.messaging.MessageHeaders;
+
+/**
+ * @author Thang PHAM
+ */
+public final class MessageUtils {
+ public static final int MSG_MAX_LENGTH = 256;
+
+ private MessageUtils() {
+ throw new AssertionError("Suppress default constructor for noninstantiability");
+ }
+
+ public static String getNonNullHeader(MessageHeaders headers, String name) {
+ String header = headers.get(name, String.class);
+ if (header == null) {
+ throw new PowsyblException("Header '" + name + "' not found");
+ }
+ return header;
+ }
+
+ /**
+ * Prevent the message from being too long for RabbitMQ.
+ * @apiNote the beginning and ending are both kept, it should make it easier to identify
+ */
+ public static String shortenMessage(@Nullable final String msg) {
+ return StringUtils.abbreviateMiddle(msg, " ... ", MSG_MAX_LENGTH);
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletion.java b/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletion.java
new file mode 100644
index 0000000..5e9b62a
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletion.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2023, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package com.powsybl.ws.commons.computation.utils.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Anis Touri > RUNNABLE = new ThreadLocal<>();
+
+ // register a new runnable for post completion execution
+ public void execute(Runnable runnable) {
+ if (TransactionSynchronizationManager.isSynchronizationActive()) {
+ List runnables = RUNNABLE.get();
+ if (runnables == null) {
+ runnables = new ArrayList<>(Collections.singletonList(runnable));
+ } else {
+ runnables.add(runnable);
+ }
+ RUNNABLE.set(runnables);
+ TransactionSynchronizationManager.registerSynchronization(this);
+ } else {
+ // if transaction synchronisation is not active
+ runnable.run();
+ }
+ }
+
+ @Override
+ public void afterCompletion(int status) {
+ List runnables = RUNNABLE.get();
+ runnables.forEach(Runnable::run);
+ RUNNABLE.remove();
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionAnnotationAspect.java b/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionAnnotationAspect.java
new file mode 100644
index 0000000..5bc51ef
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionAnnotationAspect.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2023, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.utils.annotations;
+
+import lombok.AllArgsConstructor;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Anis Touri {
+ try {
+ pjp.proceed(pjp.getArgs());
+ } catch (Throwable e) {
+ throw new PostCompletionException(e);
+ }
+ });
+ return null;
+ }
+}
diff --git a/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionException.java b/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionException.java
new file mode 100644
index 0000000..9d2918c
--- /dev/null
+++ b/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionException.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package com.powsybl.ws.commons.computation.utils.annotations;
+
+/**
+ * @author Slimane Amar
+ */
+public class PostCompletionException extends RuntimeException {
+ public PostCompletionException(Throwable t) {
+ super(t);
+ }
+}
diff --git a/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java
new file mode 100644
index 0000000..825046c
--- /dev/null
+++ b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java
@@ -0,0 +1,288 @@
+package com.powsybl.ws.commons.computation;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.computation.dto.ReportInfos;
+import com.powsybl.ws.commons.computation.service.AbstractComputationObserver;
+import com.powsybl.ws.commons.computation.service.AbstractComputationResultService;
+import com.powsybl.ws.commons.computation.service.AbstractComputationRunContext;
+import com.powsybl.ws.commons.computation.service.AbstractComputationService;
+import com.powsybl.ws.commons.computation.service.AbstractResultContext;
+import com.powsybl.ws.commons.computation.service.AbstractWorkerService;
+import com.powsybl.ws.commons.computation.service.CancelContext;
+import com.powsybl.ws.commons.computation.service.ExecutionService;
+import com.powsybl.ws.commons.computation.service.NotificationService;
+import com.powsybl.ws.commons.computation.service.ReportService;
+import com.powsybl.ws.commons.computation.service.UuidGeneratorService;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.micrometer.observation.ObservationRegistry;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.assertj.core.api.WithAssertions;
+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.junit.jupiter.MockitoExtension;
+import org.springframework.cloud.stream.function.StreamBridge;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.support.MessageBuilder;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+import static com.powsybl.ws.commons.computation.ComputationTest.MockComputationStatus.NOT_DONE;
+import static com.powsybl.ws.commons.computation.service.NotificationService.HEADER_RECEIVER;
+import static com.powsybl.ws.commons.computation.service.NotificationService.HEADER_RESULT_UUID;
+import static com.powsybl.ws.commons.computation.service.NotificationService.HEADER_USER_ID;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith({ MockitoExtension.class })
+@Slf4j
+class ComputationTest implements WithAssertions {
+ private static final String COMPUTATION_TYPE = "mockComputation";
+ @Mock
+ private VariantManager variantManager;
+ @Mock
+ private NetworkStoreService networkStoreService;
+ @Mock
+ private ReportService reportService;
+ private final ExecutionService executionService = new ExecutionService();
+ private final UuidGeneratorService uuidGeneratorService = new UuidGeneratorService();
+ @Mock
+ private StreamBridge publisher;
+ private NotificationService notificationService;
+ @Mock
+ private ObjectMapper objectMapper;
+ @Mock
+ private Network network;
+
+ public static class MockComputationResult { }
+
+ public static class MockComputationParameters { }
+
+ public enum MockComputationStatus {
+ NOT_DONE,
+ RUNNING,
+ COMPLETED
+ }
+
+ public static class MockComputationResultService extends AbstractComputationResultService {
+ Map mockDBStatus = new HashMap<>();
+
+ @Override
+ public void insertStatus(List resultUuids, MockComputationStatus status) {
+ resultUuids.forEach(uuid ->
+ mockDBStatus.put(uuid, status));
+ }
+
+ @Override
+ public void delete(UUID resultUuid) {
+ mockDBStatus.remove(resultUuid);
+ }
+
+ @Override
+ public void deleteAll() {
+ mockDBStatus.clear();
+ }
+
+ @Override
+ public MockComputationStatus findStatus(UUID resultUuid) {
+ return mockDBStatus.get(resultUuid);
+ }
+ }
+
+ public static class MockComputationObserver extends AbstractComputationObserver {
+ protected MockComputationObserver(@NonNull ObservationRegistry observationRegistry, @NonNull MeterRegistry meterRegistry) {
+ super(observationRegistry, meterRegistry);
+ }
+
+ @Override
+ protected String getComputationType() {
+ return COMPUTATION_TYPE;
+ }
+
+ @Override
+ protected String getResultStatus(MockComputationResult res) {
+ return res != null ? "OK" : "NOK";
+ }
+ }
+
+ public static class MockComputationRunContext extends AbstractComputationRunContext {
+ // makes the mock computation to behave in a specific way
+ @Getter @Setter
+ ComputationResultWanted computationResWanted = ComputationResultWanted.SUCCESS;
+
+ protected MockComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos,
+ String userId, String provider, MockComputationParameters parameters) {
+ super(networkUuid, variantId, receiver, reportInfos, userId, provider, parameters);
+ }
+ }
+
+ public static class MockComputationResultContext extends AbstractResultContext {
+ protected MockComputationResultContext(UUID resultUuid, MockComputationRunContext runContext) {
+ super(resultUuid, runContext);
+ }
+ }
+
+ public class MockComputationService extends AbstractComputationService {
+ protected MockComputationService(NotificationService notificationService, MockComputationResultService resultService, ObjectMapper objectMapper, UuidGeneratorService uuidGeneratorService, String defaultProvider) {
+ super(notificationService, resultService, objectMapper, uuidGeneratorService, defaultProvider);
+ }
+
+ @Override
+ public List getProviders() {
+ return List.of();
+ }
+
+ @Override
+ public UUID runAndSaveResult(MockComputationRunContext runContext) {
+ return resultUuid;
+ }
+ }
+
+ enum ComputationResultWanted {
+ SUCCESS,
+ FAIL
+ }
+
+ public class MockComputationWorkerService extends AbstractWorkerService {
+ protected MockComputationWorkerService(NetworkStoreService networkStoreService, NotificationService notificationService, ReportService reportService, MockComputationResultService resultService, ExecutionService executionService, AbstractComputationObserver observer, ObjectMapper objectMapper) {
+ super(networkStoreService, notificationService, reportService, resultService, executionService, observer, objectMapper);
+ }
+
+ @Override
+ protected AbstractResultContext fromMessage(Message message) {
+ return resultContext;
+ }
+
+ @Override
+ protected void saveResult(Network network, AbstractResultContext resultContext, MockComputationResult result) { }
+
+ @Override
+ protected String getComputationType() {
+ return COMPUTATION_TYPE;
+ }
+
+ @Override
+ protected CompletableFuture getCompletableFuture(MockComputationRunContext runContext, String provider, UUID resultUuid) {
+ final CompletableFuture completableFuture = new CompletableFuture<>();
+ switch (runContext.getComputationResWanted()) {
+ case FAIL:
+ completableFuture.completeExceptionally(new RuntimeException("Computation failed but with an artificially longer messaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaage"));
+ break;
+ case SUCCESS:
+ return CompletableFuture.supplyAsync(MockComputationResult::new);
+ }
+ return completableFuture;
+ }
+ }
+
+ private MockComputationWorkerService workerService;
+ private MockComputationService computationService;
+ private MockComputationResultContext resultContext;
+ final UUID networkUuid = UUID.fromString("11111111-1111-1111-1111-111111111111");
+ final UUID reportUuid = UUID.fromString("22222222-2222-2222-2222-222222222222");
+ final UUID resultUuid = UUID.fromString("33333333-3333-3333-3333-333333333333");
+ final String reporterId = "44444444-4444-4444-4444-444444444444";
+ final String userId = "MockComputation_UserId";
+ final String receiver = "MockComputation_Receiver";
+ final String provider = "MockComputation_Provider";
+ Message message;
+ MockComputationRunContext runContext;
+
+ @BeforeEach
+ void init() {
+ MockComputationResultService resultService = new MockComputationResultService();
+ notificationService = new NotificationService(publisher);
+ workerService = new MockComputationWorkerService(
+ networkStoreService,
+ notificationService,
+ reportService,
+ resultService,
+ executionService,
+ new MockComputationObserver(ObservationRegistry.create(), new SimpleMeterRegistry()),
+ objectMapper
+ );
+ computationService = new MockComputationService(notificationService, resultService, objectMapper, uuidGeneratorService, provider);
+
+ MessageBuilder builder = MessageBuilder
+ .withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .setHeader(HEADER_USER_ID, userId);
+ message = builder.build();
+
+ runContext = new MockComputationRunContext(networkUuid, null, receiver,
+ new ReportInfos(reportUuid, reporterId, COMPUTATION_TYPE), userId, provider, new MockComputationParameters());
+ runContext.setNetwork(network);
+ resultContext = new MockComputationResultContext(resultUuid, runContext);
+ }
+
+ void initComputationExecution() {
+ when(networkStoreService.getNetwork(eq(networkUuid), any(PreloadingStrategy.class)))
+ .thenReturn(network);
+ when(network.getVariantManager()).thenReturn(variantManager);
+ }
+
+ @Test
+ void testComputationSuccess() {
+ // inits
+ initComputationExecution();
+ runContext.setComputationResWanted(ComputationResultWanted.SUCCESS);
+
+ // execution / cleaning
+ workerService.consumeRun().accept(message);
+
+ // test the course
+ verify(notificationService.getPublisher(), times(1)).send(eq("publishResult-out-0"), isA(Message.class));
+ }
+
+ @Test
+ void testComputationFailed() {
+ // inits
+ initComputationExecution();
+ runContext.setComputationResWanted(ComputationResultWanted.FAIL);
+
+ // execution / cleaning
+ workerService.consumeRun().accept(message);
+
+ // test the course
+ verify(notificationService.getPublisher(), times(1)).send(eq("publishFailed-out-0"), isA(Message.class));
+ }
+
+ @Test
+ void testComputationCancelled() {
+ MockComputationStatus baseStatus = NOT_DONE;
+ computationService.setStatus(List.of(resultUuid), baseStatus);
+ assertEquals(baseStatus, computationService.getStatus(resultUuid));
+
+ computationService.stop(resultUuid, receiver);
+
+ // test the course
+ verify(notificationService.getPublisher(), times(1)).send(eq("publishCancel-out-0"), isA(Message.class));
+
+ Message cancelMessage = MessageBuilder.withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .build();
+ CancelContext cancelContext = CancelContext.fromMessage(cancelMessage);
+ assertEquals(resultUuid, cancelContext.resultUuid());
+ assertEquals(receiver, cancelContext.receiver());
+ }
+}
diff --git a/src/test/java/com/powsybl/ws/commons/computation/service/ReportServiceTest.java b/src/test/java/com/powsybl/ws/commons/computation/service/ReportServiceTest.java
new file mode 100644
index 0000000..57c6556
--- /dev/null
+++ b/src/test/java/com/powsybl/ws/commons/computation/service/ReportServiceTest.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package com.powsybl.ws.commons.computation.service;
+
+import com.powsybl.commons.report.ReportNode;
+import com.powsybl.ws.commons.computation.ComputationConfig;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient;
+import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.test.web.client.match.MockRestRequestMatchers;
+import org.springframework.test.web.client.response.MockRestResponseCreators;
+import org.springframework.web.client.RestClientException;
+
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * @author Mathieu Deharbe reportService.sendReport(REPORT_UUID, reportNode));
+ }
+
+ @Test
+ void testSendReportFailed() {
+ final ReportNode reportNode = ReportNode.newRootReportNode().withMessageTemplate("test", "a test").build();
+ server.expect(MockRestRequestMatchers.method(HttpMethod.PUT))
+ .andExpect(MockRestRequestMatchers.requestTo("http://report-server/v1/reports/" + REPORT_ERROR_UUID))
+ .andRespond(MockRestResponseCreators.withServerError());
+ assertThatThrownBy(() -> reportService.sendReport(REPORT_ERROR_UUID, reportNode)).isInstanceOf(RestClientException.class);
+ }
+
+ @Test
+ void testDeleteReport() {
+ server.expect(MockRestRequestMatchers.method(HttpMethod.DELETE))
+ .andExpect(MockRestRequestMatchers.requestTo("http://report-server/v1/reports/" + REPORT_UUID + "?reportTypeFilter=MockReportType&errorOnReportNotFound=false"))
+ .andExpect(MockRestRequestMatchers.content().bytes(new byte[0]))
+ .andRespond(MockRestResponseCreators.withSuccess());
+ assertThatNoException().isThrownBy(() -> reportService.deleteReport(REPORT_UUID, "MockReportType"));
+ }
+
+ @Test
+ void testDeleteReportFailed() {
+ server.expect(MockRestRequestMatchers.method(HttpMethod.DELETE))
+ .andExpect(MockRestRequestMatchers.requestTo("http://report-server/v1/reports/" + REPORT_ERROR_UUID + "?reportTypeFilter=MockReportType&errorOnReportNotFound=false"))
+ .andExpect(MockRestRequestMatchers.content().bytes(new byte[0]))
+ .andRespond(MockRestResponseCreators.withServerError());
+ assertThatThrownBy(() -> reportService.deleteReport(REPORT_ERROR_UUID, "MockReportType")).isInstanceOf(RestClientException.class);
+ }
+}
+