/*
* @(#)ArrayNotificationBuffer.java 1.21 03/12/19
*
* Copyright 2004 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package com.sun.jmx.remote.internal;
import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.HashMap;
import java.util.Map;
import javax.management.InstanceNotFoundException;
import javax.management.ListenerNotFoundException;
import javax.management.MalformedObjectNameException;
import javax.management.MBeanServer;
import javax.management.MBeanServerNotification;
import javax.management.Notification;
import javax.management.NotificationBroadcaster;
import javax.management.NotificationFilter;
import javax.management.NotificationFilterSupport;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.QueryEval;
import javax.management.QueryExp;
import javax.management.remote.NotificationResult;
import javax.management.remote.TargetedNotification;
import com.sun.jmx.remote.util.EnvHelp;
import com.sun.jmx.remote.util.ClassLogger;
/** A circular buffer of notifications received from an MBean server. */
public class ArrayNotificationBuffer implements NotificationBuffer {
public static final int DEFAULT_BUFFER_SIZE = 1000;
public static final String BUFFER_SIZE_PROPERTY =
"jmx.remote.x.buffer.size";
private boolean disposed = false;
// FACTORY STUFF, INCLUDING SHARING
private static final
HashMap/*<MBeanServer,ArrayNotificationBuffer>*/ mbsToBuffer =
new HashMap(1);
private final Collection/*<ShareBuffer>*/ sharers = new HashSet(1);
public static synchronized NotificationBuffer
getNotificationBuffer(MBeanServer mbs, Map env) {
//Find out queue size
int defaultQueueSize = DEFAULT_BUFFER_SIZE;
try {
String s = (String)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return System.getProperty(BUFFER_SIZE_PROPERTY);
}
});
if (s != null)
defaultQueueSize = Integer.parseInt(s);
} catch (RuntimeException e) {
logger.warning("ServerNotifForwarder",
"Can't use System property "+
BUFFER_SIZE_PROPERTY+ ": " + e);
logger.debug("ServerNotifForwarder", e);
}
int queueSize = defaultQueueSize;
try {
queueSize = (int)
EnvHelp.getIntegerAttribute(env,BUFFER_SIZE_PROPERTY,
defaultQueueSize,0,
Integer.MAX_VALUE);
} catch (RuntimeException e) {
logger.warning("ServerNotifForwarder",
"Can't determine queuesize (using default): "+
e);
logger.debug("ServerNotifForwarder", e);
}
ArrayNotificationBuffer buf =
(ArrayNotificationBuffer) mbsToBuffer.get(mbs);
if (buf == null) {
buf = new ArrayNotificationBuffer(mbs, queueSize);
mbsToBuffer.put(mbs, buf);
}
return buf.new ShareBuffer(queueSize);
}
public static synchronized void removeNotificationBuffer(MBeanServer mbs){
mbsToBuffer.remove(mbs);
}
synchronized void addSharer(ShareBuffer sharer) {
if (sharer.getSize() > queueSize)
resize(sharer.getSize());
sharers.add(sharer);
}
synchronized void removeSharer(ShareBuffer sharer) {
sharers.remove(sharer);
if (sharers.isEmpty())
dispose();
else {
int max = 0;
for (Iterator it = sharers.iterator(); it.hasNext(); ) {
ShareBuffer buf = (ShareBuffer) it.next();
int bufsize = buf.getSize();
if (bufsize > max)
max = bufsize;
}
if (max < queueSize)
resize(max);
}
}
private void resize(int newSize) {
if (newSize == queueSize)
return;
while (queue.size() > newSize)
dropNotification();
queue.resize(newSize);
queueSize = newSize;
}
private class ShareBuffer implements NotificationBuffer {
ShareBuffer(int size) {
this.size = size;
addSharer(this);
}
public NotificationResult
fetchNotifications(Set/*<ListenerInfo>*/ listeners,
long startSequenceNumber,
long timeout,
int maxNotifications)
throws InterruptedException {
NotificationBuffer buf = ArrayNotificationBuffer.this;
return buf.fetchNotifications(listeners, startSequenceNumber,
timeout, maxNotifications);
}
public void dispose() {
ArrayNotificationBuffer.this.removeSharer(this);
}
int getSize() {
return size;
}
private final int size;
}
// ARRAYNOTIFICATIONBUFFER IMPLEMENTATION
private ArrayNotificationBuffer(MBeanServer mbs, int queueSize) {
if (logger.traceOn())
logger.trace("Constructor", "queueSize=" + queueSize);
if (mbs == null || queueSize < 1)
throw new IllegalArgumentException("Bad args");
this.mBeanServer = mbs;
this.queueSize = queueSize;
this.queue = new ArrayQueue(queueSize);
this.earliestSequenceNumber = System.currentTimeMillis();
this.nextSequenceNumber = this.earliestSequenceNumber;
createListeners();
logger.trace("Constructor", "ends");
}
private synchronized boolean isDisposed() {
return disposed;
}
public void dispose() {
logger.trace("dispose", "starts");
synchronized(this) {
removeNotificationBuffer(mBeanServer);
disposed = true;
//Notify potential waiting fetchNotification call
notifyAll();
}
destroyListeners();
logger.trace("dispose", "ends");
}
/**
* <p>Fetch notifications that match the given listeners.</p>
*
* <p>The operation only considers notifications with a sequence
* number at least <code>startSequenceNumber</code>. It will take
* no longer than <code>timeout</code>, and will return no more
* than <code>maxNotifications</code> different notifications.</p>
*
* <p>If there are no notifications matching the criteria, the
* operation will block until one arrives, subject to the
* timeout.</p>
*
* @param listeners a Set of {@link ListenerInfo} that reflects
* the filters to be applied to notifications. Accesses to this
* Set are synchronized on the Set object. The Set is consulted
* for selected notifications that are present when the method
* starts, and for selected notifications that arrive while it is
* executing. The contents of the Set can be modified, with
* appropriate synchronization, while the method is running.
* @param startSequenceNumber the first sequence number to
* consider.
* @param timeout the maximum time to wait. May be 0 to indicate
* not to wait if there are no notifications.
* @param maxNotifications the maximum number of notifications to
* return. May be 0 to indicate a wait for eligible notifications
* that will return a usable <code>nextSequenceNumber</code>. The
* {@link TargetedNotification} array in the returned {@link
* NotificationResult} may contain more than this number of
* elements but will not contain more than this number of
* different notifications.
*/
public NotificationResult
fetchNotifications(Set/*<ListenerInfo>*/ listeners,
long startSequenceNumber,
long timeout,
int maxNotifications)
throws InterruptedException {
logger.trace("fetchNotifications", "starts");
if (startSequenceNumber < 0 || isDisposed()) {
synchronized(this) {
return new NotificationResult(earliestSequenceNumber(),
nextSequenceNumber(),
new TargetedNotification[0]);
}
}
// Check arg validity
if (listeners == null
|| startSequenceNumber < 0 || timeout < 0
|| maxNotifications < 0) {
logger.trace("fetchNotifications", "Bad args");
throw new IllegalArgumentException("Bad args to fetch");
}
if (logger.debugOn()) {
logger.trace("fetchNotifications",
"listener-length=" + listeners.size() + "; startSeq=" +
startSequenceNumber + "; timeout=" + timeout +
"; max=" + maxNotifications);
}
if (startSequenceNumber > nextSequenceNumber()) {
final String msg = "Start sequence number too big: " +
startSequenceNumber + " > " + nextSequenceNumber();
logger.trace("fetchNotifications", msg);
throw new IllegalArgumentException(msg);
}
/* Determine the end time corresponding to the timeout value.
Caller may legitimately supply Long.MAX_VALUE to indicate no
timeout. In that case the addition will overflow and produce
a negative end time. Set end time to Long.MAX_VALUE in that
case. We assume System.currentTimeMillis() is positive. */
long endTime = System.currentTimeMillis() + timeout;
if (endTime < 0) // overflow
endTime = Long.MAX_VALUE;
if (logger.debugOn())
logger.debug("fetchNotifications", "endTime=" + endTime);
/* We set earliestSeq the first time through the loop. If we
set it here, notifications could be dropped before we
started examining them, so earliestSeq might not correspond
to the earliest notification we examined. */
long earliestSeq = -1;
long nextSeq = startSequenceNumber;
List/*<TargetedNotification>*/ notifs = new ArrayList();
/* On exit from this loop, notifs, earliestSeq, and nextSeq must
all be correct values for the returned NotificationResult. */
while (true) {
logger.debug("fetchNotifications", "main loop starts");
NamedNotification candidate;
/* Get the next available notification regardless of filters,
or wait for one to arrive if there is none. */
synchronized (this) {
/* First time through. The current earliestSequenceNumber
is the first one we could have examined. */
if (earliestSeq < 0) {
earliestSeq = earliestSequenceNumber();
if (logger.debugOn()) {
logger.debug("fetchNotifications",
"earliestSeq=" + earliestSeq);
}
if (nextSeq < earliestSeq) {
nextSeq = earliestSeq;
logger.debug("fetchNotifications",
"nextSeq=earliestSeq");
}
} else
earliestSeq = earliestSequenceNumber();
/* If many notifications have been dropped since the
last time through, nextSeq could now be earlier
than the current earliest. If so, notifications
may have been lost and we return now so the caller
can see this next time it calls. */
if (nextSeq < earliestSeq) {
logger.trace("fetchNotifications",
"nextSeq=" + nextSeq + " < " + "earliestSeq=" +
earliestSeq + " so may have lost notifs");
break;
}
if (nextSeq < nextSequenceNumber()) {
candidate = notificationAt(nextSeq);
if (logger.debugOn()) {
logger.debug("fetchNotifications", "candidate: " +
candidate);
logger.debug("fetchNotifications", "nextSeq now " +
nextSeq);
}
} else {
/* nextSeq is the largest sequence number. If we
already got notifications, return them now.
Otherwise wait for some to arrive, with
timeout. */
if (notifs.size() > 0) {
logger.debug("fetchNotifications",
"no more notifs but have some so don't wait");
break;
}
long toWait = endTime - System.currentTimeMillis();
if (toWait <= 0) {
logger.debug("fetchNotifications", "timeout");
break;
}
/* dispose called */
if (isDisposed()) {
if (logger.debugOn())
logger.debug("fetchNotifications",
"dispose callled, no wait");
return new NotificationResult(earliestSequenceNumber(),
nextSequenceNumber(),
new TargetedNotification[0]);
}
if (logger.debugOn())
logger.debug("fetchNotifications",
"wait(" + toWait + ")");
wait(toWait);
continue;
}
}
/* We have a candidate notification. See if it matches
our filters. We do this outside the synchronized block
so we don't hold up everyone accessing the buffer
(including notification senders) while we evaluate
potentially slow filters. */
ObjectName name = candidate.getObjectName();
Notification notif = candidate.getNotification();
List/*<TargetedNotification>*/ matchedNotifs = new ArrayList();
logger.debug("fetchNotifications",
"applying filters to candidate");
synchronized (listeners) {
for (Iterator it = listeners.iterator(); it.hasNext(); ) {
ListenerInfo li = (ListenerInfo) it.next();
ObjectName pattern = li.getObjectName();
NotificationFilter filter = li.getNotificationFilter();
if (logger.debugOn()) {
logger.debug("fetchNotifications",
"pattern=<" + pattern + ">; filter=" + filter);
}
if (pattern.apply(name)) {
logger.debug("fetchNotifications", "pattern matches");
if (filter == null
|| filter.isNotificationEnabled(notif)) {
logger.debug("fetchNotifications",
"filter matches");
Integer listenerID = li.getListenerID();
TargetedNotification tn =
new TargetedNotification(notif, listenerID);
matchedNotifs.add(tn);
}
}
}
}
if (matchedNotifs.size() > 0) {
/* We only check the max size now, so that our
returned nextSeq is as large as possible. This
prevents the caller from thinking it missed
interesting notifications when in fact we knew they
weren't. */
if (maxNotifications <= 0) {
logger.debug("fetchNotifications",
"reached maxNotifications");
break;
}
--maxNotifications;
if (logger.debugOn())
logger.debug("fetchNotifications", "add: " +
matchedNotifs);
notifs.addAll(matchedNotifs);
}
++nextSeq;
} // end while
/* Construct and return the result. */
int nnotifs = notifs.size();
TargetedNotification[] resultNotifs =
new TargetedNotification[nnotifs];
notifs.toArray(resultNotifs);
NotificationResult nr =
new NotificationResult(earliestSeq, nextSeq, resultNotifs);
if (logger.debugOn())
logger.debug("fetchNotifications", nr.toString());
logger.trace("fetchNotifications", "ends");
return nr;
}
synchronized long earliestSequenceNumber() {
return earliestSequenceNumber;
}
synchronized long nextSequenceNumber() {
return nextSequenceNumber;
}
synchronized void addNotification(NamedNotification notif) {
if (logger.traceOn())
logger.trace("addNotification", notif.toString());
while (queue.size() >= queueSize) {
dropNotification();
if (logger.debugOn()) {
logger.debug("addNotification",
"dropped oldest notif, earliestSeq=" +
earliestSequenceNumber);
}
}
queue.add(notif);
nextSequenceNumber++;
if (logger.debugOn())
logger.debug("addNotification", "nextSeq=" + nextSequenceNumber);
notifyAll();
}
private void dropNotification() {
queue.remove(0);
earliestSequenceNumber++;
}
synchronized NamedNotification notificationAt(long seqNo) {
long index = seqNo - earliestSequenceNumber;
if (index < 0 || index > Integer.MAX_VALUE) {
final String msg = "Bad sequence number: " + seqNo + " (earliest "
+ earliestSequenceNumber + ")";
logger.trace("notificationAt", msg);
throw new IllegalArgumentException(msg);
}
return (NamedNotification) queue.get((int) index);
}
private static class NamedNotification {
NamedNotification(ObjectName sender, Notification notif) {
this.sender = sender;
this.notification = notif;
}
ObjectName getObjectName() {
return sender;
}
Notification getNotification() {
return notification;
}
public String toString() {
return "NamedNotification(" + sender + ", " + notification + ")";
}
private final ObjectName sender;
private final Notification notification;
}
/*
* Add our listener to every NotificationBroadcaster MBean
* currently in the MBean server and to every
* NotificationBroadcaster later created.
*
* It would be really nice if we could just do
* mbs.addNotificationListener(new ObjectName("*:*"), ...);
* Definitely something for the next version of JMX.
*
* There is a nasty race condition that we can't easily solve. We
* first register for MBean-creation notifications so we can add
* listeners to new MBeans, then we query the existing MBeans to
* add listeners to them. The problem is that a new MBean could
* arrive after we register for creations but before the query has
* completed. Then we could see the MBean both in the query and
* in an MBean-creation notification, and we would end up
* registering our listener twice.
*
* To solve this problem, we have separate listener instances for
* the MBeans found by the query and the MBeans found by the
* creation notifications. When we get a creation notification,
* we add the listener for that, then we remove the listener for
* queries if it is there. This means that there's a very small
* window of time when we could get the same notification from
* both listeners. However, for that to happen we would have to
* hit TWO unlikely race conditions: an MBean created during our
* initial query, and a notification from that MBean emitted while
* we were adding the listener from the creation notification.
* And the behaviour if we do hit those two conditions is that one
* or more notifications are duplicated during a very small
* period.
*/
private synchronized void createListeners() {
logger.debug("createListeners", "starts");
try {
AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Object run() throws InstanceNotFoundException {
mBeanServer.addNotificationListener(delegateName,
creationListener,
creationFilter,
null);
return null;
}
});
logger.debug("createListeners", "added creationListener");
} catch (Exception pe) {
final Exception e = extractException(pe);
final String msg = "Can't add listener to MBean server delegate: ";
RuntimeException re = new IllegalArgumentException(msg + e);
EnvHelp.initCause(re, e);
logger.fine("createListeners", msg + e);
logger.debug("createListeners", e);
throw re;
}
Set names;
try {
names = (Set)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return mBeanServer.queryNames(null, broadcasterQuery);
}
});
} catch (RuntimeException e) {
logger.fine("createListeners", "Failed to query names: " + e);
logger.debug("createListeners", e);
throw e;
}
for (Iterator it = names.iterator(); it.hasNext(); ) {
ObjectName name = (ObjectName) it.next();
addBufferListener(name, queryBufferListener);
}
logger.debug("createListeners", "ends");
}
private void addBufferListener(final ObjectName name,
final NotificationListener bufferListener) {
if (logger.debugOn())
logger.debug("addBufferListener", ""+name);
try {
AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Object run() throws InstanceNotFoundException {
mBeanServer.addNotificationListener(name,
bufferListener,
null,
name);
return null;
}
});
} catch (Exception e) {
logger.trace("addBufferListener", extractException(e));
/* This can happen if the MBean was unregistered just
after the query. Or user NotificationBroadcaster might
throw unexpected exception. */
}
}
private synchronized void createdNotification(MBeanServerNotification n) {
if (destroyed) {
logger.trace("createNotification",
"NotificationBuffer was destroyed");
return;
}
final String shouldEqual =
MBeanServerNotification.REGISTRATION_NOTIFICATION;
if (!n.getType().equals(shouldEqual)) {
logger.warning("createNotification", "bad type: " + n.getType());
return;
}
final ObjectName name = n.getMBeanName();
if (logger.debugOn())
logger.debug("createdNotification", "for: " + name);
try {
Boolean instanceOf = (Boolean)
AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Object run() throws InstanceNotFoundException {
return new Boolean(
mBeanServer.isInstanceOf(name,
broadcasterClass));
}
});
if (!instanceOf.booleanValue()) {
logger.debug("createdNotification",
"not a NotificationBroadcaster");
return;
}
} catch (Exception e) {
logger.trace("createdNotification", extractException(e));
/* Could happen if the MBean was immediately unregistered. */
return;
}
addBufferListener(name, creationBufferListener);
try {
AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Object run() throws
InstanceNotFoundException, ListenerNotFoundException {
mBeanServer.removeNotificationListener(
name,
queryBufferListener);
return null;
}
});
logger.trace("createdNotification",
"remove queryBufferListener worked!");
} catch (PrivilegedActionException pe) {
final Exception e = extractException(pe);
if (e instanceof ListenerNotFoundException) {
logger.debug("createdNotification",
"remove queryBufferListener got " +
"ListenerNotFoundException as expected");
// Expected: see comment before createListeners()
} else {
logger.trace("createdNotification", e);
}
}
}
private class BufferListener implements NotificationListener {
public void handleNotification(Notification notif, Object handback) {
if (logger.debugOn()) {
logger.debug("BufferListener.handleNotification",
"notif=" + notif + "; handback=" + handback);
}
ObjectName name = (ObjectName) handback;
addNotification(new NamedNotification(name, notif));
}
}
private final NotificationListener queryBufferListener =
new BufferListener();
private final NotificationListener creationBufferListener =
new BufferListener();
private static class BroadcasterQuery
extends QueryEval implements QueryExp {
public boolean apply(final ObjectName name) {
final MBeanServer mbs = QueryEval.getMBeanServer();
try {
Boolean isBroadcaster = (Boolean)
AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Object run()
throws InstanceNotFoundException {
return new Boolean(
mbs.isInstanceOf(name,
broadcasterClass));
}
});
if (logger.debugOn())
logger.debug("BroadcasterQuery", name + " -> " +
isBroadcaster);
return isBroadcaster.booleanValue();
} catch (PrivilegedActionException pe) {
logger.debug("BroadcasterQuery", extractException(pe));
return false;
}
}
}
private static final QueryExp broadcasterQuery = new BroadcasterQuery();
private static final NotificationFilter creationFilter;
static {
NotificationFilterSupport nfs = new NotificationFilterSupport();
nfs.enableType(MBeanServerNotification.REGISTRATION_NOTIFICATION);
creationFilter = nfs;
}
private final NotificationListener creationListener =
new NotificationListener() {
public void handleNotification(Notification notif,
Object handback) {
logger.debug("creationListener", "handleNotification called");
createdNotification((MBeanServerNotification) notif);
}
};
private synchronized void destroyListeners() {
logger.debug("destroyListeners", "starts");
destroyed = true;
Set names = (Set)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return mBeanServer.queryNames(null, broadcasterQuery);
}
});
for (Iterator it = names.iterator(); it.hasNext(); ) {
final ObjectName name = (ObjectName) it.next();
if (logger.debugOn())
logger.debug("destroyListeners",
"remove listener from " + name);
// remove creationBufferListener or queryBufferListener
for (int i = 0; i < 2; i++) {
final boolean creation = (i == 0);
final NotificationListener listener =
creation ? creationBufferListener : queryBufferListener;
final String what =
(creation ?
"creationBufferListener" :
"queryBufferListener");
try {
AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Object run()
throws
InstanceNotFoundException,
ListenerNotFoundException {
mBeanServer.removeNotificationListener(
name,
listener);
return null;
}
});
if (logger.debugOn()) {
logger.debug("destroyListeners", "removed " + what);
}
} catch (PrivilegedActionException pe) {
final Exception e = extractException(pe);
if (e instanceof ListenerNotFoundException) {
if (logger.debugOn()) {
logger.debug("destroyListeners",
"ListenerNotFoundException for " + what +
" (normal)");
}
} else {
logger.trace("destroyListeners", e);
}
}
}
}
logger.debug("destroyListeners", "ends");
}
/**
* Iterate until we extract the real exception
* from a stack of PrivilegedActionExceptions.
*/
private static Exception extractException(Exception e) {
while (e instanceof PrivilegedActionException) {
e = ((PrivilegedActionException)e).getException();
}
return e;
}
private static final ClassLogger logger =
new ClassLogger("javax.management.remote.misc",
"ArrayNotificationBuffer");
private static final ObjectName delegateName;
static {
try {
delegateName =
ObjectName.getInstance("JMImplementation:" +
"type=MBeanServerDelegate");
} catch (MalformedObjectNameException e) {
RuntimeException re =
new RuntimeException("Can't create delegate name: " + e);
EnvHelp.initCause(re, e);
logger.error("<init>", "Can't create delegate name: " + e);
logger.debug("<init>",e);
throw re;
}
}
private final MBeanServer mBeanServer;
private final ArrayQueue queue;
private int queueSize;
private long earliestSequenceNumber;
private long nextSequenceNumber;
private boolean destroyed;
static final String broadcasterClass =
NotificationBroadcaster.class.getName();
}
|