/*
* Copyright 2009 Red Hat, Inc.
* Red Hat 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.hornetq.core.protocol.core.impl;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.hornetq.api.core.HornetQBuffer;
import org.hornetq.api.core.HornetQException;
import org.hornetq.api.core.HornetQInterruptedException;
import org.hornetq.api.core.Interceptor;
import org.hornetq.core.client.HornetQClientLogger;
import org.hornetq.core.client.HornetQClientMessageBundle;
import org.hornetq.core.protocol.core.Channel;
import org.hornetq.core.protocol.core.ChannelHandler;
import org.hornetq.core.protocol.core.CommandConfirmationHandler;
import org.hornetq.core.protocol.core.CoreRemotingConnection;
import org.hornetq.core.protocol.core.Packet;
import org.hornetq.core.protocol.core.impl.wireformat.HornetQExceptionMessage;
import org.hornetq.core.protocol.core.impl.wireformat.PacketsConfirmedMessage;
import org.hornetq.spi.core.protocol.RemotingConnection;
/**
* A ChannelImpl
*
* @author <a href="mailto:tim.fox@jboss.com">Tim Fox</a>
*/
public final class ChannelImpl implements Channel
{
public enum CHANNEL_ID {
/**
* Used for core protocol management.
* @see CoreProtocolManager
*/
PING(0),
/** Session creation and attachment. */
SESSION(1),
/** Replication, i.e. for backups that do not share the journal. */
REPLICATION(2),
/**
* Channels [0-9] are reserved for the system, user channels must be greater than that.
*/
USER(10);
public final long id;
CHANNEL_ID(long id)
{
this.id = id;
}
protected static String idToString(long code)
{
for (CHANNEL_ID channel:EnumSet.allOf(CHANNEL_ID.class)){
if (channel.id==code) return channel.toString();
}
return Long.toString(code);
}
}
private static final boolean isTrace = HornetQClientLogger.LOGGER.isTraceEnabled();
private volatile long id;
private ChannelHandler handler;
private Packet response;
private final java.util.Queue<Packet> resendCache;
private volatile int firstStoredCommandID;
private final AtomicInteger lastConfirmedCommandID = new AtomicInteger(-1);
private volatile CoreRemotingConnection connection;
private volatile boolean closed;
private final Lock lock = new ReentrantLock();
private final Condition sendCondition = lock.newCondition();
private final Condition failoverCondition = lock.newCondition();
private final Object sendLock = new Object();
private final Object sendBlockingLock = new Object();
private boolean failingOver;
private final int confWindowSize;
private int receivedBytes;
private CommandConfirmationHandler commandConfirmationHandler;
private volatile boolean transferring;
private final List<Interceptor> interceptors;
public ChannelImpl(final CoreRemotingConnection connection, final long id, final int confWindowSize, final List<Interceptor> interceptors)
{
this.connection = connection;
this.id = id;
this.confWindowSize = confWindowSize;
if (confWindowSize != -1)
{
resendCache = new ConcurrentLinkedQueue<Packet>();
}
else
{
resendCache = null;
}
this.interceptors = interceptors;
}
public boolean supports(final byte packetType)
{
int version = connection.getClientVersion();
switch (packetType)
{
case PacketImpl.CLUSTER_TOPOLOGY_V2:
return version >= 122;
default:
return true;
}
}
public long getID()
{
return id;
}
public int getLastConfirmedCommandID()
{
return lastConfirmedCommandID.get();
}
public Lock getLock()
{
return lock;
}
public int getConfirmationWindowSize()
{
return confWindowSize;
}
public void returnBlocking()
{
lock.lock();
try
{
response = new HornetQExceptionMessage(HornetQClientMessageBundle.BUNDLE.unblockingACall());
sendCondition.signal();
}
finally
{
lock.unlock();
}
}
public boolean sendAndFlush(final Packet packet)
{
return send(packet, true, false);
}
public boolean send(final Packet packet)
{
return send(packet, false, false);
}
public boolean sendBatched(final Packet packet)
{
return send(packet, false, true);
}
public void setTransferring(boolean transferring)
{
this.transferring = transferring;
}
// This must never called by more than one thread concurrently
public boolean send(final Packet packet, final boolean flush, final boolean batch)
{
if (invokeInterceptors(packet, interceptors, connection) != null)
{
return false;
}
synchronized (sendLock)
{
packet.setChannelID(id);
if (isTrace)
{
HornetQClientLogger.LOGGER.trace("Sending packet nonblocking " + packet + " on channeID=" + id);
}
HornetQBuffer buffer = packet.encode(connection);
lock.lock();
try
{
if (failingOver)
{
// TODO - don't hardcode this timeout
try
{
failoverCondition.await(10000, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e)
{
throw new HornetQInterruptedException(e);
}
}
// Sanity check
if (transferring)
{
throw new IllegalStateException("Cannot send a packet while channel is doing failover");
}
if (resendCache != null && packet.isRequiresConfirmations())
{
resendCache.add(packet);
}
}
finally
{
lock.unlock();
}
if (isTrace)
{
HornetQClientLogger.LOGGER.trace("Writing buffer for channelID=" + id);
}
// The actual send must be outside the lock, or with OIO transport, the write can block if the tcp
// buffer is full, preventing any incoming buffers being handled and blocking failover
connection.getTransportConnection().write(buffer, flush, batch);
return true;
}
}
public Packet sendBlocking(final Packet packet) throws HornetQException
{
String interceptionResult = invokeInterceptors(packet, interceptors, connection);
if (interceptionResult != null)
{
// if we don't throw an exception here the client might not unblock
throw HornetQClientMessageBundle.BUNDLE.interceptorRejectedPacket(interceptionResult);
}
if (closed)
{
throw HornetQClientMessageBundle.BUNDLE.connectionDestroyed();
}
if (connection.getBlockingCallTimeout() == -1)
{
throw new IllegalStateException("Cannot do a blocking call timeout on a server side connection");
}
// Synchronized since can't be called concurrently by more than one thread and this can occur
// E.g. blocking acknowledge() from inside a message handler at some time as other operation on main thread
synchronized (sendBlockingLock)
{
packet.setChannelID(id);
final HornetQBuffer buffer = packet.encode(connection);
lock.lock();
try
{
if (failingOver)
{
try
{
if(connection.getBlockingCallFailoverTimeout() < 0)
{
while (failingOver)
{
failoverCondition.await();
}
}
else
{
if (!failoverCondition.await(connection.getBlockingCallFailoverTimeout(), TimeUnit.MILLISECONDS))
{
HornetQClientLogger.LOGGER.debug("timed-out waiting for failover condition");
}
}
}
catch (InterruptedException e)
{
throw new HornetQInterruptedException(e);
}
}
response = null;
if (resendCache != null && packet.isRequiresConfirmations())
{
resendCache.add(packet);
}
connection.getTransportConnection().write(buffer, false, false);
long toWait = connection.getBlockingCallTimeout();
long start = System.currentTimeMillis();
while (!closed && response == null && toWait > 0)
{
try
{
sendCondition.await(toWait, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e)
{
throw new HornetQInterruptedException(e);
}
if (closed)
{
break;
}
final long now = System.currentTimeMillis();
toWait -= now - start;
start = now;
}
if (response == null)
{
throw HornetQClientMessageBundle.BUNDLE.timedOutSendingPacket(packet.getType());
}
if (response.getType() == PacketImpl.EXCEPTION)
{
final HornetQExceptionMessage mem = (HornetQExceptionMessage)response;
HornetQException e = mem.getException();
e.fillInStackTrace();
throw e;
}
}
finally
{
lock.unlock();
}
return response;
}
}
/**
*
* @param packet the packet to intercept
* @return the name of the interceptor that returned <code>false</code> or <code>null</code> if no interceptors
* returned <code>false</code>.
*/
public static String invokeInterceptors(final Packet packet, final List<Interceptor> interceptors, final RemotingConnection connection)
{
if (interceptors != null)
{
for (final Interceptor interceptor : interceptors)
{
try
{
boolean callNext = interceptor.intercept(packet, connection);
if (HornetQClientLogger.LOGGER.isDebugEnabled())
{
// use a StringBuilder for speed since this may be executed a lot
StringBuilder msg = new StringBuilder();
msg.append("Invocation of interceptor ").append(interceptor.getClass().getName()).append(" on ").
append(packet).append(" returned ").append(callNext);
HornetQClientLogger.LOGGER.debug(msg.toString());
}
if (!callNext)
{
return interceptor.getClass().getName();
}
}
catch (final Throwable e)
{
HornetQClientLogger.LOGGER.errorCallingInterceptor(e, interceptor);
}
}
}
return null;
}
public void setCommandConfirmationHandler(final CommandConfirmationHandler handler)
{
if (confWindowSize < 0)
{
final String msg =
"You can't set confirmationHandler on a connection with confirmation-window-size < 0."
+ " Look at the documentation for more information.";
throw new IllegalStateException(msg);
}
commandConfirmationHandler = handler;
}
public void setHandler(final ChannelHandler handler)
{
this.handler = handler;
}
public void close()
{
if (closed)
{
return;
}
if (!connection.isDestroyed() && !connection.removeChannel(id))
{
throw HornetQClientMessageBundle.BUNDLE.noChannelToClose(id);
}
if(failingOver)
{
unlock();
}
closed = true;
}
public void transferConnection(final CoreRemotingConnection newConnection)
{
// Needs to synchronize on the connection to make sure no packets from
// the old connection get processed after transfer has occurred
synchronized (connection.getTransferLock())
{
connection.removeChannel(id);
// And switch it
final CoreRemotingConnection rnewConnection = newConnection;
rnewConnection.putChannel(id, this);
connection = rnewConnection;
transferring = true;
}
}
public void replayCommands(final int otherLastConfirmedCommandID)
{
if (resendCache != null)
{
if (isTrace)
{
HornetQClientLogger.LOGGER.trace("Replaying commands on channelID=" + id);
}
clearUpTo(otherLastConfirmedCommandID);
for (final Packet packet : resendCache)
{
doWrite(packet);
}
}
}
public void lock()
{
lock.lock();
failingOver = true;
lock.unlock();
}
public void unlock()
{
lock.lock();
failingOver = false;
failoverCondition.signalAll();
lock.unlock();
}
public CoreRemotingConnection getConnection()
{
return connection;
}
// Needs to be synchronized since can be called by remoting service timer thread too for timeout flush
public synchronized void flushConfirmations()
{
if (resendCache != null && receivedBytes != 0)
{
receivedBytes = 0;
final Packet confirmed = new PacketsConfirmedMessage(lastConfirmedCommandID.get());
confirmed.setChannelID(id);
doWrite(confirmed);
}
}
public void confirm(final Packet packet)
{
if (resendCache != null && packet.isRequiresConfirmations())
{
lastConfirmedCommandID.incrementAndGet();
receivedBytes += packet.getPacketSize();
if (receivedBytes >= confWindowSize)
{
receivedBytes = 0;
final Packet confirmed = new PacketsConfirmedMessage(lastConfirmedCommandID.get());
confirmed.setChannelID(id);
doWrite(confirmed);
}
}
}
public void clearCommands()
{
if (resendCache != null)
{
lastConfirmedCommandID.set(-1);
firstStoredCommandID = 0;
resendCache.clear();
}
}
public void handlePacket(final Packet packet)
{
if (packet.getType() == PacketImpl.PACKETS_CONFIRMED)
{
if (resendCache != null)
{
final PacketsConfirmedMessage msg = (PacketsConfirmedMessage)packet;
clearUpTo(msg.getCommandID());
}
if (!connection.isClient())
{
handler.handlePacket(packet);
}
return;
}
else
{
if (packet.isResponse())
{
confirm(packet);
lock.lock();
try
{
response = packet;
sendCondition.signal();
}
finally
{
lock.unlock();
}
}
else if (handler != null)
{
handler.handlePacket(packet);
}
}
}
private void doWrite(final Packet packet)
{
final HornetQBuffer buffer = packet.encode(connection);
connection.getTransportConnection().write(buffer, false, false);
}
private void clearUpTo(final int lastReceivedCommandID)
{
final int numberToClear = 1 + lastReceivedCommandID - firstStoredCommandID;
if (numberToClear == -1)
{
throw HornetQClientMessageBundle.BUNDLE.invalidCommandID(lastReceivedCommandID);
}
int sizeToFree = 0;
for (int i = 0; i < numberToClear; i++)
{
final Packet packet = resendCache.poll();
if (packet == null)
{
HornetQClientLogger.LOGGER.cannotFindPacketToClear(lastReceivedCommandID, firstStoredCommandID);
firstStoredCommandID = lastReceivedCommandID + 1;
return;
}
if (packet.getType() != PacketImpl.PACKETS_CONFIRMED)
{
sizeToFree += packet.getPacketSize();
}
if (commandConfirmationHandler != null)
{
commandConfirmationHandler.commandConfirmed(packet);
}
}
firstStoredCommandID += numberToClear;
}
@Override
public String toString()
{
return "Channel[id=" + CHANNEL_ID.idToString(id) + ", handler=" + handler + "]";
}
}