/* * Copyright (c) 2001-2007 Sun Microsystems, Inc. All rights reserved. * * The Sun Project JXTA(TM) Software License * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The end-user documentation included with the redistribution, if any, must * include the following acknowledgment: "This product includes software * developed by Sun Microsystems, Inc. for JXTA(TM) technology." * Alternately, this acknowledgment may appear in the software itself, if * and wherever such third-party acknowledgments normally appear. * * 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must * not be used to endorse or promote products derived from this software * without prior written permission. For written permission, please contact * Project JXTA at http://www.jxta.org. * * 5. Products derived from this software may not be called "JXTA", nor may * "JXTA" appear in their name, without prior written permission of Sun. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN * MICROSYSTEMS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * JXTA is a registered trademark of Sun Microsystems, Inc. in the United * States and other countries. * * Please see the license information page at : * <http://www.jxta.org/project/www/license.html> for instructions on use of * the license in source files. * * ==================================================================== * * This software consists of voluntary contributions made by many individuals * on behalf of Project JXTA. For more information on Project JXTA, please see * http://www.jxta.org. * * This license is based on the BSD license adopted by the Apache Foundation. */ package net.jxta.impl.endpoint.relay; import java.io.IOException; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.spi.AbstractSelectableChannel; import java.nio.channels.spi.AbstractSelectionKey; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; import net.jxta.endpoint.EndpointAddress; import net.jxta.endpoint.Message; import net.jxta.endpoint.Messenger; import net.jxta.impl.endpoint.BlockingMessenger; import net.jxta.impl.util.TimeUtils; import net.jxta.logging.Logging; import net.jxta.peer.PeerID; /** * A client of the Relay Server */ class RelayServerClient extends AbstractSelectableChannel implements Runnable { /** * Logger */ private static final Logger LOG = Logger.getLogger(RelayServerClient.class.getName()); /** * the Relay Server of this client */ private final RelayServer server; /** * the peerId of this client */ private final PeerID clientPeerId; /** * The absolute time at which the lease expires. */ private long leaseExpireAt = 0; /** * The time at which the message queue expires. */ private long queueStallAt = Long.MAX_VALUE; /** * The amount of time in relative milliseconds which we will allow the * client to stall (not poll for messages). */ private final long stallTimeout; /** * The messenger we are currently using to send messages or {@code null} if * we have no current way to send messages to the client. */ private Messenger messenger = null; /** * We allow a 1 message queue of high priority messages. */ private QueuedMessage outOfBandMessage = null; /** * A queue of message for this client */ private final BlockingQueue<QueuedMessage> messageList; /** * Our current set of valid operations. */ private volatile int readyOps = 0; /** * A message queued for sending to the client. */ private static class QueuedMessage { final Message message; final String destService; final String destParam; QueuedMessage(Message message, String destService, String destParam) { this.message = message; this.destService = destService; this.destParam = destParam; } } RelayServerClient(RelayServer server, PeerID clientPeerId, long leaseLength, long stallTimeout, int clientQueueSize) { super(null); try { configureBlocking(false); } catch(IOException impossible ) { throw new Error("Unhandled IOException", impossible); } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("new Client peerId=" + clientPeerId + " lease=" + leaseLength); } this.server = server; this.clientPeerId = clientPeerId; this.stallTimeout = stallTimeout; messageList = new ArrayBlockingQueue<QueuedMessage>(clientQueueSize); // initialize the lease renewLease(leaseLength); } /** * {@inheritDoc} */ public boolean equals(Object target) { if(target == this) { return true; } if(target instanceof RelayServerClient) { return clientPeerId.equals(((RelayServerClient) target).clientPeerId); } return false; } /** * {@inheritDoc} */ public int hashCode() { return clientPeerId.hashCode(); } /** * {@inheritDoc} * * <p/>Send all of the queued messages to the client. */ public void run() { try { if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Sending queued messages for " + this); } int failedInARow = 0; // We only last as long as the client channel remains open. while(isOpen()) { Messenger useMessenger; QueuedMessage message; boolean wasOOB; synchronized (this) { // No messenger? Nothing for us to do. if( null == messenger) { break; } // If our messenger is unusable, quit. if (0 == (messenger.getState() & Messenger.USABLE)) { queueStallAt = Math.min(queueStallAt, TimeUtils.toAbsoluteTimeMillis(stallTimeout)); messenger = null; break; } if (outOfBandMessage != null) { message = outOfBandMessage; outOfBandMessage = null; wasOOB = true; } else { message = messageList.poll(); wasOOB = false; } // No messages? We are now inactive. if(null == message) { setReadyOps(0); break; } useMessenger = messenger; } // send the message try { useMessenger.sendMessageB(message.message, message.destService, message.destParam); // A message was sent. Queue is no longer stalled. synchronized (this) { failedInARow = 0; queueStallAt = Long.MAX_VALUE; } } catch (Exception e) { // Check that the exception is not due to the message rather // than the messenger, and then drop the message. In this // case we give the messenger the benefit of the doubt and // keep it open, renewing the lease as above. (this could be // the last message). For now the transports do not tell the // difference, so we count the number of times we failed in // a row. After three times, kill the message rather than // the messenger. // put the message back synchronized (this) { if (++failedInARow >= 3) { failedInARow = 0; queueStallAt = Long.MAX_VALUE; continue; } // Ok, if we cannot push back the message, below, we // should reset failedInARow, since we won't be retrying // the same message. But it does not realy matter so // let's keep things simple. if (outOfBandMessage == null) { outOfBandMessage = message; } // If we are still holding the same messenger, kill it. if(useMessenger == messenger) { queueStallAt = Math.min(queueStallAt, TimeUtils.toAbsoluteTimeMillis(stallTimeout)); messenger = null; } else { useMessenger = null; } } // If we're here, we decided to close the messenger. We do // that out of sync. if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.log(Level.INFO, "Giving up on unusable messenger : " + useMessenger, e); } if(null != useMessenger) { useMessenger.close(); useMessenger = null; } } } } catch (Throwable all) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Uncaught Throwable in thread :" + Thread.currentThread().getName(), all); } } finally { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Stopped sending queued messages for " + this); } // Re-register with the selector for future messages. synchronized(this) { if((null != messenger) && isOpen()) { try { register(server.selector, SelectionKey.OP_WRITE, null); server.selector.wakeup(); } catch(ClosedChannelException betterNotBe) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Channel unexpectedly closed!", betterNotBe); } } } } } } /** * {@inheritDoc} */ @Override public String toString() { return super.toString() + "[" + clientPeerId + "," + getQueueSize() + "," + (messenger == null ? "-m" : "+m") + "," + TimeUtils.toRelativeTimeMillis(queueStallAt) + "," + TimeUtils.toRelativeTimeMillis(leaseExpireAt) + "]"; } /** * {@inheritDoc} */ protected void implCloseSelectableChannel() throws IOException { if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info( "Closing " + this); } Messenger messengerToClose = messenger; messenger = null; if(null != messengerToClose) { messengerToClose.close(); } queueStallAt = 0; leaseExpireAt = 0; messageList.clear(); } /** * {@inheritDoc} */ protected void implConfigureBlocking(boolean block) throws IOException { } /** * {@inheritDoc} */ public int validOps() { return SelectionKey.OP_WRITE; } /** * Return the current Ops state. * * @return the current ops state, if any. */ int readyOps() { return readyOps; } /** * Adjust the current Ops state. * * @param readyOps the new ops state. */ private void setReadyOps(int readyOps) { this.readyOps = readyOps; ClientSelectionKey key = (ClientSelectionKey) keyFor(server.selector); if(null != key) { key.setReadyOps(readyOps); } } /** * Creates a new selection key for the specified selector. * * @param selector The target selector. * @param ops The initial interest ops. * @param att The attached object. */ ClientSelectionKey newSelectionKey(RelayServer.ClientSelector selector, int ops, Object att) { return new ClientSelectionKey(selector, ops, att); } /** * Returns the number of items we have queued for the client. * * @return The number of queued messages including the out of band message. */ private int getQueueSize() { return (null != outOfBandMessage) ? 1 : 0 + messageList.size(); } /** * Remove all queued messages. The Out of band message (if any) is retained. */ void flushQueue() { messageList.clear(); } /** * If {@code true} then: * <ul> * <li>This client connection has been closed.</li> * <li>The client lease has expired.</li> * <li>The client has failed to poll recently.</li> * </ul> * * @return {@code true} if this client is no longer usable otherwise * {@code false}. */ boolean isExpired() { long now = TimeUtils.timeNow(); boolean isExpired = !isOpen() || (now > leaseExpireAt) || (now > queueStallAt); if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) { LOG.finer( this + " : isExpired() = " + isExpired ); } return isExpired; } long getLeaseRemaining() { return TimeUtils.toRelativeTimeMillis(leaseExpireAt); } protected PeerID getClientPeerId() { return clientPeerId; } boolean renewLease(long leaseLength) { // It is ok to renew a lease past the expiration time, as long as the // client has not been closed yet. if (!isOpen()) { return false; } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine( this + " : additional lease = " + leaseLength ); } leaseExpireAt = TimeUtils.toAbsoluteTimeMillis(leaseLength); return true; } /** * Replace the messenger currently in use with a new messenger. * * @param newMessenger The messenger to be used to send to the client. * @return If {@code true} then the new messenger has been accepted and * will be used to send messages to the client. If {@code false} then the * messenger will not be used. */ boolean addMessenger(Messenger newMessenger) { // make sure we are being passed a valid messenger if ((null == newMessenger) || (0 == (newMessenger.getState() & Messenger.USABLE))) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Ignorning bad messenger (" + newMessenger + ")"); } return false; } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("New messenger : " + newMessenger ); } // Unless we change our mind, we'll close the new messenger. // If we do not keep it, we must close it. Otherwise the client on the // other end will never know what happened. // Its connection will be left hanging for a long time. Messenger messengerToClose = newMessenger; synchronized (this) { if (isOpen()) { // Swap messengers; we'll close the old one if there was one. messengerToClose = messenger; messenger = newMessenger; if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Messenger (" + messenger + ")"); } // If we had no previous messenger then register this channel. if(null == messengerToClose) { try { register(server.selector, SelectionKey.OP_WRITE, null); server.selector.wakeup(); } catch(ClosedChannelException betterNotBe) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Channel unexpectedly closed!", betterNotBe); } } } // If we have waiting messages, select this channel. if (getQueueSize() > 0) { setReadyOps(SelectionKey.OP_WRITE); } } } // Now that we are out of sync, close the unused messenger. // In either case, we claim that we kept the new one. if (messengerToClose != null) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Closing messenger : " + messengerToClose ); } messengerToClose.close(); } return true; } /** * Add a message to the tail of the list * * @param message The message to be enqueued. * @param outOfBand if true, indicates outbound * @return {@code true} if the message was enqueued otherwise {@code false}. */ private boolean queueMessage(Message message, String destService, String destParam, boolean outOfBand) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("queueMessage for " + this); } synchronized (this) { if (!isOpen()) { return false; } QueuedMessage qm = new QueuedMessage(message, destService, destParam); if (outOfBand) { // We have a single oob message pending. outOfBandMessage = qm; } else { // We will simply discard the new msg when the queue is full // to avoid penalty of dropping earlier reliable message if (!messageList.offer(qm)) { if (Logging.SHOW_WARNING) { LOG.warning("Dropping " + message + " for peer " + clientPeerId); } } else if(Logging.SHOW_INFO && (messageList.size() % 50 == 0) ) LOG.info("Message queue size for client " + clientPeerId + " now " + messageList.size()); } // Normally, if messenger is null we knew it already: // it becomes null only when we detect that it breaks while trying // to send. However, let's imagine it's possible that we never had // one so far. Be careful that this is not a one-time event; we // must not keep renewing the short lease; that would ruin it's // purpose. if ((null == messenger) || (0 == (messenger.getState() & Messenger.USABLE))) { queueStallAt = Math.min(queueStallAt, TimeUtils.toAbsoluteTimeMillis(stallTimeout)); } else { setReadyOps(SelectionKey.OP_WRITE); } } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("done queueMessage for " + this ); } return true; } /** * {@inheritDoc} */ Messenger getMessenger(EndpointAddress destAddr, boolean outOfBand) { return new RelayMessenger(destAddr, outOfBand); } private class RelayMessenger extends BlockingMessenger { private final boolean outOfBand; public RelayMessenger(EndpointAddress destAddress, boolean outOfBand) { // We do not use self destruction super(RelayServerClient.this.server.group.getPeerGroupID(), destAddress, false); this.outOfBand = outOfBand; } /* * The cost of just having a finalize routine is high. The finalizer is * a bottleneck and can delay garbage collection all the way to heap * exhaustion. Leave this comment as a reminder to future maintainers. * Below is the reason why finalize is not needed here. * * This is never given to application layers directly. No need * to close-on-finalize. * protected void finalize() { } */ /** * {@inheritDoc} */ @Override public boolean isIdleImpl() { // We do not use self destruction return false; } /** * {@inheritDoc} */ @Override public void closeImpl() { // Nothing to do. The underlying connection is not affected. // The messenger will be marked closed by the state machine once completely down; that's it. } /** * {@inheritDoc} */ @Override public EndpointAddress getLogicalDestinationImpl() { return new EndpointAddress(RelayServerClient.this.clientPeerId, null, null); } /** * {@inheritDoc} * * <p/>Send messages. Messages are queued and then processed when there is a transport messenger. */ @Override public void sendMessageBImpl(Message message, String serviceName, String serviceParam) throws IOException { if (!RelayServerClient.this.isOpen()) { IOException failure = new IOException("Messenger was closed, it cannot be used to send messages."); if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, failure.getMessage(), failure); } throw failure; } // Prepare the final destination address of the message EndpointAddress useAddr = getDestAddressToUse(serviceName, serviceParam); // simply enqueue the message. // We clone it, since we pretend it's been sent synchronously. if(!RelayServerClient.this.queueMessage(message.clone(), useAddr.getServiceName(), useAddr.getServiceParameter(), outOfBand)) { throw new IOException("Message could not be queued."); } } } /** * Our Selection key */ class ClientSelectionKey extends AbstractSelectionKey { private final RelayServer.ClientSelector selector; private volatile int ops; private volatile int readyOps = 0; ClientSelectionKey(RelayServer.ClientSelector selector, int ops, Object att) { this.selector = selector; interestOps(ops); attach(att); } /** * {@inheritDoc} */ public RelayServerClient channel() { return RelayServerClient.this; } /** * {@inheritDoc} */ public Selector selector() { return selector; } /** * {@inheritDoc} */ public int interestOps() { return ops; } /** * {@inheritDoc} */ public ClientSelectionKey interestOps(int ops) { if(!isValid()) throw new CancelledKeyException(); this.ops = ops; return this; } /** * {@inheritDoc} */ public int readyOps() { if(!isValid()) throw new CancelledKeyException(); return readyOps; } /** * Set the readyOps for this key. * * @param readyOps The new ops value. */ public void setReadyOps(int readyOps) { if(!isValid()) throw new CancelledKeyException(); this.readyOps = readyOps; selector.keyChanged(this); } } }