Skip to content

Commit

Permalink
Add batching APIs to the TokenHolder interface.
Browse files Browse the repository at this point in the history
This allows the clients of the TokenHolder to query and/or create more than one token at a time.
The IsolatedTransactionTokenCreator is then able to create all the missing tokens in a single implicit transaction,
instead of one transaction per token.

The next step is to expose this API further up the stack, in TokenWrite.
  • Loading branch information
chrisvest committed May 22, 2018
1 parent 8b29424 commit eac8f5e
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 93 deletions.
Expand Up @@ -42,6 +42,7 @@
public class Predicates
{
public static final IntPredicate ALWAYS_TRUE_INT = v -> true;
public static final IntPredicate ALWAYS_FALSE_INT = v -> false;

private static final int DEFAULT_POLL_INTERVAL = 20;

Expand Down
Expand Up @@ -20,14 +20,20 @@
package org.neo4j.kernel.impl.core;

import java.util.List;
import java.util.function.IntPredicate;

import org.neo4j.collection.primitive.Primitive;
import org.neo4j.collection.primitive.PrimitiveIntSet;
import org.neo4j.graphdb.TransactionFailureException;
import org.neo4j.internal.kernel.api.exceptions.KernelException;
import org.neo4j.kernel.api.exceptions.ReadOnlyDbException;
import org.neo4j.kernel.lifecycle.LifecycleAdapter;
import org.neo4j.storageengine.api.Token;
import org.neo4j.storageengine.api.TokenFactory;

import static org.neo4j.function.Predicates.ALWAYS_FALSE_INT;
import static org.neo4j.function.Predicates.ALWAYS_TRUE_INT;

/**
* Keeps a cache of tokens using {@link InMemoryTokenCache}.
* When asked for a token that isn't in the cache, delegates to a TokenCreator to create the token,
Expand Down Expand Up @@ -82,12 +88,13 @@ public int getOrCreateId( String name )
}
catch ( Throwable e )
{
throw new TransactionFailureException( "Could not create token", e );
throw new TransactionFailureException( "Could not create token.", e );
}
}

/**
* Create and put new token in cache.
*
* @param name token name
* @return newly created token id
* @throws KernelException
Expand All @@ -112,6 +119,79 @@ private synchronized int createToken( String name ) throws KernelException
return id;
}

@Override
public void getOrCreateIds( String[] names, int[] ids )
{
if ( names.length != ids.length )
{
throw new IllegalArgumentException( "Name and id arrays must have the same length." );
}
// Assume all tokens exist and try to resolve them. Break out on the first missing token.
boolean hasUnresolvedTokens = resolveIds( names, ids, ALWAYS_TRUE_INT );

if ( hasUnresolvedTokens )
{
createMissingTokens( names, ids );
}
}

private boolean resolveIds( String[] names, int[] ids, IntPredicate unresolvedIndexCheck )
{
boolean foundUnresolvable = false;
for ( int i = 0; i < ids.length; i++ )
{
Integer id = tokenCache.getId( names[i] );
if ( id != null )
{
ids[i] = id;
}
else
{
foundUnresolvable = true;
if ( unresolvedIndexCheck.test( i ) )
{
// If the check returns `true`, it's a signal that we should stop early.
break;
}
}
}
return foundUnresolvable;
}

private synchronized void createMissingTokens( String[] names, int[] ids )
{
// We redo the resolving under the lock, to make sure that these ids are really missing, and won't be
// created concurrently with us.
PrimitiveIntSet unresolvedIndexes = Primitive.intSet();
resolveIds( names, ids, i -> !unresolvedIndexes.add( i ) );
if ( !unresolvedIndexes.isEmpty() )
{
// We still have unresolved ids to create.
createUnresolvedTokens( unresolvedIndexes, names, ids );
unresolvedIndexes.visitKeys( i ->
{
tokenCache.put( tokenFactory.newToken( names[i], ids[i] ) );
return false;
} );
}
}

private void createUnresolvedTokens( PrimitiveIntSet unresolvedIndexes, String[] names, int[] ids )
{
try
{
tokenCreator.createTokens( names, ids, unresolvedIndexes );
}
catch ( ReadOnlyDbException e )
{
throw new TransactionFailureException( e.getMessage(), e );
}
catch ( Throwable e )
{
throw new TransactionFailureException( "Could not create tokens.", e );
}
}

@Override
public TOKEN getTokenById( int id ) throws TokenNotFoundException
{
Expand Down Expand Up @@ -140,6 +220,12 @@ public int getIdByName( String name )
return id;
}

@Override
public boolean getIdsByNames( String[] names, int[] ids )
{
return resolveIds( names, ids, ALWAYS_FALSE_INT );
}

@Override
public Iterable<TOKEN> getAllTokens()
{
Expand Down
Expand Up @@ -19,6 +19,7 @@
*/
package org.neo4j.kernel.impl.core;

import java.util.function.IntPredicate;
import java.util.function.Supplier;

import org.neo4j.internal.kernel.api.Kernel;
Expand Down Expand Up @@ -63,6 +64,24 @@ public synchronized int createToken( String name ) throws KernelException
}
}

@Override
public synchronized void createTokens( String[] names, int[] ids, IntPredicate filter ) throws KernelException
{
Kernel kernel = kernelSupplier.get();
try ( Session session = kernel.beginSession( LoginContext.AUTH_DISABLED );
Transaction tx = session.beginTransaction( Type.implicit ) )
{
for ( int i = 0; i < ids.length; i++ )
{
if ( filter.test( i ) )
{
ids[i] = createKey( tx, names[i] );
}
}
tx.success();
}
}

abstract int createKey( Transaction transaction, String name )
throws IllegalTokenNameException, TooManyLabelsException;
}
Expand Up @@ -19,17 +19,24 @@
*/
package org.neo4j.kernel.impl.core;

import java.util.function.IntPredicate;

import org.neo4j.kernel.api.exceptions.ReadOnlyDbException;

/**
* When the database is marked as read-only, then no tokens should be created
* When the database is marked as read-only, then no tokens can be created.
*/
public class ReadOnlyTokenCreator
implements TokenCreator
public class ReadOnlyTokenCreator implements TokenCreator
{
@Override
public int createToken( String name ) throws ReadOnlyDbException
{
throw new ReadOnlyDbException();
}

@Override
public void createTokens( String[] names, int[] ids, IntPredicate filter ) throws ReadOnlyDbException
{
throw new ReadOnlyDbException();
}
}
Expand Up @@ -19,17 +19,31 @@
*/
package org.neo4j.kernel.impl.core;

import java.util.function.IntPredicate;

import org.neo4j.internal.kernel.api.exceptions.KernelException;

public interface TokenCreator
{
/**
* Create a token by the given name and return the newly allocated id for this token.
*
* <p>
* It is assumed that the token name is not already being used.
*
* @param name The token name to allocate.
* @return The id of the allocated token name.
* @throws KernelException If the inner transaction used to allocate the token encountered a problem.
*/
int createToken( String name ) throws KernelException;

/**
* Create the tokens by the given names, and store their ids in the corresponding entry in the {@code ids} array,
* but only if the {@code indexFilter} returns {@code true} for the given index.
*
* @param names The array of token names we potentially want to create new ids for.
* @param ids The array into which we still store the id we create for the various token names.
* @param indexFilter A filter for the array indexes for which a token needs an id.
* @throws KernelException If the inner transaction used to allocate the tokens encountered a problem.
*/
void createTokens( String[] names, int[] ids, IntPredicate indexFilter ) throws KernelException;
}
Expand Up @@ -31,15 +31,44 @@ public interface TokenHolder<TOKEN extends Token>

void addToken( TOKEN token ) throws NonUniqueTokenException;

/**
* Get the id of the token by the given name, or create a new id for the token if it does not have one already,
* and then return that id.
* <p>
* This method is thread-safe, and will ensure that distinct tokens will not have multiple ids allocated for them.
*
* @param name The name of the token to get the id for.
* @return The (possibly newly created) id of the given token.
*/
int getOrCreateId( String name );

/**
* Resolve the ids of the given token {@code names} into the array for {@code ids}.
* <p>
* Any tokens that don't already have an id will have one created for it.
*/
void getOrCreateIds( String[] names, int[] ids );

TOKEN getTokenById( int id ) throws TokenNotFoundException;

TOKEN getTokenByIdOrNull( int id );

/** Returns the id, or {@link #NO_ID} if no token with this name exists. */
/**
* Returns the id, or {@link #NO_ID} if no token with this name exists.
*/
int getIdByName( String name );

/**
* Resolve the ids of the given token {@code names} into the array for {@code ids}.
* <p>
* Any tokens that don't already have an id will not be resolved, and the corrosponding entry in the {@code ids}
* array will be left untouched. If you wish for those unresolved id entries to end up with the {@link #NO_ID}
* value, you must first fill the array with that value before calling this method.
*
* @return {@code true} if some of the token names could not be resolved, {@code false} otherwise.
*/
boolean getIdsByNames( String[] names, int[] ids );

Iterable<TOKEN> getAllTokens();

int size();
Expand Down

0 comments on commit eac8f5e

Please sign in to comment.