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
43 changes: 43 additions & 0 deletions src/main/java/org/gridsuite/study/server/dto/BuildInfos.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,56 @@ public class BuildInfos {
// map with modification groups as key, modification to excludes as value
private Map<UUID, Set<UUID>> modificationUuidsToExclude = new HashMap<>();

/**
* Reports generated during this build operation.
* These are new reports created when applying modifications.
*/
private List<ReportInfos> reportsInfos = new ArrayList<>();

/**
* Reports inherited from the nearest built parent node.
* These ensure child nodes maintain references to ancestor reports,
* preventing premature deletion when intermediate nodes are unbuilt.
*/
private List<ReportInfos> inheritedReportsInfos = new ArrayList<>();

public void insertModificationInfos(UUID modificationGroupUuid, Set<UUID> modificationUuidsToExclude, ReportInfos reportInfos) {
if (modificationUuidsToExclude != null && !modificationUuidsToExclude.isEmpty()) {
this.modificationUuidsToExclude.put(modificationGroupUuid, modificationUuidsToExclude);
}
modificationGroupUuids.add(0, modificationGroupUuid);
reportsInfos.add(0, reportInfos);
}

/**
* Adds a report inherited from a built parent node.
* Inherited reports are added at the end to maintain proper ordering:
* parent reports come before child reports.
*
* @param nodeUuid The UUID of the node that owns this report
* @param reportUuid The UUID of the report
*/
public void addInheritedReport(UUID nodeUuid, UUID reportUuid) {
inheritedReportsInfos.add(new ReportInfos(reportUuid, nodeUuid));
}

/**
* Gets all reports (both new and inherited) as a single map.
* Useful for storing the complete report set in the database.
*
* @return Map of node UUID to report UUID
*/
public Map<UUID, UUID> getAllReportsAsMap() {
Map<UUID, UUID> allReports = new LinkedHashMap<>();

// Add inherited reports first (parent reports come first)
inheritedReportsInfos.forEach(r ->
allReports.put(r.nodeUuid(), r.reportUuid()));

// Add new reports (may override inherited if same node)
reportsInfos.forEach(r ->
allReports.put(r.nodeUuid(), r.reportUuid()));

return allReports;
}
}
7 changes: 5 additions & 2 deletions src/main/java/org/gridsuite/study/server/dto/ReportInfos.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
@Schema(description = "Report infos")
public record ReportInfos(
UUID reportUuid,
UUID nodeUuid
UUID nodeUuid,
ReportMode reportMode
) {

public ReportInfos(UUID reportUuid, UUID nodeUuid) {
this(reportUuid, nodeUuid, ReportMode.APPEND);
}
}

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

/**
* Enum to specify how reports should be handled when building nodes.
* @author Achour BERRAHMA <achour.berrahma at rte-france.com>
*/
public enum ReportMode {
APPEND,
REPLACE
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

/**
Expand Down Expand Up @@ -65,4 +66,53 @@ public interface RootNetworkNodeInfoRepository extends JpaRepository<RootNetwork

@Query(value = "SELECT count(rnni) > 0 FROM RootNetworkNodeInfoEntity rnni WHERE rnni.rootNetwork.id = :rootNetworkUuid AND rnni.nodeInfo.idNode IN :nodesUuids AND rnni.blockedNode = true ")
boolean existsByNodeUuidsAndBlockedNode(UUID rootNetworkUuid, List<UUID> nodesUuids);

/**
* Finds report UUIDs that are still referenced by other RootNetworkNodeInfo entities.
* <p>
* This is a critical safety query that prevents cascade deletion of shared reports.
* It performs a bulk check in a single database round trip for efficiency.
* <p>
* <strong>Use case:</strong> When invalidating a node, we need to determine which
* reports can be safely deleted. This query identifies reports that are still needed
* by other nodes.
*
* @param reportUuids Set of report UUIDs to check for references
* @param excludedEntityId The RootNetworkNodeInfo ID to exclude from the check
* (typically the node being deleted/invalidated)
* @return Set of report UUIDs that are still referenced by other entities
*/
@Query("""
SELECT DISTINCT VALUE(rnni.modificationReports)
FROM RootNetworkNodeInfoEntity rnni
JOIN rnni.modificationReports mr
WHERE VALUE(rnni.modificationReports) IN :reportUuids
AND rnni.id != :excludedEntityId
""")
Set<UUID> findReferencedReportUuidsExcludingEntity(Set<UUID> reportUuids, UUID excludedEntityId);

/**
* Finds report UUIDs for a specific node by searching across all RootNetworkNodeInfo
* entities in the given root network.
* <p>
* This method searches the modificationReports maps of all nodes in the root network
* to find if any node has already generated a report for the target node.
* <p>
* <strong>Important:</strong> Based on our code logic, there should be at most one report UUID
* assigned to any given node across the entire root network. The returned Set should contain
* either 0 or 1 elements. If the Set contains multiple values, this indicates a data
* inconsistency that should be investigated.
*
* @param targetNodeUuid the node UUID to find a report for (the map key)
* @param rootNetworkUuid the root network to search within
* @return a Set containing the report UUID(s) found in the modificationReports maps,
* expected to be empty or contain exactly one element under normal conditions
*/
@Query("""
SELECT DISTINCT VALUE(rnni.modificationReports)
FROM RootNetworkNodeInfoEntity rnni
WHERE rnni.rootNetwork.id = :rootNetworkUuid
AND KEY(rnni.modificationReports) = :targetNodeUuid
""")
Set<UUID> findReportUuidsForNodeInRootNetwork(UUID targetNodeUuid, UUID rootNetworkUuid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -747,12 +747,17 @@ public boolean hasModifications(@NonNull UUID nodeUuid, boolean stashed) {
}

@Transactional
public UUID getReportUuid(UUID nodeUuid, UUID rootNetworkUuid) {
public Optional<UUID> getReportUuid(UUID nodeUuid, UUID rootNetworkUuid) {
NodeEntity nodeEntity = getNodeEntity(nodeUuid);
if (nodeEntity.getType().equals(NodeType.ROOT)) {
return rootNetworkService.getRootReportUuid(rootNetworkUuid);
return Optional.ofNullable(rootNetworkService.getRootReportUuid(rootNetworkUuid));
} else {
return rootNetworkNodeInfoService.getRootNetworkNodeInfo(nodeUuid, rootNetworkUuid).orElseThrow(() -> new StudyException(ROOT_NETWORK_NOT_FOUND)).getModificationReports().get(nodeUuid);
return Optional.ofNullable(
rootNetworkNodeInfoService.getRootNetworkNodeInfo(nodeUuid, rootNetworkUuid)
.orElseThrow(() -> new StudyException(ROOT_NETWORK_NOT_FOUND))
.getModificationReports()
.get(nodeUuid)
);
}
}

Expand Down Expand Up @@ -846,21 +851,107 @@ public List<NodeEntity> getAllNodes(UUID studyUuid) {
return nodesRepository.findAllByStudyId(studyUuid);
}

/**
* Gets or generates a modification report UUID for a node.
* <p>
* Search priority:
* 1. Check if the node being built already has a report for this node
* 2. Search across ALL nodes in the root network for existing reports
* 3. Generate a new UUID if not found anywhere
* <p>
* This implements intelligent report reuse to avoid duplicate reports when
* multiple nodes are built in different orders.
*
* @param nodeUuid the node that needs a report
* @param rootNetworkUuid the root network context
* @param nodeToBuildUuid the node currently being built
* @return the report UUID to use (existing or new)
*/
private UUID getModificationReportUuid(UUID nodeUuid, UUID rootNetworkUuid, UUID nodeToBuildUuid) {
return self.getModificationReports(nodeToBuildUuid, rootNetworkUuid).getOrDefault(nodeUuid, UUID.randomUUID());
Map<UUID, UUID> targetNodeReports = self.getModificationReports(nodeToBuildUuid, rootNetworkUuid);
if (targetNodeReports.containsKey(nodeUuid)) {
return targetNodeReports.get(nodeUuid);
}

Optional<UUID> crossNodeReportUuid = rootNetworkNodeInfoService
.findExistingReportUuidForNode(nodeUuid, rootNetworkUuid);

return crossNodeReportUuid.orElseGet(UUID::randomUUID);
}

/**
* Inherits modification reports from the nearest built ancestor node.
* <p>
* When building a node, we inherit all report references from its nearest built parent.
* This ensures that if an intermediate node is later deleted, reports referenced by
* descendant nodes won't be accidentally deleted.
* <p>
* Example node tree:
* <pre>
* ROOT (built)
* └─ N1 (not built, report: r1)
* └─ N2 (built, reports: {N1→r1, N2→r2})
* └─ N3 (not built, report: r3)
* └─ N4 (building, report: r4)
*
* When N4 builds:
* 1. Finds N2 as nearest built parent
* 2. Inherits {N1→r1, N2→r2} from N2
* 3. Adds its own {N3→r3, N4→r4}
* 4. Final reports: {N1→r1, N2→r2, N3→r3, N4→r4}
*
* If N2 is later deleted, reports r1 and r2 won't be deleted
* because N4 still references them.
* </pre>
*
* @param builtParentNodeUuid UUID of the built parent node
* @param rootNetworkUuid Root network context
* @param buildInfos Build information to populate with inherited reports
*/
private void inheritModificationReportsFromBuiltParent(UUID builtParentNodeUuid, UUID rootNetworkUuid, BuildInfos buildInfos) {
Map<UUID, UUID> parentReports = self.getModificationReports(
builtParentNodeUuid,
rootNetworkUuid
);

if (parentReports == null || parentReports.isEmpty()) {
return;
}

Set<UUID> alreadyCollectedNodes = buildInfos.getReportsInfos().stream()
.map(ReportInfos::nodeUuid)
.collect(Collectors.toSet());

parentReports.entrySet().stream()
.filter(entry -> !alreadyCollectedNodes.contains(entry.getKey()))
.forEach(entry -> buildInfos.addInheritedReport(
entry.getKey(),
entry.getValue()
));
}

/**
* Recursively collects build information by traversing up the node tree.
* Stops at the first built parent and inherits its reports.
*
* @param nodeEntity Current node being processed
* @param rootNetworkUuid Root network context
* @param buildInfos Accumulator for build information
* @param nodeToBuildUuid The target node being built
*/
private void getBuildInfos(NodeEntity nodeEntity, UUID rootNetworkUuid, BuildInfos buildInfos, UUID nodeToBuildUuid) {
AbstractNode node = getSimpleNode(nodeEntity.getIdNode());
if (node.getType() == NodeType.NETWORK_MODIFICATION) {
NetworkModificationNode modificationNode = (NetworkModificationNode) node;
RootNetworkNodeInfoEntity rootNetworkNodeInfoEntity = rootNetworkNodeInfoService.getRootNetworkNodeInfo(nodeEntity.getIdNode(), rootNetworkUuid).orElseThrow(() -> new StudyException(ROOT_NETWORK_NOT_FOUND));
if (!rootNetworkNodeInfoEntity.getNodeBuildStatus().toDto().isBuilt()) {
UUID reportUuid = getModificationReportUuid(nodeEntity.getIdNode(), rootNetworkUuid, nodeToBuildUuid);
buildInfos.insertModificationInfos(modificationNode.getModificationGroupUuid(), rootNetworkNodeInfoEntity.getModificationsUuidsToExclude(), new ReportInfos(reportUuid, modificationNode.getId()));
ReportInfos reportInfos = new ReportInfos(reportUuid, modificationNode.getId(), ReportMode.REPLACE);
buildInfos.insertModificationInfos(modificationNode.getModificationGroupUuid(), rootNetworkNodeInfoEntity.getModificationsUuidsToExclude(), reportInfos);
getBuildInfos(nodeEntity.getParentNode(), rootNetworkUuid, buildInfos, nodeToBuildUuid);
} else {
buildInfos.setOriginVariantId(self.getVariantId(nodeEntity.getIdNode(), rootNetworkUuid));
inheritModificationReportsFromBuiltParent(nodeEntity.getIdNode(), rootNetworkUuid, buildInfos);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ public InvalidateNodeInfos invalidateRootNetworkNode(RootNetworkNodeInfoEntity r
InvalidateNodeInfos invalidateNodeInfos = getInvalidationComputationInfos(rootNetworkNodeInfoEntity, invalidateTreeParameters.computationsInvalidationMode());

if (notOnlyChildrenBuildStatus) {
rootNetworkNodeInfoEntity.getModificationReports().forEach((key, value) -> invalidateNodeInfos.addReportUuid(value));
collectDeletableReports(rootNetworkNodeInfoEntity, invalidateNodeInfos);
invalidateNodeInfos.addVariantId(rootNetworkNodeInfoEntity.getVariantId());
invalidateBuildStatus(rootNetworkNodeInfoEntity, invalidateNodeInfos);
}
Expand All @@ -265,10 +265,84 @@ public InvalidateNodeInfos invalidateRootNetworkNode(RootNetworkNodeInfoEntity r
return invalidateNodeInfos;
}

/**
* Finds an existing report UUID for a node by searching across all nodes
* in the same root network.
*
* @param targetNodeUuid the node to find a report for
* @param rootNetworkUuid the root network context
* @return Optional containing the report UUID if found anywhere, empty otherwise
*/
@Transactional(readOnly = true)
public Optional<UUID> findExistingReportUuidForNode(UUID targetNodeUuid, UUID rootNetworkUuid) {
Set<UUID> reportUuids = rootNetworkNodeInfoRepository.findReportUuidsForNodeInRootNetwork(targetNodeUuid, rootNetworkUuid);
if (reportUuids.isEmpty()) {
return Optional.empty();
}
// We assume that there is only one report UUID for a given node across all nodes in the same root network,
// so we return the first one found
return Optional.of(reportUuids.iterator().next());
}

/**
* Identifies and collects report UUIDs that can be safely deleted.
* <p>
* Only reports that are exclusively owned by the specified entity
* will be marked for deletion. Reports shared with other nodes are preserved.
*
* @param rootNetworkNodeInfo The entity being invalidated
* @param invalidateNodeInfos Accumulator for deletion candidates
*/
private void collectDeletableReports(
RootNetworkNodeInfoEntity rootNetworkNodeInfo,
InvalidateNodeInfos invalidateNodeInfos) {

Map<UUID, UUID> nodeReports = rootNetworkNodeInfo.getModificationReports();

if (nodeReports == null || nodeReports.isEmpty()) {
return;
}

Set<UUID> candidateReportUuids = new HashSet<>(nodeReports.values());

Set<UUID> safeToDelete = filterForExclusivelyOwnedReports(
candidateReportUuids,
rootNetworkNodeInfo.getId()
);

safeToDelete.forEach(invalidateNodeInfos::addReportUuid);
}

/**
* Filters report UUIDs to only include those not referenced by other nodes.
* <p>
* This safety mechanism prevents cascade deletion of shared reports.
* Uses a database query to check all report references in a single round trip.
*
* @param candidateReportUuids Report UUIDs being considered for deletion
* @param ownerEntityId ID of the entity being processed
* @return Set of reports that can be safely deleted (not referenced elsewhere)
*/
private Set<UUID> filterForExclusivelyOwnedReports(
Set<UUID> candidateReportUuids,
UUID ownerEntityId) {

if (candidateReportUuids.isEmpty()) {
return Collections.emptySet();
}

Set<UUID> sharedReports = rootNetworkNodeInfoRepository
.findReferencedReportUuidsExcludingEntity(candidateReportUuids, ownerEntityId);

return candidateReportUuids.stream()
.filter(reportUuid -> !sharedReports.contains(reportUuid))
.collect(Collectors.toSet());
}

private static void invalidateBuildStatus(RootNetworkNodeInfoEntity rootNetworkNodeInfoEntity, InvalidateNodeInfos invalidateNodeInfos) {
rootNetworkNodeInfoEntity.setNodeBuildStatus(NodeBuildStatusEmbeddable.from(BuildStatus.NOT_BUILT));
rootNetworkNodeInfoEntity.setVariantId(UUID.randomUUID().toString());
rootNetworkNodeInfoEntity.setModificationReports(new HashMap<>(Map.of(rootNetworkNodeInfoEntity.getNodeInfo().getId(), UUID.randomUUID())));
rootNetworkNodeInfoEntity.setModificationReports(new HashMap<>());

invalidateNodeInfos.addNodeUuid(rootNetworkNodeInfoEntity.getNodeInfo().getIdNode());
}
Expand Down
Loading