org.refcodes.remoting.AbstractRemote Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of refcodes-remoting Show documentation
Show all versions of refcodes-remoting Show documentation
Artifact with proxy functionality for plain remote procedure calls
(RPC), a POJO alternative to RMI.
The 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 );
}
}
}
}
}