diff --git a/pom.xml b/pom.xml index 852b037..dd398be 100644 --- a/pom.xml +++ b/pom.xml @@ -38,9 +38,10 @@ - 11 2024.1.0 3.1.2 + 4.0.3 + 1.12.0 @@ -92,6 +93,12 @@ spring-boot-starter-tomcat true + + org.springframework.cloud + spring-cloud-stream + ${spring-cloud-stream.version} + true + @@ -110,6 +117,7 @@ provided + org.junit.vintage junit-vintage-engine @@ -124,11 +132,34 @@ org.springframework.boot spring-boot-starter-web test + + + ch.qos.logback + logback-classic + + org.springframework.boot spring-boot-starter-test test + + + org.aspectj + aspectjweaver + true + + + io.micrometer + micrometer-core + true + + + com.powsybl + powsybl-network-store-client + ${powsybl-network-store-client.version} + true + diff --git a/src/main/java/com/powsybl/ws/commons/computation/ComputationConfig.java b/src/main/java/com/powsybl/ws/commons/computation/ComputationConfig.java new file mode 100644 index 0000000..fc5ddc7 --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/ComputationConfig.java @@ -0,0 +1,24 @@ +package com.powsybl.ws.commons.computation; + +import com.fasterxml.jackson.databind.InjectableValues; +import com.powsybl.commons.report.ReportNodeDeserializer; +import com.powsybl.commons.report.ReportNodeJsonModule; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Clock; + +@Configuration +public class ComputationConfig { + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { + return builder -> builder.modulesToInstall(new ReportNodeJsonModule()) + .postConfigurer(objMapper -> objMapper.setInjectableValues(new InjectableValues.Std().addValue(ReportNodeDeserializer.DICTIONARY_VALUE_ID, null))); + } + + @Bean + public Clock clock() { + return Clock.systemUTC(); + } +} diff --git a/src/main/java/com/powsybl/ws/commons/computation/dto/ReportInfos.java b/src/main/java/com/powsybl/ws/commons/computation/dto/ReportInfos.java new file mode 100644 index 0000000..16bb651 --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/dto/ReportInfos.java @@ -0,0 +1,24 @@ +/** + * 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.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.util.UUID; + +/** + * @author Florent MILLOT + */ +@Builder +@Schema(description = "Report infos") +public record ReportInfos( + UUID reportUuid, + String reporterId, + String computationType +) { +} diff --git a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationObserver.java b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationObserver.java new file mode 100644 index 0000000..69c1991 --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationObserver.java @@ -0,0 +1,77 @@ +/** + * 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 io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; + +/** + * @author Mathieu Deharbe + * @param 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); + } +} +