jdk/test/javax/management/remote/mandatory/notif/NotificationBufferDeadlockTest.java
author xdono
Thu, 02 Oct 2008 19:58:32 -0700
changeset 1247 b4c26443dee5
parent 1004 5ba8217eb504
child 4156 acaa49a2768a
permissions -rw-r--r--
6754988: Update copyright year Summary: Update for files that have been modified starting July 2008 Reviewed-by: ohair, tbell

/*
 * Copyright 2005-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

/*
 * @test
 * @bug 6239400
 * @summary Tests NotificationBuffer doesn't hold locks when adding listeners.
 * @author Eamonn McManus
 * @run clean NotificationBufferDeadlockTest
 * @run build NotificationBufferDeadlockTest
 * @run main NotificationBufferDeadlockTest
 */

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import javax.management.*;
import javax.management.remote.*;

/*
 * Regression test for a rare but not unheard-of deadlock condition in
 * the notification buffer support for connector servers.
 * See bug 6239400 for the description of the bug and the example that
 * showed it up.
 *
 * Here we test that, when the connector server adds its listener to an
 * MBean, it is not holding a lock that would prevent another thread from
 * emitting a notification (from that MBean or another one).  This is
 * important, because we don't know how user MBeans might implement
 * NotificationBroadcaster.addNotificationListener, and in particular we
 * can't be sure that the method is well-behaved and can never do a
 * blocking operation, such as attempting to acquire a lock that is also
 * acquired when notifications are emitted.
 *
 * The test creates a special MBean whose addNotificationListener method
 * does the standard addNotificationListener logic inherited
 * from NotificationBroadcasterSupport, then
 * creates another thread that emits a notification from the same MBean.
 * The addNotificationListener method waits for this thread to complete.
 * If the notification buffer logic is incorrect, then emitting the
 * notification will attempt to acquire the lock on the buffer, but that
 * lock is being held by the thread that called addNotificationListener,
 * so there will be deadlock.
 *
 * We use this DeadlockMBean several times.  First, we create one and then
 * add a remote listener to it.  The first time you add a remote listener
 * through a connector server, the connector server adds its own listener
 * to all NotificationBroadcaster MBeans.  If it holds a lock while doing
 * this, we will see deadlock.
 *
 * Then we create a second DeadlockMBean.  When a new MBean is created that
 * is a NotificationBroadcaster, the connector server adds its listener to
 * that MBean too.  Again if it holds a lock while doing this, we will see
 * deadlock.
 *
 * Finally, we do some magic with MBeanServerForwarders so that while
 * queryNames is running (to find MBeans to which listeners must be added)
 * we will create new MBeans.  This tests that this tricky situation is
 * handled correctly.  It also tests the queryNames that is run when the
 * notification buffer is being destroyed (to remove the listeners).
 *
 * We cause all of our test MBeans to emit exactly one notification and
 * check that we have received exactly one notification from each MBean.
 * If the logic for adding the notification buffer's listener is incorrect
 * we could remove zero or two notifications from an MBean.
 */
import javax.management.remote.rmi.RMIConnectorServer;
public class NotificationBufferDeadlockTest {
    public static void main(String[] args) throws Exception {
        System.out.println("Check no deadlock if notif sent while initial " +
                           "remote listeners being added");
        final String[] protos = {"rmi", "iiop", "jmxmp"};
        for (String p : protos) {
            try {
                test(p);
            } catch (Exception e) {
                System.out.println("TEST FAILED: GOT EXCEPTION:");
                e.printStackTrace(System.out);
                failure = e.toString();
            }
        }
        if (failure == null)
            return;
        else
            throw new Exception("TEST FAILED: " + failure);
    }

    private static void test(String proto) throws Exception {
        test(proto, false);
        test(proto, true);
    }

    private static void test(String proto, boolean eventService) throws Exception {
        System.out.println("Testing protocol " + proto + " with" +
                (eventService ? "" : "out") + " event service");
        MBeanServer mbs = MBeanServerFactory.newMBeanServer();
        ObjectName testName = newName();
        DeadlockTest test = new DeadlockTest();
        mbs.registerMBean(test, testName);
        JMXServiceURL url = new JMXServiceURL("service:jmx:" + proto + ":///");
        JMXConnectorServer cs;
        try {
            Map<String, String> env = Collections.singletonMap(
                    RMIConnectorServer.DELEGATE_TO_EVENT_SERVICE,
                    Boolean.toString(eventService));
            cs =
                JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
        } catch (MalformedURLException e) {
            System.out.println("...protocol not supported, ignoring");
            return;
        }

        MBeanServerForwarder createDuringQueryForwarder = (MBeanServerForwarder)
            Proxy.newProxyInstance(new Object() {}.getClass().getClassLoader(),
                                   new Class[] {MBeanServerForwarder.class},
                                   new CreateDuringQueryInvocationHandler());
        cs.setMBeanServerForwarder(createDuringQueryForwarder);
        cs.start();
        JMXServiceURL addr = cs.getAddress();
        JMXConnector cc = JMXConnectorFactory.connect(addr);
        MBeanServerConnection mbsc = cc.getMBeanServerConnection();
        try {
            String fail = test(mbsc, testName);
            if (fail != null)
                System.out.println("FAILED: " + fail);
            failure = fail;
        } finally {
            cc.close();
            cs.stop();
        }
    }

    private static String test(MBeanServerConnection mbsc,
                               ObjectName testName) throws Exception {

        NotificationListener dummyListener = new NotificationListener() {
            public void handleNotification(Notification n, Object h) {
            }
        };
        thisFailure = null;
        mbsc.addNotificationListener(testName, dummyListener, null, null);
        if (thisFailure != null)
            return thisFailure;
        ObjectName newName = newName();
        mbsc.createMBean(DeadlockTest.class.getName(), newName);
        if (thisFailure != null)
            return thisFailure;
        Set<ObjectName> names =
            mbsc.queryNames(new ObjectName("d:type=DeadlockTest,*"), null);
        System.out.printf("...found %d test MBeans\n", names.size());

        sources.clear();
        countListener = new MyListener(names.size());

        for (ObjectName name : names)
            mbsc.addNotificationListener(name, countListener, null, null);
        if (thisFailure != null)
            return thisFailure;
        for (ObjectName name : names)
            mbsc.invoke(name, "send", null, null);

        if (!countListener.waiting(MAX_WAITING_TIME)) {
            return "did not get " + names.size() + " notifs as expected\n";
        }

        if (!sources.containsAll(names))
            return "missing names: " + sources;
        return thisFailure;
    }

    public static interface DeadlockTestMBean {
        public void send();
    }

    public static class DeadlockTest extends NotificationBroadcasterSupport
            implements DeadlockTestMBean {
        @Override
        public void addNotificationListener(NotificationListener listener,
                                            NotificationFilter filter,
                                            Object handback) {
            super.addNotificationListener(listener, filter, handback);
            Thread t = new Thread() {
                @Override
                public void run() {
                    Notification n =
                        new Notification("type", DeadlockTest.this, 0L);
                    DeadlockTest.this.sendNotification(n);
                }
            };
            t.start();
            try {
                t.join(5000L);
            } catch (Exception e) {
                thisFailure = "Join exception: " + e;
            }
            if (t.isAlive())
                thisFailure = "Deadlock detected";
        }

        public void send() {
            sendNotification(new Notification(TESTING_TYPE, DeadlockTest.this, 1L));
        }
    }

    private static class CreateDuringQueryInvocationHandler
            implements InvocationHandler {
        public Object invoke(Object proxy, Method m, Object[] args)
                throws Throwable {
            if (m.getName().equals("setMBeanServer")) {
                mbs = (MBeanServer) args[0];
                return null;
            }
            createMBeanIfQuery(m);
            Object ret = m.invoke(mbs, args);
            createMBeanIfQuery(m);
            return ret;
        }

        private void createMBeanIfQuery(Method m) throws InterruptedException {
            if (m.getName().equals("queryNames")) {
                Thread t = new Thread() {
                    public void run() {
                        try {
                            mbs.createMBean(DeadlockTest.class.getName(),
                                            newName());
                        } catch (Exception e) {
                            e.printStackTrace();
                            thisFailure = e.toString();
                        }
                    }
                };
                t.start();
                t.join(5000);
                if (t.isAlive())
                    failure = "Query deadlock detected";
            }
        }

        private MBeanServer mbs;
    }

    private static synchronized ObjectName newName() {
        try {
            return new ObjectName("d:type=DeadlockTest,instance=" +
                                  ++nextNameIndex);
        } catch (MalformedObjectNameException e) {
            throw new IllegalArgumentException("bad ObjectName", e);
        }
    }

    private static class MyListener implements NotificationListener {
        public MyListener(int waitNB) {
            this.waitNB= waitNB;
        }

        public void handleNotification(Notification n, Object h) {
            System.out.println("MyListener got: "+n.getSource()+" "+n.getType());

            synchronized(this) {
                if (TESTING_TYPE.equals(n.getType())) {
                    sources.add((ObjectName) n.getSource());

                    if (sources.size() == waitNB) {
                        this.notifyAll();
                    }
                }
            }
        }

        public boolean waiting(long timeout) {
            final long startTime = System.currentTimeMillis();
            long toWait = timeout;

            synchronized(this) {
                while(sources.size() < waitNB && toWait > 0) {
                    try {
                        this.wait(toWait);
                    } catch (InterruptedException ire) {
                        break;
                    }

                    toWait = timeout -
                        (System.currentTimeMillis() - startTime);
                }
            }

            return sources.size() == waitNB;
        }

        private final int waitNB;
    }

    static String thisFailure;
    static String failure;
    static int nextNameIndex;
    static final long MAX_WAITING_TIME = 10000;

    private static MyListener countListener;
    private static final List<ObjectName> sources = new Vector();

    private static final String TESTING_TYPE = "testing_type";
}