/*
* 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.qakka.core.impl;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.usergrid.persistence.actorsystem.ActorSystemFig;
import org.apache.usergrid.persistence.qakka.api.URIStrategy;
import org.apache.usergrid.persistence.qakka.core.*;
import org.apache.usergrid.persistence.qakka.distributed.DistributedQueueService;
import org.apache.usergrid.persistence.qakka.exceptions.BadRequestException;
import org.apache.usergrid.persistence.qakka.exceptions.NotFoundException;
import org.apache.usergrid.persistence.qakka.exceptions.QakkaRuntimeException;
import org.apache.usergrid.persistence.qakka.serialization.queuemessages.DatabaseQueueMessage;
import org.apache.usergrid.persistence.qakka.serialization.queuemessages.DatabaseQueueMessageBody;
import org.apache.usergrid.persistence.qakka.serialization.queuemessages.MessageCounterSerialization;
import org.apache.usergrid.persistence.qakka.serialization.queuemessages.QueueMessageSerialization;
import org.apache.usergrid.persistence.qakka.serialization.sharding.ShardSerialization;
import org.apache.usergrid.persistence.qakka.serialization.transferlog.TransferLogSerialization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Singleton
public class QueueMessageManagerImpl implements QueueMessageManager {
private static final Logger logger = LoggerFactory.getLogger( QueueMessageManagerImpl.class );
private final ActorSystemFig actorSystemFig;
private final QueueManager queueManager;
private final QueueMessageSerialization queueMessageSerialization;
private final DistributedQueueService distributedQueueService;
private final TransferLogSerialization transferLogSerialization;
private final URIStrategy uriStrategy;
private final MessageCounterSerialization messageCounterSerialization;
private final ShardSerialization shardSerialization;
private final CassandraClient cassandraClient;
@Inject
public QueueMessageManagerImpl(
ActorSystemFig actorSystemFig,
QueueManager queueManager,
QueueMessageSerialization queueMessageSerialization,
DistributedQueueService distributedQueueService,
TransferLogSerialization transferLogSerialization,
URIStrategy uriStrategy,
MessageCounterSerialization messageCounterSerialization,
ShardSerialization shardSerialization,
CassandraClient cassandraClient ) {
this.actorSystemFig = actorSystemFig;
this.queueManager = queueManager;
this.queueMessageSerialization = queueMessageSerialization;
this.distributedQueueService = distributedQueueService;
this.transferLogSerialization = transferLogSerialization;
this.uriStrategy = uriStrategy;
this.messageCounterSerialization = messageCounterSerialization;
this.shardSerialization = shardSerialization;
this.cassandraClient = cassandraClient;
}
@Override
public void sendMessages(String queueName, List<String> destinationRegions,
Long delayMs, Long expirationSecs, String contentType, ByteBuffer messageData) {
if ( queueManager.getQueueConfig( queueName ) == null ) {
throw new NotFoundException( "Queue " + queueName + " not found" );
}
logger.trace("Sending message to queue {} regions {}", queueName, destinationRegions);
// TODO: implement delay and expiration
// Preconditions.checkArgument(delayMs == null || delayMs > 0L,
// "Delay milliseconds must be greater than zero");
// Preconditions.checkArgument(expirationSecs == null || expirationSecs > 0L,
// "Expiration seconds must be greater than zero");
// get current time
Long currentTimeMs = System.currentTimeMillis();
// create message id
UUID messageId = QakkaUtils.getTimeUuid();
Long deliveryTime = delayMs != null ? currentTimeMs + delayMs : null;
Long expirationTime = expirationSecs != null ? currentTimeMs + (1000 * expirationSecs) : null;
// write message data to C*
queueMessageSerialization.writeMessageData(
messageId, new DatabaseQueueMessageBody(messageData, contentType));
for (String region : destinationRegions) {
transferLogSerialization.recordTransferLog(
queueName, actorSystemFig.getRegionLocal(), region, messageId );
// send message to destination region's queue
DistributedQueueService.Status status = null;
try {
status = distributedQueueService.sendMessageToRegion(
queueName,
actorSystemFig.getRegionLocal(),
region,
messageId,
deliveryTime,
expirationTime );
//logger.debug("Send message to queueName {} in region {}", queueName, region );
} catch ( QakkaRuntimeException qae ) {
logger.error("Error sending message " + messageId + " to " + region, qae);
}
}
}
@Override
public List<QueueMessage> getNextMessages(String queueName, int count) {
Collection<DatabaseQueueMessage> dbMessages = distributedQueueService.getNextMessages( queueName, count );
List<QueueMessage> queueMessages = joinMessages( queueName, dbMessages );
if ( queueMessages.size() < count && queueMessages.size() < dbMessages.size() ) {
logger.debug("Messages failed to join for queue:{}, get more", queueName);
// some messages failed to join, get more
dbMessages = distributedQueueService.getNextMessages( queueName, count - queueMessages.size() );
queueMessages.addAll( joinMessages( queueName, dbMessages ) );
}
return queueMessages;
}
private List<QueueMessage> joinMessages( String queueName, Collection<DatabaseQueueMessage> dbMessages) {
List<QueueMessage> queueMessages = new ArrayList<>();
for (DatabaseQueueMessage dbMessage : dbMessages) {
DatabaseQueueMessageBody data = queueMessageSerialization.loadMessageData( dbMessage.getMessageId() );
if ( data != null ) {
QueueMessage queueMessage = new QueueMessage(
dbMessage.getQueueMessageId(),
queueName,
null, // sending region
dbMessage.getRegion(), // receiving region
dbMessage.getMessageId(),
null, // delay until date
null, // expiration date
dbMessage.getQueuedAt(),
null, // retries
true );
queueMessage.setContentType( data.getContentType() );
if ( "application/json".equals( data.getContentType() )) {
try {
String json = new String( data.getBlob().array(), "UTF-8");
queueMessage.setData( json );
} catch (UnsupportedEncodingException e) {
logger.error("Error decoding data for messageId=" + queueMessage.getMessageId(), e);
}
} else {
try {
queueMessage.setHref( uriStrategy.queueMessageDataURI(
queueName, queueMessage.getQueueMessageId()).toString());
} catch (URISyntaxException e) {
throw new QakkaRuntimeException( "Error forming URI for message data", e );
}
}
queueMessages.add( queueMessage );
} else if ( (System.currentTimeMillis() - dbMessage.getQueuedAt()) > TimeUnit.HOURS.toMillis(2) ) {
logger.warn("Queue Message does not have corresponding data after 2 hours, removing from queue - " +
"queueName: {}, region: {}, queueMessageId: {}", dbMessage.getQueueName(), dbMessage.getRegion(),
dbMessage.getQueueMessageId());
queueMessageSerialization.deleteMessage(dbMessage.getQueueName(), dbMessage.getRegion(),
dbMessage.getShardId(), dbMessage.getType(), dbMessage.getQueueMessageId());
}
}
return queueMessages;
}
@Override
public void ackMessage(String queueName, UUID queueMessageId) {
DistributedQueueService.Status status = distributedQueueService.ackMessage( queueName, queueMessageId );
if ( DistributedQueueService.Status.NOT_INFLIGHT.equals( status )) {
throw new BadRequestException( "Message not inflight" );
} else if ( DistributedQueueService.Status.BAD_REQUEST.equals( status )) {
throw new BadRequestException( "Bad request" );
} else if ( DistributedQueueService.Status.ERROR.equals( status )) {
throw new QakkaRuntimeException( "Unable to ack message due to error" );
}
}
@Override
public void requeueMessage(String queueName, UUID messageId, Long delayMs) {
// TODO: implement requeueMessage
throw new UnsupportedOperationException( "requeueMessage not yet implemented" );
}
// TODO: implement delete of message data too
// @Override
// public void clearMessageData( queueName ) {
// }
@Override
public void clearMessages( String queueName ) {
queueMessageSerialization.deleteAllMessages( queueName );
shardSerialization.deleteAllShards( queueName, actorSystemFig.getRegionLocal() );
}
@Override
public ByteBuffer getMessageData( UUID messageId ) {
DatabaseQueueMessageBody body = queueMessageSerialization.loadMessageData( messageId );
return body != null ? body.getBlob() : null;
}
/**
* Get but do not put inflight specified queue message, first looking in INFLIGHT table then DEFAULT.
*/
@Override
public QueueMessage getMessage( String queueName, UUID queueMessageId ) {
QueueMessage queueMessage = null;
// first look in INFLIGHT storage
DatabaseQueueMessage dbMessage = queueMessageSerialization.loadMessage(
queueName, actorSystemFig.getRegionLocal(), null,
DatabaseQueueMessage.Type.INFLIGHT, queueMessageId );
if ( dbMessage == null ) {
// not found, so now look in DEFAULT storage
dbMessage = queueMessageSerialization.loadMessage(
queueName, actorSystemFig.getRegionLocal(), null,
DatabaseQueueMessage.Type.DEFAULT, queueMessageId );
}
if ( dbMessage != null ) {
queueMessage = new QueueMessage(
dbMessage.getQueueMessageId(),
queueName,
null, // sending region
dbMessage.getRegion(), // receiving region
dbMessage.getMessageId(),
null, // delay until date
null, // expiration date
dbMessage.getQueuedAt(),
null, // retries
true );
}
return queueMessage;
}
@Override
public long getQueueDepth( String queueName, DatabaseQueueMessage.Type type ) {
return messageCounterSerialization.getCounterValue( queueName, type );
}
}