From 4d4e51c41fed79427fb621fd9fcc8e5e23bfb287 Mon Sep 17 00:00:00 2001 From: David Beaumont Date: Wed, 10 Sep 2025 11:49:02 +0000 Subject: [PATCH] 8365483: Test sun/rmi/runtime/Log/6409194/NoConsoleOutput.java sometimes fails Reviewed-by: dfuchs, jpai --- .../java/util/logging/StreamHandler.java | 13 +- .../logging/StreamHandlerRacyCloseTest.java | 137 ++++++++++++++++++ 2 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 test/jdk/java/util/logging/StreamHandlerRacyCloseTest.java diff --git a/src/java.logging/share/classes/java/util/logging/StreamHandler.java b/src/java.logging/share/classes/java/util/logging/StreamHandler.java index ddfea9674dc..3b9af54814d 100644 --- a/src/java.logging/share/classes/java/util/logging/StreamHandler.java +++ b/src/java.logging/share/classes/java/util/logging/StreamHandler.java @@ -214,13 +214,16 @@ public class StreamHandler extends Handler { try { synchronized (this) { + // Re-check writer between isLoggable() and here. Writer writer = this.writer; - if (!doneHeader) { - writer.write(formatter.getHead(this)); - doneHeader = true; + if (writer != null) { + if (!doneHeader) { + writer.write(formatter.getHead(this)); + doneHeader = true; + } + writer.write(msg); + synchronousPostWriteHook(); } - writer.write(msg); - synchronousPostWriteHook(); } } catch (Exception ex) { // We don't want to throw an exception here, but we diff --git a/test/jdk/java/util/logging/StreamHandlerRacyCloseTest.java b/test/jdk/java/util/logging/StreamHandlerRacyCloseTest.java new file mode 100644 index 00000000000..5bb465088e5 --- /dev/null +++ b/test/jdk/java/util/logging/StreamHandlerRacyCloseTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025, 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.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.ErrorManager; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.SimpleFormatter; +import java.util.logging.StreamHandler; + +import org.junit.jupiter.api.Test; + +/* + * @test + * @bug 8365483 + * @summary verify that concurrent calls to publish() and close() on a + * StreamHandler do not cause unexpected exceptions + * @run junit StreamHandlerRacyCloseTest + */ +public class StreamHandlerRacyCloseTest { + + private static final class ExceptionTrackingErrorManager extends ErrorManager { + private final AtomicReference firstError = new AtomicReference<>(); + + @Override + public void error(String msg, Exception ex, int code) { + // just track one/first exception, that's good enough for this test + this.firstError.compareAndSet(null, new RuntimeException(msg, ex)); + } + } + + @Test + void testRacyClose() throws Exception { + final int numTimes = 100; + try (ExecutorService executor = Executors.newFixedThreadPool(numTimes)) { + final List> tasks = new ArrayList<>(); + for (int i = 1; i <= numTimes; i++) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // construct a StreamHandler with an ErrorManager which propagates + // any errors that happen during publish() + final StreamHandler handler = new StreamHandler(baos, new SimpleFormatter()); + handler.setErrorManager(new ExceptionTrackingErrorManager()); + + final CountDownLatch latch = new CountDownLatch(2); + // create a publisher and closer task which will run concurrently + tasks.add(new Publisher(handler, latch)); + tasks.add(new Closer(handler, latch)); + } + // submit the tasks and expect successful completion of each + final List> completed = executor.invokeAll(tasks); + for (var f : completed) { + f.get(); + } + } + } + + private static final class Closer implements Callable { + private final StreamHandler handler; + private final CountDownLatch startLatch; + + private Closer(final StreamHandler handler, final CountDownLatch startLatch) { + this.handler = handler; + this.startLatch = startLatch; + } + + @Override + public Void call() throws Exception { + // notify the other task of our readiness + this.startLatch.countDown(); + // wait for the other task to arrive + this.startLatch.await(); + // close the handler + this.handler.close(); + // propagate any exception that may have been caught by the error manager + final var errMgr = (ExceptionTrackingErrorManager) this.handler.getErrorManager(); + if (errMgr.firstError.get() != null) { + throw errMgr.firstError.get(); + } + return null; + } + } + + private static final class Publisher implements Callable { + private final StreamHandler handler; + private final CountDownLatch startLatch; + + private Publisher(final StreamHandler handler, final CountDownLatch startLatch) { + this.handler = handler; + this.startLatch = startLatch; + } + + @Override + public Void call() throws Exception { + final LogRecord record = new LogRecord(Level.WARNING, "hello world"); + // notify the other task of our readiness + this.startLatch.countDown(); + // wait for the other task to arrive + this.startLatch.await(); + // publish the record + this.handler.publish(record); + // propagate any exception that may have been caught by the error manager + final var errMgr = (ExceptionTrackingErrorManager) this.handler.getErrorManager(); + if (errMgr.firstError.get() != null) { + throw errMgr.firstError.get(); + } + return null; + } + } +}