Skip to content

Commit 03724ef

Browse files
committed
feat: add computation quotas check
1 parent a76a79c commit 03724ef

11 files changed

Lines changed: 452 additions & 272 deletions

File tree

src/main/java/org/gridsuite/study/server/controller/StudyController.java

Lines changed: 258 additions & 238 deletions
Large diffs are not rendered by default.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright (c) 2026, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
package org.gridsuite.study.server.dto;
8+
9+
/**
10+
* @author Ghiles Abdellah {@literal <ghiles.abdellah at rte-france.com>}
11+
*/
12+
public enum OperationType {
13+
CASES,
14+
BUILD,
15+
LOAD_FLOW,
16+
SECURITY,
17+
SENSITIVITY,
18+
SHORT_CIRCUIT,
19+
VOLTAGE_INIT,
20+
PCC_MIN,
21+
STATE_ESTIMATION,
22+
BALANCE_ADJUSTEMENT,
23+
DYNAMIC_SIMULATION,
24+
DYNAMIC_SECURITY,
25+
DYNAMIC_MARGIN;
26+
27+
public static OperationType mapFromComputationType(ComputationType computationType) {
28+
return switch (computationType) {
29+
case LOAD_FLOW -> OperationType.LOAD_FLOW;
30+
case SECURITY_ANALYSIS -> OperationType.SECURITY;
31+
case SENSITIVITY_ANALYSIS -> OperationType.SENSITIVITY;
32+
case VOLTAGE_INITIALIZATION -> OperationType.VOLTAGE_INIT;
33+
case DYNAMIC_SIMULATION -> OperationType.DYNAMIC_SIMULATION;
34+
case DYNAMIC_SECURITY_ANALYSIS -> OperationType.DYNAMIC_SECURITY;
35+
case DYNAMIC_MARGIN_CALCULATION -> OperationType.DYNAMIC_MARGIN;
36+
case STATE_ESTIMATION -> OperationType.STATE_ESTIMATION;
37+
case PCC_MIN -> OperationType.PCC_MIN;
38+
case SHORT_CIRCUIT, SHORT_CIRCUIT_ONE_BUS -> OperationType.SHORT_CIRCUIT;
39+
};
40+
}
41+
}

src/main/java/org/gridsuite/study/server/dto/UserProfileInfos.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import lombok.Getter;
1212
import lombok.NoArgsConstructor;
1313

14+
import java.util.Map;
1415
import java.util.UUID;
1516

1617
/**
@@ -45,11 +46,11 @@ public class UserProfileInfos {
4546

4647
private UUID voltageInitParameterId;
4748

48-
Integer maxAllowedBuilds;
49-
5049
private UUID spreadsheetConfigCollectionId;
5150

5251
private UUID networkVisualizationParameterId;
5352

5453
private UUID workspaceId;
54+
55+
private Map<OperationType, Integer> maxComputationQuotas;
5556
}

src/main/java/org/gridsuite/study/server/error/StudyBusinessErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ public enum StudyBusinessErrorCode implements BusinessErrorCode {
3131
TOO_MANY_NAD_CONFIGS("study.tooManyNadConfigs"),
3232
TOO_MANY_MAP_CARDS("study.tooManyMapCards"),
3333
ELEMENT_ALREADY_EXISTS("study.elementAlreadyExists"),
34-
UNPROCESSABLE_IMPORT_PARAMETER("study.unprocessableImportParameter");
34+
UNPROCESSABLE_IMPORT_PARAMETER("study.unprocessableImportParameter"),
35+
MAX_OPERATION_TYPE_EXCEEDED("study.maxOperationTypeExceeded");
3536

3637
private final String value;
3738

src/main/java/org/gridsuite/study/server/service/ConsumerService.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public class ConsumerService {
6969
private final RootNetworkNodeInfoService rootNetworkNodeInfoService;
7070
private final DirectoryService directoryService;
7171
private final ComputationParametersService computationParametersService;
72+
private final UserAdminService userAdminService;
7273

7374
public ConsumerService(ObjectMapper objectMapper,
7475
NotificationService notificationService,
@@ -79,7 +80,8 @@ public ConsumerService(ObjectMapper objectMapper,
7980
StudyConfigService studyConfigService,
8081
RootNetworkNodeInfoService rootNetworkNodeInfoService,
8182
DirectoryService directoryService,
82-
ComputationParametersService computationParametersService) {
83+
ComputationParametersService computationParametersService,
84+
UserAdminService userAdminService) {
8385
this.objectMapper = objectMapper;
8486
this.notificationService = notificationService;
8587
this.studyService = studyService;
@@ -90,6 +92,7 @@ public ConsumerService(ObjectMapper objectMapper,
9092
this.rootNetworkNodeInfoService = rootNetworkNodeInfoService;
9193
this.directoryService = directoryService;
9294
this.computationParametersService = computationParametersService;
95+
this.userAdminService = userAdminService;
9396
}
9497

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

402+
// free quota
403+
if (userId != null) {
404+
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
405+
}
406+
399407
// send notification for failed computation
400408
UUID studyUuid = networkModificationTreeService.getStudyUuidForNodeId(receiverObj.getNodeUuid());
401409
notificationService.emitStudyError(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateFailedType(), errorMessage, userId);
@@ -417,6 +425,14 @@ public void consumeCalculationStopped(Message<String> msg, ComputationType compu
417425
// send notification for stopped computation
418426
notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateStatusType());
419427

428+
// free quota
429+
String resultId = msg.getHeaders().get(RESULT_UUID, String.class);
430+
String userId = msg.getHeaders().get(HEADER_USER_ID, String.class);
431+
if (resultId != null && userId != null) {
432+
UUID resultUuid = UUID.fromString(resultId);
433+
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
434+
}
435+
420436
LOGGER.info("{} stopped for node '{}'", computationType.getLabel(), receiverObj.getNodeUuid());
421437
} catch (JsonProcessingException e) {
422438
LOGGER.error(e.toString());
@@ -469,6 +485,12 @@ public void consumeCalculationDebug(Message<String> msg, ComputationType computa
469485

470486
String errorMessage = (String) msg.getHeaders().get(HEADER_ERROR_MESSAGE);
471487
String userId = (String) msg.getHeaders().get(HEADER_USER_ID);
488+
489+
// free quota
490+
if (userId != null) {
491+
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
492+
}
493+
472494
notificationService.emitComputationDebugFileStatus(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType, userId, resultUuid, errorMessage);
473495
}));
474496
}
@@ -500,6 +522,12 @@ public void consumeCalculationResult(Message<String> msg, ComputationType comput
500522
handleLoadFlowSuccess(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), resultUuid, userId);
501523
}
502524

525+
// free quota
526+
String userId = msg.getHeaders().get(HEADER_USER_ID, String.class);
527+
if (userId != null) {
528+
userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid);
529+
}
530+
503531
// send notifications
504532
notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateStatusType());
505533
notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateResultType());

src/main/java/org/gridsuite/study/server/service/StudyService.java

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.slf4j.Logger;
6262
import org.slf4j.LoggerFactory;
6363
import org.springframework.beans.factory.annotation.Autowired;
64+
import org.springframework.beans.factory.annotation.Value;
6465
import org.springframework.context.annotation.Lazy;
6566
import org.springframework.data.domain.Pageable;
6667
import org.springframework.data.domain.Sort;
@@ -165,6 +166,9 @@ public enum ReportType {
165166

166167
private final StudyService self;
167168

169+
@Value("${study.enable-operation-quotas}")
170+
private Boolean shouldCheckOperationQuotas = true;
171+
168172
@Autowired
169173
public StudyService(
170174
StudyRepository studyRepository,
@@ -1009,6 +1013,7 @@ private void handleLoadflowRequest(StudyEntity studyEntity, UUID nodeUuid, UUID
10091013
new LoadFlowService.ParametersInfos(lfParametersUuid, withRatioTapChangers, isSecurityNode), lfReportUuid, userId);
10101014
rootNetworkNodeInfoService.updateLoadflowResultUuid(nodeUuid, rootNetworkUuid, result, withRatioTapChangers);
10111015

1016+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(LOAD_FLOW), result);
10121017
notificationService.emitStudyChanged(studyEntity.getId(), nodeUuid, rootNetworkUuid, LOAD_FLOW.getUpdateStatusType());
10131018
notificationService.emitElementUpdated(studyEntity.getId(), userId);
10141019
}
@@ -1324,7 +1329,10 @@ public UUID runSecurityAnalysis(@NonNull UUID studyUuid, @NonNull UUID nodeUuid,
13241329
StudyEntity study = getStudy(studyUuid);
13251330
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);
13261331

1327-
return handleSecurityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId);
1332+
UUID result = handleSecurityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId);
1333+
1334+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(SECURITY_ANALYSIS), result);
1335+
return result;
13281336
}
13291337

13301338
private UUID handleSecurityAnalysisRequest(StudyEntity study, UUID nodeUuid, UUID rootNetworkUuid, String userId) {
@@ -1737,20 +1745,25 @@ public void handleBuildSuccess(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUu
17371745
}
17381746

17391747
private long getAllowedBuildNodesUpToQuota(@NonNull UUID studyUuid, @NonNull UUID rootNetworkUuid, @NonNull String userId) {
1740-
return userAdminService.getUserMaxAllowedBuilds(userId).map(maxBuilds -> {
1748+
Map<OperationType, Integer> userMaxQuotas = userAdminService.getUserMaxQuota(userId);
1749+
1750+
return Optional.ofNullable(userMaxQuotas.get(OperationType.BUILD.name())).map(maxBuilds -> {
17411751
long nbBuiltNodes = networkModificationTreeService.countBuiltNodes(studyUuid, rootNetworkUuid);
17421752
return maxBuilds - nbBuiltNodes;
17431753
}).orElse(Long.MAX_VALUE);
17441754
}
17451755

17461756
public void assertNoMaxBuilds(@NonNull UUID studyUuid, @NonNull UUID rootNetworkUuid, @NonNull String userId) {
1757+
Map<OperationType, Integer> userMaxQuotas = userAdminService.getUserMaxQuota(userId);
1758+
17471759
// check restrictions on node builds number
1748-
userAdminService.getUserMaxAllowedBuilds(userId).ifPresent(maxBuilds -> {
1760+
Integer maxBuilds = userMaxQuotas.get(OperationType.BUILD.name());
1761+
if (maxBuilds != null) {
17491762
long nbBuiltNodes = networkModificationTreeService.countBuiltNodes(studyUuid, rootNetworkUuid);
17501763
if (nbBuiltNodes >= maxBuilds) {
17511764
throw new StudyException(MAX_NODE_BUILDS_EXCEEDED, "max allowed built nodes reached", Map.of("limit", maxBuilds));
17521765
}
1753-
});
1766+
}
17541767
}
17551768

17561769
@Transactional
@@ -2601,7 +2614,10 @@ public UUID runSensitivityAnalysis(@NonNull UUID studyUuid, @NonNull UUID nodeUu
26012614
StudyEntity study = getStudy(studyUuid);
26022615
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);
26032616

2604-
return handleSensitivityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId);
2617+
UUID result = handleSensitivityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId);
2618+
2619+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(SENSITIVITY_ANALYSIS), result);
2620+
return result;
26052621
}
26062622

26072623
private UUID handleSensitivityAnalysisRequest(StudyEntity study, UUID nodeUuid, UUID rootNetworkUuid, String userId) {
@@ -2634,7 +2650,10 @@ public UUID runShortCircuit(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUuid,
26342650
StudyEntity studyEntity = getStudy(studyUuid);
26352651
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);
26362652

2637-
return handleShortCircuitRequest(studyEntity, nodeUuid, rootNetworkUuid, busId, debug, userId);
2653+
UUID result = handleShortCircuitRequest(studyEntity, nodeUuid, rootNetworkUuid, busId, debug, userId);
2654+
2655+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(SHORT_CIRCUIT), result);
2656+
return result;
26382657
}
26392658

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

2664-
return handleVoltageInitRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
2683+
UUID result = handleVoltageInitRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
2684+
2685+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(VOLTAGE_INITIALIZATION), result);
2686+
return result;
26652687
}
26662688

26672689
private UUID handleVoltageInitRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) {
@@ -2949,7 +2971,10 @@ public UUID runDynamicSimulation(@NonNull UUID studyUuid, @NonNull UUID nodeUuid
29492971
StudyEntity studyEntity = getStudy(studyUuid);
29502972
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);
29512973

2952-
return handleDynamicSimulationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
2974+
UUID result = handleDynamicSimulationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
2975+
2976+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(DYNAMIC_SIMULATION), result);
2977+
return result;
29532978
}
29542979

29552980
private UUID handleDynamicSimulationRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) {
@@ -3021,7 +3046,10 @@ public UUID runDynamicSecurityAnalysis(@NonNull UUID studyUuid, @NonNull UUID no
30213046
StudyEntity studyEntity = getStudy(studyUuid);
30223047
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);
30233048

3024-
return handleDynamicSecurityAnalysisRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
3049+
UUID result = handleDynamicSecurityAnalysisRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
3050+
3051+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(DYNAMIC_SECURITY_ANALYSIS), result);
3052+
return result;
30253053
}
30263054

30273055
private UUID handleDynamicSecurityAnalysisRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) {
@@ -3100,7 +3128,10 @@ public UUID runDynamicMarginCalculation(@NonNull UUID studyUuid, @NonNull UUID n
31003128
StudyEntity studyEntity = getStudy(studyUuid);
31013129
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);
31023130

3103-
return handleDynamicMarginCalculationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
3131+
UUID result = handleDynamicMarginCalculationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId);
3132+
3133+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(DYNAMIC_MARGIN_CALCULATION), result);
3134+
return result;
31043135
}
31053136

31063137
private UUID handleDynamicMarginCalculationRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) {
@@ -3322,15 +3353,21 @@ public UUID runStateEstimation(@NonNull UUID studyUuid, @NonNull UUID nodeUuid,
33223353
StudyEntity studyEntity = getStudy(studyUuid);
33233354
networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid);
33243355

3325-
return handleStateEstimationRequest(studyEntity, nodeUuid, rootNetworkUuid, userId, debug);
3356+
UUID result = handleStateEstimationRequest(studyEntity, nodeUuid, rootNetworkUuid, userId, debug);
3357+
3358+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(STATE_ESTIMATION), result);
3359+
return result;
33263360
}
33273361

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

3333-
return handlePccMinRequest(studyEntity, nodeUuid, rootNetworkUuid, userId);
3367+
UUID result = handlePccMinRequest(studyEntity, nodeUuid, rootNetworkUuid, userId);
3368+
3369+
userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(PCC_MIN), result);
3370+
return result;
33343371
}
33353372

33363373
private UUID handleStateEstimationRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, String userId, boolean debug) {
@@ -3734,4 +3771,26 @@ public void invalidateStudyRootNetwork(UUID studyUuid, UUID rootNetworkUuid, Str
37343771
rootNetworkService.updateRootNetworkIndexationStatus(studyUuid, rootNetworkUuid, RootNetworkIndexationStatus.NOT_INDEXED);
37353772
notificationService.emitRootNetworksUpdated(studyUuid);
37363773
}
3774+
3775+
public void assertOnQuotasAvailability(ComputationType computationType, String userId) {
3776+
if (!shouldCheckOperationQuotas) {
3777+
return;
3778+
}
3779+
3780+
Map<OperationType, Integer> userMaxQuotas = userAdminService.getUserMaxQuota(userId);
3781+
Map<OperationType, Integer> userCurrentQuotas = userAdminService.getUserCurrentQuota(userId);
3782+
OperationType quotaType = OperationType.mapFromComputationType(computationType);
3783+
3784+
Integer maxComputation = userMaxQuotas.get(quotaType.name());
3785+
Integer currentComputation = userCurrentQuotas.get(quotaType.name());
3786+
3787+
if (maxComputation != null && currentComputation != null && currentComputation >= maxComputation) {
3788+
throw new StudyException(MAX_OPERATION_TYPE_EXCEEDED, "Max number of " + computationType.name() + " already reached",
3789+
Map.of("maxComputation", maxComputation, "currentComputation", currentComputation));
3790+
}
3791+
}
3792+
3793+
public Boolean getOperationQuotaStatus() {
3794+
return shouldCheckOperationQuotas;
3795+
}
37373796
}

0 commit comments

Comments
 (0)