diff --git a/src/main/java/org/gridsuite/study/server/controller/SupervisionController.java b/src/main/java/org/gridsuite/study/server/controller/SupervisionController.java index 329c028fcc..d8d9d98408 100644 --- a/src/main/java/org/gridsuite/study/server/controller/SupervisionController.java +++ b/src/main/java/org/gridsuite/study/server/controller/SupervisionController.java @@ -188,4 +188,11 @@ public ResponseEntity invalidateStudy(@PathVariable("studyUuid") UUID stud return ResponseEntity.ok().build(); } + @GetMapping(value = "/studies/loaded") + @Operation(summary = "Get the study uuids whose network is currently loaded") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "List of the study uuids whose network is currently loaded")}) + public ResponseEntity> getLoadedStudies(@Parameter(description = "Study uuids to filter") @RequestParam("ids") List studyUuids) { + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(supervisionService.getLoadedStudyUuids(studyUuids)); + } + } diff --git a/src/main/java/org/gridsuite/study/server/dto/NetworkLoadStatus.java b/src/main/java/org/gridsuite/study/server/dto/NetworkLoadStatus.java new file mode 100644 index 0000000000..b8f6f17402 --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/dto/NetworkLoadStatus.java @@ -0,0 +1,17 @@ +/** + * 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 Ghazwa Rehili + */ +public enum NetworkLoadStatus { + LOADED, + UNLOADED, + LOADING, + UNLOADING +} diff --git a/src/main/java/org/gridsuite/study/server/repository/StudyEntity.java b/src/main/java/org/gridsuite/study/server/repository/StudyEntity.java index 0aae3bb0dd..f44fc1c821 100644 --- a/src/main/java/org/gridsuite/study/server/repository/StudyEntity.java +++ b/src/main/java/org/gridsuite/study/server/repository/StudyEntity.java @@ -8,6 +8,7 @@ import jakarta.persistence.*; import lombok.*; +import org.gridsuite.study.server.dto.NetworkLoadStatus; import org.gridsuite.study.server.dto.RootNetworkIndexationStatus; import org.gridsuite.study.server.repository.rootnetwork.RootNetworkEntity; import org.gridsuite.study.server.repository.voltageinit.StudyVoltageInitParametersEntity; @@ -132,6 +133,11 @@ public class StudyEntity extends AbstractManuallyAssignedIdentifierEntity @Column(name = "mono_root", columnDefinition = "boolean default true") private boolean monoRoot; + @Enumerated(EnumType.STRING) + @Column(name = "network_load_status") + @Builder.Default + private NetworkLoadStatus networkLoadStatus = NetworkLoadStatus.LOADED; + @Embedded @Builder.Default private SpreadsheetParametersEntity spreadsheetParameters = new SpreadsheetParametersEntity(); diff --git a/src/main/java/org/gridsuite/study/server/repository/StudyRepository.java b/src/main/java/org/gridsuite/study/server/repository/StudyRepository.java index b31b92a368..b247675fba 100644 --- a/src/main/java/org/gridsuite/study/server/repository/StudyRepository.java +++ b/src/main/java/org/gridsuite/study/server/repository/StudyRepository.java @@ -6,10 +6,12 @@ */ package org.gridsuite.study.server.repository; +import org.gridsuite.study.server.dto.NetworkLoadStatus; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -22,4 +24,6 @@ public interface StudyRepository extends JpaRepository { @EntityGraph(attributePaths = {"rootNetworks"}, type = EntityGraph.EntityGraphType.LOAD) Optional findWithRootNetworksById(UUID id); + + List findAllByIdInAndNetworkLoadStatus(List ids, NetworkLoadStatus networkLoadStatus); } diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index caa968de67..6dff23f9f9 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -19,6 +19,7 @@ import org.gridsuite.study.server.dto.*; import org.gridsuite.study.server.dto.InvalidateNodeTreeParameters.ComputationsInvalidationMode; import org.gridsuite.study.server.dto.InvalidateNodeTreeParameters.InvalidationMode; +import org.gridsuite.study.server.dto.NetworkLoadStatus; import org.gridsuite.study.server.dto.caseimport.CaseImportAction; import org.gridsuite.study.server.dto.computation.ComputationParameterUUIDs; import org.gridsuite.study.server.dto.elasticsearch.EquipmentInfos; @@ -447,10 +448,16 @@ private void recreateNetwork(RootNetworkInfos rootNetworkInfos, UUID studyUuid, ? new HashMap<>(rootNetworkService.getImportParameters(rootNetworkInfos.getId())) : importParameters; + self.updateNetworkLoadStatus(studyUuid, NetworkLoadStatus.LOADING); persistNetwork(rootNetworkInfos, studyUuid, null, userId, importParametersToUse, CaseImportAction.NETWORK_RECREATION, reportId); notificationService.emitElementUpdated(studyUuid, userId); } + @Transactional + public void updateNetworkLoadStatus(UUID studyUuid, NetworkLoadStatus networkLoadStatus) { + getStudy(studyUuid).setNetworkLoadStatus(networkLoadStatus); + } + public UUID duplicateStudy(UUID sourceStudyUuid, String userId) { Objects.requireNonNull(sourceStudyUuid); @@ -633,6 +640,7 @@ public CreatedStudyBasicInfos updateNetwork(UUID studyUuid, UUID rootNetworkUuid RootNetworkEntity rootNetworkEntity = rootNetworkService.getRootNetwork(rootNetworkUuid).orElseThrow(() -> new StudyException(NOT_FOUND, "Root network not found")); rootNetworkService.updateNetwork(rootNetworkEntity, networkInfos); + studyEntity.setNetworkLoadStatus(NetworkLoadStatus.LOADED); CreatedStudyBasicInfos createdStudyBasicInfos = toCreatedStudyBasicInfos(studyEntity); studyInfosService.add(createdStudyBasicInfos); @@ -2705,6 +2713,7 @@ public Map getAllComputationsStatus(@NonNull UUID study public void invalidateStudyRootNetwork(UUID studyUuid, UUID rootNetworkUuid, String userId, boolean updateCase) { rootNetworkService.assertIsRootNetworkInStudy(studyUuid, rootNetworkUuid); + self.updateNetworkLoadStatus(studyUuid, NetworkLoadStatus.UNLOADING); var rootNodeUuid = networkModificationTreeService.getStudyRootNodeUuid(studyUuid); // First we unbuild all nodes doUnbuildNodeTree(studyUuid, rootNodeUuid, true, true, userId); @@ -2713,6 +2722,7 @@ public void invalidateStudyRootNetwork(UUID studyUuid, UUID rootNetworkUuid, Str if (!updateCase) { rootNetworkService.updateRootNetworkIndexationStatus(studyUuid, rootNetworkUuid, RootNetworkIndexationStatus.NOT_INDEXED); } + self.updateNetworkLoadStatus(studyUuid, NetworkLoadStatus.UNLOADED); notificationService.emitRootNetworksUpdated(studyUuid); } diff --git a/src/main/java/org/gridsuite/study/server/service/SupervisionService.java b/src/main/java/org/gridsuite/study/server/service/SupervisionService.java index 30d06886a7..b594ff6a4e 100644 --- a/src/main/java/org/gridsuite/study/server/service/SupervisionService.java +++ b/src/main/java/org/gridsuite/study/server/service/SupervisionService.java @@ -13,7 +13,6 @@ import org.gridsuite.study.server.elasticsearch.EquipmentInfosService; import org.gridsuite.study.server.elasticsearch.StudyInfosService; import org.gridsuite.study.server.networkmodificationtree.entities.RootNetworkNodeInfoEntity; -import org.gridsuite.study.server.notification.NotificationService; import org.gridsuite.study.server.repository.StudyEntity; import org.gridsuite.study.server.repository.StudyRepository; import org.gridsuite.study.server.repository.rootnetwork.RootNetworkEntity; @@ -97,8 +96,6 @@ public class SupervisionService { private final RootNetworkService rootNetworkService; - private final NotificationService notificationService; - private static final String SUPERVISION_USER = "Supervision"; public SupervisionService(StudyService studyService, @@ -120,8 +117,7 @@ public SupervisionService(StudyService studyService, ElasticsearchOperations elasticsearchOperations, StudyInfosService studyInfosService, RootNetworkService rootNetworkService, - StudyRepository studyRepository, - NotificationService notificationService) { + StudyRepository studyRepository) { this.studyService = studyService; this.networkModificationTreeService = networkModificationTreeService; this.loadFlowService = loadFlowService; @@ -143,7 +139,6 @@ public SupervisionService(StudyService studyService, this.studyInfosService = studyInfosService; this.rootNetworkService = rootNetworkService; this.studyRepository = studyRepository; - this.notificationService = notificationService; } @Transactional @@ -414,10 +409,16 @@ public void invalidateStudy(UUID studyUuid) { var rootNodeUuid = networkModificationTreeService.getStudyRootNodeUuid(studyUuid); studyService.unblockNodeTree(studyUuid, rootNodeUuid); } - notificationService.emitElementUpdated(studyUuid, SUPERVISION_USER); LOGGER.trace("Study {} nodes builds deleted and root node invalidated in : {} milliseconds", studyUuid, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime.get())); } + @Transactional(readOnly = true) + public List getLoadedStudyUuids(List studyUuids) { + return studyRepository.findAllByIdInAndNetworkLoadStatus(studyUuids, NetworkLoadStatus.LOADED).stream() + .map(StudyEntity::getId) + .toList(); + } + @Transactional public void recreateStudyIndices() { recreateIndex(CreatedStudyBasicInfos.class); diff --git a/src/main/resources/db/changelog/changesets/changelog_20260818T152042Z.xml b/src/main/resources/db/changelog/changesets/changelog_20260818T152042Z.xml new file mode 100644 index 0000000000..bf3920c6f7 --- /dev/null +++ b/src/main/resources/db/changelog/changesets/changelog_20260818T152042Z.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index b031633b6c..33b57c51c3 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -389,3 +389,6 @@ databaseChangeLog: - include: file: changesets/changelog_20260703T090337Z.xml relativeToChangelogFile: true + - include: + file: changesets/changelog_20260818T152042Z.xml + relativeToChangelogFile: true diff --git a/src/test/java/org/gridsuite/study/server/SupervisionControllerTest.java b/src/test/java/org/gridsuite/study/server/SupervisionControllerTest.java index 002cdfdf90..11a6f0cbeb 100644 --- a/src/test/java/org/gridsuite/study/server/SupervisionControllerTest.java +++ b/src/test/java/org/gridsuite/study/server/SupervisionControllerTest.java @@ -369,5 +369,28 @@ void testInvalidateStudy() throws Exception { allRootNetworkUuids.forEach(rootNetworkUuid -> rootNetworkNodeInfoRepository.findAllByRootNetworkId(rootNetworkUuid) .forEach(info -> assertThat(info.getBlockedNode()).isFalse())); + + // Check that the study network is now marked as unloaded, and that invalidating a study does not + // update its last modification date in directory-server (no element-update message emitted) + assertEquals(NetworkLoadStatus.UNLOADED, studyRepository.findById(STUDY_UUID).orElseThrow().getNetworkLoadStatus()); + } + + @Test + void testGetLoadedStudies() throws Exception { + initStudy(); + assertEquals(NetworkLoadStatus.LOADED, studyRepository.findById(STUDY_UUID).orElseThrow().getNetworkLoadStatus()); + UUID unknownStudyUuid = UUID.randomUUID(); + MvcResult mvcResult = mockMvc.perform(get("/v1/supervision/studies/loaded") + .queryParam("ids", STUDY_UUID.toString(), unknownStudyUuid.toString())) + .andExpectAll(status().isOk(), content().contentType(MediaType.APPLICATION_JSON)).andReturn(); + List loadedStudyUuids = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { }); + assertEquals(List.of(STUDY_UUID), loadedStudyUuids); + mockMvc.perform(delete("/v1/supervision/studies/{studyUuid}/invalidate", STUDY_UUID)) + .andExpect(status().isOk()); + mvcResult = mockMvc.perform(get("/v1/supervision/studies/loaded") + .queryParam("ids", STUDY_UUID.toString(), unknownStudyUuid.toString())) + .andExpectAll(status().isOk(), content().contentType(MediaType.APPLICATION_JSON)).andReturn(); + loadedStudyUuids = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { }); + assertThat(loadedStudyUuids).isEmpty(); } }