diff --git a/afs-cassandra/src/main/java/com/powsybl/afs/cassandra/CassandraAppStorage.java b/afs-cassandra/src/main/java/com/powsybl/afs/cassandra/CassandraAppStorage.java index 9d8c9397..e36d37d9 100644 --- a/afs-cassandra/src/main/java/com/powsybl/afs/cassandra/CassandraAppStorage.java +++ b/afs-cassandra/src/main/java/com/powsybl/afs/cassandra/CassandraAppStorage.java @@ -48,6 +48,8 @@ public class CassandraAppStorage extends AbstractAppStorage { public static final String REF_NOT_FOUND = "REFERENCE_NOT_FOUND"; + public static final String ORPHAN_NODE = "ORPHAN_NODE"; + private final String fileSystemName; private final Supplier contextSupplier; @@ -1489,7 +1491,7 @@ public void close() { @Override public List getSupportedFileSystemChecks() { - return ImmutableList.of(FileSystemCheckOptions.EXPIRED_INCONSISTENT_NODES, REF_NOT_FOUND); + return ImmutableList.of(FileSystemCheckOptions.EXPIRED_INCONSISTENT_NODES, REF_NOT_FOUND, ORPHAN_NODE); } @Override @@ -1505,6 +1507,9 @@ public List checkFileSystem(FileSystemCheckOptions options case REF_NOT_FOUND: checkReferenceNotFound(results, options); break; + case ORPHAN_NODE: + checkOrphanNode(results, options); + break; default: LOGGER.warn("Check {} not supported in {}", type, getClass()); } @@ -1513,6 +1518,38 @@ public List checkFileSystem(FileSystemCheckOptions options return results; } + private void checkOrphanNode(List results, FileSystemCheckOptions options) { + // get all child id which parent name is null + ResultSet resultSet = getSession().execute(select(ID, CHILD_ID, NAME, CHILD_NAME).from(CHILDREN_BY_NAME_AND_CLASS)); + List orphanIds = new ArrayList<>(); + Set fakeParentIds = new HashSet<>(); + for (Row row : resultSet) { + if (row.getString(NAME) == null) { + UUID nodeId = row.getUUID(CHILD_ID); + String nodeName = row.getString(CHILD_NAME); + UUID fakeParentId = row.getUUID(ID); + FileSystemCheckIssue issue = new FileSystemCheckIssue().setNodeId(nodeId.toString()) + .setNodeName(nodeName) + .setType(ORPHAN_NODE) + .setDescription(nodeName + "(" + nodeId + ") is an orphan node. Its fake parent id=" + fakeParentId); + if (options.isRepair()) { + orphanIds.add(nodeId); + fakeParentIds.add(fakeParentId); + issue.setRepaired(true); + issue.setResolutionDescription("Deleted node [name=" + nodeName + ", id=" + nodeId + "] and reference to null name node [id=" + fakeParentId + "]"); + } + results.add(issue); + } + } + if (options.isRepair()) { + orphanIds.forEach(this::deleteNode); + for (UUID fakeParentId : fakeParentIds) { + getSession().execute(delete().from(CHILDREN_BY_NAME_AND_CLASS) + .where(eq(ID, fakeParentId))); + } + } + } + private void checkReferenceNotFound(List results, FileSystemCheckOptions options) { List statements = new ArrayList<>(); Set notFoundIds = new HashSet<>(); diff --git a/afs-cassandra/src/test/java/com/powsybl/afs/cassandra/CassandraAppStorageTest.java b/afs-cassandra/src/test/java/com/powsybl/afs/cassandra/CassandraAppStorageTest.java index c9e0c289..5f5fc417 100644 --- a/afs-cassandra/src/test/java/com/powsybl/afs/cassandra/CassandraAppStorageTest.java +++ b/afs-cassandra/src/test/java/com/powsybl/afs/cassandra/CassandraAppStorageTest.java @@ -16,6 +16,9 @@ import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; import org.junit.Rule; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collections; @@ -26,7 +29,8 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.insertInto; import static com.powsybl.afs.cassandra.CassandraConstants.*; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assert.fail; import static org.junit.Assert.*; /** @@ -50,6 +54,35 @@ protected void nextDependentTests() { testInconsistendNodeRepair(); testAbsentChildRepair(); testGetParentWithInconsistentChild(); + testOrphanNodeRepair(); + } + + private void testOrphanNodeRepair() { + NodeInfo orphanNode = storage.createNode(UUIDs.timeBased().toString(), "orphanNodes", FOLDER_PSEUDO_CLASS, "", 0, new NodeGenericMetadata()); + storage.setConsistent(orphanNode.getId()); + try (OutputStream os = storage.writeBinaryData(orphanNode.getId(), "blob")) { + os.write("word2".getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + fail(); + } + storage.flush(); + NodeInfo orphanChild = storage.createNode(orphanNode.getId(), "orphanChild", FOLDER_PSEUDO_CLASS, "", 0, new NodeGenericMetadata()); + storage.setConsistent(orphanChild.getId()); + FileSystemCheckOptions repairOption = new FileSystemCheckOptionsBuilder() + .addCheckTypes(CassandraAppStorage.ORPHAN_NODE) + .repair().build(); + List issues = storage.checkFileSystem(repairOption); + assertThat(issues).hasOnlyOneElementSatisfying(i -> assertEquals(orphanNode.getId(), i.getNodeId())); + assertThatThrownBy(() -> storage.getNodeInfo(orphanNode.getId())) + .isInstanceOf(CassandraAfsException.class) + .hasMessageContaining("not found"); + assertThatThrownBy(() -> storage.getParentNode(orphanNode.getId())) + .isInstanceOf(CassandraAfsException.class) + .hasMessageContaining("not found"); + assertThatThrownBy(() -> storage.getNodeInfo(orphanChild.getId())) + .isInstanceOf(CassandraAfsException.class) + .hasMessageContaining("not found"); + assertThat(storage.getDataNames(orphanNode.getId())).isEmpty(); } void testInconsistendNodeRepair() { @@ -149,7 +182,7 @@ void testAbsentChildRepair() { void testSupportedChecks() { assertThat(storage.getSupportedFileSystemChecks()) - .containsExactlyInAnyOrder(CassandraAppStorage.REF_NOT_FOUND, FileSystemCheckOptions.EXPIRED_INCONSISTENT_NODES); + .containsExactlyInAnyOrder(CassandraAppStorage.REF_NOT_FOUND, FileSystemCheckOptions.EXPIRED_INCONSISTENT_NODES, CassandraAppStorage.ORPHAN_NODE); } private NodeInfo createFolder(NodeInfo parent, String name) {