From 6cf2c9271fbd839ea269df2884119c474147430d Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Tue, 28 May 2024 16:19:15 +0200 Subject: [PATCH 01/18] generic computation classes (short circuit code version) Signed-off-by: Mathieu DEHARBE --- pom.xml | 41 ++- .../commons/computation/dto/ReportInfos.java | 24 ++ .../service/AbstractComputationObserver.java | 76 ++++++ .../AbstractComputationResultService.java | 25 ++ .../AbstractComputationRunContext.java | 46 ++++ .../service/AbstractComputationService.java | 76 ++++++ .../service/AbstractResultContext.java | 75 ++++++ .../service/AbstractWorkerService.java | 244 ++++++++++++++++++ .../computation/service/CancelContext.java | 45 ++++ .../computation/service/ExecutionService.java | 44 ++++ .../service/NotificationService.java | 117 +++++++++ .../computation/service/ReportService.java | 86 ++++++ .../service/UuidGeneratorService.java | 22 ++ .../computation/utils/MessageUtils.java | 42 +++ .../utils/annotations/PostCompletion.java | 21 ++ .../annotations/PostCompletionAdapter.java | 47 ++++ .../PostCompletionAnnotationAspect.java | 47 ++++ .../annotations/PostCompletionException.java | 17 ++ 18 files changed, 1094 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/powsybl/ws/commons/computation/dto/ReportInfos.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationObserver.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationResultService.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationRunContext.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationService.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/AbstractResultContext.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/CancelContext.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/ExecutionService.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/NotificationService.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/ReportService.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/service/UuidGeneratorService.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/utils/MessageUtils.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletion.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionAdapter.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionAnnotationAspect.java create mode 100644 src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionException.java diff --git a/pom.xml b/pom.xml index 852b037..fb49c37 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,6 @@ - 11 2024.1.0 3.1.2 @@ -130,5 +129,45 @@ spring-boot-starter-test test + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + + org.springframework.cloud + spring-cloud-stream + 4.0.3 + + + org.aspectj + aspectjweaver + + + com.powsybl + powsybl-commons + + + com.powsybl + powsybl-computation + + + com.powsybl + powsybl-computation-local + + + com.powsybl + powsybl-network-store-client + 1.11.0 + + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-registry-prometheus + runtime + 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..6e30ae3 --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationObserver.java @@ -0,0 +1,76 @@ +/** + * 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.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 +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..b758e10 --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationService.java @@ -0,0 +1,76 @@ +/** + * 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; + @Getter + private final String defaultProvider; + + protected UuidGeneratorService uuidGeneratorService; + protected T resultService; + + 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..62749c0 --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractResultContext.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.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import static com.powsybl.ws.commons.computation.service.NotificationService.*; + +/** + * @author Mathieu Deharbe + * @param run context specific to a computation, including parameters + */ +@Getter +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(ObjectMapper objectMapper) { + String parametersJson; + try { + parametersJson = objectMapper.writeValueAsString(runContext.getParameters()); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + return MessageBuilder.withPayload(parametersJson) + .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..a3444ae --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java @@ -0,0 +1,244 @@ +/** + * 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.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, T 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 T resultService; + + protected AbstractWorkerService(NetworkStoreService networkStoreService, + NotificationService notificationService, + ReportService reportService, + T 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(S 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); + S 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()); + } + } finally { + futures.remove(resultContext.getResultUuid()); + cancelComputationRequests.remove(resultContext.getResultUuid()); + } + }; + } + + public Consumer> consumeCancel() { + return message -> cancelAsync(CancelContext.fromMessage(message)); + } + + protected abstract void saveResult(Network network, AbstractResultContext resultContext, S result); + + protected void sendResultMessage(AbstractResultContext resultContext, Map additionalHeaders) { + notificationService.sendResultMessage(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(), + resultContext.getRunContext().getUserId(), additionalHeaders); + + } + + protected void publishFail(AbstractResultContext resultContext, String message, Map additionalHeaders) { + notificationService.publishFail(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(), + message, resultContext.getRunContext().getUserId(), getComputationType(), additionalHeaders); + } + + protected abstract void sendResultMessage(AbstractResultContext resultContext, S result); + + protected abstract void publishFail(AbstractResultContext resultContext, String message); + + /** + * 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(R ignoredRunContext) { + LOGGER.info("Run {} computation...", getComputationType()); + } + + protected S run(R runContext, UUID resultUuid) throws Exception { + String provider = runContext.getProvider(); + if (provider == null) { + provider = ""; + } + 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, String.format("%s (%s)", reportType, provider)) + .withUntypedValue("providerToUse", 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); + S result = future == null ? null : observer.observeRun("run", runContext, future::get); + postRun(runContext, rootReporter); + 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 + */ + protected void postRun(R runContext, AtomicReference rootReportNode) { + if (runContext.getReportInfos().reportUuid() != null) { + observer.observe("report.send", runContext, () -> reportService.sendReport(runContext.getReportInfos().reportUuid(), rootReportNode.get())); + } + } + + protected CompletableFuture runAsync( + R 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(R 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..daa9304 --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/service/ReportService.java @@ -0,0 +1,86 @@ +/** + * 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 com.powsybl.commons.report.ReportNodeJsonModule; +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; + ReportNodeJsonModule reportNodeJsonModule = new ReportNodeJsonModule(); + objectMapper.registerModule(reportNodeJsonModule); + } + + 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..5f131de --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/utils/MessageUtils.java @@ -0,0 +1,42 @@ +/* + * 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.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(String msg) { + if (msg == null) { + return null; + } + + 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); + return; + } + // 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..43912c5 --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionAnnotationAspect.java @@ -0,0 +1,47 @@ +/** + * 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 + */ + +public class PostCompletionException extends RuntimeException { + public PostCompletionException(Throwable t) { + super(t); + } +} From 93f15dea54186ab4abd2ddfd8ca5cb075eea28a5 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 30 May 2024 16:39:17 +0200 Subject: [PATCH 02/18] jackson config Signed-off-by: Mathieu DEHARBE --- .../commons/computation/config/JacksonConfig.java | 14 ++++++++++++++ .../commons/computation/service/ReportService.java | 3 --- 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/powsybl/ws/commons/computation/config/JacksonConfig.java diff --git a/src/main/java/com/powsybl/ws/commons/computation/config/JacksonConfig.java b/src/main/java/com/powsybl/ws/commons/computation/config/JacksonConfig.java new file mode 100644 index 0000000..6009c8d --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/config/JacksonConfig.java @@ -0,0 +1,14 @@ +package com.powsybl.ws.commons.computation.config; + +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.powsybl.commons.report.ReportNodeJsonModule; + +@Configuration +public class JacksonConfig { + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { + return builder -> builder.build().registerModule(new ReportNodeJsonModule()); + } +} 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 index daa9304..1fc1a0f 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/ReportService.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/ReportService.java @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.powsybl.commons.PowsyblException; import com.powsybl.commons.report.ReportNode; -import com.powsybl.commons.report.ReportNodeJsonModule; import lombok.Setter; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; @@ -47,8 +46,6 @@ public ReportService(ObjectMapper objectMapper, this.reportServerBaseUri = reportServerBaseUri; this.objectMapper = objectMapper; this.restTemplate = restTemplate; - ReportNodeJsonModule reportNodeJsonModule = new ReportNodeJsonModule(); - objectMapper.registerModule(reportNodeJsonModule); } private String getReportServerURI() { From 4b47edcc502c6db0567e8a2e2f74cbc48e9f6127 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 30 May 2024 17:28:00 +0200 Subject: [PATCH 03/18] remove one dependency Signed-off-by: Mathieu DEHARBE --- pom.xml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index fb49c37..a79a057 100644 --- a/pom.xml +++ b/pom.xml @@ -129,15 +129,15 @@ spring-boot-starter-test test - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.1.0 - org.springframework.cloud spring-cloud-stream - 4.0.3 + 4.0.3 + + + com.powsybl + powsybl-network-store-client + 1.11.0 org.aspectj @@ -155,11 +155,6 @@ com.powsybl powsybl-computation-local - - com.powsybl - powsybl-network-store-client - 1.11.0 - io.micrometer micrometer-core From fe3a1422ab2320f7a832692639d232191155f888 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 30 May 2024 18:35:17 +0200 Subject: [PATCH 04/18] turns versions numbers into properties Signed-off-by: Mathieu DEHARBE --- pom.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a79a057..09aa9dd 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,8 @@ 2024.1.0 3.1.2 + 4.0.3 + 1.11.0 @@ -132,12 +134,12 @@ org.springframework.cloud spring-cloud-stream - 4.0.3 + ${spring-cloud-stream.version} com.powsybl powsybl-network-store-client - 1.11.0 + ${powsybl-network-store-client.version} org.aspectj From 0acb13c6c411ab3af28a50cf9b9350cd47ef866d Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Mon, 3 Jun 2024 18:11:26 +0200 Subject: [PATCH 05/18] few adaptations (voltage init etc) Signed-off-by: Mathieu DEHARBE --- .../service/AbstractWorkerService.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) 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 index a3444ae..8d196e9 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java @@ -144,6 +144,7 @@ public Consumer> consumeRun() { LOGGER.error(NotificationService.getFailedMessage(getComputationType()), e); publishFail(resultContext, e.getMessage()); resultService.delete(resultContext.getResultUuid()); + this.handleNonCancellationException(resultContext, e); } } finally { futures.remove(resultContext.getResultUuid()); @@ -152,6 +153,14 @@ public Consumer> consumeRun() { }; } + /** + * 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)); } @@ -169,9 +178,15 @@ protected void publishFail(AbstractResultContext resultContext, String messag message, resultContext.getRunContext().getUserId(), getComputationType(), additionalHeaders); } - protected abstract void sendResultMessage(AbstractResultContext resultContext, S result); + protected void sendResultMessage(AbstractResultContext resultContext, S ignoredResult) { + notificationService.sendResultMessage(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(), + resultContext.getRunContext().getUserId(), null); + } - protected abstract void publishFail(AbstractResultContext resultContext, String message); + 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 @@ -183,9 +198,6 @@ protected void preRun(R ignoredRunContext) { protected S run(R runContext, UUID resultUuid) throws Exception { String provider = runContext.getProvider(); - if (provider == null) { - provider = ""; - } AtomicReference rootReporter = new AtomicReference<>(ReportNode.NO_OP); ReportNode reportNode = ReportNode.NO_OP; @@ -204,7 +216,7 @@ protected S run(R runContext, UUID resultUuid) throws Exception { preRun(runContext); CompletableFuture future = runAsync(runContext, provider, resultUuid); S result = future == null ? null : observer.observeRun("run", runContext, future::get); - postRun(runContext, rootReporter); + postRun(runContext, rootReporter, result); return result; } @@ -212,8 +224,9 @@ protected S run(R runContext, UUID resultUuid) throws Exception { * 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(R runContext, AtomicReference rootReportNode) { + protected void postRun(R runContext, AtomicReference rootReportNode, S ignoredResult) { if (runContext.getReportInfos().reportUuid() != null) { observer.observe("report.send", runContext, () -> reportService.sendReport(runContext.getReportInfos().reportUuid(), rootReportNode.get())); } From 259548033e61146620fbaedeffb824767a6e244a Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Tue, 4 Jun 2024 12:13:48 +0200 Subject: [PATCH 06/18] handles null providers Signed-off-by: Mathieu DEHARBE --- .../commons/computation/service/AbstractWorkerService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 8d196e9..68cc943 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java @@ -21,6 +21,7 @@ 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; @@ -205,8 +206,8 @@ protected S run(R runContext, UUID resultUuid) throws Exception { 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, String.format("%s (%s)", reportType, provider)) - .withUntypedValue("providerToUse", provider).add(); + 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)); From 09e84b3f092f598af9188aa61d1bc7583840c1f5 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Wed, 5 Jun 2024 15:41:33 +0200 Subject: [PATCH 07/18] computation test v1 Signed-off-by: Mathieu DEHARBE --- .../powsybl/ws/commons/ComputationTest.java | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 src/test/java/com/powsybl/ws/commons/ComputationTest.java diff --git a/src/test/java/com/powsybl/ws/commons/ComputationTest.java b/src/test/java/com/powsybl/ws/commons/ComputationTest.java new file mode 100644 index 0000000..1d76471 --- /dev/null +++ b/src/test/java/com/powsybl/ws/commons/ComputationTest.java @@ -0,0 +1,200 @@ +package com.powsybl.ws.commons; + +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.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.NonNull; +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.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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 NetworkStoreService networkStoreService; + @Mock + private ReportService reportService; + @Mock + private ExecutionService executionService; + @Mock + private NotificationService notificationService; + @Mock + private ObjectMapper objectMapper; + @Mock + private Network network; + + public class MockComputationResult {} + public class MockComputationParameters {} + public enum MockComputationStatus { + NOT_DONE, + RUNNING, + COMPLETED + } + @Service + public class MockComputationResultService extends AbstractComputationResultService { + @Override + public void insertStatus(List resultUuids, MockComputationStatus status) {} + @Override + public void delete(UUID resultUuid) {} + @Override + public void deleteAll() {} + @Override + public MockComputationStatus findStatus(UUID resultUuid) { + return null; + } + } + @Service + public 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 class MockComputationRunContext extends AbstractComputationRunContext { + 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 class MockComputationResultContext extends AbstractResultContext { + protected MockComputationResultContext(UUID resultUuid, MockComputationRunContext runContext) { + super(resultUuid, runContext); + } + } + @Service + 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; + } + } + @Service + 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) { + return CompletableFuture.supplyAsync(MockComputationResult::new); + } + } + + @Mock + private MockComputationResultService resultService; + @Mock + private VariantManager variantManager; + @Mock + private MockComputationWorkerService workerService; + 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; + + @BeforeEach + void init() { + workerService = new MockComputationWorkerService( + networkStoreService, + notificationService, + reportService, + resultService, + executionService, + new MockComputationObserver(ObservationRegistry.create(), new SimpleMeterRegistry()), + objectMapper + ); + MessageBuilder builder = MessageBuilder + .withPayload("") + .setHeader(HEADER_RESULT_UUID, resultUuid.toString()) + .setHeader(HEADER_RECEIVER, receiver) + .setHeader(HEADER_USER_ID, userId); + message = builder.build(); + } + + @Test + void testComputationImplementation() throws Exception { + // inits + final MockComputationRunContext runContext = new MockComputationRunContext(networkUuid, null, receiver, + new ReportInfos(reportUuid, reporterId, COMPUTATION_TYPE), userId, provider, new MockComputationParameters()); + runContext.setNetwork(network); + resultContext = new MockComputationResultContext(resultUuid, runContext); + + when(networkStoreService.getNetwork(eq(networkUuid), any(PreloadingStrategy.class))) + .thenReturn(network); + when(network.getVariantManager()).thenReturn(variantManager); + + // execution / cleaning + workerService.consumeRun().accept(message); + workerService.consumeCancel().accept(message); + + // test the course + // TODO : comment vérifier qu'il n'y a plus rien à annuler (plus rien dans les futures) + verify(notificationService, times(1)) + .sendResultMessage(resultUuid, receiver, userId, null); + } + + // TODO : faire un test publishFail ?? + // TODO : et une annulation ?? (avec un getCompletableFuture qui aurait un léger délai pour pouvoir annuler) +} From 0d6145e15464079907b8d079d79028fc50c53493 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 6 Jun 2024 11:15:44 +0200 Subject: [PATCH 08/18] testComputationFailed Signed-off-by: Mathieu DEHARBE --- .../powsybl/ws/commons/ComputationTest.java | 110 ++++++++++++++---- 1 file changed, 89 insertions(+), 21 deletions(-) diff --git a/src/test/java/com/powsybl/ws/commons/ComputationTest.java b/src/test/java/com/powsybl/ws/commons/ComputationTest.java index 1d76471..f56567c 100644 --- a/src/test/java/com/powsybl/ws/commons/ComputationTest.java +++ b/src/test/java/com/powsybl/ws/commons/ComputationTest.java @@ -19,7 +19,9 @@ 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; @@ -34,7 +36,10 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import static com.powsybl.ws.commons.ComputationTest.MockComputationStatus.COMPLETED; 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; @@ -61,80 +66,129 @@ class ComputationTest implements WithAssertions { @Mock private Network network; - public class MockComputationResult {} - public class MockComputationParameters {} + public static class MockComputationResult { } + + public static class MockComputationParameters { } + public enum MockComputationStatus { NOT_DONE, RUNNING, COMPLETED } + @Service - public class MockComputationResultService extends AbstractComputationResultService { + public static class MockComputationResultService extends AbstractComputationResultService { @Override - public void insertStatus(List resultUuids, MockComputationStatus status) {} + public void insertStatus(List resultUuids, MockComputationStatus status) { } + @Override - public void delete(UUID resultUuid) {} + public void delete(UUID resultUuid) { } + @Override - public void deleteAll() {} + public void deleteAll() { } + @Override public MockComputationStatus findStatus(UUID resultUuid) { return null; } } + @Service - public class MockComputationObserver extends AbstractComputationObserver { + 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 class MockComputationRunContext extends AbstractComputationRunContext { - protected MockComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos, String userId, String provider, MockComputationParameters parameters) { + + 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 class MockComputationResultContext extends AbstractResultContext { + + public static class MockComputationResultContext extends AbstractResultContext { protected MockComputationResultContext(UUID resultUuid, MockComputationRunContext runContext) { super(resultUuid, runContext); } } + @Service 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, + SLOW_SUCCESS + } + @Service 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) {} + protected void saveResult(Network network, AbstractResultContext resultContext, MockComputationResult result) { } + @Override - protected String getComputationType() { return COMPUTATION_TYPE;} + protected String getComputationType() { + return COMPUTATION_TYPE; + } + @Override protected CompletableFuture getCompletableFuture(MockComputationRunContext runContext, String provider, UUID resultUuid) { - return CompletableFuture.supplyAsync(MockComputationResult::new); + final CompletableFuture completableFuture = new CompletableFuture<>(); + switch (runContext.getComputationResWanted()) { + case FAIL: + completableFuture.completeExceptionally(new RuntimeException("Computation failed")); + break; + case SLOW_SUCCESS: + ExecutorService executorService = Executors.newFixedThreadPool(1); + executorService.submit(() -> { + Thread.sleep(5000); + // TODO : try something like this : await().atMost(2, Duration.SECONDS).until(didTheThing()); // Compliant + completableFuture.complete(new MockComputationResult()); + return COMPLETED; + }); + break; + case SUCCESS: + return CompletableFuture.supplyAsync(MockComputationResult::new); + } + return completableFuture; } } @@ -153,6 +207,7 @@ protected CompletableFuture getCompletableFuture(MockComp final String receiver = "MockComputation_Receiver"; final String provider = "MockComputation_Provider"; Message message; + MockComputationRunContext runContext; @BeforeEach void init() { @@ -171,12 +226,8 @@ void init() { .setHeader(HEADER_RECEIVER, receiver) .setHeader(HEADER_USER_ID, userId); message = builder.build(); - } - @Test - void testComputationImplementation() throws Exception { - // inits - final MockComputationRunContext runContext = new MockComputationRunContext(networkUuid, null, receiver, + runContext = new MockComputationRunContext(networkUuid, null, receiver, new ReportInfos(reportUuid, reporterId, COMPUTATION_TYPE), userId, provider, new MockComputationParameters()); runContext.setNetwork(network); resultContext = new MockComputationResultContext(resultUuid, runContext); @@ -184,10 +235,15 @@ void testComputationImplementation() throws Exception { when(networkStoreService.getNetwork(eq(networkUuid), any(PreloadingStrategy.class))) .thenReturn(network); when(network.getVariantManager()).thenReturn(variantManager); + } + + @Test + void testComputationImplementation() { + // inits + runContext.setComputationResWanted(ComputationResultWanted.SUCCESS); // execution / cleaning workerService.consumeRun().accept(message); - workerService.consumeCancel().accept(message); // test the course // TODO : comment vérifier qu'il n'y a plus rien à annuler (plus rien dans les futures) @@ -195,6 +251,18 @@ void testComputationImplementation() throws Exception { .sendResultMessage(resultUuid, receiver, userId, null); } - // TODO : faire un test publishFail ?? - // TODO : et une annulation ?? (avec un getCompletableFuture qui aurait un léger délai pour pouvoir annuler) + @Test + void testComputationFailed() { + // inits + runContext.setComputationResWanted(ComputationResultWanted.FAIL); + + // execution / cleaning + workerService.consumeRun().accept(message); + + // test the course + verify(notificationService, times(1)) + .publishFail(resultUuid, receiver, "java.lang.RuntimeException: Computation failed", userId, COMPUTATION_TYPE, null); + } + + // TODO : et une fonction qui teste l'annulation ?? (avec un getCompletableFuture SLOW_SUCCESS qui aurait un léger délai pour pouvoir annuler ?) } From 636b97662306fd6c410ec9643b292d30d34b379d Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 13 Jun 2024 16:55:04 +0200 Subject: [PATCH 09/18] restrict powsybl-network-store-client dependencies Signed-off-by: Mathieu DEHARBE --- pom.xml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 09aa9dd..32c253b 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 2024.1.0 3.1.2 4.0.3 - 1.11.0 + 1.12.0 @@ -136,11 +136,6 @@ spring-cloud-stream ${spring-cloud-stream.version} - - com.powsybl - powsybl-network-store-client - ${powsybl-network-store-client.version} - org.aspectj aspectjweaver @@ -166,5 +161,22 @@ micrometer-registry-prometheus runtime + + + + com.powsybl + powsybl-network-store-iidm-impl + ${powsybl-network-store-client.version} + + + com.powsybl + powsybl-network-store-client + ${powsybl-network-store-client.version} + + + com.powsybl + powsybl-network-store-model + ${powsybl-network-store-client.version} + From 85aa51af4b344929635386050d9a73c6e6f62d15 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Thu, 13 Jun 2024 17:40:17 +0200 Subject: [PATCH 10/18] clean useless functions Signed-off-by: Mathieu DEHARBE --- .../computation/service/AbstractWorkerService.java | 11 ----------- 1 file changed, 11 deletions(-) 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 index 68cc943..c4107d5 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java @@ -168,17 +168,6 @@ public Consumer> consumeCancel() { protected abstract void saveResult(Network network, AbstractResultContext resultContext, S result); - protected void sendResultMessage(AbstractResultContext resultContext, Map additionalHeaders) { - notificationService.sendResultMessage(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(), - resultContext.getRunContext().getUserId(), additionalHeaders); - - } - - protected void publishFail(AbstractResultContext resultContext, String message, Map additionalHeaders) { - notificationService.publishFail(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(), - message, resultContext.getRunContext().getUserId(), getComputationType(), additionalHeaders); - } - protected void sendResultMessage(AbstractResultContext resultContext, S ignoredResult) { notificationService.sendResultMessage(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(), resultContext.getRunContext().getUserId(), null); From 48c23e1a62c2cf277bdd50e7a12bd8c5295764b1 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Mon, 17 Jun 2024 14:34:40 +0200 Subject: [PATCH 11/18] post tristan review Signed-off-by: Mathieu DEHARBE --- pom.xml | 43 ++++----------- .../service/AbstractComputationObserver.java | 3 +- .../AbstractComputationRunContext.java | 4 +- .../service/AbstractComputationService.java | 5 +- .../service/AbstractResultContext.java | 22 ++++---- .../service/AbstractWorkerService.java | 54 +++++++++---------- .../computation/utils/MessageUtils.java | 7 +-- .../annotations/PostCompletionAdapter.java | 6 +-- .../PostCompletionAnnotationAspect.java | 17 ++---- .../annotations/PostCompletionException.java | 1 - .../{ => computation}/ComputationTest.java | 4 +- 11 files changed, 65 insertions(+), 101 deletions(-) rename src/test/java/com/powsybl/ws/commons/{ => computation}/ComputationTest.java (98%) diff --git a/pom.xml b/pom.xml index 32c253b..129ee5b 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,12 @@ spring-boot-starter-tomcat true + + org.springframework.cloud + spring-cloud-stream + ${spring-cloud-stream.version} + true + @@ -131,52 +137,21 @@ spring-boot-starter-test test - - org.springframework.cloud - spring-cloud-stream - ${spring-cloud-stream.version} - org.aspectj aspectjweaver - - - com.powsybl - powsybl-commons - - - com.powsybl - powsybl-computation - - - com.powsybl - powsybl-computation-local + true io.micrometer micrometer-core - - - io.micrometer - micrometer-registry-prometheus - runtime - - - - - com.powsybl - powsybl-network-store-iidm-impl - ${powsybl-network-store-client.version} + true com.powsybl powsybl-network-store-client ${powsybl-network-store-client.version} - - - com.powsybl - powsybl-network-store-model - ${powsybl-network-store-client.version} + true 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 index 6e30ae3..69c1991 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationObserver.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationObserver.java @@ -10,6 +10,7 @@ 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; @@ -18,7 +19,7 @@ * @param powsybl Result class specific to the computation * @param

powsybl and gridsuite parameters specifics to the computation */ -@Getter +@Getter(AccessLevel.PROTECTED) public abstract class AbstractComputationObserver { protected static final String OBSERVATION_PREFIX = "app.computation."; protected static final String PROVIDER_TAG_NAME = "provider"; 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 index 6962d4f..818255c 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationRunContext.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationRunContext.java @@ -26,8 +26,8 @@ public abstract class AbstractComputationRunContext

{ private final String receiver; private final ReportInfos reportInfos; private final String userId; - private String provider; - private P parameters; + private final String provider; + private final P parameters; private ReportNode reportNode; private Network network; 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 index b758e10..95575c0 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationService.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationService.java @@ -24,11 +24,10 @@ public abstract class AbstractComputationService * @param run context specific to a computation, including parameters */ -@Getter +@Data public abstract class AbstractResultContext> { protected static final String RESULT_UUID_HEADER = "resultUuid"; @@ -49,11 +51,13 @@ protected AbstractResultContext(UUID resultUuid, R runContext) { } public Message toMessage(ObjectMapper objectMapper) { - String parametersJson; - try { - parametersJson = objectMapper.writeValueAsString(runContext.getParameters()); - } catch (JsonProcessingException e) { - throw new UncheckedIOException(e); + String parametersJson = ""; + if (objectMapper != null) { + try { + parametersJson = objectMapper.writeValueAsString(runContext.getParameters()); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } } return MessageBuilder.withPayload(parametersJson) .setHeader(RESULT_UUID_HEADER, resultUuid.toString()) @@ -69,7 +73,7 @@ public Message toMessage(ObjectMapper objectMapper) { .build(); } - protected Map getSpecificMsgHeaders() { + 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 index c4107d5..e40baff 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractWorkerService.java @@ -34,12 +34,12 @@ /** * @author Mathieu Deharbe - * @param powsybl Result class specific to the computation - * @param Run context specific to a computation, including parameters + * @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 + * @param result service specific to the computation */ -public abstract class AbstractWorkerService, P, T extends AbstractComputationResultService> { +public abstract class AbstractWorkerService, P, S extends AbstractComputationResultService> { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractWorkerService.class); protected final Lock lockRunAndCancel = new ReentrantLock(); @@ -48,17 +48,17 @@ public abstract class AbstractWorkerService observer; - protected final Map> futures = new ConcurrentHashMap<>(); + protected final AbstractComputationObserver observer; + protected final Map> futures = new ConcurrentHashMap<>(); protected final Map cancelComputationRequests = new ConcurrentHashMap<>(); - protected final T resultService; + protected final S resultService; protected AbstractWorkerService(NetworkStoreService networkStoreService, NotificationService notificationService, ReportService reportService, - T resultService, + S resultService, ExecutionService executionService, - AbstractComputationObserver observer, + AbstractComputationObserver observer, ObjectMapper objectMapper) { this.networkStoreService = networkStoreService; this.notificationService = notificationService; @@ -100,7 +100,7 @@ private void cancelAsync(CancelContext cancelContext) { cancelComputationRequests.put(cancelContext.resultUuid(), cancelContext); // find the completableFuture associated with result uuid - CompletableFuture future = futures.get(cancelContext.resultUuid()); + CompletableFuture future = futures.get(cancelContext.resultUuid()); if (future != null) { future.cancel(true); // cancel computation in progress } @@ -110,22 +110,22 @@ private void cancelAsync(CancelContext cancelContext) { } } - protected abstract AbstractResultContext fromMessage(Message message); + protected abstract AbstractResultContext fromMessage(Message message); - protected boolean resultCanBeSaved(S result) { + protected boolean resultCanBeSaved(R result) { return result != null; } public Consumer> consumeRun() { return message -> { - AbstractResultContext resultContext = fromMessage(message); + AbstractResultContext resultContext = fromMessage(message); try { long startTime = System.nanoTime(); Network network = getNetwork(resultContext.getRunContext().getNetworkUuid(), resultContext.getRunContext().getVariantId()); resultContext.getRunContext().setNetwork(network); - S result = run(resultContext.getRunContext(), resultContext.getResultUuid()); + R result = run(resultContext.getRunContext(), resultContext.getResultUuid()); LOGGER.info("Just run in {}s", TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime)); @@ -159,21 +159,21 @@ public Consumer> consumeRun() { * @param resultContext The context of the computation * @param exception The exception to handle */ - protected void handleNonCancellationException(AbstractResultContext resultContext, Exception exception) { + protected void handleNonCancellationException(AbstractResultContext resultContext, Exception exception) { } public Consumer> consumeCancel() { return message -> cancelAsync(CancelContext.fromMessage(message)); } - protected abstract void saveResult(Network network, AbstractResultContext resultContext, S result); + protected abstract void saveResult(Network network, AbstractResultContext resultContext, R result); - protected void sendResultMessage(AbstractResultContext resultContext, S ignoredResult) { + 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) { + protected void publishFail(AbstractResultContext resultContext, String message) { notificationService.publishFail(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(), message, resultContext.getRunContext().getUserId(), getComputationType(), null); } @@ -182,11 +182,11 @@ protected void publishFail(AbstractResultContext resultContext, String messag * 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(R ignoredRunContext) { + protected void preRun(C ignoredRunContext) { LOGGER.info("Run {} computation...", getComputationType()); } - protected S run(R runContext, UUID resultUuid) throws Exception { + 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; @@ -204,8 +204,8 @@ protected S run(R runContext, UUID resultUuid) throws Exception { runContext.setReportNode(reportNode); preRun(runContext); - CompletableFuture future = runAsync(runContext, provider, resultUuid); - S result = future == null ? null : observer.observeRun("run", runContext, future::get); + CompletableFuture future = runAsync(runContext, provider, resultUuid); + R result = future == null ? null : observer.observeRun("run", runContext, future::get); postRun(runContext, rootReporter, result); return result; } @@ -216,14 +216,14 @@ protected S run(R runContext, UUID resultUuid) throws Exception { * @param rootReportNode root of the reporter tree * @param ignoredResult The result of the computation */ - protected void postRun(R runContext, AtomicReference rootReportNode, S ignoredResult) { + 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( - R runContext, + protected CompletableFuture runAsync( + C runContext, String provider, UUID resultUuid) { lockRunAndCancel.lock(); @@ -231,7 +231,7 @@ protected CompletableFuture runAsync( if (resultUuid != null && cancelComputationRequests.get(resultUuid) != null) { return null; } - CompletableFuture future = getCompletableFuture(runContext, provider, resultUuid); + CompletableFuture future = getCompletableFuture(runContext, provider, resultUuid); if (resultUuid != null) { futures.put(resultUuid, future); } @@ -243,5 +243,5 @@ protected CompletableFuture runAsync( protected abstract String getComputationType(); - protected abstract CompletableFuture getCompletableFuture(R runContext, String provider, UUID resultUuid); + protected abstract CompletableFuture getCompletableFuture(C runContext, String provider, UUID resultUuid); } 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 index 5f131de..a18903d 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/utils/MessageUtils.java +++ b/src/main/java/com/powsybl/ws/commons/computation/utils/MessageUtils.java @@ -8,6 +8,7 @@ import com.powsybl.commons.PowsyblException; import org.apache.commons.lang3.StringUtils; +import org.springframework.lang.Nullable; import org.springframework.messaging.MessageHeaders; /** @@ -32,11 +33,7 @@ public static String getNonNullHeader(MessageHeaders headers, String name) { * 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(String msg) { - if (msg == null) { - return null; - } - + 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/PostCompletionAdapter.java b/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionAdapter.java index c64d473..6f35353 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionAdapter.java +++ b/src/main/java/com/powsybl/ws/commons/computation/utils/annotations/PostCompletionAdapter.java @@ -32,10 +32,10 @@ public void execute(Runnable runnable) { } RUNNABLE.set(runnables); TransactionSynchronizationManager.registerSynchronization(this); - return; + } else { + // if transaction synchronisation is not active + runnable.run(); } - // if transaction synchronisation is not active - runnable.run(); } @Override 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 index 43912c5..5bc51ef 100644 --- 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 @@ -24,24 +24,13 @@ public class PostCompletionAnnotationAspect { @Around("@annotation(com.powsybl.ws.commons.computation.utils.annotations.PostCompletion)") public Object executePostCompletion(final ProceedingJoinPoint pjp) { - postCompletionAdapter.execute(new PjpAfterCompletionRunnable(pjp)); - return null; - } - - private static final class PjpAfterCompletionRunnable implements Runnable { - private final ProceedingJoinPoint pjp; - - public PjpAfterCompletionRunnable(ProceedingJoinPoint pjp) { - this.pjp = pjp; - } - - @Override - public void run() { + postCompletionAdapter.execute(() -> { 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 index ae61797..9d2918c 100644 --- 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 @@ -9,7 +9,6 @@ /** * @author Slimane Amar */ - public class PostCompletionException extends RuntimeException { public PostCompletionException(Throwable t) { super(t); diff --git a/src/test/java/com/powsybl/ws/commons/ComputationTest.java b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java similarity index 98% rename from src/test/java/com/powsybl/ws/commons/ComputationTest.java rename to src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java index f56567c..8425ec6 100644 --- a/src/test/java/com/powsybl/ws/commons/ComputationTest.java +++ b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java @@ -1,4 +1,4 @@ -package com.powsybl.ws.commons; +package com.powsybl.ws.commons.computation; import com.fasterxml.jackson.databind.ObjectMapper; import com.powsybl.iidm.network.Network; @@ -39,7 +39,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static com.powsybl.ws.commons.ComputationTest.MockComputationStatus.COMPLETED; +import static com.powsybl.ws.commons.computation.ComputationTest.MockComputationStatus.COMPLETED; 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; From 4e2cbe5529a38d61369e8a8734572760715426ec Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Mon, 17 Jun 2024 18:51:46 +0200 Subject: [PATCH 12/18] testComputationService + remove@Service from tests Signed-off-by: Mathieu DEHARBE --- .../commons/computation/ComputationTest.java | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java index 8425ec6..6cc8a60 100644 --- a/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java +++ b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java @@ -31,18 +31,21 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.stereotype.Service; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static com.powsybl.ws.commons.computation.ComputationTest.MockComputationStatus.COMPLETED; +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.Mockito.times; @@ -60,6 +63,8 @@ class ComputationTest implements WithAssertions { @Mock private ExecutionService executionService; @Mock + private UuidGeneratorService uuidGeneratorService; + @Mock private NotificationService notificationService; @Mock private ObjectMapper objectMapper; @@ -76,24 +81,31 @@ public enum MockComputationStatus { COMPLETED } - @Service public static class MockComputationResultService extends AbstractComputationResultService { + Map mockDBStatus = new HashMap<>(); + @Override - public void insertStatus(List resultUuids, MockComputationStatus status) { } + public void insertStatus(List resultUuids, MockComputationStatus status) { + resultUuids.forEach(uuid -> + mockDBStatus.put(uuid, status)); + } @Override - public void delete(UUID resultUuid) { } + public void delete(UUID resultUuid) { + mockDBStatus.remove(resultUuid); + } @Override - public void deleteAll() { } + public void deleteAll() { + mockDBStatus.clear(); + } @Override public MockComputationStatus findStatus(UUID resultUuid) { - return null; + return mockDBStatus.get(resultUuid); } } - @Service public static class MockComputationObserver extends AbstractComputationObserver { protected MockComputationObserver(@NonNull ObservationRegistry observationRegistry, @NonNull MeterRegistry meterRegistry) { super(observationRegistry, meterRegistry); @@ -127,7 +139,6 @@ protected MockComputationResultContext(UUID resultUuid, MockComputationRunContex } } - @Service public class MockComputationService extends AbstractComputationService { protected MockComputationService(NotificationService notificationService, MockComputationResultService resultService, ObjectMapper objectMapper, UuidGeneratorService uuidGeneratorService, String defaultProvider) { super(notificationService, resultService, objectMapper, uuidGeneratorService, defaultProvider); @@ -150,7 +161,6 @@ enum ComputationResultWanted { SLOW_SUCCESS } - @Service 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); @@ -192,12 +202,13 @@ protected CompletableFuture getCompletableFuture(MockComp } } - @Mock private MockComputationResultService resultService; @Mock private VariantManager variantManager; @Mock private MockComputationWorkerService workerService; + @Mock + 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"); @@ -211,6 +222,7 @@ protected CompletableFuture getCompletableFuture(MockComp @BeforeEach void init() { + resultService = new MockComputationResultService(); workerService = new MockComputationWorkerService( networkStoreService, notificationService, @@ -220,6 +232,8 @@ void init() { 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()) @@ -231,22 +245,24 @@ void init() { 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 testComputationImplementation() { + void testComputationSuccess() { // inits + initComputationExecution(); runContext.setComputationResWanted(ComputationResultWanted.SUCCESS); // execution / cleaning workerService.consumeRun().accept(message); // test the course - // TODO : comment vérifier qu'il n'y a plus rien à annuler (plus rien dans les futures) verify(notificationService, times(1)) .sendResultMessage(resultUuid, receiver, userId, null); } @@ -254,6 +270,7 @@ void testComputationImplementation() { @Test void testComputationFailed() { // inits + initComputationExecution(); runContext.setComputationResWanted(ComputationResultWanted.FAIL); // execution / cleaning @@ -264,5 +281,20 @@ void testComputationFailed() { .publishFail(resultUuid, receiver, "java.lang.RuntimeException: Computation failed", userId, COMPUTATION_TYPE, null); } - // TODO : et une fonction qui teste l'annulation ?? (avec un getCompletableFuture SLOW_SUCCESS qui aurait un léger délai pour pouvoir annuler ?) + @Test + void testComputationService() { + MockComputationStatus baseStatus = NOT_DONE; + computationService.setStatus(List.of(resultUuid), baseStatus); + assertEquals(baseStatus, computationService.getStatus(resultUuid)); + + computationService.stop(resultUuid, receiver); + + // test the course + /*Message cancelMessage = MessageBuilder.withPayload("") + .setHeader(HEADER_RESULT_UUID, resultUuid.toString()) + .setHeader(HEADER_RECEIVER, receiver) + .build(); + verify(notificationService, times(1)) + .sendCancelMessage(cancelMessage);*/ + } } From f291428f133af439efcc3378fcd40ef3192134a2 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Mon, 17 Jun 2024 19:43:21 +0200 Subject: [PATCH 13/18] testComputationCancelled Signed-off-by: Mathieu DEHARBE --- .../commons/computation/ComputationTest.java | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java index 6cc8a60..f551937 100644 --- a/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java +++ b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java @@ -12,6 +12,7 @@ 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; @@ -37,10 +38,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import static com.powsybl.ws.commons.computation.ComputationTest.MockComputationStatus.COMPLETED; 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; @@ -48,6 +46,7 @@ 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; @@ -63,7 +62,7 @@ class ComputationTest implements WithAssertions { @Mock private ExecutionService executionService; @Mock - private UuidGeneratorService uuidGeneratorService; + private UuidGeneratorService uuidGeneratorService = new UuidGeneratorService(); @Mock private NotificationService notificationService; @Mock @@ -157,8 +156,7 @@ public UUID runAndSaveResult(MockComputationRunContext runContext) { enum ComputationResultWanted { SUCCESS, - FAIL, - SLOW_SUCCESS + FAIL } public class MockComputationWorkerService extends AbstractWorkerService { @@ -186,15 +184,6 @@ protected CompletableFuture getCompletableFuture(MockComp case FAIL: completableFuture.completeExceptionally(new RuntimeException("Computation failed")); break; - case SLOW_SUCCESS: - ExecutorService executorService = Executors.newFixedThreadPool(1); - executorService.submit(() -> { - Thread.sleep(5000); - // TODO : try something like this : await().atMost(2, Duration.SECONDS).until(didTheThing()); // Compliant - completableFuture.complete(new MockComputationResult()); - return COMPLETED; - }); - break; case SUCCESS: return CompletableFuture.supplyAsync(MockComputationResult::new); } @@ -202,7 +191,6 @@ protected CompletableFuture getCompletableFuture(MockComp } } - private MockComputationResultService resultService; @Mock private VariantManager variantManager; @Mock @@ -222,7 +210,7 @@ protected CompletableFuture getCompletableFuture(MockComp @BeforeEach void init() { - resultService = new MockComputationResultService(); + MockComputationResultService resultService = new MockComputationResultService(); workerService = new MockComputationWorkerService( networkStoreService, notificationService, @@ -282,7 +270,7 @@ void testComputationFailed() { } @Test - void testComputationService() { + void testComputationCancelled() { MockComputationStatus baseStatus = NOT_DONE; computationService.setStatus(List.of(resultUuid), baseStatus); assertEquals(baseStatus, computationService.getStatus(resultUuid)); @@ -290,11 +278,16 @@ void testComputationService() { computationService.stop(resultUuid, receiver); // test the course - /*Message cancelMessage = MessageBuilder.withPayload("") + verify(notificationService, times(1)) + .sendCancelMessage(isA(Message.class)); + + // TODO : how to get the message from the previous call instead of creating this one ? + Message cancelMessage = MessageBuilder.withPayload("") .setHeader(HEADER_RESULT_UUID, resultUuid.toString()) .setHeader(HEADER_RECEIVER, receiver) .build(); - verify(notificationService, times(1)) - .sendCancelMessage(cancelMessage);*/ + CancelContext cancelContext = CancelContext.fromMessage(cancelMessage); + assertEquals(resultUuid, cancelContext.resultUuid()); + assertEquals(receiver, cancelContext.receiver()); } } From fdf16d48f9a9fe79f8ad4c677bc3727952095f4d Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Tue, 18 Jun 2024 11:48:59 +0200 Subject: [PATCH 14/18] ReportServiceTest Signed-off-by: Mathieu DEHARBE --- pom.xml | 12 ++ .../service/ReportServiceTest.java | 119 ++++++++++++++++++ .../SpringBootApplicationForTest.java | 3 +- 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/powsybl/ws/commons/computation/service/ReportServiceTest.java diff --git a/pom.xml b/pom.xml index 129ee5b..e12a657 100644 --- a/pom.xml +++ b/pom.xml @@ -117,6 +117,7 @@ provided + org.junit.vintage junit-vintage-engine @@ -137,6 +138,17 @@ spring-boot-starter-test test + + com.squareup.okhttp3 + okhttp + test + + + com.squareup.okhttp3 + mockwebserver + test + + org.aspectj aspectjweaver 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..c13f370 --- /dev/null +++ b/src/test/java/com/powsybl/ws/commons/computation/service/ReportServiceTest.java @@ -0,0 +1,119 @@ +/** + * 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.InjectableValues; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.commons.report.ReportNodeDeserializer; +import com.powsybl.commons.report.ReportNodeJsonModule; +import lombok.extern.slf4j.Slf4j; +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.jetbrains.annotations.NotNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.Objects; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +/** + * @author Mathieu Deharbe reportService.sendReport(REPORT_ERROR_UUID, reportNode)); + } + + @Test + public void testDeleteReport() { + reportService.deleteReport(REPORT_UUID, "MockReportType"); + assertThrows(RestClientException.class, () -> reportService.deleteReport(REPORT_ERROR_UUID, "MockReportType")); + } +} + diff --git a/src/test/java/com/powsybl/ws_common_spring_test/SpringBootApplicationForTest.java b/src/test/java/com/powsybl/ws_common_spring_test/SpringBootApplicationForTest.java index 6c880ca..7fbc5c6 100644 --- a/src/test/java/com/powsybl/ws_common_spring_test/SpringBootApplicationForTest.java +++ b/src/test/java/com/powsybl/ws_common_spring_test/SpringBootApplicationForTest.java @@ -1,5 +1,6 @@ package com.powsybl.ws_common_spring_test; +import com.powsybl.ws.commons.computation.config.JacksonConfig; import org.springframework.boot.autoconfigure.SpringBootApplication; /** @@ -9,6 +10,6 @@ @SuppressWarnings({ "java:S2187" //this isn't a class containing tests }) -@SpringBootApplication +@SpringBootApplication(scanBasePackageClasses = {JacksonConfig.class}) public class SpringBootApplicationForTest { } From 2e024ea93c23bbaa275473e9f0529c9be57fa5c6 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Tue, 18 Jun 2024 12:45:37 +0200 Subject: [PATCH 15/18] notificationService implementation instead of Mock Signed-off-by: Mathieu DEHARBE --- .../commons/computation/ComputationTest.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java index f551937..825046c 100644 --- a/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java +++ b/src/test/java/com/powsybl/ws/commons/computation/ComputationTest.java @@ -30,6 +30,7 @@ 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; @@ -56,14 +57,15 @@ 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 ExecutionService executionService; - @Mock - private UuidGeneratorService uuidGeneratorService = new UuidGeneratorService(); - @Mock + private StreamBridge publisher; private NotificationService notificationService; @Mock private ObjectMapper objectMapper; @@ -182,7 +184,7 @@ protected CompletableFuture getCompletableFuture(MockComp final CompletableFuture completableFuture = new CompletableFuture<>(); switch (runContext.getComputationResWanted()) { case FAIL: - completableFuture.completeExceptionally(new RuntimeException("Computation failed")); + completableFuture.completeExceptionally(new RuntimeException("Computation failed but with an artificially longer messaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaage")); break; case SUCCESS: return CompletableFuture.supplyAsync(MockComputationResult::new); @@ -191,11 +193,7 @@ protected CompletableFuture getCompletableFuture(MockComp } } - @Mock - private VariantManager variantManager; - @Mock private MockComputationWorkerService workerService; - @Mock private MockComputationService computationService; private MockComputationResultContext resultContext; final UUID networkUuid = UUID.fromString("11111111-1111-1111-1111-111111111111"); @@ -211,6 +209,7 @@ protected CompletableFuture getCompletableFuture(MockComp @BeforeEach void init() { MockComputationResultService resultService = new MockComputationResultService(); + notificationService = new NotificationService(publisher); workerService = new MockComputationWorkerService( networkStoreService, notificationService, @@ -251,8 +250,7 @@ void testComputationSuccess() { workerService.consumeRun().accept(message); // test the course - verify(notificationService, times(1)) - .sendResultMessage(resultUuid, receiver, userId, null); + verify(notificationService.getPublisher(), times(1)).send(eq("publishResult-out-0"), isA(Message.class)); } @Test @@ -265,8 +263,7 @@ void testComputationFailed() { workerService.consumeRun().accept(message); // test the course - verify(notificationService, times(1)) - .publishFail(resultUuid, receiver, "java.lang.RuntimeException: Computation failed", userId, COMPUTATION_TYPE, null); + verify(notificationService.getPublisher(), times(1)).send(eq("publishFailed-out-0"), isA(Message.class)); } @Test @@ -278,10 +275,8 @@ void testComputationCancelled() { computationService.stop(resultUuid, receiver); // test the course - verify(notificationService, times(1)) - .sendCancelMessage(isA(Message.class)); + verify(notificationService.getPublisher(), times(1)).send(eq("publishCancel-out-0"), isA(Message.class)); - // TODO : how to get the message from the previous call instead of creating this one ? Message cancelMessage = MessageBuilder.withPayload("") .setHeader(HEADER_RESULT_UUID, resultUuid.toString()) .setHeader(HEADER_RECEIVER, receiver) From 1f74215b12d0ec450ad86a9796fa2b025693ac54 Mon Sep 17 00:00:00 2001 From: Mathieu DEHARBE Date: Tue, 18 Jun 2024 14:35:22 +0200 Subject: [PATCH 16/18] =?UTF-8?q?accessibilit=C3=A9=20in=20AbstractComputa?= =?UTF-8?q?tionRunContext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mathieu DEHARBE --- .../computation/service/AbstractComputationRunContext.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 818255c..6962d4f 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationRunContext.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractComputationRunContext.java @@ -26,8 +26,8 @@ public abstract class AbstractComputationRunContext

{ private final String receiver; private final ReportInfos reportInfos; private final String userId; - private final String provider; - private final P parameters; + private String provider; + private P parameters; private ReportNode reportNode; private Network network; From 4b94c667b162dbd49845fb73bc395d82bd0594ba Mon Sep 17 00:00:00 2001 From: Tristan Chuine Date: Wed, 19 Jun 2024 12:31:34 +0200 Subject: [PATCH 17/18] Update test --- pom.xml | 16 +-- .../computation/ComputationConfig.java | 17 +++ .../computation/config/JacksonConfig.java | 14 -- .../service/ReportServiceTest.java | 132 +++++++----------- .../SpringBootApplicationForTest.java | 3 +- 5 files changed, 75 insertions(+), 107 deletions(-) create mode 100644 src/main/java/com/powsybl/ws/commons/computation/ComputationConfig.java delete mode 100644 src/main/java/com/powsybl/ws/commons/computation/config/JacksonConfig.java diff --git a/pom.xml b/pom.xml index e12a657..dd398be 100644 --- a/pom.xml +++ b/pom.xml @@ -132,22 +132,18 @@ org.springframework.boot spring-boot-starter-web test + + + ch.qos.logback + logback-classic + + org.springframework.boot spring-boot-starter-test test - - com.squareup.okhttp3 - okhttp - test - - - com.squareup.okhttp3 - mockwebserver - test - org.aspectj 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..b2685f0 --- /dev/null +++ b/src/main/java/com/powsybl/ws/commons/computation/ComputationConfig.java @@ -0,0 +1,17 @@ +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; + +@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))); + } +} diff --git a/src/main/java/com/powsybl/ws/commons/computation/config/JacksonConfig.java b/src/main/java/com/powsybl/ws/commons/computation/config/JacksonConfig.java deleted file mode 100644 index 6009c8d..0000000 --- a/src/main/java/com/powsybl/ws/commons/computation/config/JacksonConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.powsybl.ws.commons.computation.config; - -import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import com.powsybl.commons.report.ReportNodeJsonModule; - -@Configuration -public class JacksonConfig { - @Bean - public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { - return builder -> builder.build().registerModule(new ReportNodeJsonModule()); - } -} 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 index c13f370..57c6556 100644 --- a/src/test/java/com/powsybl/ws/commons/computation/service/ReportServiceTest.java +++ b/src/test/java/com/powsybl/ws/commons/computation/service/ReportServiceTest.java @@ -7,113 +7,83 @@ package com.powsybl.ws.commons.computation.service; -import com.fasterxml.jackson.databind.InjectableValues; -import com.fasterxml.jackson.databind.SerializationFeature; import com.powsybl.commons.report.ReportNode; -import com.powsybl.commons.report.ReportNodeDeserializer; -import com.powsybl.commons.report.ReportNodeJsonModule; -import lombok.extern.slf4j.Slf4j; -import okhttp3.HttpUrl; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.jetbrains.annotations.NotNull; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.http.HttpStatus; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.test.context.junit4.SpringRunner; +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 org.springframework.web.client.RestTemplate; -import java.io.IOException; -import java.util.Objects; import java.util.UUID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; +import static org.assertj.core.api.Assertions.*; /** * @author Mathieu Deharbe reportService.sendReport(REPORT_UUID, reportNode)); } - private String initMockWebServer() throws IOException { - server = new MockWebServer(); - server.start(); - - final Dispatcher dispatcher = new Dispatcher() { - @NotNull - @Override - public MockResponse dispatch(RecordedRequest request) { - String requestPath = Objects.requireNonNull(request.getPath()); - if (requestPath.equals(String.format("/v1/reports/%s", REPORT_UUID))) { - assertEquals(REPORT_JSON, request.getBody().readUtf8()); - return new MockResponse().setResponseCode(HttpStatus.OK.value()); - } else if (requestPath.equals(String.format("/v1/reports/%s?reportTypeFilter=MockReportType&errorOnReportNotFound=false", REPORT_UUID))) { - assertEquals("", request.getBody().readUtf8()); - return new MockResponse().setResponseCode(HttpStatus.OK.value()); - } else if (requestPath.equals(String.format("/v1/reports/%s", REPORT_ERROR_UUID))) { - return new MockResponse().setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); - } else { - return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.value()).setBody("Path not supported: " + request.getPath()); - } - } - }; - - server.setDispatcher(dispatcher); - - // Ask the server for its URL. You'll need this to make HTTP requests. - HttpUrl baseHttpUrl = server.url(""); - return baseHttpUrl.toString().substring(0, baseHttpUrl.toString().length() - 1); + @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 - public void testSendReport() { - ReportNode reportNode = ReportNode.newRootReportNode().withMessageTemplate("test", "a test").build(); - reportService.sendReport(REPORT_UUID, reportNode); - assertThrows(RestClientException.class, () -> reportService.sendReport(REPORT_ERROR_UUID, reportNode)); + 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 - public void testDeleteReport() { - reportService.deleteReport(REPORT_UUID, "MockReportType"); - assertThrows(RestClientException.class, () -> reportService.deleteReport(REPORT_ERROR_UUID, "MockReportType")); + 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); } } diff --git a/src/test/java/com/powsybl/ws_common_spring_test/SpringBootApplicationForTest.java b/src/test/java/com/powsybl/ws_common_spring_test/SpringBootApplicationForTest.java index 7fbc5c6..6c880ca 100644 --- a/src/test/java/com/powsybl/ws_common_spring_test/SpringBootApplicationForTest.java +++ b/src/test/java/com/powsybl/ws_common_spring_test/SpringBootApplicationForTest.java @@ -1,6 +1,5 @@ package com.powsybl.ws_common_spring_test; -import com.powsybl.ws.commons.computation.config.JacksonConfig; import org.springframework.boot.autoconfigure.SpringBootApplication; /** @@ -10,6 +9,6 @@ @SuppressWarnings({ "java:S2187" //this isn't a class containing tests }) -@SpringBootApplication(scanBasePackageClasses = {JacksonConfig.class}) +@SpringBootApplication public class SpringBootApplicationForTest { } From e9701120f2ffa65d66ba646483c110db4ce80dc3 Mon Sep 17 00:00:00 2001 From: Tristan Chuine Date: Thu, 20 Jun 2024 14:08:54 +0200 Subject: [PATCH 18/18] Mockable time in messages Signed-off-by: Tristan Chuine --- .../ws/commons/computation/ComputationConfig.java | 7 +++++++ .../computation/service/AbstractResultContext.java | 11 +++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/powsybl/ws/commons/computation/ComputationConfig.java b/src/main/java/com/powsybl/ws/commons/computation/ComputationConfig.java index b2685f0..fc5ddc7 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/ComputationConfig.java +++ b/src/main/java/com/powsybl/ws/commons/computation/ComputationConfig.java @@ -7,6 +7,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.time.Clock; + @Configuration public class ComputationConfig { @Bean @@ -14,4 +16,9 @@ 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/service/AbstractResultContext.java b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractResultContext.java index 80280dc..dfed304 100644 --- a/src/main/java/com/powsybl/ws/commons/computation/service/AbstractResultContext.java +++ b/src/main/java/com/powsybl/ws/commons/computation/service/AbstractResultContext.java @@ -10,9 +10,12 @@ 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; @@ -50,7 +53,7 @@ protected AbstractResultContext(UUID resultUuid, R runContext) { this.runContext = Objects.requireNonNull(runContext); } - public Message toMessage(ObjectMapper objectMapper) { + public Message toMessage(@Nullable final Clock clock, @Nullable final ObjectMapper objectMapper) { String parametersJson = ""; if (objectMapper != null) { try { @@ -59,7 +62,11 @@ public Message toMessage(ObjectMapper objectMapper) { throw new UncheckedIOException(e); } } - return MessageBuilder.withPayload(parametersJson) + 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())