diff --git a/src/main/java/org/gridsuite/study/server/notification/NotificationService.java b/src/main/java/org/gridsuite/study/server/notification/NotificationService.java index cabfda537c..1d218b53f0 100644 --- a/src/main/java/org/gridsuite/study/server/notification/NotificationService.java +++ b/src/main/java/org/gridsuite/study/server/notification/NotificationService.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.gridsuite.study.server.dto.ComputationType; +import org.gridsuite.study.server.dto.QuotaType; import org.gridsuite.study.server.dto.RootNetworkIndexationStatus; import org.gridsuite.study.server.networkmodificationtree.dto.InsertMode; import org.gridsuite.study.server.nodeactivity.NodeActivityInfos; @@ -43,6 +44,7 @@ public class NotificationService { public static final String HEADER_ROOT_NETWORKS_UUIDS = "rootNetworksUuids"; public static final String HEADER_STUDY_UUID = "studyUuid"; public static final String HEADER_UPDATE_TYPE = "updateType"; + public static final String HEADER_QUOTA_TYPE = "quotaType"; public static final String HEADER_COMPUTATION_TYPE = "computationType"; public static final String HEADER_COMPUTATION_SUBTYPE = "computationSubtype"; public static final String HEADER_RESULT_UUID = "resultUuid"; @@ -110,6 +112,7 @@ public class NotificationService { public static final String UPDATE_TYPE_COMPUTATION_PARAMETERS = "computationParametersUpdated"; public static final String UPDATE_COMPUTATION_RESULT_COLUMN_FILTER = "computationResultColumnFilterUpdated"; public static final String UPDATE_COMPUTATION_RESULT_GLOBAL_FILTER = "computationResultGlobalFilterUpdated"; + public static final String UPDATE_QUOTA_USAGE = "updateUserQuotaUsage"; public static final String UPDATE_NETWORK_VISUALIZATION_PARAMETERS = "networkVisualizationParametersUpdated"; public static final String UPDATE_NODE_ACTIVITIES = "nodeActivitiesUpdated"; @@ -561,4 +564,14 @@ public void emitNetworkExportFinished(UUID studyUuid, UUID exportUuid, @NonNull .setHeader(HEADER_ERROR, error) ); } + + public void emitQuotaChange(String userId, QuotaType quotaType) { + Message message = MessageBuilder.withPayload("") + .setHeader(HEADER_USER_ID, userId) + .setHeader(HEADER_QUOTA_TYPE, quotaType) + .build(); + + MESSAGE_OUTPUT_LOGGER.debug(MESSAGE_LOG, message); + updatePublisher.send("publishQuotaUpdate-out-0", message); + } } diff --git a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java index a3f20f9bab..d1096bacf7 100644 --- a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java +++ b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java @@ -431,7 +431,7 @@ public void consumeCalculationFailed(Message msg, ComputationType comput // free quota if (userId != null && resultUuid != null) { - userAdminService.endOperationWithQuota(userId, QuotaType.mapFromComputationType(computationType), resultUuid); + handleQuotaEnd(computationType, userId, resultUuid); } // send notification for failed computation @@ -441,6 +441,12 @@ public void consumeCalculationFailed(Message msg, ComputationType comput } } + private void handleQuotaEnd(ComputationType computationType, String userId, UUID resultUuid) { + QuotaType quotaType = QuotaType.mapFromComputationType(computationType); + userAdminService.endOperationWithQuota(userId, quotaType, resultUuid); + notificationService.emitQuotaChange(userId, quotaType); + } + public void consumeCalculationStopped(Message msg, ComputationType computationType) { String receiver = msg.getHeaders().get(HEADER_RECEIVER, String.class); if (!Strings.isBlank(receiver)) { @@ -460,7 +466,7 @@ public void consumeCalculationStopped(Message msg, ComputationType compu String userId = msg.getHeaders().get(HEADER_USER_ID, String.class); if (resultId != null && userId != null) { UUID resultUuid = UUID.fromString(resultId); - userAdminService.endOperationWithQuota(userId, QuotaType.mapFromComputationType(computationType), resultUuid); + handleQuotaEnd(computationType, userId, resultUuid); } LOGGER.info("{} stopped for node '{}'", computationType.getLabel(), receiverObj.getNodeUuid()); @@ -544,7 +550,7 @@ public void consumeCalculationResult(Message msg, ComputationType comput // free quota String userId = msg.getHeaders().get(HEADER_USER_ID, String.class); if (userId != null) { - userAdminService.endOperationWithQuota(userId, QuotaType.mapFromComputationType(computationType), resultUuid); + handleQuotaEnd(computationType, userId, resultUuid); } // send notifications diff --git a/src/main/java/org/gridsuite/study/server/service/common/AbstractComputationService.java b/src/main/java/org/gridsuite/study/server/service/common/AbstractComputationService.java index 296ff7dc3e..606fe3c787 100644 --- a/src/main/java/org/gridsuite/study/server/service/common/AbstractComputationService.java +++ b/src/main/java/org/gridsuite/study/server/service/common/AbstractComputationService.java @@ -8,6 +8,7 @@ package org.gridsuite.study.server.service.common; import org.gridsuite.study.server.dto.ComputationType; +import org.gridsuite.study.server.dto.QuotaType; import org.gridsuite.study.server.dto.UserProfileInfos; import org.gridsuite.study.server.error.StudyException; import org.gridsuite.study.server.notification.NotificationService; @@ -121,4 +122,10 @@ protected void emitComputationParametersChanged(UUID studyUuid, String userId, protected void updateComputationResultUuid(UUID nodeUuid, UUID rootNetworkUuid, UUID computationResultUuid, ComputationType computationType) { rootNetworkNodeInfoService.updateComputationResultUuid(nodeUuid, rootNetworkUuid, computationResultUuid, computationType); } + + protected void handleQuotaStart(String userId, UUID result, ComputationType computationType) { + QuotaType quotaType = QuotaType.mapFromComputationType(computationType); + userAdminService.startOperationWithQuota(userId, quotaType, result); + notificationService.emitQuotaChange(userId, quotaType); + } } diff --git a/src/main/java/org/gridsuite/study/server/service/dynamicmargincalculation/DynamicMarginCalculationService.java b/src/main/java/org/gridsuite/study/server/service/dynamicmargincalculation/DynamicMarginCalculationService.java index d88d985af3..f0897d3cb0 100644 --- a/src/main/java/org/gridsuite/study/server/service/dynamicmargincalculation/DynamicMarginCalculationService.java +++ b/src/main/java/org/gridsuite/study/server/service/dynamicmargincalculation/DynamicMarginCalculationService.java @@ -8,7 +8,6 @@ package org.gridsuite.study.server.service.dynamicmargincalculation; import lombok.NonNull; -import org.gridsuite.study.server.dto.QuotaType; import org.gridsuite.study.server.dto.UserProfileInfos; import org.gridsuite.study.server.error.StudyException; import org.gridsuite.study.server.notification.NotificationService; @@ -88,7 +87,7 @@ public UUID runDynamicMarginCalculation(@NonNull UUID studyUuid, @NonNull UUID n UUID result = handleDynamicMarginCalculationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(DYNAMIC_MARGIN_CALCULATION), result); + handleQuotaStart(userId, result, DYNAMIC_MARGIN_CALCULATION); return result; } diff --git a/src/main/java/org/gridsuite/study/server/service/dynamicsecurityanalysis/DynamicSecurityAnalysisService.java b/src/main/java/org/gridsuite/study/server/service/dynamicsecurityanalysis/DynamicSecurityAnalysisService.java index fa864bfe57..f5e2871df3 100644 --- a/src/main/java/org/gridsuite/study/server/service/dynamicsecurityanalysis/DynamicSecurityAnalysisService.java +++ b/src/main/java/org/gridsuite/study/server/service/dynamicsecurityanalysis/DynamicSecurityAnalysisService.java @@ -8,7 +8,6 @@ package org.gridsuite.study.server.service.dynamicsecurityanalysis; import lombok.NonNull; -import org.gridsuite.study.server.dto.QuotaType; import org.gridsuite.study.server.dto.UserProfileInfos; import org.gridsuite.study.server.dto.dynamicsimulation.DynamicSimulationStatus; import org.gridsuite.study.server.error.StudyException; @@ -90,7 +89,7 @@ public UUID runDynamicSecurityAnalysis(@NonNull UUID studyUuid, @NonNull UUID no UUID result = handleDynamicSecurityAnalysisRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(DYNAMIC_SECURITY_ANALYSIS), result); + handleQuotaStart(userId, result, DYNAMIC_SECURITY_ANALYSIS); return result; } diff --git a/src/main/java/org/gridsuite/study/server/service/dynamicsimulation/DynamicSimulationService.java b/src/main/java/org/gridsuite/study/server/service/dynamicsimulation/DynamicSimulationService.java index 300fe0dc18..2cacf50a2c 100644 --- a/src/main/java/org/gridsuite/study/server/service/dynamicsimulation/DynamicSimulationService.java +++ b/src/main/java/org/gridsuite/study/server/service/dynamicsimulation/DynamicSimulationService.java @@ -7,7 +7,6 @@ package org.gridsuite.study.server.service.dynamicsimulation; import lombok.NonNull; -import org.gridsuite.study.server.dto.QuotaType; import org.gridsuite.study.server.dto.UserProfileInfos; import org.gridsuite.study.server.dto.dynamicsimulation.event.EventInfos; import org.gridsuite.study.server.error.StudyException; @@ -94,7 +93,7 @@ public UUID runDynamicSimulation(@NonNull UUID studyUuid, @NonNull UUID nodeUuid UUID result = handleDynamicSimulationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(DYNAMIC_SIMULATION), result); + handleQuotaStart(userId, result, DYNAMIC_SIMULATION); return result; } diff --git a/src/main/java/org/gridsuite/study/server/service/loadflow/LoadFlowService.java b/src/main/java/org/gridsuite/study/server/service/loadflow/LoadFlowService.java index 1b4626703a..6a855eb859 100644 --- a/src/main/java/org/gridsuite/study/server/service/loadflow/LoadFlowService.java +++ b/src/main/java/org/gridsuite/study/server/service/loadflow/LoadFlowService.java @@ -244,7 +244,7 @@ private void handleLoadflowRequest(StudyEntity studyEntity, UUID nodeUuid, UUID new LoadFlowRestService.ParametersInfos(lfParametersUuid, withRatioTapChangers, isSecurityNode), lfReportUuid, userId); rootNetworkNodeInfoService.updateLoadflowResultUuid(nodeUuid, rootNetworkUuid, result, withRatioTapChangers); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(LOAD_FLOW), result); + handleQuotaStart(userId, result, LOAD_FLOW); notificationService.emitStudyChanged(studyEntity.getId(), nodeUuid, rootNetworkUuid, LOAD_FLOW.getUpdateStatusType()); notificationService.emitElementUpdated(studyEntity.getId(), userId); } diff --git a/src/main/java/org/gridsuite/study/server/service/pccmin/PccMinService.java b/src/main/java/org/gridsuite/study/server/service/pccmin/PccMinService.java index 0cd1273bc6..54557d3ad7 100644 --- a/src/main/java/org/gridsuite/study/server/service/pccmin/PccMinService.java +++ b/src/main/java/org/gridsuite/study/server/service/pccmin/PccMinService.java @@ -70,7 +70,7 @@ public UUID runPccMin(@NonNull UUID studyUuid, @NonNull UUID nodeUuid, @NonNull UUID result = handlePccMinRequest(studyEntity, nodeUuid, rootNetworkUuid, userId); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(PCC_MIN), result); + handleQuotaStart(userId, result, PCC_MIN); return result; } diff --git a/src/main/java/org/gridsuite/study/server/service/securityanalysis/SecurityAnalysisService.java b/src/main/java/org/gridsuite/study/server/service/securityanalysis/SecurityAnalysisService.java index f976787838..8d69f48f97 100644 --- a/src/main/java/org/gridsuite/study/server/service/securityanalysis/SecurityAnalysisService.java +++ b/src/main/java/org/gridsuite/study/server/service/securityanalysis/SecurityAnalysisService.java @@ -66,7 +66,7 @@ public UUID runSecurityAnalysis(@NonNull UUID studyUuid, @NonNull UUID nodeUuid, UUID result = handleSecurityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(SECURITY_ANALYSIS), result); + handleQuotaStart(userId, result, SECURITY_ANALYSIS); return result; } diff --git a/src/main/java/org/gridsuite/study/server/service/sensitivityanalysis/SensitivityAnalysisService.java b/src/main/java/org/gridsuite/study/server/service/sensitivityanalysis/SensitivityAnalysisService.java index 424646cd6d..9a391c1fc9 100644 --- a/src/main/java/org/gridsuite/study/server/service/sensitivityanalysis/SensitivityAnalysisService.java +++ b/src/main/java/org/gridsuite/study/server/service/sensitivityanalysis/SensitivityAnalysisService.java @@ -7,7 +7,6 @@ package org.gridsuite.study.server.service.sensitivityanalysis; import lombok.NonNull; -import org.gridsuite.study.server.dto.QuotaType; import org.gridsuite.study.server.dto.UserProfileInfos; import org.gridsuite.study.server.notification.NotificationService; import org.gridsuite.study.server.repository.StudyEntity; @@ -81,7 +80,7 @@ public UUID runSensitivityAnalysis(@NonNull UUID studyUuid, @NonNull UUID nodeUu UUID result = handleSensitivityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(SENSITIVITY_ANALYSIS), result); + handleQuotaStart(userId, result, SENSITIVITY_ANALYSIS); return result; } diff --git a/src/main/java/org/gridsuite/study/server/service/shortcircuit/ShortCircuitService.java b/src/main/java/org/gridsuite/study/server/service/shortcircuit/ShortCircuitService.java index b4c3191f74..3db6915da2 100644 --- a/src/main/java/org/gridsuite/study/server/service/shortcircuit/ShortCircuitService.java +++ b/src/main/java/org/gridsuite/study/server/service/shortcircuit/ShortCircuitService.java @@ -72,7 +72,7 @@ public UUID runShortCircuit(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUuid, UUID result = handleShortCircuitRequest(studyEntity, nodeUuid, rootNetworkUuid, busId, debug, userId); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(SHORT_CIRCUIT), result); + handleQuotaStart(userId, result, SHORT_CIRCUIT); return result; } diff --git a/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationService.java b/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationService.java index 4a7c024451..f87a5a39ec 100644 --- a/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationService.java +++ b/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationService.java @@ -57,7 +57,7 @@ public UUID runStateEstimation(@NonNull UUID studyUuid, @NonNull UUID nodeUuid, UUID result = handleStateEstimationRequest(studyEntity, nodeUuid, rootNetworkUuid, userId, debug); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(STATE_ESTIMATION), result); + handleQuotaStart(userId, result, STATE_ESTIMATION); return result; } diff --git a/src/main/java/org/gridsuite/study/server/service/voltageinit/VoltageInitService.java b/src/main/java/org/gridsuite/study/server/service/voltageinit/VoltageInitService.java index ab64f7cede..72597704e2 100644 --- a/src/main/java/org/gridsuite/study/server/service/voltageinit/VoltageInitService.java +++ b/src/main/java/org/gridsuite/study/server/service/voltageinit/VoltageInitService.java @@ -6,7 +6,6 @@ */ package org.gridsuite.study.server.service.voltageinit; -import org.gridsuite.study.server.dto.QuotaType; import org.gridsuite.study.server.dto.ReportInfos; import org.gridsuite.study.server.dto.UserProfileInfos; import org.gridsuite.study.server.dto.VariantInfos; @@ -75,7 +74,7 @@ public UUID runVoltageInit(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUuid, UUID result = handleVoltageInitRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); - userAdminService.startOperationWithQuota(userId, QuotaType.mapFromComputationType(VOLTAGE_INITIALIZATION), result); + handleQuotaStart(userId, result, VOLTAGE_INITIALIZATION); return result; } diff --git a/src/main/resources/config/application.yaml b/src/main/resources/config/application.yaml index 0ea863bac6..6dbbdc91f2 100644 --- a/src/main/resources/config/application.yaml +++ b/src/main/resources/config/application.yaml @@ -22,6 +22,8 @@ spring: destination: ${powsybl-ws.rabbitmq.destination.prefix:}study.update publishElementUpdate-out-0: destination: ${powsybl-ws.rabbitmq.destination.prefix:}element.update + publishQuotaUpdate-out-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}quota.update consumeSaResult-in-0: destination: ${powsybl-ws.rabbitmq.destination.prefix:}sa.result group: studySaResultGroup diff --git a/src/test/java/org/gridsuite/study/server/loadflow/LoadFLowUnitTest.java b/src/test/java/org/gridsuite/study/server/loadflow/LoadFLowUnitTest.java index f9fb1dcf8b..cf6440076f 100644 --- a/src/test/java/org/gridsuite/study/server/loadflow/LoadFLowUnitTest.java +++ b/src/test/java/org/gridsuite/study/server/loadflow/LoadFLowUnitTest.java @@ -11,6 +11,7 @@ import org.gridsuite.study.server.dto.ComputationType; import org.gridsuite.study.server.dto.InvalidateNodeInfos; import org.gridsuite.study.server.dto.InvalidateNodeTreeParameters; +import org.gridsuite.study.server.dto.QuotaType; import org.gridsuite.study.server.dto.workflow.RerunLoadFlowInfos; import org.gridsuite.study.server.networkmodificationtree.dto.BuildStatus; import org.gridsuite.study.server.networkmodificationtree.entities.NodeBuildStatusEmbeddable; @@ -139,6 +140,26 @@ void testRunLoadFlowWithExistingResult() { verify(studyService, times(1)).assertCanRunOnConstructionNode(eq(studyUuid), eq(nodeUuid), any(), any()); } + @Test + void testSendLoadflowRequestStartsQuotaAndEmitsQuotaChange() { + StudyEntity studyEntity = new StudyEntity(); + studyEntity.setId(studyUuid); + + when(studyRepository.findById(studyUuid)).thenReturn(Optional.of(studyEntity)); + doReturn(false).when(networkModificationTreeService).isSecurityNode(nodeUuid); + doReturn(Map.of()).when(networkModificationTreeService).getComputationReports(nodeUuid, rootNetworkUuid); + doReturn(variantId).when(networkModificationTreeService).getVariantId(nodeUuid, rootNetworkUuid); + when(rootNetworkService.getNetworkUuid(rootNetworkUuid)).thenReturn(networkUuid); + when(loadFlowRestService.runLoadFlow(any(), any(), any(), any(), any(), anyString())).thenReturn(loadflowResultUuid); + + loadFlowService.sendLoadflowRequest(studyUuid, nodeUuid, rootNetworkUuid, loadflowResultUuid, false, userId); + + verify(userAdminService, times(1)).startOperationWithQuota(userId, QuotaType.LOAD_FLOW, loadflowResultUuid); + verify(notificationService, times(1)).emitQuotaChange(userId, QuotaType.LOAD_FLOW); + verify(notificationService, times(1)).emitStudyChanged(eq(studyUuid), eq(nodeUuid), eq(rootNetworkUuid), anyString()); + verify(notificationService, times(1)).emitElementUpdated(studyUuid, userId); + } + @Test void testRerunLoadFlow() { testRerunLoadFlow(false, true); diff --git a/src/test/java/org/gridsuite/study/server/notification/NotificationServiceTest.java b/src/test/java/org/gridsuite/study/server/notification/NotificationServiceTest.java new file mode 100644 index 0000000000..8fdb62811f --- /dev/null +++ b/src/test/java/org/gridsuite/study/server/notification/NotificationServiceTest.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2026, 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 org.gridsuite.study.server.notification; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.gridsuite.study.server.dto.QuotaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.messaging.Message; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +/** + * @author Ghiles Abdellah {@literal } + * + * Unit tests for {@link NotificationService}, focusing on the quota change notification + * introduced alongside {@link QuotaType}. + */ +@ExtendWith(MockitoExtension.class) +class NotificationServiceTest { + private static final String USER_ID = "userId"; + + @Mock + private StreamBridge updatePublisher; + + private NotificationService notificationService; + + @BeforeEach + void setUp() { + notificationService = new NotificationService(updatePublisher, new ObjectMapper()); + } + + @Test + void testEmitQuotaChange() { + notificationService.emitQuotaChange(USER_ID, QuotaType.SHORT_CIRCUIT); + + ArgumentCaptor> messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(updatePublisher).send(eq("publishQuotaUpdate-out-0"), messageCaptor.capture()); + + Message message = messageCaptor.getValue(); + assertThat(message.getHeaders().get(NotificationService.HEADER_USER_ID)).isEqualTo(USER_ID); + assertThat(message.getHeaders().get(NotificationService.HEADER_QUOTA_TYPE)).isEqualTo(QuotaType.SHORT_CIRCUIT); + } +}