/*
* Copyright to 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.event;
import com.sun.jini.config.Config;
import com.sun.jini.proxy.BasicProxyTrustVerifier;
import net.jini.config.Configuration;
import net.jini.config.EmptyConfiguration;
import net.jini.core.entry.Entry;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEvent;
import net.jini.core.event.UnknownEventException;
import net.jini.core.lease.Lease;
import net.jini.core.lookup.ServiceID;
import net.jini.core.lookup.ServiceItem;
import net.jini.export.Exporter;
import net.jini.security.BasicProxyPreparer;
import net.jini.security.ProxyPreparer;
import net.jini.security.TrustVerifier;
import net.jini.security.proxytrust.ServerProxyTrust;
import org.rioproject.impl.config.ExporterConfig;
import org.rioproject.event.*;
import org.rioproject.impl.util.ThrowableUtil;
import org.rioproject.impl.util.TimeConstants;
import org.rioproject.util.TimeUtil;
import org.rioproject.impl.watch.StopWatch;
import org.rioproject.impl.watch.Watch;
import org.rioproject.impl.watch.WatchDataSourceRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.rmi.MarshalledObject;
import java.rmi.RemoteException;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* The BasicEventConsumer is a helper class that manages the
* discovery of {@link org.rioproject.event.EventProducer} instances that provide support for
* user defined events. The BasicEventConsumer is to be used as a
* local (within a JVM) utility, managing the discovery, event registration and
* leasing of {@link net.jini.core.event.EventRegistration} objects on behalf of
* client(s). In this manner, clients wishing to easily register (subscribe) for the
* notification of an event in the distributed system need not be overly
* concerned with the underlying semantics and management of event
* registrations, leases and events.
*
* @author Dennis Reedy
*/
public class BasicEventConsumer implements EventConsumer, ServerProxyTrust {
/** The remote ref (e.g. stub or dynamic proxy) for the BasicEventConsumer */
private EventConsumer eventConsumer;
/** The Exporter for the BasicEventConsumer */
private Exporter exporter;
/** The proxyPreparer for EventRegistration */
private ProxyPreparer eventLeasePreparer;
protected final List<RemoteServiceEventListener> eventSubscribers =
Collections.synchronizedList(new ArrayList<RemoteServiceEventListener>());
protected final Hashtable<ServiceID, EventLeaseManager> leaseTable = new Hashtable<ServiceID, EventLeaseManager>();
protected final Map<Long, EventRegistration> eventRegistrationTable = new Hashtable<Long, EventRegistration>();
protected EventDescriptor edTemplate;
protected int received = 0;
protected long sktime, ektime;
protected MarshalledObject handback = null;
/** Default Lease duration is 5 minutes */
protected static final long DEFAULT_LEASE_DURATION = TimeConstants.FIVE_MINUTES;
protected long leaseDuration = DEFAULT_LEASE_DURATION;
protected StopWatch responseWatch = null;
protected WatchDataSourceRegistry watchRegistry = null;
/** Number of retries to attempt to connect to an EventProducer */
private int connectRetries;
private static final int DEFAULT_CONNECT_RETRY_COUNT = 3;
/** How long to wait between retries */
private long retryWait;
private static final long DEFAULT_RETRY_WAIT = TimeConstants.ONE_SECOND;
public static final String RESPONSE_WATCH = "Response Time";
static final String COMPONENT = "org.rioproject.event";
/** The Logger */
static Logger logger = LoggerFactory.getLogger(BasicEventConsumer.class);
private final Configuration config;
private final ExecutorService service;
/**
* Create a BasicEventConsumer with an EventDescriptor
*
* @param edTemplate The EventDescriptor template
*
* @throws Exception If the BasicEventConsumer cannot be created
*/
public BasicEventConsumer(final EventDescriptor edTemplate) throws Exception {
this(edTemplate, null, null, null);
}
/**
* Create a BasicEventConsumer with a RemoteServiceEventListener
*
* @param listener The RemoteServiceEventListener
*
* @throws Exception If the BasicEventConsumer cannot be created
*/
public BasicEventConsumer(final RemoteServiceEventListener listener) throws Exception {
this(null, listener, null, null);
}
/**
* Create a BasicEventConsumer with an EventDescriptor and a
* RemoteServiceEventListener
*
* @param edTemplate The EventDescriptor template
* @param listener The RemoteServiceEventListener
*
* @throws Exception If the BasicEventConsumer cannot be created
*/
public BasicEventConsumer(final EventDescriptor edTemplate, final RemoteServiceEventListener listener)
throws Exception {
this(edTemplate, listener, null, null);
}
/**
* Create a BasicEventConsumer with an EventDescriptor and a
* RemoteServiceEventListener
*
* @param edTemplate The EventDescriptor template
* @param listener The RemoteServiceEventListener
* @param config Configuration object
*
* @throws Exception If the BasicEventConsumer cannot be created
*/
public BasicEventConsumer(final EventDescriptor edTemplate,
final RemoteServiceEventListener listener,
final Configuration config) throws Exception {
this(edTemplate, listener, null, config);
}
/**
* Create a BasicEventConsumer with an EventDescriptor, a
* RemoteServiceEventListener, and a MarshalledObject to be used as a
* handback
*
* @param edTemplate The EventDescriptor template
* @param listener The RemoteServiceEventListener
* @param handback The MarshalledObject to be used as a handback
* @param config Configuration object
*
* @throws Exception If the BasicEventConsumer cannot be created
*/
public BasicEventConsumer(final EventDescriptor edTemplate,
final RemoteServiceEventListener listener,
final MarshalledObject handback,
final Configuration config) throws Exception {
ProxyPreparer basicLeasePreparer = new BasicProxyPreparer();
if(config == null)
this.config = EmptyConfiguration.INSTANCE;
else
this.config = config;
leaseDuration = Config.getLongEntry(this.config,
COMPONENT,
"eventLeaseDuration",
DEFAULT_LEASE_DURATION, //default
1000*60, // min
Long.MAX_VALUE); // max
exporter = ExporterConfig.getExporter(this.config, COMPONENT, "eventConsumerExporter");
eventLeasePreparer = (ProxyPreparer)this.config.getEntry(COMPONENT,
"eventLeasePreparer",
ProxyPreparer.class,
basicLeasePreparer);
connectRetries = Config.getIntEntry(this.config,
COMPONENT,
"connectRetries",
DEFAULT_CONNECT_RETRY_COUNT, // default
0, // min
5); // max
retryWait = Config.getLongEntry(this.config,
COMPONENT,
"retryWait",
DEFAULT_RETRY_WAIT, // default
0, // min
Long.MAX_VALUE); // max
eventConsumer = (EventConsumer)exporter.export(this);
//refQueue = new ReferenceQueue();
this.edTemplate = edTemplate;
if(edTemplate!=null)
logger.trace("Create BasicEventConsumer for EventDescriptor : {}", edTemplate.toString());
this.handback = handback;
if(listener != null)
eventSubscribers.add(listener);
service = Executors.newCachedThreadPool();
}
/**
* The terminate method will de-register for event notifications across all
* discovered EventProducer instances. Invocation of this method will result
* in the cancellation of any leases involved with event registration and
* the removal from the event notification pool. This method will also
* unexport the EventConsumer, removing it from the RMI runtime. This method
* will also destroy the response time watch if it was created
*/
public void terminate() {
if(service!=null)
service.shutdownNow();
/* Deregister all listeners */
RemoteServiceEventListener[] listeners = getListeners();
for (RemoteServiceEventListener listener : listeners) {
try {
deregister(listener);
} catch (Exception e) {
logger.warn("Deregistering EventConsumer", e);
}
}
/* Destroy the watch */
destroyWatch();
/* If we exported, unexport, use force=true */
if(eventConsumer != null) {
try {
exporter.unexport(true);
} catch(IllegalStateException e) {
logger.warn("EventConsumer not unexported", e);
}
}
}
/**
* Register a RemoteServiceEventListener to this EventConsumer. If there are
* no RemoteServiceEventListener instances registered events will be dropped
* by this BasicEventConsumer. Each registered RemoteServiceEventListener
* will be notified for each event received.
*
* @param listener The RemoteServiceEventListener
* @return true if the RemoteServiceEventListener has been added
*/
public boolean register(final RemoteServiceEventListener listener) {
return (register(listener, null));
}
/**
* Register a RemoteServiceEventListener to this EventConsumer with a
* MarshalledObject handback object. If there are no
* RemoteServiceEventListener instances registered events will be dropped by
* this BasicEventConsumer.
*
* Each registered RemoteServiceEventListener will be notified for each
* event received
*
* @param listener The RemoteServiceEventListener
* @param handback The MarshalledObject to be used as a handback
* @return true if the RemoteServiceEventListener has been added
*/
public boolean register(final RemoteServiceEventListener listener, final MarshalledObject handback) {
this.handback = handback;
boolean added = eventSubscribers.add(listener);
return (added);
}
/**
* De-registers a registered RemoteServiceEventListener from this
* EventConsumer
*
* @param listener The RemoteServiceEventListener
* @return true if the RemoteServiceEventListener was removed
*/
public boolean deregister(final RemoteServiceEventListener listener) {
return(removeListener(listener));
}
/**
* Create a response time watch for this EventConsumer, which will track the
* response time for event consumers, measured by how long the response time
* takes
*
* @param watchRegistry The WatchDataSourceRegistry to register the watch
*
* @return The Watch
*/
public Watch createWatch(final WatchDataSourceRegistry watchRegistry) {
if(watchRegistry == null)
throw new IllegalArgumentException("watchRegistry is null");
if(responseWatch!=null)
return responseWatch;
responseWatch = new StopWatch(RESPONSE_WATCH, config);
this.watchRegistry = watchRegistry;
watchRegistry.register(responseWatch);
return responseWatch;
}
/**
* Destroys the response time watch. Once this method is called the response
* time watch will be rendered useless
*/
public void destroyWatch() {
if(watchRegistry != null) {
watchRegistry.deregister(responseWatch);
}
if(responseWatch != null) {
try {
responseWatch.getWatchDataSource().clear();
responseWatch.getWatchDataSource().close();
} catch(RemoteException e) {
logger.warn("RemoteException Destroying Watch", e);
}
}
responseWatch = null;
}
/**
* Get the response time watch for this EventConsumer
*
* @return The response time watch for this EventConsumer
*/
public Watch getWatch() {
return (responseWatch);
}
/**
* Given a ServiceItem this method checks to see if the ServiceItem contains
* a proxy of type EventProducer, performs event registration and ensures the
* Lease contained in the event registration is managed by a
* LeaseRenewalManager
*
* @param item The ServiceItem
* @return The {@link net.jini.core.event.EventRegistration}, or null if the
* service is not an {@link org.rioproject.event.EventProducer} or the {@link EventDescriptor}
* template the BasicEventConsumer was started with cannot be matched
*
* @throws IllegalArgumentException if the item parameter is null
*/
public EventRegistration register(final ServiceItem item) {
if(item==null)
throw new IllegalArgumentException("item is null");
if(edTemplate==null)
throw new IllegalStateException("An EventDescriptor template has not " +
"been set when creating the " +
"BasicEventConsumer, this utility " +
"cannot determine the EventDescriptor " +
"to register for. Check your use " +
"of the BasicEventConsumer and " +
"construct it with an EventDescriptor");
if(!(item.service instanceof EventProducer)) {
logger.trace("Service is not an EventProducer");
return (null);
}
EventDescriptor eDesc = getDescriptor(item.attributeSets, edTemplate);
if(eDesc == null) {
logger.debug("Cannot get EventDescriptor match");
logger.trace("ServiceItem.service ClassLoader {}, EventDescriptor {}, EventDescriptor ClassLoader {}",
item.service.getClass().getClassLoader().toString(),
edTemplate.toString(),
edTemplate.getClass().getClassLoader().toString());
return (null);
}
EventProducer producer = (EventProducer)item.service;
return register(producer, eDesc, item.serviceID);
}
/**
* Register for notification of event from an {@link EventProducer}.
*
* @param eventProducer The EventProducer, must not be null
* @param eventDesc The EventDescriptor, must not be null
* @param serviceID The serviceID of the EventProducer to register
* @return The {@link net.jini.core.event.EventRegistration}, or null if the
* service is not an {@link EventProducer} or the {@link EventDescriptor}
* template the BasicEventConsumer was started with cannot be matched
*
* @throws IllegalArgumentException if any og the parameters are null
*/
public EventRegistration register(final EventProducer eventProducer,
final EventDescriptor eventDesc,
final ServiceID serviceID) {
if(eventProducer==null)
throw new IllegalArgumentException("eventProducer is null");
if(eventDesc==null)
throw new IllegalArgumentException("eventDesc is null");
if(serviceID==null)
throw new IllegalArgumentException("serviceID is null");
if(leaseTable.containsKey(serviceID)) {
logger.trace("Already registered to EventProducer");
return eventRegistrationTable.get(eventDesc.eventID);
}
EventRegistration eReg = null;
Lease lease = connect(eventProducer, eventDesc);
if(lease!=null) {
EventLeaseManager eventLeaseManager = new EventLeaseManager(eventProducer, lease, eventDesc);
leaseTable.put(serviceID, eventLeaseManager);
service.submit(eventLeaseManager);
eReg = eventRegistrationTable.get(eventDesc.eventID);
}
return (eReg);
}
/**
* Connect to the EventProducer and get a Lease
*
* @param producer The EventProducer
* @param eDesc The EventDescriptor
*
* @return The Lease for the @link net.jini.core.event.EventRegistration}
*/
Lease connect(final EventProducer producer, final EventDescriptor eDesc) {
Lease lease = null;
for(int i=0; i<connectRetries; i++) {
try {
EventRegistration eReg = producer.register(eDesc, eventConsumer, handback, leaseDuration);
eventRegistrationTable.put(eDesc.eventID, eReg);
lease = (Lease)eventLeasePreparer.prepareProxy(eReg.getLease());
long leaseTime = lease.getExpiration() - System.currentTimeMillis();
if(leaseTime>0) {
logger.trace("Event Registration Lease acquired and prepared. Duration={} ({} seconds)",
leaseTime, (leaseTime / 1000));
break;
} else {
logger.warn("Invalid Lease time [{}], retry count [{}]", leaseTime, i);
try {
lease.cancel();
} catch(Exception e ) {
logger.trace("Cancelling Lease with invalid lease time", e);
}
if(retryWait>0) {
try {
Thread.sleep(retryWait);
} catch(InterruptedException ie) {
/* should not happen */
}
}
}
} catch(Exception e) {
/* Determine if we should even try to reconnect */
if(!ThrowableUtil.isRetryable(e)) {
logger.warn("Unrecoverable Exception getting EventRegistration for {}, {}: {}",
eDesc.toString(), e.getClass().getName(), e.getMessage());
logger.trace("Unrecoverable Exception getting EventRegistration for {}", eDesc.toString(), e);
break;
} else {
if(retryWait>0) {
try {
Thread.sleep(retryWait);
} catch(InterruptedException ignore) {
/* ignore */
}
}
}
}
}
return (lease);
}
/**
* Returns the source object of an EventRegistration given an event ID
*
* @param eventID The eventID
* @return If found, returns the source object associated with the
* eventID, otherwise returns null
*/
public Object getEventRegistrationSource(final long eventID) {
EventRegistration eReg = eventRegistrationTable.get(eventID);
Object source = null;
if(eReg!=null)
source = eReg.getSource();
return (source);
}
/**
* This method handles the cleanup for removing a registration from a
* EventProducer instance
*
* @param serviceID The serviceID of the EventProducer to deregister
*
* @throws IllegalArgumentException if the serviceID parameter is null
*/
public void deregister(final ServiceID serviceID) {
deregister(serviceID, true);
}
/**
* This method handles the cleanup for removing a registration from a
* EventProducer instance
*
* @param serviceID The serviceID of the EventProducer to deregister
* @param disconnect Whether to explicitly cancel the lease with the
* EventProducer, or just let the least time out
*
* @throws IllegalArgumentException if the serviceID parameter is null
*/
public void deregister(final ServiceID serviceID, final boolean disconnect) {
if(serviceID==null)
throw new IllegalArgumentException("serviceID is null");
EventLeaseManager elm = leaseTable.remove(serviceID);
if(elm != null) {
try {
elm.drop(disconnect);
} catch(Exception e) {
/* ignore */
}
}
}
/**
* Remote event notification. This method is called by an EventProducer to
* notify the RemoteEventListener of a state change through a RemoteEvent
*
* @throws UnknownEventException If the RemoteEvent cannot be downcast to a
* RemoteServiceEvent
*/
public void notify(final RemoteEvent rEvent)throws UnknownEventException {
if(!(rEvent instanceof RemoteServiceEvent))
throw new UnknownEventException("Unsupported event class");
RemoteServiceEvent rsEvent = (RemoteServiceEvent)rEvent;
service.submit(new ClientNotification(rsEvent));
}
/**
* Returns a {@link net.jini.security.TrustVerifier} which can be used to verify
* that a given proxy to this event consumer can be trusted
*/
public TrustVerifier getProxyVerifier() {
return (new BasicProxyTrustVerifier(eventConsumer));
}
/**
* Method to return a matching EventDescriptor from a service's set of
* attributes <br>
*
* @param attrs Array of attributes
* @param template The EventDescriptor template
*
* @return An EventDescriptor if found or null if no match
*/
protected EventDescriptor getDescriptor(final Entry[] attrs, final EventDescriptor template) {
EventDescriptor matchedDescriptor = null;
/* Traverse the attribute collection, for each EventDescriptor
* attribute, first check if the attribute has a "matches" method. If it
* does invoke the method to determine 'match-ability' */
for (Entry attr : attrs) {
if (attr instanceof EventDescriptor) {
EventDescriptor ed = (EventDescriptor) attr;
try {
ed.getClass().getMethod("matches", EventDescriptor.class);
} catch (NoSuchMethodException e) {
logger.warn("Rio version mismatch, {} missing matches method", EventDescriptor.class.getName(), e);
return (null);
}
if (ed.matches(template)) {
matchedDescriptor = ed;
break;
}
}
}
logger.trace("Matched [{}] ? {}", template.toString(), (matchedDescriptor==null?"NO":"YES"));
return (matchedDescriptor);
}
/**
* Convenience method to print statistics for every thousand events sent.
* This method will only print result if the Logger has it's Level set to
* FINEST
*/
protected void printStats() {
if(!logger.isTraceEnabled())
return;
if(received == 0)
sktime = System.currentTimeMillis();
int m = received % 1000;
if(m == 0 && received > 0) {
ektime = System.currentTimeMillis();
float tmp = (ektime - sktime) / 1000.f;
logger.trace(String.format("Recvd [%d], \t[1000/%f]\t[%f/Second]", received, tmp, 1000.f/tmp));
sktime = System.currentTimeMillis();
}
}
private boolean removeListener(final RemoteServiceEventListener l) {
boolean removed = eventSubscribers.remove(l);
if(removed) {
if(eventSubscribers.isEmpty()) {
List<ServiceID> keyList = new ArrayList<ServiceID>();
for(Enumeration<ServiceID> e = leaseTable.keys(); e.hasMoreElements();) {
keyList.add(e.nextElement());
}
for (ServiceID sid : keyList) {
deregister(sid);
}
}
}
return (removed);
}
/**
* Override finalize to ensure we unexport ourselves. This is needed if
* @throws Throwable
*/
protected void finalize() throws Throwable {
terminate();
super.finalize();
}
/**
* Get all registered RemoteServiceEventListeners
*
* @return An array of all registered RemoteServiceEventListener objects
*/
protected RemoteServiceEventListener[] getListeners() {
return eventSubscribers.toArray(new RemoteServiceEventListener[eventSubscribers.size()]);
}
/**
* Notify client asynchronously
*/
class ClientNotification implements Runnable {
RemoteServiceEvent rsEvent;
ClientNotification(RemoteServiceEvent rsEvent) {
this.rsEvent = rsEvent;
}
public void run() {
long startTime = System.currentTimeMillis();
logger.trace("Received RemoteServiceEvent [{}], Number of subscribers : {}",
rsEvent.getClass().getName(), eventSubscribers.size());
RemoteServiceEventListener[] listeners = getListeners();
for (RemoteServiceEventListener listener : listeners) {
logger.trace("Notify subscriber [{}]", listener.getClass().getName());
listener.notify(rsEvent);
received++;
printStats();
}
if(responseWatch != null) {
long now = System.currentTimeMillis();
long elapsed = now - startTime;
responseWatch.setElapsedTime(elapsed, now);
}
}
}
/**
* Use customized Lease renewal to manage leases to EventRegistration
* leases. This is needed because leases constructed to EventProducer
* instances may be shorter then 5 minutes. If we use LeaseRenewalManager
* and the leases are shorter then 5 minutes, then after 5 minutes the lease
* is allowed to expire.
*/
class EventLeaseManager implements Runnable {
private final long leaseTime;
private boolean keepAlive = true;
private final EventProducer producer;
private Lease lease;
private final EventDescriptor eDesc;
EventLeaseManager(final EventProducer producer, final Lease lease, final EventDescriptor eDesc) {
this.producer = producer;
this.lease = lease;
this.leaseTime = lease.getExpiration() - System.currentTimeMillis();
this.eDesc = eDesc;
logger.trace("Created EventLeaseManager, EventDescriptor={}, Lease Time={} seconds",
eDesc.toString(), (leaseTime / 1000));
}
void drop(boolean disconnect) {
keepAlive = false;
if(disconnect) {
try {
lease.cancel();
} catch(Exception ignore) {
/* ignore */
}
}
}
public void run() {
while (keepAlive) {
try {
long waitTillRenew = TimeUtil.computeLeaseRenewalTime(leaseTime);
logger.trace("Wait to renew {} lease for {}", eDesc.toString(), TimeUtil.format(waitTillRenew));
Thread.sleep(waitTillRenew);
} catch(InterruptedException ignore) {
/* ignore */
}
if(lease != null) {
try {
logger.trace("Renew lease for {}", eDesc.toString());
lease.renew(leaseTime);
} catch(Exception e) {
/* Determine if we should even try to reconnect */
if(!ThrowableUtil.isRetryable(e)) {
keepAlive = false;
logger.warn("EventLeaseManager, Unrecoverable Exception renewing Lease, dropping Lease renewal for {}",
eDesc.toString());
logger.trace("Unrecoverable Exception renewing Lease for {}", eDesc.toString(), e);
}
if(keepAlive) {
logger.trace("Attempt to reconnect to producer {} for event {}",
producer.toString(), eDesc.toString());
lease = connect(producer, eDesc);
if(lease==null) {
logger.warn("EventLeaseManager, Unable to obtain Lease, dropping Lease renewal for {}",
eDesc.toString());
keepAlive = false;
} else {
logger.trace("Reconnect succeeded to producer {} for event {}",
producer.toString(), eDesc.toString());
}
}
}
}
}
logger.trace("EventLeaseManager {} leaving", eDesc.toString());
}
}
}