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

org.neo4j.consistency.checker.RelationshipChainChecker Maven / Gradle / Ivy

There is a newer version: 5.26.1
Show newest version
/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://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.consistency.checker;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.neo4j.consistency.checker.ParallelExecution.ThrowingRunnable;
import org.neo4j.consistency.checking.cache.CacheAccess;
import org.neo4j.consistency.checking.full.ConsistencyFlags;
import org.neo4j.consistency.report.ConsistencyReport;
import org.neo4j.internal.helpers.collection.LongRange;
import org.neo4j.internal.helpers.progress.ProgressListener;
import org.neo4j.io.pagecache.PageCursor;
import org.neo4j.io.pagecache.context.CursorContext;
import org.neo4j.kernel.impl.store.RelationshipStore;
import org.neo4j.kernel.impl.store.record.RelationshipRecord;
import org.neo4j.time.Stopwatch;

import static java.lang.Math.max;
import static org.neo4j.consistency.checker.RelationshipLink.SOURCE_NEXT;
import static org.neo4j.consistency.checker.RelationshipLink.SOURCE_PREV;
import static org.neo4j.consistency.checker.RelationshipLink.TARGET_NEXT;
import static org.neo4j.consistency.checker.RelationshipLink.TARGET_PREV;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.NEXT;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.PREV;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.SLOT_FIRST_IN_CHAIN;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.SLOT_HAS_MULTIPLE_RELATIONSHIPS;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.SLOT_IN_USE;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.SLOT_PREV_OR_NEXT;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.SLOT_REFERENCE;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.SLOT_RELATIONSHIP_ID;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.SLOT_SOURCE_OR_TARGET;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.SOURCE;
import static org.neo4j.consistency.checking.cache.CacheSlots.RelationshipLink.TARGET;
import static org.neo4j.consistency.checking.cache.CacheSlots.longOf;
import static org.neo4j.internal.helpers.Format.duration;
import static org.neo4j.kernel.impl.store.record.Record.NULL_REFERENCE;
import static org.neo4j.kernel.impl.store.record.RecordLoad.FORCE;

/**
 * Checks relationship chains, i.e. their internal pointers between relationship records.
 */
class RelationshipChainChecker implements Checker
{
    private static final String RELATIONSHIP_CONSISTENCY_CHECKER_TAG = "relationshipConsistencyChecker";
    private static final String SINGLE_RELATIONSHIP_CONSISTENCY_CHECKER_TAG = "simpleChainsRelationshipConsistencyChecker";
    private final ConsistencyReport.Reporter reporter;
    private final CheckerContext context;
    private final int numberOfChainCheckers;
    private final CacheAccess cacheAccess;
    private final RecordLoading recordLoader;
    private final ProgressListener progress;

    RelationshipChainChecker( CheckerContext context )
    {
        this.context = context;
        this.reporter = context.reporter;
        // There will be two threads in addition to the relationship checkers:
        // - Relationship store scanner
        // - (Internal) thread helping pre-fetching the relationship store pages
        this.numberOfChainCheckers = max( 1, context.execution.getNumberOfThreads() - 2 );
        this.cacheAccess = context.cacheAccess;
        this.recordLoader = context.recordLoader;
        this.progress = context.progressReporter( this, "Relationship chains", context.neoStores.getRelationshipStore().getHighId() * 2 );
    }

    @Override
    public void check( LongRange nodeIdRange, boolean firstRange, boolean lastRange ) throws Exception
    {
        // Forward scan (cache prev pointers)
        checkDirection( nodeIdRange, ScanDirection.FORWARD );

        // Backward scan (cache next pointers)
        context.paddedDebug( "%s moving over to backwards relationship chain checking", getClass().getSimpleName() );
        checkDirection( nodeIdRange, ScanDirection.BACKWARD );
    }

    private void checkDirection( LongRange nodeIdRange, ScanDirection direction ) throws Exception
    {
        RelationshipStore relationshipStore = context.neoStores.getRelationshipStore();
        long highId = relationshipStore.getHighId();
        AtomicBoolean end = new AtomicBoolean();
        int numberOfThreads = numberOfChainCheckers + 1;
        ThrowingRunnable[] workers = new ThrowingRunnable[numberOfThreads];
        ProgressListener localProgress = progress.threadLocalReporter();
        ArrayBlockingQueue[] threadQueues = new ArrayBlockingQueue[numberOfChainCheckers];
        BatchedRelationshipRecords[] threadBatches = new BatchedRelationshipRecords[numberOfChainCheckers];
        for ( int i = 0; i < numberOfChainCheckers; i++ )
        {
            threadQueues[i] = new ArrayBlockingQueue<>( 20 );
            threadBatches[i] = new BatchedRelationshipRecords();
            workers[i] = relationshipVsRelationshipChecker( nodeIdRange, direction, relationshipStore, threadQueues[i], end, i );
        }

        // Record reader
        workers[workers.length - 1] = () ->
        {
            RelationshipRecord relationship = relationshipStore.newRecord();
            try ( var cursorContext = new CursorContext( context.pageCacheTracer.createPageCursorTracer( RELATIONSHIP_CONSISTENCY_CHECKER_TAG ) );
                  var cursor = relationshipStore.openPageCursorForReadingWithPrefetching( 0, cursorContext ) )
            {
                int recordsPerPage = relationshipStore.getRecordsPerPage();
                long id = direction.startingId( highId );
                while ( id >= 0 && id < highId && !context.isCancelled() )
                {
                    for ( int i = 0; i < recordsPerPage && id >= 0 && id < highId; i++, id = direction.nextId( id ) )
                    {
                        relationshipStore.getRecordByCursor( id, relationship, FORCE, cursor );
                        localProgress.add( 1 );
                        if ( relationship.inUse() )
                        {
                            queueRelationshipCheck( threadQueues, threadBatches, relationship );
                        }
                    }
                }
                processLastRelationshipChecks( threadQueues, threadBatches, end );
                localProgress.done();
            }
        };

        Stopwatch stopwatch = Stopwatch.start();
        cacheAccess.clearCache();
        context.execution.runAll( getClass().getSimpleName() + "-" + direction.name(), workers );
        detectSingleRelationshipChainInconsistencies( nodeIdRange );
        context.paddedDebug( "%s %s took %s", this, direction, duration( stopwatch.elapsed( TimeUnit.MILLISECONDS ) ) );
    }

    @Override
    public boolean shouldBeChecked( ConsistencyFlags flags )
    {
        return flags.isCheckGraph();
    }

    private void detectSingleRelationshipChainInconsistencies( LongRange nodeIdRange )
    {
        CacheAccess.Client client = cacheAccess.client();
        try ( var cursorContext = new CursorContext( context.pageCacheTracer.createPageCursorTracer( SINGLE_RELATIONSHIP_CONSISTENCY_CHECKER_TAG ) ) )
        {
            for ( long nodeId = nodeIdRange.from(); nodeId < nodeIdRange.to(); nodeId++ )
            {
                boolean inUse = client.getBooleanFromCache( nodeId, SLOT_IN_USE );
                boolean hasMultipleRelationships = client.getBooleanFromCache( nodeId, SLOT_HAS_MULTIPLE_RELATIONSHIPS );
                if ( inUse && !hasMultipleRelationships )
                {
                    long reference = client.getFromCache( nodeId, SLOT_REFERENCE );
                    long relationshipId = client.getFromCache( nodeId, SLOT_RELATIONSHIP_ID );
                    long sourceOrTarget = client.getFromCache( nodeId, SLOT_SOURCE_OR_TARGET );
                    long prevOrNext = client.getFromCache( nodeId, SLOT_PREV_OR_NEXT );
                    boolean isFirstInChain = client.getBooleanFromCache( nodeId, SLOT_FIRST_IN_CHAIN );

                    boolean consistent;
                    if ( prevOrNext == PREV )
                    {
                        // we don't know here if this chain belongs to a group and has external degrees, because if so it could have any value here
                        consistent = isFirstInChain;
                    }
                    else
                    {
                        consistent = NULL_REFERENCE.is( reference );
                    }

                    if ( !consistent )
                    {
                        RelationshipStore relationshipStore = context.neoStores.getRelationshipStore();
                        RelationshipRecord relationship = relationshipStore.getRecord( relationshipId, relationshipStore.newRecord(), FORCE, cursorContext );
                        RelationshipRecord referenceRelationship =
                                relationshipStore.getRecord( reference, relationshipStore.newRecord(), FORCE, cursorContext );
                        linkOf( sourceOrTarget == SOURCE, prevOrNext == PREV ).reportDoesNotReferenceBack( reporter, relationship, referenceRelationship );
                    }
                }
            }
        }
    }

    private static RelationshipLink linkOf( boolean source, boolean prev )
    {
        if ( source )
        {
            return prev ? SOURCE_PREV : SOURCE_NEXT;
        }
        return prev ? TARGET_PREV : TARGET_NEXT;
    }

    private ThrowingRunnable relationshipVsRelationshipChecker( LongRange nodeIdRange, ScanDirection direction, RelationshipStore store,
            ArrayBlockingQueue queue, AtomicBoolean end, int threadId )
    {
        final RelationshipRecord relationship = store.newRecord();
        final RelationshipRecord otherRelationship = store.newRecord();
        final CacheAccess.Client client = cacheAccess.client();
        final RelationshipLink sourceCachePointer = direction.sourceLink;
        final RelationshipLink targetCachePointer = direction.targetLink;
        final long prevOrNext = direction.cacheSlot;
        return () ->
        {
            try ( var cursorContext = new CursorContext( context.pageCacheTracer.createPageCursorTracer( RELATIONSHIP_CONSISTENCY_CHECKER_TAG ) );
                  var otherRelationshipCursor = store.openPageCursorForReading( 0, cursorContext ) )
            {
                while ( (!end.get() || !queue.isEmpty()) && !context.isCancelled() )
                {
                    BatchedRelationshipRecords batch = queue.poll( 100, TimeUnit.MILLISECONDS );
                    if ( batch != null )
                    {
                        while ( batch.fillNext( relationship ) && !context.isCancelled() )
                        {
                            long firstNode = relationship.getFirstNode();
                            long secondNode = relationship.getSecondNode();
                            // Intentionally not checking nodes outside highId of node store because RelationshipChecker will spot this inconsistency
                            boolean processStartNode =
                                    Math.abs( firstNode % numberOfChainCheckers ) == threadId && nodeIdRange.isWithinRangeExclusiveTo( firstNode );
                            boolean processEndNode =
                                    Math.abs( secondNode % numberOfChainCheckers ) == threadId && nodeIdRange.isWithinRangeExclusiveTo( secondNode );
                            if ( processStartNode )
                            {
                                checkRelationshipLink( direction, SOURCE_PREV, relationship, client, otherRelationship, otherRelationshipCursor, store,
                                        cursorContext );
                                checkRelationshipLink( direction, SOURCE_NEXT, relationship, client, otherRelationship, otherRelationshipCursor, store,
                                        cursorContext );
                            }
                            if ( processEndNode )
                            {
                                checkRelationshipLink( direction, TARGET_PREV, relationship, client, otherRelationship, otherRelationshipCursor, store,
                                        cursorContext );
                                checkRelationshipLink( direction, TARGET_NEXT, relationship, client, otherRelationship, otherRelationshipCursor, store,
                                        cursorContext );
                            }
                            if ( processStartNode )
                            {
                                boolean wasInUse = client.getBooleanFromCache( firstNode, SLOT_IN_USE );
                                long link = sourceCachePointer.link( relationship );
                                if ( link < NULL_REFERENCE.longValue() )
                                {
                                    sourceCachePointer.reportDoesNotReferenceBack( reporter, relationship, otherRelationship );
                                }
                                else
                                {
                                    client.putToCache( firstNode, relationship.getId(), link, SOURCE, prevOrNext, 1,
                                            longOf( wasInUse ), longOf( relationship.isFirstInFirstChain() ) );
                                }

                            }
                            if ( processEndNode )
                            {
                                boolean wasInUse = client.getBooleanFromCache( secondNode, SLOT_IN_USE );

                                long link = targetCachePointer.link( relationship );
                                if ( link < NULL_REFERENCE.longValue() )
                                {
                                    targetCachePointer.reportDoesNotReferenceBack( reporter, relationship, otherRelationship );
                                }
                                else
                                {
                                    client.putToCache( secondNode, relationship.getId(), link, TARGET, prevOrNext, 1,
                                            longOf( wasInUse ), longOf( relationship.isFirstInSecondChain() ) );
                                }

                            }
                        }
                    }
                }
            }
        };
    }

    private void checkRelationshipLink( ScanDirection direction, RelationshipLink link, RelationshipRecord relationshipCursor,
            CacheAccess.Client client, RelationshipRecord otherRelationship, PageCursor otherRelationshipCursor, RelationshipStore store,
            CursorContext cursorContext )
    {
        long relationshipId = relationshipCursor.getId();
        long nodeId = link.node( relationshipCursor );
        long linkId = link.link( relationshipCursor );
        long fromCache = client.getFromCache( nodeId, SLOT_RELATIONSHIP_ID );
        boolean cachedLinkInUse = client.getBooleanFromCache( nodeId, SLOT_IN_USE );
        if ( !link.endOfChain( relationshipCursor ) && cachedLinkInUse )
        {
            if ( fromCache != linkId )
            {
                // We can't use the cache since it doesn't contain the relationship right before us in this chain
                if ( direction.exclude( relationshipId, linkId ) )
                {
                    return;
                }
                else if ( !NULL_REFERENCE.is( fromCache ) )
                {
                    // Load it from store
                    store.getRecordByCursor( linkId, otherRelationship, FORCE, otherRelationshipCursor );
                }
                else
                {
                    otherRelationship.clear();
                    link.reportDoesNotReferenceBack( reporter, recordLoader.relationship( relationshipCursor.getId(), cursorContext ), otherRelationship );
                }
            }
            else
            {
                // OK good we can use the cached values representing a relationship right before us in this chain
                otherRelationship.clear();
                otherRelationship.setId( linkId );
                long other = client.getFromCache( nodeId, SLOT_REFERENCE );
                NodeLink nodeLink = client.getFromCache( nodeId, SLOT_SOURCE_OR_TARGET ) == SOURCE ? NodeLink.SOURCE : NodeLink.TARGET;
                nodeLink.setNode( otherRelationship, nodeId );
                link.setOther( otherRelationship, nodeLink, other );
                otherRelationship.setInUse( client.getBooleanFromCache( nodeId, SLOT_IN_USE ) );
                otherRelationship.setCreated();
            }
            checkRelationshipLink( direction, link, otherRelationship, relationshipId, nodeId, linkId, cursorContext );
        }
    }

    private void checkRelationshipLink( ScanDirection direction, RelationshipLink thing, RelationshipRecord otherRelationship, long relationshipId, long nodeId,
            long linkId, CursorContext cursorContext )
    {
        // Perform the checks
        NodeLink nodeLink = NodeLink.select( otherRelationship, nodeId );
        if ( nodeLink == null )
        {
            thing.reportOtherNode( reporter, recordLoader.relationship( relationshipId, cursorContext ), recordLoader.relationship( linkId, cursorContext ) );
        }
        else
        {
            if ( thing.other( otherRelationship, nodeLink ) != relationshipId )
            {
                // Read the relationship from store and do the check on that actual record instead, should happen rarely anyway
                if ( otherRelationship.isCreated() )
                {
                    recordLoader.relationship( otherRelationship, otherRelationship.getId(), cursorContext );
                    // Call this method one more time, now with !created
                    checkRelationshipLink( direction, thing, otherRelationship, relationshipId, nodeId, linkId, cursorContext );
                    return;
                }

                thing.reportDoesNotReferenceBack( reporter, recordLoader.relationship( relationshipId, cursorContext ),
                        recordLoader.relationship( linkId, cursorContext ) );
            }
            else
            {
                if ( !direction.exclude( relationshipId, linkId ) && !otherRelationship.inUse() )
                {
                    thing.reportNotUsedRelationshipReferencedInChain( reporter, recordLoader.relationship( relationshipId, cursorContext ),
                            recordLoader.relationship( linkId, cursorContext ) );
                }
            }
        }
    }

    private void queueRelationshipCheck( ArrayBlockingQueue[] threadQueues, BatchedRelationshipRecords[] threadBatches,
            RelationshipRecord relationshipCursor ) throws InterruptedException
    {
        int sourceThread = (int) Math.abs( relationshipCursor.getFirstNode() % numberOfChainCheckers );
        queueRelationshipCheck( threadQueues, threadBatches, relationshipCursor, sourceThread );
        int targetThread = (int) Math.abs( relationshipCursor.getSecondNode() % numberOfChainCheckers );
        if ( targetThread != sourceThread )
        {
            queueRelationshipCheck( threadQueues, threadBatches, relationshipCursor, targetThread );
        }
    }

    private static void queueRelationshipCheck( ArrayBlockingQueue[] threadQueues, BatchedRelationshipRecords[] threadBatches,
            RelationshipRecord relationshipCursor, int thread ) throws InterruptedException
    {
        if ( !threadBatches[thread].hasMoreSpace() )
        {
            threadQueues[thread].put( threadBatches[thread] );
            threadBatches[thread] = new BatchedRelationshipRecords();
        }
        threadBatches[thread].add( relationshipCursor );
    }

    private static void processLastRelationshipChecks( ArrayBlockingQueue[] threadQueues,
            BatchedRelationshipRecords[] threadBatches,
            AtomicBoolean end ) throws Exception
    {
        for ( int i = 0; i < threadBatches.length; i++ )
        {
            if ( threadBatches[i].numberOfRelationships() > 0 )
            {
                threadQueues[i].put( threadBatches[i] );
            }
        }
        end.set( true );
    }

    @Override
    public String toString()
    {
        return String.format( "%s[highId:%d]",getClass().getSimpleName(), context.neoStores.getRelationshipStore().getHighId() );
    }

    private enum ScanDirection
    {
        FORWARD( SOURCE_PREV, TARGET_PREV, PREV )
        {
            @Override
            boolean exclude( long id, long reference )
            {
                return !NULL_REFERENCE.is( reference ) && reference > id;
            }

            @Override
            long nextId( long id )
            {
                return id + 1;
            }

            @Override
            long startingId( long highId )
            {
                return 0;
            }
        },
        BACKWARD( SOURCE_NEXT, TARGET_NEXT, NEXT )
        {
            @Override
            boolean exclude( long id, long reference )
            {
                return !NULL_REFERENCE.is( reference ) && reference < id;
            }

            @Override
            long nextId( long id )
            {
                return id - 1;
            }

            @Override
            long startingId( long highId )
            {
                return highId - 1;
            }
        };

        final RelationshipLink sourceLink;
        final RelationshipLink targetLink;
        final long cacheSlot;

        ScanDirection( RelationshipLink sourceLink, RelationshipLink targetLink, long cacheSlot )
        {
            this.sourceLink = sourceLink;
            this.targetLink = targetLink;
            this.cacheSlot = cacheSlot;
        }

        abstract boolean exclude( long id, long reference );

        abstract long nextId( long id );

        abstract long startingId( long highId );
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy