-
Notifications
You must be signed in to change notification settings - Fork 0
Apply network modifications asynchronously #816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ce3adf7
34c3869
878901a
3d5621e
3155bd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,6 @@ | |
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import java.util.*; | ||
| import java.util.concurrent.CompletableFuture; | ||
|
|
||
| /** | ||
| * @author Franck Lecuyer <franck.lecuyer at rte-france.com> | ||
|
|
@@ -95,37 +94,24 @@ public ResponseEntity<Map<UUID, UUID>> duplicateGroup(@RequestParam("groupUuid") | |
| @PutMapping(value = "/groups/{groupUuid}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) | ||
| @Operation(summary = "For a list of network modifications passed in body, Move them before another one or at the end of the list, or Duplicate them at the end of the list") | ||
| @ApiResponse(responseCode = "200", description = "The modification list of the group has been updated.") | ||
| public CompletableFuture<ResponseEntity<NetworkModificationsResult>> handleNetworkModifications( | ||
| public ResponseEntity<List<UUID>> handleNetworkModifications( | ||
| @Parameter(description = "updated group UUID, where modifications are pasted") @PathVariable("groupUuid") UUID targetGroupUuid, | ||
| @Parameter(description = "kind of modification", required = true) @RequestParam(value = "action") GroupModificationAction action, | ||
| @Parameter(description = "the modification Uuid to move before (MOVE option, empty means moving at the end)") @RequestParam(value = "before", required = false) UUID beforeModificationUuid, | ||
| @Parameter(description = "origin group UUID, where modifications are copied or cut") @RequestParam(value = "originGroupUuid", required = false) UUID originGroupUuid, | ||
| @Parameter(description = "modifications can be applied (default is true)") @RequestParam(value = "build", required = false, defaultValue = "true") Boolean canApply, | ||
| @Parameter(description = "Receiver for async apply notification") @RequestParam(value = "receiver", required = false) String receiver, | ||
| @RequestBody Pair<List<UUID>, List<ModificationApplicationContext>> modificationContextInfos) { | ||
| return switch (action) { | ||
| case COPY -> | ||
| networkModificationService.duplicateModifications( | ||
| targetGroupUuid, | ||
| originGroupUuid, | ||
| modificationContextInfos.getFirst(), | ||
| modificationContextInfos.getSecond() | ||
| ).thenApply(ResponseEntity.ok()::body); | ||
| case MOVE -> { | ||
| UUID sourceGroupUuid = originGroupUuid == null ? targetGroupUuid : originGroupUuid; | ||
| boolean applyModifications = canApply; | ||
| if (sourceGroupUuid.equals(targetGroupUuid)) { | ||
| applyModifications = false; | ||
| } | ||
| yield networkModificationService.moveModifications( | ||
| targetGroupUuid, | ||
| sourceGroupUuid, | ||
| beforeModificationUuid, | ||
| modificationContextInfos.getFirst(), | ||
| modificationContextInfos.getSecond(), | ||
| applyModifications | ||
| ).thenApply(ResponseEntity.ok()::body); | ||
| } | ||
| UUID sourceGroupUuid = originGroupUuid == null ? targetGroupUuid : originGroupUuid; | ||
| List<UUID> savedUuids = switch (action) { | ||
| case COPY -> networkModificationService.saveDuplicateModifications( | ||
| targetGroupUuid, originGroupUuid, modificationContextInfos.getFirst()); | ||
| case MOVE -> networkModificationService.saveMoveModifications( | ||
| targetGroupUuid, sourceGroupUuid, beforeModificationUuid, modificationContextInfos.getFirst()); | ||
| }; | ||
| if (receiver != null) { | ||
| networkModificationService.applicationRequest(targetGroupUuid, savedUuids, modificationContextInfos.getSecond(), receiver); | ||
| } | ||
|
Comment on lines
+111
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Save and publish are non-atomic; publish failure can cause duplicate persisted modifications on retries. Both endpoints persist first, then call broker publish. If publish fails after save, the request can fail while data is already committed, and client retries can duplicate modifications. This should be hardened with an outbox/idempotency strategy (or explicit partial-success contract). Also applies to: 137-139 🤖 Prompt for AI Agents |
||
| return ResponseEntity.ok(savedUuids); | ||
| } | ||
|
|
||
| @DeleteMapping(value = "/groups/{groupUuid}") | ||
|
|
@@ -142,11 +128,16 @@ public ResponseEntity<Void> deleteModificationGroup(@Parameter(description = "Gr | |
| @ApiResponses(value = { | ||
| @ApiResponse(responseCode = "200", description = "The network modification was created"), | ||
| @ApiResponse(responseCode = "404", description = "The network or equipment was not found")}) | ||
| public CompletableFuture<ResponseEntity<NetworkModificationsResult>> createNetworkModification( | ||
| public ResponseEntity<List<UUID>> createNetworkModification( | ||
| @Parameter(description = "Group UUID") @RequestParam(name = "groupUuid") UUID groupUuid, | ||
| @Parameter(description = "Receiver for async apply notification") @RequestParam(name = "receiver", required = false) String receiver, | ||
| @RequestBody Pair<ModificationInfos, List<ModificationApplicationContext>> modificationContextInfos) { | ||
| modificationContextInfos.getFirst().check(); | ||
| return networkModificationService.createNetworkModification(groupUuid, modificationContextInfos.getFirst(), modificationContextInfos.getSecond()).thenApply(ResponseEntity.ok()::body); | ||
| List<UUID> savedUuids = networkModificationService.saveNetworkModification(groupUuid, modificationContextInfos.getFirst()); | ||
| if (receiver != null) { | ||
| networkModificationService.applicationRequest(groupUuid, savedUuids, modificationContextInfos.getSecond(), receiver); | ||
| } | ||
| return ResponseEntity.ok(savedUuids); | ||
| } | ||
|
|
||
| @PutMapping(value = "/network-modifications/{uuid}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /** | ||
| * 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.modification.server.service; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.core.type.TypeReference; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.powsybl.commons.PowsyblException; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import org.gridsuite.modification.server.dto.ModificationApplicationContext; | ||
| import org.springframework.messaging.Message; | ||
| import org.springframework.messaging.MessageHeaders; | ||
| import org.springframework.messaging.support.MessageBuilder; | ||
|
|
||
| import java.io.UncheckedIOException; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
|
|
||
| import static org.gridsuite.modification.server.service.NotificationService.RECEIVER_HEADER; | ||
|
|
||
| /** | ||
| * @author Ayoub LABIDI <ayoub.labidi_externe at rte-france.com> | ||
| */ | ||
| @Getter | ||
| @AllArgsConstructor | ||
| public class ApplicationExecutionContext { | ||
|
|
||
| private static final String GROUP_UUID_HEADER = "groupUuid"; | ||
| private static final String MODIFICATION_UUIDS_HEADER = "modificationUuids"; | ||
|
|
||
| private final UUID groupUuid; | ||
| private final List<UUID> modificationUuids; | ||
| private final List<ModificationApplicationContext> applicationContexts; | ||
| private final String receiver; | ||
|
|
||
| private static String getNonNullHeader(MessageHeaders headers, String name) { | ||
| String header = (String) headers.get(name); | ||
| if (header == null) { | ||
| throw new PowsyblException("Header '" + name + "' not found"); | ||
| } | ||
| return header; | ||
| } | ||
|
|
||
| public static ApplicationExecutionContext fromMessage(Message<String> message, ObjectMapper objectMapper) { | ||
| MessageHeaders headers = message.getHeaders(); | ||
| UUID groupUuid = UUID.fromString(getNonNullHeader(headers, GROUP_UUID_HEADER)); | ||
| String receiver = getNonNullHeader(headers, RECEIVER_HEADER); | ||
| try { | ||
| List<UUID> modificationUuids = objectMapper.readValue( | ||
| getNonNullHeader(headers, MODIFICATION_UUIDS_HEADER), | ||
| new TypeReference<>() { }); | ||
| List<ModificationApplicationContext> applicationContexts = objectMapper.readValue( | ||
| message.getPayload(), | ||
| new TypeReference<>() { }); | ||
| return new ApplicationExecutionContext(groupUuid, modificationUuids, applicationContexts, receiver); | ||
| } catch (JsonProcessingException e) { | ||
| throw new UncheckedIOException(e); | ||
| } | ||
| } | ||
|
|
||
| public Message<String> toMessage(ObjectMapper objectMapper) { | ||
| try { | ||
| String contextsJson = objectMapper.writeValueAsString(applicationContexts); | ||
| String uuidsJson = objectMapper.writeValueAsString(modificationUuids); | ||
| return MessageBuilder.withPayload(contextsJson) | ||
| .setHeader(GROUP_UUID_HEADER, groupUuid.toString()) | ||
| .setHeader(MODIFICATION_UUIDS_HEADER, uuidsJson) | ||
| .setHeader(RECEIVER_HEADER, receiver) | ||
| .build(); | ||
| } catch (JsonProcessingException e) { | ||
| throw new UncheckedIOException(e); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /** | ||
| * 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.modification.server.service; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import lombok.NonNull; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.gridsuite.modification.server.dto.NetworkModificationsResult; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.messaging.Message; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.function.Consumer; | ||
|
|
||
| /** | ||
| * @author Ayoub LABIDI <ayoub.labidi_externe at rte-france.com> | ||
| */ | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class ModificationApplicationWorkerService { | ||
|
|
||
| @NonNull private final NetworkModificationService networkModificationService; | ||
| @NonNull private final NotificationService notificationService; | ||
| @NonNull private final ObjectMapper objectMapper; | ||
|
|
||
| @Bean | ||
| public Consumer<Message<String>> consumeApplication() { | ||
| return message -> { | ||
| ApplicationExecutionContext ctx = ApplicationExecutionContext.fromMessage(message, objectMapper); | ||
| notificationService.emitApplicationResultMessage( | ||
| new NetworkModificationsResult( | ||
| ctx.getModificationUuids(), | ||
| networkModificationService.applyModificationsByUuids( | ||
| ctx.getGroupUuid(), ctx.getModificationUuids(), ctx.getApplicationContexts())), | ||
| ctx.getReceiver()); | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the resolved source group UUID in the COPY branch.
Line 104 computes a fallback
sourceGroupUuidfor missingoriginGroupUuid, but Line 107 passesoriginGroupUuiddirectly. This makes COPY behavior inconsistent with MOVE and can break same-group copies whenoriginGroupUuidis omitted.Suggested fix
List<UUID> savedUuids = switch (action) { case COPY -> networkModificationService.saveDuplicateModifications( - targetGroupUuid, originGroupUuid, modificationContextInfos.getFirst()); + targetGroupUuid, sourceGroupUuid, modificationContextInfos.getFirst()); case MOVE -> networkModificationService.saveMoveModifications( targetGroupUuid, sourceGroupUuid, beforeModificationUuid, modificationContextInfos.getFirst()); };📝 Committable suggestion
🤖 Prompt for AI Agents