8029281: Synchronization issues in Logger and LogManager
Summary: Fixes several race conditions in logging which have been at the root cause of intermittent test failures.
Reviewed-by: mchung, plevart
diff --git a/test/java/util/logging/TestLoggerBundleSync.java b/test/java/util/logging/TestLoggerBundleSync.java
new file mode 100644
index 0000000..bbac23c
--- /dev/null
+++ b/test/java/util/logging/TestLoggerBundleSync.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.security.Permission;
+import java.security.Policy;
+import java.security.ProtectionDomain;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListResourceBundle;
+import java.util.Objects;
+import java.util.ResourceBundle;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * @test
+ * @bug 8029281 8028763
+ * @summary Attempts to detect synchronization issues with getResourceBundle()
+ *          and getResourceBundleName(). It might also detect issues in the way
+ *          that the logger tree is cleaned up after a logger has been garbage
+ *          collected. This test helped find the root cause of 8029092, so if
+ *          this test fails one might also expect failures in
+ *          java/util/logging/Logger/logrb/TestLogrbResourceBundle.java and
+ *          java/util/logging/Logger/setResourceBundle/TestSetResourceBundle.java.
+ *          Note that this is a best effort test. Running it in a loop to
+ *          reproduce intermittent issues can be a good idea.
+ * @run main/othervm TestLoggerBundleSync
+ * @author danielfuchs
+ */
+public class TestLoggerBundleSync {
+
+    static volatile Exception thrown = null;
+    static volatile boolean goOn = true;
+
+    static final int READERS = 3;
+    static final long TIME = 4 * 1000; // 4 sec.
+    static final long STEP = 1 * 1000;  // message every 1 sec.
+    static final int  LCOUNT = 50; // change bundle 50 times...
+    static final AtomicLong setRBcount = new AtomicLong(0);
+    static final AtomicLong setRBNameCount = new AtomicLong(0);
+    static final AtomicLong getRBcount = new AtomicLong(0);
+    static final AtomicLong checkCount = new AtomicLong(0);
+    static final AtomicLong nextLong = new AtomicLong(0);
+
+    public static class MyBundle extends ListResourceBundle {
+        @Override
+        protected Object[][] getContents() {
+            return new Object[][] {
+               {"dummy", "foo"}
+            };
+        }
+    }
+
+    public static final class MyBundle1 extends MyBundle { };
+    public static final class MyBundle2 extends MyBundle { };
+    public static final class MyBundle3 extends MyBundle { };
+
+
+    public static final class LoggerRB {
+        public final String resourceBundleName;
+        public final ResourceBundle userBundle;
+        public LoggerRB(String name, ResourceBundle bundle) {
+            resourceBundleName = name;
+            userBundle = bundle;
+        }
+    }
+
+    static final List<Class<? extends ResourceBundle>> classes = new ArrayList<>();
+    static {
+        classes.add(MyBundle1.class);
+        classes.add(MyBundle2.class);
+        classes.add(MyBundle3.class);
+    }
+
+
+    /**
+     * This test will run both with and without a security manager.
+     *
+     * The test starts a number of threads that will attempt to concurrently
+     * set resource bundles on Logger, and verifies the consistency of the
+     * obtained results.
+     *
+     * This is a best effort test.
+     *
+     * @param args the command line arguments
+     */
+    public static void main(String[] args) throws Exception {
+
+        try {
+            // test without security
+            System.out.println("No security");
+            test();
+
+            // test with security
+            System.out.println("\nWith security");
+            Policy.setPolicy(new Policy() {
+                @Override
+                public boolean implies(ProtectionDomain domain, Permission permission) {
+                    if (super.implies(domain, permission)) return true;
+                    // System.out.println("Granting " + permission);
+                    return true; // all permissions
+                }
+            });
+            System.setSecurityManager(new SecurityManager());
+            test();
+        } finally {
+            SetRB.executor.shutdownNow();
+            SetRBName.executor.shutdownNow();
+        }
+    }
+
+    /**
+     * Starts all threads, wait 15secs, then stops all threads.
+     * @throws Exception if a deadlock was detected or an error occurred.
+     */
+    public static void test() throws Exception {
+          goOn = true;
+          thrown = null;
+          long sGetRBCount = getRBcount.get();
+          long sSetRBCount  = setRBcount.get();
+          long sSetRBNameCount  = setRBNameCount.get();
+          long sCheckCount = checkCount.get();
+          long sNextLong = nextLong.get();
+          List<Thread> threads = new ArrayList<>();
+          for (Class<? extends ResourceBundle> type : classes) {
+              threads.add(new SetRB(type));
+              threads.add(new SetRBName(type));
+          }
+          for (int i =0 ; i < READERS ; i++) {
+              threads.add(new GetRB());
+          }
+          threads.add(new DeadlockDetector());
+          threads.add(0, new Stopper(TIME));
+          for (Thread t : threads) {
+              t.start();
+          }
+          for (Thread t : threads) {
+              try {
+                  t.join();
+              } catch (Exception x) {
+                  fail(x);
+              }
+          }
+          if (thrown != null) {
+              throw thrown;
+          }
+          System.out.println("Passed: " + (nextLong.longValue() - sNextLong)
+                  + " unique loggers created");
+          System.out.println("\t " +(getRBcount.get() - sGetRBCount)
+                  + " loggers tested by " + READERS + " Thread(s),");
+          System.out.println("\t " + (setRBcount.get() - sSetRBCount)
+                  + " resource bundles set by " + classes.size() + " Thread(s),");
+          System.out.println("\t " + (setRBNameCount.get() - sSetRBNameCount)
+                  + " resource bundle names set by " + classes.size() + " Thread(s),");
+          System.out.println("\t ThreadMXBean.findDeadlockedThreads called "
+                  + (checkCount.get() -sCheckCount) + " times by 1 Thread.");
+
+    }
+
+    final static class GetRB extends Thread {
+        final static class MyHandler extends Handler {
+            volatile ResourceBundle rb;
+            volatile String rbName;
+            @Override
+            public synchronized void publish(LogRecord record) {
+                rb = record.getResourceBundle();
+                rbName = record.getResourceBundleName();
+            }
+
+            @Override
+            public void flush() {
+            }
+
+            @Override
+            public void close() throws SecurityException {
+            }
+        };
+        final MyHandler handler = new MyHandler();
+        @Override
+        public void run() {
+            try {
+                handler.setLevel(Level.FINEST);
+                while (goOn) {
+                    Logger l;
+                    Logger foo = Logger.getLogger("foo");
+                    Logger bar = Logger.getLogger("foo.bar");
+                    for (long i=0; i < nextLong.longValue() + 100 ; i++) {
+                        if (!goOn) break;
+                        l = Logger.getLogger("foo.bar.l"+i);
+                        final ResourceBundle b = l.getResourceBundle();
+                        final String name = l.getResourceBundleName();
+                        if (b != null) {
+                            if (!name.equals(b.getBaseBundleName())) {
+                                throw new RuntimeException("Unexpected bundle name: "
+                                        +b.getBaseBundleName());
+                            }
+                        }
+                        Logger ll = Logger.getLogger(l.getName()+".bie.bye");
+                        ResourceBundle hrb;
+                        String hrbName;
+                        ll.setLevel(Level.FINEST);
+                        ll.addHandler(handler);
+                        ll.fine("dummy");
+                        ll.removeHandler(handler);
+                        hrb = handler.rb;
+                        hrbName = handler.rbName;
+                        if (name != null) {
+                            if (!name.equals(hrbName)) {
+                                throw new RuntimeException("Unexpected bundle name: "
+                                        +hrb.getBaseBundleName());
+                            }
+                            if (!name.equals(hrb.getBaseBundleName())) {
+                                throw new RuntimeException("Unexpected bundle name: "
+                                        +hrb.getBaseBundleName());
+                            }
+                        }
+
+                        getRBcount.incrementAndGet();
+                        if (!goOn) break;
+                        Thread.sleep(1);
+                    }
+                }
+           } catch (Exception x) {
+               fail(x);
+           }
+        }
+    }
+
+    final static class SetRB extends Thread {
+        final Class<? extends ResourceBundle> type;
+        final static ExecutorService executor = Executors.newSingleThreadExecutor();
+        final static class CheckRBTask implements Callable<Exception> {
+            final Logger logger;
+            volatile String rbName;
+            volatile ResourceBundle rb;
+
+            public CheckRBTask(Logger logger) {
+                this.logger = logger;
+            }
+
+            @Override
+            public Exception call() throws Exception {
+                try {
+                    final String name = logger.getResourceBundleName();
+                    if (!Objects.equals(name, rbName)) {
+                        throw new RuntimeException("Unexpected rbname for "
+                            + logger.getName() + ": " + name);
+                    }
+                    final ResourceBundle b = logger.getResourceBundle();
+                    if (b != rb) {
+                        throw new RuntimeException("Unexpected rb for "
+                            + logger.getName() + ": " + b);
+                    }
+                } catch(Exception x) {
+                    return x;
+                }
+                return null;
+            }
+
+            public void check() throws Exception {
+                final FutureTask<Exception> futureTask = new FutureTask<>(this);
+                executor.submit(futureTask);
+                Exception x = futureTask.get();
+                if ( x != null) {
+                    throw new RuntimeException("Check failed: "+x,x);
+                }
+            }
+        }
+        SetRB(Class<? extends ResourceBundle> type) {
+            super("SetRB["+type.getSimpleName()+"]");
+            this.type = type;
+        }
+        @Override
+        public void run() {
+            try {
+                while (goOn) {
+                    Logger l;
+                    Logger foo = Logger.getLogger("foo");
+                    Logger bar = Logger.getLogger("foo.bar");
+                    l = Logger.getLogger("foo.bar.l"+nextLong.incrementAndGet());
+                    final CheckRBTask checkTask = new CheckRBTask(l);
+                    checkTask.check();
+
+                    for (int i=0; i < LCOUNT ; i++) {
+                        if (!goOn) break;
+
+                        ResourceBundle b = ResourceBundle.getBundle(type.getName());
+                        try {
+                            l.setResourceBundle(b);
+                            checkTask.rb = b;
+                            checkTask.rbName = type.getName();
+                            checkTask.check();
+                            if (!goOn) break;
+
+                            String name = l.getResourceBundleName();
+                            ResourceBundle bb = l.getResourceBundle();
+                            if (!type.getName().equals(name)) {
+                                throw new RuntimeException(this.getName()
+                                        + ": Unexpected name: "+name);
+                            }
+                            if (!b.getBaseBundleName().equals(name)) {
+                                throw new RuntimeException(this.getName()
+                                        + ": Unexpected base name: " +
+                                        b.getBaseBundleName());
+                            }
+                            if (b != bb) {
+                                throw new RuntimeException(this.getName()
+                                        + ": Unexpected bundle: "+bb);
+                            }
+                            setRBcount.incrementAndGet();
+                        } catch (IllegalArgumentException x) {
+                            final String name = l.getResourceBundleName();
+                            if (!name.startsWith(MyBundle.class.getName())) {
+                                throw new RuntimeException(this.getName()
+                                        + ": Unexpected name: "+name, x);
+                            } else if (type.getName().equals(name)) {
+                                throw new RuntimeException(this.getName()
+                                        + ": Unexpected exception for "+name, x);
+                            }
+                            throw x;
+                        }
+                        l.fine("I'm fine");
+                        if (!goOn) break;
+                        Thread.sleep(1);
+                    }
+                }
+            } catch (Exception x) {
+                fail(x);
+            }
+        }
+    }
+
+    final static class SetRBName extends Thread {
+        int nexti = 0;
+        final Class<? extends ResourceBundle> type;
+        final static ExecutorService executor = Executors.newSingleThreadExecutor();
+        final static class CheckRBNameTask implements Callable<Exception> {
+            final Logger logger;
+            volatile String rbName;
+
+            public CheckRBNameTask(Logger logger) {
+                this.logger = logger;
+            }
+
+            @Override
+            public Exception call() throws Exception {
+                try {
+                    final String name = logger.getResourceBundleName();
+                    if (!Objects.equals(name, rbName)) {
+                        throw new RuntimeException("Unexpected rbname for "
+                            + logger.getName() + ": " + name);
+                    }
+                    final ResourceBundle b = logger.getResourceBundle();
+                    if (!Objects.equals(b == null ? null : b.getBaseBundleName(), rbName)) {
+                        throw new RuntimeException("Unexpected base name for "
+                            + logger.getName() + ": " + b.getBaseBundleName());
+                    }
+                } catch(Exception x) {
+                    return x;
+                }
+                return null;
+            }
+
+            public void check() throws Exception {
+                final FutureTask<Exception> futureTask = new FutureTask<>(this);
+                executor.submit(futureTask);
+                Exception x = futureTask.get();
+                if ( x != null) {
+                    throw new RuntimeException("Check failed: "+x,x);
+                }
+            }
+
+        }
+        SetRBName(Class<? extends ResourceBundle> type) {
+            super("SetRB["+type.getSimpleName()+"]");
+            this.type = type;
+        }
+        @Override
+        public void run() {
+            try {
+                while (goOn) {
+                    Logger foo = Logger.getLogger("foo");
+                    Logger bar = Logger.getLogger("foo.bar");
+                    Logger l = Logger.getLogger("foo.bar.l"+nextLong.incrementAndGet());
+                    final CheckRBNameTask checkTask = new CheckRBNameTask(l);
+                    checkTask.check();
+
+                    for (int i=0; i < LCOUNT ; i++) {
+                        if (!goOn) break;
+
+                        try {
+                            Logger l2 = Logger.getLogger(l.getName(), type.getName());
+                            if (l2 != l) {
+                                System.err.println("**** ERROR WITH "+l.getName());
+                                throw new RuntimeException("l2 != l ["
+                                        + l2 + "(" + l2.getName() + ") != "
+                                        + l  + "(" + l.getName()  + ")]");
+                            }
+                            checkTask.rbName = type.getName();
+                            checkTask.check();
+                            if (!goOn) break;
+
+                            String name = l.getResourceBundleName();
+                            ResourceBundle bb = l.getResourceBundle();
+                            if (!type.getName().equals(name)) {
+                                throw new RuntimeException(this.getName()
+                                        + ": Unexpected name: "+name);
+                            }
+                            if (!bb.getBaseBundleName().equals(name)) {
+                                throw new RuntimeException(this.getName()
+                                        + ": Unexpected base name: "
+                                        + bb.getBaseBundleName());
+                            }
+                            setRBNameCount.incrementAndGet();
+                        } catch (IllegalArgumentException x) {
+                            final String name = l.getResourceBundleName();
+                            if (!name.startsWith(MyBundle.class.getName())) {
+                                throw new RuntimeException(this.getName()
+                                        + ": Unexpected name: "+name, x);
+                            } else if (type.getName().equals(name)) {
+                                throw new RuntimeException(this.getName()
+                                        + ": Unexpected exception for "+name, x);
+                            }
+                            throw x;
+                        }
+                        l.fine("I'm fine");
+                        if (!goOn) break;
+                        Thread.sleep(1);
+                    }
+                }
+            } catch (Exception x) {
+                fail(x);
+            }
+        }
+    }
+
+    final static class DeadlockDetector extends Thread {
+
+        @Override
+        public void run() {
+            while(goOn) {
+                try {
+                    long[] ids = ManagementFactory.getThreadMXBean().findDeadlockedThreads();
+                    checkCount.incrementAndGet();
+                    ids = ids == null ? new long[0] : ids;
+                    if (ids.length == 1) {
+                        throw new RuntimeException("Found 1 deadlocked thread: "+ids[0]);
+                    } else if (ids.length > 0) {
+                        ThreadInfo[] infos = ManagementFactory.getThreadMXBean().getThreadInfo(ids);
+                        System.err.println("Found "+ids.length+" deadlocked threads: ");
+                        for (ThreadInfo inf : infos) {
+                            System.err.println(inf.toString());
+                        }
+                        throw new RuntimeException("Found "+ids.length+" deadlocked threads");
+                    }
+                    Thread.sleep(100);
+                } catch(InterruptedException | RuntimeException x) {
+                    fail(x);
+                }
+            }
+        }
+
+    }
+
+    static final class Stopper extends Thread {
+        long start;
+        long time;
+
+        Stopper(long time) {
+            start = System.currentTimeMillis();
+            this.time = time;
+        }
+
+        @Override
+        public void run() {
+            try {
+                long rest, previous;
+                previous = time;
+                while (goOn && (rest = start - System.currentTimeMillis() + time) > 0) {
+                    if (previous == time || previous - rest >= STEP) {
+                        Logger.getLogger("remaining").info(String.valueOf(rest)+"ms remaining...");
+                        previous = rest == time ? rest -1 : rest;
+                        System.gc();
+                    }
+                    if (goOn == false) break;
+                    Thread.sleep(Math.min(rest, 100));
+                }
+                System.out.println(System.currentTimeMillis() - start
+                        + " ms elapsed ("+time+ " requested)");
+                goOn = false;
+            } catch(InterruptedException | RuntimeException x) {
+                fail(x);
+            }
+        }
+
+    }
+
+    static void fail(Exception x) {
+        x.printStackTrace();
+        if (thrown == null) {
+            thrown = x;
+        }
+        goOn = false;
+    }
+}