Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
484 changes: 252 additions & 232 deletions src/main/java/org/gridsuite/study/server/controller/StudyController.java

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions src/main/java/org/gridsuite/study/server/dto/OperationType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* 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.dto;

/**
* @author Ghiles Abdellah {@literal <ghiles.abdellah at rte-france.com>}
*/
public enum OperationType {
CASES,
BUILD,
LOAD_FLOW,
SECURITY,
SENSITIVITY,
SHORT_CIRCUIT,
VOLTAGE_INIT,
PCC_MIN,
STATE_ESTIMATION,
BALANCE_ADJUSTEMENT,
DYNAMIC_SIMULATION,
DYNAMIC_SECURITY,
DYNAMIC_MARGIN;

public static OperationType mapFromComputationType(ComputationType computationType) {
return switch (computationType) {
case LOAD_FLOW -> OperationType.LOAD_FLOW;
case SECURITY_ANALYSIS -> OperationType.SECURITY;
case SENSITIVITY_ANALYSIS -> OperationType.SENSITIVITY;
case VOLTAGE_INITIALIZATION -> OperationType.VOLTAGE_INIT;
case DYNAMIC_SIMULATION -> OperationType.DYNAMIC_SIMULATION;
case DYNAMIC_SECURITY_ANALYSIS -> OperationType.DYNAMIC_SECURITY;
case DYNAMIC_MARGIN_CALCULATION -> OperationType.DYNAMIC_MARGIN;
case STATE_ESTIMATION -> OperationType.STATE_ESTIMATION;
case PCC_MIN -> OperationType.PCC_MIN;
case SHORT_CIRCUIT, SHORT_CIRCUIT_ONE_BUS -> OperationType.SHORT_CIRCUIT;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.Map;
import java.util.UUID;

/**
Expand Down Expand Up @@ -45,11 +46,11 @@ public class UserProfileInfos {

private UUID voltageInitParameterId;

Integer maxAllowedBuilds;

private UUID spreadsheetConfigCollectionId;

private UUID networkVisualizationParameterId;

private UUID workspaceId;

private Map<OperationType, Integer> maxComputationQuotas;
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public enum StudyBusinessErrorCode implements BusinessErrorCode {
TOO_MANY_NAD_CONFIGS("study.tooManyNadConfigs"),
TOO_MANY_MAP_CARDS("study.tooManyMapCards"),
ELEMENT_ALREADY_EXISTS("study.elementAlreadyExists"),
UNPROCESSABLE_IMPORT_PARAMETER("study.unprocessableImportParameter");
UNPROCESSABLE_IMPORT_PARAMETER("study.unprocessableImportParameter"),
MAX_OPERATION_TYPE_EXCEEDED("study.maxOperationTypeExceeded");

private final String value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public class ConsumerService {
private final RootNetworkNodeInfoService rootNetworkNodeInfoService;
private final DirectoryService directoryService;
private final ComputationParametersService computationParametersService;
private final UserAdminService userAdminService;

public ConsumerService(ObjectMapper objectMapper,
NotificationService notificationService,
Expand All @@ -79,7 +80,8 @@ public ConsumerService(ObjectMapper objectMapper,
StudyConfigService studyConfigService,
RootNetworkNodeInfoService rootNetworkNodeInfoService,
DirectoryService directoryService,
ComputationParametersService computationParametersService) {
ComputationParametersService computationParametersService,
UserAdminService userAdminService) {
this.objectMapper = objectMapper;
this.notificationService = notificationService;
this.studyService = studyService;
Expand All @@ -90,6 +92,7 @@ public ConsumerService(ObjectMapper objectMapper,
this.rootNetworkNodeInfoService = rootNetworkNodeInfoService;
this.directoryService = directoryService;
this.computationParametersService = computationParametersService;
this.userAdminService = userAdminService;
}

@Bean
Expand Down Expand Up @@ -396,6 +399,11 @@ public void consumeCalculationFailed(Message<String> msg, ComputationType comput
if (receiverObj != null) {
handleUnblockNode(receiverObj, computationType);

// free quota
if (userId != null) {
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
}
Comment on lines +402 to +405

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Also guard on resultUuid != null before freeing quota.

Here resultUuid can be null (the resultUuid header may be absent on a failure message — it's only assigned when resultId != null). With only the userId != null check, endOperationWithQuota is called with a null operation id, producing a malformed URL such as /quota/LOAD_FLOW/null/end. The stopped handler at Line 431 correctly guards both. Align this handler:

🐛 Proposed fix
-                    // free quota
-                    if (userId != null) {
+                    // free quota
+                    if (userId != null && resultUuid != null) {
                         userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// free quota
if (userId != null) {
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
}
// free quota
if (userId != null && resultUuid != null) {
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/org/gridsuite/study/server/service/ConsumerService.java` around
lines 402 - 405, The quota release path in ConsumerService’s handler only checks
userId before calling userAdminService.endOperationWithQuota, so it can pass a
null resultUuid and build a malformed quota URL. Update the free-quota block to
require both userId and resultUuid before invoking endOperationWithQuota,
matching the guard already used in the stopped handler, and keep the call to
OperationType.mapFromComputationType(computationType) unchanged.


// send notification for failed computation
UUID studyUuid = networkModificationTreeService.getStudyUuidForNodeId(receiverObj.getNodeUuid());
notificationService.emitStudyError(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateFailedType(), errorMessage, userId);
Expand All @@ -417,6 +425,14 @@ public void consumeCalculationStopped(Message<String> msg, ComputationType compu
// send notification for stopped computation
notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateStatusType());

// free quota
String resultId = msg.getHeaders().get(RESULT_UUID, String.class);
String userId = msg.getHeaders().get(HEADER_USER_ID, String.class);
if (resultId != null && userId != null) {
UUID resultUuid = UUID.fromString(resultId);
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
}

LOGGER.info("{} stopped for node '{}'", computationType.getLabel(), receiverObj.getNodeUuid());
} catch (JsonProcessingException e) {
LOGGER.error(e.toString());
Expand Down Expand Up @@ -469,6 +485,12 @@ public void consumeCalculationDebug(Message<String> msg, ComputationType computa

String errorMessage = (String) msg.getHeaders().get(HEADER_ERROR_MESSAGE);
String userId = (String) msg.getHeaders().get(HEADER_USER_ID);

// free quota
if (userId != null) {
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
}

notificationService.emitComputationDebugFileStatus(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType, userId, resultUuid, errorMessage);
}));
}
Expand Down Expand Up @@ -500,6 +522,12 @@ public void consumeCalculationResult(Message<String> msg, ComputationType comput
handleLoadFlowSuccess(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), resultUuid, userId);
}

// free quota
String userId = msg.getHeaders().get(HEADER_USER_ID, String.class);
if (userId != null) {
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
}

// send notifications
notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateStatusType());
notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateResultType());
Expand Down
83 changes: 71 additions & 12 deletions src/main/java/org/gridsuite/study/server/service/StudyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
Expand Down Expand Up @@ -165,6 +166,9 @@

private final StudyService self;

@Value("${study.enable-operation-quotas}")
private Boolean shouldCheckOperationQuotas = true;

@Autowired
public StudyService(
StudyRepository studyRepository,
Expand Down Expand Up @@ -1009,6 +1013,7 @@
new LoadFlowService.ParametersInfos(lfParametersUuid, withRatioTapChangers, isSecurityNode), lfReportUuid, userId);
rootNetworkNodeInfoService.updateLoadflowResultUuid(nodeUuid, rootNetworkUuid, result, withRatioTapChangers);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(LOAD_FLOW), result);
notificationService.emitStudyChanged(studyEntity.getId(), nodeUuid, rootNetworkUuid, LOAD_FLOW.getUpdateStatusType());
notificationService.emitElementUpdated(studyEntity.getId(), userId);
}
Expand Down Expand Up @@ -1324,7 +1329,10 @@
StudyEntity study = getStudy(studyUuid);
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);

return handleSecurityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId);
UUID result = handleSecurityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(SECURITY_ANALYSIS), result);
return result;
}

private UUID handleSecurityAnalysisRequest(StudyEntity study, UUID nodeUuid, UUID rootNetworkUuid, String userId) {
Expand Down Expand Up @@ -1737,20 +1745,25 @@
}

private long getAllowedBuildNodesUpToQuota(@NonNull UUID studyUuid, @NonNull UUID rootNetworkUuid, @NonNull String userId) {
return userAdminService.getUserMaxAllowedBuilds(userId).map(maxBuilds -> {
Map<OperationType, Integer> userMaxQuotas = userAdminService.getUserMaxQuota(userId);

return Optional.ofNullable(userMaxQuotas.get(OperationType.BUILD)).map(maxBuilds -> {
long nbBuiltNodes = networkModificationTreeService.countBuiltNodes(studyUuid, rootNetworkUuid);
return maxBuilds - nbBuiltNodes;
}).orElse(Long.MAX_VALUE);
}

public void assertNoMaxBuilds(@NonNull UUID studyUuid, @NonNull UUID rootNetworkUuid, @NonNull String userId) {
Map<OperationType, Integer> userMaxQuotas = userAdminService.getUserMaxQuota(userId);

// check restrictions on node builds number
userAdminService.getUserMaxAllowedBuilds(userId).ifPresent(maxBuilds -> {
Integer maxBuilds = userMaxQuotas.get(OperationType.BUILD);
if (maxBuilds != null) {
long nbBuiltNodes = networkModificationTreeService.countBuiltNodes(studyUuid, rootNetworkUuid);
if (nbBuiltNodes >= maxBuilds) {
throw new StudyException(MAX_NODE_BUILDS_EXCEEDED, "max allowed built nodes reached", Map.of("limit", maxBuilds));
}
});
}
}

@Transactional
Expand Down Expand Up @@ -2601,7 +2614,10 @@
StudyEntity study = getStudy(studyUuid);
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);

return handleSensitivityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId);
UUID result = handleSensitivityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(SENSITIVITY_ANALYSIS), result);
return result;
}

private UUID handleSensitivityAnalysisRequest(StudyEntity study, UUID nodeUuid, UUID rootNetworkUuid, String userId) {
Expand Down Expand Up @@ -2634,7 +2650,10 @@
StudyEntity studyEntity = getStudy(studyUuid);
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);

return handleShortCircuitRequest(studyEntity, nodeUuid, rootNetworkUuid, busId, debug, userId);
UUID result = handleShortCircuitRequest(studyEntity, nodeUuid, rootNetworkUuid, busId, debug, userId);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(SHORT_CIRCUIT), result);
return result;
}

private UUID handleShortCircuitRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, Optional<String> busId, boolean debug, String userId) {
Expand All @@ -2661,7 +2680,10 @@
StudyEntity studyEntity = getStudy(studyUuid);
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);

return handleVoltageInitRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
UUID result = handleVoltageInitRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(VOLTAGE_INITIALIZATION), result);
return result;
}

private UUID handleVoltageInitRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) {
Expand Down Expand Up @@ -2949,7 +2971,10 @@
StudyEntity studyEntity = getStudy(studyUuid);
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);

return handleDynamicSimulationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
UUID result = handleDynamicSimulationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(DYNAMIC_SIMULATION), result);
return result;
}

private UUID handleDynamicSimulationRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) {
Expand Down Expand Up @@ -3021,7 +3046,10 @@
StudyEntity studyEntity = getStudy(studyUuid);
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);

return handleDynamicSecurityAnalysisRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
UUID result = handleDynamicSecurityAnalysisRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(DYNAMIC_SECURITY_ANALYSIS), result);
return result;
}

private UUID handleDynamicSecurityAnalysisRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) {
Expand Down Expand Up @@ -3100,7 +3128,10 @@
StudyEntity studyEntity = getStudy(studyUuid);
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);

return handleDynamicMarginCalculationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
UUID result = handleDynamicMarginCalculationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(DYNAMIC_MARGIN_CALCULATION), result);
return result;
}

private UUID handleDynamicMarginCalculationRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) {
Expand Down Expand Up @@ -3322,15 +3353,21 @@
StudyEntity studyEntity = getStudy(studyUuid);
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);

return handleStateEstimationRequest(studyEntity, nodeUuid, rootNetworkUuid, userId, debug);
UUID result = handleStateEstimationRequest(studyEntity, nodeUuid, rootNetworkUuid, userId, debug);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(STATE_ESTIMATION), result);
return result;
}

@Transactional
public UUID runPccMin(@NonNull UUID studyUuid, @NonNull UUID nodeUuid, @NonNull UUID rootNetworkUuid, String userId) {
StudyEntity studyEntity = getStudy(studyUuid);
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);

return handlePccMinRequest(studyEntity, nodeUuid, rootNetworkUuid, userId);
UUID result = handlePccMinRequest(studyEntity, nodeUuid, rootNetworkUuid, userId);

userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(PCC_MIN), result);
return result;
}

private UUID handleStateEstimationRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, String userId, boolean debug) {
Expand Down Expand Up @@ -3734,4 +3771,26 @@
rootNetworkService.updateRootNetworkIndexationStatus(studyUuid, rootNetworkUuid, RootNetworkIndexationStatus.NOT_INDEXED);
notificationService.emitRootNetworksUpdated(studyUuid);
}

public void assertOnQuotasAvailability(ComputationType computationType, String userId) {
if (!shouldCheckOperationQuotas) {

Check warning on line 3776 in src/main/java/org/gridsuite/study/server/service/StudyService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a primitive boolean expression here.

See more on https://sonarcloud.io/project/issues?id=org.gridsuite%3Astudy-server&issues=AZ84K-QX3A2JsegH4jGZ&open=AZ84K-QX3A2JsegH4jGZ&pullRequest=1013
return;
}

Map<OperationType, Integer> userMaxQuotas = userAdminService.getUserMaxQuota(userId);
Map<OperationType, Integer> userCurrentQuotas = userAdminService.getUserCurrentQuota(userId);
OperationType quotaType = OperationType.mapFromComputationType(computationType);

Integer maxComputation = userMaxQuotas.get(quotaType);
Integer currentComputation = userCurrentQuotas.get(quotaType);

if (maxComputation != null && currentComputation != null && currentComputation >= maxComputation) {
throw new StudyException(MAX_OPERATION_TYPE_EXCEEDED, "Max number of " + computationType.name() + " already reached",
Map.of("maxComputation", maxComputation, "currentComputation", currentComputation));
}
}

public Boolean getOperationQuotaStatus() {
return shouldCheckOperationQuotas;
}
}
Loading
Loading