/* * 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.pipe; import net.jxta.endpoint.EndpointAddress; import net.jxta.endpoint.EndpointService; import net.jxta.endpoint.Message; import net.jxta.endpoint.Messenger; import net.jxta.id.ID; import net.jxta.impl.util.TimeUtils; import net.jxta.logging.Logging; import net.jxta.peergroup.PeerGroup; import net.jxta.pipe.OutputPipe; import net.jxta.pipe.PipeID; import net.jxta.protocol.PipeAdvertisement; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; /** * An implementation of Ouput Pipe which sends messages on the pipe * asynchronously. The <code>send()</code> method for this implementation will * never block. */ class NonBlockingOutputPipe implements PipeResolver.Listener, OutputPipe, Runnable { /** * Logger */ private static final Logger LOG = Logger.getLogger(NonBlockingOutputPipe.class.getName()); /** * Amount of time an idle worker thread will linger */ private static final long IDLEWORKERLINGER = 10 * TimeUtils.ASECOND; /** * Minimum Query interval. Queries will not be sent more frequently than * this interval. */ private static final long QUERYINTERVALMIN = 15 * TimeUtils.ASECOND; /** * Query timeout minimum. Waits for query response will not be shorter than * this interval. */ private static final long QUERYTIMEOUTMIN = 1 * TimeUtils.AMINUTE; /** * If true then the pipe has been closed and will no longer accept messages. */ private volatile boolean closed = false; /** * If true then this pipe has just migrated. Used to prevent re-entering * migration from an unfinished migration. */ private boolean migrated = false; /** * Group in which we are working. */ private PeerGroup peerGroup = null; /** * The endpoint of our group. */ private EndpointService endpoint = null; /** * The pipe resolver we will use for migrate and verify. */ private PipeResolver pipeResolver = null; /** * The advertisement we were created from. */ private PipeAdvertisement pAdv = null; /** * The current peer the pipe is resolved to. */ private ID destPeer = null; /** * The set of peers to which the pipe can be resolved. */ private Set<? extends ID> resolvablePeers = null; /** * The endpoint destination address for the remote peer we are resolved to. */ private EndpointAddress destAddress = null; private Messenger destMessenger = null; /** * The worker thread which actually sends messages on the pipe */ private volatile Thread serviceThread = null; /** * Absolute time in milliseconds at which we will send the next verify * request. */ private long nextVerifyAt = 0; /** * Queue of messages waiting to be sent. */ private final BlockingQueue<Message> queue = new LinkedBlockingQueue<Message>(50); /** * Tracks the state of our worker thread. */ enum WorkerState { /** * Find a new eligible destination peer which is listening on the pipe. */ STARTMIGRATE, /** * Issue resolution queries and wait for responses */ PENDINGMIGRATE, /** * Determine if the destination peer is still listening on the pipe. */ STARTVERIFY, /** * Issue verify queries and wait for responses */ PENDINGVERIFY, /** * Acquire a messenger to the destination peer. */ ACQUIREMESSENGER, /** * Send messages via the messenger to the destination peer. */ SENDMESSAGES, /** * Exit. */ CLOSED } /** * The current state of the worker thread */ private WorkerState workerstate; /** * The query id we are currently operating under. */ private int queryID = -1; /** * Create a new output pipe * * @param peerGroup peergroup we are working in. * @param pipeResolver the piperesolver this pipe is bound to. * @param pAdv advertisement for the pipe we are supporting. * @param destPeer the peer this pipe is currently bound to. * @param peers the set of peers we allow this pipe to be bound to. */ public NonBlockingOutputPipe(PeerGroup peerGroup, PipeResolver pipeResolver, PipeAdvertisement pAdv, ID destPeer, Set<? extends ID> peers) { this.peerGroup = peerGroup; endpoint = peerGroup.getEndpointService(); this.pipeResolver = pipeResolver; this.pAdv = pAdv; this.destPeer = destPeer; this.resolvablePeers = new HashSet<ID>(peers); if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Constructing for " + getPipeID()); } workerstate = WorkerState.ACQUIREMESSENGER; startServiceThread(); } /** * {@inheritDoc} */ @Override protected void finalize() throws Throwable { if (!closed) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("Pipe is being finalized without being previously closed. This is likely a bug."); } } close(); super.finalize(); } /** * {@inheritDoc} */ public synchronized void close() { // Close the queue so that no more messages are accepted if (!isClosed()) { if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Closing for " + getPipeID()); } } closed = true; } /** * {@inheritDoc} */ public boolean isClosed() { return closed; } /** * {@inheritDoc} */ public final String getType() { return pAdv.getType(); } /** * {@inheritDoc} */ public final ID getPipeID() { return pAdv.getPipeID(); } /** * {@inheritDoc} */ public final String getName() { return pAdv.getName(); } /** * {@inheritDoc} */ public final PipeAdvertisement getAdvertisement() { return pAdv; } /** * {@inheritDoc} */ public boolean send(Message msg) throws IOException { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Queuing " + msg + " for pipe " + getPipeID()); } boolean pushed = false; while (!isClosed()) { try { pushed = queue.offer(msg,100 * TimeUtils.AMILLISECOND,TimeUnit.MILLISECONDS); break; } catch (InterruptedException woken) { Thread.interrupted(); } } if (!pushed && isClosed()) { IOException failed = new IOException("Could not enqueue " + msg + " for sending. Pipe is closed."); if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, failed.getMessage(), failed); } throw failed; } startServiceThread(); return pushed; } /** * {@inheritDoc} * <p/> * Sends the messages. * <p/> * This method does a lot of things. It has several distinct states: * <p/> * <table border="1"> * <thead> * <tr> * <th>STATE</th> * <th>Activity</th> * <tr> * </thead> * <p/> * <tbody> * <tr> * <th>ACQUIREMESSENGER</th * <td>Acquire a messenger to the specified destination peer. If a * messenger is acquired, then go to <b>SENDMESSAGES</b> state * otherwise go to <b>STARTMIGRATE</b>.</td> * </tr> * <p/> * <tr> * <th>SENDMESSAGES</th> * <td>Send messages until queue is closed and all messages have * been sent. Go to state <b>CLOSED</b> when done. If the messenger * becomes closed then go to <b>ACQUIREMESSENGER</b>. <emphasis>If * there are no messages to send for <code>IDLEWORKERLINGER</code> * milliseconds then the worker thread will exit. It will only be * restarted if another message is eventually enqueued.</emphasis> * </td> * </tr> * <p/> * <tr> * <th>STARTVERIFY</th> * <td>Starts a verification query(s) to the destination peer. This * state is activated after * <code>PipeServiceImpl.VERIFYINTERVAL</code> milliseconds of * sending messages. The query responses will be tracked in the * <b>PENDINGVERIFY</b> state.</td> * </tr> * <p/> * <tr> * <th>STARTMIGRATE</th> * <td>Starts a query(s) for peers listening on this pipe. The * query responses will be tracked in the <b>PENDINGMIGRATE</b> * state.</td> * </tr> * <p/> * <tr> * <th>PENDINGVERIFY</th> * <td>Issues query messages to verify that the destination peer is * still listening on the pipe. Queries are issued every * <code>QUERYINTERVAL</code> milliseconds. If a positive response * is received, go to state <b>ACQUIREMESSENGER</b>. If no response * is received within <b>QUERYTIMEOUT</b> milliseconds or a * negative response is received then go to state * <b>STARTMIGRATE</b>.</td> * </tr> * <p/> * <tr> * <th>PENDINGMIGRATE</th> * <td>Issues query messages to find a new destination peer. * Queries are issued every <code>QUERYINTERVAL</code> milliseconds. * If a positive response is received, go to state * <b>ACQUIREMESSENGER</b>. If no positive response from an * eligible peer is received within <b>QUERYTIMEOUT</b> * milliseconds go to state <b>CLOSED</b>.</td> * </tr> * <p/> * <tr> * <th>CLOSED</th> * <td>Exit the worker thread.</td> * </tr> * </tbody> * </table> */ public void run() { long absoluteTimeoutAt = -1; long nextQueryAt = -1; try { // state loop while (WorkerState.CLOSED != workerstate) { synchronized (this) { LOG.fine("NON-BLOCKING WORKER AT STATE : " + workerstate + ((WorkerState.SENDMESSAGES == workerstate) ? "\n\t" + TimeUtils.toRelativeTimeMillis(nextVerifyAt, TimeUtils.timeNow()) + " until verify." : "")); // switch() emulation if ((WorkerState.STARTVERIFY == workerstate) || (WorkerState.STARTMIGRATE == workerstate)) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { if (null == destPeer) { LOG.fine("Starting re-resolve for \'" + getPipeID()); } else { LOG.fine("Starting verify for \'" + getPipeID() + "\' to : " + destPeer); } } queryID = PipeResolver.getNextQueryID(); pipeResolver.addListener(getPipeID(), this, queryID); absoluteTimeoutAt = TimeUtils.toAbsoluteTimeMillis( Math.max(QUERYTIMEOUTMIN, (PipeServiceImpl.VERIFYINTERVAL / 20))); nextQueryAt = TimeUtils.timeNow(); if (WorkerState.STARTVERIFY == workerstate) { workerstate = WorkerState.PENDINGVERIFY; } else if (WorkerState.STARTMIGRATE == workerstate) { workerstate = WorkerState.PENDINGMIGRATE; } // move on to the next state. } else if ((WorkerState.PENDINGVERIFY == workerstate) || (WorkerState.PENDINGMIGRATE == workerstate)) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine( "Pipe " + ((WorkerState.PENDINGVERIFY == workerstate) ? "verify" : "migrate") + "in progress. Continues for " + TimeUtils.toRelativeTimeMillis(absoluteTimeoutAt, TimeUtils.timeNow()) + "ms. Next query in " + TimeUtils.toRelativeTimeMillis(nextQueryAt, TimeUtils.timeNow()) + "ms."); } // check to see if we are completely done. if (TimeUtils.toRelativeTimeMillis(absoluteTimeoutAt, TimeUtils.timeNow()) <= 0) { pipeResolver.removeListener(getPipeID(), queryID); if (WorkerState.PENDINGVERIFY == workerstate) { if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Pipe \'" + getPipeID() + "\' has migrated from " + destPeer); } workerstate = WorkerState.STARTMIGRATE; // move on to the next state. continue; } else { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("Pipe \'" + getPipeID() + "\' cannot be migrated and is being closed"); } workerstate = WorkerState.CLOSED; close(); // move on to the next state. continue; } } // check if its time ot send another copy of the query. if (TimeUtils.toRelativeTimeMillis(nextQueryAt, TimeUtils.timeNow()) <= 0) { if (null != destPeer) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine( "Sending out verify query (" + queryID + ") for \'" + getPipeID() + "\' to : " + destPeer); } pipeResolver.sendPipeQuery(pAdv, Collections.singleton(destPeer), queryID); } else { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Sending out resolve query (" + queryID + ") for " + getPipeID()); } pipeResolver.sendPipeQuery(pAdv, resolvablePeers, queryID); } nextQueryAt = TimeUtils.toAbsoluteTimeMillis( Math.max(QUERYINTERVALMIN, (PipeServiceImpl.VERIFYINTERVAL / 50))); } long sleep = TimeUtils.toRelativeTimeMillis(Math.min(nextQueryAt, absoluteTimeoutAt), TimeUtils.timeNow()); if (sleep >= 0) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Waiting " + sleep + "ms for response for (" + queryID + ") for " + getPipeID()); } try { wait(sleep); } catch (InterruptedException woken) { Thread.interrupted(); } } // move on to the next state. } else if (WorkerState.ACQUIREMESSENGER == workerstate) { if ((null == destMessenger) || destMessenger.isClosed()) { destMessenger = null; if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Getting messenger to \'" + destPeer + "\' for pipe " + getPipeID()); } destAddress = mkAddress(destPeer, getPipeID()); // todo 20031011 bondolo@jxta.org This should not be done under sync destMessenger = endpoint.getMessenger(destAddress); if (destMessenger == null) { // We could not get a messenger to the peer, forget it and try again. if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("Could not get messenger to : " + destPeer + ". "); } if (migrated) { // we can't migrate again, we never finished. // the last migrate! workerstate = WorkerState.CLOSED; close(); } else { workerstate = WorkerState.STARTMIGRATE; } pipeResolver.removeListener((PipeID) getPipeID(), queryID); queryID = -1; destPeer = null; destAddress = null; // move on to the next state. continue; } else { // migration completed. migrated = false; } } else { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Using existing messenger to : " + destPeer); } } workerstate = WorkerState.SENDMESSAGES; nextVerifyAt = TimeUtils.toAbsoluteTimeMillis(PipeServiceImpl.VERIFYINTERVAL); // move on to the next state. continue; // can't just fall through because we would start sending messages immediately. } else if (WorkerState.SENDMESSAGES == workerstate) { // is it time to do verification again? if (TimeUtils.toRelativeTimeMillis(nextVerifyAt, TimeUtils.timeNow()) <= 0) { workerstate = WorkerState.STARTVERIFY; pipeResolver.removeListener(getPipeID(), queryID); queryID = -1; } // move on to the next state. } else if (WorkerState.CLOSED == workerstate) { queue.clear(); // they aren't going to be sent if (null != destMessenger) { destMessenger.close(); destMessenger = null; } serviceThread = null; break; } else { LOG.warning("Unrecognized state in worker thread : " + workerstate); } } // now actually send messages. We don't do this under the global sync. if (WorkerState.SENDMESSAGES == workerstate) { Message msg = null; try { msg = queue.poll(IDLEWORKERLINGER, TimeUnit.MILLISECONDS); } catch (InterruptedException woken) { Thread.interrupted(); continue; } if (null == msg) { synchronized (this) { // before deciding to die, we need to make sure that // nobody snuck something into the queue. If there // is, then we have to be the one to service the // queue. if (null == queue.peek()) { if (closed) { workerstate = WorkerState.CLOSED; continue; } else { serviceThread = null; break; } } else { continue; } } } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Sending " + msg + " on " + getPipeID()); } if (!destMessenger.isClosed()) { try { destMessenger.sendMessageB(msg, null, null); } catch (IOException failed) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Failure sending " + msg + " on " + getPipeID(), failed); } } } // May be now closed due to failing to send. if (destMessenger.isClosed()) { synchronized (this) { workerstate = WorkerState.ACQUIREMESSENGER; destMessenger = null; } } } } } catch (Throwable all) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Uncaught Throwable in thread :" + Thread.currentThread().getName(), all); } // give another thread the chance to start unless one already has. // If the exception was caused by damaged state on this object then // starting a new Thread may just cause the same exception again. // Unfortunate tradeoff. synchronized (this) { if (serviceThread == Thread.currentThread()) { serviceThread = null; } } } finally { if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info( "Thread exit : " + Thread.currentThread().getName() + "\n\tworker state : " + workerstate ); } } } /** * Starts the worker thread if it is not already running. */ private synchronized void startServiceThread() { // if there is no service thread, start one. if ((null == serviceThread) && !closed) { serviceThread = new Thread(peerGroup.getHomeThreadGroup(), this , "Worker Thread for NonBlockingOutputPipe : " + getPipeID()); serviceThread.setDaemon(true); serviceThread.start(); if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info( "Thread start : " + serviceThread.getName() + "\n\tworker state : " + workerstate ); } } } /** * Convenience method for constructing a peer endpoint address from its * peer id * * @param destPeer the desitnation peer * @param pipeID the pipe to put in the param field. * @return the pipe endpoint address. */ protected EndpointAddress mkAddress(ID destPeer, ID pipeID) { return new EndpointAddress("jxta", destPeer.getUniqueValue().toString(), "PipeService", pipeID.toString()); } /** * {@inheritDoc} */ public synchronized boolean pipeNAKEvent(PipeResolver.Event event) { if (((workerstate == WorkerState.PENDINGVERIFY) || (workerstate == WorkerState.ACQUIREMESSENGER) || (workerstate == WorkerState.SENDMESSAGES)) && (event.getPeerID().equals(destPeer) && (event.getQueryID() == queryID))) { // we have been told that the destination peer no longer wants // to talk with us. We will try to migrate to another peer. if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("Pipe \'" + getPipeID() + "\' is closed at " + event.getPeerID()); } workerstate = WorkerState.STARTMIGRATE; pipeResolver.removeListener(getPipeID(), queryID); queryID = -1; destPeer = null; destAddress = null; if (null != destMessenger) { destMessenger.close(); destMessenger = null; } notify(); return true; } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Ignoring NAK from " + event.getPeerID()); } // didn't refer to us or we don't care. return false; } /** * {@inheritDoc} */ public synchronized boolean pipeResolveEvent(PipeResolver.Event event) { if (((workerstate == WorkerState.PENDINGVERIFY) || (workerstate == WorkerState.PENDINGMIGRATE)) && (event.getQueryID() == queryID)) { if ((workerstate == WorkerState.PENDINGVERIFY) && !event.getPeerID().equals(destPeer)) { // not from the right peer so ignore it. if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Ignoring response from " + event.getPeerID()); } return false; } else { if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Pipe \'" + getPipeID() + "\' is verified for " + destPeer); } } workerstate = WorkerState.ACQUIREMESSENGER; migrated = true; destPeer = event.getPeerID(); if ((workerstate == WorkerState.PENDINGMIGRATE) && Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) { LOG.info("Pipe \'" + getPipeID() + "\' has migrated to " + destPeer); } notify(); return true; } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Ignoring resolve from " + event.getPeerID()); } // didn't refer to us or we don't care. return false; } }