package org.infinispan.atomic.container;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache;
import org.infinispan.atomic.Updatable;
import org.infinispan.commons.marshall.jboss.GenericJBossMarshaller;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.event.CacheEntryEvent;
import org.infinispan.util.concurrent.TimeoutException;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;
/**
* @author Pierre Sutra
* @since 7.2
*/
@Listener(sync = false, clustered = true, primaryOnly = true)
public class Container {
//
// CLASS FIELDS
//
private static final MethodFilter methodFilter = new MethodFilter() {
@Override
public boolean isHandled(Method m) {
// ignore finalize() and externalization related methods.
return !m.getName().equals("finalize")
&& !m.getName().equals("readExternal")
&& !m.getName().equals("writeExternal");
}
};
private static Log log = LogFactory.getLog(Container.class);
private static final int CALL_TTIMEOUT_TIME = 3000;
private static final int RETRIEVE_TTIMEOUT_TIME = 3000;
private static Executor globalExecutors = Executors.newCachedThreadPool();
//
// OBJECT FIELDS
//
private Cache cache;
private Object key;
private Object object;
private Class clazz;
private Object proxy;
private List<String> updateMethods;
private Executor callExecutor = new SerialExecutor(globalExecutors);
private Boolean withReadOptimization; // serialize operation on the object copy
private volatile int listenerState; // 0 = not installed, 1 = installed, -1 = disposed
private final Container listener = this;
private Map<Long,CallFuture> registeredCalls;
private CallFuture retrieve_future;
private ArrayList<CallInvoke> pending_calls;
private CallRetrieve retrieve_call;
//
// PUBLIC METHODS
//
public Container(final Cache c, final Class cl, final Object k, final boolean readOptimization,
final boolean forceNew, final List<String> methods, final Object... initArgs)
throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, InterruptedException, ExecutionException, NoSuchMethodException, InvocationTargetException {
cache = c;
clazz = cl;
key = clazz.getSimpleName()+"#"+k.toString(); // to avoid collisions
withReadOptimization = readOptimization;
listenerState = 0;
updateMethods = methods;
registeredCalls = new ConcurrentHashMap<Long, CallFuture>();
// build the proxy
MethodHandler handler = new MethodHandler() {
public Object invoke(Object self, Method m, Method proceed, Object[] args) throws Throwable {
GenericJBossMarshaller marshaller = new GenericJBossMarshaller();
// 1 - local operation
if ( withReadOptimization
&& ! updateMethods.contains(m.getName()) ) {
if (log.isDebugEnabled()) log.debugf("Executing %s locally", m.getName());
return callObject(object, m.getName(), args);
}
// 2 - remote operation
// 2.1 - if necessary, listener installation and object re-creation
initObject(true, forceNew, initArgs);
// 2.2 - call creation
long callID = nextCallID();
CallInvoke invoke = new CallInvoke(callID,m.getName(),args);
byte[] bb = marshaller.objectToByteBuffer(invoke);
CallFuture future = new CallFuture();
registeredCalls.put(callID, future);
// 2.3 - call execution
cache.put(Container.this.key, bb);
if (log.isDebugEnabled()) log.debugf("Waiting on %s", future.toString());
Object ret = future.get(CALL_TTIMEOUT_TIME,TimeUnit.MILLISECONDS);
registeredCalls.remove(callID);
if(!future.isDone()){
throw new TimeoutException("Unable to execute "+invoke+" on "+clazz+ " @ "+ Container.this.key);
}
if (log.isDebugEnabled()) log.debugf("Return %s %s ", invoke.toString(), (ret==null ? "null" : ret.toString()));
return ret;
}
};
ProxyFactory fact = new ProxyFactory();
fact.setSuperclass(clazz);
fact.setFilter(methodFilter);
proxy = fact.createClass().newInstance();
((ProxyObject)proxy).setHandler(handler);
initObject(false, forceNew, initArgs);
}
/**
* Internal use of the listener API.
*
* @param event of class CacheEntryModifiedEvent
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@CacheEntryModified
@CacheEntryCreated
@Deprecated
public void onCacheModification(CacheEntryEvent event){
if( !event.getKey().equals(key) )
return;
if(event.isPre())
return;
try {
GenericJBossMarshaller marshaller = new GenericJBossMarshaller();
byte[] bb = (byte[]) event.getValue();
Call call = (Call) marshaller.objectFromByteBuffer(bb);
if (log.isDebugEnabled()) log.debugf("Receive %s (isOriginLocal=%s) ", call, event.isOriginLocal());
callExecutor.execute(new AtomicObjectContainerTask(call));
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void dispose(boolean keepPersistent) throws IOException, InterruptedException{
if (log.isDebugEnabled()) log.debugf("Disposing ");
if (!registeredCalls.isEmpty()){
log.warnf("Cannot dispose - registeredCalls non-empty");
return;
}
if (listenerState==1){
cache.removeListener(listener);
if ( keepPersistent ) {
if (log.isDebugEnabled()) log.debugf("Persisted");
GenericJBossMarshaller marshaller = new GenericJBossMarshaller();
CallPersist persist = new CallPersist(0,object);
byte[] bb = marshaller.objectToByteBuffer(persist);
cache.put(key, bb);
}
}
listenerState = -1;
}
public Object getProxy(){
return proxy;
}
public Class getClazz(){
return clazz;
}
@Override
public String toString(){
return "Container ["+this.key.toString()+"]";
}
//
// PRIVATE METHODS
//
/**
*
* @param invocation
* @return true if the operation is local
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
private boolean handleInvocation(CallInvoke invocation)
throws InvocationTargetException, IllegalAccessException {
Object ret = callObject(object, invocation.method, invocation.arguments);
CallFuture future = registeredCalls.get(invocation.callID);
if(future!=null){
if (log.isDebugEnabled()) log.debugf("Updating %s",future.toString());
future.setReturnValue(ret);
return true;
}else{
log.debugf("No future for %s", invocation.callID);
}
return false;
}
private void initObject(boolean installListener, boolean forceNew, Object... initArgs) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
if (object!=null && (!installListener || listenerState==1))
return;
if (installListener)
installListener();
if( !forceNew){
GenericJBossMarshaller marshaller = new GenericJBossMarshaller();
try {
Call persist = (Call) marshaller.objectFromByteBuffer((byte[]) cache.get(key));
if(persist instanceof CallPersist){
if (log.isDebugEnabled()) log.debugf("Persisted %s",key.toString());
object = ((CallPersist)persist).object;
}else{
installListener();
if (log.isDebugEnabled()) log.debugf("Retrieving object %s",key.toString());
if (installListener==false)
throw new IllegalAccessException();
retrieve_future = new CallFuture();
retrieve_call = new CallRetrieve(nextCallID());
marshaller = new GenericJBossMarshaller();
cache.put(key,marshaller.objectToByteBuffer(retrieve_call));
retrieve_future.get(RETRIEVE_TTIMEOUT_TIME,TimeUnit.MILLISECONDS);
if(!retrieve_future.isDone()) throw new TimeoutException();
if (log.isDebugEnabled()) log.debugf("Object %s retrieved", key.toString());
assert object!=null;
}
if (object instanceof Updatable){
((Updatable)object).setCache(this.cache);
((Updatable)object).setKey(this.key);
}
return;
} catch (Exception e) {
if (log.isDebugEnabled()) log.debugf("Unable to retrieve object %s from the cache.", key.toString());
}
}
boolean found=false;
Constructor[] allConstructors = clazz.getDeclaredConstructors();
for (Constructor ctor : allConstructors) {
Class<?>[] pType = ctor.getParameterTypes();
if(pType.length==initArgs.length){
found=true;
for (int i = 0; i < pType.length; i++) {
if(!pType[i].isAssignableFrom(initArgs[i].getClass())){
found=false;
break;
}
}
if(found){
object = ctor.newInstance(initArgs);
break;
}
}
}
if (object instanceof Updatable){
((Updatable)object).setCache(this.cache);
((Updatable)object).setKey(this.key);
}
if(found) {
if (log.isDebugEnabled()) {
log.debugf("Object %s[%s] is created (AtomicObject=%s)",
key.toString(),
clazz.getSimpleName(),
Boolean.toString(object instanceof Updatable));
}
} else {
throw new IllegalArgumentException("Unable to find constructor for " + clazz.toString() + " with " + initArgs);
}
}
//
// HELPERS
//
private void installListener(){
if (listenerState==1)
return;
if (log.isDebugEnabled()) log.debugf("Installing listener %s",key);
cache.addListener(listener);
listenerState = 1;
}
private long nextCallID(){
Random random = new Random(System.nanoTime());
return Thread.currentThread().getId()*random.nextLong();
}
private synchronized Object callObject(Object obj, String method, Object[] args) throws InvocationTargetException, IllegalAccessException {
if (log.isDebugEnabled()) log.debugf("Calling %s()", method.toString());
boolean isFound = false;
Object ret = null;
for (Method m : obj .getClass().getMethods()) { // only public methods (inherited and not)
if (method.equals(m.getName())) {
boolean isAssignable = true;
Class[] argsTypes = m.getParameterTypes();
if(argsTypes.length == args.length){
for(int i=0; i<argsTypes.length; i++){
if( !argsTypes[i].isAssignableFrom(args[i].getClass()) ){
isAssignable = false;
break;
}
}
}else{
isAssignable = false;
}
if(!isAssignable)
continue;
ret = m.invoke(obj, args);
isFound = true;
break;
}
}
if(!isFound)
throw new IllegalStateException("Method "+method+" not found.");
return ret;
}
//
// INNER CLASSES
//
private class AtomicObjectContainerTask implements Runnable{
public Call call;
public AtomicObjectContainerTask(Call c){
call = c;
}
@Override
public void run() {
try {
if (call instanceof CallInvoke) {
if(object != null){
CallInvoke invocation = (CallInvoke) call;
handleInvocation(invocation);
}else if (pending_calls != null) {
pending_calls.add((CallInvoke) call);
}
} else if (call instanceof CallRetrieve) {
if (object != null ) {
if (log.isDebugEnabled()) log.debugf("Sending persistent state");
CallPersist persist = new CallPersist(0,object);
GenericJBossMarshaller marshaller = new GenericJBossMarshaller();
cache.put(key, marshaller.objectToByteBuffer(persist));
}else if (retrieve_call != null && retrieve_call.callID == ((CallRetrieve)call).callID) {
assert pending_calls == null;
pending_calls = new ArrayList<CallInvoke>();
}
} else { // AtomicObjectCallPersist
if (log.isDebugEnabled()) log.debugf("Persistent state received");
if (object == null && pending_calls != null) {
object = ((CallPersist)call).object;
for(CallInvoke invocation : pending_calls){
handleInvocation(invocation);
}
pending_calls = null;
retrieve_future.setReturnValue(null);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}