// 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.ArrayKit;
import com.starlight.intrepid.exception.IllegalProxyDelegateException;
import com.starlight.intrepid.exception.UnknownMethodException;
import com.starlight.intrepid.exception.UnknownObjectException;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.TObjectLongMap;
import gnu.trove.map.custom_hash.TObjectIntCustomHashMap;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.map.hash.TObjectLongHashMap;
import gnu.trove.procedure.TObjectLongProcedure;
import gnu.trove.procedure.TObjectProcedure;
import gnu.trove.strategy.IdentityHashingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Externalizable;
import java.io.Serializable;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*
*/
class LocalCallHandler {
private static final Logger LOG = LoggerFactory.getLogger( LocalCallHandler.class );
// NOTE: ID zero is reserved
private static final boolean OBJECT_ID_STARTS_AT_ONE =
System.getProperty( "intrepid.oid_start_at_one" ) != null;
private static final long LOCAL_VM_INITIAL_RESERVATION_DURATION_NS =
TimeUnit.MILLISECONDS.toNanos(
Long.getLong( "intrepid.local_call_handler.initial_reservation",
TimeUnit.MINUTES.toMillis( 5 ) ).longValue() ); // 5 min
private static final Class[] PROXY_CLASS_ARRAY = new Class[] { Proxy.class };
private static final Class[] REGISTRY_CLASSES =
new Class[] { Registry.class, Proxy.class };
private final VMID vmid;
private final LocalRegistry local_registry;
private Intrepid instance; // for call context info
private final PerformanceListener performance_listeners;
private final Lock map_lock = new ReentrantLock();
private final TObjectIntMap<Object> object_to_id_map =
new TObjectIntCustomHashMap<Object>( new IdentityHashingStrategy<Object>() );
private final TIntObjectMap<ProxyInfo> id_to_object_map =
new TIntObjectHashMap<ProxyInfo>();
// NOTE: ID zero is reserved
private final AtomicInteger object_id_counter =
new AtomicInteger( OBJECT_ID_STARTS_AT_ONE ? 1 : new Random().nextInt() );
private final LeasePruner lease_pruner = new LeasePruner();
private final ReferenceQueue<Proxy> ref_queue = new ReferenceQueue<Proxy>();
LocalCallHandler( VMID vmid, PerformanceListener performance_listeners ) {
this.vmid = vmid;
this.performance_listeners = performance_listeners;
LeaseManager.registerLocalHandler( vmid, this );
local_registry = new LocalRegistry( vmid );
bindRegistry( local_registry, vmid );
}
void initInstance( Intrepid instance ) {
this.instance = instance;
}
VMID getVMID() {
return vmid;
}
void shutdown() {
LeaseManager.deregisterLocalHandler( vmid );
}
Proxy createProxy( Object delegate, String persistent_name ) {
// Make sure it's not already a proxy
if ( delegate instanceof Proxy ) return ( Proxy ) delegate;
// Assume we're actually going to create the proxy so we minimize the time with
// the map locked.
// Do this before hitting the map so we know it's a proxy-able object
Class[] interfaces = findProxyInterfaces( delegate.getClass() );
TIntObjectMap<Method> method_map = new TIntObjectHashMap<Method>();
for( Class ifc : interfaces ) {
TIntObjectMap<Method> map = MethodMap.generateMethodMap( ifc );
method_map.putAll( map );
}
TObjectIntMap<MethodIDTemplate> reverse_method_map =
MethodMap.generateReverseMethodMap( method_map );
Proxy proxy = null;
map_lock.lock();
try {
if ( object_to_id_map.containsKey( delegate ) ) {
int object_id = object_to_id_map.get( delegate );
ProxyInfo proxy_info = id_to_object_map.get( object_id );
proxy = proxy_info.promoteReference();
assert proxy != null :
"Unable to promote reference for delegate: " + delegate;
}
// If we were unable to get a proxy from the maps, create one now.
if ( proxy == null ) {
int object_id = object_id_counter.incrementAndGet();
if ( object_id == 0 ) { // NOTE: ID zero is reserved
object_id = object_id_counter.incrementAndGet();
}
object_to_id_map.put( delegate, object_id );
LOG.debug( "Adding proxy {} for: {}", Integer.valueOf( object_id ),
delegate );
ClassLoader class_loader = delegate.getClass().getClassLoader();
if ( class_loader == ClassLoader.getSystemClassLoader() ||
class_loader == null ) {
class_loader = Intrepid.class.getClassLoader();
}
proxy = ( Proxy ) java.lang.reflect.Proxy.newProxyInstance(
class_loader,
ArrayKit.combine( Class.class, interfaces, PROXY_CLASS_ARRAY ),
new ProxyInvocationHandler( vmid, object_id, reverse_method_map,
persistent_name, delegate ) );
ProxyInfo proxy_info = new ProxyInfo( proxy, ref_queue, object_id,
delegate, method_map, vmid );
id_to_object_map.put( object_id, proxy_info );
}
}
catch( IllegalArgumentException ex ) {
StringBuilder buf = new StringBuilder();
for( Class clazz : interfaces ) {
if ( buf.length() != 0 ) buf.append( ", " );
buf.append( Modifier.toString( clazz.getModifiers() ) );
buf.append( ' ' );
buf.append( clazz.getName() );
}
throw new IllegalArgumentException( ex.getMessage() + " (Interfaces: " +
buf.toString() + ")", ex );
}
finally {
map_lock.unlock();
}
return proxy;
}
public Object invoke( int object_id, int method_id, Object[] args,
boolean local_origination, String persistent_name ) throws Throwable {
ProxyInfo proxy_info;
map_lock.lock();
try {
proxy_info = id_to_object_map.get( object_id );
}
finally {
map_lock.unlock();
}
if ( proxy_info == null ) {
throw new UnknownObjectException( object_id, persistent_name, vmid );
}
Method method = proxy_info.method_map.get( method_id );
if ( method == null ) {
throw new UnknownMethodException( method_id );
}
Thread my_thread = Thread.currentThread();
final String original_thread_name = my_thread.getName();
try {
if ( !local_origination ) {
my_thread.setName( "Intrepid invoke: " + methodToString( method ) +
" (source: " + IntrepidContext.getCallingVMID() + ")" );
}
method.setAccessible( true );
// TODO: user context??
if ( local_origination ) {
IntrepidContext.setCallInfo( instance, vmid, null, null );
}
return method.invoke( proxy_info.delegate, args );
}
catch( InvocationTargetException ex ) {
throw ex.getCause();
}
finally {
if ( local_origination ) IntrepidContext.clearCallInfo();
else my_thread.setName( original_thread_name );
}
}
/**
* Find the method associated with a given object and method ID.
*
* @return The method ID or null if not found.
*/
Method lookupMethodForID( int object_id, int method_id ) {
ProxyInfo proxy_info;
map_lock.lock();
try {
proxy_info = id_to_object_map.get( object_id );
}
finally {
map_lock.unlock();
}
if ( proxy_info == null ) return null;
return proxy_info.method_map.get( method_id );
}
LocalRegistry getLocalRegistry() {
return local_registry;
}
Object getLocalDelegate( int object_id ) {
ProxyInfo proxy_info;
map_lock.lock();
try {
proxy_info = id_to_object_map.get( object_id );
}
finally {
map_lock.unlock();
}
if ( proxy_info == null ) return null;
return proxy_info.delegate;
}
void renewLease( int object_id, VMID originating_vm ) {
ProxyInfo info;
map_lock.lock();
try {
info = id_to_object_map.get( object_id );
}
finally {
map_lock.unlock();
}
if ( info == null ) return;
info.renewLease( originating_vm );
}
void giveUpLease( int object_id, VMID originating_vm ) {
ProxyInfo info;
map_lock.lock();
try {
info = id_to_object_map.get( object_id );
}
finally {
map_lock.unlock();
}
if ( info == null ) return;
info.giveUpLease( originating_vm );
}
// This should only be called from the Lease Manager
void pruneLeases() {
if ( LeaseManager.LEASE_DEBUGGING ) System.out.println( "---pruneLeases---" );
LOG.trace( "---pruneLeases---" );
lease_pruner.reset();
map_lock.lock();
try {
ProxyWeakReference ref;
while( ( ref = ( ProxyWeakReference ) ref_queue.poll() ) != null ) {
object_to_id_map.remove( ref.delegate );
id_to_object_map.remove( ref.object_id );
if ( LeaseManager.LEASE_DEBUGGING ) {
System.out.println( "Removing object " + ref.object_id + ": " +
ref.delegate_to_string );
}
if ( LOG.isTraceEnabled() ) {
LOG.trace( "Removing object {}: {}", Integer.valueOf( ref.object_id ),
ref.delegate_to_string );
}
performance_listeners.leasedObjectRemoved( vmid, ref.object_id );
}
id_to_object_map.forEachValue( lease_pruner );
}
finally {
map_lock.unlock();
}
}
void markBound( int object_id, boolean bound ) {
map_lock.lock();
try {
ProxyInfo info = id_to_object_map.get( object_id );
if ( info == null ) return;
info.bound = bound;
if ( bound ) info.promoteReference();
performance_listeners.leaseInfoUpdated( vmid, info.object_id,
info.delegate_to_string, info.strong_ref != null,
info.vmid_lease_map.size(), false, false );
}
finally {
map_lock.unlock();
}
}
// Package-private for testability
static Class[] findProxyInterfaces( Class object_class ) {
Set<Class> class_set = new HashSet<Class>();
findProxyInterfaces( object_class, class_set );
if ( class_set.isEmpty() ) {
throw new IllegalProxyDelegateException( "No valid interfaces found." );
}
// Make sure it's Serializable
class_set.add( Serializable.class );
return class_set.toArray( new Class[ class_set.size() ] );
}
private void bindRegistry( LocalRegistry registry, VMID vmid ) {
// Registry is always object ID "0
TIntObjectMap<Method> method_map = MethodMap.generateMethodMap( Registry.class );
TObjectIntMap<MethodIDTemplate> reverse_method_map =
MethodMap.generateReverseMethodMap( method_map );
ProxyInfo proxy_info;
map_lock.lock();
try {
object_to_id_map.put( registry, 0 );
ClassLoader class_loader = registry.getClass().getClassLoader();
Proxy proxy = ( Proxy ) java.lang.reflect.Proxy.newProxyInstance(
class_loader, REGISTRY_CLASSES,
new ProxyInvocationHandler( vmid, 0, reverse_method_map, null, registry ) );
// NOTE: force us to always hold a strong reference
proxy_info = new ProxyInfo( proxy, ref_queue, 0, registry, method_map,
vmid, true );
id_to_object_map.put( 0, proxy_info );
}
finally {
map_lock.unlock();
}
}
private static void findProxyInterfaces( Class clazz, Set<Class> interface_list ) {
Class[] implemented_classes = clazz.getInterfaces();
for( Class implemented_class : implemented_classes ) {
// Add all interfaces, except for Externalizable
if ( Externalizable.class.isAssignableFrom( implemented_class ) ) continue;
// Skip non-public interfaces
if ( !Modifier.isPublic( implemented_class.getModifiers() ) ) continue;
interface_list.add( implemented_class );
}
Class super_class = clazz.getSuperclass();
if ( super_class == null ) return;
findProxyInterfaces( super_class, interface_list );
}
private static String methodToString( Method method ) {
StringBuilder buf = new StringBuilder();
buf.append( method.getDeclaringClass().getSimpleName() );
buf.append( '.' );
buf.append( method.getName() );
buf.append( '(' );
boolean first = true;
for( Class<?> type : method.getParameterTypes() ) {
if ( first ) first = false;
else {
buf.append( ',' );
}
buf.append( type.getSimpleName() );
}
buf.append( ')' );
return buf.toString();
}
private class ProxyInfo {
private final int object_id;
private final boolean force_strong_ref;
private volatile boolean bound = false;
private volatile Proxy strong_ref;
private final ProxyWeakReference weak_ref;
private final Object delegate;
private final TIntObjectMap<Method> method_map;
private final String delegate_to_string;
private final StackTraceElement[] allocation_trace;
// Map containing leases and the time at which they are next expected to renew
private final TObjectLongMap<VMID> vmid_lease_map =
new TObjectLongHashMap<VMID>();
private final Lock vmid_lease_map_lock = new ReentrantLock();
ProxyInfo( Proxy proxy, ReferenceQueue<Proxy> ref_queue, int object_id,
Object delegate, TIntObjectMap<Method> method_map, VMID local_vmid ) {
this( proxy, ref_queue, object_id, delegate, method_map, local_vmid,
LeaseManager.DGC_DISABLED );
}
ProxyInfo( Proxy proxy, ReferenceQueue<Proxy> ref_queue, int object_id,
Object delegate, TIntObjectMap<Method> method_map, VMID local_vmid,
boolean force_strong_ref ) {
this.object_id = proxy.__intrepid__getObjectID();
this.force_strong_ref = force_strong_ref;
// NOTE: automatically grab strong reference
strong_ref = proxy;
// Automatically register a reservation for this VMID for 30 sec. That will
// prevent the reservation from going away too quickly if it is purely a
// remotely referenced proxy.
vmid_lease_map.put( local_vmid,
System.nanoTime() + LOCAL_VM_INITIAL_RESERVATION_DURATION_NS );
this.delegate = delegate;
this.delegate_to_string = delegate.toString();
this.method_map = method_map;
weak_ref = new ProxyWeakReference( proxy, object_id, ref_queue, delegate,
delegate_to_string );
if ( LeaseManager.LEASE_DEBUGGING ) {
allocation_trace = new Throwable().getStackTrace();
}
else allocation_trace = null;
notifyPerformanceListeners( false, false );
}
void renewLease( VMID originating_vmid ) {
promoteReference();
vmid_lease_map_lock.lock();
try {
vmid_lease_map.put( originating_vmid,
System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(
LeaseManager.LEASE_DURATION_MS ) );
}
finally {
vmid_lease_map_lock.unlock();
}
notifyPerformanceListeners( true, false );
}
void giveUpLease( VMID originating_vmid ) {
vmid_lease_map_lock.lock();
try {
vmid_lease_map.remove( originating_vmid );
if ( vmid_lease_map.isEmpty() ) {
if ( LeaseManager.LEASE_DEBUGGING ) {
System.out.println( "demoting reference for " + object_id +
" (" + delegate_to_string +
") after giveUpLease since vmid_lease_map is empty." );
}
demoteReference();
}
}
finally {
vmid_lease_map_lock.unlock();
}
notifyPerformanceListeners( false, true );
}
/**
* Attempt to promote the reference to a strong reference and return whether or
* that action was successful. This can be unsuccessful if the weak reference was
* already GC'ed. If successful the Proxy will be returned. Otherwise null will
* be returned.
*/
private Proxy promoteReference() {
if ( bound || force_strong_ref || strong_ref != null ) return strong_ref;
strong_ref = weak_ref.get();
if ( LeaseManager.LEASE_DEBUGGING && strong_ref == null ) {
System.out.println( ">>> Referent for weak reference was null " +
"for object " + object_id + " (" + delegate_to_string + ")" );
}
notifyPerformanceListeners( false, false );
return strong_ref;
}
/**
* Demote the reference to a weak reference.
*
* @return true if the reference was actually demoted.
*/
private boolean demoteReference() {
if ( bound || force_strong_ref ) return false;
Proxy old_ref = strong_ref;
if ( LeaseManager.LEASE_DEBUGGING && old_ref != null ) {
synchronized ( System.out ) {
vmid_lease_map_lock.lock();
try {
System.out.println( ">>> Demoting reference for " + object_id +
"(" + delegate_to_string + "). Lease map: " + vmid_lease_map );
}
finally {
vmid_lease_map_lock.unlock();
}
if ( allocation_trace != null ) {
for( StackTraceElement element : allocation_trace ) {
System.out.println( ">>> at " + element );
}
}
}
}
strong_ref = null;
notifyPerformanceListeners( false, false );
return old_ref != null;
}
private void notifyPerformanceListeners( boolean renew, boolean release ) {
performance_listeners.leaseInfoUpdated( vmid, object_id, delegate_to_string,
strong_ref != null, vmid_lease_map.size(), renew, release );
}
@Override
public String toString() {
return "{ProxyInfo " + delegate_to_string + "}";
}
}
private static class ProxyWeakReference extends WeakReference<Proxy> {
private final int object_id;
private final Object delegate;
private final String delegate_to_string;
public ProxyWeakReference( Proxy proxy, int object_id,
ReferenceQueue<Proxy> ref_queue, Object delegate, String delegate_to_string ) {
super( proxy, ref_queue );
this.object_id = object_id;
this.delegate = delegate;
this.delegate_to_string = delegate_to_string;
}
}
private static class LeasePruner implements TObjectProcedure<ProxyInfo>,
TObjectLongProcedure<VMID> {
private final VMID[] vms_to_remove = new VMID[ 250 ];
private int next_vm_index = 0;
private long current_time;
void reset() {
next_vm_index = 0;
current_time = System.nanoTime();
}
@Override
public boolean execute( ProxyInfo info ) {
// See if this proxy has lease management disabled.
if ( info.force_strong_ref ) return true;
info.vmid_lease_map_lock.lock();
try {
// If there are no entries, exit quickly
if ( info.vmid_lease_map.isEmpty() ) {
if ( LeaseManager.LEASE_DEBUGGING ) {
System.out.println( "demoting reference for " + info.object_id +
" (" + info.delegate_to_string +
") vmid_lease_map is empty at start of LeasePruner." );
}
boolean demoted = info.demoteReference();
if ( demoted ) {
LOG.debug( "Reference demoted for {}",
Integer.valueOf( info.object_id ) );
}
return true;
}
info.vmid_lease_map.forEachEntry( this );
// If any VMID's were marked for removal, remove them now.
if ( next_vm_index > 0 ) {
for( int i = 0; i < next_vm_index; i++ ) {
info.vmid_lease_map.remove( vms_to_remove[ i ] );
}
next_vm_index = 0;
}
if ( info.vmid_lease_map.isEmpty() ) {
if ( LeaseManager.LEASE_DEBUGGING ) {
System.out.println( "demoting reference for " + info.object_id +
" (" + info.delegate_to_string +
") vmid_lease_map is empty after pruning entries." );
}
boolean demoted = info.demoteReference();
if ( demoted ) {
LOG.debug( "Reference demoted for {}",
Integer.valueOf( info.object_id ) );
}
}
}
finally {
info.vmid_lease_map_lock.unlock();
}
return true;
}
@Override
public boolean execute( VMID vmid, long expiration_time ) {
if ( expiration_time < current_time ) {
vms_to_remove[ next_vm_index ] = vmid;
next_vm_index++;
}
// Stop if we're about to overrun the array
return next_vm_index < vms_to_remove.length;
}
}
}