// Copyright (c) 2010 Rob Eden.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * 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.
// * Neither the name of Intrepid nor the
// names of its contributors may be used to endorse or promote products
// derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS 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 <COPYRIGHT HOLDER> 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.
package com.starlight.intrepid;
import com.starlight.ValidationKit;
import com.starlight.intrepid.auth.AuthenticationHandler;
import com.starlight.intrepid.auth.ConnectionArgs;
import com.starlight.intrepid.auth.RequestUserCredentialReinit;
import com.starlight.intrepid.exception.ChannelRejectedException;
import com.starlight.intrepid.exception.ConnectionFailureException;
import com.starlight.intrepid.exception.IntrepidRuntimeException;
import com.starlight.intrepid.spi.IntrepidSPI;
import com.starlight.intrepid.spi.mina.MINAIntrepidSPI;
import com.starlight.listeners.ListenerSupport;
import com.starlight.listeners.ListenerSupportFactory;
import com.starlight.thread.ScheduledExecutor;
import com.starlight.thread.SharedThreadPool;
import java.io.IOException;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.channels.ByteChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* This class provides static functions for accessing Intrepid's main functionality.
*/
public class Intrepid {
private static final int CORE_POOL_SIZE =
Integer.getInteger( "intrepid.thread_pool.core", 2 ).intValue();
private static final int MAX_POOL_SIZE =
Integer.getInteger( "intrepid.thread_pool.max", Integer.MAX_VALUE ).intValue();
private static final int KEEPALIVE =
Integer.getInteger( "intrepid.thread_pool.keepalive", 61000 ).intValue();
private static final long CONNECT_TIMEOUT =
Long.getLong( "intrepid.connect.timeout", 10000 ).longValue();
private static final Lock local_instance_map_lock = new ReentrantLock();
private static final Map<VMID,Intrepid> local_instance_map =
new HashMap<VMID,Intrepid>();
private static final ThreadLocal<Intrepid> thread_instance =
new InheritableThreadLocal<Intrepid>();
private static final ListenerSupport<IntrepidInstanceListener,Void> instance_listeners =
ListenerSupportFactory.create( IntrepidInstanceListener.class, true );
/** This is a hook for testing while allows the "inter-instance bridging" provided
* by {@link #findLocalInstance(VMID,boolean)} to be disabled when trying to shortcut.
* When disabled, calls to local instance will user the full stack. */
static volatile boolean disable_inter_instance_bridge =
System.getProperty( "intrepid.disable_inter_instance_bridge" ) != null;
private final IntrepidSPI spi;
private final VMID vmid;
private final LocalCallHandler local_handler;
private final RemoteCallHandler remote_handler;
private final ListenerSupport<ConnectionListener,Void> connection_listeners;
private final ListenerSupport<PerformanceListener,Void> performance_listeners;
private final PerformanceControl performance_control;
private volatile boolean closed = false;
/**
* Initialize Intrepid with the SPI to implement the lower-level communication
* mechanism.
*
* @param setup Builder object containing the settings for the instance.
* Null will create an instance with the default settings for
* client usage.
*
* @throws IllegalStateException If already initialized (without being shut down).
* @throws IOException If an error occurs when initializing the SPI
* driver. This generally means server socket setup.
*/
public static Intrepid create( IntrepidSetup setup )
throws IOException {
if ( setup == null ) setup = new IntrepidSetup();
IntrepidSPI spi = setup.getSPI();
if ( spi == null ) spi = new MINAIntrepidSPI();
ScheduledExecutor thread_pool = setup.getThreadPool();
if ( thread_pool == null ) {
thread_pool = SharedThreadPool.INSTANCE;
}
InetAddress server_address = setup.getServerAddress();
Integer server_port = setup.getServerPort();
AuthenticationHandler auth_handler = setup.getAuthHandler();
String vmid_hint = setup.getVMIDHint();
if ( vmid_hint == null ) {
if ( server_address != null ) vmid_hint = server_address.getHostAddress();
else {
try {
InetAddress local_host = InetAddress.getLocalHost();
if ( local_host != null ) {
vmid_hint = local_host.getHostAddress();
// Weed out silly IP's.
if ( vmid_hint.equals( "127.0.0.1" ) ) vmid_hint = null;
}
}
catch ( UnknownHostException ex ) {
// ignore
}
}
}
ListenerSupport<ConnectionListener,Void> connection_listeners =
ListenerSupportFactory.create( ConnectionListener.class, false );
if ( setup.getConnectionListener() != null ) {
connection_listeners.add( setup.getConnectionListener() );
}
ListenerSupport<PerformanceListener,Void> performance_listeners =
ListenerSupportFactory.create( PerformanceListener.class, false );
final PerformanceListener perf_listener = setup.getPerformanceListener();
if ( perf_listener != null ) {
performance_listeners.add( perf_listener );
}
VMID vmid = new VMID( UUID.randomUUID(), vmid_hint );
// Create handlers
LocalCallHandler local_handler =
new LocalCallHandler( vmid, performance_listeners.dispatch() );
RemoteCallHandler remote_handler = new RemoteCallHandler( spi, auth_handler,
local_handler, vmid, thread_pool, performance_listeners,
setup.getChannelAcceptor() );
// Init SPI
spi.init( server_address, server_port, vmid_hint, remote_handler,
connection_listeners.dispatch(), thread_pool, vmid,
ProxyInvocationHandler.DESERIALIZING_VMID, performance_listeners.dispatch(),
setup.getUnitTestHook() );
Intrepid instance = new Intrepid( spi, vmid, local_handler, remote_handler,
connection_listeners, performance_listeners );
local_handler.initInstance( instance );
remote_handler.initInstance( instance );
instance_listeners.dispatch().instanceOpened( vmid, instance );
return instance;
}
/**
* Indicates whether or not the given object is a proxy.
*/
public static boolean isProxy( Object object ) {
return ProxyKit.isProxy( object );
}
/**
* Similar to {@link #createProxy(Object)}, but this can be done in a static context
* and so has some additional requirements. The method needs to be able to determine
* the applicable <tt>Intrepid</tt> instance. The will be done either by:
* <ol>
* <li>A thread-local instance has been set via {@link #setThreadInstance(Intrepid)}.</li>
* <li>This is called from within an Intrepid call, in which case that instance
* will be used.</li>
* <li>There is one and only one active instance in the VM.</li>
* </ol>
* The second option will likely be the more typical scenario. For example, if a proxy
* should be contained inside a serialized object being returned from a method call,
* this would be suitable for use.
*
* @param delegate The object to which the proxy will delegate.
*
* @return A proxy object.
*
* @throws IllegalStateException If more than one Intrepid instance is active in
* the local VM.
*/
@SuppressWarnings( "UnusedDeclaration" )
public static Object staticCreateProxy( Object delegate ) {
if ( isProxy( delegate ) ) return delegate;
// See if there's a thread instance set
Intrepid instance = thread_instance.get();
// See if there is an active call context from which we can determine the instance
if ( instance == null ) {
instance = IntrepidContext.getActiveInstance();
}
// If not, see if there is only one active instance
if ( instance == null ) {
local_instance_map_lock.lock();
try {
if ( local_instance_map.size() != 1 ) {
throw new IllegalStateException( "This method can only be used if " +
" is one and only one instance active but there are " +
local_instance_map.size() + " instances active." );
}
instance = local_instance_map.values().iterator().next();
}
finally {
local_instance_map_lock.unlock();
}
}
return instance.createProxy( delegate );
}
/**
* Sets the instance to be used by {@link #staticCreateProxy(Object)}. This value is
* inherited to child threads, so it only need be set on the parent. See the
* documentation for {@link #staticCreateProxy(Object)} for the algorithm used to
* determine the instance use if this is not set.
*
* @param instance The instance to be used or null to clear the current value.
*/
public static void setThreadInstance( Intrepid instance ) {
if ( instance == null ) thread_instance.remove();
else thread_instance.set( instance );
}
/**
* Add a listener that will be notified when a new Intrepid instance is created.
*/
public static void addInstanceListener( IntrepidInstanceListener listener ) {
instance_listeners.add( listener );
}
/**
* @see #addInstanceListener(IntrepidInstanceListener)
*/
@SuppressWarnings( "UnusedDeclaration" )
public static void removeInstanceListener( IntrepidInstanceListener listener ) {
instance_listeners.remove( listener );
}
/**
* Find the Intrepid instance with the VMID in the local VM.
*
* @see #disable_inter_instance_bridge
*/
static Intrepid findLocalInstance( VMID vmid, boolean trying_to_shortcut ) {
if ( trying_to_shortcut && disable_inter_instance_bridge ) return null;
local_instance_map_lock.lock();
try {
return local_instance_map.get( vmid );
}
finally {
local_instance_map_lock.unlock();
}
}
/**
* Returns the instance that has a connection to the given VMID or null if none.
*/
static Intrepid findInstanceWithRemoteSession( VMID remote_vmid ) {
// Check the thread-specified instance first
Intrepid t_instance = thread_instance.get();
if ( t_instance != null && t_instance.spi.hasConnection( remote_vmid ) ) {
return t_instance;
}
local_instance_map_lock.lock();
try {
for( Intrepid instance : local_instance_map.values() ) {
if ( instance.spi.hasConnection( remote_vmid ) ) return instance;
}
return null;
}
finally {
local_instance_map_lock.unlock();
}
}
private Intrepid( IntrepidSPI spi, VMID vmid, LocalCallHandler local_handler,
RemoteCallHandler remote_handler,
ListenerSupport<ConnectionListener,Void> connection_listeners,
ListenerSupport<PerformanceListener,Void> performance_listeners ) {
this.spi = spi;
this.vmid = vmid;
this.local_handler = local_handler;
this.remote_handler = remote_handler;
this.connection_listeners = connection_listeners;
this.performance_listeners = performance_listeners;
this.performance_control = new PerformanceControlWrapper();
local_instance_map_lock.lock();
try {
local_instance_map.put( vmid, this );
}
finally {
local_instance_map_lock.unlock();
}
}
public void close() {
closed = true;
local_instance_map_lock.lock();
try {
local_instance_map.remove( vmid );
}
finally {
local_instance_map_lock.unlock();
}
local_handler.shutdown();
spi.shutdown();
instance_listeners.dispatch().instanceClosed( getLocalVMID() );
}
/**
* Wrap the delegate object in a proxy, if it isn't already.
*
* @param delegate The object to which the proxy will delegate.
*
* @return A proxy object.
*/
public Object createProxy( Object delegate ) {
if ( closed ) throw new IllegalStateException( "Closed" );
if ( isProxy( delegate ) ) return delegate;
return local_handler.createProxy( delegate, null );
}
/**
* Indicates whether or not the given object is a local proxy.
*
* @see #isProxy(Object)
*/
public boolean isProxyLocal( Object object ) {
return ProxyKit.isProxyLocal( object, vmid );
}
/**
* If the given object is a local proxy, this returns the object it delegates calls
* to, if available. If it is not a proxy or the proxy is not local, it returns null.
* <p>
* The delegate may not be available under certain circumstances, generally
* encountered during test (if the proxy has been serialized and the
* {@link Intrepid#disable_inter_instance_bridge inter-instance bridge} is disabled).
*
* @see #isProxy(Object)
* @see #isProxyLocal(Object)
*/
public Object getLocalProxyDelegate( Object object ) {
return ProxyKit.getLocalProxyDelegate( object, vmid );
}
/**
* Returns the VMID the given proxy object points to. If the object is not a proxy or
* is local, it returns null.
*
* @see #isProxy(Object)
* @see #isProxyLocal(Object)
*/
public VMID getRemoteProxyVMID( Object object ) {
return ProxyKit.getRemoteProxyVMID( object, vmid );
}
/**
* Return the VMID of the local VM.
*/
public VMID getLocalVMID() {
return vmid;
}
/**
* Return a pointer to a remote registry.
*/
public Registry getRemoteRegistry( VMID vmid ) {
if ( closed ) throw new IllegalStateException( "Closed" );
return remote_handler.getRemoteRegistry( vmid );
}
/**
* Get the local registry.
*/
public LocalRegistry getLocalRegistry() {
LocalRegistry registry = local_handler.getLocalRegistry();
registry.setInstance( this );
return registry;
}
/**
* Connect to remote host, throwing an exception immediately if the host is not
* reachable.
*
* @param host Host to connect to.
* @param args (Optional) SPI-dependant connection args.
* @param attachment An object the caller can associate with the connection
*
* @return The VMID of the remote host.
*
* @throws IOException Thrown if an error occurs while trying to
* connect.
* @throws ConnectionFailureException If the connection failed due to an
* authentication/authorization failure.
*
* @see #tryConnect
*/
public VMID connect( InetAddress host, int port, ConnectionArgs args,
Object attachment ) throws IOException {
long timeout = CONNECT_TIMEOUT;
TimeUnit timeout_unit = TimeUnit.MILLISECONDS;
if ( args instanceof RequestUserCredentialReinit ) {
timeout = 5;
timeout_unit = TimeUnit.MINUTES;
}
return spi.connect( host, port, args, attachment, timeout, timeout_unit, false );
}
/**
* Wait for a connection to the remote host.
*
* @param host Host to connect to.
* @param args (Optional) SPI-dependant connection args.
* @param timeout Time to wait for the connection. Note that this is a soft
* timeout, so it's guaranteed to try for at least the time given
* and not start any long operations after the time has expired.
* @param timeout_units Time unit for <tt>timeout</tt> argument.
*
* @return The VMID of the remote host. This will always be non-null.
* If the timeout is reached, an exception (indicating the most
* recent failure cause) will be thrown.
*
* @throws IOException Thrown if an error occurs while trying to connect.
*/
public VMID tryConnect( InetAddress host, int port, ConnectionArgs args,
Object attachment, long timeout, TimeUnit timeout_units )
throws IOException, InterruptedException {
ValidationKit.checkNonnull( host, "host" );
if ( closed ) throw new IllegalStateException( "Closed" );
return spi.connect( host, port, args, attachment, timeout, timeout_units, true );
}
/**
* Disconnect from a remote host.
*/
public void disconnect( VMID host_vmid ) {
if ( closed ) throw new IllegalStateException( "Closed" );
ValidationKit.checkNonnull( host_vmid, "host_vmid" );
spi.disconnect( host_vmid );
}
/**
* Creates a virtual channel to the given destination operating over the Intrepid
* connection. Channels allow for higher performance data streaming than is possible
* with individual method calls.
*
* @param destination The destination VM. This cannot specify the local instance.
* @param attachment An optional attachment for identifying the channel to the
* server.
*
* @return The channel, if successful. The returned channel will not
* support non-blocking mode.
*
* @throws IOException Indicates a communication-related failure.
* @throws ChannelRejectedException Indicates the channel was rejected by the server.
*/
public ByteChannel createChannel( VMID destination, Serializable attachment )
throws IOException, ChannelRejectedException {
ValidationKit.checkNonnull( destination, "destination" );
if ( destination.equals( vmid ) ) {
throw new IllegalArgumentException( "Destination cannot be local instance" );
}
return remote_handler.channelCreate( destination, attachment );
}
/**
* Returns the server port in use, if applicable.
*/
public Integer getServerPort() {
return spi.getServerPort();
}
/**
* Ping a remote connection to see if it's responding.
*
* @param vmid VMID of the instance to ping.
* @param timeout Time allowed for the response.
* @param timeout_unit Unit for <tt>timeout</tt>.
*
* @return The time in which the response was received.
*
* @throws TimeoutException Thrown if the timeout expires.
* @throws IntrepidRuntimeException Thrown if a communication error occurs.
*/
public long ping( VMID vmid, long timeout, TimeUnit timeout_unit ) throws
TimeoutException, IntrepidRuntimeException, InterruptedException {
return remote_handler.ping( vmid, timeout, timeout_unit );
}
/**
* Add a listener that is notified when connections are opened or closed.
*/
public void addConnectionListener( ConnectionListener listener ) {
connection_listeners.add( listener );
}
/**
* Remove a {@link ConnectionListener}.
*
* @see #addConnectionListener(ConnectionListener)
*/
public void removeConnectionListener( ConnectionListener listener ) {
connection_listeners.remove( listener );
}
/**
* Add a listener that is notified regarding method calls and related performance
* statistics.
*
* @return A {@link PerformanceControl} object for tuning performance parameters,
* generally for debugging/performance testing.
*/
public PerformanceControl addPerformanceListener( PerformanceListener listener ) {
performance_listeners.add( listener );
return performance_control;
}
/**
* Remove a {@link PerformanceListener}.
*
* @see #addPerformanceListener(PerformanceListener)
*/
public void removePerformanceListener( PerformanceListener listener ) {
performance_listeners.remove( listener );
}
/**
* Return a pointer to a local handler.
*/
LocalCallHandler getLocalCallHandler() {
if ( closed ) throw new IllegalStateException( "Closed" );
return local_handler;
}
/**
* Return a pointer to a remote handler.
*/
RemoteCallHandler getRemoteCallHandler() {
if ( closed ) throw new IllegalStateException( "Closed" );
return remote_handler;
}
/**
* Return a pointer to the SPI.
*/
IntrepidSPI getSPI() {
if ( closed ) throw new IllegalStateException( "Closed" );
return spi;
}
@Override
public String toString() {
return "Intrepid{vmid=" + vmid + '}';
}
private class PerformanceControlWrapper implements PerformanceControl {
@Override
public void setMessageSendDelay( Long delay_ms ) {
spi.setMessageSendDelay( delay_ms );
}
}
}