/* * * * Licensed to the Apache Software Foundation (ASF) under one * * or more contributor license agreements. See the NOTICE file * * distributed with this work for additional information * * regarding copyright ownership. The ASF licenses this file * * to you under the Apache License, Version 2.0 (the * * "License"); you may not use this file except in compliance * * with the License. You may obtain a copy of the License at * * * * http://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, * * software distributed under the License is distributed on an * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * * KIND, either express or implied. See the License for the * * specific language governing permissions and limitations * * under the License. * */ package org.apache.usergrid.persistence.graph.serialization.impl; import java.util.*; import org.apache.cassandra.db.marshal.BytesType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.usergrid.persistence.core.astyanax.BucketScopedRowKey; import org.apache.usergrid.persistence.core.astyanax.BucketScopedRowKeySerializer; import org.apache.usergrid.persistence.core.CassandraConfig; import org.apache.usergrid.persistence.core.astyanax.ColumnSearch; import org.apache.usergrid.persistence.core.astyanax.CompositeFieldSerializer; import org.apache.usergrid.persistence.core.astyanax.IdRowCompositeSerializer; import org.apache.usergrid.persistence.core.astyanax.MultiRowColumnIterator; import org.apache.usergrid.persistence.core.astyanax.MultiTenantColumnFamily; import org.apache.usergrid.persistence.core.astyanax.MultiTenantColumnFamilyDefinition; import org.apache.usergrid.persistence.core.astyanax.StringColumnParser; import org.apache.usergrid.persistence.core.datastax.TableDefinition; import org.apache.usergrid.persistence.core.migration.schema.Migration; import org.apache.usergrid.persistence.core.scope.ApplicationScope; import org.apache.usergrid.persistence.core.shard.ExpandingShardLocator; import org.apache.usergrid.persistence.core.shard.StringHashUtils; import org.apache.usergrid.persistence.core.util.ValidationUtils; import org.apache.usergrid.persistence.graph.Edge; import org.apache.usergrid.persistence.graph.GraphFig; import org.apache.usergrid.persistence.graph.SearchEdgeType; import org.apache.usergrid.persistence.graph.SearchIdType; import org.apache.usergrid.persistence.graph.serialization.EdgeMetadataSerialization; import org.apache.usergrid.persistence.graph.serialization.util.GraphValidation; import org.apache.usergrid.persistence.model.entity.Id; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.hash.Funnel; import com.google.common.hash.PrimitiveSink; import com.google.inject.Inject; import com.google.inject.Singleton; import com.netflix.astyanax.Keyspace; import com.netflix.astyanax.MutationBatch; import com.netflix.astyanax.model.CompositeBuilder; import com.netflix.astyanax.model.CompositeParser; import com.netflix.astyanax.serializers.StringSerializer; import com.netflix.astyanax.util.RangeBuilder; /** * Class to perform all edge metadata I/O */ @Singleton public class EdgeMetadataSerializationV2Impl implements EdgeMetadataSerialization, Migration { private static final byte[] HOLDER = new byte[] { 0 }; //row key serializers private static final IdRowCompositeSerializer ID_SER = IdRowCompositeSerializer.get(); private static final BucketScopedRowKeySerializer<Id> ROW_KEY_SER = new BucketScopedRowKeySerializer<>( ID_SER ); private static final StringSerializer STRING_SERIALIZER = StringSerializer.get(); private static final EdgeTypeRowCompositeSerializer EDGE_SER = new EdgeTypeRowCompositeSerializer(); private static final BucketScopedRowKeySerializer<EdgeIdTypeKey> EDGE_TYPE_ROW_KEY = new BucketScopedRowKeySerializer<>( EDGE_SER ); private static final StringColumnParser PARSER = StringColumnParser.get(); /** * V1 CF's. We can't delete these until a full migration has been run */ /** * CFs where the row key contains the source node id */ private static final MultiTenantColumnFamily<BucketScopedRowKey<Id>, String> CF_SOURCE_EDGE_TYPES = new MultiTenantColumnFamily<>( "Graph_Source_Edge_Types_V2", ROW_KEY_SER, STRING_SERIALIZER ); //all target id types for source edge type private static final MultiTenantColumnFamily<BucketScopedRowKey<EdgeIdTypeKey>, String> CF_SOURCE_EDGE_ID_TYPES = new MultiTenantColumnFamily<>( "Graph_Source_Edge_Id_Types_V2", EDGE_TYPE_ROW_KEY, STRING_SERIALIZER ); /** * CFs where the row key is the target node id */ private static final MultiTenantColumnFamily<BucketScopedRowKey<Id>, String> CF_TARGET_EDGE_TYPES = new MultiTenantColumnFamily<>( "Graph_Target_Edge_Types_V2", ROW_KEY_SER, STRING_SERIALIZER ); //all source id types for target edge type private static final MultiTenantColumnFamily<BucketScopedRowKey<EdgeIdTypeKey>, String> CF_TARGET_EDGE_ID_TYPES = new MultiTenantColumnFamily<>( "Graph_Target_Edge_Id_Types_V2", EDGE_TYPE_ROW_KEY, STRING_SERIALIZER ); private static final Comparator<String> STRING_COMPARATOR = new Comparator<String>() { @Override public int compare( final String o1, final String o2 ) { return o1.compareTo( o2 ); } }; /** * Funnel for hashing IDS */ private static final Funnel<Id> ID_FUNNEL = new Funnel<Id>() { @Override public void funnel( final Id from, final PrimitiveSink into ) { final UUID id = from.getUuid(); final String type = from.getType(); into.putLong( id.getMostSignificantBits() ); into.putLong( id.getLeastSignificantBits() ); into.putString( type, StringHashUtils.UTF8 ); } }; /** * Funnel for hashing IDS */ private static final Funnel<EdgeIdTypeKey> EDGE_TYPE_FUNNEL = new Funnel<EdgeIdTypeKey>() { @Override public void funnel( final EdgeIdTypeKey from, final PrimitiveSink into ) { final UUID id = from.node.getUuid(); final String type = from.node.getType(); into.putLong( id.getMostSignificantBits() ); into.putLong( id.getLeastSignificantBits() ); into.putString( type, StringHashUtils.UTF8 ); into.putString( from.edgeType, StringHashUtils.UTF8 ); } }; protected final Keyspace keyspace; private final CassandraConfig cassandraConfig; private final GraphFig graphFig; /** * Locator for all id buckets */ private ExpandingShardLocator<Id> idExpandingShardLocator; /** * Locator for all edge types */ private ExpandingShardLocator<EdgeIdTypeKey> edgeTypeExpandingShardLocator; @Inject public EdgeMetadataSerializationV2Impl( final Keyspace keyspace, final CassandraConfig cassandraConfig, final GraphFig graphFig ) { Preconditions.checkNotNull( "cassandraConfig is required", cassandraConfig ); Preconditions.checkNotNull( "consistencyFig is required", graphFig ); Preconditions.checkNotNull( "keyspace is required", keyspace ); this.keyspace = keyspace; this.cassandraConfig = cassandraConfig; this.graphFig = graphFig; //set up the shard locator instances idExpandingShardLocator = new ExpandingShardLocator<>( ID_FUNNEL, cassandraConfig.getShardSettings() ); edgeTypeExpandingShardLocator = new ExpandingShardLocator<>( EDGE_TYPE_FUNNEL, cassandraConfig.getShardSettings() ); } @Override public MutationBatch writeEdge( final ApplicationScope scope, final Edge edge ) { ValidationUtils.validateApplicationScope( scope ); GraphValidation.validateEdge( edge ); final Id scopeId = scope.getApplication(); final Id source = edge.getSourceNode(); final Id target = edge.getTargetNode(); final String edgeType = edge.getType(); final long timestamp = edge.getTimestamp(); final MutationBatch batch = keyspace.prepareMutationBatch().withConsistencyLevel( cassandraConfig.getWriteCL() ) .withTimestamp( timestamp ); //add source->target edge type to meta data final int sourceKeyBucket = idExpandingShardLocator.getCurrentBucket( source ); final BucketScopedRowKey<Id> sourceKey = BucketScopedRowKey.fromKey( scopeId, source, sourceKeyBucket ); batch.withRow( CF_SOURCE_EDGE_TYPES, sourceKey ).putColumn( edgeType, HOLDER ); //write source->target edge type and id type to meta data final EdgeIdTypeKey sourceTargetTypeKey = new EdgeIdTypeKey( source, edgeType ); final int sourceTargetTypeBucket = edgeTypeExpandingShardLocator.getCurrentBucket( sourceTargetTypeKey ); final BucketScopedRowKey<EdgeIdTypeKey> sourceTypeKey = BucketScopedRowKey.fromKey( scopeId, sourceTargetTypeKey, sourceTargetTypeBucket ); batch.withRow( CF_SOURCE_EDGE_ID_TYPES, sourceTypeKey ).putColumn( target.getType(), HOLDER ); final int targetKeyBucket = idExpandingShardLocator.getCurrentBucket( target ); final BucketScopedRowKey<Id> targetKey = BucketScopedRowKey.fromKey( scopeId, target, targetKeyBucket ); batch.withRow( CF_TARGET_EDGE_TYPES, targetKey ).putColumn( edgeType, HOLDER ); //write target<--source edge type and id type to meta data final EdgeIdTypeKey targetSourceTypeKey = new EdgeIdTypeKey( target, edgeType ); final int targetSourceTypeKeyBucket = edgeTypeExpandingShardLocator.getCurrentBucket( targetSourceTypeKey ); final BucketScopedRowKey<EdgeIdTypeKey> targetTypeKey = BucketScopedRowKey.fromKey( scopeId, targetSourceTypeKey, targetSourceTypeKeyBucket ); batch.withRow( CF_TARGET_EDGE_ID_TYPES, targetTypeKey ).putColumn( source.getType(), HOLDER ); return batch; } @Override public MutationBatch removeEdgeTypeFromSource( final ApplicationScope scope, final Edge edge ) { return removeEdgeTypeFromSource( scope, edge.getSourceNode(), edge.getType(), edge.getTimestamp() ); } @Override public MutationBatch removeEdgeTypeFromSource( final ApplicationScope scope, final Id sourceNode, final String type, final long version ) { return removeEdgeType( scope, sourceNode, type, version, CF_SOURCE_EDGE_TYPES ); } @Override public MutationBatch removeIdTypeFromSource( final ApplicationScope scope, final Edge edge ) { return removeIdTypeFromSource( scope, edge.getSourceNode(), edge.getType(), edge.getTargetNode().getType(), edge.getTimestamp() ); } @Override public MutationBatch removeIdTypeFromSource( final ApplicationScope scope, final Id sourceNode, final String type, final String idType, final long version ) { return removeIdType( scope, sourceNode, idType, type, version, CF_SOURCE_EDGE_ID_TYPES ); } @Override public MutationBatch removeEdgeTypeToTarget( final ApplicationScope scope, final Edge edge ) { return removeEdgeTypeToTarget( scope, edge.getTargetNode(), edge.getType(), edge.getTimestamp() ); } @Override public MutationBatch removeEdgeTypeToTarget( final ApplicationScope scope, final Id targetNode, final String type, final long version ) { return removeEdgeType( scope, targetNode, type, version, CF_TARGET_EDGE_TYPES ); } @Override public MutationBatch removeIdTypeToTarget( final ApplicationScope scope, final Edge edge ) { return removeIdTypeToTarget( scope, edge.getTargetNode(), edge.getType(), edge.getSourceNode().getType(), edge.getTimestamp() ); } @Override public MutationBatch removeIdTypeToTarget( final ApplicationScope scope, final Id targetNode, final String type, final String idType, final long version ) { return removeIdType( scope, targetNode, idType, type, version, CF_TARGET_EDGE_ID_TYPES ); } /** * Remove the edge * * @param scope The scope * @param rowKeyId The id to use in the row key * @param edgeType The edge type * @param version The version of the edge * @param cf The column family */ private MutationBatch removeEdgeType( final ApplicationScope scope, final Id rowKeyId, final String edgeType, final long version, final MultiTenantColumnFamily<BucketScopedRowKey<Id>, String> cf ) { //write target<--source edge type meta data final int currentShard = idExpandingShardLocator.getCurrentBucket( rowKeyId ); final BucketScopedRowKey<Id> rowKey = BucketScopedRowKey.fromKey( scope.getApplication(), rowKeyId, currentShard ); final MutationBatch batch = keyspace.prepareMutationBatch().withTimestamp( version ); batch.withRow( cf, rowKey ).deleteColumn( edgeType ); return batch; } /** * Remove the id type * * @param scope The scope to use * @param rowId The id to use in the row key * @param idType The id type to use in the column * @param edgeType The edge type to use in the column * @param version The version to use on the column * @param cf The column family to use * * @return A populated mutation with the remove operations */ private MutationBatch removeIdType( final ApplicationScope scope, final Id rowId, final String idType, final String edgeType, final long version, final MultiTenantColumnFamily<BucketScopedRowKey<EdgeIdTypeKey>, String> cf ) { final EdgeIdTypeKey edgeIdTypeKey = new EdgeIdTypeKey( rowId, edgeType ); final int currentShard = edgeTypeExpandingShardLocator.getCurrentBucket( edgeIdTypeKey ); final MutationBatch batch = keyspace.prepareMutationBatch().withTimestamp( version ); //write target<--source edge type and id type to meta data final BucketScopedRowKey<EdgeIdTypeKey> rowKey = BucketScopedRowKey.fromKey( scope.getApplication(), edgeIdTypeKey, currentShard ); batch.withRow( cf, rowKey ).deleteColumn( idType ); return batch; } @Override public Iterator<String> getEdgeTypesFromSource( final ApplicationScope scope, final SearchEdgeType search ) { return getEdgeTypes( scope, search, CF_SOURCE_EDGE_TYPES ); } @Override public Iterator<String> getIdTypesFromSource( final ApplicationScope scope, final SearchIdType search ) { return getIdTypes( scope, search, CF_SOURCE_EDGE_ID_TYPES ); } @Override public Iterator<String> getEdgeTypesToTarget( final ApplicationScope scope, final SearchEdgeType search ) { return getEdgeTypes( scope, search, CF_TARGET_EDGE_TYPES ); } /** * Get the edge types from the search criteria. * * @param scope The org scope * @param search The edge type search info * @param cf The column family to execute on */ private Iterator<String> getEdgeTypes( final ApplicationScope scope, final SearchEdgeType search, final MultiTenantColumnFamily<BucketScopedRowKey<Id>, String> cf ) { ValidationUtils.validateApplicationScope( scope ); GraphValidation.validateSearchEdgeType( search ); final Id applicationId = scope.getApplication(); final Id searchNode = search.getNode(); final int[] bucketIds = idExpandingShardLocator.getAllBuckets( searchNode ); //no generics is intentional here final List<BucketScopedRowKey<Id>> buckets = BucketScopedRowKey.fromRange( applicationId, searchNode, bucketIds ); final ColumnSearch<String> columnSearch = createSearch( search ); return new MultiRowColumnIterator( keyspace, cf, cassandraConfig.getReadCL(), PARSER, columnSearch, STRING_COMPARATOR, buckets, graphFig.getScanPageSize() ); } @Override public Iterator<String> getIdTypesToTarget( final ApplicationScope scope, final SearchIdType search ) { return getIdTypes( scope, search, CF_TARGET_EDGE_ID_TYPES ); } /** * Get the id types from the specified column family * * @param scope The organization scope to use * @param search The search criteria * @param cf The column family to search */ public Iterator<String> getIdTypes( final ApplicationScope scope, final SearchIdType search, final MultiTenantColumnFamily<BucketScopedRowKey<EdgeIdTypeKey>, String> cf ) { ValidationUtils.validateApplicationScope( scope ); GraphValidation.validateSearchEdgeIdType( search ); final Id applicationId = scope.getApplication(); final Id searchNode = search.getNode(); final EdgeIdTypeKey edgeIdTypeKey = new EdgeIdTypeKey( searchNode, search.getEdgeType() ); final int[] bucketIds = edgeTypeExpandingShardLocator.getAllBuckets( edgeIdTypeKey ); //no generics is intentional here final List<BucketScopedRowKey<EdgeIdTypeKey>> buckets = BucketScopedRowKey.fromRange( applicationId, edgeIdTypeKey, bucketIds ); final ColumnSearch<String> columnSearch = createSearch( search ); return new MultiRowColumnIterator( keyspace, cf, cassandraConfig.getReadCL(), PARSER, columnSearch, STRING_COMPARATOR, buckets, graphFig.getScanPageSize() ); } @Override public Collection<MultiTenantColumnFamilyDefinition> getColumnFamilies() { return Arrays.asList( graphCf( CF_SOURCE_EDGE_TYPES ), graphCf( CF_TARGET_EDGE_TYPES ), graphCf( CF_SOURCE_EDGE_ID_TYPES ), graphCf( CF_TARGET_EDGE_ID_TYPES ) ); } @Override public Collection<TableDefinition> getTables() { return Collections.emptyList(); } /** * Helper to generate an edge definition by the type */ private MultiTenantColumnFamilyDefinition graphCf(MultiTenantColumnFamily cf ) { return new MultiTenantColumnFamilyDefinition( cf, BytesType.class.getSimpleName(), UTF8Type.class.getSimpleName(), BytesType.class.getSimpleName(), MultiTenantColumnFamilyDefinition.CacheOption.KEYS ); } /** * Create a new instance of our search */ private ColumnSearch<String> createSearch( final SearchEdgeType search ) { //resume from the last if specified. Also set the range return new ColumnSearch<String>() { @Override public void buildRange(final RangeBuilder rangeBuilder, final String value, String end) { rangeBuilder.setLimit( graphFig.getScanPageSize() ); if ( value != null ) { rangeBuilder.setStart( value ); } //we have a last, it's where we need to start seeking from else if ( search.getLast().isPresent() ) { rangeBuilder.setStart( search.getLast().get() ); } //no last was set, but we have a prefix, set it else if ( search.prefix().isPresent() ) { rangeBuilder.setStart( search.prefix().get() ); } //we have a prefix, so make sure we only seek to prefix + max UTF value if ( search.prefix().isPresent() ) { rangeBuilder.setEnd( search.prefix().get() + "\uffff" ); } } @Override public void buildRange( final RangeBuilder rangeBuilder ) { buildRange( rangeBuilder, null, null); } @Override public boolean skipFirst( final String first ) { final Optional<String> last = search.getLast(); if(!last.isPresent()){ return false; } return last.get().equals( first ); } }; } @Override public int getImplementationVersion() { return GraphDataVersions.META_SHARDING.getVersion(); } /** * Inner class to serialize and edgeIdTypeKey */ private static class EdgeTypeRowCompositeSerializer implements CompositeFieldSerializer<EdgeIdTypeKey> { private static final IdRowCompositeSerializer ID_SER = IdRowCompositeSerializer.get(); @Override public void toComposite( final CompositeBuilder builder, final EdgeIdTypeKey value ) { ID_SER.toComposite( builder, value.node ); builder.addString( value.edgeType ); } @Override public EdgeIdTypeKey fromComposite( final CompositeParser composite ) { final Id id = ID_SER.fromComposite( composite ); final String edgeType = composite.readString(); return new EdgeIdTypeKey( id, edgeType ); } } // // private RangeBuilder createRange( final SearchEdgeType search ) { // final RangeBuilder builder = new RangeBuilder().setLimit( graphFig.getScanPageSize() ); // // // //we have a last, it's where we need to start seeking from // if ( search.getLast().isPresent() ) { // builder.setStart( search.getLast().get() ); // } // // //no last was set, but we have a prefix, set it // else if ( search.prefix().isPresent() ) { // builder.setStart( search.prefix().get() ); // } // // // //we have a prefix, so make sure we only seek to prefix + max UTF value // if ( search.prefix().isPresent() ) { // builder.setEnd( search.prefix().get() + "\uffff" ); // } // // // return builder; // } // private void setStart( final SearchEdgeType search, final RangeBuilder builder ) { // //prefix is set, set our end marker // if ( search.getLast().isPresent() ) { // builder.setEnd( search.getLast().get() ); // } // // else if ( search.prefix().isPresent() ) { // builder.setStart( search.prefix().get() ); // } // } // // // private void setEnd( final SearchEdgeType search, final RangeBuilder builder ) { // //if our last is set, it takes precendence // // if ( search.prefix().isPresent() ) { // builder.setEnd( search.prefix().get() + "\uffff" ); // } // } /** * Simple key object for I/O */ private static class EdgeIdTypeKey { private final Id node; private final String edgeType; private EdgeIdTypeKey( final Id node, final String edgeType ) { this.node = node; this.edgeType = edgeType; } } }