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

org.neo4j.kernel.ha.com.master.MasterImpl Maven / Gradle / Ivy

Go to download

This component provides the means to set up a cluster of Neo4j instances that act together as a cluster, providing Master-Slave replication and other features.

There is a newer version: 3.5.0-beta03
Show newest version
/*
 * Copyright (c) 2002-2016 "Neo Technology,"
 * Network Engine for Objects in Lund AB [http://neotechnology.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 Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see .
 */
package org.neo4j.kernel.ha.com.master;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.neo4j.cluster.ClusterSettings;
import org.neo4j.com.RequestContext;
import org.neo4j.com.Response;
import org.neo4j.com.TransactionNotPresentOnMasterException;
import org.neo4j.com.storecopy.StoreWriter;
import org.neo4j.kernel.DeadlockDetectedException;
import org.neo4j.kernel.impl.store.id.IdType;
import org.neo4j.kernel.api.exceptions.Status;
import org.neo4j.kernel.api.exceptions.TransactionFailureException;
import org.neo4j.kernel.configuration.Config;
import org.neo4j.kernel.ha.id.IdAllocation;
import org.neo4j.kernel.ha.lock.LockResult;
import org.neo4j.kernel.ha.lock.LockStatus;
import org.neo4j.kernel.impl.locking.Locks;
import org.neo4j.kernel.impl.locking.ResourceTypes;
import org.neo4j.kernel.impl.store.StoreId;
import org.neo4j.kernel.impl.transaction.IllegalResourceException;
import org.neo4j.kernel.impl.transaction.TransactionRepresentation;
import org.neo4j.kernel.impl.util.collection.ConcurrentAccessException;
import org.neo4j.kernel.impl.util.collection.NoSuchEntryException;
import org.neo4j.kernel.lifecycle.LifecycleAdapter;
import org.neo4j.storageengine.api.lock.ResourceType;

/**
 * This is the real master code that executes on a master. The actual
 * communication over network happens in {@link org.neo4j.kernel.ha.com.slave.MasterClient} and
 * {@link MasterServer}.
 */
public class MasterImpl extends LifecycleAdapter implements Master
{
    public interface Monitor
    {
        void initializeTx( RequestContext context );
    }

    // This is a bridge SPI that MasterImpl requires to function. Eventually this should be split
    // up into many smaller APIs implemented by other services so that this is not needed.
    // This SPI allows MasterImpl to have no direct dependencies, and instead puts those dependencies into the
    // SPI implementation, thus making it easier to test this class by mocking the SPI.
    public interface SPI
    {
        boolean isAccessible();

        IdAllocation allocateIds( IdType idType );

        StoreId storeId();

        long applyPreparedTransaction( TransactionRepresentation preparedTransaction )
                throws IOException, org.neo4j.kernel.api.exceptions.TransactionFailureException;

        Integer createRelationshipType( String name );

        long getTransactionChecksum( long txId ) throws IOException;

        RequestContext flushStoresAndStreamStoreFiles( StoreWriter writer );

         Response packEmptyResponse( T response );

         Response packTransactionStreamResponse( RequestContext context, T response );

         Response packTransactionObligationResponse( RequestContext context, T response );

        int getOrCreateLabel( String name );

        int getOrCreateProperty( String name );

    }

    private final SPI spi;
    private final Config config;
    private final Monitor monitor;
    private final long epoch;

    private final ConversationManager conversationManager;

    public MasterImpl( SPI spi, ConversationManager conversationManager, Monitor monitor, Config config )
    {
        this.spi = spi;
        this.config = config;
        this.monitor = monitor;
        this.conversationManager = conversationManager;
        this.epoch = generateEpoch();
    }

    private long generateEpoch()
    {
        return ( ( ( (long) config.get( ClusterSettings.server_id ).toIntegerIndex() ) ) << 48 ) | System.currentTimeMillis();
    }

    @Override
    public void start() throws Throwable
    {
        conversationManager.start();
    }

    @Override
    public void stop()
    {
        conversationManager.stop();
    }

    /**
     * Basically for all public methods call this assertion to verify that the caller meant to call this
     * master. The epoch is the one handed out from {@link #handshake(long, StoreId)}.
     * Exceptions to the above are:
     * o {@link #handshake(long, StoreId)}
     * o {@link #copyStore(RequestContext, StoreWriter)}
     * o {@link #pullUpdates(RequestContext)}
     *
     * all other methods must have this.
     * @param context the request context containing the epoch the request thinks it's for.
     */
    private void assertCorrectEpoch( RequestContext context )
    {
        if ( this.epoch != context.getEpoch() )
        {
            throw new InvalidEpochException( epoch, context.getEpoch() );
        }
    }

    @Override
    public Response allocateIds( RequestContext context, IdType idType )
    {
        assertCorrectEpoch( context );
        IdAllocation result = spi.allocateIds( idType );
        return spi.packEmptyResponse( result );
    }

    @Override
    public Response commit( RequestContext context, TransactionRepresentation preparedTransaction )
            throws IOException, org.neo4j.kernel.api.exceptions.TransactionFailureException
    {
        assertCorrectEpoch( context );

        // There are two constraints relating to locking during commit:
        // 1) If the client is has grabbed locks, we need to ensure those locks remain live during commit
        // 2) We must hold a schema read lock while committing, to not race with schema transactions on the master
        //
        // To satisfy this, we must determine if the client is holding locks, and if so, use that lock client, and if
        // not, we use a one-off lock client. The way the client signals this is via the 'eventIdentifier' in the
        // request. -1 means no locks are held, any other number means there should be a matching lock session.

        if( context.getEventIdentifier() == Locks.Client.NO_LOCK_SESSION_ID )
        {
            // Client is not holding locks, use a temporary lock client
            try(Conversation conversation = conversationManager.acquire())
            {
                return commit0( context, preparedTransaction, conversation.getLocks() );
            }
        }
        else
        {
            // Client is holding locks, use the clients lock session
            try
            {
                Conversation conversation = conversationManager.acquire( context );
                Locks.Client locks = conversation.getLocks();
                try
                {
                    return commit0( context, preparedTransaction, locks );
                }
                finally
                {
                    conversationManager.release(context);
                }
            }
            catch(NoSuchEntryException | ConcurrentAccessException e)
            {
                throw new TransactionNotPresentOnMasterException(context);
            }
        }
    }

    private Response commit0( RequestContext context, TransactionRepresentation preparedTransaction, Locks
            .Client locks ) throws IOException, org.neo4j.kernel.api.exceptions.TransactionFailureException
    {
        if ( locks.trySharedLock( ResourceTypes.SCHEMA, ResourceTypes.schemaResource() ) )
        {
            long txId = spi.applyPreparedTransaction( preparedTransaction );
            return spi.packTransactionObligationResponse( context, txId );
        }
        else
        {
            throw new TransactionFailureException( Status.Schema.SchemaModifiedConcurrently, "Failed to commit, because another transaction is making " +
                    "schema changes. Slave commits are disallowed while schema changes are being committed. " +
                    "Retrying the transaction should yield a successful result." );
        }
    }

    @Override
    public Response createRelationshipType( RequestContext context, String name )
    {
        assertCorrectEpoch( context );
        return spi.packTransactionObligationResponse( context, spi.createRelationshipType( name ) );
    }

    @Override
    public Response createPropertyKey( RequestContext context, String name )
    {
        assertCorrectEpoch( context );
        return spi.packTransactionObligationResponse( context, spi.getOrCreateProperty( name ) );
    }

    @Override
    public Response createLabel( RequestContext context, String name )
    {
        assertCorrectEpoch( context );
        return spi.packTransactionObligationResponse( context, spi.getOrCreateLabel( name ) );
    }

    @Override
    public Response pullUpdates( RequestContext context )
    {
        return spi.packTransactionStreamResponse( context, null );
    }

    @Override
    public Response handshake( long txId, StoreId storeId )
    {
        try
        {
            long checksum = spi.getTransactionChecksum( txId );
            return spi.packEmptyResponse( new HandshakeResult( checksum, epoch ) );
        }
        catch ( IOException e )
        {
            throw new RuntimeException( "Couldn't get master ID for transaction id " + txId, e );
        }
    }

    @Override
    public Response copyStore( RequestContext requestContext, StoreWriter writer )
    {
        RequestContext context;
        try ( StoreWriter storeWriter = writer )
        {
            context = spi.flushStoresAndStreamStoreFiles( storeWriter );
        }   // close the store writer

        return spi.packTransactionStreamResponse( context, null );
    }

    @Override
    public Response newLockSession( RequestContext context ) throws TransactionFailureException
    {
        monitor.initializeTx( context );

        if ( !spi.isAccessible() )
        {
            throw new TransactionFailureException(Status.General.DatabaseUnavailable, "Database is currently not available" );
        }
        assertCorrectEpoch( context );

        try
        {
            conversationManager.begin( context );
        }
        catch ( ConcurrentAccessException e )
        {
            throw new TransactionFailureException( Status.Transaction.TransactionAccessedConcurrently, e,
                    "The lock session requested to start is already in use. " +
                    "Please retry your request in a few seconds." );
        }
        return spi.packTransactionObligationResponse( context, null );
    }

    @Override
    public Response endLockSession( RequestContext context, boolean success )
    {
        assertCorrectEpoch( context );
        conversationManager.end( context );
        if ( !success )
        {
            conversationManager.stop( context );
        }
        return spi.packTransactionObligationResponse( context, null );
    }

    @Override
    public Response acquireExclusiveLock( RequestContext context, ResourceType type,
                                                      long... resourceIds )
    {
        assertCorrectEpoch( context );
        Locks.Client session;
        try
        {
            session = conversationManager.acquire( context ).getLocks();
        }
        catch ( NoSuchEntryException | ConcurrentAccessException e)
        {
            return spi.packTransactionObligationResponse( context, new LockResult( LockStatus.NOT_LOCKED, "Unable to acquire exclusive lock: " + e.getMessage() ) );
        }
        try
        {
            for ( long resourceId : resourceIds )
            {
                session.acquireExclusive( type, resourceId );
            }
            return spi.packTransactionObligationResponse( context, new LockResult( LockStatus.OK_LOCKED ) );
        }
        catch ( DeadlockDetectedException e )
        {
            return spi.packTransactionObligationResponse( context, new LockResult( LockStatus.DEAD_LOCKED,"Can't acquire exclusive lock, because it would have caused a deadlock: " + e.getMessage() ) );
        }
        catch ( IllegalResourceException e )
        {
            return spi.packTransactionObligationResponse( context, new LockResult( LockStatus.NOT_LOCKED ) );
        }
        finally
        {
            conversationManager.release( context );
        }
    }

    @Override
    public Response acquireSharedLock( RequestContext context, ResourceType type,
                                                   long... resourceIds )
    {
        assertCorrectEpoch( context );
        Locks.Client session;
        try
        {
            session = conversationManager.acquire( context ).getLocks();
        }
        catch ( NoSuchEntryException | ConcurrentAccessException e)
        {
            return spi.packTransactionObligationResponse( context, new LockResult( LockStatus.NOT_LOCKED, "Unable to acquire shared lock: " + e.getMessage() ) );
        }
        try
        {
            for ( long resourceId : resourceIds )
            {
                session.acquireShared( type, resourceId );
            }

            return spi.packTransactionObligationResponse( context, new LockResult( LockStatus.OK_LOCKED ) );
        }
        catch ( DeadlockDetectedException e )
        {
            return spi.packTransactionObligationResponse( context, new LockResult( LockStatus.DEAD_LOCKED, e.getMessage() ) );
        }
        catch ( IllegalResourceException e )
        {
            return spi.packTransactionObligationResponse( context, new LockResult( LockStatus.NOT_LOCKED ) );
        }
        finally
        {
            conversationManager.release( context );
        }
    }

    // =====================================================================
    // Just some methods which aren't really used when running a HA cluster,
    // but exposed so that other tools can reach that information.
    // =====================================================================

    public Map> getOngoingTransactions()
    {
        Map> result = new HashMap<>();
        Set contexts = conversationManager.getActiveContexts();
        for ( RequestContext context : contexts.toArray( new RequestContext[contexts.size()] ) )
        {
            Collection txs = result.get( context.machineId() );
            if ( txs == null )
            {
                txs = new ArrayList<>();
                result.put( context.machineId(), txs );
            }
            txs.add( context );
        }
        return result;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy