/**
* Copyright 2013-2015 Seagate Technology LLC.
*
* This Source Code Form is subject to the terms of the Mozilla
* Public License, v. 2.0. If a copy of the MPL was not
* distributed with this file, You can obtain one at
* https://mozilla.org/MP:/2.0/.
*
* This program is distributed in the hope that it will be useful,
* but is provided AS-IS, WITHOUT ANY WARRANTY; including without
* the implied warranty of MERCHANTABILITY, NON-INFRINGEMENT or
* FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public
* License for more details.
*
* See www.openkinetic.org for more project information
*/
package com.seagate.kinetic.client.io;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import kinetic.client.AsyncKineticException;
import kinetic.client.ClientConfiguration;
import kinetic.client.Entry;
import kinetic.client.EntryMetadata;
import kinetic.client.EntryNotFoundException;
import kinetic.client.KineticException;
import com.seagate.kinetic.client.internal.CallbackContext;
import com.seagate.kinetic.client.internal.ClientProxy;
import com.seagate.kinetic.client.internal.MessageFactory;
import com.seagate.kinetic.client.internal.async.DeleteAsyncCallbackHandler;
import com.seagate.kinetic.client.internal.async.GetAsyncCallbackHandler;
import com.seagate.kinetic.client.internal.async.GetKeyRangeAsyncCallbackHandler;
import com.seagate.kinetic.client.internal.async.GetMetadataAsyncCallbackHandler;
import com.seagate.kinetic.client.internal.async.PutAsyncCallbackHandler;
import com.seagate.kinetic.client.io.provider.spi.ClientMessageService;
import com.seagate.kinetic.common.lib.KineticMessage;
import com.seagate.kinetic.common.lib.ProtocolMessageUtil;
import com.seagate.kinetic.proto.Kinetic.Command.MessageType;
import com.seagate.kinetic.proto.Kinetic.Message.AuthType;
/**
*
* Kinetic Client message handler.
* <p>
*
* @author chiaming Yang
* @param <AsyncCallbackHandler>
*
*/
public class MessageHandler implements ClientMessageService, Runnable {
// my logger
private final static Logger logger = Logger.getLogger(MessageHandler.class
.getName());
// XXX 07172013 chiaming: make it elastic.
private Thread myThread = null;
// my message queue
private final LinkedBlockingQueue<KineticMessage> asyncQueue = new LinkedBlockingQueue<KineticMessage>();
private final Map<Long, Object> ackmap = new ConcurrentHashMap<Long, Object>();
// flag running flag
private volatile boolean isRunning = false;
// close flag
private volatile boolean isClosed = false;
private ClientProxy client = null;
// io handler
private IoHandler iohandler = null;
private int asyncQueuedSize = 10;
// request timeout
private long requestTimeout = 30000;
private final Object syncObj = new Object();
private boolean isStatusMessageReceived = false;
/**
* Constructor.
*
* @param transport
* the iohandler associated with this message handler.
*/
public MessageHandler(IoHandler iohandler) {
this.iohandler = iohandler;
this.client = iohandler.getClient();
this.asyncQueuedSize = this.client.getConfiguration()
.getAsyncQueueSize();
this.requestTimeout = this.client.getConfiguration().getRequestTimeoutMillis();
}
/**
* process message from IoHandler.
*
* @param message
* message from IoHandler
*
* @throws InterruptedException
* if interrupted.
*/
@Override
@SuppressWarnings("unchecked")
public void routeMessage(KineticMessage message)
throws InterruptedException {
if (logger.isLoggable(Level.FINEST)) {
logger.info("read/routing message: " + message);
}
/**
* check status message has received.
*/
if (this.isStatusMessageReceived == false) {
if (message.getMessage().getAuthType() == AuthType.UNSOLICITEDSTATUS) {
// set cid
this.client.setConnectionId(message);
this.isStatusMessageReceived = true;
// notify listener
this.notifyListener(message);
return;
} else {
if (this.iohandler.shouldWaitForStatusMessage()) {
logger.warning("received unexpected message ..." + message.getMessage() + ", command=" + message.getCommand());
}
}
}
Long seq = Long.valueOf(message.getCommand().getHeader()
.getAckSequence());
Object obj = this.ackmap.get(seq);
// check if sync request
if (obj != null && (obj instanceof LinkedBlockingQueue)) {
// sync request
((LinkedBlockingQueue<KineticMessage>) obj).put(message);
} else {
// async request
// this.asyncQueue.put(message);
this.putAndCheckRunning(message);
}
}
@Override
public void run() {
try {
if (logger.isLoggable(Level.FINEST)) {
logger.finest("thread started, name=" + this.myThread.getName());
}
while (isRunning && !isClosed) {
// poll message from queue
KineticMessage msg;
try {
msg = this.asyncQueue.poll(7000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.info("Interrupted");
isClosed = true;
exitRunning();
break;
}
if (msg != null) {
// process message
doProcessMessage(msg);
} else {
// exit thread
exitRunning();
}
}
} catch (Throwable t) {
logger.log(Level.WARNING, "Main run loop failed", t);
isClosed = true;
} finally {
if (logger.isLoggable(Level.FINEST)) {
logger.finest("thread exited ." + this.myThread.getName());
}
}
}
/**
* process message.
*
* @param message
* message from IoHandler.
* @throws InterruptedException
*/
private void doProcessMessage(KineticMessage message)
throws InterruptedException {
// get ack seq
Long seq = Long.valueOf(message.getCommand().getHeader()
.getAckSequence());
// get callback instance
Object context = this.ackmap.get(seq);
if (context != null) {
try {
if (context instanceof CallbackContext) {
// invoke callback handler
invokeCallbackHandler(context, message);
} else {
logger.warning("received unknown message: " + message);
}
} finally {
// this.ackmap.remove(seq);
this.asyncDelivered(seq);
}
} else {
if (message.getMessage().getAuthType() == AuthType.UNSOLICITEDSTATUS) {
/**
* log unsolicited status message.
*/
logger.warning("received unsolicited message: "
+ message.getCommand().getStatus().getCode() + ":"
+ message.getCommand().getStatus().getStatusMessage());
/**
* XXX chiaming 01/28/2015: The only possible behavior from the
* drive is to close the current connection. So the API needs to
* handle this accordingly.
*/
this.client.close();
/**
* Call listener if one is set.
*/
this.notifyListener(message);
} else {
logger.warning("message cannot be delivered., please verify request timeout set in the configurtion., message="
+ ProtocolMessageUtil.toString(message));
}
}
}
private void notifyListener(KineticMessage message) {
try {
if (this.client.getConfiguration().getConnectionListener() != null) {
// call listener
this.client.getConfiguration().getConnectionListener()
.onMessage(message);
}
} catch (Throwable t) {
logger.log(Level.WARNING, t.getMessage(), t);
}
}
public KineticMessage write(KineticMessage message) throws IOException,
InterruptedException {
LinkedBlockingQueue<KineticMessage> lbq = new LinkedBlockingQueue<KineticMessage>(
1);
KineticMessage respond = null;
Long seq = 0L;
try {
synchronized (this) {
this.client.finalizeHeader(message);
seq = Long.valueOf(message.getCommand().getHeader()
.getSequence());
this.ackmap.put(seq, lbq);
// this.iohandler.write(message);
this.doWrite(message);
}
if (this.isClosed) {
throw new IOException("Connection is closed.");
} else {
respond = lbq.poll(this.requestTimeout, TimeUnit.MILLISECONDS);
}
} finally {
this.ackmap.remove(seq);
}
return respond;
}
public synchronized void writeAsync(KineticMessage message, Object context)
throws IOException,
InterruptedException {
this.client.finalizeHeader(message);
Long seq = Long.valueOf(message.getCommand().getHeader()
.getSequence());
while (ackmap.size() >= asyncQueuedSize && (isClosed == false)) {
this.wait();
}
this.ackmap.put(seq, context);
this.doWrite(message);
}
public synchronized void writeNoAck(KineticMessage message)
throws IOException {
this.client.finalizeHeader(message);
this.doWrite(message);
}
@SuppressWarnings("rawtypes")
private void invokeCallbackHandler(Object cbContext, KineticMessage response) {
MessageType type = response.getCommand().getHeader()
.getMessageType();
AsyncKineticException exception = this
.asyncResponseHmacCheck(response);
switch (type) {
case PUT_RESPONSE:
new PutAsyncCallbackHandler().onAsyncMessage(cbContext,
response, exception);
break;
case GET_RESPONSE:
boolean isMetadataOnly = ((CallbackContext) cbContext)
.getRequestMessage().getCommand().getBody()
.getKeyValue().getMetadataOnly();
if (isMetadataOnly)
{
new GetMetadataAsyncCallbackHandler(MessageType.GET_RESPONSE).onAsyncMessage(
cbContext, response,
exception);
}else
{
new GetAsyncCallbackHandler(MessageType.GET_RESPONSE).onAsyncMessage(
cbContext, response,
exception);
}
break;
case GETKEYRANGE_RESPONSE:
new GetKeyRangeAsyncCallbackHandler(MessageType.GETKEYRANGE_RESPONSE).onAsyncMessage(
cbContext, response,
exception);
break;
case GETNEXT_RESPONSE:
new GetAsyncCallbackHandler(MessageType.GETNEXT_RESPONSE)
.onAsyncMessage(cbContext, response, exception);
break;
case GETPREVIOUS_RESPONSE:
new GetAsyncCallbackHandler(MessageType.GETPREVIOUS_RESPONSE)
.onAsyncMessage(cbContext, response, exception);
break;
case DELETE_RESPONSE:
new DeleteAsyncCallbackHandler().onAsyncMessage(cbContext,
response, exception);
break;
default:
break;
}
}
public static AsyncKineticException checkPutReply(
CallbackContext<Entry> context) {
AsyncKineticException lce = null;
try {
MessageFactory.checkReply(context.getRequestMessage(), context.getResponseMessage());
} catch (KineticException e) {
lce = getAsyncKineticException(context, e);
}
return lce;
}
public static AsyncKineticException checkGetReply(
CallbackContext<Entry> context, MessageType messageType) {
AsyncKineticException lce = null;
try {
MessageFactory.checkReply(context.getRequestMessage(), context.getResponseMessage());
} catch (EntryNotFoundException enfe) {
//entry not found will return null entry to applications
;
} catch (KineticException e) {
lce = new AsyncKineticException(lce);
lce.setRequestMessage(context.getRequestMessage());
lce.setResponseMessage(context.getResponseMessage());
}
return lce;
}
public static AsyncKineticException checkGetKeyRangeReply(
CallbackContext<List<byte[]>> context, MessageType messageType) {
AsyncKineticException lce = null;
try {
MessageFactory.checkReply(context.getRequestMessage(), context.getResponseMessage());
} catch (KineticException e) {
lce = new AsyncKineticException(lce);
lce.setRequestMessage(context.getRequestMessage());
lce.setResponseMessage(context.getResponseMessage());
}
return lce;
}
public static AsyncKineticException checkGetMetadataReply(
CallbackContext<EntryMetadata> context, MessageType messageType) {
AsyncKineticException lce = null;
try {
MessageFactory.checkReply(context.getRequestMessage(), context.getResponseMessage());
} catch (EntryNotFoundException enfe) {
//entry not found will return null to applications
;
} catch (KineticException e) {
lce = new AsyncKineticException(lce);
lce.setRequestMessage(context.getRequestMessage());
lce.setResponseMessage(context.getResponseMessage());
}
return lce;
}
public static AsyncKineticException checkDeleteReply(
CallbackContext<Boolean> context) {
AsyncKineticException lce = null;
try {
MessageFactory.checkDeleteReply(context.getRequestMessage(), context.getResponseMessage());
} catch (KineticException e) {
lce = new AsyncKineticException(e);
lce.setRequestMessage(context.getRequestMessage());
lce.setResponseMessage(context.getResponseMessage());
}
return lce;
}
private static AsyncKineticException getAsyncKineticException(
CallbackContext<?> context, KineticException lce) {
AsyncKineticException alce = new AsyncKineticException(lce);
alce.setRequestMessage(context.getRequestMessage());
alce.setResponseMessage(context.getResponseMessage());
return alce;
}
private AsyncKineticException asyncResponseHmacCheck(KineticMessage response) {
AsyncKineticException asyncException = null;
/**
* Pin Auth does not require Hmac calculation.
*/
if (response.getMessage().getAuthType() == AuthType.HMACAUTH) {
if (this.client.checkHmac(response) == false) {
asyncException = new AsyncKineticException(
"Hmac did not compare");
}
}
return asyncException;
}
@Override
public ClientConfiguration getConfiguration() {
return this.client.getConfiguration();
}
private void doWrite(KineticMessage message) throws IOException {
if (logger.isLoggable(Level.FINEST)) {
logger.info("writing message: " + message);
}
this.iohandler.write(message);
}
/**
* Close the message handler.
*/
@Override
public void close() {
this.isClosed = true;
this.isRunning = false;
if (this.myThread != null) {
this.myThread.interrupt();
}
// wake up sync callers
this.wakeupSyncCallers();
// wakes up all wait threads for this instance.
synchronized (this) {
this.notifyAll();
}
}
@SuppressWarnings("unchecked")
private void wakeupSyncCallers() {
// logger.info("waking up sync callers ...");
for (Long id : this.ackmap.keySet().toArray(new Long[0])) {
Object obj = this.ackmap.get(id);
try {
if (obj instanceof LinkedBlockingQueue) {
// the connection is closed, unblock callers
((LinkedBlockingQueue<KineticMessage>) obj).put(new KineticMessage());
}
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
}
private synchronized void asyncDelivered(Long key) {
this.ackmap.remove(key);
this.notifyAll();
}
/**
* put message in queue and start a new thread to dispatch if none is
* started.
*
* @param message
* message to put in the async queue
* @throws InterruptedException
* if interrupted.
*/
private void putAndCheckRunning(KineticMessage message)
throws InterruptedException {
// check if the client is closed already.
if (this.isClosed) {
return;
}
this.asyncQueue.put(message);
synchronized (syncObj) {
// client is still open, check if there is a thread running and
// dispatching messages
// if not, created one and start it.
if (isRunning == false) {
// instantiate a new thread
this.myThread = new Thread(this);
// set identity
this.myThread.setName("ClientMessageHandler-"
+ client.getConfiguration().getHost() + "-"
+ client.getConfiguration().getPort());
// set running flag
this.isRunning = true;
// start dispatching messages
this.myThread.start();
}
}
}
/**
* check if the thread should exit.
*/
private void exitRunning() {
if (this.isClosed) {
return;
}
synchronized (syncObj) {
// only set flag to false if queue is empty
if (this.asyncQueue.isEmpty()) {
this.isRunning = false;
}
}
}
}