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.9
Show newest version
// /////////////////////////////////////////////////////////////////////////////
// REFCODES.ORG
// =============================================================================
// This code is copyright (c) by Siegfried Steiner, Munich, Germany and licensed
// 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/LICENSE-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.Serializable;
import java.util.concurrent.ExecutorService;

import org.refcodes.component.AbstractConnectableAutomaton;
import org.refcodes.component.CloseException;
import org.refcodes.component.ConnectionStatus;
import org.refcodes.component.Destroyable;
import org.refcodes.component.DigestException;
import org.refcodes.component.Digester;
import org.refcodes.component.OpenException;
import org.refcodes.component.Resetable;
import org.refcodes.controlflow.ControlFlowUtility;
import org.refcodes.controlflow.RetryCounter;
import org.refcodes.controlflow.RetryCounterImpl;
import org.refcodes.data.IoRetryCount;
import org.refcodes.data.LatencySleepTime;
import org.refcodes.data.LoopExtensionTime;
import org.refcodes.exception.ExceptionUtility;
import org.refcodes.exception.HiddenException;
import org.refcodes.io.DatagramTransceiver;
import org.refcodes.logger.RuntimeLogger;
import org.refcodes.logger.RuntimeLoggerFactorySingleton;
import org.refcodes.mixin.Disposable.Disposedable;

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

	/**
	 * 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 ENABLE_EXTENDED_DEBUG_LOGGING = true;

	private static RuntimeLogger LOGGER = RuntimeLoggerFactorySingleton.createRuntimeLogger();

	// /////////////////////////////////////////////////////////////////////////
	// 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 OpenException {
		_transceiver = aTransceiver;
		ControlFlowUtility.throwIllegalStateException( _isDestroyed || isOpened() );
		ConnectionStatus theConnectionStatus = _transceiver.getConnectionStatus();
		if ( theConnectionStatus != ConnectionStatus.OPENED ) {
			throw new OpenException( "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.debug( "Starting job receiver daemon <" + _jobReceiverDaemon.getClass().getName() + ">." );
		_executorService.execute( _jobReceiverDaemon );
		setConnectionStatus( ConnectionStatus.OPENED );
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized void close() throws CloseException {
		if ( isClosed() ) return;
		ControlFlowUtility.throwIllegalStateException( _isDestroyed );
		if ( ENABLE_EXTENDED_DEBUG_LOGGING ) {
			LOGGER.debug( "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 ( CloseException e ) {
				LOGGER.warn( "Destroying failed as of: " + ExceptionUtility.toMessage( e ), e );
			}
			finally {
				_executorService.shutdownNow();
				try {
					_transceiver.close();
				}
				catch ( CloseException e ) {
					LOGGER.warn( "Destroying failed as of: " + ExceptionUtility.toMessage( 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 cause is
	 *         usually wrapped by this {@link Exception}.
	 * 
	 * @see Digester
	 */
	abstract protected 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 OpenException Thrown in case opening or accessing an open line
	 *         (connection, junction, link) caused problems.
	 */
	protected void toReceiver( Message aJob ) throws OpenException {
		try {
			_transceiver.writeDatagram( aJob );
		}
		catch ( OpenException aException ) {
			if ( !(aJob instanceof CloseConnectionMessage) ) {
				throw aException;
			}
		}
		// @formatter:off
		/*
			if ( isOpened() && _transceiver.isOpened() ) {
				_transceiver.writeDatagram( aJob );
			}
			else {
				LOGGER.warn( "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 ( ENABLE_EXTENDED_DEBUG_LOGGING ) {
					LOGGER.debug( "Received a close connection job to <" + getClass().getName() + ">; closing connection." );
				}
				synchronized ( this ) {
					if ( isOpened() ) {
						close( (CloseConnectionMessage) aJob );
					}
				}
			}
			else {
				JobDigesterDaemonImpl theJobDigesterDaemon = new JobDigesterDaemonImpl();
				theJobDigesterDaemon.setInterProcessJob( 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 CloseConnectionMessageImpl() );
			}
			catch ( OpenException aMessage ) {
				LOGGER.warn( "Sending a close connection job to the communication couinterpart caused an exception.", aMessage );
			}
		}
	}

	/**
	 * 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() {}

	// /////////////////////////////////////////////////////////////////////////
	// INNER CLASSES:
	// /////////////////////////////////////////////////////////////////////////

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

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

		private boolean _isDisposed = false;

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

		/**
		 * Run.
		 */
		@Override
		public void run() {
			try {
				Message eJob;
				while ( !_isDisposed && isOpened() ) {
					if ( ENABLE_EXTENDED_DEBUG_LOGGING ) {
						LOGGER.debug( "Waiting for Datagram ..." );
					}
					eJob = (Message) _transceiver.readDatagram();
					if ( ENABLE_EXTENDED_DEBUG_LOGGING ) {
						LOGGER.debug( "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 ( OpenException | InterruptedException aException ) {
				boolean isTransceiverClosed = _transceiver.isClosed();
				synchronized ( AbstractRemote.this ) {
					if ( AbstractRemote.this.isOpened() ) {
						try {
							close();
						}
						catch ( CloseException e ) {
							LOGGER.warn( "Unable to close the malfunctioning connection: " + ExceptionUtility.toMessage( e ), e );
						}
					}
				}
				if ( !isTransceiverClosed ) {
					LOGGER.warn( "Caught an exception in daemon while reading jobs from the transceiver (receiver); the connection status is " + getConnectionStatus() + " (will get closed if not already); the transceiver's connection status is " + getConnectionStatus() + ": " + ExceptionUtility.toMessage( aException ), aException );
					throw new HiddenException( aException );
				}
			}
			finally {
				// @formatter:off
				// LOGGER.debug( "Terminating job receiver daemon <" + JobReceiverDaemon.this.getClass().getName() + ">." );
				// @formatter:on
			}
		}

		/**
		 * 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() ) {
				RetryCounter theRetryCounter = new RetryCounterImpl( IoRetryCount.NORM.getNumber(), LatencySleepTime.NORM.getMilliseconds(), LoopExtensionTime.NORM.getMilliseconds() );
				while ( AbstractRemote.this.isOpened() && theRetryCounter.hasNextRetry() && !_transceiver.isOpened() ) {
					// @formatter:off
					if ( ENABLE_EXTENDED_DEBUG_LOGGING ) LOGGER.debug( "Wait loop <" + theRetryCounter.getRetryCount() + "> while waiting for OPEN for <" + WAIT_FOR_REPLY_LOOPS + "> ms; connection status is " + getConnectionStatus() + " (transceiver connection status is " + _transceiver.getConnectionStatus() + ").");
					// @formatter:on
					theRetryCounter.nextRetry();
				}
			}
			if ( _transceiver.isOpened() && AbstractRemote.this.isOpened() ) {
				return true;
			}
			return false;
		}

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

		/**
		 * Checks if is disposed.
		 *
		 * @return true, if is disposed
		 */
		@Override
		public boolean isDisposed() {
			return _isDisposed;
		}
	}

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

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

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

		private Message _job = null;

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

		/**
		 * Instantiates a new job digester daemon impl.
		 */
		public JobDigesterDaemonImpl() {}

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

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

		/**
		 * Run.
		 */
		@Override
		public void run() {
			try {
				digest( _job );
			}
			catch ( DigestException aException ) {
				if ( !(_job instanceof SignOffProxyMessage) || isOpened() ) LOGGER.warn( "Unable to digest the job \"" + _job.getClass().getName() + "\" as of a digest exception: \"" + ExceptionUtility.toMessage( aException ) + "\"", aException );
			}
		}

		/**
		 * Sets the {@link Message} attribute.
		 * 
		 * @param aJob The job to be processed.
		 */
		public void setInterProcessJob( Message aJob ) {
			_job = aJob;
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy