| // Copyright 2021 The Bazel Authors. All rights reserved. | |
| // | |
| // 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 com.google.devtools.build.lib.util; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import com.google.common.util.concurrent.SettableFuture; | |
| import com.google.devtools.build.lib.bugreport.BugReporter; | |
| import com.google.devtools.build.lib.bugreport.Crash; | |
| import com.google.devtools.build.lib.bugreport.CrashContext; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.concurrent.CountDownLatch; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests for {@link ThreadUtils}. */ | |
| @RunWith(JUnit4.class) | |
| public class ThreadUtilsTest { | |
| // TODO(b/150299871): inspecting the output of GoogleLogger or mocking it seems too hard for now. | |
| @Test | |
| public void smoke() throws Exception { | |
| SettableFuture<Integer> future = SettableFuture.create(); | |
| int numParkThreads = 11; | |
| CountDownLatch waitForThreads = new CountDownLatch(numParkThreads + 2); | |
| List<Thread> parkThreads = new ArrayList<>(numParkThreads); | |
| for (int i = 0; i < numParkThreads; i++) { | |
| parkThreads.add( | |
| new Thread(() -> recursiveMethodPark(0, future, waitForThreads), "parkthread" + i)); | |
| } | |
| Runnable noParkRunnable = () -> recursiveMethodNoPark(0, waitForThreads); | |
| Thread noParkThread = new Thread(noParkRunnable, "noparkthread1"); | |
| Thread noParkThread2 = new Thread(noParkRunnable, "noparkthread2"); | |
| AtomicReference<Throwable> reportedException = new AtomicReference<>(); | |
| BugReporter bugReporter = | |
| new BugReporter() { | |
| @Override | |
| public void sendBugReport(Throwable exception, List<String> args, String... values) { | |
| assertThat(reportedException.get()).isNull(); | |
| reportedException.set(exception); | |
| } | |
| @Override | |
| public void sendNonFatalBugReport(Throwable exception) { | |
| throw new UnsupportedOperationException(); | |
| } | |
| @Override | |
| public void handleCrash(Crash crash, CrashContext ctx) { | |
| BugReporter.defaultInstance().handleCrash(crash, ctx); | |
| } | |
| }; | |
| parkThreads.forEach(Thread::start); | |
| noParkThread.start(); | |
| noParkThread2.start(); | |
| waitForThreads.await(); | |
| ThreadUtils.warnAboutSlowInterrupt("interrupt message", bugReporter); | |
| assertThat(reportedException.get()) | |
| .hasCauseThat() | |
| .hasMessageThat() | |
| .isEqualTo("(Wrapper exception for longest stack trace) interrupt message"); | |
| // The topmost method is either "sleep" or "sleep0" or "sleepNanos0". For example, in JDK 21, | |
| // "Thread.sleep" calls "sleepNanos" which then calls a "sleepNanos0" native method. | |
| StackTraceElement[] stackTrace = reportedException.get().getCause().getStackTrace(); | |
| if (stackTrace[0].getMethodName().equals("sleepNanos0")) { | |
| assertThat(stackTrace[1].getMethodName()).isEqualTo("sleepNanos"); | |
| assertThat(stackTrace[2].getMethodName()).isEqualTo("sleep"); | |
| assertThat(stackTrace[3].getMethodName()).isEqualTo("recursiveMethodNoPark"); | |
| } else if (stackTrace[0].getMethodName().equals("sleep0")) { | |
| assertThat(stackTrace[1].getMethodName()).isEqualTo("sleep"); | |
| assertThat(stackTrace[2].getMethodName()).isEqualTo("recursiveMethodNoPark"); | |
| } else { | |
| assertThat(stackTrace[0].getMethodName()).isEqualTo("sleep"); | |
| assertThat(stackTrace[1].getMethodName()).isEqualTo("recursiveMethodNoPark"); | |
| } | |
| future.set(1); | |
| for (Thread thread : parkThreads) { | |
| thread.join(); | |
| } | |
| noParkThread.interrupt(); | |
| noParkThread.join(); | |
| noParkThread2.interrupt(); | |
| noParkThread2.join(); | |
| } | |
| private static void recursiveMethodPark( | |
| int depth, SettableFuture<Integer> future, CountDownLatch waitForThreads) { | |
| if (depth < 100) { | |
| recursiveMethodPark(depth + 1, future, waitForThreads); | |
| return; | |
| } | |
| waitForThreads.countDown(); | |
| try { | |
| future.get(); | |
| } catch (InterruptedException | ExecutionException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| private static void recursiveMethodNoPark(int depth, CountDownLatch waitForThreads) { | |
| if (depth < 50) { | |
| recursiveMethodNoPark(depth + 1, waitForThreads); | |
| return; | |
| } | |
| waitForThreads.countDown(); | |
| try { | |
| Thread.sleep(Long.MAX_VALUE); | |
| } catch (InterruptedException e) { | |
| // Ignored. | |
| } | |
| } | |
| } |