All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.kernel.internal.DatabaseComparator Maven / Gradle / Ivy

Go to download

Neo4j kernel is a lightweight, embedded Java database designed to store data structured as graphs rather than tables. For more information, see http://neo4j.org.

There is a newer version: 5.25.1
Show newest version
/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.kernel.internal;

import static java.lang.Math.max;
import static java.lang.String.format;
import static java.util.Comparator.comparing;
import static org.neo4j.internal.helpers.Exceptions.SILENT_UNCAUGHT_EXCEPTION_HANDLER;
import static org.neo4j.internal.helpers.collection.Iterables.count;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.mutable.MutableInt;
import org.eclipse.collections.api.factory.primitive.LongLists;
import org.eclipse.collections.api.factory.primitive.LongObjectMaps;
import org.eclipse.collections.api.iterator.MutableLongIterator;
import org.eclipse.collections.api.list.primitive.MutableLongList;
import org.eclipse.collections.api.map.primitive.MutableLongObjectMap;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.internal.helpers.progress.ProgressMonitorFactory;
import org.neo4j.internal.kernel.api.TokenRead;
import org.neo4j.kernel.impl.coreapi.TransactionImpl;
import org.neo4j.values.storable.Value;
import org.neo4j.values.storable.Values;

/**
 * Storage engine agnostic way of comparing contents of two databases, given that node IDs are the same,
 * i.e. node with ID X in the first database corresponds to the same logical node X in the other database.
 */
public class DatabaseComparator {
    public static void assertDatabasesHaveTheSameLogicalContents(
            GraphDatabaseService from,
            GraphDatabaseService to,
            boolean checkDegrees,
            int totalNumThreads,
            ProgressMonitorFactory progressMonitorFactory)
            throws Exception {
        int numThreads = max(1, totalNumThreads - 1);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                numThreads,
                numThreads,
                1,
                TimeUnit.HOURS,
                new ArrayBlockingQueue<>(100),
                dontPrintOnExceptionThreadFactory());
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        int batchSize = 1_000;
        List> futures = new ArrayList<>();
        try (var fromTx = from.beginTx();
                var progress = progressMonitorFactory.singlePart(
                        "Validation",
                        ((TransactionImpl) fromTx)
                                .kernelTransaction()
                                .dataRead()
                                .estimateCountsForNode(TokenRead.ANY_LABEL))) {
            MutableLongList batch = LongLists.mutable.withInitialCapacity(batchSize);
            for (Node fromNode : fromTx.getAllNodes()) {
                batch.add(fromNode.getId());
                if (batch.size() == batchSize) {
                    scheduleAndCheckFailure(futures, executor.submit(storeValidator(from, to, batch, checkDegrees)));
                    batch = LongLists.mutable.withInitialCapacity(batchSize);
                    progress.add(batchSize);
                }
            }
            if (!batch.isEmpty()) {
                futures.add(executor.submit(storeValidator(from, to, batch, checkDegrees)));
            }
            for (Future future : futures) {
                future.get();
            }
        } finally {
            executor.shutdown();
            if (!executor.awaitTermination(1, TimeUnit.HOURS)) {
                throw new IllegalStateException("Comparison jobs didn't finish in time");
            }
        }
    }

    private static ThreadFactory dontPrintOnExceptionThreadFactory() {
        return runnable -> {
            Thread thread = new Thread(runnable);
            thread.setUncaughtExceptionHandler(SILENT_UNCAUGHT_EXCEPTION_HANDLER);
            return thread;
        };
    }

    private static void scheduleAndCheckFailure(List> futures, Future future) throws Exception {
        futures.add(future);
        Iterator> iterator = futures.iterator();
        while (iterator.hasNext()) {
            Future candidate = iterator.next();
            if (candidate.isDone() || candidate.isCancelled()) {
                // Let any exception propagate
                iterator.remove();
                candidate.get();
            } else {
                break;
            }
        }
    }

    private static Runnable storeValidator(
            GraphDatabaseService from, GraphDatabaseService to, MutableLongList batch, boolean checkDegrees) {
        return () -> {
            try (Transaction fromTx = from.beginTx();
                    Transaction toTx = to.beginTx()) {
                MutableLongIterator ids = batch.longIterator();
                while (ids.hasNext()) {
                    long fromNodeId = ids.next();
                    long toNodeId = fromNodeId;
                    Node fromNode = fromTx.getNodeById(fromNodeId);
                    Node toNode = toTx.getNodeById(toNodeId);
                    var report = compareNodes(fromNode, toNode, checkDegrees);
                    if (report.hasErrors()) {
                        String errorString = format(
                                "%s listing contents:%nfrom:%n%s%nto:%n%s",
                                report.report(), contentsOfNode(fromNode), contentsOfNode(toNode));
                        throw new RuntimeException(errorString);
                    }
                }
            }
        };
    }

    public static boolean nodesHaveEqualLogicalContents(Node fromNode, Node toNode) {
        return !compareNodes(fromNode, toNode, true).hasErrors();
    }

    private static ComparisonReport compareNodes(Node fromNode, Node toNode, boolean checkRelationships) {
        ComparisonReport report = new ComparisonReport(fromNode, toNode);
        HashSet fromLabels = new HashSet<>();
        HashSet toLabels = new HashSet<>();
        fromNode.getLabels().forEach(l -> fromLabels.add(l.name()));
        toNode.getLabels().forEach(l -> toLabels.add(l.name()));
        if (!fromLabels.equals(toLabels)) {
            report.add("Broken labels %s should be %s diff:", toLabels, fromLabels, setDiff(fromLabels, toLabels));
        }

        HashMap fromProps = new HashMap<>();
        HashMap toProps = new HashMap<>();
        fromNode.getAllProperties().forEach((s, o) -> fromProps.put(s, Values.of(o)));
        toNode.getAllProperties().forEach((s, o) -> toProps.put(s, Values.of(o)));
        if (!fromProps.equals(toProps)) {
            report.add("Broken properties %s should be %s diff:%s", toProps, fromProps, mapDiff(fromProps, toProps));
        }

        if (checkRelationships) {
            // Degrees
            long fromDegree = fromNode.getDegree();
            long toDegree = toNode.getDegree();
            if (fromDegree != toDegree) {
                // This is silly, but it may be that the record storage db is wrong in this sense... it happens all the
                // time
                // due to the old, old, really old, degrees decrement for loops bug. So go and actually double-check by
                // counting manually.
                long fromDegreeByManuallyCounting = degreeByManuallyCounting(fromNode);
                if (fromDegreeByManuallyCounting != fromDegree) {
                    fromDegree = fromDegreeByManuallyCounting;
                }

                if (fromDegree != toDegree) {
                    report.add(
                            "Broken relationships (degrees) %s should be %d diff:%s",
                            toNode.getDegree(), fromNode.getDegree(), degreesDiff(fromNode, toNode));
                }
            }

            // Relationship types
            Set fromRelationshipTypes = new HashSet<>();
            Set toRelationshipTypes = new HashSet<>();
            fromNode.getRelationshipTypes().forEach(t -> fromRelationshipTypes.add(t.name()));
            toNode.getRelationshipTypes().forEach(t -> toRelationshipTypes.add(t.name()));
            if (!fromRelationshipTypes.equals(toRelationshipTypes)) {
                report.add("Broken relationship types %s should be %s", fromRelationshipTypes, toRelationshipTypes);
            }

            // Relationships (start) -[type]-> (end), not properties yet because we don't know how to map from -> to
            // relationships
            for (String relationshipType : fromRelationshipTypes) {
                RelationshipType type = RelationshipType.withName(relationshipType);
                compareRelationships(fromNode, toNode, Direction.OUTGOING, type, report);
                compareRelationships(fromNode, toNode, Direction.INCOMING, type, report);
                compareRelationships(fromNode, toNode, Direction.BOTH, type, report);
            }
        }
        return report;
    }

    private static long degreeByManuallyCounting(Node node) {
        return count(node.getRelationships());
    }

    private static void compareRelationships(
            Node fromNode, Node toNode, Direction direction, RelationshipType type, ComparisonReport report) {
        MutableLongObjectMap fromOtherNodes = LongObjectMaps.mutable.empty();
        MutableLongObjectMap toOtherNodes = LongObjectMaps.mutable.empty();
        int fromCount = countRelationships(fromNode, direction, type, fromOtherNodes);
        int toCount = countRelationships(toNode, direction, type, toOtherNodes);
        if (fromCount != toCount) {
            report.add("Broken relationship count %s, %s %d should be %d", direction, type.name(), toCount, fromCount);
        }
        fromOtherNodes.forEachKeyValue((otherFromNodeId, fromOtherNodeCount) -> {
            long otherToNodeId = otherFromNodeId;
            MutableInt toOtherNodeCount = toOtherNodes.get(otherToNodeId);
            if (toOtherNodeCount == null || fromOtherNodeCount.intValue() != toOtherNodeCount.intValue()) {
                report.add(
                        "Broken number of relationships for %s, %s should be %s",
                        relationshipDataToString(
                                fromNode.getId(),
                                toNode.getId(),
                                direction,
                                type.name(),
                                otherFromNodeId,
                                otherToNodeId),
                        toOtherNodeCount,
                        fromOtherNodeCount);
            }
        });
    }

    private static String relationshipDataToString(
            long fromNodeId,
            long toNodeId,
            Direction direction,
            String type,
            long otherFromNodeId,
            long otherToNodeId) {
        return format(
                "(%d/%d)%s[%s]%s(%d/%d)",
                fromNodeId,
                toNodeId,
                direction == Direction.INCOMING ? "<-" : "--",
                type,
                direction == Direction.INCOMING ? "--" : "->",
                otherFromNodeId,
                otherToNodeId);
    }

    private static int countRelationships(
            Node node, Direction direction, RelationshipType type, MutableLongObjectMap otherNodes) {
        int count = 0;
        try (var relationships = node.getRelationships(direction, type)) {
            var iterator = relationships.iterator();
            while (iterator.hasNext()) {
                var relationship = iterator.next();
                otherNodes
                        .getIfAbsentPut(relationship.getOtherNodeId(node.getId()), MutableInt::new)
                        .increment();
                count++;
            }
        }
        return count;
    }

    private static String contentsOfNode(Node node) {
        StringBuilder builder = new StringBuilder();
        builder.append("Labels:");
        node.getLabels().forEach(label -> builder.append(format("%n  ")).append(label.name()));
        builder.append(format("%nProperties:"));
        node.getAllProperties().forEach((key, value) -> builder.append(format("%n  %s=%s", key, value)));
        builder.append(format("%nRelationships:"));
        TreeSet types = new TreeSet<>(comparing(RelationshipType::name));
        node.getRelationshipTypes().forEach(types::add);
        for (RelationshipType type : types) {
            node.getRelationships(Direction.BOTH, type).forEach(rel -> builder.append(format("%n  %s", rel)));
        }
        return builder.append(format("%n")).toString();
    }

    private static String degreesDiff(Node fromNode, Node toNode) {
        Set fromTypes = new HashSet<>();
        Set toTypes = new HashSet<>();
        fromNode.getRelationshipTypes().forEach(type -> fromTypes.add(type.name()));
        toNode.getRelationshipTypes().forEach(type -> toTypes.add(type.name()));
        if (!fromTypes.equals(toTypes)) {
            return "Relationship types differ: " + setDiff(fromTypes, toTypes);
        }

        StringBuilder builder = new StringBuilder();
        for (String typeName : fromTypes) {
            RelationshipType type = RelationshipType.withName(typeName);
            checkDegreeDiff(fromNode, toNode, builder, type, Direction.OUTGOING);
            checkDegreeDiff(fromNode, toNode, builder, type, Direction.INCOMING);
            checkDegreeDiff(fromNode, toNode, builder, type, Direction.BOTH);
        }
        return builder.toString();
    }

    private static void checkDegreeDiff(
            Node fromNode, Node toNode, StringBuilder builder, RelationshipType type, Direction direction) {
        int from = fromNode.getDegree(type, direction);
        int to = toNode.getDegree(type, direction);
        if (from != to) {
            builder.append(format("degree:%s,%s:%d vs %d", type.name(), direction.name(), from, to));
        }
    }

    private static  String setDiff(Set from, Set to) {
        StringBuilder builder = new StringBuilder();
        Set combined = new HashSet<>(from);
        combined.removeAll(to);
        combined.forEach(label -> builder.append(format("%n<%s", label)));
        combined = new HashSet<>(to);
        combined.removeAll(from);
        combined.forEach(label -> builder.append(format("%n>%s", label)));
        return builder.toString();
    }

    private static  String mapDiff(Map from, Map to) {
        StringBuilder builder = new StringBuilder();
        Set allKeys = new HashSet<>(from.keySet());
        allKeys.addAll(to.keySet());
        for (String key : allKeys) {
            T fromValue = from.get(key);
            T toValue = to.get(key);
            if (toValue == null) {
                builder.append(format("%n<%s=%s", key, fromValue));
            } else if (fromValue == null) {
                builder.append(format("%n>%s=%s", key, toValue));
            } else if (!fromValue.equals(toValue)) {
                builder.append(format("%n!%s=%s vs %s", key, fromValue, toValue));
            }
        }
        return builder.toString();
    }

    private static class ComparisonReport {
        private final Node fromNode;
        private final Node toNode;
        private StringBuilder builder;

        ComparisonReport(Node fromNode, Node toNode) {
            this.fromNode = fromNode;
            this.toNode = toNode;
        }

        void add(String format, Object... parameters) {
            builder().append(format("%n  " + format, parameters));
        }

        private StringBuilder builder() {
            if (builder == null) {
                builder = new StringBuilder(format("Validation failed for %s --> %s", fromNode, toNode));
            }
            return builder;
        }

        boolean hasErrors() {
            return builder != null;
        }

        String report() {
            return builder.toString();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy