/* * 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.mq.cassandra.io; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.thrift.InvalidRequestException; import org.apache.usergrid.mq.Message; import org.apache.usergrid.mq.QueueResults; import org.apache.usergrid.mq.cassandra.io.NoTransactionSearch.SearchParam; import org.apache.usergrid.persistence.exceptions.QueueException; import org.apache.usergrid.persistence.hector.CountingMutator; import org.apache.usergrid.utils.UUIDUtils; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.beans.Row; import me.prettyprint.hector.api.beans.Rows; import me.prettyprint.hector.api.exceptions.HInvalidRequestException; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.SliceQuery; import static me.prettyprint.hector.api.factory.HFactory.createColumn; import static me.prettyprint.hector.api.factory.HFactory.createMultigetSliceQuery; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; import static org.apache.usergrid.mq.Queue.QUEUE_NEWEST; import static org.apache.usergrid.mq.Queue.QUEUE_OLDEST; import static org.apache.usergrid.mq.cassandra.CassandraMQUtils.deserializeMessage; import static org.apache.usergrid.mq.cassandra.CassandraMQUtils.getQueueShardRowKey; import static org.apache.usergrid.mq.cassandra.QueueManagerImpl.ALL_COUNT; import static org.apache.usergrid.mq.cassandra.QueueManagerImpl.QUEUE_SHARD_INTERVAL; import static org.apache.usergrid.mq.cassandra.QueuesCF.CONSUMERS; import static org.apache.usergrid.mq.cassandra.QueuesCF.MESSAGE_PROPERTIES; import static org.apache.usergrid.mq.cassandra.QueuesCF.QUEUE_INBOX; import static org.apache.usergrid.mq.cassandra.QueuesCF.QUEUE_PROPERTIES; import static org.apache.usergrid.persistence.cassandra.Serializers.be; import static org.apache.usergrid.persistence.cassandra.Serializers.se; import static org.apache.usergrid.persistence.cassandra.Serializers.ue; import static org.apache.usergrid.utils.NumberUtils.roundLong; import static org.apache.usergrid.utils.UUIDUtils.getTimestampInMillis; /** @author tnine */ public abstract class AbstractSearch implements QueueSearch { private static final Logger logger = LoggerFactory.getLogger( AbstractSearch.class ); protected Keyspace ko; /** * */ public AbstractSearch( Keyspace ko ) { this.ko = ko; } /** * Get the position in the queue for the given appId, consumer and queu * * @param queueId The queueId * @param consumerId The consumerId */ public UUID getConsumerQueuePosition( UUID queueId, UUID consumerId ) { HColumn<UUID, UUID> result = HFactory.createColumnQuery( ko, ue, ue, ue ).setKey( consumerId ).setName( queueId ) .setColumnFamily( CONSUMERS.getColumnFamily() ).execute().get(); if ( result != null ) { return result.getValue(); } return null; } /** Load the messages into an array list */ protected List<Message> loadMessages( Collection<UUID> messageIds, boolean reversed ) { Rows<UUID, String, ByteBuffer> messageResults = createMultigetSliceQuery( ko, ue, se, be ).setColumnFamily( MESSAGE_PROPERTIES.getColumnFamily() ) .setKeys( messageIds ) .setRange( null, null, false, ALL_COUNT ).execute().get(); List<Message> messages = new ArrayList<Message>( messageIds.size() ); for ( Row<UUID, String, ByteBuffer> row : messageResults ) { Message message = deserializeMessage( row.getColumnSlice().getColumns() ); if ( message != null ) { messages.add( message ); } } Collections.sort( messages, new RequestedOrderComparator( messageIds ) ); return messages; } /** Create the results to return from the given messages */ protected QueueResults createResults( List<Message> messages, String queuePath, UUID queueId, UUID consumerId ) { UUID lastId = null; if ( messages != null && messages.size() > 0 ) { lastId = messages.get( messages.size() - 1 ).getUuid(); } return new QueueResults( queuePath, queueId, messages, lastId, consumerId ); } /** * Get a list of UUIDs that can be read for the client. This comes directly from the queue inbox, and DOES NOT take * into account client messages * * @param queueId The queue id to read * @param bounds The bounds to use when reading */ protected List<UUID> getQueueRange( UUID queueId, QueueBounds bounds, SearchParam params ) { if ( bounds == null ) { logger.error( "Necessary queue bounds not found" ); throw new QueueException( "Necessary queue bounds not found" ); } UUID finish_uuid = params.reversed ? bounds.getOldest() : bounds.getNewest(); List<UUID> results = new ArrayList<>( params.limit ); UUID start = params.startId; if ( start == null ) { start = params.reversed ? bounds.getNewest() : bounds.getOldest(); } if ( start == null ) { logger.error( "No first message in queue" ); return results; } if ( finish_uuid == null ) { logger.error( "No last message in queue" ); return results; } long start_ts_shard = roundLong( getTimestampInMillis( start ), QUEUE_SHARD_INTERVAL ); long finish_ts_shard = roundLong( getTimestampInMillis( finish_uuid ), QUEUE_SHARD_INTERVAL ); long current_ts_shard = start_ts_shard; if ( params.reversed ) { current_ts_shard = finish_ts_shard; } final MessageIdComparator comparator = new MessageIdComparator( params.reversed ); //should be start < finish if ( comparator.compare( start, finish_uuid ) > 0 ) { logger.warn( "Tried to perform a slice with start UUID {} after finish UUID {}.", start, finish_uuid ); throw new IllegalArgumentException( String.format( "You cannot specify a start value of %s after finish value of %s", start, finish_uuid ) ); } UUID lastValue = start; boolean firstPage = true; while ( ( current_ts_shard >= start_ts_shard ) && ( current_ts_shard <= finish_ts_shard ) && comparator.compare( start, finish_uuid ) < 1 ) { if( logger.isDebugEnabled() ) { logger.debug("Starting search with start UUID {}, finish UUID {}, and reversed {}", lastValue, finish_uuid, params.reversed); } SliceQuery<ByteBuffer, UUID, ByteBuffer> q = createSliceQuery( ko, be, ue, be ); q.setColumnFamily( QUEUE_INBOX.getColumnFamily() ); q.setKey( getQueueShardRowKey( queueId, current_ts_shard ) ); q.setRange( lastValue, finish_uuid, params.reversed, params.limit + 1 ); final List<HColumn<UUID, ByteBuffer>> cassResults = swallowOrderedExecution(q); for ( int i = 0; i < cassResults.size(); i++ ) { HColumn<UUID, ByteBuffer> column = cassResults.get( i ); final UUID columnName = column.getName(); // skip the first one, we've already read it if ( i == 0 && ( firstPage && params.skipFirst && params.startId.equals( columnName ) ) || ( !firstPage && lastValue != null && lastValue.equals( columnName ) ) ) { continue; } lastValue = columnName; results.add( columnName ); if (logger.isDebugEnabled()) { logger.debug("Added id '{}' to result set for queue id '{}'", start, queueId); } if ( results.size() >= params.limit ) { return results; } firstPage = false; } if ( params.reversed ) { current_ts_shard -= QUEUE_SHARD_INTERVAL; } else { current_ts_shard += QUEUE_SHARD_INTERVAL; } } return results; } /** * Get the bounds for the queue * * @return The bounds for the queue */ public QueueBounds getQueueBounds( UUID queueId ) { try { ColumnSlice<String, UUID> result = HFactory.createSliceQuery( ko, ue, se, ue ).setKey( queueId ) .setColumnNames( QUEUE_NEWEST, QUEUE_OLDEST ) .setColumnFamily( QUEUE_PROPERTIES.getColumnFamily() ).execute() .get(); if ( result != null && result.getColumnByName( QUEUE_OLDEST ) != null && result.getColumnByName( QUEUE_NEWEST ) != null ) { return new QueueBounds( result.getColumnByName( QUEUE_OLDEST ).getValue(), result.getColumnByName( QUEUE_NEWEST ).getValue() ); } } catch ( Exception e ) { logger.error( "Error getting oldest queue message ID", e ); } return null; } /** * Write the updated client pointer * * @param lastReturnedId This is a null safe parameter. If it's null, this won't be written since it means we didn't * read any messages */ protected void writeClientPointer( UUID queueId, UUID consumerId, UUID lastReturnedId ) { // nothing to do if ( lastReturnedId == null ) { return; } // we want to set the timestamp to the value from the time uuid. If this is // not the max time uuid to ever be written // for this consumer, we want this to be discarded to avoid internode race // conditions with clock drift. long colTimestamp = UUIDUtils.getTimestampInMicros( lastReturnedId ); Mutator<UUID> mutator = CountingMutator.createFlushingMutator( ko, ue ); if ( logger.isDebugEnabled() ) { logger.debug( "Writing last client id pointer of '{}' for queue '{}' and consumer '{}' with timestamp '{}", lastReturnedId, queueId, consumerId, colTimestamp ); } mutator.addInsertion( consumerId, CONSUMERS.getColumnFamily(), createColumn( queueId, lastReturnedId, colTimestamp, ue, ue ) ); mutator.execute(); } private class RequestedOrderComparator implements Comparator<Message> { private Map<UUID, Integer> indexCache = new HashMap<UUID, Integer>(); private RequestedOrderComparator( Collection<UUID> ids ) { int i = 0; for ( UUID id : ids ) { indexCache.put( id, i ); i++; } } /* * (non-Javadoc) * * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) */ @Override public int compare( Message o1, Message o2 ) { int o1Idx = indexCache.get( o1.getUuid() ); int o2Idx = indexCache.get( o2.getUuid() ); return o1Idx - o2Idx; } } protected static final class MessageIdComparator implements Comparator<UUID> { private final int comparator; protected MessageIdComparator( final boolean reversed ) { this.comparator = reversed ? -1 : 1; } @Override public int compare( final UUID o1, final UUID o2 ) { return UUIDUtils.compare( o1, o2 ) * comparator; } } /** * This method intentionally swallows ordered execution issues. For some reason, our Time UUID ordering does * not agree with the cassandra comparator as our micros get very close * @param query * @param <K> * @param <UUID> * @param <V> * @return */ protected static <K, UUID, V> List<HColumn<UUID, V>> swallowOrderedExecution( final SliceQuery<K, UUID, V> query ) { try { return query.execute().get().getColumns(); } catch ( HInvalidRequestException e ) { //invalid request. Occasionally we get order issues when there shouldn't be, disregard them. final Throwable invalidRequestException = e.getCause(); if ( invalidRequestException instanceof InvalidRequestException //we had a range error && ( ( InvalidRequestException ) invalidRequestException ).getWhy().contains( "range finish must come after start in the order of traversal" )) { return Collections.emptyList(); } throw e; } } }