/*
* Copyright 2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rioproject.impl.associations;
import net.jini.core.discovery.LookupLocator;
import org.rioproject.associations.Association;
import org.rioproject.associations.AssociationDescriptor;
import org.rioproject.associations.AssociationProxy;
import org.rioproject.associations.ServiceSelectionStrategy;
import org.rioproject.impl.associations.strategy.FailOver;
import org.rioproject.impl.util.ThrowableUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
/**
* Provides support for an {@link org.rioproject.associations.AssociationProxy}
*
* @author Dennis Reedy
*/
public class AssociationProxySupport<T> implements AssociationProxy<T> {
private ServiceSelectionStrategy<T> strategy;
private final List<String> proxyMethods = new ArrayList<String>();
Logger logger = LoggerFactory.getLogger(AssociationProxySupport.class);
private AtomicLong invocationCount = new AtomicLong();
private boolean terminated;
/**
* Create an InvocationHandler
*
* @param association The Association to use
*
* @return An InvocationHandler for use with a dynamic JDK proxy. This
* method returns a
* {@link AssociationProxySupport.AssociationInvocationHandler}
* instance
*/
public InvocationHandler getInvocationHandler(final Association<T> association) {
return new AssociationInvocationHandler(association, this);
}
public Association<T> getAssociation() {
return strategy.getAssociation();
}
/**
* Get the {@link ServiceSelectionStrategy}
*
* @return The <tt>ServiceSelectionStrategy</tt>
*/
public ServiceSelectionStrategy<T> getServiceSelectionStrategy() {
return strategy;
}
/**
* Set the strategy for selecting services
*
* @param strategy The {@link ServiceSelectionStrategy}. Must not be null.
*/
public void setServiceSelectionStrategy(ServiceSelectionStrategy<T> strategy) {
this.strategy = strategy;
}
/**
* Notification that an Association has been discovered
*/
public void discovered(Association<T> association, T service) {
logger.trace("Adding service for {}", association.getName());
strategy.serviceAdded(service);
}
/**
* Notification that an Association has changed
*/
public void changed(Association<T> association, T service) {
strategy.serviceRemoved(service);
}
/**
* Notification that an Association is broken
*/
public void broken(Association<T> association, T service) {
strategy.serviceRemoved(service);
}
/**
* Clean up any resources allocated
*/
public void terminate() {
terminated = true;
strategy.terminate();
}
/*
* Invokes the method on on the first available service in
* the collection of associated services. If an invocation to the service
* fails as a result of remote communication failure, the next available
* service will be used.
*
* <p>Attempts to invoke an available service will continue until either the
* invocation succeeds, or there are no more services available
*
* @param a The Association referencing a collection of associated services
* @param method The method to invoke
* @param args Method arguments
*
* @return The result of the method invocation
*
* @throws Throwable the exception to throw from the method invocation on
* the associated service instance.
*/
public Object doInvokeService(Association<T> a, Method method, Object[] args) throws Throwable {
if(terminated)
throw new IllegalStateException("The association proxy for "+formatAssociationService(a)+" "+
"has been terminated, invocations to the service through this " +
"generated proxy are not possible in it's current state. Make sure " +
"all invoking threads are terminated to resolve this issue");
Object result = null;
long stopTime = 0;
while (!terminated) {
T service = getServiceSelectionStrategy().getService();
if(service==null) {
AssociationDescriptor aDesc = a.getAssociationDescriptor();
if(aDesc.getServiceDiscoveryTimeout()>0) {
stopTime = (stopTime==0?
System.currentTimeMillis()+
aDesc.getServiceDiscoveryTimeUnits().toMillis(aDesc.getServiceDiscoveryTimeout()): stopTime);
if(System.currentTimeMillis()<stopTime) {
if(logger.isTraceEnabled()) {
logger.trace("The association proxy for {} is not available. A service discovery timeout of " +
"[{} {}], has been configured, and the computed stop time is: {}, sleep for one " +
"second and re-evaluate",
formatAssociationService(a),
aDesc.getServiceDiscoveryTimeout(),
aDesc.getServiceDiscoveryTimeUnits().name(),
new Date(stopTime));
}
Thread.sleep(1000);
continue;
} else {
String s = formatAssociationService(a);
throw new RemoteException("No services available for associated service " +
s+", "+formatDiscoveryAttributes(a)+". "+
"A timeout of "+aDesc.getServiceDiscoveryTimeout()+" "+
aDesc.getServiceDiscoveryTimeUnits()+" expired. Check network " +
"connections and ensure that the "+s+" service is deployed");
}
} else {
String s = formatAssociationService(a);
throw new RemoteException("No services available for service association " +
s+", "+formatDiscoveryAttributes(a)+". Check network " +
"connections and ensure that the ["+s+"] service is deployed. " +
"You may also want to check the service discovery timeout property, " +
"it is set to ["+aDesc.getServiceDiscoveryTimeout()+"]. Changing this " +
"value will allow Rio to wait the specified amount of time for a service " +
"to become available.");
}
}
try {
result = method.invoke(service, args);
invocationCount.incrementAndGet();
break;
} catch (Throwable t) {
if(!ThrowableUtil.isRetryable(t)) {
logger.warn("Failed to invoke method [{}], remove service [{}]", method.getName(), service.toString(), t);
getServiceSelectionStrategy().serviceRemoved(service);
if(a.removeService(service)!=null) {
logger.warn("Service [{}] removed, have [{}] services", service.toString(), a.getServiceCount());
} else {
logger.warn("Unable to remove service [{}], have [{}] services", service.toString(), a.getServiceCount());
//terminated = true;
}
} else {
if(t instanceof InvocationTargetException) {
t = t.getCause()==null? ((InvocationTargetException)t).getTargetException(): t.getCause();
}
throw t;
}
}
}
return result;
}
private String formatAssociationService(Association<T> a) {
AssociationDescriptor aDesc = a.getAssociationDescriptor();
StringBuilder sb = new StringBuilder();
String[] names = aDesc.getInterfaceNames();
sb.append("[");
for(int i=0; i<names.length; i++) {
if(i>0)
sb.append(", ");
sb.append(names[i]);
}
sb.append("]");
if(!aDesc.getName().equals(AssociationDescriptor.NO_NAME)) {
sb.append(", name [");
sb.append(aDesc.getName());
sb.append("]");
}
sb.append(", ");
sb.append("strategy=[");
String strategy = a.getAssociationDescriptor().getServiceSelectionStrategy();
strategy = (strategy==null? FailOver.class.getName() : strategy);
sb.append(strategy);
sb.append("]");
return sb.toString();
}
private String formatDiscoveryAttributes(Association<T> a) {
StringBuilder sb = new StringBuilder();
sb.append("Using discovery attributes: ");
String[] groups = a.getAssociationDescriptor().getGroups();
sb.append("groups=[");
int i=0;
for(String s : groups) {
if(i>0)
sb.append(", ");
sb.append(s);
i++;
}
sb.append("]");
LookupLocator[] locators = a.getAssociationDescriptor().getLocators();
if(locators!=null) {
sb.append(" ");
sb.append("locators=[");
i=0;
for(LookupLocator l : locators) {
if(i>0)
sb.append(", ");
sb.append(l.toString());
i++;
}
sb.append("] ");
}
return sb.toString();
}
/*
* An InvocationHandler that operates on the service returned by the
* {@link ServiceSelectionStrategy}. If an invocation to the service
* fails as a result of remote communication failure, the next service
* returned by the {@link ServiceSelectionStrategy}will be used.
*
* Attempts to invoke an available service will continue until either the
* invocation succeeds, or there are no more services available
*/
class AssociationInvocationHandler implements InvocationHandler {
Association<T> association;
Object localRef;
AssociationInvocationHandler(Association<T> association, Object localRef) {
this.association = association;
this.localRef = localRef;
}
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
Object result;
if (!isProxyMethod(method)) {
if(logger.isTraceEnabled())
logger.trace("Invoking local method [{}]", method.toString());
result = method.invoke(localRef, args);
} else {
result = doInvokeService(association, method, args);
}
return result;
}
}
/**
* Check if the method is found in the generated proxy
*
* @param method The Method to check
* @return true if the method is local to the proxy
*/
public boolean isProxyMethod(Method method) {
return proxyMethods.contains(method.toString());
}
public void setProxyInterfaces(Class[] classes) {
for (Class clazz : classes) {
Method[] methods = clazz.getMethods();
for (Method m : methods) {
proxyMethods.add(m.toString());
}
}
}
public long getInvocationCount() {
return invocationCount.get();
}
}