All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.refcodes.remoting.AbstractRemote Maven / Gradle / Ivy

Go to download

Artifact with proxy functionality for plain remote procedure calls (RPC), a POJO alternative to RMI.

There is a newer version: 3.3.8
Show newest version
// /////////////////////////////////////////////////////////////////////////////
// REFCODES.ORG
// =============================================================================
// This code is copyright (c) by Siegfried Steiner, Munich, Germany, distributed
// on an "AS IS" BASIS WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, and licen-
// sed under the following (see "http://en.wikipedia.org/wiki/Multi-licensing")
// licenses:
// =============================================================================
// GNU General Public License, v3.0 ("http://www.gnu.org/licenses/gpl-3.0.html")
// together with the GPL linking exception applied; as being applied by the GNU
// Classpath ("http://www.gnu.org/software/classpath/license.html")
// =============================================================================
// Apache License, v2.0 ("http://www.apache.org/licenses/TEXT-2.0")
// =============================================================================
// Please contact the copyright holding author(s) of the software artifacts in
// question for licensing issues not being covered by the above listed licenses,
// also regarding commercial licensing models or regarding the compatibility
// with other open source licenses.
// /////////////////////////////////////////////////////////////////////////////

package org.refcodes.remoting;

import java.io.IOException;
import java.io.Serializable;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.refcodes.component.AbstractConnectableAutomaton;
import org.refcodes.component.ConnectionStatus;
import org.refcodes.component.Destroyable;
import org.refcodes.component.DigestException;
import org.refcodes.component.Digester;
import org.refcodes.controlflow.ControlFlowUtility;
import org.refcodes.controlflow.RetryCounter;
import org.refcodes.data.IoRetryCount;
import org.refcodes.data.LatencySleepTime;
import org.refcodes.data.LoopExtensionTime;
import org.refcodes.exception.Trap;
import org.refcodes.io.DatagramTransceiver;
import org.refcodes.mixin.Disposable;
import org.refcodes.mixin.Resetable;
import org.refcodes.runtime.SystemProperty;

/**
 * Base class for {@link RemoteClient} and {@link RemoteServer} implementations
 * implementing various useful base functionality.
 */
abstract class AbstractRemote extends AbstractConnectableAutomaton implements Remote {

	// /////////////////////////////////////////////////////////////////////////
	// STATICS:
	// /////////////////////////////////////////////////////////////////////////

	private static final Logger LOGGER = Logger.getLogger( AbstractRemote.class.getName() );

	// /////////////////////////////////////////////////////////////////////////
	// CONSTANTS:
	// /////////////////////////////////////////////////////////////////////////

	/**
	 * Static instance ID in case we have a job regarding the
	 * {@link RemoteServer} or the {@link RemoteClient} as a whole. Its length
	 * must be != {@link #INSTANCE_ID_LENGTH}.
	 */
	static final String STATIC_INSTANCE_ID = "static_instance";

	/**
	 * Static session ID in case we have a job regarding the
	 * {@link RemoteServer} or the {@link RemoteClient} as a whole. Its length
	 * must be != {@link #SESSION_ID_LENGTH}.
	 */
	static final String STATIC_SESSION_ID = "static_session";

	/**
	 * The length of an instance id. Each published object is assigned an
	 * instance id). A method request / reply is assigned to an instance id
	 * additional to it's session id in order to identify the object / proxy
	 * belonging to tjhe session.
	 */
	static final int INSTANCE_ID_LENGTH = 16;

	/**
	 * The length of a session id (for method requests / replies). Each method
	 * request is assigned a unique session id, which will be used in method
	 * replies.
	 */
	static final int SESSION_ID_LENGTH = 32;

	/**
	 * Any request made waits this amount of time (in milliseconds) for a reply
	 * - if no reply reaches the requester within this time a
	 * ConnectionUnpredictableException is thrown.
	 */
	static final long WAIT_FOR_REPLY_TIMEOUT = 30000;

	/**
	 * When signing off an object / proxy, the proxy is given this amount of
	 * time (in milliseconds) to close it's active session before it is disposed
	 * and removed. Any remaining active sessions (method calls) will be replied
	 * with a ConnectionRuntimeException if the timeout was shorter then the
	 * time needed for the active sessions to finish.
	 */

	static final long WAIT_FOR_ACTIVE_SESSIONS_TIMEOUT_IN_MS = ( WAIT_FOR_REPLY_TIMEOUT * 2 / 3 );

	/**
	 * The amount of time (in milliseconds) to wait and poll for active
	 * sessions.
	 */
	static final int WAIT_FOR_ACTIVE_SESSIONS_LOOPS = 250;

	/**
	 * The amount of time (in milliseconds) to wait and poll for replies.
	 * Normally while waiting all waiting threads are woken up when a reply for
	 * this threads is received.
	 */
	static final int WAIT_FOR_REPLY_LOOPS = 250;

	/**
	 * Set to true in case the (not really time consuming) consistency checks
	 * are to be performed.
	 */
	static final boolean PERFORM_CONSISTENCY_CHECKS = false;

	/**
	 * For debugging purposes; additional log output to follow the trace of a
	 * job through the {@link RemoteServer} and the {@link RemoteClient}.
	 */
	static final boolean IS_LOG_DEBUG_ENABLED = SystemProperty.LOG_DEBUG.isEnabled();

	// /////////////////////////////////////////////////////////////////////////
	// VARIABLES:
	// /////////////////////////////////////////////////////////////////////////

	DatagramTransceiver _transceiver;
	JobReceiverDaemon _jobReceiverDaemon = null;
	private ExecutorService _executorService;
	private boolean _isDestroyed = false;

	// /////////////////////////////////////////////////////////////////////////
	// CONSTRUCTORS:
	// /////////////////////////////////////////////////////////////////////////

	/**
	 * Constructs a {@link AbstractRemote} provided with the given
	 * {@link InterProcessDigester} and configured with an
	 * {@link ExecutorService} something like
	 * {@link ControlFlowUtility#createCachedDaemonExecutorService()}.
	 */
	public AbstractRemote() {
		this( null );
	}

	/**
	 * Constructs a {@link AbstractRemote} configured with the given
	 * {@link ExecutorService} required for thread generation in an JEE
	 * environment.
	 *
	 * @param aExecutorService The {@link ExecutorService} to be used, when null
	 *        then an {@link ExecutorService} something like
	 *        {@link ControlFlowUtility#createCachedDaemonExecutorService()} is
	 *        then retrieved.
	 */
	public AbstractRemote( ExecutorService aExecutorService ) {
		if ( aExecutorService == null ) {
			_executorService = ControlFlowUtility.createCachedExecutorService( true );
		}
		else {
			_executorService = ControlFlowUtility.toManagedExecutorService( aExecutorService );
		}
	}

	// /////////////////////////////////////////////////////////////////////////
	// METHODS:
	// /////////////////////////////////////////////////////////////////////////

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean isOpenable( DatagramTransceiver aConnection ) {
		return super.isOpenable() && aConnection.isOpened();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized void open( DatagramTransceiver aTransceiver ) throws IOException {
		_transceiver = aTransceiver;
		ControlFlowUtility.throwIllegalStateException( _isDestroyed || isOpened() );
		final ConnectionStatus theConnectionStatus = _transceiver.getConnectionStatus();
		if ( theConnectionStatus != ConnectionStatus.OPENED ) {
			throw new IOException( "Unable to open the remote of type <" + getClass().getName() + "> as the underlying transcivier of type <" + _transceiver.getClass().getName() + "> is in status " + theConnectionStatus + " (not open; please open it beofrehand)." );
		}
		_jobReceiverDaemon = new JobReceiverDaemon();
		LOGGER.log( Level.FINE, "Starting job receiver daemon <" + _jobReceiverDaemon.getClass().getName() + ">." );
		_executorService.execute( _jobReceiverDaemon );
		setConnectionStatus( ConnectionStatus.OPENED );
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized void close() throws IOException {
		if ( isClosed() ) {
			return;
		}
		ControlFlowUtility.throwIllegalStateException( _isDestroyed );
		if ( IS_LOG_DEBUG_ENABLED ) {
			LOGGER.log( Level.FINE, "CLOSE called on <" + getClass().getName() + ">." );
		}
		super.close();
		// if (_jobReceiverDaemon != null ) {
		_jobReceiverDaemon.dispose();
		_jobReceiverDaemon = null;
		// }
		if ( _transceiver.isOpened() ) {
			_transceiver.close();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void destroy() {
		if ( !_isDestroyed ) {
			try {
				close();
			}
			catch ( IOException e ) {
				LOGGER.log( Level.WARNING, "Destroying failed as of: " + Trap.asMessage( e ), e );
			}
			finally {
				_executorService.shutdownNow();
				try {
					_transceiver.close();
				}
				catch ( IOException e ) {
					LOGGER.log( Level.WARNING, "Destroying failed as of: " + Trap.asMessage( e ), e );
				}
				finally {
					_isDestroyed = true;
				}
			}
		}
	}

	// /////////////////////////////////////////////////////////////////////////
	// ADAPTER:
	// /////////////////////////////////////////////////////////////////////////

	/**
	 * The {@link #digest(Message)} method is invoked in order to trigger
	 * processing of the provided {@link Message}, e.g. start execution
	 * depending on the {@link Message} instance being provided from the
	 * outside. A {@link RemoteClient} digests such a {@link Message} different
	 * than a {@link RemoteServer}. This method is inspired by the
	 * {@link Digester#digest(Object)} method.
	 * 
	 * @param aJob The {@link Message} to be digested.
	 * 
	 * @throws DigestException Thrown in case digesting (processing) a job by a
	 *         {@link Digester#digest(Object)} caused problems; the aCause is
	 *         usually wrapped by this {@link Exception}.
	 * 
	 * @see Digester
	 */
	protected abstract void digest( Message aJob ) throws DigestException;

	/**
	 * Forwards an {@link Message} from this sender to another receiver. A
	 * sub-class invokes this method when it sends an {@link Message} to the
	 * receiver counterpart. The communication path is to be implemented by the
	 * sub-class.
	 * 
	 * @param aJob the {@link Message} to be forwarded to this receiver from the
	 *        sender counterpart.
	 * 
	 * @throws IOException Thrown in case opening or accessing an open line
	 *         (connection, junction, link) caused problems.
	 */
	protected void toReceiver( Message aJob ) throws IOException {
		try {
			_transceiver.transmit( aJob );
		}
		catch ( IOException aException ) {
			if ( !( aJob instanceof CloseConnectionMessage ) ) {
				throw aException;
			}
		}
		// @formatter:off
		/*
			if ( isOpened() && _transceiver.isOpened() ) {
				_transceiver.writeDatagram( aJob );
			}
			else {
				LOGGER.log( Level.WARNING,  "Ignoring remote job <" + aJob.getClass().getName() + "> as the connection is not open; connection status is " + getConnectionStatus() + " (transciver connection status is " + _transceiver.getConnectionStatus() + ")." );
			}
				*/
		// @formatter:on
	}

	/**
	 * Provides an {@link Message} from another sender to this receiver. A
	 * sub-class invokes this method when it receives an {@link Message} from a
	 * sender counterpart. The communication path is to be implemented by the
	 * sub-class.
	 * 
	 * @param aJob the {@link Message} to be forwarded to this receiver from the
	 *        sender counterpart.
	 */
	protected void fromSender( Message aJob ) {
		ControlFlowUtility.throwIllegalStateException( _isDestroyed );
		if ( isOpened() ) {
			if ( aJob instanceof CloseConnectionMessage ) {
				if ( IS_LOG_DEBUG_ENABLED ) {
					LOGGER.log( Level.FINE, "Received a close connection job to <" + getClass().getName() + ">; closing connection." );
				}
				synchronized ( this ) {
					if ( isOpened() ) {
						close( (CloseConnectionMessage) aJob );
					}
				}
			}
			else {
				final JobDigesterDaemon theJobDigesterDaemon = new JobDigesterDaemon( aJob );
				_executorService.execute( theJobDigesterDaemon );
			}
		}
		else {
			// -----------------------------------------------------------------
			// Ignore close connection jobs as them may be received after the
			// connection has logically been closed though still an underlying
			// or encapsulating system my still invoke this method.
			// -----------------------------------------------------------------
			if ( !( aJob instanceof CloseConnectionMessage ) ) {
				throw new IllegalStateException( "Received a job <" + aJob + "> though this remote \"" + getClass().getName() + "\" is not in opened status; connection status is " + getConnectionStatus() );
			}
			else {
				LOGGER.info( "Ignoring job of type \"" + aJob.getClass().getName() + "\" being received whilst connection has been closed." );
			}
		}
	}

	// /////////////////////////////////////////////////////////////////////////
	// HOOKS:
	// /////////////////////////////////////////////////////////////////////////

	/**
	 * Hook method for retrieving the {@link ExecutorService}.
	 * 
	 * @return The {@link ExecutorService} as used by this implementation.
	 */
	protected ExecutorService getExecutorService() {
		return _executorService;
	}

	/**
	 * Closes the {@link Remote} upon a received {@link CloseConnectionMessage}.
	 * Some client ID might be considered in order to determine whether to close
	 * all or just one of many connections. This default implementation calls
	 * {@link #close()} (setting the {@link ConnectionStatus} to be
	 * {@link ConnectionStatus#CLOSED}); advanced implementations just closing
	 * dedicated connections may keep the {@link Remote} itself open (and skip
	 * calling {@link #close()} and close only a dedicated connection identified
	 * by the Meta-Data of the {@link CloseConnectionMessage}.
	 * 
	 * @param aJob The {@link CloseConnectionMessage} being received for closing
	 *        the connection.
	 */
	protected synchronized void close( CloseConnectionMessage aJob ) {
		// Close via business-logic, complete remote is to be shut down:
		if ( aJob == null ) {
			try {
				toReceiver( new CloseConnectionMessage() );
			}
			catch ( IOException e ) {
				LOGGER.log( Level.WARNING, "Cannot process close connection job <" + aJob + "> as of: " + Trap.asMessage( e ), e );
			}
		}
	}

	/**
	 * Hook to support the {@link Destroyable} interface, determines the
	 * destroyed status property; set to true when {@link #destroy()} is called.
	 * 
	 * @return The destroyed status property.
	 */
	protected boolean isDestroyed() {
		return _isDestroyed;
	}

	// /////////////////////////////////////////////////////////////////////////
	// SIGNALS:
	// /////////////////////////////////////////////////////////////////////////

	/**
	 * Hook when the connection has been opened.
	 */
	protected void onOpened() {}

	/**
	 * Hook when the connection has been closed.
	 */
	protected void onClosed() {}

	// /////////////////////////////////////////////////////////////////////////
	// JOB RECEIVER DAEMON:
	// /////////////////////////////////////////////////////////////////////////

	/**
	 * A daemon process used to read datagrams from the
	 * {@link DatagramTransceiver}.
	 */
	protected class JobReceiverDaemon implements Runnable, Disposable {

		private boolean _isDisposed = false;

		// /////////////////////////////////////////////////////////////////////
		// DAEMON:
		// /////////////////////////////////////////////////////////////////////

		/**
		 * Run.
		 */
		@Override
		public void run() {
			try {
				Message eJob;
				while ( !_isDisposed && isOpened() ) {
					if ( IS_LOG_DEBUG_ENABLED ) {
						LOGGER.log( Level.FINE, "Waiting for Datagram ..." );
					}
					eJob = (Message) _transceiver.receive();
					if ( IS_LOG_DEBUG_ENABLED ) {
						LOGGER.log( Level.FINE, "Read datagram <" + eJob + ">." );
					}
					if ( AbstractRemote.this.isOpened() ) {
						fromSender( eJob );
					}
					else {
						LOGGER.info( "Ignoring job of type \"" + eJob.getClass().getName() + "\" being received whilst connection has been closed." );
					}
				}
			}
			catch ( IOException e ) {
				final boolean isTransceiverClosed = _transceiver.isClosed();
				synchronized ( AbstractRemote.this ) {
					if ( AbstractRemote.this.isOpened() ) {
						try {
							close();
						}
						catch ( IOException e2 ) {
							LOGGER.log( Level.WARNING, "Unable to close the malfunctioning connection as of: " + Trap.asMessage( e2 ), e2 );
						}
					}
				}
				if ( !isTransceiverClosed ) {
					LOGGER.log( Level.WARNING, "Caught an exception in daemon while reading jobs from the transceiver (receiver) with connection status <" + getConnectionStatus() + "> (connection will be closed) and transceiver's connection status <" + getConnectionStatus() + "> as of: " + Trap.asMessage( e ), e );
				}
			}
			finally {
				if ( IS_LOG_DEBUG_ENABLED ) {
					LOGGER.log( Level.FINE, "Terminating job receiver daemon <" + JobReceiverDaemon.this.getClass().getName() + ">." );
				}
			}
		}

		/**
		 * Waits for the {@link DatagramTransceiver} to open up; in case it is
		 * open, then true is returned; in case it does not open within a given
		 * period of time then false is returned.
		 * 
		 * @return True in case the {@link DatagramTransceiver} is (finally)
		 *         open.
		 */
		private boolean isOpened() {
			if ( !AbstractRemote.this.isOpened() ) {
				return false;
			}
			else if ( _transceiver.isOpened() && AbstractRemote.this.isOpened() ) {
				return true;
			}
			else if ( AbstractRemote.this.isOpened() && !_transceiver.isOpened() ) {
				final RetryCounter theRetryCounter = new RetryCounter( IoRetryCount.NORM.getValue(), LatencySleepTime.NORM.getTimeMillis(), LoopExtensionTime.NORM.getTimeMillis() );
				while ( AbstractRemote.this.isOpened() && theRetryCounter.hasNextRetry() && !_transceiver.isOpened() ) {
					if ( IS_LOG_DEBUG_ENABLED ) {
						LOGGER.log( Level.FINE, "Wait loop <" + theRetryCounter.getRetryCount() + "> while waiting for OPEN for <" + WAIT_FOR_REPLY_LOOPS + "> ms; connection status is " + getConnectionStatus() + " (transceiver connection status is " + _transceiver.getConnectionStatus() + ")." );
					}
					theRetryCounter.nextRetry();
				}
			}
			if ( _transceiver.isOpened() && AbstractRemote.this.isOpened() ) {
				return true;
			}
			return false;
		}

		/**
		 * Dispose.
		 */
		@Override
		public void dispose() {
			_isDisposed = true;
		}
	}

	// /////////////////////////////////////////////////////////////////////////
	// JOB DIGESTTER DAEMON:
	// /////////////////////////////////////////////////////////////////////////

	/**
	 * A daemon to process a received {@link Message}.
	 */
	protected class JobDigesterDaemon implements Runnable, Resetable {

		// /////////////////////////////////////////////////////////////////////
		// VARIABLES:
		// /////////////////////////////////////////////////////////////////////

		private Message _job = null;

		// /////////////////////////////////////////////////////////////////////
		// CONSTRRUCTORS:
		// /////////////////////////////////////////////////////////////////////

		/**
		 * Instantiates a new job digester daemon impl.
		 */
		public JobDigesterDaemon( Message aJob ) {
			_job = aJob;
		}

		// /////////////////////////////////////////////////////////////////////
		// METHODS:
		// /////////////////////////////////////////////////////////////////////

		/**
		 * Reset.
		 */
		@Override
		public void reset() {
			_job = null;
		}

		/**
		 * Run.
		 */
		@Override
		public void run() {
			try {
				digest( _job );
			}
			catch ( DigestException e ) {
				if ( !( _job instanceof SignOffProxyMessage ) || isOpened() ) {
					LOGGER.log( Level.WARNING, "Unable to digest the job <" + _job.getClass().getName() + "> as of: " + Trap.asMessage( e ), e );
				}
			}
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy