Skip to content

Commit cfc3988

Browse files
authored
delete orphaned indexed equipments (#571)
Delete orphaned indexed equipments Signed-off-by: achour94 <[email protected]>
1 parent 152f28f commit cfc3988

File tree

6 files changed

+191
-4
lines changed

6 files changed

+191
-4
lines changed

src/main/java/org/gridsuite/study/server/SupervisionController.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1212
import io.swagger.v3.oas.annotations.responses.ApiResponses;
1313
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import org.gridsuite.study.server.service.StudyService;
1415
import org.gridsuite.study.server.service.SupervisionService;
1516

17+
import java.util.List;
1618
import java.util.UUID;
1719

1820
import org.gridsuite.study.server.dto.ComputationType;
@@ -42,8 +44,11 @@ public class SupervisionController {
4244

4345
private final SupervisionService supervisionService;
4446

45-
public SupervisionController(SupervisionService supervisionService) {
47+
private final StudyService studyService;
48+
49+
public SupervisionController(SupervisionService supervisionService, StudyService studyService) {
4650
this.supervisionService = supervisionService;
51+
this.studyService = studyService;
4752
}
4853

4954
@DeleteMapping(value = "/computation/results")
@@ -96,6 +101,21 @@ public ResponseEntity<Long> deleteStudyIndexedEquipmentsAndTombstoned(@PathVaria
96101
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(supervisionService.deleteStudyIndexedEquipmentsAndTombstoned(studyUuid));
97102
}
98103

104+
@GetMapping(value = "/orphan_indexed_network_uuids")
105+
@Operation(summary = "Get all orphan indexed equipments network uuids")
106+
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The list of orphan indexed equipments network uuids")})
107+
public ResponseEntity<List<UUID>> getOrphanIndexedEquipments() {
108+
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getAllOrphanIndexedEquipmentsNetworkUuids());
109+
}
110+
111+
@DeleteMapping(value = "/studies/{networkUuid}/indexed-equipments-by-network-uuid")
112+
@Operation(summary = "delete indexed equipments and tombstoned equipments for the given networkUuid")
113+
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "all indexed equipments and tombstoned equipments for the given networkUuid have been deleted")})
114+
public ResponseEntity<Long> deleteNetworkUuidIndexedEquipmentsAndTombstoned(@PathVariable("networkUuid") UUID networkUuid) {
115+
studyService.deleteEquipmentIndexes(networkUuid);
116+
return ResponseEntity.ok().build();
117+
}
118+
99119
@DeleteMapping(value = "/studies/{studyUuid}/nodes/builds")
100120
@Operation(summary = "Invalidate node builds for the given study")
101121
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "all built nodes for the given study have been invalidated")})

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import io.swagger.v3.oas.annotations.media.Schema;
1010
import lombok.*;
11+
import lombok.experimental.FieldNameConstants;
1112
import lombok.experimental.SuperBuilder;
1213
import org.springframework.data.annotation.AccessType;
1314
import org.springframework.data.annotation.Id;
@@ -27,6 +28,7 @@
2728
@Setter
2829
@ToString
2930
@EqualsAndHashCode
31+
@FieldNameConstants
3032
@Schema(description = "Basic equipment infos")
3133
public class BasicEquipmentInfos {
3234
@Id

src/main/java/org/gridsuite/study/server/elasticsearch/EquipmentInfosService.java

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@
77
package org.gridsuite.study.server.elasticsearch;
88

99
import co.elastic.clients.elasticsearch._types.FieldValue;
10+
import co.elastic.clients.elasticsearch._types.aggregations.*;
11+
import co.elastic.clients.elasticsearch._types.aggregations.Aggregation;
1012
import co.elastic.clients.elasticsearch._types.query_dsl.*;
1113

1214
import com.powsybl.iidm.network.VariantManagerConstants;
1315
import org.apache.commons.lang3.StringUtils;
16+
import org.apache.commons.lang3.tuple.Pair;
17+
import org.gridsuite.study.server.dto.BasicEquipmentInfos;
1418
import org.gridsuite.study.server.dto.EquipmentInfos;
1519
import org.gridsuite.study.server.dto.TombstonedEquipmentInfos;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
1622
import org.springframework.data.domain.PageRequest;
17-
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
18-
import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder;
19-
import org.springframework.data.elasticsearch.client.elc.Queries;
23+
import org.springframework.data.elasticsearch.client.elc.*;
2024
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
2125
import org.springframework.data.elasticsearch.core.SearchHit;
26+
import org.springframework.data.elasticsearch.core.SearchHits;
2227
import org.springframework.lang.NonNull;
2328
import org.springframework.stereotype.Service;
2429

@@ -42,6 +47,8 @@ public enum FieldSelector {
4247

4348
private static final int PAGE_MAX_SIZE = 400;
4449

50+
private static final int COMPOSITE_AGGREGATION_BATCH_SIZE = 1000;
51+
4552
public static final Map<String, Integer> EQUIPMENT_TYPE_SCORES = Map.ofEntries(
4653
entry("SUBSTATION", 15),
4754
entry("VOLTAGE_LEVEL", 14),
@@ -73,6 +80,8 @@ public enum FieldSelector {
7380

7481
private final ElasticsearchOperations elasticsearchOperations;
7582

83+
private static final Logger LOGGER = LoggerFactory.getLogger(EquipmentInfosService.class);
84+
7685
public EquipmentInfosService(EquipmentInfosRepository equipmentInfosRepository, TombstonedEquipmentInfosRepository tombstonedEquipmentInfosRepository, ElasticsearchOperations elasticsearchOperations) {
7786
this.equipmentInfosRepository = equipmentInfosRepository;
7887
this.tombstonedEquipmentInfosRepository = tombstonedEquipmentInfosRepository;
@@ -106,6 +115,101 @@ public long getEquipmentInfosCount() {
106115
return equipmentInfosRepository.count();
107116
}
108117

118+
private CompositeAggregation buildCompositeAggregation(String field, Map<String, FieldValue> afterKey) {
119+
List<Map<String, CompositeAggregationSource>> sources = List.of(
120+
Map.of(field, CompositeAggregationSource.of(s -> s.terms(t -> t.field(field + ".keyword")))
121+
)
122+
);
123+
124+
CompositeAggregation.Builder compositeAggregationBuilder = new CompositeAggregation.Builder()
125+
.size(COMPOSITE_AGGREGATION_BATCH_SIZE)
126+
.sources(sources);
127+
128+
if (afterKey != null) {
129+
compositeAggregationBuilder.after(afterKey);
130+
}
131+
132+
return compositeAggregationBuilder.build();
133+
}
134+
135+
/**
136+
* Constructs a NativeQuery with a composite aggregation.
137+
*
138+
* @param compositeName The name of the composite aggregation.
139+
* @param compositeAggregation The composite aggregation configuration.
140+
* @return A NativeQuery object configured with the specified composite aggregation.
141+
*/
142+
private NativeQuery buildCompositeAggregationQuery(String compositeName, CompositeAggregation compositeAggregation) {
143+
Aggregation aggregation = Aggregation.of(a -> a.composite(compositeAggregation));
144+
145+
return new NativeQueryBuilder()
146+
.withAggregation(compositeName, aggregation)
147+
.build();
148+
}
149+
150+
/**
151+
* This method is used to extract the results of a composite aggregation from Elasticsearch search hits.
152+
*
153+
* @param searchHits The search hits returned from an Elasticsearch query.
154+
* @param compositeName The name of the composite aggregation.
155+
* @return A Pair consisting of two elements:
156+
* The left element of the Pair is a list of maps, where each map represents a bucket's key. Each bucket is a result of the composite aggregation.
157+
* The right element of the Pair is the afterKey map, which is used for pagination in Elasticsearch.
158+
* If there are no more pages, the afterKey will be null.
159+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html">Elasticsearch Composite Aggregation Documentation</a>
160+
*/
161+
private Pair<List<Map<String, FieldValue>>, Map<String, FieldValue>> extractCompositeAggregationResults(SearchHits<EquipmentInfos> searchHits, String compositeName) {
162+
ElasticsearchAggregations aggregations = (ElasticsearchAggregations) searchHits.getAggregations();
163+
164+
List<Map<String, FieldValue>> results = new ArrayList<>();
165+
if (aggregations != null) {
166+
Map<String, ElasticsearchAggregation> aggregationList = aggregations.aggregationsAsMap();
167+
if (!aggregationList.isEmpty()) {
168+
Aggregate aggregate = aggregationList.get(compositeName).aggregation().getAggregate();
169+
if (aggregate.isComposite() && aggregate.composite() != null) {
170+
for (CompositeBucket bucket : aggregate.composite().buckets().array()) {
171+
Map<String, FieldValue> key = bucket.key();
172+
results.add(key);
173+
}
174+
return Pair.of(results, aggregate.composite().afterKey());
175+
}
176+
}
177+
}
178+
return Pair.of(results, null);
179+
}
180+
181+
public List<UUID> getEquipmentInfosDistinctNetworkUuids() {
182+
List<UUID> networkUuids = new ArrayList<>();
183+
Map<String, FieldValue> afterKey = null;
184+
String compositeName = "composite_agg";
185+
String networkUuidField = BasicEquipmentInfos.Fields.networkUuid;
186+
187+
do {
188+
CompositeAggregation compositeAggregation = buildCompositeAggregation(networkUuidField, afterKey);
189+
NativeQuery query = buildCompositeAggregationQuery(compositeName, compositeAggregation);
190+
191+
SearchHits<EquipmentInfos> searchHits = elasticsearchOperations.search(query, EquipmentInfos.class);
192+
Pair<List<Map<String, FieldValue>>, Map<String, FieldValue>> searchResults = extractCompositeAggregationResults(searchHits, compositeName);
193+
194+
searchResults.getLeft().stream()
195+
.map(result -> result.get(networkUuidField))
196+
.filter(Objects::nonNull)
197+
.map(FieldValue::stringValue)
198+
.map(UUID::fromString)
199+
.forEach(networkUuids::add);
200+
201+
afterKey = searchResults.getRight();
202+
} while (afterKey != null && !afterKey.isEmpty());
203+
204+
return networkUuids;
205+
}
206+
207+
public List<UUID> getOrphanEquipmentInfosNetworkUuids(List<UUID> networkUuidsInDatabase) {
208+
List<UUID> networkUuids = getEquipmentInfosDistinctNetworkUuids();
209+
networkUuids.removeAll(networkUuidsInDatabase);
210+
return networkUuids;
211+
}
212+
109213
public long getTombstonedEquipmentInfosCount() {
110214
return tombstonedEquipmentInfosRepository.count();
111215
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,16 @@ public List<CreatedStudyBasicInfos> getStudies() {
238238
.collect(Collectors.toList());
239239
}
240240

241+
public List<UUID> getStudiesNetworkUuids() {
242+
return studyRepository.findAll().stream()
243+
.map(StudyEntity::getNetworkUuid)
244+
.toList();
245+
}
246+
247+
public List<UUID> getAllOrphanIndexedEquipmentsNetworkUuids() {
248+
return equipmentInfosService.getOrphanEquipmentInfosNetworkUuids(getStudiesNetworkUuids());
249+
}
250+
241251
public String getStudyCaseName(UUID studyUuid) {
242252
Objects.requireNonNull(studyUuid);
243253
StudyEntity study = studyRepository.findById(studyUuid).orElseThrow(() -> new StudyException(STUDY_NOT_FOUND));

src/test/java/org/gridsuite/study/server/EquipmentInfosServiceTests.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,38 @@ public void testDeleteAllEquipmentInfos() {
186186
assertEquals(0, equipmentInfosService.getEquipmentInfosCount());
187187
}
188188

189+
@Test
190+
public void testGetOrphanEquipmentInfosNetworkUuids() {
191+
// index some equipment infos as orphan
192+
UUID orphanNetworkUuid = UUID.randomUUID();
193+
EquipmentInfos orphanLoadInfos = EquipmentInfos.builder().networkUuid(orphanNetworkUuid).id("id").name("name").type("LOAD").voltageLevels(Set.of(VoltageLevelInfos.builder().id("vl").name("vl").build())).build();
194+
UUID orphanNetworkUuid2 = UUID.randomUUID();
195+
EquipmentInfos orphanVlInfos = EquipmentInfos.builder().networkUuid(orphanNetworkUuid2).id("id").name("name").type("VOLTAGE_LEVEL").voltageLevels(Set.of(VoltageLevelInfos.builder().id("vl").name("vl").build())).build();
196+
197+
// index an equipment infos for the existing network
198+
EquipmentInfos loadInfos = EquipmentInfos.builder().networkUuid(NETWORK_UUID).id("id2").name("name2").type("LOAD").voltageLevels(Set.of(VoltageLevelInfos.builder().id("vl").name("vl").build())).build();
199+
200+
equipmentInfosService.addEquipmentInfos(loadInfos);
201+
equipmentInfosService.addEquipmentInfos(orphanLoadInfos);
202+
equipmentInfosService.addEquipmentInfos(orphanVlInfos);
203+
204+
// get the orphan network uuids
205+
List<UUID> orphanNetworkUuids = equipmentInfosService.getOrphanEquipmentInfosNetworkUuids(List.of(NETWORK_UUID));
206+
207+
// check that the orphan network uuids are returned
208+
assertEquals(2, orphanNetworkUuids.size());
209+
assertTrue(orphanNetworkUuids.contains(orphanNetworkUuid));
210+
assertTrue(orphanNetworkUuids.contains(orphanNetworkUuid2));
211+
212+
// delete the orphan equipment infos
213+
equipmentInfosService.deleteAllByNetworkUuid(orphanNetworkUuid);
214+
equipmentInfosService.deleteAllByNetworkUuid(orphanNetworkUuid2);
215+
216+
// check that the orphan network uuids are not returned anymore
217+
orphanNetworkUuids = equipmentInfosService.getOrphanEquipmentInfosNetworkUuids(List.of(NETWORK_UUID));
218+
assertEquals(0, orphanNetworkUuids.size());
219+
}
220+
189221
@Test
190222
public void testCloneVariant() {
191223
equipmentInfosService.addEquipmentInfos(EquipmentInfos.builder().networkUuid(NETWORK_UUID).id("id1").name("name1").type(IdentifiableType.LOAD.name()).variantId("variant1").voltageLevels(Set.of(VoltageLevelInfos.builder().id("vl1").name("vl1").build())).build());

src/test/java/org/gridsuite/study/server/StudyTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ public class StudyTest {
182182

183183
private static final String DEFAULT_PROVIDER = "defaultProvider";
184184

185+
private static final List<UUID> ORPHAN_NETWORK_UUIDS = List.of(
186+
UUID.fromString("88888888-7777-0000-abcd-000000000000"),
187+
UUID.fromString("123e4567-e89b-12d3-a456-426614174000")
188+
);
189+
185190
@Value("${non-evacuated-energy.default-provider}")
186191
String defaultNonEvacuatedEnergyProvider;
187192

@@ -298,6 +303,8 @@ private void initMockBeans(Network network) {
298303
when(networkStoreService.getNetwork(NETWORK_UUID)).thenReturn(network);
299304

300305
doNothing().when(networkStoreService).deleteNetwork(NETWORK_UUID);
306+
307+
when(equipmentInfosService.getOrphanEquipmentInfosNetworkUuids(List.of(NETWORK_UUID))).thenReturn(ORPHAN_NETWORK_UUIDS);
301308
}
302309

303310
private void initMockBeansNetworkNotExisting(Network notExistingNetwork) {
@@ -2614,6 +2621,18 @@ public void testSupervision() throws Exception {
26142621
buildStatusMessage = output.receive(TIMEOUT, studyUpdateDestination);
26152622
assertEquals(studyUuid, buildStatusMessage.getHeaders().get(NotificationService.HEADER_STUDY_UUID));
26162623
assertEquals(NotificationService.NODE_BUILD_STATUS_UPDATED, buildStatusMessage.getHeaders().get(HEADER_UPDATE_TYPE));
2624+
2625+
// Test get orphan indexed equipments
2626+
mvcResult = mockMvc.perform(get("/v1/supervision/orphan_indexed_network_uuids"))
2627+
.andExpect(status().isOk())
2628+
.andReturn();
2629+
2630+
List<UUID> orphanIndexedEquipments = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { });
2631+
assertEquals(ORPHAN_NETWORK_UUIDS, orphanIndexedEquipments);
2632+
2633+
// test delete orphan indexed equipments
2634+
mockMvc.perform(delete("/v1/supervision/studies/{networkUuid}/indexed-equipments-by-network-uuid", ORPHAN_NETWORK_UUIDS.get(0)))
2635+
.andExpect(status().isOk());
26172636
}
26182637

26192638
@After

0 commit comments

Comments
 (0)